diff -Naur drupal-7.0/.editorconfig drupal-7.66/.editorconfig --- drupal-7.0/.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.0/.gitignore drupal-7.66/.gitignore --- drupal-7.0/.gitignore 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/.gitignore 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,6 @@ +# Ignore configuration files that may contain sensitive information. +sites/*/settings*.php + +# Ignore paths that contain user-generated content. +sites/*/files +sites/*/private diff -Naur drupal-7.0/.htaccess drupal-7.66/.htaccess --- drupal-7.0/.htaccess 2010-11-23 03:59:05.000000000 +0100 +++ 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. @@ -13,23 +18,14 @@ # Follow symbolic links in this directory. Options +FollowSymLinks -# Multiviews creates problems with aliased URLs and is not needed for Drupal. -Options -Multiviews - # Make Drupal handle any 404 errors. ErrorDocument 404 /index.php -# Force simple error message for requests for non-existent favicon.ico. - - # There is no end quote below, for compatibility with Apache 1.3. - ErrorDocument 404 "The requested file favicon.ico was not found. - - # Set the default handler. DirectoryIndex index.php index.html index.htm # Override PHP settings that cannot be changed at runtime. See -# sites/default/default.settings.php and drupal_initialize_variables() in +# sites/default/default.settings.php and drupal_environment_initialize() in # includes/bootstrap.inc for settings that can be changed at runtime. # PHP 5, Apache 1 and 2. @@ -65,6 +61,17 @@ RewriteEngine on + # Set "protossl" to "s" if we were accessed via https://. This is used later + # if you enable "www." stripping or enforcement, in order to ensure that + # you don't bounce between http and https. + RewriteRule ^ - [E=protossl] + RewriteCond %{HTTPS} on + RewriteRule ^ - [E=protossl:s] + + # Make sure Authorization HTTP header is available to PHP + # even when running as CGI or FastCGI. + RewriteRule ^ - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + # Block access to "hidden" directories whose names begin with a period. This # includes directories used by version control systems such as Subversion or # Git to store control files. Files whose names begin with a period, as well @@ -78,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 @@ -87,14 +94,15 @@ # To redirect all users to access the site WITH the 'www.' prefix, # (http://example.com/... will be redirected to http://www.example.com/...) # uncomment the following: + # RewriteCond %{HTTP_HOST} . # RewriteCond %{HTTP_HOST} !^www\. [NC] - # RewriteRule ^ http://www.%{HTTP_HOST}%{REQUEST_URI} [L,R=301] + # RewriteRule ^ http%{ENV:protossl}://www.%{HTTP_HOST}%{REQUEST_URI} [L,R=301] # # To redirect all users to access the site WITHOUT the 'www.' prefix, # (http://www.example.com/... will be redirected to http://example.com/...) # uncomment the following: # RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC] - # RewriteRule ^ http://%1%{REQUEST_URI} [L,R=301] + # RewriteRule ^ http%{ENV:protossl}://%1%{REQUEST_URI} [L,R=301] # Modify the RewriteBase if you are using Drupal in a subdirectory or in a # VirtualDocumentRoot and the rewrite rules are not working properly. @@ -132,11 +140,15 @@ # Serve correct encoding type. - Header append Content-Encoding gzip + Header set Content-Encoding gzip # Force proxies to cache gzipped & non-gzipped css/js files separately. Header append Vary Accept-Encoding -# $Id: .htaccess,v 1.111 2010/11/23 02:59:05 dries Exp $ +# 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.0/CHANGELOG.txt drupal-7.66/CHANGELOG.txt --- drupal-7.0/CHANGELOG.txt 2011-01-05 07:17:58.000000000 +0100 +++ drupal-7.66/CHANGELOG.txt 2019-04-17 22:20:46.000000000 +0200 @@ -1,6 +1,1085 @@ -// $Id: CHANGELOG.txt,v 1.396 2011/01/05 06:17:58 webchick Exp $ +Drupal 7.xx, xxxx-xx-xx (development version) +----------------------- -Drupal 7.0, 2011-01-05 +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
+-----------------------
+- Fixed a fatal error on PostgreSQL databases when updating the Taxonomy module
+  from Drupal 6 to Drupal 7.
+- Fixed the default ordering of CSS files for sites using right-to-left
+  languages, to consistently place the right-to-left override file immediately
+  after the CSS it is overriding (API change: https://drupal.org/node/2058463).
+- Added a drupal_check_memory_limit() API function to allow the memory limit to
+  be checked consistently (API addition).
+- Changed the default web.config file for IIS servers to allow favicon.ico
+  files which are present in the filesystem to be accessed.
+- Fixed inconsistent support for the 'tel' protocol in Drupal's URL filtering
+  functions.
+- Performance improvement: Allowed all hooks to be included in the
+  module_implements() cache, even those that are only invoked on HTTP POST
+  requests.
+- Made the database system replace truncate queries with delete queries when
+  inside a transaction, to fix issues with PostgreSQL and other databases.
+- Fixed a bug which caused nested contextual links to display improperly.
+- Fixed a bug which prevented cached image derivatives from being flushed for
+  private files and other non-default file schemes.
+- Fixed drupal_render() to always return an empty string when there is no
+  output, rather than sometimes returning NULL (minor API change).
+- Added protection to cache_clear_all() to ensure that non-cache tables cannot
+  be truncated (API addition: a new isValidBin() method has been added to the
+  default database cache implementation).
+- Changed the default .htaccess file to support HTTP authorization in CGI
+  environments.
+- Changed the password reset form to pre-fill the username when requested via a
+  URL query parameter, and used this in the error message that appears after a
+  failed login attempt (minor data structure and behavior change).
+- Fixed broken support for foreign keys in the field API.
+- Fixed "No active batch" error when a user cancels their own account.
+- Added a description to the "access content overview" permission on the
+  permissions page (string change).
+- Added a drupal_array_diff_assoc_recursive() function to allow associative
+  arrays to be compared recursively (API addition).
+- Added human-readable labels to image styles, in addition to the existing
+  machine-readable name (API change: https://drupal.org/node/2058503).
+- Moved the drupal_get_hash_salt() function to bootstrap.inc and used it in
+  additional places in the code, for added security in the case where there is
+  no hash salt in settings.php.
+- Fixed a regression in Drupal 7.22 that caused internal server errors for
+  sites running on very old Apache 1.x web servers.
+- Numerous small bug fixes.
+- Numerous API documentation improvements.
+- Additional automated test coverage.
+
+Drupal 7.22, 2013-04-03
+-----------------------
+- Allowed the drupal_http_request() function to be overridden so that
+  additional HTTP request capabilities can be added by contributed modules.
+- Changed the Simpletest module to allow PSR-0 test classes to be used in
+  Drupal 7.
+- Removed an unnecessary "Content-Disposition" header from private file
+  downloads; it prevented many private files from being viewed inline in a web
+  browser.
+- Changed various field API functions to allow them to optionally act on a
+  single field within an entity (API addition: http://drupal.org/node/1825844).
+- Fixed a bug which prevented Drupal's file transfer functionality from working
+  on some PHP 5.4 systems.
+- Fixed incorrect log message when theme() is called for a theme hook that does
+  not exist (minor string change).
+- Fixed Drupal's token-replacement system to allow spaces in the token value.
+- Changed the default behavior after a user creates a node they do not have
+  access to view. The user will now be redirected to the front page rather than
+  an access denied page.
+- Fixed a bug which prevented empty HTTP headers (such as "0") from being set.
+  (Minor behavior change: Callers of drupal_add_http_header() must now set
+  FALSE explicitly to prevent a header from being sent at all; this was already
+  indicated in the function's documentation.)
+- Fixed OpenID errors when more than one module implements hook_openid(). The
+  behavior is now changed so that if more than one module tries to set the same
+  parameter, the last module's change takes effect.
+- Fixed a serious documentation bug: The $name variable in the
+  taxonomy-term.tpl.php theme template was incorrectly documented as being
+  sanitized when in fact it is not.
+- Fixed a bug which prevented Drupal 6 to Drupal 7 upgrades on sites which had
+  duplicate permission names in the User module's database tables.
+- Added an empty "datatype" attribute to taxonomy term and username links to
+  make the RDFa markup upward compatible with RDFa 1.1 (minor markup addition).
+- Fixed a bug which caused the denial-of-service protection added in Drupal
+  7.20 to break certain valid image URLs that had an extra slash in them.
+- Fixed a bug with update queries in the SQLite database driver that prevented
+  Drupal from being installed with SQLite on PHP 5.4.
+- Fixed enforced dependencies errors updating to recent versions of Drupal 7 on
+  certain non-MySQL databases.
+- Refactored the Field module's caching behavior to obtain large improvements
+  in memory usage for sites with many fields and instances (API addition:
+  http://drupal.org/node/1915646).
+- Fixed entity argument not being passed to implementations of
+  hook_file_download_access_alter(). The fix adds an additional context
+  parameter that can be passed when calling drupal_alter() for any hook (API
+  change: http://drupal.org/node/1882722).
+- Fixed broken support for translatable comment fields (API change:
+  http://drupal.org/node/1874724).
+- Added an assertThemeOutput() method to Simpletest to allow tests to check
+  that themed output matches an expected HTML string (API addition).
+- Added a link to "Install another module" after a module has been successfully
+  downloaded via the Update Manager (UI change).
+- Added an optional "exclusive" flag to installation profile .info files which
+  allows Drupal distributions to force a profile to be selected during
+  installation (API addition: http://drupal.org/node/1961012).
+- Fixed a bug which caused the database API to not properly close database
+  connections.
+- Added a link to the URL for running cron from outside the site to the Cron
+  settings page (UI change).
+- Fixed a bug which prevented image styles from being reverted on PHP 5.4.
+- Made the default .htaccess rules protocol sensitive to improve security for
+  sites which use HTTPS and redirect between "www" and non-"www" versions of
+  the page.
+- Numerous small bug fixes.
+- Numerous API documentation improvements.
+- Additional automated test coverage.
+
+Drupal 7.21, 2013-03-06
+-----------------------
+- Allowed sites using the 'image_allow_insecure_derivatives' variable to still
+  have partial protection from the security issues fixed in Drupal 7.20.
+
+Drupal 7.20, 2013-02-20
+-----------------------
+- Fixed security issues (denial of service). See SA-CORE-2013-002.
+
+Drupal 7.19, 2013-01-16
+-----------------------
+- Fixed security issues (multiple vulnerabilities). See SA-CORE-2013-001.
+
+Drupal 7.18, 2012-12-19
+-----------------------
+- Fixed security issues (multiple vulnerabilities). See SA-CORE-2012-004.
+
+Drupal 7.17, 2012-11-07
+-----------------------
+- Changed the default value of the '404_fast_html' variable to have a DOCTYPE
+  declaration.
+- Made it possible to use associative arrays for the 'items' variable in
+  theme_item_list().
+- Fixed a bug which prevented required form elements without a title from being
+  given an "error" class when the form fails validation.
+- Prevented duplicate HTML IDs from appearing when two forms are displayed on
+  the same page and one of them is submitted with invalid data (minor markup
+  change).
+- Fixed a bug which prevented Drupal 6 to Drupal 7 upgrades on sites which had
+  stale data in the Upload module's database tables.
+- Fixed a bug in the States API which prevented certain types of form elements
+  from being disabled when requested.
+- Allowed aggregator feed items with author names longer than 255 characters to
+  have a truncated version saved to the database (rather than causing a fatal
+  error).
+- Allowed aggregator feed items to have URLs longer than 255 characters
+  (schema change which results in several columns in the Aggregator module's
+  database tables changing from VARCHAR to TEXT fields).
+- Added hook_taxonomy_term_view() and standardized the process for rendering
+  taxonomy terms to invoke hook_entity_view() and otherwise make it consistent
+  with other entities (API change: http://drupal.org/node/1808870).
+- Added hook_entity_view_mode_alter() to allow modules to change entity view
+  modes on display (API addition: http://drupal.org/node/1833086).
+- Fixed a bug which made database queries running a "LIKE" query on blob fields
+  fail on PostgreSQL databases. This caused errors during the Drupal 6 to
+  Drupal 7 upgrade.
+- Changed the hook_menu() entry for Drupal's rss.xml page to prevent extra path
+  components from being accidentally passed to the page callback function (data
+  structure change).
+- Removed a non-standard "name" attribute from Drupal's default Content-Type
+  header for file downloads.
+- Fixed the theme settings form to properly clean up submitted values in
+  $form_state['values'] when the form is submitted (data structure change).
+- Fixed an inconsistency by removing the colon from the end of the label on
+  multi-valued form fields (minor string change).
+- Added support for 'weight' in hook_field_widget_info() to allow modules to
+  control the order in which widgets are displayed in the Field UI.
+- Updated various tables in the OpenID and Book modules to use the default
+  "empty table" text pattern (string change).
+- Added proxy server support to drupal_http_request().
+- Added "lang" attributes to language links, to better support screen readers.
+- Fixed double occurrence of a "ul" HTML tag on secondary local tasks in the
+  Seven theme (markup change).
+- Fixed bugs which caused taxonomy vocabulary and shortcut set titles to be
+  double-escaped. The fix replaces the taxonomy vocabulary overview page and
+  "Edit shortcuts" menu items' title callback entries in hook_menu() with new
+  functions that do not escape HTML characters (data structure change).
+- Modified the Update manager module to allow drupal.org to collect usage
+  statistics for individual modules and themes, rather than only for entire
+  projects.
+- Modified the node listing database query on Drupal's default front page to
+  add table aliases for better query altering (this is a data structure change
+  affecting code which implements hook_query_alter() on this query).
+- Improved the translatability of the "Field type(s) in use" message on the
+  modules page (admin-facing string change).
+- Fixed a regression which caused a "call to undefined function
+  drupal_find_base_themes()" fatal error under rare circumstances.
+- Numerous API documentation improvements.
+- Additional automated test coverage.
+
+Drupal 7.16, 2012-10-17
+-----------------------
+- Fixed security issues (Arbitrary PHP code execution and information
+  disclosure). See SA-CORE-2012-003.
+
+Drupal 7.15, 2012-08-01
+-----------------------
+- Introduced a 'user_password_reset_timeout' variable to allow the 24-hour
+  expiration for user password reset links to be adjusted (API addition).
+- Fixed database errors due to ambiguous column names that occurred when
+  EntityFieldQuery was used in certain situations.
+- Changed the drupal_array_get_nested_value() function to return a reference
+  (API addition).
+- Changed the System module's hook_block_info() implementation to assign the
+  "Main page content" and "System help" blocks to appropriate regions by
+  default and prevent error messages on the block administration page (data
+  structure change).
+- Fixed regression: Non-node entities couldn't be accessed with
+  EntityFieldQuery.
+- Fixed regression: Optional radio buttons with an empty, non-NULL default
+  value led to an illegal choice error when none were selected.
+- Reorganized the testing framework to split setUp() into specific sub-methods
+  and fix several regressions in the process.
+- Fixed bug which made it impossible to search for strings that have not been
+  translated into a particular language.
+- Renamed the "Field" column on the Manage Fields screen to "Field type", since
+  the former was confusing and inaccurate (UI change).
+- Performance improvement: Removed needless call to system_rebuild_module_data()
+  in field_sync_field_status(), greatly speeding up bulk module enable/disable.
+- Fixed bug which prevented notifications from being sent when core, module, and
+  theme updates are available.
+- Fixed bug which prevented sub-themes from inheriting the default values of
+  theme settings defined by the base theme.
+- Fixed bug which prevented the jQuery UI Datepicker from being localized.
+- Made Ajax alert dialogs respect error reporting settings.
+- Fixed bug which prevented image styles from being deleted on PHP 5.4.
+- Fixed bug: Language detection by domain only worked on port 80.
+- Fixed regression: The first plural index on a page was not calculated
+  correctly.
+- Introduced generic entity language support. Entities may now declare their
+  language property in hook_entity_info(), and modules working with entities
+  may access the language using entity_language() (API change:
+  http://drupal.org/node/1626346).
+- Added EntityFieldQuery support for taxonomy bundles.
+- Fixed issue where field form structure was incomplete if field_access()
+  returned FALSE. Instead of being incomplete, the form structure now has
+  #access set to FALSE and field form validation is skipped (data structure
+  change: http://drupal.org/node/1663020).
+- Fixed data loss issue due to field_has_data() returning inconsistent results.
+  The fix adds an optional DANGEROUS_ACCESS_CHECK_OPT_OUT tag to entity field
+  queries which field storage engines can respond to (API addition:
+  http://drupal.org/node/1597378).
+- Fixed notice: Undefined index: default_image in image_field_prepare_view()
+- Numerous API documentation improvements.
+- Additional automated test coverage.
+
+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.
+- Split field_bundle_settings out per bundle.
+- Improve UX for machine names for fields (UI change).
+- Fixed User pictures are not removed properly.
+- Fixed HTTPS sessions not working in all cases.
+- Fixed Regression: Required radios throw illegal choice error when none
+  selected.
+- Fixed allow autocompletion requests to include slashes.
+- Eliminate $user->cache and {session}.cache in favor of
+  $_SESSION['cache_expiration'][$bin] (Performance).
+- Fixed focus jumps to tab when pressing enter on a form element within tab.
+- Fixed race condition in locale() - duplicates in {locales_source}.
+- Fixed Missing "Default image" per field instance.
+- Quit clobbering people's work when they click the filter tips link
+- Form API #states: Fix conditionals to allow OR and XOR constructions.
+- Fixed Focus jumps to tab when pressing enter on a form element within tab.
+  (Accessibility)
+- Improved performance of node_access queries.
+- Fixed Fieldsets inside vertical tabs have no title and can't be collapsed.
+- Reduce size of cache_menu table (Performance).
+- Fixed unnecessary aggregation of CSS/JS (Performance).
+- Fixed taxonomy_autocomplete() produces SQL error for nonexistent field.
+- Fixed HTML filter is not run first by default, despite default weight.
+- Fixed Overlay does not work with prefixed URL paths.
+- Better debug info for field errors (string change).
+- Fixed Data corruption in comment IDs (results in broken threading on
+  PostgreSQL).
+- Fixed machine name not editable if every character is replaced.
+- Fixed user picture not appearing in comment preview (Markup change).
+- Added optional vid argument for taxonomy_get_term_by_name().
+- Fixed Invalid Unicode code range in PREG_CLASS_UNICODE_WORD_BOUNDARY fails
+  with PCRE 8.30.
+- Fixed {trigger_assignments()}.hook has only 32 characters, is too short.
+- Numerous fixes to run-tests.sh.
+- Fixed Tests in profiles/[name]/modules cannot be run and cannot use a
+  different profile for running tests.
+- Numerous JavaScript performance fixes.
+- Numerous documentation fixes.
+- Fixed All pager links have an 'active' CSS class.
+- Numerous upgrade path fixes; notably:
+  - system_update_7061() fails on inserting files with same name but different
+    case.
+  - 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
+-----------------------
+- 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.
+- Fixed file_unmanaged_copy() fails with Drupal 7.7+ and safe_mode() or
+  open_basedir().
+- Fixed Nested transactions throw exceptions when they got out of scope.
+- Fixed bugs with the Return-Path when sending mail on both Windows and
+  non-Windows systems.
+- Fixed bug with DrupalCacheArray property visibility preventing others from
+  extending it (API change: http://drupal.org/node/1422264).
+- Fixed bug with handling of non-ASCII characters in file names (API change:
+  http://drupal.org/node/1424840).
+- Reconciled field maximum length with database column size in image and
+  aggregator modules.
+- Fixes to various core JavaScript files to allow for minification and
+  aggregation.
+- Fixed Prevent tests from deleting main installation's tables when
+  parent::setUp() is not called.
+- Fixed several Poll module bugs.
+- Fixed several Shortcut module bugs.
+- Added new hook_system_theme_info() to provide ability for contributed modules
+  to test theme functionality.
+- Added ability to cancel mail sending from hook_mail_alter().
+- Added support for configurable PDO connection options, enabling master-master
+  database replication.
+- Numerous improvements to tests and test runner to pave the way for faster test
+  runs.
+- Expanded test coverage.
+- Numerous API documentation improvements.
+- Numerous performance improvements, including token replacement and render
+  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
+- Fixed running update.php doesn't always clear the cache.
+- Fixed PDO exceptions on long titles.
+- Fixed Overlay redirect does not include query string.
+- Fixed D6 modules satisfy D7 module dependencies.
+- Fixed the ordering of module hooks when using module_implements_alter().
+- Fixed "floating" submit buttons during AJAX requests.
+- Fixed timezone selected on install not propogating to admin account.
+- Added msgctx context to JS translation functions, for feature parity with t().
+- Profiles' .install files now available during hook_install_tasks().
+- Added test coverage of 7.0 -> 7.x upgrade path.
+- Numerous notice fixes.
+- Numerous documentation improvements.
+- Additional automated test coverage.
+
+Drupal 7.9, 2011-10-26
+----------------------
+- Critical fixes to OpenID to spec violations that could allow for
+  impersonation in certain scenarios. Existing OpenID users should see
+  http://drupal.org/node/1120290#comment-5092796 for more information on
+  transitioning.
+- Fixed files getting lost when adding multiple files to multiple file fields
+  at the same time.
+- Improved usability of the clean URL test screens.
+- Restored height/width attributes on images run through the theme system.
+- Fixed usability bug with first password field being pre-filled by certain
+  browser plugins.
+- Fixed file_usage_list() so that it can return more than one result.
+- Fixed bug preventing preview of private images on node form.
+- Fixed PDO error when inserting an aggregator title longer than 255 characters.
+- Spelled out what TRADITIONAL means in MySQL sql_mode.
+- Deprecated "!=" operator for DBTNG; should be "<>".
+- Added two new API functions (menu_tree_set_path()/menu_tree_get_path()) were
+  added in order to enable setting the active menu trail for dynamically
+  generated menu paths.
+- Added new "fast 404" capability in settings.php to bypass Drupal bootstrap
+  when serving 404 pages for certain file types.
+- Added format_string() function which can perform string munging ala the t()
+  function without the overhead of the translation system.
+- Numerous #states system fixes.
+- Numerous EntityFieldQuery, DBTNG, and SQLite fixes.
+- Numerous Shortcut module fixes.
+- Numerous language system fixes.
+- Numerous token fixes.
+- Numerous CSS fixes.
+- Numerous upgrade path fixes.
+- Numerous minor string fixes.
+- Numerous notice fixes.
+
+Drupal 7.8, 2011-08-31
+----------------------
+- Fixed critical upgrade path issue with multilingual sites, leading to lost
+  content.
+- Numerous fixes to upgrade path, preventing fatal errors due to incorrect
+  dependencies.
+- Fixed issue with saving files on hosts with open_basedir restrictions.
+- Fixed Update manger error when used with Overlay.
+- Fixed RTL support in Seven administration theme and Overlay.
+- Fixes to nested transaction support.
+- Introduced performance pattern to reduce Drupal core's RAM usage.
+- Added support for HTML 5 tags to filter_xss_admin().
+- Added exception handling to cron.
+- Added new hook hook_field_widget_form_alter() for contribtued modules.
+- element_validate_*() functions now available to contrib.
+- Added new maintainers for several subsystems.
+- Numerous testing system improvements.
+- Numerous markup and CSS fixes.
+- Numerous poll module fixes.
+- Numerous notice/warning fixes.
+- Numerous documentation fixes.
+- Numerous token fixes.
+
+Drupal 7.7, 2011-07-27
+----------------------
+- Fixed VERSION string.
+
+Drupal 7.6, 2011-07-27
+----------------------
+- Fixed support for remote streamwrappers.
+- AJAX now binds to 'click' instead of 'mousedown'.
+- 'Translatable' flag on fields created in UI now defaults to FALSE, to match those created via the API.
+- Performance enhancement to permissions page on large numbers of permissions.
+- More secure password generation.
+- Fix for temporary directory on Windows servers.
+- run-tests.sh now uses proc_open() instead of pcntl_fork() for better Windows support.
+- Numerous upgrade path fixes.
+- Numerous documentation fixes.
+- Numerous notice fixes.
+- Numerous fixes to improve PHP 5.4 support.
+- Numerous RTL improvements.
+
+Drupal 7.5, 2011-07-27
+----------------------
+- Fixed security issue (Access bypass), see SA-CORE-2011-003.
+
+Drupal 7.4, 2011-06-29
+----------------------
+- Rolled back patch that caused fatal errors in CTools, Feeds, and other modules using the class registry.
+- Fixed critical bug with saving default images.
+- Fixed fatal errors when uninstalling some modules.
+- Added workaround for MySQL transaction support breaking on DDL statments.
+- Improved page caching with external caching systems.
+- Fix to Batch API, which was terminating too early.
+- Numerous upgrade path fixes.
+- Performance fixes.
+- Additional test coverage.
+- Numerous documentation fixes.
+
+Drupal 7.3, 2011-06-29
+----------------------
+- Fixed security issue (Access bypass), see SA-CORE-2011-002.
+
+Drupal 7.2, 2011-05-25
+----------------------
+- Added a default .gitignore file.
+- Improved PostgreSQL and SQLite support.
+- Numerous critical performance improvements.
+- Numerous critical fixes to the upgrade path.
+- Numerous fixes to language and translation systems.
+- Numerous fixes to AJAX and #states systems.
+- Improvements to the locking system.
+- Numerous documentation fixes.
+- Numerous styling and theme system fixes.
+- Numerous fixes for schema mis-matches between Drupal 6 and 7.
+- Minor internal API clean-ups.
+
+Drupal 7.1, 2011-05-25
+----------------------
+- Fixed security issues (Cross site scripting, File access bypass), see SA-CORE-2011-001.
+
+Drupal 7.0, 2011-01-05 
 ----------------------
 - Database:
     * Fully rewritten database layer utilizing PHP 5's PDO abstraction layer.
@@ -35,8 +1114,8 @@
       order can now be customized using the Views module.
     * Removed the 'related terms' feature from taxonomy module since this can
       now be achieved with Field API.
-    * Added additional features to the default install profile, and implemented
-      a "slimmed down" install profile designed for developers.
+    * Added additional features to the default installation profile, and
+      implemented a "slimmed down" profile designed for developers.
     * Added a built-in, automated cron run feature, which is triggered by site
       visitors.
     * Added an administrator role which is assigned all permissions for
@@ -103,7 +1182,7 @@
       are available.
 - OpenID:
     * Added support for Gmail and Google Apps for Domain identifiers. Users can
-      now login with their user@domain.com identifier when domain.com is powered
+      now login with their user@example.com identifier when example.com is powered
       by Google.
     * Made the OpenID module more pluggable.
 - Added code registry:
@@ -219,6 +1298,159 @@
     * Added a locking framework to coordinate long-running operations across
       requests.
 
+Drupal 6.23-dev, xxxx-xx-xx (development release)
+---------------------------
+
+Drupal 6.22, 2011-05-25
+-----------------------
+- Made Drupal 6 work better with IIS and Internet Explorer.
+- Fixed .po file imports to work better with custom textgroups.
+- Improved code documentation at various places.
+- 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
+- Improved logging for login failures
+- Fixed an incompatibility with some contributed modules and the locking system
+- 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.
+- Better support for updated jQuery versions.
+- Reduced resource usage of update.module.
+- Fixed several issues relating to support of installation profiles and
+  distributions.
+- Added a locking framework to avoid data corruption on long operations.
+- 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
+  button on the performance settings page to achieve the same effect.
+- Added support for PHP 5.3.0 out of the box.
+- 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
+  status cache and improved cache invalidation
+- 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.
+- Improved PostgreSQL compatibility.
+- Fixed a variety of small bugs.
+
+Drupal 6.9, 2009-01-14
+----------------------
+- Fixed security issues, (Access Bypass, Validation Bypass and Hardening
+  against SQL injection), see SA-CORE-2009-001
+- Made HTTP request checking more robust and informative.
+- Fixed HTTP_HOST checking to work again with HTTP 1.0 clients and
+  basic shell scripts.
+- Removed t() calls from all schema documentation. Suggested best practice
+  changed for contributed modules, see http://drupal.org/node/322731.
+- Fixed a variety of small bugs.
+
+Drupal 6.8, 2008-12-11
+----------------------
+- Removed a previous change incompatible with PHP 5.1.x and lower.
+
+Drupal 6.7, 2008-12-10
+----------------------
+- Fixed security issues, (Cross site request forgery and Cross site scripting), see SA-2008-073
+- Updated robots.txt and .htaccess to match current file use.
+- Fixed a variety of small bugs.
+
+Drupal 6.6, 2008-10-22
+----------------------
+- Fixed security issues, (File inclusion, Cross site scripting), see SA-2008-067
+- Fixed a variety of small bugs.
+
+Drupal 6.5, 2008-10-08
+----------------------
+- Fixed security issues, (File upload access bypass, Access rules bypass,
+  BlogAPI access bypass), see SA-2008-060.
+- Fixed a variety of small bugs.
+
+Drupal 6.4, 2008-08-13
+----------------------
+- Fixed a security issue (Cross site scripting, Arbitrary file uploads via
+  BlogAPI, Cross site request forgeries and Various Upload module
+  vulnerabilities), see SA-2008-047.
+- Improved error messages during installation.
+- Fixed a bug that prevented AHAH handlers to be attached to radios widgets.
+- Fixed a variety of small bugs.
+
+Drupal 6.3, 2008-07-09
+----------------------
+- Fixed security issues, (Cross site scripting, cross site request forgery,
+  session fixation and SQL injection), see SA-2008-044.
+- Slightly modified installation process to prevent file ownership issues on
+  shared hosts.
+- Improved PostgreSQL compatibility (rewritten queries; custom blocks).
+- Upgraded to jQuery 1.2.6.
+- Performance improvements to search, menu handling and form API caches.
+- Fixed Views compatibility issues (Views for Drupal 6 requires Drupal 6.3+).
+- Fixed a variety of small bugs.
+
+Drupal 6.2, 2008-04-09
+----------------------
+- Fixed a variety of small bugs.
+- Fixed a security issue (Access bypasses), see SA-2008-026.
+
+Drupal 6.1, 2008-02-27
+----------------------
+- Fixed a variety of small bugs.
+- Fixed a security issue (Cross site scripting), see SA-2008-018.
+
 Drupal 6.0, 2008-02-13
 ----------------------
 - New, faster and better menu system.
@@ -228,7 +1460,7 @@
    * Expands the severity levels from 3 (Error, Warning, Notice) to the 8
      levels defined in RFC 3164.
    * The watchdog module is now called dblog, and is optional, but enabled by
-     default in the default install profile.
+     default in the default installation profile.
    * Extended the database log module so log messages can be filtered.
    * Added syslog module: useful for monitoring large Drupal installations.
 - Added optional e-mail notifications when users are approved, blocked, or
@@ -283,7 +1515,7 @@
     * Themed the installer with the Garland theme.
     * Added form to provide initial site information during installation.
     * Added ability to provide extra installation steps programmatically.
-    * Made it possible to import interface translations at install time.
+    * Made it possible to import interface translations during installation.
 - Added the HTML corrector filter:
     * Fixes faulty and chopped off HTML in postings.
     * Tags are now automatically closed at the end of the teaser.
@@ -321,6 +1553,95 @@
 - Removed old system updates. Updates from Drupal versions prior to 5.x will
   require upgrading to 5.x before upgrading to 6.x.
 
+Drupal 5.23, 2010-08-11
+-----------------------
+- Fixed security issues (File download access bypass, Comment unpublishing
+  bypass), see SA-CORE-2010-002.
+
+Drupal 5.22, 2010-03-03
+-----------------------
+- Fixed security issues (Open redirection, Locale module cross site scripting,
+  Blocked user session regeneration), see SA-CORE-2010-001.
+
+Drupal 5.21, 2009-12-16
+-----------------------
+- Fixed a security issue (Cross site scripting), see SA-CORE-2009-009.
+- Fixed a variety of small bugs.
+
+Drupal 5.20, 2009-09-16
+-----------------------
+- Avoid security problems resulting from writing Drupal 6-style menu
+  declarations.
+- Fixed security issues (session fixation), see SA-CORE-2009-008.
+- Fixed a variety of small bugs.
+
+Drupal 5.19, 2009-07-01
+-----------------------
+- Fixed security issues (Cross site scripting and Password leakage in URL), see
+  SA-CORE-2009-007.          
+- Fixed a variety of small bugs.
+
+Drupal 5.18, 2009-05-13
+-----------------------
+- Fixed security issues (Cross site scripting), see SA-CORE-2009-006.
+- Fixed a variety of small bugs.
+
+Drupal 5.17, 2009-04-29
+-----------------------
+- Fixed security issues (Cross site scripting and limited information
+  disclosure) see SA-CORE-2009-005.
+- Fixed a variety of small bugs.
+
+Drupal 5.16, 2009-02-25
+-----------------------
+- Fixed a security issue, (Local file inclusion on Windows), see SA-CORE-2009-004.
+- Fixed a variety of small bugs.
+
+Drupal 5.15, 2009-01-14
+-----------------------
+- Fixed security issues, (Hardening against SQL injection), see
+  SA-CORE-2009-001
+- Fixed HTTP_HOST checking to work again with HTTP 1.0 clients and basic shell
+  scripts.
+- Fixed a variety of small bugs.
+
+Drupal 5.14, 2008-12-11
+-----------------------
+- removed a previous change incompatible with PHP 5.1.x and lower.
+
+Drupal 5.13, 2008-12-10
+-----------------------
+- fixed a variety of small bugs.
+- fixed security issues, (Cross site request forgery and Cross site scripting), see SA-2008-073
+- updated robots.txt and .htaccess to match current file use.
+
+Drupal 5.12, 2008-10-22
+-----------------------
+- fixed security issues, (File inclusion), see SA-2008-067
+
+Drupal 5.11, 2008-10-08
+-----------------------
+- fixed a variety of small bugs.
+- fixed security issues, (File upload access bypass, Access rules bypass,
+  BlogAPI access bypass, Node validation bypass), see SA-2008-060
+
+Drupal 5.10, 2008-08-13
+-----------------------
+- fixed a variety of small bugs.
+- fixed security issues, (Cross site scripting, Arbitrary file uploads via
+  BlogAPI and Cross site request forgery), see SA-2008-047
+
+Drupal 5.9, 2008-07-23
+----------------------
+- fixed a variety of small bugs.
+- fixed security issues, (Session fixation), see SA-2008-046
+
+Drupal 5.8, 2008-07-09
+----------------------
+- fixed a variety of small bugs.
+- fixed security issues, (Cross site scripting, cross site request forgery, and
+  session fixation), see SA-2008-044
+
 Drupal 5.7, 2008-01-28
 ----------------------
 - fixed the input format configuration page.
@@ -373,7 +1694,7 @@
 - Added web-based installer which can:
     * Check installation and run-time requirements
     * Automatically generate the database configuration file
-    * Install pre-made 'install profiles' or distributions
+    * Install pre-made installation profiles or distributions
     * Import the database structure with automatic table prefixing
     * Be localized
 - Added new default Garland theme
@@ -433,7 +1754,7 @@
 - Removed the archive module.
 - Upgrade system:
     * Created space for update branches.
-- Forms API:
+- Form API:
     * Made it possible to programmatically submit forms.
     * Improved api for multistep forms.
 - Theme system:
@@ -456,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
@@ -569,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
@@ -737,7 +2058,7 @@
 - Filter system:
     * Added support for using multiple input formats on the site
     * Expanded the embedded PHP-code feature so it can be used everywhere
-    * Added support for role-dependant filtering, through input formats
+    * Added support for role-dependent filtering, through input formats
 - UI translation:
     * Managing translations is now completely done through the administration interface
     * Added support for importing/exporting gettext .po files
diff -Naur drupal-7.0/COPYRIGHT.txt drupal-7.66/COPYRIGHT.txt
--- drupal-7.0/COPYRIGHT.txt	2010-01-02 11:20:21.000000000 +0100
+++ drupal-7.66/COPYRIGHT.txt	2019-04-17 22:20:46.000000000 +0200
@@ -1,10 +1,9 @@
-// $Id: COPYRIGHT.txt,v 1.6 2010/01/02 10:20:21 dries Exp $
-
-All Drupal code is Copyright 2001 - 2010 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
-the Free Software Foundation.
+the Free Software Foundation; either version 2 of the License, or (at
+your option) any later version.
 
 This program is distributed in the hope that it will be useful, but
 WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
@@ -21,5 +20,25 @@
 according to the terms of the GNU General Public License or a compatible
 license, including:
 
-  jQuery - Copyright (c) 2008 - 2009 John Resig
+Javascript
+
+  Farbtastic - Copyright (c) 2010 Matt Farina
+
+  jQuery - Copyright (c) 2010 John Resig
+
+  jQuery BBQ - Copyright (c) 2010 "Cowboy" Ben Alman
+
+  jQuery Cookie - Copyright (c) 2006 Klaus Hartl
+
+  jQuery Form - Copyright (c) 2010 Mike Alsup
+
+  jQuery Once - Copyright (c) 2009 Konstantin K�fer
+
+  jQuery UI - Copyright (c) 2010 by the original authors
+    (http://jqueryui.com/about)
+
+  Sizzle.js - Copyright (c) 2010 The Dojo Foundation (http://sizzlejs.com/)
+
+PHP
 
+  ArchiveTar - Copyright (c) 1997 - 2008 Vincent Blavet
diff -Naur drupal-7.0/INSTALL.mysql.txt drupal-7.66/INSTALL.mysql.txt
--- drupal-7.0/INSTALL.mysql.txt	2010-01-11 17:25:15.000000000 +0100
+++ drupal-7.66/INSTALL.mysql.txt	2019-04-17 22:20:46.000000000 +0200
@@ -1,12 +1,11 @@
-// $Id: INSTALL.mysql.txt,v 1.12 2010/01/11 16:25:15 webchick Exp $
 
 CREATE THE MySQL DATABASE
 --------------------------
 
-This step is only necessary if you don't already have a database set-up (e.g. by
-your host). In the following examples, 'username' is an example MySQL user which
-has the CREATE and GRANT privileges. Use the appropriate user name for your
-system.
+This step is only necessary if you don't already have a database set up (e.g.,
+by your host). In the following examples, 'username' is an example MySQL user
+which has the CREATE and GRANT privileges. Use the appropriate user name for
+your system.
 
 First, you must create a new database for your Drupal site (here, 'databasename'
 is the name of the new database):
@@ -19,20 +18,23 @@
   mysql -u username -p
 
 Again, you will be asked for the 'username' database password. At the MySQL
-prompt, enter following command:
+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.0/INSTALL.pgsql.txt drupal-7.66/INSTALL.pgsql.txt
--- drupal-7.0/INSTALL.pgsql.txt	2010-04-07 17:07:58.000000000 +0200
+++ drupal-7.66/INSTALL.pgsql.txt	2019-04-17 22:20:46.000000000 +0200
@@ -1,4 +1,3 @@
-// $Id: INSTALL.pgsql.txt,v 1.9 2010/04/07 15:07:58 dries Exp $
 
 CREATE THE PostgreSQL DATABASE
 ------------------------------
@@ -7,38 +6,39 @@
 
 1. CREATE DATABASE USER
 
-   This step is only necessary if you don't already have a user set up (e.g.
-   by your host) or you want to create new user for use with Drupal only. The
-   following command creates a new user named "username" and asks for a
-   password for that user:
+   This step is only necessary if you don't already have a user set up (e.g., by
+   your host), or want to create a new user for use with Drupal only. The
+   following command creates a new user named 'username' and asks for a password
+   for that user:
 
      createuser --pwprompt --encrypted --no-createrole --no-createdb username
 
-   If there are no errors then the command was successful
+   If there are no errors, then the command was successful.
 
-2. CREATE THE DRUPAL DATABASE
+2. CREATE DRUPAL DATABASE
 
-   This step is only necessary if you don't already have a database set up (e.g.
-   by your host) or you want to create new database for use with Drupal only.
-   The following command creates a new database named "databasename", which is
-   owned by previously created "username":
+   This step is only necessary if you don't already have a database set up
+   (e.g., by your host) or want to create a new database for use with Drupal
+   only. The following command creates a new database named 'databasename',
+   which is owned by the previously created 'username':
 
      createdb --encoding=UTF8 --owner=username databasename
 
-   If there are no errors then the command was successful
+   If there are no errors, then the command was successful.
 
-3. CREATE A SCHEMA OR SCHEMAS (Optional advanced)
+3. CREATE SCHEMA OR SCHEMAS (Optional advanced step)
 
-  Drupal will run across different schemas within your database if you so wish.
-  By default, Drupal runs inside the 'public' schema but you can use $db_prefix
-  inside settings.php to define a schema for Drupal to inside of or specify tables
-  that are shared inside of a separate schema. Drupal will not create schemas for
-  you, infact the user that Drupal runs as should not be allowed to. You'll need
-  execute the SQL below as a superuser (such as a postgres user) and replace
-  'drupaluser' with the username that Drupal uses to connect to PostgreSQL with
-  and replace schema_name with a schema name you wish to use such as 'shared':
+   Drupal will run across different schemas within your database if you so wish.
+   By default, Drupal runs inside the 'public' schema but you can use $db_prefix
+   inside settings.php to define a schema for Drupal to run inside of, or
+   specify tables that are shared inside of a separate schema. Drupal will not
+   create schemas for you. In fact, the user that Drupal runs as should not be
+   allowed to do this. You'll need to execute the SQL below as a superuser,
+   replace 'username' with the username that Drupal uses to connect to
+   PostgreSQL, and replace 'schema_name' with a schema name you wish to use,
+   such as 'shared':
 
-    CREATE SCHEMA schema_name AUTHORIZATION drupaluser;
+     CREATE SCHEMA schema_name AUTHORIZATION username;
 
-  Do this for as many schemas as you need. See default.settings.php for how to
-  set which tables use which schemas.
+   Do this for as many schemas as you need. See default.settings.php for
+   instructions on how to set which tables use which schemas.
diff -Naur drupal-7.0/INSTALL.sqlite.txt drupal-7.66/INSTALL.sqlite.txt
--- drupal-7.0/INSTALL.sqlite.txt	2010-11-29 03:55:57.000000000 +0100
+++ drupal-7.66/INSTALL.sqlite.txt	2019-04-17 22:20:46.000000000 +0200
@@ -1,20 +1,19 @@
-// $Id: INSTALL.sqlite.txt,v 1.4 2010/11/29 02:55:57 webchick Exp $
 
 SQLITE REQUIREMENTS
 -------------------
 
-To use SQLite with your Drupal installation, the following requirements must
-be met: server has PHP 5.2 or later with PDO, and the PDO SQLite driver must
-be enabled.
+To use SQLite with your Drupal installation, the following requirements must be
+met: Server has PHP 5.2 or later with PDO, and the PDO SQLite driver must be
+enabled.
 
 SQLITE DATABASE CREATION
 ------------------------
 
 The Drupal installer will create the SQLite database for you. The only
-requirement is the installer must have write permissions the directory where
-the database file resides. This directory (not just the database file) also has
-to remain writeable by the web server going forward for SQLite to continue to be
-able to operate.
+requirement is that the installer must have write permissions to the directory
+where the database file resides. This directory (not just the database file) also
+has to remain writeable by the web server going forward for SQLite to continue to
+be able to operate.
 
 On the "Database configuration" form in the "Database file" field, you must
 supply the exact path to where you wish your database file to reside. It is
diff -Naur drupal-7.0/INSTALL.txt drupal-7.66/INSTALL.txt
--- drupal-7.0/INSTALL.txt	2011-01-01 23:41:24.000000000 +0100
+++ drupal-7.66/INSTALL.txt	2019-04-17 22:20:46.000000000 +0200
@@ -1,4 +1,3 @@
-// $Id: INSTALL.txt,v 1.87 2011/01/01 22:41:24 webchick Exp $
 
 CONTENTS OF THIS FILE
 ---------------------
@@ -10,7 +9,6 @@
  * Multisite configuration
  * More information
 
-
 REQUIREMENTS AND NOTES
 ----------------------
 
@@ -22,29 +20,30 @@
   - 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"
-(http://drupal.org/requirements) in the Drupal handbook.
+(http://drupal.org/requirements) in the Drupal.org online documentation.
 
 For detailed information on how to configure a test server environment using a
 variety of operating systems and web servers, see "Local server setup"
-(http://drupal.org/node/157602) in the Drupal handbook.
+(http://drupal.org/node/157602) in the Drupal.org online documentation.
 
 Note that all directories mentioned in this document are always relative to the
 directory of your Drupal installation, and commands are meant to be run from
 this directory (except for the initial commands that create that directory).
 
-
 OPTIONAL SERVER REQUIREMENTS
 ----------------------------
 
 - If you want to use Drupal's "Clean URLs" feature on an Apache web server, you
   will need the mod_rewrite module and the ability to use local .htaccess
-  files. For Clean URLs support on IIS, see "Using Clean URLs with IIS"
-  (http://drupal.org/node/3854) in the Drupal handbook.
+  files. For Clean URLs support on IIS, see "Clean URLs with IIS"
+  (http://drupal.org/node/3854) in the Drupal.org online documentation.
 
 - If you plan to use XML-based services such as RSS aggregation, you will need
   PHP's XML extension. This extension is enabled by default on most PHP
@@ -60,17 +59,18 @@
   configuration allows the web server to initiate outbound connections. Most web
   hosting setups allow this.
 
-
 INSTALLATION
 ------------
 
 1. Download and extract Drupal.
 
    You can obtain the latest Drupal release from http://drupal.org -- the files
-   are in .tar.gz format and can be extracted using most compression tools.
+   are available in .tar.gz and .zip formats and can be extracted using most
+   compression tools.
 
    To download and extract the files, on a typical Unix/Linux command line, use
-   the following commands (assuming you want version x.y of Drupal):
+   the following commands (assuming you want version x.y of Drupal in .tar.gz
+   format):
 
      wget http://drupal.org/files/projects/drupal-x.y.tar.gz
      tar -zxvf drupal-x.y.tar.gz
@@ -89,10 +89,10 @@
    initially:
 
    - Download a translation file for the correct Drupal version and language
-     from the translation server: http://localize.drupal.org/download
+     from the translation server: http://localize.drupal.org/translate/downloads
 
-   - Place the file into your installation profile's translations
-     directory. For instance, if you are using the Standard install profile,
+   - Place the file into your installation profile's translations directory.
+     For instance, if you are using the Standard installation profile,
      move the .po file into the directory:
 
        profiles/standard/translations/
@@ -257,7 +257,7 @@
    For more information on setting file permissions, see "Modifying Linux,
    Unix, and Mac file permissions" (http://drupal.org/node/202483) or
    "Modifying Windows file permissions" (http://drupal.org/node/202491) in the
-   online handbook.
+   Drupal.org online documentation.
 
 8. Set up independent "cron" maintenance jobs.
 
@@ -299,7 +299,6 @@
    scripts/ directory. (Note that these scripts must be customized like the
    above example, to add your site-specific cron key and domain name.)
 
-
 BUILDING AND CUSTOMIZING YOUR SITE
 ----------------------------------
 
@@ -317,12 +316,11 @@
 site-specific directories -- see the Multisite Configuration section, below.
 
 Never edit Drupal's core modules and themes; instead, use the hooks available in
-the Drupal API. To modify the behavior of Drupal, develope a module as described
+the Drupal API. To modify the behavior of Drupal, develop a module as described
 at http://drupal.org/developing/modules. To modify the look of Drupal, create a
 subtheme as described at http://drupal.org/node/225125, or a completely new
 theme as described at http://drupal.org/documentation/theme
 
-
 MULTISITE CONFIGURATION
 -----------------------
 
@@ -332,16 +330,15 @@
 Additional site configurations are created in subdirectories within the 'sites'
 directory. Each subdirectory must have a 'settings.php' file, which specifies
 the configuration settings. The easiest way to create additional sites is to
-copy the 'default' directory and modify the 'settings.php' file as
-appropriate. The new directory name is constructed from the site's URL. The
-configuration for www.example.com could be in 'sites/example.com/settings.php'
-(note that 'www.' should be omitted if users can access your site at
-http://example.com/).
+copy the 'default' directory and modify the 'settings.php' file as appropriate.
+The new directory name is constructed from the site's URL. The configuration for
+www.example.com could be in 'sites/example.com/settings.php' (note that 'www.'
+should be omitted if users can access your site at http://example.com/).
 
 Sites do not have to have a different domain. You can also use subdomains and
-subdirectories for Drupal sites. For example, example.com, sub.example.com,
-and sub.example.com/site3 can all be defined as independent Drupal sites. The
-setup for a configuration such as this would look like the following:
+subdirectories for Drupal sites. For example, example.com, sub.example.com, and
+sub.example.com/site3 can all be defined as independent Drupal sites. The setup
+for a configuration such as this would look like the following:
 
   sites/default/settings.php
   sites/example.com/settings.php
@@ -384,15 +381,14 @@
 For more information on configuring Drupal's file system path in a multisite
 configuration, see step 6 above.
 
-
 MORE INFORMATION
 ----------------
 
-- For additional documentation, see the online Drupal handbook:
-  http://drupal.org/handbook
+- See the Drupal.org online documentation:
+  http://drupal.org/documentation
 
-- For a list of security announcements, see the "Security announcements" page
-  at http://drupal.org/security (available as an RSS feed). This page also
+- For a list of security announcements, see the "Security advisories" page at
+  http://drupal.org/security (available as an RSS feed). This page also
   describes how to subscribe to these announcements via e-mail.
 
 - For information about the Drupal security process, or to find out how to
diff -Naur drupal-7.0/LICENSE.txt drupal-7.66/LICENSE.txt
--- drupal-7.0/LICENSE.txt	2009-01-26 15:08:40.000000000 +0100
+++ drupal-7.66/LICENSE.txt	2016-11-17 00:57:05.000000000 +0100
@@ -1,13 +1,12 @@
-// $Id: LICENSE.txt,v 1.7 2009/01/26 14:08:40 dries Exp $
-        GNU GENERAL PUBLIC LICENSE
-           Version 2, June 1991
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 2, June 1991
 
  Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
  Everyone is permitted to copy and distribute verbatim copies
  of this license document, but changing it is not allowed.
 
-          Preamble
+                            Preamble
 
   The licenses for most software are designed to take away your
 freedom to share and change it.  By contrast, the GNU General Public
@@ -57,7 +56,7 @@
   The precise terms and conditions for copying, distribution and
 modification follow.
 
-        GNU GENERAL PUBLIC LICENSE
+                    GNU GENERAL PUBLIC LICENSE
    TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
 
   0. This License applies to any program or other work which contains
@@ -256,7 +255,7 @@
 of preserving the free status of all derivatives of our free software and
 of promoting the sharing and reuse of software generally.
 
-          NO WARRANTY
+                            NO WARRANTY
 
   11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
 FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
@@ -278,9 +277,9 @@
 PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
 POSSIBILITY OF SUCH DAMAGES.
 
-         END OF TERMS AND CONDITIONS
+                     END OF TERMS AND CONDITIONS
 
-      How to Apply These Terms to Your New Programs
+            How to Apply These Terms to Your New Programs
 
   If you develop a new program, and you want it to be of the greatest
 possible use to the public, the best way to achieve this is to make it
diff -Naur drupal-7.0/MAINTAINERS.txt drupal-7.66/MAINTAINERS.txt
--- drupal-7.0/MAINTAINERS.txt	2011-01-04 02:03:23.000000000 +0100
+++ drupal-7.66/MAINTAINERS.txt	2019-04-17 22:20:46.000000000 +0200
@@ -1,147 +1,163 @@
-// $Id: MAINTAINERS.txt,v 1.53 2011/01/04 01:03:23 webchick Exp $
-
-Drupal core is maintained by the community.  To participate, go to
-
-  http://drupal.org/contribute
-
-The people listed here have agreed to do more quality assurance work for
-particular areas of Drupal.  All of them are subject to change.
 
+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 https://www.drupal.org/contribute to find out
+how.
 
 Branch maintainers
 ------------------
 
-Drupal 7
-- Dries Buytaert 'dries' 
-- Angela Byron 'webchick' 
+The Drupal Core branch maintainers oversee the development of Drupal as a whole.
+The branch maintainers for Drupal 7 are:
+
+- 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
 ---------------------
 
-AJAX system
-- Alex Bronstein 'effulgentsia' 
-- Randy Fay 'rfay' 
-- Earl Miles 'merlinofchaos' 
+The Drupal Core component maintainers oversee the development of Drupal
+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' https://www.drupal.org/u/effulgentsia
+- Earl Miles 'merlinofchaos' https://www.drupal.org/u/merlinofchaos
 
 Base system
-- Károly Négyesi 'chx' 
-- Damien Tournoud 'DamZ' 
-- Moshe Weitzman 'moshe weitzman' 
+- 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' 
+- Yves Chedemois 'yched' https://www.drupal.org/u/yched
 
 Cache system
-- Damien Tournoud 'DamZ' 
+- 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' 
-- Derek Wright 'dww' 
+- Derek Wright 'dww' https://www.drupal.org/u/dww
 
 Database system
-- Larry Garfield 'Crell' 
+- ?
 
   - MySQL driver
-    - Larry Garfield 'Crell' 
-    - David Strauss 'David Strauss' 
+    - David Strauss 'David Strauss' https://www.drupal.org/u/david-strauss
 
   - PostgreSQL driver
-    - Damien Tournoud 'DamZ' 
-    - Josh Waihi 'fiasco' 
+    - 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' 
-    - Károly Négyesi 'chx' 
+    - Damien Tournoud 'DamZ' https://www.drupal.org/u/damien-tournoud
 
 Database update system
-- ?
+- Ashok Modi 'BTMash' https://www.drupal.org/u/btmash
 
 Entity system
-- Nathaniel Catchpole 'catch' 
-- Franz Heinzmann 'Frando' 
+- 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' 
-- Aaron Winborn 'aaron' 
+- 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' 
-- Alex Bronstein 'effulgentsia' 
-- Wolfgang Ziegler 'fago' 
-- Daniel F. Kudwien 'sun' 
-- Franz Heinzmann 'Frando' 
+- 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' 
-- Nathan Haug 'quicksketch' 
+- Andrew Morton 'drewish' https://www.drupal.org/u/drewish
+- Nathan Haug 'quicksketch' https://www.drupal.org/u/quicksketch
 
 Install system
-- David Rothstein 'David_Rothstein' 
+- David Rothstein 'David_Rothstein' https://www.drupal.org/u/david_rothstein
 
 JavaScript
-- ?
+- 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' 
-- Daniel F. Kudwien 'sun' 
+- Francesco Placella 'plach' https://www.drupal.org/u/plach
+- Daniel F. Kudwien 'sun' https://www.drupal.org/u/sun
 
 Lock system
-- Damien Tournoud 'DamZ' 
+- Damien Tournoud 'DamZ' https://www.drupal.org/u/damien-tournoud
 
 Mail system
 - ?
 
 Markup
-- Jacine Luisi 'Jacine' 
-- Daniel F. Kudwien 'sun' 
+- Jacine Luisi 'Jacine' https://www.drupal.org/u/jacine
+- Daniel F. Kudwien 'sun' https://www.drupal.org/u/sun
 
 Menu system
-- Peter Wolanin 'pwolanin' 
-- Károly Négyesi 'chx' 
+- Peter Wolanin 'pwolanin' https://www.drupal.org/u/pwolanin
 
 Path system
-- Dave Reid 'davereid' 
-- Nathaniel Catchpole 'catch' 
+- 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' 
-- Alex Bronstein 'effulgentsia' 
-- Franz Heinzmann 'Frando' 
+- 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' 
-- Alex Bronstein 'effulgentsia' 
-- Joon Park 'dvessel' 
-- John Albin Wilkins 'JohnAlbin' 
+- 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' 
+- Dave Reid 'davereid' https://www.drupal.org/u/dave-reid
 
 XML-RPC system
-- Frederic G. Marand 'fgm' 
+- Frederic G. Marand 'fgm' https://www.drupal.org/u/fgm
 
 
 Topic coordinators
 ------------------
 
 Accessibility
-- Everett Zufelt 'Everett Zufelt' 
-- Brandon Bowersox 'brandonojc'  
+- Everett Zufelt 'Everett Zufelt' https://www.drupal.org/u/everett-zufelt
+- Brandon Bowersox-Johnson 'bowersox' https://www.drupal.org/u/bowersox
 
 Documentation
-- Ariane Khachatourians 'arianek' 
-- Jennifer Hodgdon 'jhodgdon' 
-
-Security
-- Heine Deelstra 'Heine' 
+- Jennifer Hodgdon 'jhodgdon' https://www.drupal.org/u/jhodgdon
 
 Translations
-- Gerhard Killesreiter 'killes' 
+- Gerhard Killesreiter 'killes' https://www.drupal.org/u/gerhard-killesreiter
 
 User experience and usability
-- Roy Scholten 'yoroy' 
-- Bojhan Somers 'Bojhan' 
+- Roy Scholten 'yoroy' https://www.drupal.org/u/yoroy
+- Bojhan Somers 'Bojhan' https://www.drupal.org/u/bojhan
+
+Node Access
+- 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
@@ -151,142 +167,141 @@
 - ?
 
 Block module
-- John Albin Wilkins 'JohnAlbin' 
+- John Albin Wilkins 'JohnAlbin' https://www.drupal.org/u/johnalbin
 
 Blog module
 - ?
 
 Book module
-- Peter Wolanin 'pwolanin' 
+- Peter Wolanin 'pwolanin' https://www.drupal.org/u/pwolanin
 
 Color module
 - ?
 
 Comment module
-- Nathaniel Catchpole 'catch' 
+- Nathaniel Catchpole 'catch' https://www.drupal.org/u/catch
 
 Contact module
-- Dave Reid 'davereid' 
+- Dave Reid 'davereid' https://www.drupal.org/u/dave-reid
 
 Contextual module
-- Daniel F. Kudwien 'sun' 
+- Daniel F. Kudwien 'sun' https://www.drupal.org/u/sun
 
 Dashboard module
 - ?
 
 Database logging module
-- Khalid Baheyeldin 'kbahey' 
+- Khalid Baheyeldin 'kbahey' https://www.drupal.org/u/kbahey
 
 Field module
-- Yves Chedemois 'yched' 
-- Barry Jaspan 'bjaspan' 
+- Yves Chedemois 'yched' https://www.drupal.org/u/yched
+- Barry Jaspan 'bjaspan' https://www.drupal.org/u/bjaspan
 
 Field UI module
-- Yves Chedemois 'yched' 
+- Yves Chedemois 'yched' https://www.drupal.org/u/yched
 
 File module
-- Aaron Winborn 'aaron' 
+- Aaron Winborn 'aaron' https://www.drupal.org/u/aaron
 
 Filter module
-- Daniel F. Kudwien 'sun' 
+- Daniel F. Kudwien 'sun' https://www.drupal.org/u/sun
 
 Forum module
-- ?
+- Lee Rowlands 'larowlan' https://www.drupal.org/u/larowlan
 
 Help module
 - ?
 
 Image module
-- Nathan Haug 'quicksketch' 
+- Nathan Haug 'quicksketch' https://www.drupal.org/u/quicksketch
 
 Locale module
-- Gábor Hojtsy 'Gábor Hojtsy' 
+- Gábor Hojtsy 'Gábor Hojtsy' https://www.drupal.org/u/gábor-hojtsy
 
 Menu module
 - ?
 
 Node module
-- Moshe Weitzman 'moshe weitzman' 
-- David Strauss 'David Strauss' 
+- Moshe Weitzman 'moshe weitzman' https://www.drupal.org/u/moshe-weitzman
+- David Strauss 'David Strauss' https://www.drupal.org/u/david-strauss
 
 OpenID module
-- Heine Deelstra 'Heine' 
-- Christian Schmidt 'c960657' 
-- Damien Tournoud 'DamZ' 
+- 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' 
+- Katherine Senzee 'ksenzee' https://www.drupal.org/u/ksenzee
 
 Path module
-- Dave Reid 'davereid' 
+- Dave Reid 'davereid' https://www.drupal.org/u/dave-reid
 
 PHP module
 - ?
 
 Poll module
-- ?
+- Andrei Mateescu 'amateescu' https://www.drupal.org/u/amateescu
 
 Profile module
 - ?
 
 RDF module
-- Stéphane Corlosquet 'scor' 
+- Stéphane Corlosquet 'scor' https://www.drupal.org/u/scor
 
 Search module
-- Doug Green 'douggreen' 
+- Doug Green 'douggreen' https://www.drupal.org/u/douggreen
 
 Shortcut module
-- David Rothstein 'David_Rothstein' 
+- David Rothstein 'David_Rothstein' https://www.drupal.org/u/david_rothstein
 
 Simpletest module
-- Jimmy Berry 'boombatower' 
-- Károly Négyesi 'chx' 
+- Jimmy Berry 'boombatower' https://www.drupal.org/u/boombatower
 
 Statistics module
-- Dave Reid 'davereid' 
+- Tim Millwood 'timmillwood' https://www.drupal.org/u/timmillwood
 
 Syslog module
-- Khalid Baheyeldin 'kbahey' 
+- Khalid Baheyeldin 'kbahey' https://www.drupal.org/u/kbahey
 
 System module
 - ?
 
 Taxonomy module
-- Nathaniel Catchpole 'catch' 
-- Benjamin Doherty 'bangpound' 
+- 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' 
+- David Strauss 'David Strauss' https://www.drupal.org/u/david-strauss
 
 Translation module
-- Francesco Placella 'plach' 
+- Francesco Placella 'plach' https://www.drupal.org/u/plach
 
 Trigger module
 - ?
 
 Update module
-- Derek Wright 'dww' 
+- Derek Wright 'dww' https://www.drupal.org/u/dww
 
 User module
-- Moshe Weitzman 'moshe weitzman' 
-- David Strauss 'David Strauss' 
+- 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' 
-- Jeff Burns 'Jeff Burnz' 
+- 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' 
+- John Albin Wilkins 'JohnAlbin' https://www.drupal.org/u/johnalbin
 
 Seven theme
-- Jeff Burns 'Jeff Burnz' 
+- Jeff Burns 'Jeff Burnz' https://www.drupal.org/u/jeff-burnz
 
 Stark theme
-- John Albin Wilkins 'JohnAlbin' 
+- John Albin Wilkins 'JohnAlbin' https://www.drupal.org/u/johnalbin
diff -Naur drupal-7.0/README.txt drupal-7.66/README.txt
--- drupal-7.0/README.txt	2010-12-02 01:20:18.000000000 +0100
+++ drupal-7.66/README.txt	2019-04-17 22:20:46.000000000 +0200
@@ -1,10 +1,10 @@
-// $Id: README.txt,v 1.1 2010/12/02 00:20:18 webchick Exp $
 
 CONTENTS OF THIS FILE
 ---------------------
 
  * About Drupal
  * Configuration and features
+ * Installation profiles
  * Appearance
  * Developing for Drupal
 
@@ -25,14 +25,14 @@
 CONFIGURATION AND FEATURES
 --------------------------
 
-Drupal core (what you get when you download and unzip a drupal-x.y.tar.gz file
-from http://drupal.org/project/drupal) has what you need to get started with
-your website. It includes several modules (extensions that add functionality)
-for common website features, such as managing content, user accounts, image
-uploading, and search. Core comes with many options that allow site-specific
-configuration. In addition to the core modules, there are thousands of
-contributed modules (for functionality not included with Drupal core)
-available for download.
+Drupal core (what you get when you download and extract a drupal-x.y.tar.gz or
+drupal-x.y.zip file from http://drupal.org/project/drupal) has what you need to
+get started with your website. It includes several modules (extensions that add
+functionality) for common website features, such as managing content, user
+accounts, image uploading, and search. Core comes with many options that allow
+site-specific configuration. In addition to the core modules, there are
+thousands of contributed modules (for functionality not included with Drupal
+core) available for download.
 
 More about configuration:
  * Install, upgrade, and maintain Drupal:
@@ -44,13 +44,47 @@
    http://drupal.org/project/modules
  * See also: "Developing for Drupal" for writing your own modules, below.
 
+INSTALLATION PROFILES
+---------------------
+
+Installation profiles define additional steps (such as enabling modules,
+defining content types, etc.) that run after the base installation provided
+by core when Drupal is first installed. There are two basic installation
+profiles provided with Drupal core.
+
+Installation profiles from the Drupal community modify the installation process
+to provide a website for a specific use case, such as a CMS for media
+publishers, a web-based project tracking tool, or a full-fledged CRM for
+non-profit organizations raising money and accepting donations. They can be
+distributed as bare installation profiles or as "distributions". Distributions
+include Drupal core, the installation profile, and all other required
+extensions, such as contributed and custom modules, themes, and third-party
+libraries. Bare installation profiles require you to download Drupal Core and
+the required extensions separately; place the downloaded profile in the
+/profiles directory before you start the installation process. Note that the
+contents of this directory may be overwritten during updates of Drupal core;
+it is advised to keep code backups or use a version control system.
+
+Additionally, modules and themes may be placed inside subdirectories in a
+specific installation profile such as profiles/your_site_profile/modules and
+profiles/your_site_profile/themes respectively to restrict their usage to only
+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
+
 APPEARANCE
 ----------
 
 In Drupal, the appearance of your site is set by the theme (themes are
 extensions that set fonts, colors, and layout). Drupal core comes with several
-themes. More themes are available for download, and you can also create your
-own custom theme.
+themes. More themes are available for download, and you can also create your own
+custom theme.
 
 More about themes:
  * Download contributed themes to sites/all/themes to modify Drupal's
diff -Naur drupal-7.0/UPGRADE.txt drupal-7.66/UPGRADE.txt
--- drupal-7.0/UPGRADE.txt	2010-10-27 20:32:54.000000000 +0200
+++ drupal-7.66/UPGRADE.txt	2019-04-17 22:20:46.000000000 +0200
@@ -1,5 +1,3 @@
-// $Id: UPGRADE.txt,v 1.27 2010/10/27 18:32:54 dries Exp $
-
 INTRODUCTION
 ------------
 This document describes how to:
@@ -13,7 +11,7 @@
 
   * If you are upgrading to Drupal version x.y, then x is known as the major
     version number, and y is known as the minor version number. The download
-    file will be named drupal-x.y.tar.gz.
+    file will be named drupal-x.y.tar.gz (or drupal-x.y.zip).
 
   * All directories mentioned in this document are relative to the directory of
     your Drupal installation.
@@ -26,6 +24,11 @@
     applying it to your live site. Even minor updates can cause your site's
     behavior to change.
 
+  * Each new release of Drupal has release notes, which explain the changes made
+    since the previous version and any special instructions needed to update or
+    upgrade to the new version. You can find a link to the release notes for the
+    version you are upgrading or updating to on the Drupal project page
+    (http://drupal.org/project/drupal).
 
 UPGRADE PROBLEMS
 ----------------
@@ -41,7 +44,6 @@
 
 More in-depth information on upgrading can be found at http://drupal.org/upgrade
 
-
 MINOR VERSION UPDATES
 ---------------------
 To update from one minor 7.x version of Drupal to any later 7.x version, after
@@ -59,11 +61,28 @@
    If you made modifications to files like .htaccess or robots.txt, you will
    need to re-apply them from your backup, after the new files are in place.
 
-   Sometimes an update includes changes to settings.php (this will be noted in
-   the release announcement). If that's the case, replace your old settings.php
-   with the new one, and copy the site-specific entries (especially the lines
-   giving the database name, user, and password) from the old settings.php to
-   the new settings.php.
+   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
+     settings.php (overwriting your previous settings.php file).
+
+   - Copy the custom and site-specific entries from the backup you made into the
+     new settings.php file. You will definitely need the lines giving the
+     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
@@ -111,7 +130,6 @@
    Disable the "Put site into maintenance mode" checkbox and save the
    configuration.
 
-
 MAJOR VERSION UPGRADE
 ---------------------
 To upgrade from a previous major version of Drupal to Drupal 7.x, after
@@ -133,15 +151,19 @@
    download Drupal 6.x and follow the instructions in its UPGRADE.txt. This
    document only applies for upgrades from 6.x to 7.x.
 
-3. Log in as user ID 1 (the site maintenance user).
+3. In addition to updating to the latest available version of Drupal 6.x core,
+   you must also upgrade all of your contributed modules for Drupal to their
+   latest Drupal 6.x versions.
 
-4. Go to Administer > Site configuration > Site maintenance. Select
+4. Log in as user ID 1 (the site maintenance user).
+
+5. Go to Administer > Site configuration > Site maintenance. Select
    "Off-line" and save the configuration.
 
-5. Go to Administer > Site building > Themes. Enable "Garland" and select it as
+6. Go to Administer > Site building > Themes. Enable "Garland" and select it as
    the default theme.
 
-6. Go to Administer > Site building > Modules. Disable all modules that are not
+7. Go to Administer > Site building > Modules. Disable all modules that are not
    listed under "Core - required" or "Core - optional". It is possible that some
    modules cannot be disabled, because others depend on them. Repeat this step
    until all non-core modules are disabled.
@@ -150,21 +172,21 @@
    no longer need their data, then you can uninstall them under the Uninstall
    tab after disabling them.
 
-7. On the command line or in your FTP client, remove the file
+8. On the command line or in your FTP client, remove the file
 
      sites/default/default.settings.php
 
-8. Remove all old core files and directories, except for the 'sites' directory
+9. Remove all old core files and directories, except for the 'sites' directory
    and any custom files you added elsewhere.
 
    If you made modifications to files like .htaccess or robots.txt, you will
    need to re-apply them from your backup, after the new files are in place.
 
-9. If you uninstalled any modules, remove them from the sites/all/modules and
+10. If you uninstalled any modules, remove them from the sites/all/modules and
    other sites/*/modules directories. Leave other modules in place, even though
    they are incompatible with Drupal 7.x.
 
-10. Download the latest Drupal 7.x release from http://drupal.org to a
+11. 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.
 
@@ -183,14 +205,14 @@
    from http://drupal.org using your web browser, extract it, and then use an
    FTP client to upload the files to your web root.
 
-11. Re-apply any modifications to files such as .htaccess or robots.txt.
+12. Re-apply any modifications to files such as .htaccess or robots.txt.
 
-12. Make your settings.php file writeable, so that the update process can
+13. Make your settings.php file writeable, so that the update process can
    convert it to the format of Drupal 7.x. settings.php is usually located in
 
      sites/default/settings.php
 
-13. Run update.php by visiting http://www.example.com/update.php (replace
+14. Run update.php by visiting http://www.example.com/update.php (replace
    www.example.com with your domain name). This will update the core database
    tables.
 
@@ -206,20 +228,19 @@
 
    - Once the upgrade is done, $update_free_access must be reverted to FALSE.
 
-14. Backup your database after the core upgrade has run.
+15. Backup your database after the core upgrade has run.
 
-15. Replace and update your non-core modules and themes, following the
+16. Replace and update your non-core modules and themes, following the
    procedures at http://drupal.org/node/948216
 
-16. Go to Administration > Reports > Status report. Verify that everything is
+17. Go to Administration > Reports > Status report. Verify that everything is
    working as expected.
 
-17. Ensure that $update_free_access is FALSE in settings.php.
+18. Ensure that $update_free_access is FALSE in settings.php.
 
-18. Go to Administration > Configuration > Development > Maintenance mode.
+19. Go to Administration > Configuration > Development > Maintenance mode.
    Disable the "Put site into maintenance mode" checkbox and save the
    configuration.
 
 To get started with Drupal 7 administration, visit
 http://drupal.org/getting-started/7/admin
-
diff -Naur drupal-7.0/authorize.php drupal-7.66/authorize.php
--- drupal-7.0/authorize.php	2010-12-29 05:07:52.000000000 +0100
+++ drupal-7.66/authorize.php	2019-04-17 22:20:46.000000000 +0200
@@ -1,20 +1,19 @@
 '),
       ));
     }
-	
+
     $output .= theme('item_list', array('items' => $links, 'title' => t('Next steps')));
   }
   // If a batch is running, let it run.
@@ -173,4 +172,3 @@
 if (!empty($output)) {
   print theme('update_page', array('content' => $output, 'show_messages' => $show_messages));
 }
-
diff -Naur drupal-7.0/cron.php drupal-7.66/cron.php
--- drupal-7.0/cron.php	2009-11-02 04:30:49.000000000 +0100
+++ drupal-7.66/cron.php	2019-04-17 22:20:46.000000000 +0200
@@ -1,5 +1,4 @@
  $count, '%orphans' => $orphans, '!link' => $link), WATCHDOG_WARNING);
+      watchdog('actions', '@count orphaned actions (%orphans) exist in the actions table. !link', array('@count' => $count, '%orphans' => $orphans, '!link' => $link), WATCHDOG_INFO);
     }
   }
 }
@@ -333,6 +336,7 @@
  *   to Jim'.
  * @param $aid
  *   The ID of this action. If omitted, a new action is created.
+ *
  * @return
  *   The ID of the action.
  */
@@ -362,6 +366,7 @@
  *
  * @param $aid
  *   The ID of the action to retrieve.
+ *
  * @return
  *   The appropriate action row from the database as an object.
  */
@@ -381,4 +386,3 @@
     ->execute();
   module_invoke_all('actions_delete', $aid);
 }
-
diff -Naur drupal-7.0/includes/ajax.inc drupal-7.66/includes/ajax.inc
--- drupal-7.0/includes/ajax.inc	2011-01-02 18:26:39.000000000 +0100
+++ drupal-7.66/includes/ajax.inc	2019-04-17 22:20:46.000000000 +0200
@@ -1,17 +1,16 @@
  'ajax', '#commands' => $commands);
  * @endcode
  *
- * When returning an AJAX command array, it is often useful to have
+ * 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);
@@ -204,18 +219,22 @@
  *   return array('#type' => 'ajax', '#commands' => $commands);
  * @endcode
  *
- * See @link ajax_commands AJAX framework commands @endlink
+ * See @link ajax_commands Ajax framework commands @endlink
  */
 
 /**
- * Render a commands array into JSON.
+ * Renders a commands array into JSON.
  *
  * @param $commands
  *   A list of macro commands generated by the use of ajax_command_*()
  *   functions.
  */
 function ajax_render($commands = array()) {
-  // AJAX responses aren't rendered with html.tpl.php, so we have to call
+  // 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
   // files that the page hasn't already loaded, so we implement simple diffing
@@ -235,9 +254,9 @@
       // @todo Inline CSS and JS items are indexed numerically. These can't be
       //   reliably diffed with array_diff_key(), since the number can change
       //   due to factors unrelated to the inline content, so for now, we strip
-      //   the inline items from AJAX responses, and can add support for them
-      //   when drupal_add_css() and drupal_add_js() are changed to using md5()
-      //   or some other hash of the inline content.
+      //   the inline items from Ajax responses, and can add support for them
+      //   when drupal_add_css() and drupal_add_js() are changed to use a hash
+      //   of the inline content as the array key.
       foreach ($items[$type] as $key => $item) {
         if (is_numeric($key)) {
           unset($items[$type][$key]);
@@ -248,26 +267,20 @@
     }
   }
 
-  // Settings are handled separately, later in this function, so that changes to
-  // the ajaxPageState setting that occur during drupal_get_css() and
-  // drupal_get_js() get included, and because the jQuery.extend() code produced
-  // by drupal_get_js() for adding settings isn't appropriate during an AJAX
-  // response, because it does not pass TRUE for the "deep" parameter, and
-  // therefore, can clobber existing settings on the page.
+  // Render the HTML to load these files, and add AJAX commands to insert this
+  // HTML in the page. We pass TRUE as the $skip_alter argument to prevent the
+  // data from being altered again, as we already altered it above. Settings are
+  // handled separately, afterwards.
   if (isset($items['js']['settings'])) {
     unset($items['js']['settings']);
   }
-
-  // Render the HTML to load these files, and add AJAX commands to insert this
-  // HTML in the page. We pass TRUE as the $skip_alter argument to prevent the
-  // data from being altered again, as we already altered it above.
   $styles = drupal_get_css($items['css'], TRUE);
   $scripts_footer = drupal_get_js('footer', $items['js'], TRUE);
   $scripts_header = drupal_get_js('header', $items['js'], TRUE);
 
   $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);
@@ -279,31 +292,31 @@
     $commands = array_merge($extra_commands, $commands);
   }
 
+  // Now add a command to merge changes and additions to Drupal.settings.
   $scripts = drupal_add_js();
   if (!empty($scripts['settings'])) {
     $settings = $scripts['settings'];
-    // Automatically extract any settings added via drupal_add_js() and make
-    // them the first command.
-    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.
+  // Allow modules to alter any Ajax response.
   drupal_alter('ajax_render', $commands);
 
   return drupal_json_encode($commands);
 }
 
 /**
- * Get a form submitted via #ajax during an AJAX callback.
+ * Gets a form submitted via #ajax during an Ajax callback.
  *
- * This will load a form from the form cache used during AJAX operations. It
+ * This will load a form from the form cache used during Ajax operations. It
  * 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() {
@@ -323,10 +336,21 @@
     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;
 
-  // When a form is rebuilt after AJAX processing, its #build_id and #action
+  // When a form is rebuilt after Ajax processing, its #build_id and #action
   // should not change.
   // @see drupal_rebuild_form()
   $form_state['rebuild_info']['copy']['#build_id'] = TRUE;
@@ -337,14 +361,14 @@
   $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);
 }
 
 /**
- * Menu callback; handles AJAX requests for the #ajax Form API property.
+ * Menu callback; handles Ajax requests for the #ajax Form API property.
  *
  * This rebuilds the form from cache and invokes the defined #ajax['callback']
- * to return an AJAX command structure for JavaScript. In case no 'callback' has
+ * to return an Ajax command structure for JavaScript. In case no 'callback' has
  * been defined, nothing will happen.
  *
  * The Form API #ajax property can be set both for buttons and other input
@@ -354,41 +378,58 @@
  * #ajax['path']. If processing is required that cannot be accomplished with
  * a callback, re-implement this function and set #ajax['path'] to the
  * enhanced function.
+ *
+ * @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
   // to be re-rendered so the browser can update the page with changed content.
-  // Since this is the generic menu callback used by many AJAX elements, it is
+  // Since this is the generic menu callback used by many Ajax elements, it is
   // up to the #ajax['callback'] function of the element (may or may not be a
-  // button) that triggered the AJAX request to determine what needs to be
+  // button) that triggered the Ajax request to determine what needs to be
   // rendered.
   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;
   }
 }
 
 /**
- * Theme callback for AJAX requests.
+ * Theme callback for Ajax requests.
  *
- * Many different pages can invoke an AJAX request to system/ajax or another
- * generic AJAX path. It is almost always desired for an AJAX response to be
+ * Many different pages can invoke an Ajax request to system/ajax or another
+ * generic Ajax path. It is almost always desired for an Ajax response to be
  * rendered using the same theme as the base page, because most themes are built
  * with the assumption that they control the entire page, so if the CSS for two
  * themes are both loaded for a given page, they may conflict with each other.
  * For example, Bartik is Drupal's default theme, and Seven is Drupal's default
  * administration theme. Depending on whether the "Use the administration theme
  * when editing or creating content" checkbox is checked, the node edit form may
- * be displayed in either theme, but the AJAX response to the Field module's
+ * be displayed in either theme, but the Ajax response to the Field module's
  * "Add another item" button should be rendered using the same theme as the rest
  * of the page. Therefore, system_menu() sets the 'theme callback' for
  * 'system/ajax' to this function, and it is recommended that modules
- * implementing other generic AJAX paths do the same.
+ * implementing other generic Ajax paths do the same.
+ *
+ * @see system_menu()
+ * @see file_menu()
  */
 function ajax_base_page_theme() {
   if (!empty($_POST['ajax_page_state']['theme']) && !empty($_POST['ajax_page_state']['theme_token'])) {
@@ -407,9 +448,9 @@
 }
 
 /**
- * Package and send the result of a page callback to the browser as an AJAX response.
+ * Packages and sends the result of a page callback as an Ajax response.
  *
- * This function is the equivalent of drupal_deliver_html_page(), but for AJAX
+ * This function is the equivalent of drupal_deliver_html_page(), but for Ajax
  * requests. Like that function, it:
  * - Adds needed HTTP headers.
  * - Prints rendered output.
@@ -425,36 +466,74 @@
  * @see drupal_deliver_html_page()
  */
 function ajax_deliver($page_callback_result) {
+  // Browsers do not allow JavaScript to read the contents of a user's local
+  // files. To work around that, the jQuery Form plugin submits forms containing
+  // a file input element to an IFRAME, instead of using XHR. Browsers do not
+  // normally expect JSON strings as content within an IFRAME, so the response
+  // must be customized accordingly.
+  // @see http://malsup.com/jquery/form/#file-upload
+  // @see Drupal.ajax.prototype.beforeSend()
+  $iframe_upload = !empty($_POST['ajax_iframe_upload']);
+
   // Emit a Content-Type HTTP header if none has been added by the page callback
   // or by a wrapping delivery callback.
   if (is_null(drupal_get_http_header('Content-Type'))) {
-    // The standard header for JSON is application/json.
-    // @see http://www.ietf.org/rfc/rfc4627.txt?number=4627
-    // However, browsers do not allow JavaScript to read the contents of a
-    // user's local files. To work around that, jQuery submits forms containing
-    // a file input element to an IFRAME, instead of using XHR.
-    // @see http://malsup.com/jquery/form/#file-upload
-    // When Internet Explorer receives application/json content in an IFRAME, it
-    // treats it as a file download and prompts the user to save it. To prevent
-    // that, we return the content as text/plain. But only for POST requests,
-    // since jQuery should always use XHR for GET requests and the incorrect
-    // mime type should not end up in page or proxy server caches.
-    // @see http://drupal.org/node/995854
-    $iframe_upload = !isset($_SERVER['HTTP_X_REQUESTED_WITH']) || $_SERVER['HTTP_X_REQUESTED_WITH'] != 'XMLHttpRequest';
-    if ($iframe_upload && $_SERVER['REQUEST_METHOD'] == 'POST') {
-      drupal_add_http_header('Content-Type', 'text/plain; charset=utf-8');
+    if (!$iframe_upload) {
+      // Standard JSON can be returned to a browser's XHR object, and to
+      // non-browser user agents.
+      // @see http://www.ietf.org/rfc/rfc4627.txt?number=4627
+      drupal_add_http_header('Content-Type', 'application/json; charset=utf-8');
     }
     else {
-      drupal_add_http_header('Content-Type', 'application/json; charset=utf-8');
+      // Browser IFRAMEs expect HTML. With most other content types, Internet
+      // Explorer presents the user with a download prompt.
+      drupal_add_http_header('Content-Type', 'text/html; charset=utf-8');
     }
   }
 
-  // Normalize whatever was returned by the page callback to an AJAX commands
-  // array.
+  // 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);
+  if (!$iframe_upload) {
+    // Standard JSON can be returned to a browser's XHR object, and to
+    // non-browser user agents.
+    print $json;
+  }
+  else {
+    // Browser IFRAMEs expect HTML. Browser extensions, such as Linkification
+    // and Skype's Browser Highlighter, convert URLs, phone numbers, etc. into
+    // links. This corrupts the JSON response. Protect the integrity of the
+    // JSON data by making it the value of a textarea.
+    // @see http://malsup.com/jquery/form/#file-upload
+    // @see http://drupal.org/node/1009382
+    print '';
+  }
+
+  // Perform end-of-request tasks.
+  ajax_footer();
+}
+
+/**
+ * Converts the return value of a page callback into an Ajax commands array.
+ *
+ * @param $page_callback_result
+ *   The result of a page callback. Can be one of:
+ *   - NULL: to indicate no content.
+ *   - An integer menu status constant: to indicate an error condition.
+ *   - A string of HTML content.
+ *   - A renderable array of content.
+ *
+ * @return
+ *   An Ajax commands array that can be passed to ajax_render().
+ */
+function ajax_prepare_response($page_callback_result) {
   $commands = array();
   if (!isset($page_callback_result)) {
     // Simply delivering an empty commands array is sufficient. This results
-    // in the AJAX request being completed, but nothing being done to the page.
+    // in the Ajax request being completed, but nothing being done to the page.
   }
   elseif (is_int($page_callback_result)) {
     switch ($page_callback_result) {
@@ -473,7 +552,7 @@
     }
   }
   elseif (is_array($page_callback_result) && isset($page_callback_result['#type']) && ($page_callback_result['#type'] == 'ajax')) {
-    // Complex AJAX callbacks can return a result that contains an error message
+    // Complex Ajax callbacks can return a result that contains an error message
     // or a specific set of commands to send to the browser.
     $page_callback_result += element_info('ajax');
     $error = $page_callback_result['#error'];
@@ -488,7 +567,7 @@
     }
   }
   else {
-    // Like normal page callbacks, simple AJAX callbacks can return HTML
+    // Like normal page callbacks, simple Ajax callbacks can return HTML
     // content, as a string or render array. This HTML is inserted in some
     // relationship to #ajax['wrapper'], as determined by which jQuery DOM
     // manipulation method is used. The method used is specified by
@@ -497,28 +576,47 @@
     $html = is_string($page_callback_result) ? $page_callback_result : drupal_render($page_callback_result);
     $commands[] = ajax_command_insert(NULL, $html);
     // Add the status messages inside the new content's wrapper element, so that
-    // on subsequent AJAX requests, it is treated as old content.
+    // on subsequent Ajax requests, it is treated as old content.
     $commands[] = ajax_command_prepend(NULL, theme('status_messages'));
   }
 
-  // Unlike the recommendation in http://malsup.com/jquery/form/#file-upload,
-  // we do not have to wrap the JSON string in a TEXTAREA, because
-  // drupal_json_encode() returns an HTML-safe JSON string.
-  print ajax_render($commands);
-  ajax_footer();
+  return $commands;
+}
+
+/**
+ * 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;
+  }
 }
 
 /**
- * Perform end-of-AJAX-request tasks.
+ * Performs end-of-Ajax-request tasks.
  *
- * This function is the equivalent of drupal_page_footer(), but for AJAX
+ * This function is the equivalent of drupal_page_footer(), but for Ajax
  * requests.
  *
  * @see drupal_page_footer()
  */
 function ajax_footer() {
-  // Even for AJAX requests, invoke hook_exit() implementations. There may be
-  // modules that need very fast AJAX responses, and therefore, run AJAX
+  // Even for Ajax requests, invoke hook_exit() implementations. There may be
+  // modules that need very fast Ajax responses, and therefore, run Ajax
   // requests with an early bootstrap.
   if (drupal_get_bootstrap_phase() == DRUPAL_BOOTSTRAP_FULL && (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update')) {
     module_invoke_all('exit');
@@ -532,7 +630,7 @@
 }
 
 /**
- * Form element process callback to handle #ajax.
+ * Form element processing handler for the #ajax form property.
  *
  * @param $element
  *   An associative array containing the properties of the element.
@@ -551,16 +649,17 @@
 }
 
 /**
- * Add AJAX information about an element to the page to communicate with JavaScript.
+ * Adds Ajax information about an element to communicate with JavaScript.
  *
  * If #ajax['path'] is set on an element, this additional JavaScript is added
- * to the page header to attach the AJAX behaviors. See ajax.js for more
+ * to the page header to attach the Ajax behaviors. See ajax.js for more
  * information.
  *
  * @param $element
  *   An associative array containing the properties of the element.
  *   Properties used:
  *   - #ajax['event']
+ *   - #ajax['prevent']
  *   - #ajax['path']
  *   - #ajax['options']
  *   - #ajax['wrapper']
@@ -589,13 +688,26 @@
       case 'submit':
       case 'button':
       case 'image_button':
-        // Use the mousedown instead of the click event because form
-        // submission via pressing the enter key triggers a click event on
-        // submit inputs, inappropriately triggering AJAX behaviors.
+        // Pressing the ENTER key within a textfield triggers the click event of
+        // the form's first submit button. Triggering Ajax in this situation
+        // leads to problems, like breaking autocomplete textfields, so we bind
+        // to mousedown instead of click.
+        // @see http://drupal.org/node/216059
         $element['#ajax']['event'] = 'mousedown';
-        // Attach an additional event handler so that AJAX behaviors
-        // can be triggered still via keyboard input.
+        // Retain keyboard accessibility by setting 'keypress'. This causes
+        // ajax.js to trigger 'event' when SPACE or ENTER are pressed while the
+        // button has focus.
         $element['#ajax']['keypress'] = TRUE;
+        // Binding to mousedown rather than click means that it is possible to
+        // trigger a click by pressing the mouse, holding the mouse button down
+        // until the Ajax request is complete and the button is re-enabled, and
+        // then releasing the mouse button. Set 'prevent' so that ajax.js binds
+        // an additional handler to prevent such a click from triggering a
+        // non-Ajax form submission. This also prevents a textfield's ENTER
+        // press triggering this button's non-Ajax form submission behavior.
+        if (!isset($element['#ajax']['prevent'])) {
+          $element['#ajax']['prevent'] = 'click';
+        }
         break;
 
       case 'password':
@@ -642,7 +754,7 @@
     unset($settings['path'], $settings['options']);
 
     // Add special data to $settings['submit'] so that when this element
-    // triggers an AJAX submission, Drupal's form processing can determine which
+    // triggers an Ajax submission, Drupal's form processing can determine which
     // element triggered it.
     // @see _form_element_triggered_scripted_submission()
     if (isset($settings['trigger_as'])) {
@@ -682,10 +794,15 @@
 
     $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.
+    // Indicate that Ajax processing was successful.
     $element['#ajax_processed'] = TRUE;
   }
   return $element;
@@ -696,16 +813,16 @@
  */
 
 /**
- * @defgroup ajax_commands AJAX framework commands
+ * @defgroup ajax_commands Ajax framework commands
  * @{
- * Functions to create various AJAX commands.
+ * Functions to create various Ajax commands.
  *
  * These functions can be used to create arrays for use with the
  * ajax_render() function.
  */
 
 /**
- * Creates a Drupal AJAX 'alert' command.
+ * Creates a Drupal Ajax 'alert' command.
  *
  * The 'alert' command instructs the client to display a JavaScript alert
  * dialog box.
@@ -727,7 +844,7 @@
 }
 
 /**
- * Creates a Drupal AJAX 'insert' command using the method in #ajax['method'].
+ * Creates a Drupal Ajax 'insert' command using the method in #ajax['method'].
  *
  * This command instructs the client to insert the given HTML using whichever
  * jQuery DOM manipulation method has been specified in the #ajax['method']
@@ -758,7 +875,7 @@
 }
 
 /**
- * Creates a Drupal AJAX 'insert/replaceWith' command.
+ * Creates a Drupal Ajax 'insert/replaceWith' command.
  *
  * The 'insert/replaceWith' command instructs the client to use jQuery's
  * replaceWith() method to replace each element matched matched by the given
@@ -778,7 +895,8 @@
  * @return
  *   An array suitable for use with the ajax_render() function.
  *
- * See @link http://docs.jquery.com/Manipulation/replaceWith#content jQuery replaceWith command @endlink
+ * See
+ * @link http://docs.jquery.com/Manipulation/replaceWith#content jQuery replaceWith command @endlink
  */
 function ajax_command_replace($selector, $html, $settings = NULL) {
   return array(
@@ -791,7 +909,7 @@
 }
 
 /**
- * Creates a Drupal AJAX 'insert/html' command.
+ * Creates a Drupal Ajax 'insert/html' command.
  *
  * The 'insert/html' command instructs the client to use jQuery's html()
  * method to set the HTML content of each element matched by the given
@@ -824,7 +942,7 @@
 }
 
 /**
- * Creates a Drupal AJAX 'insert/prepend' command.
+ * Creates a Drupal Ajax 'insert/prepend' command.
  *
  * The 'insert/prepend' command instructs the client to use jQuery's prepend()
  * method to prepend the given HTML content to the inside each element matched
@@ -857,10 +975,10 @@
 }
 
 /**
- * Creates a Drupal AJAX 'insert/append' command.
+ * Creates a Drupal Ajax 'insert/append' command.
  *
  * The 'insert/append' command instructs the client to use jQuery's append()
- * method to append the given HTML content to the inside each element matched
+ * method to append the given HTML content to the inside of each element matched
  * by the given selector.
  *
  * This command is implemented by Drupal.ajax.prototype.commands.insert()
@@ -890,7 +1008,7 @@
 }
 
 /**
- * Creates a Drupal AJAX 'insert/after' command.
+ * Creates a Drupal Ajax 'insert/after' command.
  *
  * The 'insert/after' command instructs the client to use jQuery's after()
  * method to insert the given HTML content after each element matched by
@@ -923,7 +1041,7 @@
 }
 
 /**
- * Creates a Drupal AJAX 'insert/before' command.
+ * Creates a Drupal Ajax 'insert/before' command.
  *
  * The 'insert/before' command instructs the client to use jQuery's before()
  * method to insert the given HTML content before each of elements matched by
@@ -956,7 +1074,7 @@
 }
 
 /**
- * Creates a Drupal AJAX 'remove' command.
+ * Creates a Drupal Ajax 'remove' command.
  *
  * The 'remove' command instructs the client to use jQuery's remove() method
  * to remove each of elements matched by the given selector, and everything
@@ -982,7 +1100,7 @@
 }
 
 /**
- * Creates a Drupal AJAX 'changed' command.
+ * Creates a Drupal Ajax 'changed' command.
  *
  * This command instructs the client to mark each of the elements matched by the
  * given selector as 'ajax-changed'.
@@ -1009,7 +1127,7 @@
 }
 
 /**
- * Creates a Drupal AJAX 'css' command.
+ * Creates a Drupal Ajax 'css' command.
  *
  * The 'css' command will instruct the client to use the jQuery css() method
  * to apply the CSS arguments to elements matched by the given selector.
@@ -1037,7 +1155,7 @@
 }
 
 /**
- * Creates a Drupal AJAX 'settings' command.
+ * Creates a Drupal Ajax 'settings' command.
  *
  * The 'settings' command instructs the client either to use the given array as
  * the settings for ajax-loaded content or to extend Drupal.settings with the
@@ -1068,7 +1186,7 @@
 }
 
 /**
- * Creates a Drupal AJAX 'data' command.
+ * Creates a Drupal Ajax 'data' command.
  *
  * The 'data' command instructs the client to attach the name=value pair of
  * data to the selector via jQuery's data cache.
@@ -1100,7 +1218,7 @@
 }
 
 /**
- * Creates a Drupal AJAX 'invoke' command.
+ * Creates a Drupal Ajax 'invoke' command.
  *
  * The 'invoke' command will instruct the client to invoke the given jQuery
  * method with the supplied arguments on the elements matched by the given
@@ -1131,7 +1249,7 @@
 }
 
 /**
- * Creates a Drupal AJAX 'restripe' command.
+ * Creates a Drupal Ajax 'restripe' command.
  *
  * The 'restripe' command instructs the client to restripe a table. This is
  * usually used after a table has been modified by a replace or append command.
@@ -1152,3 +1270,48 @@
   );
 }
 
+/**
+ * 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.0/includes/archiver.inc drupal-7.66/includes/archiver.inc
--- drupal-7.0/includes/archiver.inc	2010-02-01 08:17:59.000000000 +0100
+++ drupal-7.66/includes/archiver.inc	2019-04-17 22:20:46.000000000 +0200
@@ -1,5 +1,4 @@
 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));
       }
     }
   }
@@ -508,7 +524,10 @@
 }
 
 /**
- * Shutdown function; store the current batch data for the next request.
+ * Shutdown function: Stores the current batch data for the next request.
+ *
+ * @see _batch_page()
+ * @see drupal_register_shutdown_function()
  */
 function _batch_shutdown() {
   if ($batch = batch_get()) {
@@ -518,4 +537,3 @@
       ->execute();
   }
 }
-
diff -Naur drupal-7.0/includes/batch.queue.inc drupal-7.66/includes/batch.queue.inc
--- drupal-7.0/includes/batch.queue.inc	2010-12-01 01:21:02.000000000 +0100
+++ drupal-7.66/includes/batch.queue.inc	2019-04-17 22:20:46.000000000 +0200
@@ -1,25 +1,30 @@
  $this->name))->fetchObject();
     if ($item) {
@@ -30,9 +35,9 @@
   }
 
   /**
-   * Retrieve all remaining items in the queue.
+   * Retrieves all remaining items in the queue.
    *
-   * This is specific to Batch API and is not part of the DrupalQueueInterface,
+   * This is specific to Batch API and is not part of the DrupalQueueInterface.
    */
   public function getAllItems() {
     $result = array();
@@ -45,10 +50,17 @@
 }
 
 /**
- * Batch queue implementation used for non-progressive batches.
+ * Defines a batch queue for non-progressive batches.
  */
 class BatchMemoryQueue extends MemoryQueue {
 
+  /**
+   * Overrides MemoryQueue::claimItem().
+   *
+   * Unlike MemoryQueue::claimItem(), this method provides a default lease
+   * time of 0 (no expiration) instead of 30. This allows the item to be
+   * claimed repeatedly until it is deleted.
+   */
   public function claimItem($lease_time = 0) {
     if (!empty($this->queue)) {
       reset($this->queue);
@@ -58,9 +70,9 @@
   }
 
   /**
-   * Retrieve all remaining items in the queue.
+   * Retrieves all remaining items in the queue.
    *
-   * This is specific to Batch API and is not part of the DrupalQueueInterface,
+   * This is specific to Batch API and is not part of the DrupalQueueInterface.
    */
   public function getAllItems() {
     $result = array();
diff -Naur drupal-7.0/includes/bootstrap.inc drupal-7.66/includes/bootstrap.inc
--- drupal-7.0/includes/bootstrap.inc	2011-01-05 07:17:58.000000000 +0100
+++ drupal-7.66/includes/bootstrap.inc	2019-04-17 22:20:46.000000000 +0200
@@ -1,5 +1,4 @@
  'baz'), but later
+ * want to change the value of 'bar' from 'baz' to 'foobar', you cannot do so
+ * a targeted write like $object['foo']['bar'] = 'foobar'. Instead, you must
+ * overwrite the entire top-level 'foo' array with the entire set of new
+ * values: $object['foo'] = array(1, 2, 'bar' => 'foobar'). Due to this same
+ * limitation, attempts to create references to any contained data, nested or
+ * otherwise, will fail silently. So $var = &$object['foo'] will not throw an
+ * 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/arrayaccess.offsetget.php on
+ * ArrayAccess::offsetGet().
+ *
+ * By default, the class accounts for caches where calling functions might
+ * request keys in the array that won't exist even after a cache rebuild. This
+ * prevents situations where a cache rebuild would be triggered over and over
+ * due to a 'missing' item. These cases are stored internally as a value of
+ * NULL. This means that the offsetGet() and offsetExists() methods
+ * must be overridden if caching an array where the top level values can
+ * legitimately be NULL, and where $object->offsetExists() needs to correctly
+ * return (equivalent to array_key_exists() vs. isset()). This should not
+ * be necessary in the majority of cases.
+ *
+ * Classes extending this class must override at least the
+ * resolveCacheMiss() method to have a working implementation.
+ *
+ * offsetSet() is not overridden by this class by default. In practice this
+ * means that assigning an offset via arrayAccess will only apply while the
+ * object is in scope and will not be written back to the persistent cache.
+ * This follows a similar pattern to static vs. persistent caching in
+ * procedural code. Extending classes may wish to alter this behavior, for
+ * example by overriding offsetSet() and adding an automatic call to persist().
+ *
+ * @see SchemaCache
+ */
+abstract class DrupalCacheArray implements ArrayAccess {
+
+  /**
+   * A cid to pass to cache_set() and cache_get().
+   */
+  protected $cid;
+
+  /**
+   * A bin to pass to cache_set() and cache_get().
+   */
+  protected $bin;
+
+  /**
+   * An array of keys to add to the cache at the end of the request.
+   */
+  protected $keysToPersist = array();
+
+  /**
+   * Storage for the data itself.
+   */
+  protected $storage = array();
+
+  /**
+   * Constructs a DrupalCacheArray object.
+   *
+   * @param $cid
+   *   The cid for the array being cached.
+   * @param $bin
+   *   The bin to cache the array.
+   */
+  public function __construct($cid, $bin) {
+    $this->cid = $cid;
+    $this->bin = $bin;
+
+    if ($cached = cache_get($this->cid, $this->bin)) {
+     $this->storage = $cached->data;
+    }
+  }
+
+  /**
+   * Implements ArrayAccess::offsetExists().
+   */
+  public function offsetExists($offset) {
+    return $this->offsetGet($offset) !== NULL;
+  }
+
+  /**
+   * Implements ArrayAccess::offsetGet().
+   */
+  public function offsetGet($offset) {
+    if (isset($this->storage[$offset]) || array_key_exists($offset, $this->storage)) {
+      return $this->storage[$offset];
+    }
+    else {
+      return $this->resolveCacheMiss($offset);
+    }
+  }
+
+  /**
+   * Implements ArrayAccess::offsetSet().
+   */
+  public function offsetSet($offset, $value) {
+    $this->storage[$offset] = $value;
+  }
+
+  /**
+   * Implements ArrayAccess::offsetUnset().
+   */
+  public function offsetUnset($offset) {
+    unset($this->storage[$offset]);
+  }
+
+  /**
+   * Flags an offset value to be written to the persistent cache.
+   *
+   * If a value is assigned to a cache object with offsetSet(), by default it
+   * will not be written to the persistent cache unless it is flagged with this
+   * method. This allows items to be cached for the duration of a request,
+   * without necessarily writing back to the persistent cache at the end.
+   *
+   * @param $offset
+   *   The array offset that was requested.
+   * @param $persist
+   *   Optional boolean to specify whether the offset should be persisted or
+   *   not, defaults to TRUE. When called with $persist = FALSE the offset will
+   *   be unflagged so that it will not be written at the end of the request.
+   */
+  protected function persist($offset, $persist = TRUE) {
+    $this->keysToPersist[$offset] = $persist;
+  }
+
+  /**
+   * Resolves a cache miss.
+   *
+   * When an offset is not found in the object, this is treated as a cache
+   * miss. This method allows classes implementing the interface to look up
+   * the actual value and allow it to be cached.
+   *
+   * @param $offset
+   *   The offset that was requested.
+   *
+   * @return
+   *   The value of the offset, or NULL if no value was found.
+   */
+  abstract protected function resolveCacheMiss($offset);
+
+  /**
+   * Writes a value to the persistent cache immediately.
+   *
+   * @param $data
+   *   The data to write to the persistent cache.
+   * @param $lock
+   *   Whether to acquire a lock before writing to cache.
+   */
+  protected function set($data, $lock = TRUE) {
+    // Lock cache writes to help avoid stampedes.
+    // To implement locking for cache misses, override __construct().
+    $lock_name = $this->cid . ':' . $this->bin;
+    if (!$lock || lock_acquire($lock_name)) {
+      if ($cached = cache_get($this->cid, $this->bin)) {
+        $data = $cached->data + $data;
+      }
+      cache_set($this->cid, $data, $this->bin);
+      if ($lock) {
+        lock_release($lock_name);
+      }
+    }
+  }
+
+  /**
+   * Destructs the DrupalCacheArray object.
+   */
+  public function __destruct() {
+    $data = array();
+    foreach ($this->keysToPersist as $offset => $persist) {
+      if ($persist) {
+        $data[$offset] = $this->storage[$offset];
+      }
+    }
+    if (!empty($data)) {
+      $this->set($data);
+    }
+  }
+}
+
+/**
+ * Starts the timer with the specified name.
+ *
+ * If you start and stop the same timer multiple times, the measured intervals
+ * will be accumulated.
+ *
+ * @param $name
  *   The name of the timer.
  */
 function timer_start($name) {
@@ -262,10 +479,11 @@
 }
 
 /**
- * Read the current timer value without stopping the timer.
+ * Reads the current timer value without stopping the timer.
  *
- * @param name
+ * @param $name
  *   The name of the timer.
+ *
  * @return
  *   The current timer value in ms.
  */
@@ -285,10 +503,11 @@
 }
 
 /**
- * Stop the timer with the specified name.
+ * Stops the timer with the specified name.
  *
- * @param name
+ * @param $name
  *   The name of the timer.
+ *
  * @return
  *   A timer array. The array contains the number of times the timer has been
  *   started and stopped (count) and the accumulated timer value in ms (time).
@@ -312,68 +531,25 @@
 }
 
 /**
- * Find the appropriate configuration directory.
+ * Returns the appropriate configuration directory.
  *
- * Try finding a matching configuration directory by stripping the website's
- * hostname from left to right and pathname from right to left. The first
- * configuration file found will be used; the remaining will ignored. If no
- * configuration file is found, return a default value '$confdir/default'.
- *
- * Example for a fictitious site installed at
- * http://www.drupal.org:8080/mysite/test/ the 'settings.php' is searched in
- * the following directories:
- *
- *  1. $confdir/8080.www.drupal.org.mysite.test
- *  2. $confdir/www.drupal.org.mysite.test
- *  3. $confdir/drupal.org.mysite.test
- *  4. $confdir/org.mysite.test
- *
- *  5. $confdir/8080.www.drupal.org.mysite
- *  6. $confdir/www.drupal.org.mysite
- *  7. $confdir/drupal.org.mysite
- *  8. $confdir/org.mysite
- *
- *  9. $confdir/8080.www.drupal.org
- * 10. $confdir/www.drupal.org
- * 11. $confdir/drupal.org
- * 12. $confdir/org
- *
- * 13. $confdir/default
- *
- * If a file named sites.php is present in the $confdir, it will be loaded
- * prior to scanning for directories. It should define an associative array
- * named $sites, which maps domains to directories. It should be in the form
- * of:
- *
- * $sites = array(
- *   'The url to alias' => 'A directory within the sites directory'
- * );
- *
- * For example:
- *
- * $sites = array(
- *   'devexample.com' => 'example.com',
- *   'localhost.example' => 'example.com',
- * );
- *
- * The above array will cause Drupal to look for a directory named
- * "example.com" in the sites directory whenever a request comes from
- * "example.com", "devexample.com", or "localhost/example". That is useful
- * on development servers, where the domain name may not be the same as the
- * domain of the live server. Since Drupal stores file paths into the database
- * (files, system table, etc.) this will ensure the paths are correct while
- * accessed on development servers.
+ * Returns the configuration path based on the site's hostname, port, and
+ * pathname. See default.settings.php for examples on how the URL is converted
+ * to a directory.
  *
- * @param $require_settings
+ * @param bool $require_settings
  *   Only configuration directories with an existing settings.php file
  *   will be recognized. Defaults to TRUE. During initial installation,
  *   this is set to FALSE so that Drupal can detect a matching directory,
  *   then create a new settings.php file in it.
- * @param reset
+ * @param bool $reset
  *   Force a full search for matching directories even if one had been
- *   found previously.
+ *   found previously. Defaults to FALSE.
+ *
  * @return
  *   The path of the matching directory.
+ *
+ * @see default.settings.php
  */
 function conf_path($require_settings = TRUE, $reset = FALSE) {
   $conf = &drupal_static(__FUNCTION__, '');
@@ -409,7 +585,7 @@
 }
 
 /**
- * Set appropriate server variables needed for command line scripts to work.
+ * Sets appropriate server variables needed for command line scripts to work.
  *
  * This function can be called by command line scripts before bootstrapping
  * Drupal, to ensure that the page loads with the desired server parameters.
@@ -471,7 +647,7 @@
 }
 
 /**
- * Initialize PHP environment.
+ * Initializes the PHP environment.
  */
 function drupal_environment_initialize() {
   if (!isset($_SERVER['HTTP_REFERER'])) {
@@ -520,45 +696,74 @@
   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();
+    }
+  }
 }
 
 /**
- * Validate that a hostname (for example $_SERVER['HTTP_HOST']) is safe.
+ * Validates that a hostname (for example $_SERVER['HTTP_HOST']) is safe.
  *
  * @return
  *  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);
 }
 
 /**
- * Loads the configuration and sets the base URL, cookie domain, and
- * session name correctly.
+ * 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';
+}
+
+/**
+ * Sets the base URL, cookie domain, and session name from configuration.
  */
 function drupal_settings_initialize() {
   global $base_url, $base_path, $base_root;
 
-  // Export the following settings.php variables to the global namespace
+  // Export these settings.php variables to the global namespace.
   global $databases, $cookie_domain, $conf, $installed_profile, $update_free_access, $db_url, $db_prefix, $drupal_hash_salt, $is_https, $base_secure_url, $base_insecure_url;
   $conf = array();
 
   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.
     $parts = parse_url($base_url);
-    $http_protocol = $parts['scheme'];
     if (!isset($parts['path'])) {
       $parts['path'] = '';
     }
@@ -567,7 +772,7 @@
     $base_root = substr($base_url, 0, strlen($base_url) - strlen($parts['path']));
   }
   else {
-    // Create base URL
+    // Create base URL.
     $http_protocol = $is_https ? 'https' : 'http';
     $base_root = $http_protocol . '://' . $_SERVER['HTTP_HOST'];
 
@@ -593,7 +798,7 @@
   }
   else {
     // Otherwise use $base_url as session name, without the protocol
-    // to use the same session identifiers across http and https.
+    // to use the same session identifiers across HTTP and HTTPS.
     list( , $session_name) = explode('://', $base_url, 2);
     // HTTP_HOST can be modified by a visitor, but we already sanitized it
     // in drupal_settings_initialize().
@@ -627,13 +832,14 @@
 }
 
 /**
- * Returns and optionally sets the filename for a system item (module,
- * theme, etc.). The filename, whether provided, cached, or retrieved
- * from the database, is only returned if the file exists.
+ * Returns and optionally sets the filename for a system resource.
+ *
+ * The filename, whether provided, cached, or retrieved from the database, is
+ * only returned if the file exists.
  *
  * This function plays a key role in allowing Drupal's resources (modules
  * and themes) to be located in different places depending on a site's
- * configuration. For example, a module 'foo' may legally be be located
+ * configuration. For example, a module 'foo' may legally be located
  * in any of these three places:
  *
  * modules/foo/foo.module
@@ -644,76 +850,73 @@
  * the above, depending on where the module is located.
  *
  * @param $type
- *   The type of the item (i.e. theme, theme_engine, module, profile).
+ *   The type of the item (theme, theme_engine, module, profile).
  * @param $name
  *   The name of the item for which the filename is requested.
  * @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.
- */
-function drupal_get_filename($type, $name, $filename = NULL) {
+ *   The filename of the requested item or NULL if the item is not found.
+ */
+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();
 
+  // Profiles are a special case: they have a fixed location and naming.
+  if ($type == 'profile') {
+    $profile_filename = "profiles/$name/$name.profile";
+    $files[$type][$name] = file_exists($profile_filename) ? $profile_filename : FALSE;
+  }
   if (!isset($files[$type])) {
     $files[$type] = array();
   }
 
   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;
-      }
-
-      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;
-      }
+      $files[$type][$name] = _drupal_get_filename_fallback($type, $name, $trigger_error, $database_unavailable);
     }
   }
 
@@ -723,11 +926,261 @@
 }
 
 /**
- * Load the persistent variable table.
+ * 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.
+    }
+  }
+
+  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');
+  }
+}
+
+/**
+ * Loads the persistent variable table.
  *
  * The variable table is composed of values that have been saved in the table
- * with variable_set() as well as those explicitly specified in the configuration
- * file.
+ * with variable_set() as well as those explicitly specified in the
+ * configuration file.
  */
 function variable_initialize($conf = array()) {
   // NOTE: caching the variables improves performance by 20% when serving
@@ -772,7 +1225,7 @@
  *   The default value to use if this variable has never been set.
  *
  * @return
- *   The value of the variable.
+ *   The value of the variable. Unserialization is taken care of as necessary.
  *
  * @see variable_del()
  * @see variable_set()
@@ -834,7 +1287,7 @@
 }
 
 /**
- * Retrieve the current page from the cache.
+ * Retrieves the current page from the cache.
  *
  * Note: we do not serve cached pages to authenticated users, or to anonymous
  * users when $_SESSION is non-empty. $_SESSION may contain status messages
@@ -866,10 +1319,10 @@
 }
 
 /**
- * Determine the cacheability of the current page.
+ * 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.
@@ -885,7 +1338,7 @@
 }
 
 /**
- * Invoke a bootstrap hook in all bootstrap modules that implement it.
+ * Invokes a bootstrap hook in all bootstrap modules that implement it.
  *
  * @param $hook
  *   The name of the bootstrap hook to invoke.
@@ -907,8 +1360,9 @@
 }
 
 /**
- * Includes a file with the provided type and name. This prevents
- * including a theme, engine, module, etc., more than once.
+ * Includes a file with the provided type and name.
+ *
+ * This prevents including a theme, engine, module, etc., more than once.
  *
  * @param $type
  *   The type of item to load (i.e. theme, theme_engine, module).
@@ -940,7 +1394,7 @@
 }
 
 /**
- * Set an HTTP response header for the current page.
+ * Sets an HTTP response header for the current page.
  *
  * Note: When sending a Content-Type header, always include a 'charset' type,
  * too. This is necessary to avoid security bugs (e.g. UTF-7 XSS).
@@ -976,11 +1430,12 @@
 }
 
 /**
- * Get the HTTP response headers for the current page.
+ * Gets the HTTP response headers for the current page.
  *
  * @param $name
  *   An HTTP header name. If omitted, all headers are returned as name/value
  *   pairs. If an array value is FALSE, the header has been unset.
+ *
  * @return
  *   A string containing the header value, or FALSE if the header has been set,
  *   or NULL if the header has not been set.
@@ -997,6 +1452,8 @@
 }
 
 /**
+ * Sets the preferred name for the HTTP header.
+ *
  * Header names are case-insensitive, but for maximum compatibility they should
  * follow "common form" (see RFC 2617, section 4.2).
  */
@@ -1010,14 +1467,16 @@
 }
 
 /**
- * Send the HTTP response headers previously set using drupal_add_http_header().
- * Add default headers, unless they have been replaced or unset using
- * drupal_add_http_header().
- *
- * @param $default_headers
- *   An array of headers as name/value pairs.
- * @param $single
- *   If TRUE and headers have already be sent, send only the specified header.
+ * Sends the HTTP response headers that were previously set, adding defaults.
+ *
+ * Headers are set in drupal_add_http_header(). Default headers are not set
+ * if they have been replaced or unset using drupal_add_http_header().
+ *
+ * @param array $default_headers
+ *   (optional) An array of headers as name/value pairs.
+ * @param bool $only_default
+ *   (optional) If TRUE and headers have already been sent, send only the
+ *   specified headers.
  */
 function drupal_send_headers($default_headers = array(), $only_default = FALSE) {
   $headers_sent = &drupal_static(__FUNCTION__, FALSE);
@@ -1040,36 +1499,23 @@
       header($_SERVER['SERVER_PROTOCOL'] . ' ' . $value);
     }
     // Skip headers that have been unset.
-    elseif ($value) {
+    elseif ($value !== FALSE) {
       header($header_names[$name_lower] . ': ' . $value);
     }
   }
 }
 
 /**
- * Set HTTP headers in preparation for a page response.
+ * Sets HTTP headers in preparation for a page response.
  *
  * Authenticated users are always given a 'no-cache' header, and will fetch a
  * 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()
  */
@@ -1082,15 +1528,17 @@
 
   $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);
 }
 
 /**
- * Set HTTP headers in preparation for a cached page response.
+ * Sets HTTP headers in preparation for a cached page response.
  *
  * The headers allow as much as possible in proxies and browsers without any
  * particular knowledge about the pages. Modules can override these headers
@@ -1102,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.
@@ -1123,13 +1571,12 @@
     }
   }
 
-  // If a cache is served from a HTTP proxy without hitting the web server,
-  // the boot and exit hooks cannot be fired, so only allow caching in
-  // proxies if boot hooks are disabled. If the client send a session cookie,
-  // do not bother caching the page in a public proxy, because the cached copy
-  // will only be served to that particular user due to Vary: Cookie, unless
-  // the Vary header has been replaced or unset in hook_boot() (see below).
-  $max_age = !variable_get('page_cache_invoke_hooks', TRUE) && (!isset($_COOKIE[session_name()]) || isset($hook_boot_headers['vary'])) ? variable_get('page_cache_maximum_age', 0) : 0;
+  // If the client sent a session cookie, a cached copy will only be served
+  // to that one particular client due to Vary: Cookie. Thus, do not set
+  // max-age > 0, allowing the page to be cached by external proxies, when a
+  // session cookie is present unless the Vary header has been replaced or
+  // unset in hook_boot().
+  $max_age = !isset($_COOKIE[session_name()]) || isset($hook_boot_headers['vary']) ? variable_get('page_cache_maximum_age', 0) : 0;
   $default_headers['Cache-Control'] = 'public, max-age=' . $max_age;
 
   // Entity tag should change if the output changes.
@@ -1153,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
@@ -1194,7 +1641,7 @@
 }
 
 /**
- * Define the critical hooks that force modules to always be loaded.
+ * Defines the critical hooks that force modules to always be loaded.
  */
 function bootstrap_hooks() {
   return array('boot', 'exit', 'watchdog', 'language_init');
@@ -1222,185 +1669,80 @@
 /**
  * Translates a string to the current language or to a given language.
  *
- * All human-readable text that will be displayed on the site or sent to a user
- * should be passed through the t() function. This ensures that sites can be
- * fully translated into other languages.
- *
- * Here are some examples of translating static text using t():
- * @code
- *   if (!$info || !$info['extension']) {
- *     form_set_error('picture_upload', t('The uploaded file was not an image.'));
- *   }
- *
- *   $form['submit'] = array(
- *     '#type' => 'submit',
- *     '#value' => t('Log in'),
- *   );
- * @endcode
- *
- * In addition to translating static text, t() can handle text that should not
- * be translated or that might change from time to time (such as link paths)
- * and dynamic text from variables, using special "placeholders". There are
- * three styles of placeholders:
- * - !variable: Indicates that the text should be inserted as-is. This is
- *   useful for inserting variables into things like e-mail. Example:
- *   @code
- *     $message[] = t("If you don't want to receive such e-mails, you can change your settings at !url.", array('!url' => url("user/$account->uid", array('absolute' => TRUE))));
- *   @endcode
- * - @variable: Indicates that the text should be run through check_plain(), to
- *   escape HTML characters. Use this for any output that is displayed within a
- *   Drupal page. Example:
- *   @code
- *     drupal_set_title($title = t("@name's blog", array('@name' => format_username($account))), PASS_THROUGH);
- *   @endcode
- * - %variable: Indicates that the string should be HTML-escaped and highlighted
- *   with drupal_placeholder(), which shows up as emphasized.
- *   @code
- *     $message = t('%name-from sent %name-to an e-mail.', array('%name-from' => format_username($user), '%name-to' => format_username($account)));
- *   @endcode
- *
- * When using t(), try to put entire paragraphs in one t() call. This makes it
- * easier for translators, as it provides context as to what each word refers
- * to (and also allows translators to adjust word order, which may not be the
- * same in all languages). HTML markup within translation strings is allowed,
- * but should be avoided if possible. The exception is embedded links: link
- * titles add context for translators and need to be translated, so they should
- * be kept in the main string, while link URLs should be generated using
- * placeholders.
- * - Incorrect HTML in t():
- *   @code
- *     $output .= t('

Go to the @contact-page.

', array('@contact-page' => l(t('contact page'), 'contact'))); - * @endcode - * - Correct HTML in t(): - * @code - * $output .= '

' . t('Go to the contact page.', array('@contact-page' => url('contact'))) . '

'; - * @endcode - * - * Another thing that is helpful is to avoid escaping quotation marks wherever - * possible, because it can be confusing to translation teams. - * - Less desirable quotation mark escaping: - * @code - * $output .= t('Don\'t click me.'); - * @endcode - * - Better way to use quotation marks: - * @code - * $output .= t("Don't click me."); - * @endcode - * - * It is important that all translation uses the t() mechanism, because in - * addition to actually translating the text at run-time, the t() function is - * also used by text-extraction routines to find text that needs to be - * translated, and build databases of text to be translated for translation - * teams. For that reason, you must put the actual string into the t() function, - * in most cases, and not a variable. - * - Incorrect use of a variable in t(): - * @code - * $message = 'An error occurred.'; - * drupal_set_message(t($message), 'error'); - * $output .= t($message); - * @endcode - * - Correct translation of a variable with t(): - * @code - * $message = t('An error occurred.'); - * drupal_set_message($message, 'error'); - * $output .= $message; - * @endcode - * - * The only case in which variables can be passed safely through t() is when - * code-based versions of the same strings will be passed through t() (or - * otherwise extracted) elsewhere. - * - * Also, you cannot use t() early in the bootstrap process, prior to the - * DRUPAL_BOOTSTRAP_LANGUAGE phase. The language variables will not be - * initialized yet, so the string will not be translated into the correct - * language. Examples of places where t() cannot be used include: - * - In a PHP define() statement. - * - In a hook_boot() implementation. - * - * In some cases, modules may include strings in code that can't use t() - * calls. For example, a module may use an external PHP application that - * produces strings that are loaded into variables in Drupal for output. - * In these cases, module authors may include a dummy file that passes the - * relevant strings through t(). This approach will allow the strings to be - * extracted. - * - * Sample external (non-Drupal) code: - * @code - * class Time { - * public $yesterday = 'Yesterday'; - * public $today = 'Today'; - * public $tomorrow = 'Tomorrow'; - * } - * @endcode - * - * Sample dummy file: + * The t() function serves two purposes. First, at run-time it translates + * user-visible text into the appropriate language. Second, various mechanisms + * that figure out what text needs to be translated work off t() -- the text + * inside t() calls is added to the database of strings to be translated. + * These strings are expected to be in English, so the first argument should + * always be in English. To enable a fully-translatable site, it is important + * that all human-readable text that will be displayed on the site or sent to + * a user is passed through the t() function, or a related function. See the + * @link http://drupal.org/node/322729 Localization API @endlink pages for + * more information, including recommendations on how to break up or not + * break up strings for translation. + * + * @section sec_translating_vars Translating Variables + * You should never use t() to translate variables, such as calling + * @code t($text); @endcode, unless the text that the variable holds has been + * passed through t() elsewhere (e.g., $text is one of several translated + * literal strings in an array). It is especially important never to call + * @code t($user_text); @endcode, where $user_text is some text that a user + * entered - doing that can lead to cross-site scripting and other security + * problems. However, you can use variable substitution in your string, to put + * variable text such as user names or link URLs into translated text. Variable + * substitution looks like this: * @code - * // Dummy function included in example.potx.inc. - * function example_potx() { - * $strings = array( - * t('Yesterday'), - * t('Today'), - * t('Tomorrow'), - * ); - * // No return value needed, since this is a dummy function. - * } + * $text = t("@name's blog", array('@name' => format_username($account))); * @endcode - * - * Having passed strings through t() in a dummy function, it is then - * possible to pass variables through t(): + * Basically, you can put variables like @name into your string, and t() will + * substitute their sanitized values at translation time. (See the + * Localization API pages referenced above and the documentation of + * format_string() for details about how to define variables in your string.) + * Translators can then rearrange the string as necessary for the language + * (e.g., in Spanish, it might be "blog de @name"). + * + * @section sec_alt_funcs_install Use During Installation Phase + * During the Drupal installation phase, some resources used by t() wil not be + * 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 - * $time = new Time(); - * $output .= t($time->today); + * t('May', array(), array('context' => 'Long month name') + * t('May', array(), array('context' => 'Abbreviated month name') * @endcode - * - * However tempting it is, custom data from user input or other non-code - * sources should not be passed through t(). Doing so leads to the following - * problems and errors: - * - The t() system doesn't support updates to existing strings. When user - * data is updated, the next time it's passed through t(), a new record is - * created instead of an update. The database bloats over time and any - * existing translations are orphaned with each update. - * - The t() system assumes any data it receives is in English. User data may - * be in another language, producing translation errors. - * - The "Built-in interface" text group in the locale system is used to - * produce translations for storage in .po files. When non-code strings are - * passed through t(), they are added to this text group, which is rendered - * inaccurate since it is a mix of actual interface strings and various user - * input strings of uncertain origin. - * Instead, translation of these data can be done through the locale system, - * either directly through hook_local() or through helper functions provided by - * contributed modules. - * - * Incorrect: - * @code - * $item = item_load(); - * $output .= check_plain(t($item['title'])); - * @endcode - * - * During installation, st() is used in place of t(). Code that may be called - * during installation or during normal operation should use the get_t() - * helper function. + * See https://localize.drupal.org/node/2109 for more information. * * @param $string * A string containing the English string to translate. * @param $args - * An associative array of replacements to make after translation. Incidences - * of any key in this array are replaced with the corresponding value. Based - * on the first character of the key, the value is escaped and/or themed: - * - !variable: inserted as is - * - @variable: escape plain text to HTML (using check_plain()) - * - %variable: escape text and theme as a placeholder for user-submitted - * content (using check_plain() + drupal_placeholder()) + * An associative array of replacements to make after translation. Based + * on the first character of the key, the value is escaped and/or themed. + * See format_string() for details. * @param $options - * An associative array of additional options, with the following keys: - * - '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. + * 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): A string giving the context + * that the source string belongs to. See @ref sec_context above for more + * information. * * @return * The translated string. * + * @see st() + * @see get_t() + * @see format_string() * @ingroup sanitization */ function t($string, array $args = array(), array $options = array()) { @@ -1434,40 +1776,78 @@ return $string; } else { - // Transform arguments before inserting them. - foreach ($args as $key => $value) { - switch ($key[0]) { - case '@': - // Escaped only. - $args[$key] = check_plain($value); - break; + return format_string($string, $args); + } +} - case '%': - default: - // Escaped and placeholder. - $args[$key] = drupal_placeholder($value); - break; +/** + * Formats a string for HTML display by replacing variable placeholders. + * + * This function replaces variable placeholders in a string with the requested + * values and escapes the values so they can be safely displayed as HTML. It + * should be used on any unknown text that is intended to be printed to an HTML + * page (especially text that may have come from untrusted users, since in that + * case it prevents cross-site scripting and other security problems). + * + * In most cases, you should use t() rather than calling this function + * directly, since it will translate the text (on non-English-only sites) in + * addition to formatting it. + * + * @param $string + * A string containing placeholders. + * @param $args + * An associative array of replacements to make. Occurrences in $string of + * any key in $args are replaced with the corresponding value, after optional + * sanitization and formatting. The type of sanitization and formatting + * depends on the first character of the key: + * - @variable: Escaped to HTML using check_plain(). Use this as the default + * choice for anything displayed on a page on the site. + * - %variable: Escaped to HTML and formatted using drupal_placeholder(), + * which makes it display as emphasized text. + * - !variable: Inserted as is, with no sanitization or formatting. Only use + * this for text that has already been prepared for HTML display (for + * example, user-supplied text that has already been run through + * check_plain() previously, or is expected to contain some limited HTML + * tags and has already been run through filter_xss() previously). + * + * @see t() + * @ingroup sanitization + */ +function format_string($string, array $args = array()) { + // Transform arguments before inserting them. + foreach ($args as $key => $value) { + switch ($key[0]) { + case '@': + // Escaped only. + $args[$key] = check_plain($value); + break; - case '!': - // Pass-through. - } + case '%': + default: + // Escaped and placeholder. + $args[$key] = drupal_placeholder($value); + break; + + case '!': + // Pass-through. } - return strtr($string, $args); } + return strtr($string, $args); } /** - * Encode special characters in a plain-text string for display as HTML. + * Encodes special characters in a plain-text string for display as HTML. * * 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 @@ -1496,6 +1876,7 @@ * * @param $text * The text to check. + * * @return * TRUE if the text is valid UTF-8, FALSE if not. */ @@ -1510,11 +1891,12 @@ } /** - * Since $_SERVER['REQUEST_URI'] is only available on Apache, we - * generate an equivalent using other environment variables. + * Returns the equivalent of Apache's $_SERVER['REQUEST_URI'] variable. + * + * Because $_SERVER['REQUEST_URI'] is only available on Apache, we generate an + * equivalent using other environment variables. */ function request_uri() { - if (isset($_SERVER['REQUEST_URI'])) { $uri = $_SERVER['REQUEST_URI']; } @@ -1536,7 +1918,7 @@ } /** - * Log an exception. + * Logs an exception. * * This is a wrapper function for watchdog() which automatically decodes an * exception. @@ -1547,17 +1929,17 @@ * The exception that is going to be logged. * @param $message * The message to store in the log. If empty, a text that contains all useful - * information about the passed in exception is used. + * 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) { @@ -1577,7 +1959,7 @@ } /** - * Log a system message. + * Logs a system message. * * @param $type * The category to which this message belongs. Can be any string, but the @@ -1593,8 +1975,16 @@ * NULL if message is already translated or not possible to * translate. * @param $severity - * The severity of the message, as per RFC 3164. Possible values are - * WATCHDOG_ERROR, WATCHDOG_WARNING, etc. + * The severity of the message; one of the following values as defined in + * @link http://www.faqs.org/rfcs/rfc3164.html RFC 3164: @endlink + * - WATCHDOG_EMERGENCY: Emergency, system is unusable. + * - WATCHDOG_ALERT: Alert, action must be taken immediately. + * - WATCHDOG_CRITICAL: Critical conditions. + * - WATCHDOG_ERROR: Error conditions. + * - WATCHDOG_WARNING: Warning conditions. + * - WATCHDOG_NOTICE: (default) Normal but significant conditions. + * - WATCHDOG_INFO: Informational messages. + * - WATCHDOG_DEBUG: Debug-level messages. * @param $link * A link to associate with the message. * @@ -1611,6 +2001,9 @@ if (!$in_error_state && function_exists('module_implements')) { $in_error_state = TRUE; + // The user object may not exist in all conditions, so 0 is substituted if needed. + $user_uid = isset($user->uid) ? $user->uid : 0; + // Prepare the fields to be logged $log_entry = array( 'type' => $type, @@ -1619,10 +2012,12 @@ 'severity' => $severity, 'link' => $link, 'user' => $user, + 'uid' => $user_uid, 'request_uri' => $base_root . request_uri(), 'referer' => isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '', 'ip' => ip_address(), - 'timestamp' => REQUEST_TIME, + // Request time isn't accurate for long processes, use time() instead. + 'timestamp' => time(), ); // Call the logging hooks to log/process the message @@ -1637,25 +2032,40 @@ } /** - * Set a message which reflects the status of the performed operation. + * Sets a message to display to the user. * - * If the function is called with no arguments, this function returns all set - * messages without clearing them. + * Messages are stored in a session variable and displayed in page.tpl.php via + * the $messages theme variable. * - * @param $message - * The message should begin with a capital letter and always ends with a - * period '.'. - * @param $type - * The type of the message. One of the following values are possible: + * Example usage: + * @code + * drupal_set_message(t('An error occurred and processing did not complete.'), 'error'); + * @endcode + * + * @param string $message + * (optional) The translated message to be displayed to the user. For + * consistency with other messages, it should begin with a capital letter and + * end with a period. + * @param string $type + * (optional) The message's type. Defaults to 'status'. These values are + * supported: * - 'status' * - 'warning' * - 'error' - * @param $repeat - * If this is FALSE and the message is already set, then the message won't - * be repeated. + * @param bool $repeat + * (optional) If this is FALSE and the message is already set, then the + * message won't be repeated. Defaults to TRUE. + * + * @return array|null + * A multidimensional array with keys corresponding to the set message types. + * The indexed array values of each contain the set messages for that type. + * Or, if there are no messages set, the function returns NULL. + * + * @see drupal_get_messages() + * @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(); } @@ -1673,17 +2083,29 @@ } /** - * Return all messages that have been set. + * Returns all messages that have been set with drupal_set_message(). * - * @param $type - * (optional) Only return messages of this type. - * @param $clear_queue - * (optional) Set to FALSE if you do not want to clear the messages queue - * @return - * An associative array, the key is the message type, the value an array - * of messages. If the $type parameter is passed, you get only that type, - * or an empty array if there are no such messages. If $type is not passed, - * all message types are returned, or an empty array if none exist. + * @param string $type + * (optional) Limit the messages returned by type. Defaults to NULL, meaning + * all types. These values are supported: + * - NULL + * - 'status' + * - 'warning' + * - 'error' + * @param bool $clear_queue + * (optional) If this is TRUE, the queue will be cleared of messages of the + * type specified in the $type parameter. Otherwise the queue will be left + * intact. Defaults to TRUE. + * + * @return array + * A multidimensional array with keys corresponding to the set message types. + * The indexed array values of each contain the set messages for that type. + * The messages returned are limited to the type specified in the $type + * parameter. If there are no messages of the specified type, an empty array + * is returned. + * + * @see drupal_set_message() + * @see theme_status_messages() */ function drupal_get_messages($type = NULL, $clear_queue = TRUE) { if ($messages = drupal_set_message()) { @@ -1706,7 +2128,9 @@ } /** - * Get the title of the current page, for display on the page and in the title bar. + * Gets the title of the current page. + * + * The title is displayed on the page and in the title bar. * * @return * The current page's title. @@ -1723,7 +2147,9 @@ } /** - * Set the title of the current page, for display on the page and in the title bar. + * Sets the title of the current page. + * + * The title is displayed on the page and in the title bar. * * @param $title * Optional string value to assign to the page title; or if set to NULL @@ -1748,7 +2174,7 @@ } /** - * Check to see if an IP address has been blocked. + * Checks to see if an IP address has been blocked. * * Blocked IP addresses are stored in the database by default. However for * performance reasons we allow an override in settings.php. This allows us @@ -1757,6 +2183,7 @@ * * @param $ip * IP address to check. + * * @return bool * TRUE if access is denied, FALSE if access is allowed. */ @@ -1782,7 +2209,7 @@ } /** - * Handle denied users. + * Handles denied users. * * @param $ip * IP address to check. Prints a message and exits if access is denied. @@ -1797,39 +2224,73 @@ } /** + * 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 * PHP function because it can return a long string of bytes (compared to < 4 - * bytes normally from mt_rand()) and uses the best available pseudo-random source. + * bytes normally from mt_rand()) and uses the best available pseudo-random + * source. * * @param $count * The number of characters (bytes) to return in the string. */ function drupal_random_bytes($count) { // $random_state does not use drupal_static as it stores random bytes. - static $random_state, $bytes; - // 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) { - // /dev/urandom is available on many *nix systems and is considered the - // best commonly available pseudo-random source. - if ($fh = @fopen('/dev/urandom', 'rb')) { + 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($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); + } + + // 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); } - // 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() @@ -1837,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); @@ -1848,25 +2323,29 @@ } /** - * Calculate a base-64 encoded, URL-safe sha-256 hmac. + * 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('+' => '-', '/' => '_', '=' => '')); } /** - * Calculate a base-64 encoded, URL-safe sha-256 hash. + * Calculates a base-64 encoded, URL-safe sha-256 hash. * * @param $data * String to be hashed. @@ -1909,7 +2388,8 @@ * @see drupal_array_merge_deep_array() */ function drupal_array_merge_deep() { - return drupal_array_merge_deep_array(func_get_args()); + $args = func_get_args(); + return drupal_array_merge_deep_array($args); } /** @@ -1960,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(); @@ -1970,20 +2450,34 @@ } /** - * A string describing a phase of Drupal to load. Each phase adds to the - * previous one, so invoking a later phase automatically runs the earlier - * phases too. The most important usage is that if you want to access the - * Drupal database from a script without loading anything else, you can - * include bootstrap.inc, and call drupal_bootstrap(DRUPAL_BOOTSTRAP_DATABASE). - * - * @param $phase - * A constant. Allowed values are the DRUPAL_BOOTSTRAP_* constants. - * @param $new_phase + * Ensures Drupal is bootstrapped to the specified phase. + * + * In order to bootstrap Drupal from another PHP script, you can use this code: + * @code + * define('DRUPAL_ROOT', '/path/to/drupal'); + * require_once DRUPAL_ROOT . '/includes/bootstrap.inc'; + * drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL); + * @endcode + * + * @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: + * - DRUPAL_BOOTSTRAP_CONFIGURATION: Initializes configuration. + * - DRUPAL_BOOTSTRAP_PAGE_CACHE: Tries to serve a cached page. + * - DRUPAL_BOOTSTRAP_DATABASE: Initializes the database layer. + * - DRUPAL_BOOTSTRAP_VARIABLES: Initializes the variable system. + * - DRUPAL_BOOTSTRAP_SESSION: Initializes session handling. + * - DRUPAL_BOOTSTRAP_PAGE_HEADER: Sets up the page header. + * - DRUPAL_BOOTSTRAP_LANGUAGE: Finds out the language of the page. + * - DRUPAL_BOOTSTRAP_FULL: Fully loads Drupal. Validates and fixes input + * data. + * @param boolean $new_phase * A boolean, set to FALSE if calling drupal_bootstrap from inside a * function called from drupal_bootstrap (recursion). - * @return - * The most recently completed phase. * + * @return int + * The most recently completed phase. */ function drupal_bootstrap($phase = NULL, $new_phase = TRUE) { // Not drupal_static(), because does not depend on any run-time information. @@ -2004,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) { @@ -2062,7 +2557,7 @@ } /** - * Return the time zone of the current user. + * Returns the time zone of the current user. */ function drupal_get_user_timezone() { global $user; @@ -2077,7 +2572,20 @@ } /** - * Custom PHP error handler. + * Gets a salt useful for hardening against SQL injection. + * + * @return + * A salt based on information in settings.php, not in the database. + */ +function drupal_get_hash_salt() { + global $drupal_hash_salt, $databases; + // If the $drupal_hash_salt variable is empty, a hash of the serialized + // database credentials is used as a fallback salt. + return empty($drupal_hash_salt) ? hash('sha256', serialize($databases)) : $drupal_hash_salt; +} + +/** + * Provides custom PHP error handling. * * @param $error_level * The level of the error raised. @@ -2088,7 +2596,8 @@ * @param $line * The line number the error was raised at. * @param $context - * An array that points to the active symbol table at the point the error occurred. + * An array that points to the active symbol table at the point the error + * occurred. */ function _drupal_error_handler($error_level, $message, $filename, $line, $context) { require_once DRUPAL_ROOT . '/includes/errors.inc'; @@ -2096,7 +2605,7 @@ } /** - * Custom PHP exception handler. + * Provides custom PHP exception handling. * * Uncaught exceptions are those not enclosed in a try/catch block. They are * always fatal: the execution of the script will stop as soon as the exception @@ -2124,7 +2633,7 @@ } /** - * Bootstrap configuration: Setup script environment and load settings.php. + * Sets up the script environment and loads settings.php. */ function _drupal_bootstrap_configuration() { // Set the Drupal custom error handler. @@ -2136,10 +2645,14 @@ 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(); } /** - * Bootstrap page cache: Try to serve a page from cache. + * Attempts to serve a page from the cache. */ function _drupal_bootstrap_page_cache() { global $user; @@ -2195,7 +2708,7 @@ } /** - * Bootstrap database: Initialize database system and register autoload functions. + * Initializes the database system and registers autoload functions. */ function _drupal_bootstrap_database() { // Redirect the user to the installation script if Drupal has not been @@ -2244,10 +2757,13 @@ // 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'); + } } /** - * Bootstrap variables: Load system variables and all enabled bootstrap modules. + * Loads system variables and all enabled bootstrap modules. */ function _drupal_bootstrap_variables() { global $conf; @@ -2261,10 +2777,35 @@ // 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']); + } + } } /** - * Bootstrap page header: Invoke hook_boot(), initialize locking system, and send default HTTP headers. + * Invokes hook_boot(), initializes locking system, and sends HTTP headers. */ function _drupal_bootstrap_page_header() { bootstrap_invoke_all('boot'); @@ -2283,12 +2824,11 @@ * @see drupal_bootstrap() */ function drupal_get_bootstrap_phase() { - return drupal_bootstrap(); + return drupal_bootstrap(NULL, FALSE); } /** - * Checks the current User-Agent string to see if this is an internal request - * from SimpleTest. If so, returns the test prefix for this test. + * Returns the test prefix if this is an internal request from SimpleTest. * * @return * Either the simpletest prefix (the string "simpletest" followed by any @@ -2296,7 +2836,6 @@ * HMAC and timestamp. */ function drupal_valid_test_ua() { - global $drupal_hash_salt; // No reason to reset this. static $test_prefix; @@ -2310,7 +2849,7 @@ // We use the salt from settings.php to make the HMAC key, since // the database is not yet initialized and we can't access any Drupal variables. // The file properties add more entropy not easily accessible to others. - $key = $drupal_hash_salt . filectime(__FILE__) . fileinode(__FILE__); + $key = drupal_get_hash_salt() . filectime(__FILE__) . fileinode(__FILE__); $time_diff = REQUEST_TIME - $time; // Since we are making a local request a 5 second time window is allowed, // and the HMAC must match. @@ -2320,21 +2859,21 @@ } } - return FALSE; + $test_prefix = FALSE; + return $test_prefix; } /** - * Generate a user agent string with a HMAC and timestamp for simpletest. + * Generates a user agent string with a HMAC and timestamp for simpletest. */ function drupal_generate_test_ua($prefix) { - global $drupal_hash_salt; static $key; if (!isset($key)) { // We use the salt from settings.php to make the HMAC key, since // the database is not yet initialized and we can't access any Drupal variables. // The file properties add more entropy not easily accessible to others. - $key = $drupal_hash_salt . filectime(__FILE__) . fileinode(__FILE__); + $key = drupal_get_hash_salt() . filectime(__FILE__) . fileinode(__FILE__); } // Generate a moderately secure HMAC based on the database credentials. $salt = uniqid('', TRUE); @@ -2356,15 +2895,65 @@ } /** - * Return TRUE if a Drupal installation is currently being attempted. + * Returns a simple 404 Not Found page. + * + * If fast 404 pages are enabled, and this is a matching page then print a + * simple 404 page and exit. + * + * This function is called from drupal_deliver_html_page() at the time when a + * a normal 404 page is generated, but it can also optionally be called directly + * from settings.php to prevent a Drupal bootstrap on these pages. See + * documentation in settings.php for the benefits and drawbacks of using this. + * + * Paths to dynamically-generated content, such as image styles, should also be + * accounted for in this function. + */ +function drupal_fast_404() { + $exclude_paths = variable_get('404_fast_paths_exclude', FALSE); + if ($exclude_paths && !preg_match($exclude_paths, $_GET['q'])) { + $fast_paths = variable_get('404_fast_paths', FALSE); + if ($fast_paths && preg_match($fast_paths, $_GET['q'])) { + drupal_add_http_header('Status', '404 Not Found'); + $fast_404_html = variable_get('404_fast_html', '404 Not Found

Not Found

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

'); + // Replace @path in the variable with the page path. + print strtr($fast_404_html, array('@path' => check_plain(request_uri()))); + exit; + } + } +} + +/** + * Returns TRUE if a Drupal installation is currently being attempted. */ function drupal_installation_attempted() { return defined('MAINTENANCE_MODE') && MAINTENANCE_MODE == 'install'; } /** - * Return the name of the localization function. Use in code that needs to - * run both during installation and normal operation. + * Returns the name of the proper localization function. + * + * get_t() exists to support localization for code that might run during + * the installation phase, when some elements of the system might not have + * loaded. + * + * 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 + * module administration page. + * + * Example usage: + * @code + * $t = get_t(); + * $translated = $t('translate this'); + * @endcode + * + * Use t() if your code will never run during the Drupal installation phase. + * Use st() if your code will only run during installation and never any other + * time. Use get_t() if your code could run in either circumstance. + * + * @see t() + * @see st() + * @ingroup sanitization */ function get_t() { static $t; @@ -2377,7 +2966,7 @@ } /** - * Initialize all the defined language types. + * Initializes all the defined language types. */ function drupal_language_initialize() { $types = language_types(); @@ -2402,7 +2991,7 @@ } /** - * The built-in language types. + * Returns a list of the built-in language types. * * @return * An array of key-values pairs where the key is the language type and the @@ -2417,23 +3006,42 @@ } /** - * Return true if there is more than one language enabled. + * Returns TRUE if there is more than one language enabled. + * + * @return + * TRUE if more than one language is enabled. */ function drupal_multilingual() { + // The "language_count" variable stores the number of enabled languages to + // avoid unnecessarily querying the database when building the list of + // enabled languages on monolingual sites. return variable_get('language_count', 1) > 1; } /** - * Return an array of the available language types. + * Returns an array of the available language types. + * + * @return + * An array of all language types where the keys of each are the language type + * name and its value is its configurability (TRUE/FALSE). */ function language_types() { return array_keys(variable_get('language_types', drupal_language_types())); } /** - * Get a list of languages set up indexed by the specified key + * Returns a list of installed languages, indexed by the specified key. * - * @param $field The field to index the list with. + * @param $field + * (optional) The field to index the list with. + * + * @return + * An associative array, keyed on the values of $field. + * - If $field is 'weight' or 'enabled', the array is nested, with the outer + * array's values each being associative arrays with language codes as + * keys and language objects as values. + * - For all other values of $field, the array is only one level deep, and + * the array's values are language objects. */ function language_list($field = 'language') { $languages = &drupal_static(__FUNCTION__); @@ -2472,10 +3080,14 @@ } /** - * 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' => '')); @@ -2491,6 +3103,8 @@ * base_path() returns "/drupalfolder/". * - http://example.com/path/alias (which is a path alias for node/306) returns * "path/alias" as opposed to the internal path. + * - http://example.com/index.php returns an empty string (meaning: front page). + * - http://example.com/index.php?page=1 returns an empty string. * * @return * The requested Drupal URL path. @@ -2504,7 +3118,7 @@ return $path; } - if (isset($_GET['q'])) { + if (isset($_GET['q']) && is_string($_GET['q'])) { // This is a request with a ?q=foo/bar query string. $_GET['q'] is // overwritten in drupal_path_initialize(), but request_path() is called // very early in the bootstrap process, so the original value is saved in @@ -2512,11 +3126,19 @@ $path = $_GET['q']; } elseif (isset($_SERVER['REQUEST_URI'])) { - // This is a request using a clean URL. Extract the path from REQUEST_URI. + // This request is either a clean URL, or 'index.php', or nonsense. + // Extract the path from REQUEST_URI. $request_path = strtok($_SERVER['REQUEST_URI'], '?'); $base_path_len = strlen(rtrim(dirname($_SERVER['SCRIPT_NAME']), '\/')); // Unescape and strip $base_path prefix, leaving q without a leading slash. $path = substr(urldecode($request_path), $base_path_len + 1); + // If the path equals the script filename, either because 'index.php' was + // explicitly provided in the URL, or because the server added it to + // $_SERVER['REQUEST_URI'] even when it wasn't provided in the URL (some + // versions of Microsoft IIS do this), the front page should be served. + if ($path == basename($_SERVER['PHP_SELF'])) { + $path = ''; + } } else { // This is the front page. @@ -2532,16 +3154,16 @@ } /** - * Return a component of the current Drupal path. + * Returns a component of the current Drupal path. * * When viewing a page at the path "admin/structure/types", for example, arg(0) * returns "admin", arg(1) returns "structure", and arg(2) returns "types". * - * Avoid use of this function where possible, as resulting code is hard to read. - * In menu callback functions, attempt to use named arguments. See the explanation - * in menu.inc for how to construct callbacks that take arguments. When attempting - * to use this function to load an element from the current path, e.g. loading the - * node on a node page, please use menu_get_object() instead. + * Avoid use of this function where possible, as resulting code is hard to + * read. In menu callback functions, attempt to use named arguments. See the + * explanation in menu.inc for how to construct callbacks that take arguments. + * When attempting to use this function to load an element from the current + * path, e.g. loading the node on a node page, use menu_get_object() instead. * * @param $index * The index of the component, where each component is separated by a '/' @@ -2581,6 +3203,8 @@ } /** + * Returns the IP address of the client machine. + * * If Drupal is behind a reverse proxy, we use the X-Forwarded-For header * instead of $_SERVER['REMOTE_ADDR'], which would be the IP address of * the proxy server, and not the client's. The actual header name can be @@ -2615,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); + } } } } @@ -2625,15 +3256,17 @@ } /** - * @ingroup schemaapi + * @addtogroup schemaapi * @{ */ /** - * Get the schema definition of a table, or the whole database schema. + * 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. @@ -2641,6 +3274,61 @@ * If true, the schema will be rebuilt instead of retrieved from the cache. */ function drupal_get_schema($table = NULL, $rebuild = FALSE) { + static $schema; + + if ($rebuild || !isset($table)) { + $schema = drupal_get_complete_schema($rebuild); + } + elseif (!isset($schema)) { + $schema = new SchemaCache(); + } + + if (!isset($table)) { + return $schema; + } + if (isset($schema[$table])) { + return $schema[$table]; + } + else { + return FALSE; + } +} + +/** + * Extends DrupalCacheArray to allow for dynamic building of the schema cache. + */ +class SchemaCache extends DrupalCacheArray { + + /** + * Constructs a SchemaCache object. + */ + public function __construct() { + // Cache by request method. + parent::__construct('schema:runtime:' . ($_SERVER['REQUEST_METHOD'] == 'GET'), 'cache'); + } + + /** + * Overrides DrupalCacheArray::resolveCacheMiss(). + */ + protected function resolveCacheMiss($offset) { + $complete_schema = drupal_get_complete_schema(); + $value = isset($complete_schema[$offset]) ? $complete_schema[$offset] : NULL; + $this->storage[$offset] = $value; + $this->persist($offset); + return $value; + } +} + +/** + * Gets the whole database schema. + * + * The returned schema will include any modifications made by any + * module that implements hook_schema_alter(). + * + * @param $rebuild + * If true, the schema will be rebuilt instead of retrieved from the cache. + */ +function drupal_get_complete_schema($rebuild = FALSE) { static $schema = array(); if (empty($schema) || $rebuild) { @@ -2682,38 +3370,34 @@ if (!empty($schema) && (drupal_get_bootstrap_phase() == DRUPAL_BOOTSTRAP_FULL)) { cache_set('schema', $schema); } + if ($rebuild) { + cache_clear_all('schema:', 'cache', TRUE); + } } } - if (!isset($table)) { - return $schema; - } - elseif (isset($schema[$table])) { - return $schema[$table]; - } - else { - return FALSE; - } + return $schema; } /** - * @} End of "ingroup schemaapi". + * @} End of "addtogroup schemaapi". */ /** - * @ingroup registry + * @addtogroup registry * @{ */ /** - * Confirm that an interface is available. + * Confirms that an interface 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 $interface * The name of the interface to check or load. + * * @return * TRUE if the interface is currently available, FALSE otherwise. */ @@ -2722,13 +3406,14 @@ } /** - * Confirm that a class is available. + * Confirms that a class 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 $class * The name of the class to check or load. + * * @return * TRUE if the class is currently available, FALSE otherwise. */ @@ -2737,7 +3422,23 @@ } /** - * Helper to check for a resource in the registry. + * 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 * The type of resource we are looking up, or one of the constants @@ -2746,6 +3447,7 @@ * @param $name * The name of the resource, or NULL if either of the REGISTRY_* constants * is passed in. + * * @return * TRUE if the resource was found, FALSE if not. * NULL if either of the REGISTRY_* constants is passed in as $type. @@ -2753,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; } @@ -2786,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]; } @@ -2794,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. @@ -2808,7 +3513,7 @@ $lookup_cache[$cache_key] = $file; if ($file) { - require_once DRUPAL_ROOT . '/' . $file; + include_once DRUPAL_ROOT . '/' . $file; return TRUE; } else { @@ -2817,7 +3522,7 @@ } /** - * Rescan all enabled modules and rebuild the registry. + * Rescans all enabled modules and rebuilds the registry. * * Rescans all code in modules or includes directories, storing the location of * each interface or class in the database. @@ -2828,25 +3533,44 @@ } /** - * Update the registry based on the latest files listed in the database. + * Updates the registry based on the latest files listed in the database. * * This function should be used when system_rebuild_module_data() does not need * to be called, because it is already known that the list of files in the * {system} table matches those in the file system. * + * @return + * TRUE if the registry was rebuilt, FALSE if another thread was rebuilding + * in parallel and the current thread just waited for completion. + * * @see registry_rebuild() */ function registry_update() { + // install_system_module() calls module_enable() which calls into this + // function during initial system installation, so the lock system is neither + // loaded nor does its storage exist yet. + $in_installer = drupal_installation_attempted(); + if (!$in_installer && !lock_acquire(__FUNCTION__)) { + // Another request got the lock, wait for it to finish. + lock_wait(__FUNCTION__); + return FALSE; + } + require_once DRUPAL_ROOT . '/includes/registry.inc'; _registry_update(); + + if (!$in_installer) { + lock_release(__FUNCTION__); + } + return TRUE; } /** - * @} End of "ingroup registry". + * @} End of "addtogroup registry". */ /** - * Central static variable storage. + * Provides central static variable storage. * * All functions requiring a static variable to persist or cache data within * a single page request are encouraged to use this function unless it is @@ -2926,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. * @@ -2952,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. @@ -2997,17 +3719,19 @@ } /** - * Reset one or all centrally stored static variable(s). + * Resets one or all centrally stored static variable(s). * * @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); } /** - * Detect whether the current script is running in a command-line environment. + * Detects whether the current script is running in a command-line environment. */ function drupal_is_cli() { return (!isset($_SERVER['SERVER_SOFTWARE']) && (php_sapi_name() == 'cli' || (is_numeric($_SERVER['argc']) && $_SERVER['argc'] > 0))); @@ -3015,7 +3739,8 @@ /** * Formats text for emphasized display in a placeholder inside a sentence. - * Used automatically by t(). + * + * Used automatically by format_string(). * * @param $text * The text to format (plain-text). @@ -3028,7 +3753,7 @@ } /** - * Register a function for execution on shutdown. + * Registers a function for execution on shutdown. * * Wrapper for register_shutdown_function() that catches thrown exceptions to * avoid "Exception thrown without a stack frame in Unknown". @@ -3063,7 +3788,7 @@ } /** - * Internal function used to execute registered shutdown functions. + * Executes registered shutdown functions. */ function _drupal_shutdown_function() { $callbacks = &drupal_register_shutdown_function(); @@ -3073,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) { @@ -3086,3 +3815,63 @@ } } } + +/** + * Compares the memory required for an operation to the available memory. + * + * @param $required + * The memory required for the operation, expressed as a number of bytes with + * optional SI or IEC binary unit prefix (e.g. 2, 3K, 5MB, 10G, 6GiB, 8bytes, + * 9mbytes). + * @param $memory_limit + * (optional) The memory limit for the operation, expressed as a number of + * bytes with optional SI or IEC binary unit prefix (e.g. 2, 3K, 5MB, 10G, + * 6GiB, 8bytes, 9mbytes). If no value is passed, the current PHP + * memory_limit will be used. Defaults to NULL. + * + * @return + * TRUE if there is sufficient memory to allow the operation, or FALSE + * otherwise. + */ +function drupal_check_memory_limit($required, $memory_limit = NULL) { + if (!isset($memory_limit)) { + $memory_limit = ini_get('memory_limit'); + } + + // There is sufficient memory if: + // - No memory limit is set. + // - The memory limit is set to unlimited (-1). + // - 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.0/includes/cache-install.inc drupal-7.66/includes/cache-install.inc --- drupal-7.0/includes/cache-install.inc 2010-05-18 20:26:30.000000000 +0200 +++ drupal-7.66/includes/cache-install.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ get($cid); } /** - * Return data from the persistent cache when given an array of cache IDs. + * Returns data from the persistent cache when given an array of cache IDs. * * @param $cids * An array of cache IDs for the data to retrieve. This is passed by * reference, and will have the IDs successfully returned from cache removed. * @param $bin * The cache bin where the data is stored. + * * @return * An array of the items successfully returned from cache indexed by cid. */ @@ -66,50 +74,22 @@ } /** - * Store data in the persistent cache. + * Stores data in the persistent cache. * * The persistent cache is split up into several cache bins. In the default * cache implementation, each cache bin corresponds to a database table by the * same name. Other implementations might want to store several bins in data * structures that get flushed together. While it is not a problem for most * cache bins if the entries in them are flushed before their expire time, some - * might break functionality or are extremely expensive to recalculate. These - * will be marked with a (*). The other bins expired automatically by core. - * Contributed modules can add additional bins and get them expired - * automatically by implementing hook_flush_caches(). - * - * - cache: Generic cache storage bin (used for variables, theme registry, - * locale date, list of simpletest tests etc). - * - * - cache_block: Stores the content of various blocks. - * - * - cache field: Stores the field data belonging to a given object. - * - * - cache_filter: Stores filtered pieces of content. - * - * - cache_form(*): Stores multistep forms. Flushing this bin means that some - * forms displayed to users lose their state and the data already submitted - * to them. - * - * - cache_menu: Stores the structure of visible navigation menus per page. - * - * - cache_page: Stores generated pages for anonymous users. It is flushed - * very often, whenever a page changes, at least for every ode and comment - * submission. This is the only bin affected by the page cache setting on - * the administrator panel. - * - * - cache path: Stores the system paths that have an alias. - * - * - cache update(*): Stores available releases. The update server (for - * example, drupal.org) needs to produce the relevant XML for every project - * installed on the current site. As this is different for (almost) every - * site, it's very expensive to recalculate for the update server. + * might break functionality or are extremely expensive to recalculate. The + * other bins are expired automatically by core. Contributed modules can add + * additional bins and get them expired automatically by implementing + * hook_flush_caches(). * * The reasons for having several bins are as follows: - * - * - smaller bins mean smaller database tables and allow for faster selects and - * inserts - * - we try to put fast changing cache items and rather static ones into + * - Smaller bins mean smaller database tables and allow for faster selects and + * inserts. + * - We try to put fast changing cache items and rather static ones into * different bins. The effect is that only the fast changing bins will need a * lot of writes to disk. The more static bins will also be better cacheable * with MySQL's query cache. @@ -118,44 +98,67 @@ * 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 are + * 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 'cache_block', - * 'cache_bootstrap', 'cache_field', 'cache_filter', 'cache_form', - * 'cache_menu', 'cache_page', 'cache_update' or 'cache' for the default - * cache. + * (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. + * - cache_bootstrap: Stores the class registry, the system list of modules, + * the list of which modules implement which hooks, and the Drupal variable + * list. + * - cache_field: Stores the field data belonging to a given object. + * - cache_filter: Stores filtered pieces of content. + * - cache_form: Stores multistep forms. Flushing this bin means that some + * forms displayed to users lose their state and the data already submitted + * to them. This bin should not be flushed before its expired time. + * - cache_menu: Stores the structure of visible navigation menus per page. + * - cache_page: Stores generated pages for anonymous users. It is flushed + * very often, whenever a page changes, at least for every node and comment + * submission. This is the only bin affected by the page cache setting on + * 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 * general cache wipe. * - A Unix timestamp: Indicates that the item should be kept at least until * the given time, after which it behaves like CACHE_TEMPORARY. + * + * @see _update_cache_set() + * @see cache_get() */ function cache_set($cid, $data, $bin = 'cache', $expire = CACHE_PERMANENT) { return _cache_get_object($bin)->set($cid, $data, $expire); } /** - * Expire data from the cache. + * Expires data from the cache. * - * If called without arguments, expirable entries will be cleared from the - * cache_page and cache_block bins. + * If called with the arguments $cid and $bin set to NULL or omitted, then + * expirable entries will be cleared from the cache_page and cache_block bins, + * and the $wildcard argument is ignored. * * @param $cid - * If set, the cache ID to delete. Otherwise, all cache entries that can - * expire are deleted. - * + * If set, the cache ID or an array of cache IDs. Otherwise, all cache entries + * that can expire are deleted. The $wildcard argument will be ignored if set + * to NULL. * @param $bin - * If set, the bin $bin to delete from. Mandatory - * argument if $cid is set. - * + * If set, the cache bin to delete from. Mandatory argument if $cid is set. * @param $wildcard - * If $wildcard is TRUE, cache IDs starting with $cid are deleted in - * addition to the exact cache ID specified by $cid. If $wildcard is - * TRUE and $cid is '*' then the entire bin $bin is emptied. + * If TRUE, the $cid argument must contain a string value and cache IDs + * starting with $cid are deleted in addition to the exact cache ID specified + * by $cid. If $wildcard is TRUE and $cid is '*', the entire cache is emptied. */ function cache_clear_all($cid = NULL, $bin = NULL, $wildcard = FALSE) { if (!isset($cid) && !isset($bin)) { @@ -171,13 +174,14 @@ } /** - * Check if a cache bin is empty. + * Checks if a cache bin is empty. * * A cache bin is considered empty if it does not contain any valid data for any * cache ID. * * @param $bin * The cache bin to check. + * * @return * TRUE if the cache bin specified is empty. */ @@ -186,7 +190,7 @@ } /** - * Interface for cache implementations. + * Defines an interface for cache implementations. * * All cache implementations have to implement this interface. * DrupalDatabaseCache provides the default implementation, which can be @@ -223,49 +227,52 @@ * @see DrupalDatabaseCache */ interface DrupalCacheInterface { - /** - * Constructor. - * - * @param $bin - * The cache bin for which the object is created. - */ - function __construct($bin); /** - * Return data from the persistent cache. Data may be stored as either plain - * text or as serialized data. cache_get will automatically return - * unserialized objects and arrays. + * Returns data from the persistent cache. + * + * Data may be stored as either plain text or as serialized data. cache_get() + * will automatically return unserialized objects and arrays. * * @param $cid * The cache ID of the data to retrieve. + * * @return * The cache or FALSE on failure. */ function get($cid); /** - * Return data from the persistent cache when given an array of cache IDs. + * Returns data from the persistent cache when given an array of cache IDs. * * @param $cids * An array of cache IDs for the data to retrieve. This is passed by * reference, and will have the IDs successfully returned from cache * removed. + * * @return * An array of the items successfully returned from cache indexed by cid. */ function getMultiple(&$cids); /** - * Store data in the persistent cache. + * Stores data in the persistent cache. * * @param $cid * 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 @@ -277,21 +284,25 @@ /** - * Expire data from the cache. If called without arguments, expirable - * entries will be cleared from the cache_page and cache_block bins. + * Expires data from the cache. + * + * If called without arguments, expirable entries will be cleared from the + * cache_page and cache_block bins. * * @param $cid - * If set, the cache ID to delete. Otherwise, all cache entries that can - * expire are deleted. + * If set, the cache ID or an array of cache IDs. Otherwise, all cache + * entries that can expire are deleted. The $wildcard argument will be + * ignored if set to NULL. * @param $wildcard - * If set to TRUE, the $cid is treated as a substring - * to match rather than a complete ID. The match is a right hand - * match. If '*' is given as $cid, the bin $bin will be emptied. + * If TRUE, the $cid argument must contain a string value and cache IDs + * starting with $cid are deleted in addition to the exact cache ID + * specified by $cid. If $wildcard is TRUE and $cid is '*', the entire + * cache is emptied. */ function clear($cid = NULL, $wildcard = FALSE); /** - * Check if a cache bin is empty. + * Checks if a cache bin is empty. * * A cache bin is considered empty if it does not contain any valid data for * any cache ID. @@ -303,7 +314,7 @@ } /** - * Default cache implementation. + * Defines a default cache implementation. * * This is Drupal's default cache implementation. It uses the database to store * cached data. Each cache bin corresponds to a database table by the same name. @@ -311,24 +322,41 @@ class DrupalDatabaseCache implements DrupalCacheInterface { protected $bin; + /** + * Constructs a DrupalDatabaseCache object. + * + * @param $bin + * The cache bin for which the object is created. + */ function __construct($bin) { $this->bin = $bin; } + /** + * Implements DrupalCacheInterface::get(). + */ function get($cid) { $cids = array($cid); $cache = $this->getMultiple($cids); return reset($cache); } + /** + * Implements DrupalCacheInterface::getMultiple(). + */ function getMultiple(&$cids) { try { // Garbage collection necessary when enforcing a minimum cache lifetime. $this->garbageCollection($this->bin); - $query = db_select($this->bin); - $query->fields($this->bin, array('cid', 'data', 'created', 'expire', 'serialized')); - $query->condition($this->bin . '.cid', $cids, 'IN'); - $result = $query->execute(); + + // When serving cached pages, the overhead of using db_select() was found + // to add around 30% overhead to the request. Since $this->bin is a + // variable, this means the call to db_query() here uses a concatenated + // string. This is highly discouraged under any other circumstances, and + // is used here only due to the performance overhead we would incur + // otherwise. When serving an uncached page, the overhead of using + // db_select() is a much smaller proportion of the request. + $result = db_query('SELECT cid, data, created, expire, serialized FROM {' . db_escape_table($this->bin) . '} WHERE cid IN (:cids)', array(':cids' => $cids)); $cache = array(); foreach ($result as $item) { $item = $this->prepareItem($item); @@ -353,11 +381,31 @@ * The bin being requested. */ protected function garbageCollection() { - global $user; + $cache_lifetime = variable_get('cache_lifetime', 0); - // Garbage collection necessary when enforcing a minimum cache lifetime. + // Clean-up the per-user cache expiration session data, so that the session + // handler can properly clean-up the session data for anonymous users. + if (isset($_SESSION['cache_expiration'])) { + $expire = REQUEST_TIME - $cache_lifetime; + foreach ($_SESSION['cache_expiration'] as $bin => $timestamp) { + if ($timestamp < $expire) { + unset($_SESSION['cache_expiration'][$bin]); + } + } + if (!$_SESSION['cache_expiration']) { + unset($_SESSION['cache_expiration']); + } + } + + // Garbage collection of temporary items is only necessary when enforcing + // a minimum cache lifetime. + if (!$cache_lifetime) { + return; + } + // When cache lifetime is in force, avoid running garbage collection too + // often since this will remove temporary cache items indiscriminately. $cache_flush = variable_get('cache_flush_' . $this->bin, 0); - if ($cache_flush && ($cache_flush + variable_get('cache_lifetime', 0) <= REQUEST_TIME)) { + if ($cache_flush && ($cache_flush + $cache_lifetime <= REQUEST_TIME)) { // Reset the variable immediately to prevent a meltdown in heavy load situations. variable_set('cache_flush_' . $this->bin, 0); // Time to flush old cache data @@ -369,13 +417,14 @@ } /** - * Prepare a cached item. + * Prepares a cached item. * * Checks that items are either permanent or did not expire, and unserializes * data as appropriate. * * @param $cache * An item loaded from cache_get() or cache_get_multiple(). + * * @return * The item with data unserialized as appropriate or FALSE if there is no * valid item to load. @@ -386,17 +435,16 @@ if (!isset($cache->data)) { return FALSE; } - // If enforcing a minimum cache lifetime, validate that the data is - // currently valid for this user before we return it by making sure the cache - // entry was created before the timestamp in the current session's cache - // timer. The cache variable is loaded into the $user object by _drupal_session_read() - // in session.inc. If the data is permanent or we're not enforcing a minimum - // cache lifetime always return the cached data. - if ($cache->expire != CACHE_PERMANENT && variable_get('cache_lifetime', 0) && $user->cache > $cache->created) { - // This cache data is too old and thus not valid for us, ignore it. + // If the cached data is temporary and subject to a per-user minimum + // lifetime, compare the cache entry timestamp with the user session + // cache_expiration timestamp. If the cache entry is too old, ignore it. + if ($cache->expire != CACHE_PERMANENT && variable_get('cache_lifetime', 0) && isset($_SESSION['cache_expiration'][$this->bin]) && $_SESSION['cache_expiration'][$this->bin] > $cache->created) { + // Ignore cache data that is too old and thus not valid for this user. return FALSE; } + // If the data is permanent or not subject to a minimum cache lifetime, + // unserialize and return the cached data. if ($cache->serialized) { $cache->data = unserialize($cache->data); } @@ -404,6 +452,9 @@ return $cache; } + /** + * Implements DrupalCacheInterface::set(). + */ function set($cid, $data, $expire = CACHE_PERMANENT) { $fields = array( 'serialized' => 0, @@ -430,16 +481,18 @@ } } + /** + * Implements DrupalCacheInterface::clear(). + */ function clear($cid = NULL, $wildcard = FALSE) { global $user; if (empty($cid)) { if (variable_get('cache_lifetime', 0)) { - // We store the time in the current user's $user->cache variable which - // will be saved into the sessions bin by _drupal_session_write(). We then - // simulate that the cache was flushed for this user by not returning - // cached data that was cached before the timestamp. - $user->cache = REQUEST_TIME; + // We store the time in the current user's session. We then simulate + // that the cache was flushed for this user by not returning cached + // data that was cached before the timestamp. + $_SESSION['cache_expiration'][$this->bin] = REQUEST_TIME; $cache_flush = variable_get('cache_flush_' . $this->bin, 0); if ($cache_flush == 0) { @@ -467,7 +520,16 @@ else { if ($wildcard) { if ($cid == '*') { - db_truncate($this->bin)->execute(); + // Check if $this->bin is a cache table before truncating. Other + // cache_clear_all() operations throw a PDO error in this situation, + // so we don't need to verify them first. This ensures that non-cache + // tables cannot be truncated accidentally. + if ($this->isValidBin()) { + db_truncate($this->bin)->execute(); + } + else { + throw new Exception(t('Invalid or missing cache bin specified: %bin', array('%bin' => $this->bin))); + } } else { db_delete($this->bin) @@ -492,6 +554,9 @@ } } + /** + * Implements DrupalCacheInterface::isEmpty(). + */ function isEmpty() { $this->garbageCollection(); $query = db_select($this->bin); @@ -501,4 +566,25 @@ ->fetchField(); return empty($result); } + + /** + * Checks if $this->bin represents a valid cache table. + * + * This check is required to ensure that non-cache tables are not truncated + * accidentally when calling cache_clear_all(). + * + * @return boolean + */ + function isValidBin() { + if ($this->bin == 'cache' || substr($this->bin, 0, 6) == 'cache_') { + // Skip schema check for bins with standard table names. + return TRUE; + } + // These fields are required for any cache table. + $fields = array('cid', 'data', 'expire', 'created', 'serialized'); + // Load the table schema. + $schema = drupal_get_schema($this->bin); + // Confirm that all fields are present. + return isset($schema['fields']) && !array_diff($fields, array_keys($schema['fields'])); + } } diff -Naur drupal-7.0/includes/common.inc drupal-7.66/includes/common.inc --- drupal-7.0/includes/common.inc 2011-01-03 07:51:00.000000000 +0100 +++ drupal-7.66/includes/common.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ $value. + * An array of URL decoded couples $param_name => $value. */ function drupal_get_query_array($query) { $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]) : ''; } } @@ -453,7 +466,7 @@ } /** - * Parse an array into a valid, rawurlencoded query string. + * Parses an array into a valid, rawurlencoded query string. * * This differs from http_build_query() as we need to rawurlencode() (instead of * urlencode()) all query parameters. @@ -474,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)) { @@ -494,13 +507,19 @@ } /** - * Prepare a 'destination' URL query parameter for use in combination with drupal_goto(). + * Prepares a 'destination' URL query parameter for use with drupal_goto(). * * Used to direct the user back to the referring page after completing a form. * By default the current URL is returned. If a destination exists in the * previous request, that destination is returned. As such, a destination can * persist across multiple pages. * + * @return + * An associative array containing the key: + * - destination: The path provided via the destination query string or, if + * not available, the current path. + * + * @see current_path() * @see drupal_goto() */ function drupal_get_destination() { @@ -525,37 +544,32 @@ } /** - * Wrapper around parse_url() to parse 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, resp. url(). It should not be used for URLs that come from external - * sources, or URLs that link to external resources. + * This function splits both internal paths like @code node?b=c#d @endcode and + * external URLs like @code https://example.com/a?b=c#d @endcode into their + * component parts. See + * @link http://tools.ietf.org/html/rfc3986#section-3 RFC 3986 @endlink for an + * explanation of what the component parts are. * - * The returned array contains a 'path' that may be passed separately to url(). - * For example: - * @code - * $options = drupal_parse_url($_GET['destination']); - * $my_url = url($options['path'], $options); - * $my_link = l('Example link', $options['path'], $options); - * @endcode + * Note that, unlike the RFC, when passed an external URL, this function + * groups the scheme, authority, and path together into the path component. * - * This is required, because url() does not support relative URLs containing a - * query string or fragment in its $path argument. Instead, any query string - * needs to be parsed into an associative query parameter array in - * $options['query'] and the fragment into $options['fragment']. + * @param string $url + * The internal path or external URL string to parse. * - * @param $url - * The URL string to parse, f.e. $_GET['destination']. - * - * @return - * 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) { @@ -597,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']); } @@ -622,7 +637,7 @@ } /** - * Send the user to a different Drupal page. + * Sends the user to a different page. * * This issues an on-site HTTP redirect. The function makes sure the redirected * URL is formatted correctly. @@ -643,20 +658,23 @@ * callback. * * @param $path - * A Drupal path or a full URL. + * (optional) A Drupal path or a full URL, which will be passed to url() to + * compute the redirect for the URL. * @param $options - * An associative array of additional URL options to pass to url(). + * (optional) An associative array of additional URL options to pass to url(). * @param $http_response_code - * Valid values for an actual "goto" as per RFC 2616 section 10.3 are: - * - 301 Moved Permanently (the recommended value for most redirects) - * - 302 Found (default in Drupal and PHP, sometimes used for spamming search - * engines) - * - 303 See Other - * - 304 Not Modified - * - 305 Use Proxy - * - 307 Temporary Redirect (alternative to "503 Site Down for Maintenance") - * Note: Other values are defined by RFC 2616, but are rarely used and poorly - * supported. + * (optional) The HTTP status code to use for the redirection, defaults to + * 302. The valid values for 3xx redirection status codes are defined in + * @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3 RFC 2616 @endlink + * and the + * @link http://tools.ietf.org/html/draft-reschke-http-status-308-07 draft for the new HTTP status codes: @endlink + * - 301: Moved Permanently (the recommended value for most redirects). + * - 302: Found (default in Drupal and PHP, sometimes used for spamming search + * engines). + * - 303: See Other. + * - 304: Not Modified. + * - 305: Use Proxy. + * - 307: Temporary Redirect. * * @see drupal_get_destination() * @see url() @@ -671,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. @@ -687,7 +712,7 @@ } /** - * Deliver a "site is under maintenance" message to the browser. + * Delivers a "site is under maintenance" message to the browser. * * Page callback functions wanting to report a "site offline" message should * return MENU_SITE_OFFLINE instead of calling drupal_site_offline(). However, @@ -699,7 +724,7 @@ } /** - * Deliver a "page not found" error to the browser. + * Delivers a "page not found" error to the browser. * * Page callback functions wanting to report a "page not found" message should * return MENU_NOT_FOUND instead of calling drupal_not_found(). However, @@ -711,19 +736,20 @@ } /** - * Deliver a "access denied" error to the browser. + * Delivers an "access denied" error to the browser. * * Page callback functions wanting to report an "access denied" message should * return MENU_ACCESS_DENIED instead of calling drupal_access_denied(). However, * functions that are invoked in contexts where that return value might not - * bubble up to menu_execute_active_handler() should call drupal_access_denied(). + * bubble up to menu_execute_active_handler() should call + * drupal_access_denied(). */ function drupal_access_denied() { drupal_deliver_page(MENU_ACCESS_DENIED); } /** - * Perform an HTTP request. + * Performs an HTTP request. * * This is a flexible and powerful HTTP client implementation. Correctly * handles GET, POST, PUT or any other HTTP requests. Handles redirects. @@ -735,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 @@ -753,14 +780,24 @@ * received. * - redirect_code: If redirected, an integer containing the initial response * status code. - * - redirect_url: If redirected, a string containing the redirection location. + * - redirect_url: If redirected, a string containing the URL of the redirect + * target. * - error: If an error occurred, the error message. Otherwise not set. * - headers: An array containing the response headers as name/value pairs. * 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 + // implementation. + $override_function = variable_get('drupal_http_request_function', FALSE); + if (!empty($override_function) && function_exists($override_function)) { + return $override_function($url, $options); + } + $result = new stdClass(); // Parse the URL and make sure we can handle the schema. @@ -789,10 +826,53 @@ 'timeout' => 30.0, 'context' => NULL, ); + + // Merge the default headers. + $options['headers'] += array( + 'User-Agent' => 'Drupal (+http://drupal.org/)', + ); + // stream_socket_client() requires timeout to be a float. $options['timeout'] = (float) $options['timeout']; + // Use a proxy if one is defined and the host is not on the excluded list. + $proxy_server = variable_get('proxy_server', ''); + if ($proxy_server && _drupal_http_use_proxy($uri['host'])) { + // Set the scheme so we open a socket to the proxy server. + $uri['scheme'] = 'proxy'; + // Set the path to be the full URL. + $uri['path'] = $url; + // Since the URL is passed as the path, we won't use the parsed query. + unset($uri['query']); + + // Add in username and password to Proxy-Authorization header if needed. + if ($proxy_username = variable_get('proxy_username', '')) { + $proxy_password = variable_get('proxy_password', ''); + $options['headers']['Proxy-Authorization'] = 'Basic ' . base64_encode($proxy_username . (!empty($proxy_password) ? ":" . $proxy_password : '')); + } + // Some proxies reject requests with any User-Agent headers, while others + // require a specific one. + $proxy_user_agent = variable_get('proxy_user_agent', ''); + // The default value matches neither condition. + if ($proxy_user_agent === NULL) { + unset($options['headers']['User-Agent']); + } + elseif ($proxy_user_agent) { + $options['headers']['User-Agent'] = $proxy_user_agent; + } + } + switch ($uri['scheme']) { + case 'proxy': + // 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. + if (!isset($options['headers']['Host'])) { + $options['headers']['Host'] = $uri['host']; + $options['headers']['Host'] .= isset($uri['port']) && $uri['port'] != 80 ? ':' . $uri['port'] : ''; + } + break; + case 'http': case 'feed': $port = isset($uri['port']) ? $uri['port'] : 80; @@ -800,14 +880,20 @@ // 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: $result->error = 'invalid schema ' . $uri['scheme']; $result->code = -1003; @@ -832,7 +918,7 @@ // Mark that this request failed. This will trigger a check of the web // server's ability to make outgoing HTTP requests the next time that // requirements checking is performed. - // See system_requirements() + // See system_requirements(). variable_set('drupal_http_request_fails', TRUE); return $result; @@ -844,11 +930,6 @@ $path .= '?' . $uri['query']; } - // Merge the default headers. - $options['headers'] += array( - 'User-Agent' => 'Drupal (+http://drupal.org/)', - ); - // Only add Content-Length if we actually have any content or if it is a POST // or PUT request. Some non-standard servers get confused by Content-Length in // at least HEAD/GET requests, and Squid always requires Content-Length in @@ -860,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'] . (!empty($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 @@ -915,13 +996,16 @@ return $result; } // Parse response headers from the response body. - list($response, $result->data) = explode("\r\n\r\n", $response, 2); + // Be tolerant of malformed HTTP responses that separate header and body with + // \n\n or \r\r instead of \r\n\r\n. + list($response, $result->data) = preg_split("/\r\n\r\n|\n\n|\r\r/", $response, 2); $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(); @@ -990,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 @@ -1004,21 +1094,79 @@ 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; } - $result->redirect_url = $location; + if (!isset($result->redirect_url)) { + $result->redirect_url = $location; + } 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 + * TRUE if a proxy should be used for this host. + */ +function _drupal_http_use_proxy($host) { + $proxy_exceptions = variable_get('proxy_exceptions', array('localhost', '127.0.0.1')); + return !in_array(strtolower($host), $proxy_exceptions, TRUE); +} + /** * @} End of "HTTP handling". */ +/** + * Strips slashes from a string or array of strings. + * + * Callback for array_walk() within fix_gpx_magic(). + * + * @param $item + * An individual string or array of strings from superglobals. + */ function _fix_gpc_magic(&$item) { if (is_array($item)) { array_walk($item, '_fix_gpc_magic'); @@ -1029,11 +1177,19 @@ } /** - * Helper function to strip slashes from $_FILES skipping over the tmp_name keys - * since PHP generates single backslashes for file paths on Windows systems. + * Strips slashes from $_FILES items. + * + * Callback for array_walk() within fix_gpc_magic(). + * + * The tmp_name key is skipped keys since PHP generates single backslashes for + * file paths on Windows systems. * - * tmp_name does not have backslashes added see - * http://php.net/manual/en/features.file-upload.php#42280 + * @param $item + * An item from $_FILES. + * @param $key + * The key for the item within $_FILES. + * + * @see http://php.net/manual/features.file-upload.php#42280 */ function _fix_gpc_magic_files(&$item, $key) { if ($key != 'tmp_name') { @@ -1047,18 +1203,21 @@ } /** - * Fix double-escaping problems caused by "magic quotes" in some PHP installations. + * Fixes double-escaping caused by "magic quotes" in some PHP installations. + * + * @see _fix_gpc_magic() + * @see _fix_gpc_magic_files() */ function fix_gpc_magic() { - $fixed = &drupal_static(__FUNCTION__, FALSE); + static $fixed = FALSE; if (!$fixed && ini_get('magic_quotes_gpc')) { array_walk($_GET, '_fix_gpc_magic'); array_walk($_POST, '_fix_gpc_magic'); array_walk($_COOKIE, '_fix_gpc_magic'); array_walk($_REQUEST, '_fix_gpc_magic'); array_walk($_FILES, '_fix_gpc_magic_files'); - $fixed = TRUE; } + $fixed = TRUE; } /** @@ -1068,12 +1227,14 @@ */ /** - * Verify the syntax of the given e-mail address. + * Verifies the syntax of the given e-mail address. * - * Empty e-mail addresses are allowed. See RFC 2822 for details. + * This uses the + * @link http://php.net/manual/filter.filters.validate.php PHP e-mail validation filter. @endlink * * @param $mail * A string containing an e-mail address. + * * @return * TRUE if the address is in a valid format. */ @@ -1082,7 +1243,7 @@ } /** - * Verify the syntax of the given URL. + * Verifies the syntax of the given URL. * * This function should only be used on actual URLs. It should not be used for * Drupal menu paths, which can contain arbitrary characters. @@ -1091,6 +1252,7 @@ * The URL to verify. * @param $absolute * Whether the URL is absolute (beginning with a scheme such as "http:"). + * * @return * TRUE if the URL is in a valid format. */ @@ -1123,7 +1285,7 @@ */ /** - * Register an event for the current visitor to the flood control mechanism. + * Registers an event for the current visitor to the flood control mechanism. * * @param $name * The name of an event. @@ -1150,7 +1312,7 @@ } /** - * Make the flood control mechanism forget about an event for the current visitor. + * Makes the flood control mechanism forget an event for the current visitor. * * @param $name * The name of an event. @@ -1168,7 +1330,7 @@ } /** - * Checks whether user is allowed to proceed with the specified event. + * Checks whether a user is allowed to proceed with the specified event. * * Events can have thresholds saying that each user can only do that event * a certain number of times in a time window. This function verifies that the @@ -1262,7 +1424,7 @@ } /** - * Strips dangerous protocols (e.g. 'javascript:') from a URI and encodes it for output to an HTML attribute value. + * Strips dangerous protocols from a URI and encodes it for output to HTML. * * @param $uri * A plain-text URI that might contain dangerous protocols. @@ -1282,7 +1444,7 @@ } /** - * Very permissive XSS/HTML filter for admin-only use. + * Applies a very permissive XSS/HTML filter for admin-only use. * * Use only for fields where it is impractical to use the * whole filter system, but where some (mainly inline) mark-up @@ -1292,11 +1454,11 @@ * for scripts and styles. */ function filter_xss_admin($string) { - return filter_xss($string, array('a', 'abbr', 'acronym', 'address', 'b', 'bdo', 'big', 'blockquote', 'br', 'caption', 'cite', 'code', 'col', 'colgroup', 'dd', 'del', 'dfn', 'div', 'dl', 'dt', 'em', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'ins', 'kbd', 'li', 'ol', 'p', 'pre', 'q', 'samp', 'small', 'span', 'strong', 'sub', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr', 'tt', 'ul', 'var')); + return filter_xss($string, array('a', 'abbr', 'acronym', 'address', 'article', 'aside', 'b', 'bdi', 'bdo', 'big', 'blockquote', 'br', 'caption', 'cite', 'code', 'col', 'colgroup', 'command', 'dd', 'del', 'details', 'dfn', 'div', 'dl', 'dt', 'em', 'figcaption', 'figure', 'footer', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'i', 'img', 'ins', 'kbd', 'li', 'mark', 'menu', 'meter', 'nav', 'ol', 'output', 'p', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'section', 'small', 'span', 'strong', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time', 'tr', 'tt', 'u', 'ul', 'var', 'wbr')); } /** - * Filters an HTML string to prevent cross-site-scripting (XSS) vulnerabilities. + * Filters HTML to prevent cross-site-scripting (XSS) vulnerabilities. * * Based on kses by Ulf Harnhammar, see http://sourceforge.net/projects/kses. * For examples of various XSS attacks, see: http://ha.ckers.org/xss.html. @@ -1319,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 @@ -1327,21 +1488,21 @@ if (!drupal_validate_utf8($string)) { return ''; } - // Store the text format + // Store the text format. _filter_xss_split($allowed_tags, TRUE); - // Remove NULL characters (ignored by some browsers) + // Remove NULL characters (ignored by some browsers). $string = str_replace(chr(0), '', $string); - // Remove Netscape 4 JS entities + // Remove Netscape 4 JS entities. $string = preg_replace('%&\s*\{[^}]*(\}\s*;?|$)%', '', $string); - // Defuse all HTML entities + // Defuse all HTML entities. $string = str_replace('&', '&', $string); - // Change back only well-formed entities in our whitelist - // Decimal numeric entities + // Change back only well-formed entities in our whitelist: + // Decimal numeric entities. $string = preg_replace('/&#([0-9]+;)/', '&#\1', $string); - // Hexadecimal numeric entities + // Hexadecimal numeric entities. $string = preg_replace('/&#[Xx]0*((?:[0-9A-Fa-f]{2})+;)/', '&#x\1', $string); - // Named entities + // Named entities. $string = preg_replace('/&([A-Za-z][A-Za-z0-9]*;)/', '&\1', $string); return preg_replace_callback('% @@ -1365,6 +1526,7 @@ * If $store is FALSE then the array has one element, the HTML tag to process. * @param $store * Whether to store $m. + * * @return * If the element isn't allowed, an empty string. Otherwise, the cleaned up * version of the HTML element. @@ -1380,16 +1542,16 @@ $string = $m[1]; if (substr($string, 0, 1) != '<') { - // We matched a lone ">" character + // We matched a lone ">" character. return '>'; } elseif (strlen($string) == 1) { - // We matched a lone "<" character + // We matched a lone "<" character. return '<'; } - if (!preg_match('%^<\s*(/\s*)?([a-zA-Z0-9]+)([^>]*)>?|()$%', $string, $matches)) { - // Seriously malformed + if (!preg_match('%^<\s*(/\s*)?([a-zA-Z0-9\-]+)([^>]*)>?|()$%', $string, $matches)) { + // Seriously malformed. return ''; } @@ -1403,7 +1565,7 @@ } if (!isset($allowed_html[strtolower($elem)])) { - // Disallowed HTML element + // Disallowed HTML element. return ''; } @@ -1419,7 +1581,7 @@ $attrlist = preg_replace('%(\s?)/\s*$%', '\1', $attrlist, -1, $count); $xhtml_slash = $count ? ' /' : ''; - // Clean up attributes + // Clean up attributes. $attr2 = implode(' ', _filter_xss_attributes($attrlist)); $attr2 = preg_replace('/[<>]/', '', $attr2); $attr2 = strlen($attr2) ? ' ' . $attr2 : ''; @@ -1444,7 +1606,7 @@ switch ($mode) { case 0: - // Attribute name, href for instance + // Attribute name, href for instance. if (preg_match('/^([-a-zA-Z]+)/', $attr, $match)) { $attrname = strtolower($match[1]); $skip = ($attrname == 'style' || substr($attrname, 0, 2) == 'on'); @@ -1454,7 +1616,7 @@ break; case 1: - // Equals sign or valueless ("selected") + // Equals sign or valueless ("selected"). if (preg_match('/^\s*=\s*/', $attr)) { $working = 1; $mode = 2; $attr = preg_replace('/^\s*=\s*/', '', $attr); @@ -1471,7 +1633,7 @@ break; case 2: - // Attribute value, a URL after href= for instance + // Attribute value, a URL after href= for instance. if (preg_match('/^"([^"]*)"(\s+|$)/', $attr, $match)) { $thisval = filter_xss_bad_protocol($match[1]); @@ -1508,7 +1670,7 @@ } if ($working == 0) { - // not well formed, remove and try again + // Not well formed; remove and try again. $attr = preg_replace('/ ^ ( @@ -1532,15 +1694,16 @@ } /** - * Processes an HTML attribute value and ensures it does not contain an URL with a disallowed protocol (e.g. javascript:). + * Processes an HTML attribute value and strips dangerous protocols from URLs. * * @param $string * The string with the attribute value. * @param $decode - * (Deprecated) Whether to decode entities in the $string. Set to FALSE if the + * (deprecated) Whether to decode entities in the $string. Set to FALSE if the * $string is in plain text, TRUE otherwise. Defaults to TRUE. This parameter * is deprecated and will be removed in Drupal 8. To process a plain-text URI, * call drupal_strip_dangerous_protocols() or check_url() instead. + * * @return * Cleaned up and HTML-escaped version of $string. */ @@ -1594,7 +1757,7 @@ } /** - * Format a single RSS item. + * Formats a single RSS item. * * Arbitrary elements may be added using the $args associative array. */ @@ -1610,7 +1773,7 @@ } /** - * Format XML elements. + * Formats XML elements. * * @param $array * An array where each item represents an element and is either a: @@ -1619,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 = ''; @@ -1634,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"; @@ -1649,7 +1818,7 @@ } /** - * Format a string containing a count of items. + * Formats a string containing a count of items. * * This function ensures that the string is pluralized correctly. Since t() is * called by this function, make sure not to pass already-localized strings to @@ -1665,37 +1834,33 @@ * $output = format_plural($update_count, * 'Changed the content type of 1 post from %old-type to %new-type.', * 'Changed the content type of @count posts from %old-type to %new-type.', - * array('%old-type' => $info->old_type, '%new-type' => $info->new_type))); + * array('%old-type' => $info->old_type, '%new-type' => $info->new_type)); * @endcode * * @param $count * The item count to display. * @param $singular - * The string for the singular case. Please make sure it is clear this is - * singular, to ease translation (e.g. use "1 new comment" instead of "1 new"). - * Do not use @count in the singular string. + * The string for the singular case. Make sure it is clear this is singular, + * to ease translation (e.g. use "1 new comment" instead of "1 new"). Do not + * use @count in the singular string. * @param $plural - * The string for the plural case. Please make sure it is clear this is plural, - * to ease translation. Use @count in place of the item count, as in "@count - * new comments". + * The string for the plural case. Make sure it is clear this is plural, to + * ease translation. Use @count in place of the item count, as in + * "@count new comments". * @param $args - * An associative array of replacements to make after translation. Incidences + * An associative array of replacements to make after translation. Instances * of any key in this array are replaced with the corresponding value. - * Based on the first character of the key, the value is escaped and/or themed: - * - !variable: inserted as is - * - @variable: escape plain text to HTML (check_plain) - * - %variable: escape text and theme as a placeholder for user-submitted - * content (check_plain + drupal_placeholder) - * Note that you do not need to include @count in this array. - * This replacement is done automatically for the plural case. + * Based on the first character of the key, the value is escaped and/or + * themed. See format_string(). Note that you do not need to include @count + * in this array; this replacement is done automatically for the plural case. * @param $options - * An associative array of additional options, with the following keys: - * - 'langcode' (default to the current language) The language code to - * translate to a language other than what is used to display the page. - * - 'context' (default to the empty context) The context the source string - * belongs to. + * An associative array of additional options. See t() for allowed keys. + * * @return * A translated string. + * + * @see t() + * @see format_string() */ function format_plural($count, $singular, $plural, array $args = array(), array $options = array()) { $args['@count'] = $count; @@ -1705,7 +1870,8 @@ // Get the plural index through the gettext formula. $index = (function_exists('locale_get_plural')) ? locale_get_plural($count, isset($options['langcode']) ? $options['langcode'] : NULL) : -1; - // Backwards compatibility. + // If the index cannot be computed, use the plural as a fallback (which + // allows for most flexiblity with the replaceable @count value). if ($index < 0) { return t($plural, $args, $options); } @@ -1724,11 +1890,12 @@ } /** - * Parse a given byte count. + * Parses a given byte count. * * @param $size * A size expressed as a number of bytes with optional SI or IEC binary unit * prefix (e.g. 2, 3K, 5MB, 10G, 6GiB, 8 bytes, 9mbytes). + * * @return * An integer representation of the size in bytes. */ @@ -1745,13 +1912,14 @@ } /** - * Generate a string representation for the given byte count. + * Generates a string representation for the given byte count. * * @param $size * A size in bytes. * @param $langcode * Optional language code to translate to a language other than what is used * to display the page. + * * @return * A translated string representation of the size. */ @@ -1784,19 +1952,20 @@ } /** - * Format a time interval with the requested granularity. + * Formats a time interval with the requested granularity. * - * @param $timestamp + * @param $interval * The length of the interval in seconds. * @param $granularity * How many different units to display in the string. * @param $langcode * Optional language code to translate to a language other than * what is used to display the page. + * * @return * A translated string representation of the interval. */ -function format_interval($timestamp, $granularity = 2, $langcode = NULL) { +function format_interval($interval, $granularity = 2, $langcode = NULL) { $units = array( '1 year|@count years' => 31536000, '1 month|@count months' => 2592000, @@ -1809,9 +1978,9 @@ $output = ''; foreach ($units as $key => $value) { $key = explode('|', $key); - if ($timestamp >= $value) { - $output .= ($output ? ' ' : '') . format_plural(floor($timestamp / $value), $key[0], $key[1], array(), array('langcode' => $langcode)); - $timestamp %= $value; + if ($interval >= $value) { + $output .= ($output ? ' ' : '') . format_plural(floor($interval / $value), $key[0], $key[1], array(), array('langcode' => $langcode)); + $interval %= $value; $granularity--; } @@ -1841,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 @@ -1924,10 +2093,11 @@ /** * Returns an ISO8601 formatted date based on the given date. * - * Can be used as a callback for RDF mappings. + * Callback for use within hook_rdf_mapping() implementations. * * @param $date * A UNIX timestamp. + * * @return string * An ISO8601 formatted date. */ @@ -1938,7 +2108,9 @@ } /** - * Callback function for preg_replace_callback(). + * Translates a formatted date string. + * + * Callback for preg_replace_callback() within format_date(). */ function _format_date_callback(array $matches = NULL, $new_langcode = NULL) { // We cache translations to avoid redundant and rather costly calls to t(). @@ -1974,7 +2146,10 @@ /** * Format a username. * - * By default, the passed in object's 'name' property is used if it exists, or + * This is also the label callback implementation of + * callback_entity_info_label() for user_entity_info(). + * + * By default, the passed-in object's 'name' property is used if it exists, or * else, the site-defined value for the 'anonymous' variable. However, a module * may override this by implementing hook_username_alter(&$name, $account). * @@ -2005,8 +2180,9 @@ * alternative than url(). * * @param $path - * The internal path or external URL being linked to, such as "node/34" or - * "http://example.com/foo". A few notes: + * (optional) The internal path or external URL being linked to, such as + * "node/34" or "http://example.com/foo". The default value is equivalent to + * passing in ''. A few notes: * - If you provide a full URL, it will be considered an external URL. * - If you provide only the path (e.g. "node/34"), it will be * considered an internal link. In this case, it should be a system URL, @@ -2022,7 +2198,8 @@ * include them in $path, or use $options['query'] to let this function * URL encode them. * @param $options - * An associative array of additional options, with the following elements: + * (optional) An associative array of additional options, with the following + * elements: * - 'query': An array of query key/value-pairs (without any URL-encoding) to * append to the URL. * - 'fragment': A fragment identifier (named anchor) to append to the URL. @@ -2038,7 +2215,7 @@ * for the URL. If $options['language'] is omitted, the global $language_url * will be used. * - 'https': Whether this URL should point to a secure location. If not - * defined, the current scheme is used, so the user stays on http or https + * defined, the current scheme is used, so the user stays on HTTP or HTTPS * respectively. TRUE enforces HTTPS and FALSE enforces HTTP, but HTTPS can * only be enforced when the variable 'https' is set to TRUE. * - 'base_url': Only used internally, to modify the base URL when a language @@ -2053,8 +2230,8 @@ * Drupal on a web server that cannot be configured to automatically find * index.php, then hook_url_outbound_alter() can be implemented to force * this value to 'index.php'. - * - 'entity_type': The entity type of the object that called url(). Only set if - * url() is invoked by entity_uri(). + * - 'entity_type': The entity type of the object that called url(). Only + * set if url() is invoked by entity_uri(). * - 'entity': The entity object (such as a node) for which the URL is being * generated. Only set if url() is invoked by entity_uri(). * @@ -2071,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. @@ -2116,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. @@ -2143,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, '/'); } } @@ -2179,7 +2361,7 @@ } /** - * Return TRUE if a path is external to Drupal (e.g. http://example.com). + * Returns TRUE if a path is external to Drupal (e.g. http://example.com). * * If a path cannot be assessed by Drupal's menu handler, then we must * treat it as potentially insecure. @@ -2187,18 +2369,31 @@ * @param $path * The internal path or external URL being linked to, such as "node/34" or * "http://example.com/foo". + * * @return * Boolean TRUE or FALSE, where TRUE indicates an external path. */ function url_is_external($path) { $colonpos = strpos($path, ':'); - // Only call the slow drupal_strip_dangerous_protocols() if $path contains a - // ':' before any / ? or #. - 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); } /** - * Format an attribute string for a HTTP header. + * Formats an attribute string for an HTTP header. * * @param $attributes * An associative array of attributes such as 'rel'. @@ -2220,7 +2415,7 @@ } /** - * Converts an associative array to an attribute string for use in XML/HTML tags. + * Converts an associative array to an XML/HTML tag attribute string. * * Each array key and its value will be formatted into an attribute string. * If a value is itself an array, then its elements are concatenated to a single @@ -2238,7 +2433,7 @@ * drupal_attributes(array('title' => t(''))); * * // The statement below demonstrates dangerous use of drupal_attributes, and - * // will return an onmouseout attribute with javascript code that, when used + * // will return an onmouseout attribute with JavaScript code that, when used * // as attribute in a tag, will cause users to be redirected to another site. * // * // In this case, the 'onmouseout' attribute should not be whitelisted -- @@ -2251,7 +2446,7 @@ * An associative array of key-value pairs to be converted to attributes. * * @return - * A string ready for insertion in a tag. + * A string ready for insertion in a tag (starts with a space). * * @ingroup sanitization */ @@ -2266,36 +2461,49 @@ /** * Formats an internal or external URL link as an HTML anchor tag. * - * This function correctly handles aliased paths, and adds an 'active' class + * This function correctly handles aliased paths and adds an 'active' class * attribute to links that point to the current page (for theming), so all * internal links output by modules should be generated by this function if * possible. * - * @param $text - * The link text for the anchor tag. - * @param $path + * However, for links enclosed in translatable text you should use t() and + * embed the HTML anchor tag directly in the translated string. For example: + * @code + * t('Visit the settings page', array('@url' => url('admin'))); + * @endcode + * This keeps the context of the link title ('settings' in the example) for + * translators. + * + * @param string $text + * The translated link text for the anchor tag. + * @param string $path * The internal path or external URL being linked to, such as "node/34" or * "http://example.com/foo". After the url() function is called to construct * the URL from $path and $options, the resulting URL is passed through * check_plain() before it is inserted into the HTML anchor tag, to ensure * well-formed HTML. See url() for more information and notes. * @param array $options - * An associative array of additional options, with the following elements: + * An associative array of additional options. Defaults to an empty array. It + * may contain the following elements. * - 'attributes': An associative array of HTML attributes to apply to the * anchor tag. If element 'class' is included, it must be an array; 'title' * must be a string; other elements are more flexible, as they just need * to work in a call to drupal_attributes($options['attributes']). * - 'html' (default FALSE): Whether $text is HTML or just plain-text. For * example, to make an image tag into a link, this must be set to TRUE, or - * you will see the escaped HTML image tag. + * you will see the escaped HTML image tag. $text is not sanitized if + * 'html' is TRUE. The calling function must ensure that $text is already + * safe. * - 'language': An optional language object. If the path being linked to is * internal to the site, $options['language'] is used to determine whether * the link is "active", or pointing to the current page (the language as * well as the path must match). This element is also used by url(). * - Additional $options elements used by the url() function. * - * @return + * @return string * An HTML string containing a link to the given path. + * + * @see url() */ function l($text, $path, array $options = array()) { global $language_url; @@ -2331,7 +2539,7 @@ // rendering. if (variable_get('theme_link', TRUE)) { drupal_theme_initialize(); - $registry = theme_get_registry(); + $registry = theme_get_registry(FALSE); // We don't want to duplicate functionality that's in theme(), so any // hint of a module or theme doing anything at all special with the 'link' // theme hook should simply result in theme() being called. This includes @@ -2383,9 +2591,9 @@ * basis in hook_page_delivery_callback_alter(). * * For example, the same page callback function can be used for an HTML - * version of the page and an AJAX version of the page. The page callback + * version of the page and an Ajax version of the page. The page callback * function just needs to decide what content is to be returned and the - * delivery callback function will send it as an HTML page or an AJAX + * delivery callback function will send it as an HTML page or an Ajax * response, as appropriate. * * In order for page callbacks to be reusable in different delivery formats, @@ -2439,7 +2647,7 @@ } /** - * Package and send the result of a page callback to the browser as HTML. + * Packages and sends the result of a page callback to the browser as HTML. * * @param $page_callback_result * The result of a page callback. Can be one of: @@ -2459,6 +2667,19 @@ drupal_add_http_header('Content-Type', 'text/html; charset=utf-8'); } + // Send appropriate HTTP-Header for browsers and search engines. + 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? @@ -2469,9 +2690,15 @@ watchdog('page not found', check_plain($_GET['q']), NULL, WATCHDOG_WARNING); + // Check for and return a fast 404 page if configured. + drupal_fast_404(); + // 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', '')); @@ -2485,7 +2712,7 @@ if (empty($return) || $return == MENU_NOT_FOUND || $return == MENU_ACCESS_DENIED) { // Standard 404 handler. drupal_set_title(t('Page not found')); - $return = t('The requested page could not be found.'); + $return = t('The requested page "@path" could not be found.', array('@path' => request_uri())); } drupal_set_page_content($return); @@ -2500,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', '')); @@ -2541,7 +2771,7 @@ } /** - * Perform end-of-request tasks. + * Performs end-of-request tasks. * * This function sets the page cache if appropriate, and allows modules to * react to the closing of the page by calling hook_exit(). @@ -2564,11 +2794,12 @@ _registry_check_code(REGISTRY_WRITE_LOOKUP_CACHE); drupal_cache_system_paths(); module_implements_write_cache(); + drupal_file_scan_write_cache(); system_run_automated_cron(); } /** - * Perform end-of-request tasks. + * Performs end-of-request tasks. * * In some cases page requests need to end without calling drupal_page_footer(). * In these cases, call drupal_exit() instead. There should rarely be a reason @@ -2590,7 +2821,7 @@ } /** - * Form an associative array from a linear array. + * Forms an associative array from a linear array. * * This function walks through the provided array and constructs an associative * array out of it. The keys of the resulting array will be the values of the @@ -2602,7 +2833,8 @@ * A linear array. * @param $function * A name of a function to apply to all values before output. - * @result + * + * @return * An associative array. */ function drupal_map_assoc($array, $function = NULL) { @@ -2624,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 @@ -2645,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); + } } } @@ -2658,17 +2894,17 @@ * The name of the item for which the path is requested. * * @return - * The path to the requested item. + * The path to the requested item or an empty string if the item is not found. */ function drupal_get_path($type, $name) { return dirname(drupal_get_filename($type, $name)); } /** - * Return the base URL path (i.e., directory) of the Drupal installation. + * Returns the base URL path (i.e., directory) of the Drupal installation. * - * base_path() prefixes and suffixes a "/" onto the returned path if the path is - * not empty. At the very least, this will return "/". + * base_path() adds a "/" to the beginning and end of the returned path if the + * path is not empty. At the very least, this will return "/". * * Examples: * - http://example.com returns "/" because the path is empty. @@ -2679,12 +2915,12 @@ } /** - * Add a LINK tag with a distinct 'rel' attribute to the page's HEAD. + * Adds a LINK tag with a distinct 'rel' attribute to the page's HEAD. * - * This function can be called as long the HTML header hasn't been sent, - * which on normal pages is up through the preprocess step of theme('html'). - * Adding a link will overwrite a prior link with the exact same 'rel' and - * 'href' attributes. + * This function can be called as long the HTML header hasn't been sent, which + * on normal pages is up through the preprocess step of theme('html'). Adding + * a link will overwrite a prior link with the exact same 'rel' and 'href' + * attributes. * * @param $attributes * Associative array of element attributes including 'href' and 'rel'. @@ -2738,17 +2974,18 @@ * @param $data * (optional) The stylesheet data to be added, depending on what is passed * through to the $options['type'] parameter: - * - 'file': The path to the CSS file relative to the base_path(), e.g., - * "modules/devel/devel.css". Note that Modules should always prefix the - * names of their CSS files with the module name; for example, - * system-menus.css rather than simply menus.css. Themes can override - * module-supplied CSS files based on their filenames, and this prefixing - * helps prevent confusing name collisions for theme developers. See - * drupal_get_css() where the overrides are performed. Also, if the + * - 'file': The path to the CSS file relative to the base_path(), or a + * stream wrapper URI. For example: "modules/devel/devel.css" or + * "public://generated_css/stylesheet_1.css". Note that Modules should + * always prefix the names of their CSS files with the module name; for + * example, system-menus.css rather than simply menus.css. Themes can + * override module-supplied CSS files based on their filenames, and this + * prefixing helps prevent confusing name collisions for theme developers. + * See drupal_get_css() where the overrides are performed. Also, if the * direction of the current language is right-to-left (Hebrew, Arabic, * etc.), the function will also look for an RTL CSS file and append it to - * the list. The name of this file should have an '-rtl.css' suffix. For - * example a CSS file called 'mymodule-name.css' will have a + * the list. The name of this file should have an '-rtl.css' suffix. For + * example, a CSS file called 'mymodule-name.css' will have a * 'mymodule-name-rtl.css' file added to the list, if exists in the same * directory. This CSS file should contain overrides for properties which * should be reversed or otherwise different in a right-to-left display. @@ -2773,7 +3010,7 @@ * - 'group': A number identifying the group in which to add the stylesheet. * Available constants are: * - CSS_SYSTEM: Any system-layer CSS. - * - CSS_DEFAULT: Any module-layer CSS. + * - CSS_DEFAULT: (default) Any module-layer CSS. * - CSS_THEME: Any theme-layer CSS. * The group number serves as a weight: the markup for loading a stylesheet * within a lower weight group is output to the page before the markup for @@ -2820,9 +3057,18 @@ * * @return * An array of queued cascading stylesheets. + * + * @see drupal_get_css() */ 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)) { @@ -2858,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']) { @@ -2878,7 +3125,7 @@ } /** - * Returns a themed representation of all stylesheets that should be attached to the page. + * Returns a themed representation of all stylesheets to attach to the page. * * It loads the CSS in order, with 'module' first, then 'theme' afterwards. * This ensures proper cascading of styles so themes can easily override @@ -2900,8 +3147,11 @@ * (optional) If set to TRUE, this function skips calling drupal_alter() on * $css, useful when the calling function passes a $css array that has already * been altered. + * * @return * A string of XHTML CSS tags. + * + * @see drupal_add_css() */ function drupal_get_css($css = NULL, $skip_alter = FALSE) { if (!isset($css)) { @@ -2916,12 +3166,24 @@ // Sort CSS items, so that they appear in the correct order. uasort($css, 'drupal_sort_css_js'); + // Provide the page with information about the individual CSS files used, + // information not otherwise available when CSS aggregation is enabled. The + // setting is attached later in this function, but is set here, so that CSS + // files removed below are still considered "used" and prevented from being + // added in a later AJAX request. + // Skip if no files were added to the page or jQuery.extend() will overwrite + // the Drupal.settings.ajaxPageState.css object with an empty array. + if (!empty($css)) { + // Cast the array to an object to be on the safe side even if not empty. + $setting['ajaxPageState']['css'] = (object) array_fill_keys(array_keys($css), 1); + } + // Remove the overridden CSS files. Later CSS files override former ones. $previous_item = array(); foreach ($css as $key => $item) { if ($item['type'] == 'file') { // If defined, force a unique basename for this file. - $basename = isset($item['basename']) ? $item['basename'] : basename($item['data']); + $basename = isset($item['basename']) ? $item['basename'] : drupal_basename($item['data']); if (isset($previous_item[$basename])) { // Remove the previous item that shared the same base name. unset($css[$previous_item[$basename]]); @@ -2936,20 +3198,32 @@ '#items' => $css, ); - // Provide the page with information about the individual CSS files used, - // information not otherwise available when CSS aggregation is enabled. - $setting['ajaxPageState']['css'] = array_fill_keys(array_keys($css), 1); - $styles['#attached']['js'][] = array('type' => 'setting', 'data' => $setting); + if (!empty($setting)) { + $styles['#attached']['js'][] = array('type' => 'setting', 'data' => $setting); + } return drupal_render($styles); } /** - * Function used by uasort to sort the array structures returned by drupal_add_css() and drupal_add_js(). + * Sorts CSS and JavaScript resources. + * + * Callback for uasort() within: + * - drupal_get_css() + * - drupal_get_js() * * This sort order helps optimize front-end performance while providing modules * and themes with the necessary control for ordering the CSS and JavaScript * appearing on a page. + * + * @param $a + * First item for comparison. The compared items should be associative arrays + * of member items from drupal_add_css() or drupal_add_js(). + * @param $b + * Second item for comparison. + * + * @see drupal_add_css() + * @see drupal_add_js() */ function drupal_sort_css_js($a, $b) { // First order by group, so that, for example, all items in the CSS_SYSTEM @@ -3002,7 +3276,7 @@ * are always groupable, and items of the 'external' type are never groupable. * This function also ensures that the process of grouping items does not change * their relative order. This requirement may result in multiple groups for the - * same type, media, and browsers, if needed to accomodate other items in + * same type, media, and browsers, if needed to accommodate other items in * between. * * @param $css @@ -3016,6 +3290,7 @@ * 'items' key, which is the subset of items from $css that are in the group. * * @see drupal_pre_render_styles() + * @see system_element_info() */ function drupal_group_css($css) { $groups = array(); @@ -3098,6 +3373,7 @@ * * @see drupal_group_css() * @see drupal_pre_render_styles() + * @see system_element_info() */ function drupal_aggregate_css(&$css_groups) { $preprocess_css = (variable_get('preprocess_css', FALSE) && (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update')); @@ -3276,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; @@ -3359,8 +3639,8 @@ * in $css while the value is the cache file name. The cache file is generated * in two cases. First, if there is no file name value for the key, which will * happen if a new file name has been added to $css or after the lookup - * variable is emptied to force a rebuild of the cache. Second, the cache - * file is generated if it is missing on disk. Old cache files are not deleted + * variable is emptied to force a rebuild of the cache. Second, the cache file + * is generated if it is missing on disk. Old cache files are not deleted * immediately when the lookup variable is emptied, but are deleted after a set * period by drupal_delete_file_if_stale(). This ensures that files referenced * by a cached page will still be available. @@ -3375,7 +3655,13 @@ $data = ''; $uri = ''; $map = variable_get('drupal_css_cache_files', array()); - $key = hash('sha256', serialize($css)); + // Create a new array so that only the file names are used to create the hash. + // This prevents new aggregates from being created unnecessarily. + $css_data = array(); + foreach ($css as $css_file) { + $css_data[] = $css_file['data']; + } + $key = hash('sha256', serialize($css_data)); if (isset($map[$key])) { $uri = $map[$key]; } @@ -3438,9 +3724,7 @@ } /** - * Helper function for drupal_build_css_cache(). - * - * This function will prefix all paths within a CSS file. + * Prefixes all paths within a CSS file for drupal_build_css_cache(). */ function _drupal_build_css_path($matches, $base = NULL) { $_base = &drupal_static(__FUNCTION__); @@ -3497,33 +3781,40 @@ 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; } /** - * Process the contents of a stylesheet for aggregation. + * Processes the contents of a stylesheet for aggregation. * * @param $contents * The contents of the stylesheet. * @param $optimize * (optional) Boolean whether CSS contents should be minified. Defaults to * FALSE. + * * @return * Contents of the stylesheet including the imported stylesheets. */ 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. @@ -3542,8 +3833,8 @@ // Remove certain whitespace. // There are different conditions for removing leading and trailing // whitespace. - // @see http://php.net/manual/en/regexp.reference.subpatterns.php - $contents = preg_replace_callback('< + // @see http://php.net/manual/regexp.reference.subpatterns.php + $contents = preg_replace('< # Strip leading and trailing whitespace. \s*([@{};,])\s* # Strip only leading whitespace from: @@ -3554,7 +3845,10 @@ # - Colon: Retain :pseudo-selectors. | ([\(:])\s+ >xS', - '_drupal_load_stylesheet_content', + // Only one of the three capturing groups will match, so its reference + // will contain the wanted value and the references for the + // two non-matching groups will be replaced with empty strings. + '$1$2$3', $contents ); // End the file with a new line. @@ -3564,21 +3858,11 @@ // 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; } /** - * Helper for drupal_load_stylesheet_content(). - */ -function _drupal_load_stylesheet_content($matches) { - // Discard the full match. - unset($matches[0]); - // Use the non-empty match. - return current(array_filter($matches)); -} - -/** * Loads stylesheets recursively and returns contents with corrected paths. * * This function is used for recursive loading of stylesheets and @@ -3598,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); } /** @@ -3620,7 +3904,7 @@ } /** - * Prepare a string for use as a valid CSS identifier (element, class or ID name). + * Prepares a string for use as a CSS identifier (element, class, or ID name). * * http://www.w3.org/TR/CSS21/syndata.html#characters shows the syntax for valid * CSS identifiers (including element names, classes, and IDs in selectors.) @@ -3629,10 +3913,26 @@ * The identifier to clean. * @param $filter * An array of string replacements to use on the identifier. + * * @return * 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); @@ -3650,39 +3950,47 @@ } /** - * Prepare a string for use as a valid class name. + * Prepares a string for use as a valid class name. * * Do not pass one string containing multiple classes as they will be * incorrectly concatenated with dashes, i.e. "one two" will become "one-two". * * @param $class * The class name to clean. + * * @return * 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]; } /** - * Prepare a string for use as a valid HTML ID and guarantee uniqueness. + * Prepares a string for use as a valid HTML ID and guarantees uniqueness. * * This function ensures that each passed HTML ID value only exists once on the * page. By tracking the already returned ids, this function enables forms, * blocks, and other content to be output multiple times on the same page, * without breaking (X)HTML validation. * - * For already existing ids, a counter is appended to the id string. Therefore, + * For already existing IDs, a counter is appended to the ID string. Therefore, * JavaScript and CSS code should not rely on any value that was generated by * this function and instead should rely on manually added CSS classes or * similarly reliable constructs. * - * Two consecutive hyphens separate the counter from the original id. To manage - * uniqueness across multiple AJAX requests on the same page, AJAX requests + * Two consecutive hyphens separate the counter from the original ID. To manage + * uniqueness across multiple Ajax requests on the same page, Ajax requests * POST an array of all IDs currently present on the page, which are used to * prime this function's cache upon first invocation. * - * To allow reverse-parsing of ids submitted via AJAX, any multiple consecutive + * To allow reverse-parsing of IDs submitted via Ajax, any multiple consecutive * hyphens in the originally passed $id are replaced with a single hyphen. * * @param $id @@ -3692,18 +4000,22 @@ * The cleaned ID. */ function drupal_html_id($id) { - // If this is an AJAX request, then content returned by this page request will - // be merged with content already on the base page. The HTML ids must be + // If this is an Ajax request, then content returned by this page request will + // 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'); + // take into account IDs that are already in use on the base page. + 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 // function's $seen_ids static variable to that state information in order // to have it properly initialized for this page request. However, no such // page state API exists, so instead, ajax.js adds all of the in-use HTML - // ids to the POST data of AJAX submissions. Direct use of $_POST is + // IDs to the POST data of Ajax submissions. Direct use of $_POST is // normally not recommended as it could open up security risks, but because // the raw POST data is cast to a number before being returned by this // function, this usage is safe. @@ -3716,7 +4028,16 @@ // requested id. $_POST['ajax_html_ids'] contains the ids as they were // returned by this function, potentially with the appended counter, so // we parse that to reconstruct the $seen_ids array. - foreach ($_POST['ajax_html_ids'] as $seen_id) { + if (isset($_POST['ajax_html_ids'][0]) && strpos($_POST['ajax_html_ids'][0], ',') === FALSE) { + $ajax_html_ids = $_POST['ajax_html_ids']; + } + else { + // jquery.form.js may send the server a comma-separated string as the + // first element of an array (see http://drupal.org/node/1575060), so + // we need to convert it to an array in that case. + $ajax_html_ids = explode(',', $_POST['ajax_html_ids'][0]); + } + foreach ($ajax_html_ids as $seen_id) { // We rely on '--' being used solely for separating a base id from the // counter, which this function ensures when returning an id. $parts = explode('--', $seen_id, 2); @@ -3732,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(' ' => '-', '_' => '-', '[' => '-', ']' => '')); @@ -3750,7 +4074,7 @@ // The counter needs to be appended with a delimiter that does not exist in // the base ID. Requiring a unique delimiter helps ensure that we really do // return unique IDs and also helps us re-create the $seen_ids array during - // AJAX requests. + // Ajax requests. if (isset($seen_ids[$id])) { $id = $id . '--' . ++$seen_ids[$id]; } @@ -3768,7 +4092,7 @@ * page region that is output by the theme (Drupal core already handles this in * the standard template preprocess implementation). Standardizing the class * names in this way allows modules to implement certain features, such as - * drag-and-drop or dynamic AJAX loading, in a theme-independent way. + * drag-and-drop or dynamic Ajax loading, in a theme-independent way. * * @param $region * The name of the page region (for example, 'page_top' or 'content'). @@ -3796,7 +4120,7 @@ * to tell the user that a new message arrived, by opening a pop up, alert * box, etc.). This should only be used for JavaScript that cannot be executed * from a file. When adding inline code, make sure that you are not relying on - * $() being the jQuery function. Wrap your code in + * $() being the jQuery function. Wrap your code in * @code (function ($) {... })(jQuery); @endcode * or use jQuery() instead of $(). * - Add external JavaScript ('external'): Allows the inclusion of external @@ -3842,7 +4166,8 @@ * actually needed. * * @param $data - * (optional) If given, the value depends on the $options parameter: + * (optional) If given, the value depends on the $options parameter, or + * $options['type'] if $options is passed as an associative array: * - 'file': Path to the file relative to base_path(). * - 'inline': The JavaScript code that should be placed in the given scope. * - 'external': The absolute path to an external JavaScript file that is not @@ -3915,8 +4240,15 @@ * 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> - * tag. Defaults to FALSE. + * - 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.0/modules/field/field.api.php drupal-7.66/modules/field/field.api.php --- drupal-7.0/modules/field/field.api.php 2010-12-14 20:50:05.000000000 +0100 +++ drupal-7.66/modules/field/field.api.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,8 +1,11 @@ type]['form']['title'])) { + $info['node'][$bundle->type]['form']['title']['weight'] = -20; } } } /** - * @} End of "ingroup field_fieldable_type" - */ - -/** * @defgroup field_types Field Types API * @{ - * Define field types, widget types, display formatter types, storage types. + * Define field types. * - * The bulk of the Field Types API are related to field types. A field type - * represents a particular type of data (integer, string, date, etc.) that - * can be attached to a fieldable entity. hook_field_info() defines the basic - * properties of a field type, and a variety of other field hooks are called by - * the Field Attach API to perform field-type-specific actions. - * - * @see hook_field_info() - * @see hook_field_info_alter() - * @see hook_field_schema() - * @see hook_field_load() - * @see hook_field_validate() - * @see hook_field_presave() - * @see hook_field_insert() - * @see hook_field_update() - * @see hook_field_delete() - * @see hook_field_delete_revision() - * @see hook_field_prepare_view() - * @see hook_field_is_empty() + * In the Field API, each field has a type, which determines what kind of data + * (integer, string, date, etc.) the field can hold, which settings it provides, + * and so on. The data type(s) accepted by a field are defined in + * hook_field_schema(); other basic properties of a field are defined in + * hook_field_info(). The other hooks below are called by the Field Attach API + * to perform field-type-specific actions. * * The Field Types API also defines two kinds of pluggable handlers: widgets - * and formatters, which specify how the field appears in edit forms and in - * displayed entities. Widgets and formatters can be implemented by a field-type - * module for its own field types, or by a third-party module to extend the - * behavior of existing field types. - * - * @see hook_field_widget_info() - * @see hook_field_formatter_info() + * and formatters. @link field_widget Widgets @endlink specify how the field + * appears in edit forms, while @link field_formatter formatters @endlink + * specify how the field appears in displayed entities. * * A third kind of pluggable handlers, storage backends, is defined by the * @link field_storage Field Storage API @endlink. + * + * See @link field Field API @endlink for information about the other parts of + * the Field API. */ /** * Define Field API field types. * + * Along with this hook, you also need to implement other hooks. See + * @link field_types Field Types API @endlink for more information. + * * @return * An array whose keys are field type names and whose values are arrays * describing the field type, with the following key/value pairs: @@ -211,8 +210,11 @@ /** * Define the Field API schema for a field structure. * - * This hook MUST be defined in .install for it to be detected during - * installation and upgrade. + * This is invoked when a field is created, in order to obtain the database + * schema from the module that defines the field's type. + * + * This hook must be defined in the module's .install file for it to be detected + * during installation and upgrade. * * @param $field * A field structure. @@ -257,8 +259,8 @@ } $columns += array( 'format' => array( - 'type' => 'int', - 'unsigned' => TRUE, + 'type' => 'varchar', + 'length' => 255, 'not null' => FALSE, ), ); @@ -444,9 +446,14 @@ } /** - * Define custom insert behavior for this module's field types. + * Define custom insert behavior for this module's field data. * - * Invoked from field_attach_insert(). + * This hook is invoked from field_attach_insert() on the module that defines a + * field, during the process of inserting an entity object (node, taxonomy term, + * etc.). It is invoked just before the data for this field on the particular + * entity object is inserted into field storage. Only field modules that are + * storing or tracking information outside the standard field storage mechanism + * need to implement this hook. * * @param $entity_type * The type of $entity. @@ -460,6 +467,9 @@ * The language associated with $items. * @param $items * $entity->{$field['field_name']}[$langcode], or an empty array if unset. + * + * @see hook_field_update() + * @see hook_field_delete() */ function hook_field_insert($entity_type, $entity, $field, $instance, $langcode, &$items) { if (variable_get('taxonomy_maintain_index_table', TRUE) && $field['storage']['type'] == 'field_sql_storage' && $entity_type == 'node' && $entity->status) { @@ -477,9 +487,14 @@ } /** - * Define custom update behavior for this module's field types. + * Define custom update behavior for this module's field data. * - * Invoked from field_attach_update(). + * This hook is invoked from field_attach_update() on the module that defines a + * field, during the process of updating an entity object (node, taxonomy term, + * etc.). It is invoked just before the data for this field on the particular + * entity object is updated into field storage. Only field modules that are + * storing or tracking information outside the standard field storage mechanism + * need to implement this hook. * * @param $entity_type * The type of $entity. @@ -493,6 +508,9 @@ * The language associated with $items. * @param $items * $entity->{$field['field_name']}[$langcode], or an empty array if unset. + * + * @see hook_field_insert() + * @see hook_field_delete() */ function hook_field_update($entity_type, $entity, $field, $instance, $langcode, &$items) { if (variable_get('taxonomy_maintain_index_table', TRUE) && $field['storage']['type'] == 'field_sql_storage' && $entity_type == 'node') { @@ -558,10 +576,14 @@ } /** - * Define custom delete behavior for this module's field types. + * Define custom delete behavior for this module's field data. * - * This hook is invoked just before the data is deleted from field storage - * in field_attach_delete(). + * This hook is invoked from field_attach_delete() on the module that defines a + * field, during the process of deleting an entity object (node, taxonomy term, + * etc.). It is invoked just before the data for this field on the particular + * entity object is deleted from field storage. Only field modules that are + * storing or tracking information outside the standard field storage mechanism + * need to implement this hook. * * @param $entity_type * The type of $entity. @@ -575,6 +597,9 @@ * The language associated with $items. * @param $items * $entity->{$field['field_name']}[$langcode], or an empty array if unset. + * + * @see hook_field_insert() + * @see hook_field_update() */ function hook_field_delete($entity_type, $entity, $field, $instance, $langcode, &$items) { list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity); @@ -585,7 +610,7 @@ // be counted in hook_file_references(). $item['file_field_type'] = $entity_type; $item['file_field_id'] = $id; - file_field_delete_file($item, $field); + file_field_delete_file($item, $field, $entity_type, $id); } } @@ -610,10 +635,11 @@ * $entity->{$field['field_name']}[$langcode], or an empty array if unset. */ function hook_field_delete_revision($entity_type, $entity, $field, $instance, $langcode, &$items) { + list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity); foreach ($items as $delta => $item) { // For hook_file_references, remember that this file is being deleted. $item['file_field_name'] = $field['field_name']; - if (file_field_delete_file($item, $field)) { + if (file_field_delete_file($item, $field, $entity_type, $id)) { $items[$delta] = NULL; } } @@ -638,6 +664,8 @@ * The source entity from which field values are being copied. * @param $source_langcode * The source language from which field values are being copied. + * + * @ingroup field_language */ function hook_field_prepare_translation($entity_type, $entity, $field, $instance, $langcode, &$items, $source_entity, $source_langcode) { // If the translating user is not permitted to use the assigned text format, @@ -670,15 +698,32 @@ } /** - * Expose Field API widget types. - * - * Widgets are Form API elements with additional processing capabilities. - * Widget hooks are typically called by the Field Attach API during the - * creation of the field form structure with field_attach_form(). + * @} End of "defgroup field_types". + */ + +/** + * @defgroup field_widget Field Widget API + * @{ + * Define Field API widget types. * - * @see hook_field_widget_info_alter() - * @see hook_field_widget_form() - * @see hook_field_widget_error() + * Field API widgets specify how fields are displayed in edit forms. Fields of a + * given @link field_types field type @endlink may be edited using more than one + * widget. In this case, the Field UI module allows the site builder to choose + * which widget to use. Widget types are defined by implementing + * hook_field_widget_info(). + * + * Widgets are @link forms_api_reference.html Form API @endlink elements with + * additional processing capabilities. Widget hooks are typically called by the + * Field Attach API during the creation of the field form structure with + * field_attach_form(). + * + * @see field + * @see field_types + * @see field_formatter + */ + +/** + * Expose Field API widget types. * * @return * An array describing the widget types implemented by the module. @@ -705,9 +750,19 @@ * - FIELD_BEHAVIOR_DEFAULT: (default) If the widget accepts default * values. * - FIELD_BEHAVIOR_NONE: if the widget does not support default values. + * - weight: (optional) An integer to determine the weight of this widget + * relative to other widgets in the Field UI when selecting a widget for a + * given field instance. + * + * @see hook_field_widget_info_alter() + * @see hook_field_widget_form() + * @see hook_field_widget_form_alter() + * @see hook_field_widget_WIDGET_TYPE_form_alter() + * @see hook_field_widget_error() + * @see hook_field_widget_settings_form() */ function hook_field_widget_info() { - return array( + return array( 'text_textfield' => array( 'label' => t('Text field'), 'field types' => array('text'), @@ -734,6 +789,8 @@ 'multiple values' => FIELD_BEHAVIOR_DEFAULT, 'default value' => FIELD_BEHAVIOR_DEFAULT, ), + // As an advanced widget, force it to sink to the bottom of the choices. + 'weight' => 2, ), ); } @@ -758,7 +815,7 @@ /** * Return the form for a single field widget. * - * Field widget form elements should be based on the passed in $element, which + * Field widget form elements should be based on the passed-in $element, which * contains the base form element properties derived from the field * configuration. * @@ -784,8 +841,8 @@ * properties from $field and $instance and set them as ad-hoc * $element['#custom'] properties, for later use by its element callbacks. * - * @see field_widget_field() - * @see field_widget_instance() + * Other modules may alter the form element provided by this function using + * hook_field_widget_form_alter(). * * @param $form * The form structure where widgets are being attached to. This might be a @@ -827,13 +884,112 @@ * * @return * The form elements for a single widget for this field. + * + * @see field_widget_field() + * @see field_widget_instance() + * @see hook_field_widget_form_alter() + * @see hook_field_widget_WIDGET_TYPE_form_alter() */ function hook_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) { $element += array( '#type' => $instance['widget']['type'], '#default_value' => isset($items[$delta]) ? $items[$delta] : '', ); - return $element; + return array('value' => $element); +} + +/** + * Alter forms for field widgets provided by other modules. + * + * @param $element + * The field widget form element as constructed by hook_field_widget_form(). + * @param $form_state + * An associative array containing the current state of the form. + * @param $context + * An associative array containing the following key-value pairs, matching the + * arguments received by hook_field_widget_form(): + * - form: The form structure to which widgets are being attached. This may be + * a full form structure, or a sub-element of a larger form. + * - field: The field structure. + * - instance: The field instance structure. + * - langcode: The language associated with $items. + * - items: Array of default values for this field. + * - delta: The order of this item in the array of subelements (0, 1, 2, etc). + * + * @see hook_field_widget_form() + * @see hook_field_widget_WIDGET_TYPE_form_alter() + */ +function hook_field_widget_form_alter(&$element, &$form_state, $context) { + // Add a css class to widget form elements for all fields of type mytype. + if ($context['field']['type'] == 'mytype') { + // Be sure not to overwrite existing attributes. + $element['#attributes']['class'][] = 'myclass'; + } +} + +/** + * Alter widget forms for a specific widget provided by another module. + * + * Modules can implement hook_field_widget_WIDGET_TYPE_form_alter() to modify a + * specific widget form, rather than using hook_field_widget_form_alter() and + * checking the widget type. + * + * @param $element + * The field widget form element as constructed by hook_field_widget_form(). + * @param $form_state + * An associative array containing the current state of the form. + * @param $context + * An associative array containing the following key-value pairs, matching the + * arguments received by hook_field_widget_form(): + * - "form": The form structure where widgets are being attached to. This + * might be a full form structure, or a sub-element of a larger form. + * - "field": The field structure. + * - "instance": The field instance structure. + * - "langcode": The language associated with $items. + * - "items": Array of default values for this field. + * - "delta": The order of this item in the array of subelements (0, 1, 2, + * etc). + * + * @see hook_field_widget_form() + * @see hook_field_widget_form_alter() + */ +function hook_field_widget_WIDGET_TYPE_form_alter(&$element, &$form_state, $context) { + // Code here will only act on widgets of type WIDGET_TYPE. For example, + // hook_field_widget_mymodule_autocomplete_form_alter() will only act on + // widgets of type 'mymodule_autocomplete'. + $element['#autocomplete_path'] = 'mymodule/autocomplete_path'; +} + +/** + * Alters the widget properties of a field instance before it gets displayed. + * + * Note that instead of hook_field_widget_properties_alter(), which is called + * for all fields on all entity types, + * hook_field_widget_properties_ENTITY_TYPE_alter() may be used to alter widget + * properties for fields on a specific entity type only. + * + * This hook is called once per field per added or edit entity. If the result + * of the hook involves reading from the database, it is highly recommended to + * statically cache the information. + * + * @param $widget + * The instance's widget properties. + * @param $context + * An associative array containing: + * - entity_type: The entity type; e.g., 'node' or 'user'. + * - entity: The entity object. + * - field: The field that the widget belongs to. + * - instance: The instance of the field. + * + * @see hook_field_widget_properties_ENTITY_TYPE_alter() + */ +function hook_field_widget_properties_alter(&$widget, $context) { + // Change a widget's type according to the time of day. + $field = $context['field']; + if ($context['entity_type'] == 'node' && $field['field_name'] == 'field_foo') { + $time = date('H'); + $widget['type'] = $time < 12 ? 'widget_am' : 'widget_pm'; + } } /** @@ -856,9 +1012,32 @@ * An associative array containing the current state of the form. */ function hook_field_widget_error($element, $error, $form, &$form_state) { - form_error($element['value'], $error['message']); + form_error($element, $error['message']); } + +/** + * @} End of "defgroup field_widget". + */ + + +/** + * @defgroup field_formatter Field Formatter API + * @{ + * Define Field API formatter types. + * + * Field API formatters specify how fields are displayed when the entity to + * which the field is attached is displayed. Fields of a given + * @link field_types field type @endlink may be displayed using more than one + * formatter. In this case, the Field UI module allows the site builder to + * choose which formatter to use. Field formatters are defined by implementing + * hook_field_formatter_info(). + * + * @see field + * @see field_types + * @see field_widget + */ + /** * Expose Field API formatter types. * @@ -919,8 +1098,8 @@ * Perform alterations on Field API formatter types. * * @param $info - * Array of informations on formatter types exposed by - * hook_field_field_formatter_info() implementations. + * An array of information on formatter types exposed by + * hook_field_formatter_info() implementations. */ function hook_field_formatter_info_alter(&$info) { // Add a setting to a formatter type. @@ -1077,11 +1256,11 @@ } /** - * @} End of "ingroup field_type" + * @} End of "defgroup field_formatter". */ /** - * @ingroup field_attach + * @addtogroup field_attach * @{ */ @@ -1133,7 +1312,7 @@ * * See field_attach_load() for details and arguments. */ -function hook_field_attach_load($entity_type, &$entities, $age, $options) { +function hook_field_attach_load($entity_type, $entities, $age, $options) { // @todo Needs function body. } @@ -1143,9 +1322,33 @@ * This hook is invoked after the field module has performed the operation. * * See field_attach_validate() for details and arguments. + * + * @param $entity_type + * The type of $entity; e.g., 'node' or 'user'. + * @param $entity + * The entity with fields to validate. + * @param array $errors + * The array of errors (keyed by field name, language code, and delta) that + * have already been reported for the entity. The function should add its + * errors to this array. Each error is an associative array with the following + * keys and values: + * - error: An error code (should be a string prefixed with the module name). + * - message: The human readable message to be displayed. */ function hook_field_attach_validate($entity_type, $entity, &$errors) { - // @todo Needs function body. + // Make sure any images in article nodes have an alt text. + if ($entity_type == 'node' && $entity->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.'), + ); + } + } + } + } } /** @@ -1271,7 +1474,7 @@ */ function hook_field_attach_purge($entity_type, $entity, $field, $instance) { // find the corresponding data in mymodule and purge it - if($entity_type == 'node' && $field->field_name == 'my_field_name') { + if ($entity_type == 'node' && $field->field_name == 'my_field_name') { mymodule_remove_mydata($entity->nid); } } @@ -1319,7 +1522,7 @@ * * This hook is invoked after the field module has performed the operation. * - * @param &$entity + * @param $entity * The entity being prepared for translation. * @param $context * An associative array containing: @@ -1347,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 @@ -1362,12 +1567,14 @@ * This hook is invoked from field_available_languages() to allow modules to * alter the array of available languages for the given field. * - * @param &$languages + * @param $languages * A reference to an array of language codes to be made available. * @param $context * 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. @@ -1418,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. @@ -1433,15 +1640,11 @@ } /** - * @} End of "ingroup field_attach" + * @} End of "addtogroup field_attach". */ -/********************************************************************** - * Field Storage API - **********************************************************************/ - /** - * @ingroup field_storage + * @addtogroup field_storage * @{ */ @@ -1581,12 +1784,15 @@ * non-deleted fields. If unset or FALSE, only non-deleted fields should be * loaded. */ -function hook_field_storage_load($entity_type, &$entities, $age, $fields, $options) { - $field_info = field_info_field_by_ids(); +function hook_field_storage_load($entity_type, $entities, $age, $fields, $options) { $load_current = $age == FIELD_LOAD_CURRENT; foreach ($fields as $field_id => $ids) { - $field = $field_info[$field_id]; + // By the time this hook runs, the relevant field definitions have been + // populated and cached in FieldInfo, so calling field_info_field_by_id() + // on each field individually is more efficient than loading all fields in + // memory upfront with field_info_field_by_ids(). + $field = field_info_field_by_id($field_id); $field_name = $field['field_name']; $table = $load_current ? _field_sql_storage_tablename($field) : _field_sql_storage_revision_tablename($field); @@ -1691,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, @@ -2095,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 @@ -2107,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(); @@ -2122,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 @@ -2137,7 +2355,7 @@ * found in the 'display' key of $instance definitions. * @param $context * An associative array containing: - * - entity_type: The entity type; e.g. 'node' or 'user'. + * - entity_type: The entity type; e.g., 'node' or 'user'. * - field: The field being rendered. * - instance: The instance being rendered. * - entity: The entity being rendered. @@ -2172,7 +2390,7 @@ * found in the 'display' key of $instance definitions. * @param $context * An associative array containing: - * - entity_type: The entity type; e.g. 'node' or 'user'. + * - entity_type: The entity type; e.g., 'node' or 'user'. * - field: The field being rendered. * - instance: The instance being rendered. * - entity: The entity being rendered. @@ -2188,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 @@ -2199,45 +2421,15 @@ * by pseudo-field names. * @param $context * An associative array containing: - * - entity_type: The entity type; e.g. 'node' or 'user'. + * - 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') { - $displays['description']['visibility'] = FALSE; - } -} - -/** - * Alters the widget properties of a field instance before it gets displayed. - * - * Note that instead of hook_field_widget_properties_alter(), which is called - * for all fields on all entity types, - * hook_field_widget_properties_ENTITY_TYPE_alter() may be used to alter widget - * properties for fields on a specific entity type only. - * - * This hook is called once per field per added or edit entity. If the result - * of the hook involves reading from the database, it is highly recommended to - * statically cache the information. - * - * @param $widget - * The instance's widget properties. - * @param $context - * An associative array containing: - * - entity_type: The entity type; e.g. 'node' or 'user'. - * - entity: The entity object. - * - field: The field that the widget belongs to. - * - instance: The instance of the field. - * - * @see hook_field_widget_properties_ENTITY_TYPE_alter() - */ -function hook_field_widget_properties_alter(&$widget, $context) { - // Change a widget's type according to the time of day. - $field = $context['field']; - if ($context['entity_type'] == 'node' && $field['field_name'] == 'field_foo') { - $time = date('H'); - $widget['type'] = $time < 12 ? 'widget_am' : 'widget_pm'; + $displays['description']['visible'] = FALSE; } } @@ -2257,12 +2449,14 @@ * The instance's widget properties. * @param $context * An associative array containing: - * - entity_type: The entity type; e.g. 'node' or 'user'. + * - entity_type: The entity type; e.g., 'node' or 'user'. * - entity: The entity object. * - field: The field that the widget belongs to. * - 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. @@ -2274,15 +2468,7 @@ } /** - * @} End of "ingroup field_storage" - */ - -/********************************************************************** - * Field CRUD API - **********************************************************************/ - -/** - * @ingroup field_crud + * @addtogroup field_crud * @{ */ @@ -2388,7 +2574,7 @@ * * @param $instance * The instance as it is post-update. - * @param $prior_$instance + * @param $prior_instance * The instance as it was pre-update. */ function hook_field_update_instance($instance, $prior_instance) { @@ -2416,7 +2602,7 @@ * @param $field * The field record just read from the database. */ -function hook_field_read_field(&$field) { +function hook_field_read_field($field) { // @todo Needs function body. } @@ -2462,7 +2648,7 @@ * @param $instance * The instance being purged. */ -function hook_field_purge_field_instance($instance) { +function hook_field_purge_instance($instance) { db_delete('my_module_field_instance_info') ->condition('id', $instance['id']) ->execute(); @@ -2476,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); @@ -2493,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') @@ -2514,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); @@ -2531,13 +2723,9 @@ } /** - * @} End of "ingroup field_crud" + * @} End of "addtogroup field_crud". */ -/********************************************************************** - * TODO: I'm not sure where these belong yet. - **********************************************************************/ - /** * Determine whether the user has access to a given field. * @@ -2557,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') { @@ -2564,3 +2754,7 @@ } return TRUE; } + +/** + * @} End of "addtogroup hooks". + */ diff -Naur drupal-7.0/modules/field/field.attach.inc drupal-7.66/modules/field/field.attach.inc --- drupal-7.0/modules/field/field.attach.inc 2010-12-07 06:09:58.000000000 +0100 +++ drupal-7.66/modules/field/field.attach.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ NULL, ); $options += $default_options; - $field_info = field_info_field_by_ids(); $fields = array(); $grouped_instances = array(); @@ -297,24 +306,26 @@ foreach ($instances as $instance) { $field_id = $instance['field_id']; $field_name = $instance['field_name']; - $field = $field_info[$field_id]; + $field = field_info_field_by_id($field_id); $function = $options['default'] ? 'field_default_' . $op : $field['module'] . '_field_' . $op; if (function_exists($function)) { // Add the field to the list of fields to invoke the hook on. if (!isset($fields[$field_id])) { $fields[$field_id] = $field; } - // Group the corresponding instances and entities. - $grouped_instances[$field_id][$id] = $instance; - $grouped_entities[$field_id][$id] = $entities[$id]; // Extract the field values into a separate variable, easily accessed // by hook implementations. // Unless a language suggestion is provided we iterate on all the // available languages. $available_languages = field_available_languages($entity_type, $field); - $languages = _field_language_suggestion($available_languages, $options['language'], $field_name); + $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(); + // Group the instances and entities corresponding to the current + // field. + $grouped_instances[$field_id][$langcode][$id] = $instance; + $grouped_entities[$field_id][$langcode][$id] = $entities[$id]; } } } @@ -327,8 +338,10 @@ $field_name = $field['field_name']; $function = $options['default'] ? 'field_default_' . $op : $field['module'] . '_field_' . $op; // Iterate over all the field translations. - foreach ($grouped_items[$field_id] as $langcode => $items) { - $results = $function($entity_type, $grouped_entities[$field_id], $field, $grouped_instances[$field_id], $langcode, $grouped_items[$field_id][$langcode], $a, $b); + foreach ($grouped_items[$field_id] as $langcode => &$items) { + $entities = $grouped_entities[$field_id][$langcode]; + $instances = $grouped_instances[$field_id][$langcode]; + $results = $function($entity_type, $entities, $field, $instances, $langcode, $items, $a, $b); if (isset($results)) { // Collect results by entity. // For hooks with array results, we merge results together. @@ -346,8 +359,8 @@ // Populate field values back in the entities, but avoid replacing missing // fields with an empty array (those are not equivalent on update). - foreach ($grouped_entities[$field_id] as $id => $entity) { - foreach ($grouped_items[$field_id] as $langcode => $items) { + foreach ($grouped_entities[$field_id] as $langcode => $entities) { + foreach ($entities as $id => $entity) { if ($grouped_items[$field_id][$langcode][$id] !== array() || isset($entity->{$field_name}[$langcode])) { $entity->{$field_name}[$langcode] = $grouped_items[$field_id][$langcode][$id]; } @@ -541,16 +554,23 @@ * @param $langcode * The language the field values are going to be entered, if no language * is provided the default site language will be used. + * @param array $options + * An associative array of additional options. See _field_invoke() for + * details. * * @see field_form_get_state() * @see field_form_set_state() */ -function field_attach_form($entity_type, $entity, &$form, &$form_state, $langcode = NULL) { +function field_attach_form($entity_type, $entity, &$form, &$form_state, $langcode = NULL, $options = array()) { + // Validate $options since this is a new parameter added after Drupal 7 was + // released. + $options = is_array($options) ? $options : array(); + // Set #parents to 'top-level' by default. $form += array('#parents' => array()); // If no language is provided use the default site language. - $options = array('language' => field_valid_language($langcode)); + $options['language'] = field_valid_language($langcode); $form += (array) _field_invoke_default('form', $entity_type, $entity, $form, $form_state, $options); // Add custom weight handling. @@ -600,7 +620,6 @@ * non-deleted fields are operated on. */ function field_attach_load($entity_type, $entities, $age = FIELD_LOAD_CURRENT, $options = array()) { - $field_info = field_info_field_by_ids(); $load_current = $age == FIELD_LOAD_CURRENT; // Merge default options. @@ -678,7 +697,7 @@ } // Collect the storage backend if the field has not been loaded yet. if (!isset($skip_fields[$field_id])) { - $field = $field_info[$field_id]; + $field = field_info_field_by_id($field_id); $storages[$field['storage']['type']][$field_id][] = $load_current ? $id : $vid; } } @@ -691,10 +710,11 @@ } // Invoke field-type module's hook_field_load(). - _field_invoke_multiple('load', $entity_type, $queried_entities, $age, $options); + $null = NULL; + _field_invoke_multiple('load', $entity_type, $queried_entities, $age, $null, $options); // Invoke hook_field_attach_load(): let other modules act on loading the - // entitiy. + // entity. module_invoke_all('field_attach_load', $entity_type, $queried_entities, $age, $options); // Build cache data. @@ -754,13 +774,21 @@ * If validation errors are found, a FieldValidationException is thrown. The * 'errors' property contains the array of errors, keyed by field name, * language and delta. + * @param array $options + * An associative array of additional options. See _field_invoke() for + * details. */ -function field_attach_validate($entity_type, $entity) { +function field_attach_validate($entity_type, $entity, $options = array()) { + // Validate $options since this is a new parameter added after Drupal 7 was + // released. + $options = is_array($options) ? $options : array(); + $errors = array(); // Check generic, field-type-agnostic errors first. - _field_invoke_default('validate', $entity_type, $entity, $errors); + $null = NULL; + _field_invoke_default('validate', $entity_type, $entity, $errors, $null, $options); // Check field-type specific errors. - _field_invoke('validate', $entity_type, $entity, $errors); + _field_invoke('validate', $entity_type, $entity, $errors, $null, $options); // Let other modules validate the entity. // Avoid module_invoke_all() to let $errors be taken by reference. @@ -780,11 +808,11 @@ * There are two levels of validation for fields in forms: widget * validation, and field validation. * - Widget validation steps are specific to a given widget's own form - * structure and UI metaphors. They are executed through FAPI's - * #element_validate property during normal form validation. + * structure and UI metaphors. They are executed through FAPI's + * #element_validate property during normal form validation. * - Field validation steps are common to a given field type, independently of - * the specific widget being used in a given form. They are defined in the - * field type's implementation of hook_field_validate(). + * the specific widget being used in a given form. They are defined in the + * field type's implementation of hook_field_validate(). * * This function performs field validation in the context of a form * submission. It converts field validation errors into form errors @@ -802,14 +830,21 @@ * full form structure, or a sub-element of a larger form. * @param $form_state * An associative array containing the current state of the form. + * @param array $options + * An associative array of additional options. See _field_invoke() for + * details. */ -function field_attach_form_validate($entity_type, $entity, $form, &$form_state) { +function field_attach_form_validate($entity_type, $entity, $form, &$form_state, $options = array()) { + // Validate $options since this is a new parameter added after Drupal 7 was + // released. + $options = is_array($options) ? $options : array(); + // Extract field values from submitted values. _field_invoke_default('extract_form_values', $entity_type, $entity, $form, $form_state); // Perform field_level validation. try { - field_attach_validate($entity_type, $entity); + field_attach_validate($entity_type, $entity, $options); } catch (FieldValidationException $e) { // Pass field-level validation errors back to widgets for accurate error @@ -821,7 +856,7 @@ field_form_set_state($form['#parents'], $field_name, $langcode, $form_state, $field_state); } } - _field_invoke_default('form_errors', $entity_type, $entity, $form, $form_state); + _field_invoke_default('form_errors', $entity_type, $entity, $form, $form_state, $options); } } @@ -842,12 +877,19 @@ * full form structure, or a sub-element of a larger form. * @param $form_state * An associative array containing the current state of the form. + * @param array $options + * An associative array of additional options. See _field_invoke() for + * details. */ -function field_attach_submit($entity_type, $entity, $form, &$form_state) { +function field_attach_submit($entity_type, $entity, $form, &$form_state, $options = array()) { + // Validate $options since this is a new parameter added after Drupal 7 was + // released. + $options = is_array($options) ? $options : array(); + // Extract field values from submitted values. - _field_invoke_default('extract_form_values', $entity_type, $entity, $form, $form_state); + _field_invoke_default('extract_form_values', $entity_type, $entity, $form, $form_state, $options); - _field_invoke_default('submit', $entity_type, $entity, $form, $form_state); + _field_invoke_default('submit', $entity_type, $entity, $form, $form_state, $options); // Let other modules act on submitting the entity. // Avoid module_invoke_all() to let $form_state be taken by reference. @@ -878,7 +920,7 @@ /** * Save field data for a new entity. * - * The passed in entity must already contain its id and (if applicable) + * The passed-in entity must already contain its id and (if applicable) * revision id attributes. * Default values (if any) will be saved for fields not present in the * $entity. @@ -934,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 @@ -1075,8 +1123,20 @@ * An array of entities, keyed by entity id. * @param $view_mode * View mode, e.g. 'full', 'teaser'... + * @param $langcode + * (Optional) The language the field values are to be shown in. If no language + * is provided the current language is used. + * @param array $options + * An associative array of additional options. See _field_invoke() for + * details. */ -function field_attach_prepare_view($entity_type, $entities, $view_mode) { +function field_attach_prepare_view($entity_type, $entities, $view_mode, $langcode = NULL, $options = array()) { + // Validate $options since this is a new parameter added after Drupal 7 was + // released. + $options = is_array($options) ? $options : array(); + + $options['language'] = array(); + // To ensure hooks are only run once per entity, only process items without // the _field_view_prepared flag. // @todo: resolve this more generally for both entity and field level hooks. @@ -1086,17 +1146,22 @@ // Add this entity to the items to be prepared. $prepare[$id] = $entity; + // Determine the actual language to display for each field, given the + // languages available in the field data. + $options['language'][$id] = field_language($entity_type, $entity, NULL, $langcode); + // Mark this item as prepared. $entity->_field_view_prepared = TRUE; } } + $null = NULL; // First let the field types do their preparation. - _field_invoke_multiple('prepare_view', $entity_type, $prepare); + _field_invoke_multiple('prepare_view', $entity_type, $prepare, $null, $null, $options); // Then let the formatters do their own specific massaging. // field_default_prepare_view() takes care of dispatching to the correct // formatters according to the display settings for the view mode. - _field_invoke_multiple_default('prepare_view', $entity_type, $prepare, $view_mode); + _field_invoke_multiple_default('prepare_view', $entity_type, $prepare, $view_mode, $null, $options); } /** @@ -1142,14 +1207,21 @@ * @param $langcode * The language the field values are to be shown in. If no language is * provided the current language is used. + * @param array $options + * An associative array of additional options. See _field_invoke() for + * details. * @return * A renderable array for the field values. */ -function field_attach_view($entity_type, $entity, $view_mode, $langcode = NULL) { +function field_attach_view($entity_type, $entity, $view_mode, $langcode = NULL, $options = array()) { + // Validate $options since this is a new parameter added after Drupal 7 was + // released. + $options = is_array($options) ? $options : array(); + // Determine the actual language to display for each field, given the // languages available in the field data. $display_language = field_language($entity_type, $entity, NULL, $langcode); - $options = array('language' => $display_language); + $options['language'] = $display_language; // Invoke field_default_view(). $null = NULL; @@ -1296,12 +1368,9 @@ field_cache_clear(); // Update bundle settings. - $settings = variable_get('field_bundle_settings', array()); - if (isset($settings[$entity_type][$bundle_old])) { - $settings[$entity_type][$bundle_new] = $settings[$entity_type][$bundle_old]; - unset($settings[$entity_type][$bundle_old]); - variable_set('field_bundle_settings', $settings); - } + $settings = variable_get('field_bundle_settings_' . $entity_type . '__' . $bundle_old, array()); + variable_set('field_bundle_settings_' . $entity_type . '__' . $bundle_new, $settings); + variable_del('field_bundle_settings_' . $entity_type . '__' . $bundle_old); // Let other modules act on renaming the bundle. module_invoke_all('field_attach_rename_bundle', $entity_type, $bundle_old, $bundle_new); @@ -1323,8 +1392,10 @@ * The bundle to delete. */ function field_attach_delete_bundle($entity_type, $bundle) { - // First, delete the instances themseves. - $instances = field_info_instances($entity_type, $bundle); + // First, delete the instances themselves. field_read_instances() must be + // used here since field_info_instances() does not return instances for + // disabled entity types or bundles. + $instances = field_read_instances(array('entity_type' => $entity_type, 'bundle' => $bundle), array('include_inactive' => 1)); foreach ($instances as $instance) { field_delete_instance($instance); } @@ -1333,11 +1404,7 @@ field_cache_clear(); // Clear bundle display settings. - $settings = variable_get('field_bundle_settings', array()); - if (isset($settings[$entity_type][$bundle])) { - unset($settings[$entity_type][$bundle]); - variable_set('field_bundle_settings', $settings); - } + variable_del('field_bundle_settings_' . $entity_type . '__' . $bundle); // Let other modules act on deleting the bundle. module_invoke_all('field_attach_delete_bundle', $entity_type, $bundle, $instances); @@ -1345,5 +1412,5 @@ /** - * @} End of "defgroup field_attach" + * @} End of "defgroup field_attach". */ diff -Naur drupal-7.0/modules/field/field.crud.inc drupal-7.66/modules/field/field.crud.inc --- drupal-7.0/modules/field/field.crud.inc 2011-01-02 18:26:39.000000000 +0100 +++ drupal-7.66/modules/field/field.crud.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ $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() @@ -186,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); @@ -241,9 +244,11 @@ // $prior_field may no longer be right. module_load_install($field['module']); $schema = (array) module_invoke($field['module'], 'field_schema', $field); - $schema += array('columns' => array(), 'indexes' => array()); + $schema += array('columns' => array(), 'indexes' => array(), 'foreign keys' => array()); // 'columns' are hardcoded in the field type. $field['columns'] = $schema['columns']; + // 'foreign keys' are hardcoded in the field type. + $field['foreign keys'] = $schema['foreign keys']; // 'indexes' can be both hardcoded in the field type, and specified in the // incoming $field definition. $field += array( @@ -283,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); @@ -316,7 +321,11 @@ * Reads in fields that match an array of conditions. * * @param array $params - * An array of conditions to match against. + * An array of conditions to match against. Keys are columns from the + * 'field_config' table, values are conditions to match. Additionally, + * conditions on the 'entity_type' and 'bundle' columns from the + * 'field_config_instance' table are supported (select fields having an + * instance on a given bundle). * @param array $include_additional * The default behavior of this function is to not return fields that * are inactive or have been deleted. Setting @@ -334,8 +343,21 @@ // Turn the conditions into a query. foreach ($params as $key => $value) { + // Allow filtering on the 'entity_type' and 'bundle' columns of the + // field_config_instance table. + if ($key == 'entity_type' || $key == 'bundle') { + if (empty($fci_join)) { + $fci_join = $query->join('field_config_instance', 'fci', 'fc.id = fci.field_id'); + } + $key = 'fci.' . $key; + } + else { + $key = 'fc.' . $key; + } + $query->condition($key, $value); } + if (!isset($include_additional['include_inactive']) || !$include_additional['include_inactive']) { $query ->condition('fc.active', 1) @@ -408,7 +430,7 @@ ->execute(); // Clear the cache. - field_cache_clear(TRUE); + field_cache_clear(); module_invoke_all('field_delete_field', $field); } @@ -422,7 +444,6 @@ * will be given the following default values: * - label: the field name * - description: empty string - * - weight: 0 * - required: FALSE * - default_value_function: empty string * - settings: each omitted setting is given the default value specified in @@ -444,8 +465,8 @@ * * @return * The $instance array with the id property filled in. - * @throw - * FieldException + * + * @throws FieldException * * See: @link field Field API data structures @endlink. */ @@ -503,18 +524,30 @@ * Updates an instance of a field. * * @param $instance - * An associative array representing an instance structure. The required - * keys and values are: + * An associative array representing an instance structure. The following + * required array elements specify which field instance is being updated: * - entity_type: The type of the entity the field is attached to. * - bundle: The bundle this field belongs to. * - field_name: The name of an existing field. - * Read-only_id properties are assigned automatically. Any other - * properties specified in $instance overwrite the existing values for - * the instance. + * The other array elements represent properties of the instance, and all + * properties must be specified or their default values will be used (except + * internal-use properties, which are assigned automatically). To avoid + * losing the previously stored properties of the instance when making a + * change, first load the instance with field_info_instance(), then override + * the values you want to override, and finally save using this function. + * Example: + * @code + * // 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['required'] = TRUE; + * // Write the changed definition back. + * field_update_instance($instance_info); + * @endcode * - * @throw - * FieldException + * @throws FieldException * + * @see field_info_instance() * @see field_create_instance() */ function field_update_instance($instance) { @@ -635,12 +668,11 @@ } /** - * Reads a single instance record directly from the database. + * Reads a single instance record from the database. * - * Generally, you should use the field_info_instance() instead. - * - * This function will not return deleted instances. Use - * field_read_instances() instead for this purpose. + * Generally, you should use field_info_instance() instead, as it + * provides caching and allows other modules the opportunity to + * append additional formatters, widgets, and other information. * * @param $entity_type * The type of entity to which the field is bound. @@ -730,7 +762,7 @@ * An instance structure. * @param $field_cleanup * If TRUE, the field will be deleted as well if its last instance is being - * deleted. If FALSE, it is the caller's responsability to handle the case of + * deleted. If FALSE, it is the caller's responsibility to handle the case of * fields left without instances. Defaults to TRUE. */ function field_delete_instance($instance, $field_cleanup = TRUE) { @@ -827,16 +859,20 @@ * ), * ); * @endcode + * + * See @link field Field API @endlink for information about the other parts of + * the Field API. */ /** * Purges a batch of deleted Field API data, instances, or fields. * - * This function will purge deleted field data on up to a specified maximum - * number of entities and then return. If a deleted field instance with no - * remaining data records is found, the instance itself will be purged. - * If a deleted field with no remaining field instances is found, the field - * itself will be purged. + * This function will purge deleted field data in batches. The batch size + * is defined as an argument to the function, and once each batch is finished, + * it continues with the next batch until all have completed. If a deleted field + * instance with no remaining data records is found, the instance itself will + * be purged. If a deleted field with no remaining field instances is found, the + * field itself will be purged. * * @param $batch_size * The maximum number of field data records to purge before returning. diff -Naur drupal-7.0/modules/field/field.default.inc drupal-7.66/modules/field/field.default.inc --- drupal-7.0/modules/field/field.default.inc 2010-11-21 20:09:18.000000000 +0100 +++ drupal-7.66/modules/field/field.default.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ $field['cardinality']) { $errors[$field['field_name']][$langcode][0][] = array( 'error' => 'field_cardinality', - 'message' => t('%name: this field cannot hold more than @count values.', array('%name' => t($instance['label']), '@count' => $field['cardinality'])), + 'message' => t('%name: this field cannot hold more than @count values.', array('%name' => $instance['label'], '@count' => $field['cardinality'])), ); } } @@ -135,21 +134,25 @@ * - the name of a view mode * - or an array of display settings to use for display, as found in the * 'display' entry of $instance definitions. -*/ + */ function field_default_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items, $display) { // Group entities, instances and items by formatter module. $modules = array(); foreach ($instances as $id => $instance) { if (is_string($display)) { $view_mode = $display; - $display = field_get_display($instance, $view_mode, $entities[$id]); + $instance_display = field_get_display($instance, $view_mode, $entities[$id]); + } + else { + $instance_display = $display; } - if ($display['type'] !== 'hidden') { - $module = $display['module']; + + if ($instance_display['type'] !== 'hidden') { + $module = $instance_display['module']; $modules[$module] = $module; $grouped_entities[$module][$id] = $entities[$id]; $grouped_instances[$module][$id] = $instance; - $grouped_displays[$module][$id] = $display; + $grouped_displays[$module][$id] = $instance_display; // hook_field_formatter_prepare_view() alters $items by reference. $grouped_items[$module][$id] = &$items[$id]; } @@ -165,17 +168,16 @@ } /** - * Builds a renderable array for field values. + * Builds a renderable array for one field on one entity instance. * * @param $entity_type * The type of $entity; e.g. 'node' or 'user'. - * @param $entities - * An array of entities being displayed, keyed by entity id. + * @param $entity + * A single object of type $entity_type. * @param $field * The field structure for the operation. - * @param $instances - * Array of instance structures for $field for each entity, keyed by entity - * id. + * @param $instance + * An array containing each field on $entity's bundle. * @param $langcode * The language associated to $items. * @param $items @@ -211,7 +213,7 @@ $info = array( '#theme' => 'field', '#weight' => $display['weight'], - '#title' => t($instance['label']), + '#title' => $instance['label'], '#access' => field_access('view', $field, $entity_type, $entity), '#label_display' => $display['label'], '#view_mode' => $view_mode, diff -Naur drupal-7.0/modules/field/field.form.inc drupal-7.66/modules/field/field.form.inc --- drupal-7.0/modules/field/field.form.inc 2010-11-20 20:57:01.000000000 +0100 +++ drupal-7.66/modules/field/field.form.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ $field, - 'instance' => $instance, - 'items_count' => count($items), - 'array_parents' => array(), - 'errors' => array(), - ); - field_form_set_state($parents, $field_name, $langcode, $form_state, $field_state); - } - // If field module handles multiple values for this form element, and we - // are displaying an individual element, process the multiple value form. - if (!isset($get_delta) && field_behaviors_widget('multiple values', $instance) == FIELD_BEHAVIOR_DEFAULT) { - $elements = field_multiple_value_form($field, $instance, $langcode, $items, $form, $form_state); - } - // If the widget is handling multiple values (e.g Options), or if we are - // displaying an individual element, just get a single form element and - // make it the $delta value. - else { - $delta = isset($get_delta) ? $get_delta : 0; - $function = $instance['widget']['module'] . '_field_widget_form'; - if (function_exists($function)) { - $element = array( - '#entity_type' => $instance['entity_type'], - '#bundle' => $instance['bundle'], - '#field_name' => $field_name, - '#language' => $langcode, - '#field_parents' => $parents, - '#columns' => array_keys($field['columns']), - '#title' => check_plain(t($instance['label'])), - '#description' => field_filter_xss($instance['description']), - // Only the first widget should be required. - '#required' => $delta == 0 && $instance['required'], - '#delta' => $delta, + // Store field information in $form_state. + if (!field_form_get_state($parents, $field_name, $langcode, $form_state)) { + $field_state = array( + 'field' => $field, + 'instance' => $instance, + 'items_count' => count($items), + 'array_parents' => array(), + 'errors' => array(), + ); + field_form_set_state($parents, $field_name, $langcode, $form_state, $field_state); + } + + // If field module handles multiple values for this form element, and we are + // displaying an individual element, process the multiple value form. + if (!isset($get_delta) && field_behaviors_widget('multiple values', $instance) == FIELD_BEHAVIOR_DEFAULT) { + // Store the entity in the form. + $form['#entity'] = $entity; + $elements = field_multiple_value_form($field, $instance, $langcode, $items, $form, $form_state); + } + // If the widget is handling multiple values (e.g Options), or if we are + // displaying an individual element, just get a single form element and make + // it the $delta value. + else { + $delta = isset($get_delta) ? $get_delta : 0; + $function = $instance['widget']['module'] . '_field_widget_form'; + if (function_exists($function)) { + $element = array( + '#entity' => $entity, + '#entity_type' => $instance['entity_type'], + '#bundle' => $instance['bundle'], + '#field_name' => $field_name, + '#language' => $langcode, + '#field_parents' => $parents, + '#columns' => array_keys($field['columns']), + '#title' => check_plain($instance['label']), + '#description' => field_filter_xss($instance['description']), + // Only the first widget should be required. + '#required' => $delta == 0 && $instance['required'], + '#delta' => $delta, + ); + if ($element = $function($form, $form_state, $field, $instance, $langcode, $items, $delta, $element)) { + // Allow modules to alter the field widget form element. + $context = array( + 'form' => $form, + 'field' => $field, + 'instance' => $instance, + 'langcode' => $langcode, + 'items' => $items, + 'delta' => $delta, ); - if ($element = $function($form, $form_state, $field, $instance, $langcode, $items, $delta, $element)) { - // If we're processing a specific delta value for a field where the - // field module handles multiples, set the delta in the result. - // For fields that handle their own processing, we can't make - // assumptions about how the field is structured, just merge in the - // returned element. - if (field_behaviors_widget('multiple values', $instance) == FIELD_BEHAVIOR_DEFAULT) { - $elements[$delta] = $element; - } - else { - $elements = $element; - } + drupal_alter(array('field_widget_form', 'field_widget_' . $instance['widget']['type'] . '_form'), $element, $form_state, $context); + + // If we're processing a specific delta value for a field where the + // field module handles multiples, set the delta in the result. + // For fields that handle their own processing, we can't make + // assumptions about how the field is structured, just merge in the + // returned element. + if (field_behaviors_widget('multiple values', $instance) == FIELD_BEHAVIOR_DEFAULT) { + $elements[$delta] = $element; + } + else { + $elements = $element; } } } } - if ($elements) { - // Also aid in theming of field widgets by rendering a classified - // container. - $addition[$field_name] = array( - '#type' => 'container', - '#attributes' => array( - 'class' => array( - 'field-type-' . drupal_html_class($field['type']), - 'field-name-' . drupal_html_class($field_name), - 'field-widget-' . drupal_html_class($instance['widget']['type']), - ), + // Also aid in theming of field widgets by rendering a classified container. + $addition[$field_name] = array( + '#type' => 'container', + '#attributes' => array( + 'class' => array( + 'field-type-' . drupal_html_class($field['type']), + 'field-name-' . drupal_html_class($field_name), + 'field-widget-' . drupal_html_class($instance['widget']['type']), ), - '#weight' => $instance['widget']['weight'], - ); - } + ), + '#weight' => $instance['widget']['weight'], + ); // Populate the 'array_parents' information in $form_state['field'] after // the form is built, so that we catch changes in the form structure performed @@ -123,6 +163,7 @@ // when $langcode is unknown. '#language' => $langcode, $langcode => $elements, + '#access' => field_access('edit', $field, $entity_type, $entity), ); return $addition; @@ -152,8 +193,8 @@ break; } - $title = check_plain(t($instance['label'])); - $description = field_filter_xss(t($instance['description'])); + $title = check_plain($instance['label']); + $description = field_filter_xss($instance['description']); $id_prefix = implode('-', array_merge($parents, array($field_name))); $wrapper_id = drupal_html_id($id_prefix . '-add-more-wrapper'); @@ -166,6 +207,7 @@ $multiple = $field['cardinality'] > 1 || $field['cardinality'] == FIELD_CARDINALITY_UNLIMITED; $element = array( '#entity_type' => $instance['entity_type'], + '#entity' => $form['#entity'], '#bundle' => $instance['bundle'], '#field_name' => $field_name, '#language' => $langcode, @@ -194,6 +236,18 @@ '#weight' => 100, ); } + + // Allow modules to alter the field widget form element. + $context = array( + 'form' => $form, + 'field' => $field, + 'instance' => $instance, + 'langcode' => $langcode, + 'items' => $items, + 'delta' => $delta, + ); + drupal_alter(array('field_widget_form', 'field_widget_' . $instance['widget']['type'] . '_form'), $element, $form_state, $context); + $field_elements[$delta] = $element; } } @@ -251,11 +305,11 @@ if ($element['#cardinality'] > 1 || $element['#cardinality'] == FIELD_CARDINALITY_UNLIMITED) { $table_id = drupal_html_id($element['#field_name'] . '_values'); $order_class = $element['#field_name'] . '-delta-order'; - $required = !empty($element['#required']) ? '*' : ''; + $required = !empty($element['#required']) ? theme('form_required_marker', $variables) : ''; $header = array( array( - 'data' => '", + 'data' => '", 'colspan' => 2, 'class' => array('field-label'), ), @@ -336,31 +390,33 @@ $field_state = field_form_get_state($form['#parents'], $field['field_name'], $langcode, $form_state); if (!empty($field_state['errors'])) { - $function = $instance['widget']['module'] . '_field_widget_error'; - $function_exists = function_exists($function); - - // Locate the correct element in the the form. + // Locate the correct element in the form. $element = drupal_array_get_nested_value($form_state['complete form'], $field_state['array_parents']); - - $multiple_widget = field_behaviors_widget('multiple values', $instance) != FIELD_BEHAVIOR_DEFAULT; - foreach ($field_state['errors'] as $delta => $delta_errors) { - // For multiple single-value widgets, pass errors by delta. - // For a multiple-value widget, all errors are passed to the main widget. - $error_element = $multiple_widget ? $element : $element[$delta]; - foreach ($delta_errors as $error) { - if ($function_exists) { - $function($error_element, $error, $form, $form_state); - } - else { - // Make sure that errors are reported (even incorrectly flagged) if - // the widget module fails to implement hook_field_widget_error(). - form_error($error_element, $error['error']); + // Only set errors if the element is accessible. + if (!isset($element['#access']) || $element['#access']) { + $function = $instance['widget']['module'] . '_field_widget_error'; + $function_exists = function_exists($function); + + $multiple_widget = field_behaviors_widget('multiple values', $instance) != FIELD_BEHAVIOR_DEFAULT; + foreach ($field_state['errors'] as $delta => $delta_errors) { + // For multiple single-value widgets, pass errors by delta. + // For a multiple-value widget, pass all errors to the main widget. + $error_element = $multiple_widget ? $element : $element[$delta]; + foreach ($delta_errors as $error) { + if ($function_exists) { + $function($error_element, $error, $form, $form_state); + } + else { + // Make sure that errors are reported (even incorrectly flagged) if + // the widget module fails to implement hook_field_widget_error(). + form_error($error_element, $error['message']); + } } } + // Reinitialize the errors list for the next submit. + $field_state['errors'] = array(); + field_form_set_state($form['#parents'], $field['field_name'], $langcode, $form_state, $field_state); } - // Reinitialize the errors list for the next submit. - $field_state['errors'] = array(); - field_form_set_state($form['#parents'], $field['field_name'], $langcode, $form_state, $field_state); } } @@ -374,7 +430,7 @@ * to return just the changed part of the form. */ function field_add_more_submit($form, &$form_state) { - $button = $form_state['clicked_button']; + $button = $form_state['triggering_element']; // Go one level up in the form, to the widgets container. $element = drupal_array_get_nested_value($form, array_slice($button['#array_parents'], 0, -1)); @@ -399,7 +455,7 @@ * @see field_add_more_submit() */ function field_add_more_js($form, $form_state) { - $button = $form_state['clicked_button']; + $button = $form_state['triggering_element']; // Go one level up in the form, to the widgets container. $element = drupal_array_get_nested_value($form, array_slice($button['#array_parents'], 0, -1)); @@ -414,7 +470,7 @@ return; } - // Add a DIV around the delta receiving the AJAX effect. + // Add a DIV around the delta receiving the Ajax effect. $delta = $element['#max_delta']; $element[$delta]['#prefix'] = '
' . (isset($element[$delta]['#prefix']) ? $element[$delta]['#prefix'] : ''); $element[$delta]['#suffix'] = (isset($element[$delta]['#suffix']) ? $element[$delta]['#suffix'] : '') . '
'; diff -Naur drupal-7.0/modules/field/field.info drupal-7.66/modules/field/field.info --- drupal-7.0/modules/field/field.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/field/field.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: field.info,v 1.10 2010/12/20 19:59:41 webchick Exp $ name = Field description = Field API to add fields to entities like nodes and users. package = Core @@ -6,13 +5,13 @@ core = 7.x files[] = field.module files[] = field.attach.inc +files[] = field.info.class.inc files[] = tests/field.test dependencies[] = field_sql_storage required = TRUE stylesheets[all][] = theme/field.css -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/field/field.info.class.inc drupal-7.66/modules/field/field.info.class.inc --- drupal-7.0/modules/field/field.info.class.inc 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/field/field.info.class.inc 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,686 @@ +fieldMap = NULL; + + $this->fieldsById = array(); + $this->fieldIdsByName = array(); + $this->loadedAllFields = FALSE; + $this->unknownFields = array(); + + $this->bundleInstances = array(); + $this->loadedAllInstances = FALSE; + $this->emptyBundles = array(); + + $this->bundleExtraFields = array(); + + cache_clear_all('field_info:', 'cache_field', TRUE); + } + + /** + * Collects a lightweight map of fields across bundles. + * + * @return + * An array keyed by field name. Each value is an array with two entries: + * - type: The field type. + * - bundles: The bundles in which the field appears, as an array with + * entity types as keys and the array of bundle names as values. + */ + public function getFieldMap() { + // Read from the "static" cache. + if ($this->fieldMap !== NULL) { + return $this->fieldMap; + } + + // Read from persistent cache. + if ($cached = cache_get('field_info:field_map', 'cache_field')) { + $map = $cached->data; + + // Save in "static" cache. + $this->fieldMap = $map; + + return $map; + } + + $map = array(); + + $query = db_query('SELECT fc.type, fci.field_name, fci.entity_type, fci.bundle FROM {field_config_instance} fci INNER JOIN {field_config} fc ON fc.id = fci.field_id WHERE fc.active = 1 AND fc.storage_active = 1 AND fc.deleted = 0 AND fci.deleted = 0'); + foreach ($query as $row) { + $map[$row->field_name]['bundles'][$row->entity_type][] = $row->bundle; + $map[$row->field_name]['type'] = $row->type; + } + + // Save in "static" and persistent caches. + $this->fieldMap = $map; + if (lock_acquire('field_info:field_map')) { + cache_set('field_info:field_map', $map, 'cache_field'); + lock_release('field_info:field_map'); + } + + return $map; + } + + /** + * Returns all active fields, including deleted ones. + * + * @return + * An array of field definitions, keyed by field ID. + */ + public function getFields() { + // Read from the "static" cache. + if ($this->loadedAllFields) { + return $this->fieldsById; + } + + // Read from persistent cache. + if ($cached = cache_get('field_info:fields', 'cache_field')) { + $this->fieldsById = $cached->data; + } + else { + // Collect and prepare fields. + foreach (field_read_fields(array(), array('include_deleted' => TRUE)) as $field) { + $this->fieldsById[$field['id']] = $this->prepareField($field); + } + + // Store in persistent cache. + 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. + foreach ($this->fieldsById as $field) { + if (!$field['deleted']) { + $this->fieldIdsByName[$field['field_name']] = $field['id']; + } + } + + $this->loadedAllFields = TRUE; + + return $this->fieldsById; + } + + /** + * Retrieves all active, non-deleted instances definitions. + * + * @param $entity_type + * (optional) The entity type. + * + * @return + * If $entity_type is not set, all instances keyed by entity type and bundle + * name. If $entity_type is set, all instances for that entity type, keyed + * by bundle name. + */ + public function getInstances($entity_type = NULL) { + // If the full list is not present in "static" cache yet. + if (!$this->loadedAllInstances) { + + // Read from persistent cache. + if ($cached = cache_get('field_info:instances', 'cache_field')) { + $this->bundleInstances = $cached->data; + } + else { + // Collect and prepare instances. + + // We also need to populate the static field cache, since it will not + // be set by subsequent getBundleInstances() calls. + $this->getFields(); + + // Initialize empty arrays for all existing entity types and bundles. + // This is not strictly needed, but is done to preserve the behavior of + // field_info_instances() before http://drupal.org/node/1915646. + foreach (field_info_bundles() as $existing_entity_type => $bundles) { + foreach ($bundles as $bundle => $bundle_info) { + $this->bundleInstances[$existing_entity_type][$bundle] = array(); + } + } + + foreach (field_read_instances() as $instance) { + $field = $this->getField($instance['field_name']); + $instance = $this->prepareInstance($instance, $field['type']); + $this->bundleInstances[$instance['entity_type']][$instance['bundle']][$instance['field_name']] = $instance; + } + + // Store in persistent cache. + if (lock_acquire('field_info:instances')) { + cache_set('field_info:instances', $this->bundleInstances, 'cache_field'); + lock_release('field_info:instances'); + } + } + + $this->loadedAllInstances = TRUE; + } + + if (isset($entity_type)) { + return isset($this->bundleInstances[$entity_type]) ? $this->bundleInstances[$entity_type] : array(); + } + else { + return $this->bundleInstances; + } + } + + /** + * Returns a field definition from a field name. + * + * This method only retrieves active, non-deleted fields. + * + * @param $field_name + * The field name. + * + * @return + * The field definition, or NULL if no field was found. + */ + public function getField($field_name) { + // Read from the "static" cache. + if (isset($this->fieldIdsByName[$field_name])) { + $field_id = $this->fieldIdsByName[$field_name]; + return $this->fieldsById[$field_id]; + } + if (isset($this->unknownFields[$field_name])) { + return; + } + + // Do not check the (large) persistent cache, but read the definition. + + // Cache miss: read from definition. + if ($field = field_read_field($field_name)) { + $field = $this->prepareField($field); + + // Save in the "static" cache. + $this->fieldsById[$field['id']] = $field; + $this->fieldIdsByName[$field['field_name']] = $field['id']; + + return $field; + } + else { + $this->unknownFields[$field_name] = TRUE; + } + } + + /** + * Returns a field definition from a field ID. + * + * This method only retrieves active fields, deleted or not. + * + * @param $field_id + * The field ID. + * + * @return + * The field definition, or NULL if no field was found. + */ + public function getFieldById($field_id) { + // Read from the "static" cache. + if (isset($this->fieldsById[$field_id])) { + return $this->fieldsById[$field_id]; + } + if (isset($this->unknownFields[$field_id])) { + return; + } + + // No persistent cache, fields are only persistently cached as part of a + // bundle. + + // Cache miss: read from definition. + if ($fields = field_read_fields(array('id' => $field_id), array('include_deleted' => TRUE))) { + $field = current($fields); + $field = $this->prepareField($field); + + // Store in the static cache. + $this->fieldsById[$field['id']] = $field; + if (!$field['deleted']) { + $this->fieldIdsByName[$field['field_name']] = $field['id']; + } + + return $field; + } + else { + $this->unknownFields[$field_id] = TRUE; + } + } + + /** + * Retrieves the instances for a bundle. + * + * The function also populates the corresponding field definitions in the + * "static" cache. + * + * @param $entity_type + * The entity type. + * @param $bundle + * The bundle name. + * + * @return + * The array of instance definitions, keyed by field name. + */ + public function getBundleInstances($entity_type, $bundle) { + // Read from the "static" cache. + if (isset($this->bundleInstances[$entity_type][$bundle])) { + return $this->bundleInstances[$entity_type][$bundle]; + } + if (isset($this->emptyBundles[$entity_type][$bundle])) { + return array(); + } + + // Read from the persistent cache. + if ($cached = cache_get("field_info:bundle:$entity_type:$bundle", 'cache_field')) { + $info = $cached->data; + + // Extract the field definitions and save them in the "static" cache. + foreach ($info['fields'] as $field) { + if (!isset($this->fieldsById[$field['id']])) { + $this->fieldsById[$field['id']] = $field; + if (!$field['deleted']) { + $this->fieldIdsByName[$field['field_name']] = $field['id']; + } + } + } + unset($info['fields']); + + // Store the instance definitions in the "static" cache'. Empty (or + // non-existent) bundles are stored separately, so that they do not + // pollute the global list returned by getInstances(). + if ($info['instances']) { + $this->bundleInstances[$entity_type][$bundle] = $info['instances']; + } + else { + $this->emptyBundles[$entity_type][$bundle] = TRUE; + } + + return $info['instances']; + } + + // Cache miss: collect from the definitions. + + $instances = array(); + + // Collect the fields in the bundle. + $params = array('entity_type' => $entity_type, 'bundle' => $bundle); + $fields = field_read_fields($params); + + // This iterates on non-deleted instances, so deleted fields are kept out of + // the persistent caches. + foreach (field_read_instances($params) as $instance) { + $field = $fields[$instance['field_name']]; + + $instance = $this->prepareInstance($instance, $field['type']); + $instances[$field['field_name']] = $instance; + + // If the field is not in our global "static" list yet, add it. + if (!isset($this->fieldsById[$field['id']])) { + $field = $this->prepareField($field); + + $this->fieldsById[$field['id']] = $field; + $this->fieldIdsByName[$field['field_name']] = $field['id']; + } + } + + // Store in the 'static' cache'. Empty (or non-existent) bundles are stored + // separately, so that they do not pollute the global list returned by + // getInstances(). + if ($instances) { + $this->bundleInstances[$entity_type][$bundle] = $instances; + } + else { + $this->emptyBundles[$entity_type][$bundle] = TRUE; + } + + // The persistent cache additionally contains the definitions of the fields + // involved in the bundle. + $cache = array( + 'instances' => $instances, + 'fields' => array() + ); + foreach ($instances as $instance) { + $cache['fields'][] = $this->fieldsById[$instance['field_id']]; + } + + 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; + } + + /** + * Retrieves the "extra fields" for a bundle. + * + * @param $entity_type + * The entity type. + * @param $bundle + * The bundle name. + * + * @return + * The array of extra fields. + */ + public function getBundleExtraFields($entity_type, $bundle) { + // Read from the "static" cache. + if (isset($this->bundleExtraFields[$entity_type][$bundle])) { + return $this->bundleExtraFields[$entity_type][$bundle]; + } + + // Read from the persistent cache. + if ($cached = cache_get("field_info:bundle_extra:$entity_type:$bundle", 'cache_field')) { + $this->bundleExtraFields[$entity_type][$bundle] = $cached->data; + return $this->bundleExtraFields[$entity_type][$bundle]; + } + + // Cache miss: read from hook_field_extra_fields(). Note: given the current + // shape of the hook, we have no other way than collecting extra fields on + // all bundles. + $info = array(); + $extra = module_invoke_all('field_extra_fields'); + drupal_alter('field_extra_fields', $extra); + // Merge in saved settings. + if (isset($extra[$entity_type][$bundle])) { + $info = $this->prepareExtraFields($extra[$entity_type][$bundle], $entity_type, $bundle); + } + + // Store in the 'static' and persistent caches. + $this->bundleExtraFields[$entity_type][$bundle] = $info; + 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]; + } + + /** + * Prepares a field definition for the current run-time context. + * + * @param $field + * The raw field structure as read from the database. + * + * @return + * The field definition completed for the current runtime context. + */ + public function prepareField($field) { + // Make sure all expected field settings are present. + $field['settings'] += field_info_field_settings($field['type']); + $field['storage']['settings'] += field_info_storage_settings($field['storage']['type']); + + // Add storage details. + $details = (array) module_invoke($field['storage']['module'], 'field_storage_details', $field); + drupal_alter('field_storage_details', $details, $field); + $field['storage']['details'] = $details; + + // Populate the list of bundles using the field. + $field['bundles'] = array(); + if (!$field['deleted']) { + $map = $this->getFieldMap(); + if (isset($map[$field['field_name']])) { + $field['bundles'] = $map[$field['field_name']]['bundles']; + } + } + + return $field; + } + + /** + * Prepares an instance definition for the current run-time context. + * + * @param $instance + * The raw instance structure as read from the database. + * @param $field_type + * The field type. + * + * @return + * The field instance array completed for the current runtime context. + */ + public function prepareInstance($instance, $field_type) { + // Make sure all expected instance settings are present. + $instance['settings'] += field_info_instance_settings($field_type); + + // Set a default value for the instance. + if (field_behaviors_widget('default value', $instance) == FIELD_BEHAVIOR_DEFAULT && !isset($instance['default_value'])) { + $instance['default_value'] = NULL; + } + + // Prepare widget settings. + $instance['widget'] = $this->prepareInstanceWidget($instance['widget'], $field_type); + + // Prepare display settings. + foreach ($instance['display'] as $view_mode => $display) { + $instance['display'][$view_mode] = $this->prepareInstanceDisplay($display, $field_type); + } + + // Fall back to 'hidden' for view modes configured to use custom display + // settings, and for which the instance has no explicit settings. + $entity_info = entity_get_info($instance['entity_type']); + $view_modes = array_merge(array('default'), array_keys($entity_info['view modes'])); + $view_mode_settings = field_view_mode_settings($instance['entity_type'], $instance['bundle']); + foreach ($view_modes as $view_mode) { + if ($view_mode == 'default' || !empty($view_mode_settings[$view_mode]['custom_settings'])) { + if (!isset($instance['display'][$view_mode])) { + $instance['display'][$view_mode] = array( + 'type' => 'hidden', + 'label' => 'above', + 'settings' => array(), + 'weight' => 0, + ); + } + } + } + + return $instance; + } + + /** + * Prepares widget properties for the current run-time context. + * + * @param $widget + * Widget specifications as found in $instance['widget']. + * @param $field_type + * The field type. + * + * @return + * The widget properties completed for the current runtime context. + */ + public function prepareInstanceWidget($widget, $field_type) { + $field_type_info = field_info_field_types($field_type); + + // Fill in default values. + $widget += array( + 'type' => $field_type_info['default_widget'], + 'settings' => array(), + 'weight' => 0, + ); + + $widget_type_info = field_info_widget_types($widget['type']); + // Fall back to default formatter if formatter type is not available. + if (!$widget_type_info) { + $widget['type'] = $field_type_info['default_widget']; + $widget_type_info = field_info_widget_types($widget['type']); + } + $widget['module'] = $widget_type_info['module']; + // Fill in default settings for the widget. + $widget['settings'] += field_info_widget_settings($widget['type']); + + return $widget; + } + + /** + * Adapts display specifications to the current run-time context. + * + * @param $display + * Display specifications as found in $instance['display']['a_view_mode']. + * @param $field_type + * The field type. + * + * @return + * The display properties completed for the current runtime context. + */ + public function prepareInstanceDisplay($display, $field_type) { + $field_type_info = field_info_field_types($field_type); + + // Fill in default values. + $display += array( + 'label' => 'above', + '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. + if (!$formatter_type_info) { + $display['type'] = $field_type_info['default_formatter']; + $formatter_type_info = field_info_formatter_types($display['type']); + } + $display['module'] = $formatter_type_info['module']; + // Fill in default settings for the formatter. + $display['settings'] += field_info_formatter_settings($display['type']); + } + + return $display; + } + + /** + * Prepares 'extra fields' for the current run-time context. + * + * @param $extra_fields + * The array of extra fields, as collected in hook_field_extra_fields(). + * @param $entity_type + * The entity type. + * @param $bundle + * The bundle name. + * + * @return + * The list of extra fields completed for the current runtime context. + */ + public function prepareExtraFields($extra_fields, $entity_type, $bundle) { + $entity_type_info = entity_get_info($entity_type); + $bundle_settings = field_bundle_settings($entity_type, $bundle); + $extra_fields += array('form' => array(), 'display' => array()); + + $result = array(); + // Extra fields in forms. + foreach ($extra_fields['form'] as $name => $field_data) { + $settings = isset($bundle_settings['extra_fields']['form'][$name]) ? $bundle_settings['extra_fields']['form'][$name] : array(); + if (isset($settings['weight'])) { + $field_data['weight'] = $settings['weight']; + } + $result['form'][$name] = $field_data; + } + + // Extra fields in displayed entities. + $data = $extra_fields['display']; + foreach ($extra_fields['display'] as $name => $field_data) { + $settings = isset($bundle_settings['extra_fields']['display'][$name]) ? $bundle_settings['extra_fields']['display'][$name] : array(); + $view_modes = array_merge(array('default'), array_keys($entity_type_info['view modes'])); + foreach ($view_modes as $view_mode) { + if (isset($settings[$view_mode])) { + $field_data['display'][$view_mode] = $settings[$view_mode]; + } + else { + $field_data['display'][$view_mode] = array( + 'weight' => $field_data['weight'], + 'visible' => TRUE, + ); + } + } + unset($field_data['weight']); + $result['display'][$name] = $field_data; + } + + return $result; + } +} diff -Naur drupal-7.0/modules/field/field.info.inc drupal-7.66/modules/field/field.info.inc --- drupal-7.0/modules/field/field.info.inc 2010-12-17 02:36:04.000000000 +0100 +++ drupal-7.66/modules/field/field.info.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ flush(); +} + +/** + * Collates all information on existing fields and instances. + * + * Deprecated. This function is kept to ensure backwards compatibility, but has + * a serious performance impact, and should be absolutely avoided. + * See http://drupal.org/node/1915646. + * + * Use the regular field_info_*() API functions to access the information, or + * field_info_cache_clear() to clear the cached data. + */ +function _field_info_collate_fields($reset = FALSE) { + if ($reset) { + _field_info_field_cache()->flush(); + return; + } + + $cache = _field_info_field_cache(); + + // Collect fields, and build the array of IDs keyed by field_name. + $fields = $cache->getFields(); + $field_ids = array(); + foreach ($fields as $id => $field) { + if (!$field['deleted']) { + $field_ids[$field['field_name']] = $id; + } + } + + // Collect extra fields for all entity types. + $extra_fields = array(); + foreach (field_info_bundles() as $entity_type => $bundles) { + foreach ($bundles as $bundle => $info) { + $extra_fields[$entity_type][$bundle] = $cache->getBundleExtraFields($entity_type, $bundle); + } + } + + return array( + 'fields' => $fields, + 'field_ids' => $field_ids, + 'instances' => $cache->getInstances(), + 'extra_fields' => $extra_fields, + ); } /** @@ -51,8 +123,8 @@ * the field type. * - 'widget types': Array of hook_field_widget_info() results, keyed by * widget_type. Each element has the following components: label, field - * types, settings, and behaviors from hook_field_widget_info(), as well - * as module, giving the module that exposes the widget type. + * types, settings, weight, and behaviors from hook_field_widget_info(), + * as well as module, giving the module that exposes the widget type. * - 'formatter types': Array of hook_field_formatter_info() results, keyed by * formatter_type. Each element has the following components: label, field * types, and behaviors from hook_field_formatter_info(), as well as @@ -121,6 +193,7 @@ } } drupal_alter('field_widget_info', $info['widget types']); + uasort($info['widget types'], 'drupal_sort_weight'); // Populate formatter types. foreach (module_implements('field_formatter_info') as $module) { @@ -150,96 +223,11 @@ } drupal_alter('field_storage_info', $info['storage types']); - cache_set("field_info_types:$langcode", $info, 'cache_field'); - } - } - - return $info; -} - -/** - * Collates all information on existing fields and instances. - * - * @param $reset - * If TRUE, clear the cache. The information will be rebuilt from the - * database next time it is needed. Defaults to FALSE. - * - * @return - * If $reset is TRUE, nothing. - * If $reset is FALSE, an array containing the following elements: - * - fields: Array of existing fields, keyed by field ID. This element - * lists deleted and non-deleted fields, but not inactive ones. - * Each field has an additional element, 'bundles', which is an array - * of all non-deleted instances of that field. - * - field_ids: Array of field IDs, keyed by field name. This element - * only lists non-deleted, active fields. - * - instances: Array of existing instances, keyed by entity type, bundle - * name and field name. This element only lists non-deleted instances - * whose field is active. - */ -function _field_info_collate_fields($reset = FALSE) { - static $info; - - if ($reset) { - $info = NULL; - cache_clear_all('field_info_fields', 'cache_field'); - return; - } - - if (!isset($info)) { - if ($cached = cache_get('field_info_fields', 'cache_field')) { - $info = $cached->data; - } - else { - $definitions = array( - 'field_ids' => field_read_fields(array(), array('include_deleted' => 1)), - 'instances' => field_read_instances(), - ); - - // Populate 'fields' with all fields, keyed by ID. - $info['fields'] = array(); - foreach ($definitions['field_ids'] as $key => $field) { - $info['fields'][$key] = $definitions['field_ids'][$key] = _field_info_prepare_field($field); - } - - // Build an array of field IDs for non-deleted fields, keyed by name. - $info['field_ids'] = array(); - foreach ($info['fields'] as $key => $field) { - if (!$field['deleted']) { - $info['field_ids'][$field['field_name']] = $key; - } - } - - // Populate 'instances'. Only non-deleted instances are considered. - $info['instances'] = array(); - foreach (field_info_bundles() as $entity_type => $bundles) { - foreach ($bundles as $bundle => $bundle_info) { - $info['instances'][$entity_type][$bundle] = array(); - } - } - foreach ($definitions['instances'] as $instance) { - $field = $info['fields'][$instance['field_id']]; - $instance = _field_info_prepare_instance($instance, $field); - $info['instances'][$instance['entity_type']][$instance['bundle']][$instance['field_name']] = $instance; - // Enrich field definitions with the list of bundles where they have - // instances. NOTE: Deleted fields in $info['field_ids'] are not - // enriched because all of their instances are deleted, too, and - // are thus not in $definitions['instances']. - $info['fields'][$instance['field_id']]['bundles'][$instance['entity_type']][] = $instance['bundle']; + // 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"); } - - // Populate 'extra_fields'. - $extra = module_invoke_all('field_extra_fields'); - drupal_alter('field_extra_fields', $extra); - // Merge in saved settings. - foreach ($extra as $entity_type => $bundles) { - foreach ($bundles as $bundle => $extra_fields) { - $extra_fields = _field_info_prepare_extra_fields($extra_fields, $entity_type, $bundle); - $info['extra_fields'][$entity_type][$bundle] = $extra_fields; - } - } - - cache_set('field_info_fields', $info, 'cache_field'); } } @@ -249,192 +237,66 @@ /** * Prepares a field definition for the current run-time context. * - * Since the field was last saved or updated, new field settings can be - * expected. + * The functionality has moved to the FieldInfo class. This function is kept as + * a backwards-compatibility layer. See http://drupal.org/node/1915646. * - * @param $field - * The raw field structure as read from the database. + * @see FieldInfo::prepareField() */ function _field_info_prepare_field($field) { - // Make sure all expected field settings are present. - $field['settings'] += field_info_field_settings($field['type']); - $field['storage']['settings'] += field_info_storage_settings($field['storage']['type']); - - // Add storage details. - $details = (array) module_invoke($field['storage']['module'], 'field_storage_details', $field); - drupal_alter('field_storage_details', $details, $field, $instance); - $field['storage']['details'] = $details; - - // Initialize the 'bundles' list. - $field['bundles'] = array(); - - return $field; + $cache = _field_info_field_cache(); + return $cache->prepareField($field); } /** * Prepares an instance definition for the current run-time context. * - * Since the instance was last saved or updated, a number of things might have - * changed: widgets or formatters disabled, new settings expected, new view - * modes added... + * The functionality has moved to the FieldInfo class. This function is kept as + * a backwards-compatibility layer. See http://drupal.org/node/1915646. * - * @param $instance - * The raw instance structure as read from the database. - * @param $field - * The field structure for the instance. - * - * @return - * Field instance array. + * @see FieldInfo::prepareInstance() */ function _field_info_prepare_instance($instance, $field) { - $field_type = field_info_field_types($field['type']); - - // Make sure all expected instance settings are present. - $instance['settings'] += field_info_instance_settings($field['type']); - - // Set a default value for the instance. - if (field_behaviors_widget('default value', $instance) == FIELD_BEHAVIOR_DEFAULT && !isset($instance['default_value'])) { - $instance['default_value'] = NULL; - } - - $instance['widget'] = _field_info_prepare_instance_widget($field, $instance['widget']); - - foreach ($instance['display'] as $view_mode => $display) { - $instance['display'][$view_mode] = _field_info_prepare_instance_display($field, $display); - } - - // Fallback to 'hidden' for view modes configured to use custom display - // settings, and for which the instance has no explicit settings. - $entity_info = entity_get_info($instance['entity_type']); - $view_modes = array_merge(array('default'), array_keys($entity_info['view modes'])); - $view_mode_settings = field_view_mode_settings($instance['entity_type'], $instance['bundle']); - foreach ($view_modes as $view_mode) { - if ($view_mode == 'default' || !empty($view_mode_settings[$view_mode]['custom_settings'])) { - if (!isset($instance['display'][$view_mode])) { - $instance['display'][$view_mode] = array( - 'type' => 'hidden', - 'label' => 'above', - 'settings' => array(), - 'weight' => 0, - ); - } - } - } - - return $instance; + $cache = _field_info_field_cache(); + return $cache->prepareInstance($instance, $field['type']); } /** * Adapts display specifications to the current run-time context. * - * @param $field - * The field structure for the instance. - * @param $display - * Display specifications as found in - * $instance['display']['some_view_mode']. + * The functionality has moved to the FieldInfo class. This function is kept as + * a backwards-compatibility layer. See http://drupal.org/node/1915646. + * + * @see FieldInfo::prepareInstanceDisplay() */ function _field_info_prepare_instance_display($field, $display) { - $field_type = field_info_field_types($field['type']); - - // Fill in default values. - $display += array( - 'label' => 'above', - 'type' => $field_type['default_formatter'], - 'settings' => array(), - 'weight' => 0, - ); - if ($display['type'] != 'hidden') { - $formatter_type = field_info_formatter_types($display['type']); - // Fallback to default formatter if formatter type is not available. - if (!$formatter_type) { - $display['type'] = $field_type['default_formatter']; - $formatter_type = field_info_formatter_types($display['type']); - } - $display['module'] = $formatter_type['module']; - // Fill in default settings for the formatter. - $display['settings'] += field_info_formatter_settings($display['type']); - } - - return $display; + $cache = _field_info_field_cache(); + return $cache->prepareInstanceDisplay($display, $field['type']); } /** * Prepares widget specifications for the current run-time context. * - * @param $field - * The field structure for the instance. - * @param $widget - * Widget specifications as found in $instance['widget']. + * The functionality has moved to the FieldInfo class. This function is kept as + * a backwards-compatibility layer. See http://drupal.org/node/1915646. + * + * @see FieldInfo::prepareInstanceWidget() */ function _field_info_prepare_instance_widget($field, $widget) { - $field_type = field_info_field_types($field['type']); - - // Fill in default values. - $widget += array( - 'type' => $field_type['default_widget'], - 'settings' => array(), - 'weight' => 0, - ); - - $widget_type = field_info_widget_types($widget['type']); - // Fallback to default formatter if formatter type is not available. - if (!$widget_type) { - $widget['type'] = $field_type['default_widget']; - $widget_type = field_info_widget_types($widget['type']); - } - $widget['module'] = $widget_type['module']; - // Fill in default settings for the widget. - $widget['settings'] += field_info_widget_settings($widget['type']); - - return $widget; + $cache = _field_info_field_cache(); + return $cache->prepareInstanceWidget($widget, $field['type']); } /** * Prepares 'extra fields' for the current run-time context. * - * @param $extra_fields - * The array of extra fields, as collected in hook_field_extra_fields(). - * @param $entity_type - * The entity type. - * @param $bundle - * The bundle name. + * The functionality has moved to the FieldInfo class. This function is kept as + * a backwards-compatibility layer. See http://drupal.org/node/1915646. + * + * @see FieldInfo::prepareExtraFields() */ function _field_info_prepare_extra_fields($extra_fields, $entity_type, $bundle) { - $entity_type_info = entity_get_info($entity_type); - $bundle_settings = field_bundle_settings($entity_type, $bundle); - $extra_fields += array('form' => array(), 'display' => array()); - - $result = array(); - // Extra fields in forms. - foreach ($extra_fields['form'] as $name => $field_data) { - $settings = isset($bundle_settings['extra_fields']['form'][$name]) ? $bundle_settings['extra_fields']['form'][$name] : array(); - if (isset($settings['weight'])) { - $field_data['weight'] = $settings['weight']; - } - $result['form'][$name] = $field_data; - } - - // Extra fields in displayed entities. - $data = $extra_fields['display']; - foreach ($extra_fields['display'] as $name => $field_data) { - $settings = isset($bundle_settings['extra_fields']['display'][$name]) ? $bundle_settings['extra_fields']['display'][$name] : array(); - $view_modes = array_merge(array('default'), array_keys($entity_type_info['view modes'])); - foreach ($view_modes as $view_mode) { - if (isset($settings[$view_mode])) { - $field_data['display'][$view_mode] = $settings[$view_mode]; - } - else { - $field_data['display'][$view_mode] = array( - 'weight' => $field_data['weight'], - 'visible' => TRUE, - ); - } - } - unset($field_data['weight']); - $result['display'][$name] = $field_data; - } - - return $result; + $cache = _field_info_field_cache(); + return $cache->prepareExtraFields($extra_fields, $entity_type, $bundle); } /** @@ -461,7 +323,7 @@ * Returns information about field types from hook_field_info(). * * @param $field_type - * (optional) A field type name. If ommitted, all field types will be + * (optional) A field type name. If omitted, all field types will be * returned. * * @return @@ -485,7 +347,7 @@ * Returns information about field widgets from hook_field_widget_info(). * * @param $widget_type - * (optional) A widget type name. If ommitted, all widget types will be + * (optional) A widget type name. If omitted, all widget types will be * returned. * * @return @@ -510,7 +372,7 @@ * Returns information about field formatters from hook_field_formatter_info(). * * @param $formatter_type - * (optional) A formatter type name. If ommitted, all formatter types will be + * (optional) A formatter type name. If omitted, all formatter types will be * returned. * * @return @@ -535,7 +397,7 @@ * Returns information about field storage from hook_field_storage_info(). * * @param $storage_type - * (optional) A storage type name. If ommitted, all storage types will be + * (optional) A storage type name. If omitted, all storage types will be * returned. * * @return @@ -582,21 +444,61 @@ } /** + * Returns a lightweight map of fields across bundles. + * + * The function only returns active, non deleted fields. + * + * @return + * An array keyed by field name. Each value is an array with two entries: + * - type: The field type. + * - bundles: The bundles in which the field appears, as an array with entity + * types as keys and the array of bundle names as values. + * Example: + * @code + * array( + * 'body' => array( + * 'bundles' => array( + * 'node' => array('page', 'article'), + * ), + * 'type' => 'text_with_summary', + * ), + * ); + * @endcode + */ +function field_info_field_map() { + $cache = _field_info_field_cache(); + return $cache->getFieldMap(); +} + +/** * Returns all field definitions. * + * Use of this function should be avoided when possible, since it loads and + * statically caches a potentially large array of information. Use + * field_info_field_map() instead. + * + * When iterating over the fields present in a given bundle after a call to + * field_info_instances($entity_type, $bundle), it is recommended to use + * field_info_field() on each individual field instead. + * * @return * An array of field definitions, keyed by field name. Each field has an * additional property, 'bundles', which is an array of all the bundles to * which this field belongs keyed by entity type. + * + * @see field_info_field_map() */ function field_info_fields() { + $cache = _field_info_field_cache(); + $info = $cache->getFields(); + $fields = array(); - $info = _field_info_collate_fields(); - foreach ($info['fields'] as $key => $field) { + foreach ($info as $key => $field) { if (!$field['deleted']) { $fields[$field['field_name']] = $field; } } + return $fields; } @@ -605,21 +507,21 @@ * * @param $field_name * The name of the field to retrieve. $field_name can only refer to a - * non-deleted, active field. Use field_read_fields() to retrieve information - * on deleted or inactive fields. + * non-deleted, active field. For deleted fields, use + * field_info_field_by_id(). To retrieve information about inactive fields, + * use field_read_fields(). * * @return * The field array, as returned by field_read_fields(), with an * additional element 'bundles', whose value is an array of all the bundles - * this field belongs to keyed by entity type. + * this field belongs to keyed by entity type. NULL if the field was not + * found. * * @see field_info_field_by_id() */ function field_info_field($field_name) { - $info = _field_info_collate_fields(); - if (isset($info['field_ids'][$field_name])) { - return $info['fields'][$info['field_ids'][$field_name]]; - } + $cache = _field_info_field_cache(); + return $cache->getField($field_name); } /** @@ -627,7 +529,7 @@ * * @param $field_id * The id of the field to retrieve. $field_id can refer to a - * deleted field. + * deleted field, but not an inactive one. * * @return * The field array, as returned by field_read_fields(), with an @@ -637,17 +539,19 @@ * @see field_info_field() */ function field_info_field_by_id($field_id) { - $info = _field_info_collate_fields(); - if (isset($info['fields'][$field_id])) { - return $info['fields'][$field_id]; - } + $cache = _field_info_field_cache(); + return $cache->getFieldById($field_id); } /** * Returns the same data as field_info_field_by_id() for every field. * - * This function is typically used when handling all fields of some entities - * to avoid thousands of calls to field_info_field_by_id(). + * Use of this function should be avoided when possible, since it loads and + * statically caches a potentially large array of information. + * + * When iterating over the fields present in a given bundle after a call to + * field_info_instances($entity_type, $bundle), it is recommended to use + * field_info_field() on each individual field instead. * * @return * An array, each key is a field ID and the values are field arrays as @@ -658,52 +562,73 @@ * @see field_info_field_by_id() */ function field_info_field_by_ids() { - $info = _field_info_collate_fields(); - return $info['fields']; + $cache = _field_info_field_cache(); + return $cache->getFields(); } /** * Retrieves information about field instances. * + * Use of this function to retrieve instances across separate bundles (i.e. + * when the $bundle parameter is NULL) should be avoided when possible, since + * it loads and statically caches a potentially large array of information. Use + * field_info_field_map() instead. + * + * When retrieving the instances of a specific bundle (i.e. when both + * $entity_type and $bundle_name are provided), the function also populates a + * static cache with the corresponding field definitions, allowing fast + * retrieval of field_info_field() later in the request. + * * @param $entity_type - * The entity type for which to return instances. + * (optional) The entity type for which to return instances. * @param $bundle_name - * The bundle name for which to return instances. + * (optional) The bundle name for which to return instances. If $entity_type + * is NULL, the $bundle_name parameter is ignored. * * @return * If $entity_type is not set, return all instances keyed by entity type and * bundle name. If $entity_type is set, return all instances for that entity * type, keyed by bundle name. If $entity_type and $bundle_name are set, return * all instances for that bundle. + * + * @see field_info_field_map() */ function field_info_instances($entity_type = NULL, $bundle_name = NULL) { - $info = _field_info_collate_fields(); + $cache = _field_info_field_cache(); + if (!isset($entity_type)) { - return $info['instances']; + return $cache->getInstances(); } if (!isset($bundle_name)) { - return $info['instances'][$entity_type]; + return $cache->getInstances($entity_type); } - if (isset($info['instances'][$entity_type][$bundle_name])) { - return $info['instances'][$entity_type][$bundle_name]; - } - return array(); + + return $cache->getBundleInstances($entity_type, $bundle_name); } /** * Returns an array of instance data for a specific field and bundle. * + * The function populates a static cache with all fields and instances used in + * the bundle, allowing fast retrieval of field_info_field() or + * field_info_instance() later in the request. + * * @param $entity_type * The entity type for the instance. * @param $field_name * The field name for the instance. * @param $bundle_name * The bundle name for the instance. + * + * @return + * An associative array of instance data for the specific field and bundle; + * NULL if the instance does not exist. */ function field_info_instance($entity_type, $field_name, $bundle_name) { - $info = _field_info_collate_fields(); - if (isset($info['instances'][$entity_type][$bundle_name][$field_name])) { - return $info['instances'][$entity_type][$bundle_name][$field_name]; + $cache = _field_info_field_cache(); + $info = $cache->getBundleInstances($entity_type, $bundle_name); + if (isset($info[$field_name])) { + return $info[$field_name]; } } @@ -735,7 +660,7 @@ * 'default' => array( * 'weight' => The weight of the component in displayed entities in * this view mode, - * 'visibility' => Whether the component is visible or hidden in + * 'visible' => TRUE if the component is visible, FALSE if hidden, in * displayed entities in this view mode, * ), * 'teaser' => array( @@ -761,11 +686,10 @@ * The array of pseudo-field elements in the bundle. */ function field_info_extra_fields($entity_type, $bundle, $context) { - $info = _field_info_collate_fields(); - if (isset($info['extra_fields'][$entity_type][$bundle][$context])) { - return $info['extra_fields'][$entity_type][$bundle][$context]; - } - return array(); + $cache = _field_info_field_cache(); + $info = $cache->getBundleExtraFields($entity_type, $bundle); + + return isset($info[$context]) ? $info[$context] : array(); } /** @@ -793,7 +717,7 @@ if ($context == 'form') { $weights[] = $instance['widget']['weight']; } - else { + elseif (isset($instance['display'][$context]['weight'])) { $weights[] = $instance['display'][$context]['weight']; } } @@ -888,5 +812,5 @@ } /** - * @} End of "defgroup field_info" + * @} End of "defgroup field_info". */ diff -Naur drupal-7.0/modules/field/field.install drupal-7.66/modules/field/field.install --- drupal-7.0/modules/field/field.install 2011-01-02 18:26:39.000000000 +0100 +++ drupal-7.66/modules/field/field.install 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ array('id'), 'indexes' => array( 'field_name' => array('field_name'), - // Used by field_read_fields(). + // Used by field_sync_field_status(). 'active' => array('active'), 'storage_active' => array('storage_active'), 'deleted' => array('deleted'), // Used by field_modules_disabled(). 'module' => array('module'), 'storage_module' => array('storage_module'), - // Used by field_associate_fields(). 'type' => array('type'), 'storage_type' => array('storage_type'), ), @@ -130,7 +128,7 @@ 'not null' => TRUE, 'default' => '' ), - 'entity_type' => array( + 'entity_type' => array( 'type' => 'varchar', 'length' => 32, 'not null' => TRUE, @@ -164,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; } @@ -174,7 +173,7 @@ * This function can be used for databases whose schema is at field module * version 7000 or higher. * - * @ingroup update-api-6.x-to-7.x + * @ingroup update_api */ function _update_7000_field_create_field(&$field) { // Merge in default values.` @@ -255,7 +254,7 @@ * @param $field_name * The field name to delete. * - * @ingroup update-api-6.x-to-7.x + * @ingroup update_api */ function _update_7000_field_delete_field($field_name) { $table_name = 'field_data_' . $field_name; @@ -286,7 +285,7 @@ * * This function is valid for a database schema version 7000. * - * @ingroup update-api-6.x-to-7.x + * @ingroup update_api */ function _update_7000_field_delete_instance($field_name, $entity_type, $bundle) { // Delete field instance configuration data. @@ -310,18 +309,29 @@ /** * Utility function: fetch all the field definitions from the database. * + * Warning: unlike the field_read_fields() API function, this function returns + * all fields by default, including deleted and inactive fields, unless + * specified otherwise in the $conditions parameter. + * * @param $conditions * An array of conditions to limit the select query to. + * @param $key + * The name of the field property the return array is indexed by. Using + * anything else than 'id' might cause incomplete results if the $conditions + * do not filter out deleted fields. + * + * @return + * An array of fields matching $conditions, keyed by the property specified + * by the $key parameter. + * + * @ingroup update_api */ -function _update_7000_field_read_fields(array $conditions = array()) { +function _update_7000_field_read_fields(array $conditions = array(), $key = 'id') { $fields = array(); $query = db_select('field_config', 'fc', array('fetch' => PDO::FETCH_ASSOC)) - ->fields('fc') - ->condition('deleted', 0); - if (!empty($conditions)) { - foreach ($conditions as $column => $value) { - $query->condition($column, $value); - } + ->fields('fc'); + foreach ($conditions as $column => $value) { + $query->condition($column, $value); } foreach ($query->execute() as $record) { $field = unserialize($record['data']); @@ -338,7 +348,7 @@ $field['translatable'] = $record['translatable']; $field['deleted'] = $record['deleted']; - $fields[$field['field_name']] = $field; + $fields[$field[$key]] = $field; } return $fields; } @@ -349,7 +359,7 @@ * This function can be used for databases whose schema is at field module * version 7000 or higher. * - * @ingroup update-api-6.x-to-7.x + * @ingroup update_api */ function _update_7000_field_create_instance($field, &$instance) { // Merge in defaults. @@ -427,5 +437,57 @@ } /** - * @} End of "addtogroup updates-6.x-to-7.x" + * @} End of "addtogroup updates-6.x-to-7.x". + */ + +/** + * @addtogroup updates-7.x-extra + * @{ + */ + +/** + * Split the all-inclusive field_bundle_settings variable per bundle. + */ +function field_update_7002() { + $settings = variable_get('field_bundle_settings', array()); + if ($settings) { + foreach ($settings as $entity_type => $entity_type_settings) { + foreach ($entity_type_settings as $bundle => $bundle_settings) { + variable_set('field_bundle_settings_' . $entity_type . '__' . $bundle, $bundle_settings); + } + } + variable_del('field_bundle_settings'); + } +} + +/** + * Add the FieldInfo class to the class registry. + */ +function field_update_7003() { + // Empty update to force a rebuild of the registry. +} + +/** + * 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.0/modules/field/field.module drupal-7.66/modules/field/field.module --- drupal-7.0/modules/field/field.module 2011-01-02 18:26:39.000000000 +0100 +++ drupal-7.66/modules/field/field.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ $field_name. Maximum length is 32 characters. - * - type (string) - * The type of the field, such as 'text' or 'image'. Field types - * are defined by modules that implement hook_field_info(). - * - entity_types (array) - * The array of entity types that can hold instances of this field. If - * empty or not specified, the field can have instances in any entity type. - * - cardinality (integer) - * The number of values the field can hold. Legal values are any - * positive integer or FIELD_CARDINALITY_UNLIMITED. - * - translatable (integer) - * Whether the field is translatable. - * - locked (integer) - * Whether or not the field is available for editing. If TRUE, users can't - * change field settings or create new instances of the field in the UI. - * Defaults to FALSE. - * - module (string, read-only) - * The name of the module that implements the field type. - * - active (integer, read-only) - * TRUE if the module that implements the field type is currently - * enabled, FALSE otherwise. - * - deleted (integer, read-only) - * TRUE if this field has been deleted, FALSE otherwise. Deleted - * fields are ignored by the Field Attach API. This property exists - * because fields can be marked for deletion but only actually - * destroyed by a separate garbage-collection process. - * - columns (array, read-only). - * An array of the Field API columns used to store each value of - * this field. The column list may depend on field settings; it is - * not constant per field type. Field API column specifications are - * exactly like Schema API column specifications but, depending on - * the field storage module in use, the name of the column may not - * represent an actual column in an SQL database. - * - indexes (array). - * An array of indexes on data columns, using the same definition format - * as Schema API index specifications. Only columns that appear in the - * 'columns' setting are allowed. Note that field types can specify - * default indexes, which can be modified or added to when - * creating a field. + * - id (integer, read-only): The primary identifier of the field. It is + * assigned automatically by field_create_field(). + * - field_name (string): The name of the field. Each field name is unique + * within Field API. When a field is attached to an entity, the field's data + * is stored in $entity->$field_name. Maximum length is 32 characters. + * - type (string): The type of the field, such as 'text' or 'image'. Field + * types are defined by modules that implement hook_field_info(). + * - entity_types (array): The array of entity types that can hold instances + * of this field. If empty or not specified, the field can have instances + * in any entity type. + * - cardinality (integer): The number of values the field can hold. Legal + * values are any positive integer or FIELD_CARDINALITY_UNLIMITED. + * - translatable (integer): Whether the field is translatable. + * - locked (integer): Whether or not the field is available for editing. If + * TRUE, users can't change field settings or create new instances of the + * field in the UI. Defaults to FALSE. + * - module (string, read-only): The name of the module that implements the + * field type. + * - active (integer, read-only): TRUE if the module that implements the field + * type is currently enabled, FALSE otherwise. + * - deleted (integer, read-only): TRUE if this field has been deleted, FALSE + * otherwise. Deleted fields are ignored by the Field Attach API. This + * property exists because fields can be marked for deletion but only + * actually destroyed by a separate garbage-collection process. + * - columns (array, read-only): An array of the Field API columns used to + * store each value of this field. The column list may depend on field + * settings; it is not constant per field type. Field API column + * specifications are exactly like Schema API column specifications but, + * depending on the field storage module in use, the name of the column may + * not represent an actual column in an SQL database. + * - indexes (array): An array of indexes on data columns, using the same + * definition format as Schema API index specifications. Only columns that + * appear in the 'columns' setting are allowed. Note that field types can + * specify default indexes, which can be modified or added to when + * creating a field. * - foreign keys: (optional) An associative array of relations, using the same - * structure as the 'foreign keys' definition of hook_schema(). Note, however, - * that the field data is not necessarily stored in SQL. Also, the possible - * usage is limited, as you cannot specify another field as related, only - * existing SQL tables, such as filter formats. - * - settings (array) - * A sub-array of key/value pairs of field-type-specific settings. Each - * field type module defines and documents its own field settings. - * - storage (array) - * A sub-array of key/value pairs identifying the storage backend to use for - * the for the field. - * - type (string) - * The storage backend used by the field. Storage backends are defined - * by modules that implement hook_field_storage_info(). - * - module (string, read-only) - * The name of the module that implements the storage backend. - * - active (integer, read-only) - * TRUE if the module that implements the storage backend is currently - * enabled, FALSE otherwise. - * - settings (array) - * A sub-array of key/value pairs of settings. Each storage backend - * defines and documents its own settings. + * structure as the 'foreign keys' definition of hook_schema(). Note, + * however, that the field data is not necessarily stored in SQL. Also, the + * possible usage is limited, as you cannot specify another field as + * related, only existing SQL tables, such as filter formats. + * - settings (array): A sub-array of key/value pairs of field-type-specific + * settings. Each field type module defines and documents its own field + * settings. + * - storage (array): A sub-array of key/value pairs identifying the storage + * backend to use for the for the field: + * - type (string): The storage backend used by the field. Storage backends + * are defined by modules that implement hook_field_storage_info(). + * - module (string, read-only): The name of the module that implements the + * storage backend. + * - active (integer, read-only): TRUE if the module that implements the + * storage backend is currently enabled, FALSE otherwise. + * - settings (array): A sub-array of key/value pairs of settings. Each + * storage backend defines and documents its own settings. * * Field instance definitions are represented as an array of key/value pairs. * * array $instance: - * - id (integer, read-only) - * The primary identifier of this field instance. It is assigned - * automatically by field_create_instance(). - * - field_id (integer, read-only) - * The foreign key of the field attached to the bundle by this instance. - * It is populated automatically by field_create_instance(). - * - field_name (string) - * The name of the field attached to the bundle by this instance. - * - entity_type (string) - * The name of the entity type the instance is attached to. - * - bundle (string) - * The name of the bundle that the field is attached to. - * - label (string) - * A human-readable label for the field when used with this - * bundle. For example, the label will be the title of Form API - * elements for this instance. - * - description (string) - * A human-readable description for the field when used with this - * bundle. For example, the description will be the help text of - * Form API elements for this instance. - * - required (integer) - * TRUE if a value for this field is required when used with this - * bundle, FALSE otherwise. Currently, required-ness is only enforced - * during Form API operations, not by field_attach_load(), - * field_attach_insert(), or field_attach_update(). - * - default_value_function (string) - * The name of the function, if any, that will provide a default value. - * - default_value (array) - * If default_value_function is not set, then fixed values can be provided. - * - deleted (integer, read-only) - * TRUE if this instance has been deleted, FALSE otherwise. - * Deleted instances are ignored by the Field Attach API. - * This property exists because instances can be marked for deletion but - * only actually destroyed by a separate garbage-collection process. - * - settings (array) - * A sub-array of key/value pairs of field-type-specific instance - * settings. Each field type module defines and documents its own - * instance settings. - * - widget (array) - * A sub-array of key/value pairs identifying the Form API input widget - * for the field when used by this bundle. - * - type (string) - * The type of the widget, such as text_textfield. Widget types - * are defined by modules that implement hook_field_widget_info(). - * - settings (array) - * A sub-array of key/value pairs of widget-type-specific settings. - * Each field widget type module defines and documents its own - * widget settings. - * - weight (float) - * The weight of the widget relative to the other elements in entity - * edit forms. - * - module (string, read-only) - * The name of the module that implements the widget type. - * - display (array) - * A sub-array of key/value pairs identifying the way field values should - * be displayed in each of the entity type's view modes, plus the 'default' - * mode. For each view mode, Field UI lets site administrators define - * whether they want to use a dedicated set of display options or the - * 'default' options to reduce the number of displays to maintain as they - * add new fields. For nodes, on a fresh install, only the 'teaser' view - * mode is configured to use custom display options, all other view modes - * defined use the 'default' options by default. When programmatically - * adding field instances on nodes, it is therefore recommended to at least - * specify display options for 'default' and 'teaser'. - * - default (array) - * A sub-array of key/value pairs describing the display options to be - * used when the field is being displayed in view modes that are not - * configured to use dedicated display options. - * - label (string) - * Position of the label. 'inline', 'above' and 'hidden' are the - * values recognized by the default 'field' theme implementation. - * - type (string) - * The type of the display formatter, or 'hidden' for no display. - * - settings (array) - * A sub-array of key/value pairs of display options specific to - * the formatter. - * - weight (float) - * The weight of the field relative to the other entity components - * displayed in this view mode. - * - module (string, read-only) - * The name of the module which implements the display formatter. - * - some_mode - * A sub-array of key/value pairs describing the display options to be - * used when the field is being displayed in the 'some_mode' view mode. - * Those options will only be actually applied at run time if the view - * mode is not configured to use default settings for this bundle. - * - ... - * - other_mode - * - ... + * - id (integer, read-only): The primary identifier of this field instance. + * It is assigned automatically by field_create_instance(). + * - field_id (integer, read-only): The foreign key of the field attached to + * the bundle by this instance. It is populated automatically by + * field_create_instance(). + * - field_name (string): The name of the field attached to the bundle by this + * instance. + * - entity_type (string): The name of the entity type the instance is attached + * to. + * - bundle (string): The name of the bundle that the field is attached to. + * - label (string): A human-readable label for the field when used with this + * bundle. For example, the label will be the title of Form API elements + * for this instance. + * - description (string): A human-readable description for the field when + * used with this bundle. For example, the description will be the help + * text of Form API elements for this instance. + * - required (integer): TRUE if a value for this field is required when used + * with this bundle, FALSE otherwise. Currently, required-ness is only + * enforced during Form API operations, not by field_attach_load(), + * field_attach_insert(), or field_attach_update(). + * - default_value_function (string): The name of the function, if any, that + * will provide a default value. + * - default_value (array): If default_value_function is not set, then fixed + * values can be provided. + * - deleted (integer, read-only): TRUE if this instance has been deleted, + * FALSE otherwise. Deleted instances are ignored by the Field Attach API. + * This property exists because instances can be marked for deletion but + * only actually destroyed by a separate garbage-collection process. + * - settings (array): A sub-array of key/value pairs of field-type-specific + * instance settings. Each field type module defines and documents its own + * instance settings. + * - widget (array): A sub-array of key/value pairs identifying the Form API + * input widget for the field when used by this bundle: + * - type (string): The type of the widget, such as text_textfield. Widget + * types are defined by modules that implement hook_field_widget_info(). + * - settings (array): A sub-array of key/value pairs of + * widget-type-specific settings. Each field widget type module defines + * and documents its own widget settings. + * - weight (float): The weight of the widget relative to the other elements + * in entity edit forms. + * - module (string, read-only): The name of the module that implements the + * widget type. + * - display (array): A sub-array of key/value pairs identifying the way field + * values should be displayed in each of the entity type's view modes, plus + * the 'default' mode. For each view mode, Field UI lets site administrators + * define whether they want to use a dedicated set of display options or the + * 'default' options to reduce the number of displays to maintain as they + * add new fields. For nodes, on a fresh install, only the 'teaser' view + * mode is configured to use custom display options, all other view modes + * defined use the 'default' options by default. When programmatically + * adding field instances on nodes, it is therefore recommended to at least + * specify display options for 'default' and 'teaser': + * - default (array): A sub-array of key/value pairs describing the display + * options to be used when the field is being displayed in view modes + * that are not configured to use dedicated display options: + * - label (string): Position of the label. 'inline', 'above' and + * 'hidden' are the values recognized by the default 'field' theme + * implementation. + * - type (string): The type of the display formatter, or 'hidden' for + * no display. + * - settings (array): A sub-array of key/value pairs of display + * options specific to the formatter. + * - weight (float): The weight of the field relative to the other entity + * components displayed in this view mode. + * - module (string, read-only): The name of the module which implements + * the display formatter. + * - some_mode: A sub-array of key/value pairs describing the display + * options to be used when the field is being displayed in the 'some_mode' + * view mode. Those options will only be actually applied at run time if + * the view mode is not configured to use default settings for this bundle: + * - ... + * - other_mode: + * - ... + * + * The (default) render arrays produced for field instances are documented at + * field_attach_view(). * * Bundles are represented by two strings, an entity type and a bundle name. * @@ -307,13 +279,6 @@ class FieldUpdateForbiddenException extends FieldException {} /** - * Implements hook_flush_caches(). - */ -function field_flush_caches() { - return array('cache_field'); -} - -/** * Implements hook_help(). */ function field_help($path, $arg) { @@ -321,7 +286,7 @@ case 'admin/help#field': $output = ''; $output .= '

' . t('About') . '

'; - $output .= '

' . t('The Field module allows custom data fields to be defined for entity types (entities include content items, comments, user accounts, and taxonomy terms). The Field module takes care of storing, loading, editing, and rendering field data. Most users will not interact with the Field module directly, but will instead use the Field UI module user interface. Module developers can use the Field API to make new entity types "fieldable" and thus allow fields to be attached to them. For more information, see the online handbook entry for Field module.', array('@field-ui-help' => url('admin/help/field_ui'), '@field' => 'http://drupal.org/handbook/modules/field')) . '

'; + $output .= '

' . t('The Field module allows custom data fields to be defined for entity types (entities include content items, comments, user accounts, and taxonomy terms). The Field module takes care of storing, loading, editing, and rendering field data. Most users will not interact with the Field module directly, but will instead use the Field UI module user interface. Module developers can use the Field API to make new entity types "fieldable" and thus allow fields to be attached to them. For more information, see the online handbook entry for Field module.', array('@field-ui-help' => url('admin/help/field_ui'), '@field' => 'http://drupal.org/documentation/modules/field')) . '

'; $output .= '

' . t('Uses') . '

'; $output .= '
'; $output .= '
' . t('Enabling field types') . '
'; @@ -352,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() { @@ -367,77 +347,121 @@ /** * Implements hook_cron(). - * - * Purges some deleted Field API data, if any exists. */ function field_cron() { + // Refresh the 'active' status of fields. + field_sync_field_status(); + + // Do a pass of purging on deleted Field API data, if any exists. $limit = variable_get('field_purge_batch_size', 10); field_purge_batch($limit); } /** - * Implements hook_modules_uninstalled(). + * Implements hook_system_info_alter(). + * + * Goes through a list of all modules that provide a field type, and makes them + * required if there are any active fields of that type. */ -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); +function field_system_info_alter(&$info, $file, $type) { + if ($type == 'module' && module_hook($file->name, 'field_info')) { + $fields = field_read_fields(array('module' => $file->name), array('include_deleted' => TRUE)); + if ($fields) { + $info['required'] = TRUE; + + // Provide an explanation message (only mention pending deletions if there + // remains no actual, non-deleted fields) + $non_deleted = FALSE; + foreach ($fields as $field) { + if (empty($field['deleted'])) { + $non_deleted = TRUE; + break; + } + } + if ($non_deleted) { + if (module_exists('field_ui')) { + $explanation = t('Field type(s) in use - see Field list', array('@fields-page' => url('admin/reports/fields'))); + } + else { + $explanation = t('Fields type(s) in use'); + } + } + else { + $explanation = t('Fields pending deletion'); + } + $info['explanation'] = $explanation; + } } } /** + * Implements hook_flush_caches(). + */ +function field_flush_caches() { + // Refresh the 'active' status of fields. + field_sync_field_status(); + + // Request a flush of our cache table. + return array('cache_field'); +} + +/** * Implements hook_modules_enabled(). */ function field_modules_enabled($modules) { - foreach ($modules as $module) { - field_associate_fields($module); - } - field_cache_clear(); + // Refresh the 'active' status of fields. + field_sync_field_status(); } /** * Implements hook_modules_disabled(). */ function field_modules_disabled($modules) { - // Track fields whose field type is being disabled. + // Refresh the 'active' status of fields. + field_sync_field_status(); +} + +/** + * Refreshes the 'active' and 'storage_active' columns for fields. + */ +function field_sync_field_status() { + // Refresh the 'active' and 'storage_active' columns according to the current + // set of enabled modules. + $modules = module_list(); + foreach ($modules as $module_name) { + field_associate_fields($module_name); + } db_update('field_config') ->fields(array('active' => 0)) - ->condition('module', $modules, 'IN') + ->condition('module', $modules, 'NOT IN') ->execute(); - - // Track fields whose storage backend is being disabled. db_update('field_config') ->fields(array('storage_active' => 0)) - ->condition('storage_module', $modules, 'IN') + ->condition('storage_module', $modules, 'NOT IN') ->execute(); - - field_cache_clear(); } /** * Allows a module to update the database for fields and columns it controls. * - * @param string $module + * @param $module * The name of the module to update on. */ function field_associate_fields($module) { // Associate field types. - $field_types =(array) module_invoke($module, 'field_info'); - foreach ($field_types as $name => $field_info) { - watchdog('field', 'Updating field type %type with module %module.', array('%type' => $name, '%module' => $module)); + $field_types = (array) module_invoke($module, 'field_info'); + if ($field_types) { db_update('field_config') ->fields(array('module' => $module, 'active' => 1)) - ->condition('type', $name) + ->condition('type', array_keys($field_types)) ->execute(); } // Associate storage backends. $storage_types = (array) module_invoke($module, 'field_storage_info'); - foreach ($storage_types as $name => $storage_info) { - watchdog('field', 'Updating field storage %type with module %module.', array('%type' => $name, '%module' => $module)); + if ($storage_types) { db_update('field_config') ->fields(array('storage_module' => $module, 'storage_active' => 1)) - ->condition('storage_type', $name) + ->condition('storage_type', array_keys($storage_types)) ->execute(); } } @@ -446,7 +470,7 @@ * Helper function to get the default value for a field on an entity. * * @param $entity_type - * The type of $entity; e.g. 'node' or 'user'. + * The type of $entity; e.g., 'node' or 'user'. * @param $entity * The entity for the operation. * @param $field @@ -484,7 +508,6 @@ */ function _field_filter_items($field, $items) { $function = $field['module'] . '_field_is_empty'; - function_exists($function); foreach ((array) $items as $delta => $item) { // Explicitly break if the function is undefined. if ($function($item, $field)) { @@ -532,64 +555,42 @@ /** * Gets or sets administratively defined bundle settings. * - * For each bundle, settings are provided as a nested array with the following - * structure: - * @code - * array( - * 'view_modes' => array( - * // One sub-array per view mode for the entity type: - * 'full' => array( - * 'custom_display' => Whether the view mode uses custom display - * settings or settings of the 'default' mode, - * ), - * 'teaser' => ... - * ), - * 'extra_fields' => array( - * 'form' => array( - * // One sub-array per pseudo-field in displayed entities: - * 'extra_field_1' => array( - * 'weight' => The weight of the pseudo-field, - * ), - * 'extra_field_2' => ... - * ), - * 'display' => array( - * // One sub-array per pseudo-field in displayed entities: - * 'extra_field_1' => array( - * // One sub-array per view mode for the entity type, including - * // the 'default' mode: - * 'default' => array( - * 'weight' => The weight of the pseudo-field, - * 'visibility' => Whether the pseudo-field is visible or hidden, - * ), - * 'full' => ... - * ), - * 'extra_field_2' => ... - * ), - * ), - * ); - * @endcode - * - * @param $entity_type - * The type of $entity; e.g. 'node' or 'user'. - * @param $bundle + * @param string $entity_type + * The type of $entity; e.g., 'node' or 'user'. + * @param string $bundle * The bundle name. - * @param $settings - * (optional) The settings to store. + * @param array|null $settings + * (optional) The settings to store, an associative array with the following + * elements: + * - view_modes: An associative array keyed by view mode, with the following + * key/value pairs: + * - custom_settings: Boolean specifying whether the view mode uses a + * dedicated set of display options (TRUE), or the 'default' options + * (FALSE). Defaults to FALSE. + * - extra_fields: An associative array containing the form and display + * settings for extra fields (also known as pseudo-fields): + * - form: An associative array whose keys are the names of extra fields, + * and whose values are associative arrays with the following elements: + * - weight: The weight of the extra field, determining its position on an + * entity form. + * - display: An associative array whose keys are the names of extra fields, + * and whose values are associative arrays keyed by the name of view + * modes. This array must include an item for the 'default' view mode. + * Each view mode sub-array contains the following elements: + * - weight: The weight of the extra field, determining its position when + * an entity is viewed. + * - visible: TRUE if the extra field is visible, FALSE otherwise. * - * @return + * @return array|null * If no $settings are passed, the current settings are returned. */ function field_bundle_settings($entity_type, $bundle, $settings = NULL) { - $stored_settings = variable_get('field_bundle_settings', array()); - if (isset($settings)) { - $stored_settings[$entity_type][$bundle] = $settings; - - variable_set('field_bundle_settings', $stored_settings); + variable_set('field_bundle_settings_' . $entity_type . '__' . $bundle, $settings); field_info_cache_clear(); } else { - $settings = isset($stored_settings[$entity_type][$bundle]) ? $stored_settings[$entity_type][$bundle] : array(); + $settings = variable_get('field_bundle_settings_' . $entity_type . '__' . $bundle, array()); $settings += array( 'view_modes' => array(), 'extra_fields' => array(), @@ -675,7 +676,7 @@ * Returns the display settings to use for pseudo-fields in a given view mode. * * @param $entity_type - * The type of $entity; e.g. 'node' or 'user'. + * The type of $entity; e.g., 'node' or 'user'. * @param $bundle * The bundle name. * @param $view_mode @@ -774,7 +775,7 @@ * Returns a renderable array for a single field value. * * @param $entity_type - * The type of $entity; e.g. 'node' or 'user'. + * The type of $entity; e.g., 'node' or 'user'. * @param $entity * The entity containing the field to display. Must at least contain the id * key and the field data to display. @@ -822,16 +823,18 @@ * * 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. * * The function takes care of invoking the prepare_view steps. It also respects * field access permissions. * * @param $entity_type - * The type of $entity; e.g. 'node' or 'user'. + * The type of $entity; e.g., 'node' or 'user'. * @param $entity * The entity containing the field to display. Must at least contain the id * key and the field data to display. @@ -865,6 +868,8 @@ * used. * @return * A renderable array for the field value. + * + * @see field_view_value() */ function field_view_field($entity_type, $entity, $field_name, $display = array(), $langcode = NULL) { $output = array(); @@ -872,7 +877,8 @@ if ($field = field_info_field($field_name)) { if (is_array($display)) { // When using custom display settings, fill in default values. - $display = _field_info_prepare_instance_display($field, $display); + $cache = _field_info_field_cache(); + $display = $cache->prepareInstanceDisplay($display, $field["type"]); } // Hook invocations are done through the _field_invoke() functions in @@ -903,6 +909,7 @@ 'entity' => $entity, 'view_mode' => '_custom', 'display' => $display, + 'language' => $langcode, ); drupal_alter('field_attach_view', $result, $context); @@ -918,7 +925,7 @@ * Returns the field items in the language they currently would be displayed. * * @param $entity_type - * The type of $entity. + * The type of $entity; e.g., 'node' or 'user'. * @param $entity * The entity containing the data to be displayed. * @param $field_name @@ -945,28 +952,44 @@ */ 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'); + + 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" - * - "view" + * - 'edit' + * - 'view' * @param $field - * The field on which the operation is to be performed. + * The full field structure array for the field on which the operation is to + * be performed. See field_info_field(). * @param $entity_type - * The type of $entity; e.g. 'node' or 'user'. + * The type of $entity; e.g., 'node' or 'user'. * @param $entity * (optional) The entity for the operation. * @param $account * (optional) The account to check, if not given use currently logged in user. + * * @return * TRUE if the operation is allowed; * FALSE if the operation is denied. @@ -992,7 +1015,7 @@ * Helper function to extract the bundle name of from a bundle object. * * @param $entity_type - * The type of $entity; e.g. 'node' or 'user'. + * The type of $entity; e.g., 'node' or 'user'. * @param $bundle * The bundle object (or string if bundles for this entity type do not exist * as standalone objects). @@ -1087,7 +1110,7 @@ } } /** - * @} End of "defgroup field" + * @} End of "defgroup field". */ /** @@ -1137,7 +1160,7 @@ * - label_hidden: A boolean indicating to show or hide the field label. * - title_attributes: A string containing the attributes for the title. * - label: The label for the field. - * - content_attributes: A string containing the attaributes for the content's + * - content_attributes: A string containing the attributes for the content's * div. * - items: An array of field items. * - item_attributes: An array of attributes for each item. @@ -1173,31 +1196,37 @@ } /** - * Helper form element validator: integer. + * DEPRECATED: Helper form element validator: integer. + * + * Use element_validate_integer() instead. + * + * @deprecated + * @see element_validate_integer() */ function _element_validate_integer($element, &$form_state) { - $value = $element['#value']; - if ($value !== '' && (!is_numeric($value) || intval($value) != $value)) { - form_error($element, t('%name must be an integer.', array('%name' => $element['#title']))); - } + element_validate_integer($element, $form_state); } /** - * Helper form element validator: integer > 0. + * DEPRECATED: Helper form element validator: integer > 0. + * + * Use element_validate_integer_positive() instead. + * + * @deprecated + * @see element_validate_integer_positive() */ function _element_validate_integer_positive($element, &$form_state) { - $value = $element['#value']; - if ($value !== '' && (!is_numeric($value) || intval($value) != $value || $value <= 0)) { - form_error($element, t('%name must be a positive integer.', array('%name' => $element['#title']))); - } + element_validate_integer_positive($element, $form_state); } /** - * Helper form element validator: number. + * DEPRECATED: Helper form element validator: number. + * + * Use element_validate_number() instead. + * + * @deprecated + * @see element_validate_number() */ function _element_validate_number($element, &$form_state) { - $value = $element['#value']; - if ($value != '' && !is_numeric($value)) { - form_error($element, t('%name must be a number.', array('%name' => $element['#title']))); - } + element_validate_number($element, $form_state); } diff -Naur drupal-7.0/modules/field/field.multilingual.inc drupal-7.66/modules/field/field.multilingual.inc --- drupal-7.0/modules/field/field.multilingual.inc 2011-01-03 19:03:54.000000000 +0100 +++ drupal-7.66/modules/field/field.multilingual.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ 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 @@ -181,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); + } } } @@ -189,7 +241,7 @@ foreach ($field['foreign keys'] as $specifier => $specification) { $real_name = _field_sql_storage_indexname($field['field_name'], $specifier); $current['foreign keys'][$real_name]['table'] = $specification['table']; - foreach ($specification['columns'] as $column => $referenced) { + foreach ($specification['columns'] as $column_name => $referenced) { $sql_storage_column = _field_sql_storage_columnname($field['field_name'], $column_name); $current['foreign keys'][$real_name]['columns'][$sql_storage_column] = $referenced; } @@ -237,13 +289,37 @@ function field_sql_storage_field_storage_update_field($field, $prior_field, $has_data) { if (! $has_data) { // There is no data. Re-create the tables completely. - $prior_schema = _field_sql_storage_schema($prior_field); - foreach ($prior_schema as $name => $table) { - db_drop_table($name, $table); - } - $schema = _field_sql_storage_schema($field); - foreach ($schema as $name => $table) { - db_create_table($name, $table); + + if (Database::getConnection()->supportsTransactionalDDL()) { + // If the database supports transactional DDL, we can go ahead and rely + // on it. If not, we will have to rollback manually if something fails. + $transaction = db_transaction(); + } + + try { + $prior_schema = _field_sql_storage_schema($prior_field); + foreach ($prior_schema as $name => $table) { + db_drop_table($name, $table); + } + $schema = _field_sql_storage_schema($field); + foreach ($schema as $name => $table) { + db_create_table($name, $table); + } + } + catch (Exception $e) { + if (Database::getConnection()->supportsTransactionalDDL()) { + $transaction->rollback(); + } + else { + // Recreate tables. + $prior_schema = _field_sql_storage_schema($prior_field); + foreach ($prior_schema as $name => $table) { + if (!db_table_exists($name)) { + db_create_table($name, $table); + } + } + } + throw $e; } } else { @@ -266,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); @@ -301,11 +387,14 @@ * Implements hook_field_storage_load(). */ function field_sql_storage_field_storage_load($entity_type, $entities, $age, $fields, $options) { - $field_info = field_info_field_by_ids(); $load_current = $age == FIELD_LOAD_CURRENT; foreach ($fields as $field_id => $ids) { - $field = $field_info[$field_id]; + // By the time this hook runs, the relevant field definitions have been + // populated and cached in FieldInfo, so calling field_info_field_by_id() + // on each field individually is more efficient than loading all fields in + // memory upfront with field_info_field_by_ids(). + $field = field_info_field_by_id($field_id); $field_name = $field['field_name']; $table = $load_current ? _field_sql_storage_tablename($field) : _field_sql_storage_revision_tablename($field); @@ -396,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, @@ -469,7 +558,6 @@ * Implements hook_field_storage_query(). */ function field_sql_storage_field_storage_query(EntityFieldQuery $query) { - $groups = array(); if ($query->age == FIELD_LOAD_CURRENT) { $tablename_function = '_field_sql_storage_tablename'; $id_key = 'entity_id'; @@ -479,47 +567,42 @@ $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); - $select_query->addTag('entity_field_access'); + // 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. + if (!isset($query->tags['DANGEROUS_ACCESS_CHECK_OPT_OUT'])) { + $select_query->addTag('entity_field_access'); + } $select_query->addMetaData('base_table', $tablename); $select_query->fields($table_alias, array('entity_type', 'entity_id', 'revision_id', 'bundle')); $field_base_table = $table_alias; } - if ($field['cardinality'] != 1) { + if ($field['cardinality'] != 1 || $field['translatable']) { $select_query->distinct(); } } - // Add field conditions. - foreach ($query->fieldConditions as $key => $condition) { - $table_alias = $table_aliases[$key]; - $field = $condition['field']; - // Add the specified condition. - $sql_field = "$table_alias." . _field_sql_storage_columnname($field['field_name'], $condition['column']); - $query->addCondition($select_query, $sql_field, $condition); - // Add delta / language group conditions. - foreach (array('delta', 'language') as $column) { - if (isset($condition[$column . '_group'])) { - $group_name = $condition[$column . '_group']; - if (!isset($groups[$column][$group_name])) { - $groups[$column][$group_name] = $table_alias; - } - else { - $select_query->where("$table_alias.$column = " . $groups[$column][$group_name] . ".$column"); - } - } - } - } + // Add field conditions. We need a fresh grouping cache. + drupal_static_reset('_field_sql_storage_query_field_conditions'); + _field_sql_storage_query_field_conditions($query, $select_query, $query->fieldConditions, $table_aliases, '_field_sql_storage_columnname'); + + // Add field meta conditions. + _field_sql_storage_query_field_conditions($query, $select_query, $query->fieldMetaConditions, $table_aliases, '_field_sql_storage_query_columnname'); if (isset($query->deleted)) { $select_query->condition("$field_base_table.deleted", (int) $query->deleted); @@ -593,6 +676,51 @@ } /** + * Adds field (meta) conditions to the given query objects respecting groupings. + * + * @param EntityFieldQuery $query + * The field query object to be processed. + * @param SelectQuery $select_query + * The SelectQuery that should get grouping conditions. + * @param condtions + * The conditions to be added. + * @param $table_aliases + * An associative array of table aliases keyed by field index. + * @param $column_callback + * A callback that should return the column name to be used for the field + * conditions. Accepts a field name and a field column name as parameters. + */ +function _field_sql_storage_query_field_conditions(EntityFieldQuery $query, SelectQuery $select_query, $conditions, $table_aliases, $column_callback) { + $groups = &drupal_static(__FUNCTION__, array()); + foreach ($conditions as $key => $condition) { + $table_alias = $table_aliases[$key]; + $field = $condition['field']; + // Add the specified condition. + $sql_field = "$table_alias." . $column_callback($field['field_name'], $condition['column']); + $query->addCondition($select_query, $sql_field, $condition); + // Add delta / language group conditions. + foreach (array('delta', 'language') as $column) { + if (isset($condition[$column . '_group'])) { + $group_name = $condition[$column . '_group']; + if (!isset($groups[$column][$group_name])) { + $groups[$column][$group_name] = $table_alias; + } + else { + $select_query->where("$table_alias.$column = " . $groups[$column][$group_name] . ".$column"); + } + } + } + } +} + +/** + * Field meta condition column callback. + */ +function _field_sql_storage_query_columnname($field_name, $column) { + return $column; +} + +/** * Implements hook_field_storage_delete_revision(). * * This function actually deletes the data from the database. diff -Naur drupal-7.0/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.0/modules/field/modules/field_sql_storage/field_sql_storage.test 2010-12-14 20:50:05.000000000 +0100 +++ drupal-7.66/modules/field/modules/field_sql_storage/field_sql_storage.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,9 +1,8 @@ table, 't')->fields('t')->execute()->fetchAllAssoc('delta', PDO::FETCH_ASSOC); foreach ($values as $delta => $value) { if ($delta < $this->field['cardinality']) { - $this->assertEqual($rows[$delta][$this->field_name . '_value'], $value['value'], t("Value $delta is inserted correctly")); + $this->assertEqual($rows[$delta][$this->field_name . '_value'], $value['value'], format_string("Value %delta is inserted correctly", array('%delta' => $delta))); } else { $this->assertFalse(array_key_exists($delta, $rows), "No extraneous value gets inserted."); @@ -146,7 +145,7 @@ $rows = db_select($this->table, 't')->fields('t')->execute()->fetchAllAssoc('delta', PDO::FETCH_ASSOC); foreach ($values as $delta => $value) { if ($delta < $this->field['cardinality']) { - $this->assertEqual($rows[$delta][$this->field_name . '_value'], $value['value'], t("Value $delta is updated correctly")); + $this->assertEqual($rows[$delta][$this->field_name . '_value'], $value['value'], format_string("Value %delta is updated correctly", array('%delta' => $delta))); } else { $this->assertFalse(array_key_exists($delta, $rows), "No extraneous value gets updated."); @@ -176,7 +175,7 @@ $rows = db_select($this->table, 't')->fields('t')->execute()->fetchAllAssoc('delta', PDO::FETCH_ASSOC); foreach ($values as $delta => $value) { if ($delta < $this->field['cardinality']) { - $this->assertEqual($rows[$delta][$this->field_name . '_value'], $value['value'], t("Update with no field_name entry leaves value $delta untouched")); + $this->assertEqual($rows[$delta][$this->field_name . '_value'], $value['value'], format_string("Update with no field_name entry leaves value %delta untouched", array('%delta' => $delta))); } } @@ -184,7 +183,7 @@ $entity->{$this->field_name} = NULL; field_attach_update($entity_type, $entity); $rows = db_select($this->table, 't')->fields('t')->execute()->fetchAllAssoc('delta', PDO::FETCH_ASSOC); - $this->assertEqual(count($rows), 0, t("Update with an empty field_name entry empties the field.")); + $this->assertEqual(count($rows), 0, "Update with an empty field_name entry empties the field."); } /** @@ -307,6 +306,31 @@ } /** + * Test that failure to create fields is handled gracefully. + */ + function testFieldUpdateFailure() { + // Create a text field. + $field = array('field_name' => 'test_text', 'type' => 'text', 'settings' => array('max_length' => 255)); + $field = field_create_field($field); + + // Attempt to update the field in a way that would break the storage. + $prior_field = $field; + $field['settings']['max_length'] = -1; + try { + field_update_field($field); + $this->fail(t('Update succeeded.')); + } + catch (Exception $e) { + $this->pass(t('Update properly failed.')); + } + + // Ensure that the field tables are still there. + foreach (_field_sql_storage_schema($prior_field) as $table_name => $table_info) { + $this->assertTrue(db_table_exists($table_name), format_string('Table %table exists.', array('%table' => $table_name))); + } + } + + /** * Test adding and removing indexes while data is present. */ function testFieldUpdateIndexesWithData() { @@ -321,8 +345,8 @@ // Verify the indexes we will create do not exist yet. foreach ($tables as $table) { - $this->assertFalse(Database::getConnection()->schema()->indexExists($table, 'value'), t("No index named value exists in $table")); - $this->assertFalse(Database::getConnection()->schema()->indexExists($table, 'value_format'), t("No index named value_format exists in $table")); + $this->assertFalse(Database::getConnection()->schema()->indexExists($table, 'value'), format_string("No index named value exists in %table", array('%table' => $table))); + $this->assertFalse(Database::getConnection()->schema()->indexExists($table, 'value_format'), format_string("No index named value_format exists in %table", array('%table' => $table))); } // Add data so the table cannot be dropped. @@ -331,24 +355,24 @@ 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"), t("Index on value created in $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"), t("Index on value_format created in $table")); - $this->assertFalse(Database::getConnection()->schema()->indexExists($table, "{$field_name}_value"), t("Index on value removed in $table")); + $this->assertTrue(Database::getConnection()->schema()->indexExists($table, "{$field_name}_value_format"), format_string("Index on value_format created in %table", array('%table' => $table))); + $this->assertFalse(Database::getConnection()->schema()->indexExists($table, "{$field_name}_value"), format_string("Index on value removed in %table", array('%table' => $table))); } // Verify that the tables were not dropped. $entity = field_test_create_stub_entity(0, 0, $instance['bundle']); field_attach_load('test_entity', array(0 => $entity)); - $this->assertEqual($entity->{$field_name}[LANGUAGE_NONE][0]['value'], 'field data', t("Index changes performed without dropping the tables")); + $this->assertEqual($entity->{$field_name}[LANGUAGE_NONE][0]['value'], 'field data', "Index changes performed without dropping the tables"); } /** @@ -363,19 +387,19 @@ $instance = field_info_instance($this->instance['entity_type'], $this->instance['field_name'], $this->instance['bundle']); // The storage details are indexed by a storage engine type. - $this->assertTrue(array_key_exists('sql', $field['storage']['details']), t('The storage type is SQL.')); + $this->assertTrue(array_key_exists('sql', $field['storage']['details']), 'The storage type is SQL.'); // The SQL details are indexed by table name. $details = $field['storage']['details']['sql']; - $this->assertTrue(array_key_exists($current, $details[FIELD_LOAD_CURRENT]), t('Table name is available in the instance array.')); - $this->assertTrue(array_key_exists($revision, $details[FIELD_LOAD_REVISION]), t('Revision table name is available in the instance array.')); + $this->assertTrue(array_key_exists($current, $details[FIELD_LOAD_CURRENT]), 'Table name is available in the instance array.'); + $this->assertTrue(array_key_exists($revision, $details[FIELD_LOAD_REVISION]), 'Revision table name is available in the instance array.'); // Test current and revision storage details together because the columns // are the same. foreach ((array) $this->field['columns'] as $column_name => $attributes) { $storage_column_name = _field_sql_storage_columnname($this->field['field_name'], $column_name); - $this->assertEqual($details[FIELD_LOAD_CURRENT][$current][$column_name], $storage_column_name, t('Column name %value matches the definition in %bin.', array('%value' => $column_name, '%bin' => $current))); - $this->assertEqual($details[FIELD_LOAD_REVISION][$revision][$column_name], $storage_column_name, t('Column name %value matches the definition in %bin.', array('%value' => $column_name, '%bin' => $revision))); + $this->assertEqual($details[FIELD_LOAD_CURRENT][$current][$column_name], $storage_column_name, format_string('Column name %value matches the definition in %bin.', array('%value' => $column_name, '%bin' => $current))); + $this->assertEqual($details[FIELD_LOAD_REVISION][$revision][$column_name], $storage_column_name, format_string('Column name %value matches the definition in %bin.', array('%value' => $column_name, '%bin' => $revision))); } } @@ -383,21 +407,180 @@ * Test foreign key support. */ function testFieldSqlStorageForeignKeys() { - // Create a decimal field. + // Create a 'shape' field, with a configurable foreign key (see + // field_test_field_schema()). $field_name = 'testfield'; - $field = array('field_name' => $field_name, 'type' => 'text'); - $field = field_create_field($field); - // Retrieve the field and instance with field_info and verify the foreign - // keys are in place. + $foreign_key_name = 'shape'; + $field = array('field_name' => $field_name, 'type' => 'shape', 'settings' => array('foreign_key_name' => $foreign_key_name)); + field_create_field($field); + + // Retrieve the field definition and check that the foreign key is in place. + $field = field_info_field($field_name); + $this->assertEqual($field['foreign keys'][$foreign_key_name]['table'], $foreign_key_name, 'Foreign key table name preserved through CRUD'); + $this->assertEqual($field['foreign keys'][$foreign_key_name]['columns'][$foreign_key_name], 'id', 'Foreign key column name preserved through CRUD'); + + // Update the field settings, it should update the foreign key definition + // too. + $foreign_key_name = 'color'; + $field['settings']['foreign_key_name'] = $foreign_key_name; + field_update_field($field); + + // Retrieve the field definition and check that the foreign key is in place. $field = field_info_field($field_name); - $this->assertEqual($field['foreign keys']['format']['table'], 'filter_format', t('Foreign key table name preserved through CRUD')); - $this->assertEqual($field['foreign keys']['format']['columns']['format'], 'format', t('Foreign key column name preserved through CRUD')); + $this->assertEqual($field['foreign keys'][$foreign_key_name]['table'], $foreign_key_name, 'Foreign key table name modified after update'); + $this->assertEqual($field['foreign keys'][$foreign_key_name]['columns'][$foreign_key_name], 'id', 'Foreign key column name modified after update'); + // Now grab the SQL schema and verify that too. - $schema = drupal_get_schema(_field_sql_storage_tablename($field)); - $this->assertEqual(count($schema['foreign keys']), 1, t("There is 1 foreign key in the schema")); + $schema = drupal_get_schema(_field_sql_storage_tablename($field), TRUE); + $this->assertEqual(count($schema['foreign keys']), 1, 'There is 1 foreign key in the schema'); $foreign_key = reset($schema['foreign keys']); - $filter_column = _field_sql_storage_columnname($field['field_name'], 'format'); - $this->assertEqual($foreign_key['table'], 'filter_format', t('Foreign key table name preserved in the schema')); - $this->assertEqual($foreign_key['columns'][$filter_column], 'format', t('Foreign key column name preserved in the schema')); + $foreign_key_column = _field_sql_storage_columnname($field['field_name'], $foreign_key_name); + $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.0/modules/field/modules/list/list.info drupal-7.66/modules/field/modules/list/list.info --- drupal-7.0/modules/field/modules/list/list.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/field/modules/list/list.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: list.info,v 1.10 2010/12/20 19:59:41 webchick Exp $ name = List description = Defines list field types. Use with Options to create selection lists. package = Core @@ -8,8 +7,7 @@ dependencies[] = options files[] = tests/list.test -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/field/modules/list/list.install drupal-7.66/modules/field/modules/list/list.install --- drupal-7.0/modules/field/modules/list/list.install 2010-12-18 01:50:03.000000000 +0100 +++ drupal-7.66/modules/field/modules/list/list.install 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ 'list')); - foreach ($fields as $field_name => $field) { + foreach ($fields as $field) { $update = array(); // Translate the old string format into the new array format. @@ -61,8 +60,8 @@ $allowed_values = _list_update_7001_extract_allowed_values($allowed_values, $position_keys); // Additionally, float keys need to be disambiguated ('.5' is '0.5'). - if ($field['type'] == 'list_number') { - $keys = array_map(create_function('$a', 'return (string) (float) $a;'), array_keys($allowed_values)); + if ($field['type'] == 'list_number' && !empty($allowed_values)) { + $keys = array_map('_list_update_7001_float_string_cast', array_keys($allowed_values)); $allowed_values = array_combine($keys, array_values($allowed_values)); } @@ -90,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. @@ -116,3 +122,24 @@ return $values; } + +/** + * @addtogroup updates-7.x-extra + * @{ + */ + +/** + * Re-apply list_update_7001() for deleted fields. + */ +function list_update_7002() { + // See http://drupal.org/node/1022924: list_update_7001() intitally + // overlooked deleted fields, which then caused fatal errors when the fields + // were being purged. + // list_update_7001() has the required checks to ensure it is reentrant, so + // it can simply be executed once more.. + list_update_7001(); +} + +/** + * @} End of "addtogroup updates-7.x-extra". + */ diff -Naur drupal-7.0/modules/field/modules/list/list.module drupal-7.66/modules/field/modules/list/list.module --- drupal-7.0/modules/field/modules/list/list.module 2010-12-18 01:50:03.000000000 +0100 +++ drupal-7.66/modules/field/modules/list/list.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ $field['field_name']))); } } } @@ -374,13 +388,13 @@ * - 'list_illegal_value': The value is not part of the list of allowed values. */ function list_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) { - $allowed_values = list_allowed_values($field); + $allowed_values = list_allowed_values($field, $instance, $entity_type, $entity); foreach ($items as $delta => $item) { if (!empty($item['value'])) { if (!empty($allowed_values) && !isset($allowed_values[$item['value']])) { $errors[$field['field_name']][$langcode][$delta][] = array( 'error' => 'list_illegal_value', - 'message' => t('%name: illegal value.', array('%name' => t($instance['label']))), + 'message' => t('%name: illegal value.', array('%name' => $instance['label'])), ); } } @@ -420,8 +434,8 @@ /** * Implements hook_options_list(). */ -function list_options_list($field) { - return list_allowed_values($field); +function list_options_list($field, $instance, $entity_type, $entity) { + return list_allowed_values($field, $instance, $entity_type, $entity); } /** @@ -448,7 +462,7 @@ switch ($display['type']) { case 'list_default': - $allowed_values = list_allowed_values($field); + $allowed_values = list_allowed_values($field, $instance, $entity_type, $entity); foreach ($items as $delta => $item) { if (isset($allowed_values[$item['value']])) { $output = field_filter_xss($allowed_values[$item['value']]); diff -Naur drupal-7.0/modules/field/modules/list/tests/list.test drupal-7.66/modules/field/modules/list/tests/list.test --- drupal-7.0/modules/field/modules/list/tests/list.test 2010-12-18 01:50:03.000000000 +0100 +++ drupal-7.66/modules/field/modules/list/tests/list.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,9 +1,8 @@ assertTrue(!empty($form[$this->field_name][$langcode][1]), t('Option 1 exists')); - $this->assertTrue(!empty($form[$this->field_name][$langcode][2]), t('Option 2 exists')); - $this->assertTrue(!empty($form[$this->field_name][$langcode][3]), t('Option 3 exists')); + $this->assertTrue(!empty($form[$this->field_name][$langcode][1]), 'Option 1 exists'); + $this->assertTrue(!empty($form[$this->field_name][$langcode][2]), 'Option 2 exists'); + $this->assertTrue(!empty($form[$this->field_name][$langcode][3]), 'Option 3 exists'); + + // Use one of the values in an actual entity, and check that this value + // cannot be removed from the list. + $entity = field_test_create_stub_entity(); + $entity->{$this->field_name}[$langcode][0] = array('value' => 1); + field_test_entity_save($entity); + $this->field['settings']['allowed_values'] = array(2 => 'Two'); + try { + field_update_field($this->field); + $this->fail(t('Cannot update a list field to not include keys with existing data.')); + } + catch (FieldException $e) { + $this->pass(t('Cannot update a list field to not include keys with existing data.')); + } + // Empty the value, so that we can actually remove the option. + $entity->{$this->field_name}[$langcode] = array(); + field_test_entity_save($entity); // Removed options do not appear. $this->field['settings']['allowed_values'] = array(2 => 'Two'); field_update_field($this->field); $entity = field_test_create_stub_entity(); $form = drupal_get_form('field_test_entity_form', $entity); - $this->assertTrue(empty($form[$this->field_name][$langcode][1]), t('Option 1 does not exist')); - $this->assertTrue(!empty($form[$this->field_name][$langcode][2]), t('Option 2 exists')); - $this->assertTrue(empty($form[$this->field_name][$langcode][3]), t('Option 3 does not exist')); + $this->assertTrue(empty($form[$this->field_name][$langcode][1]), 'Option 1 does not exist'); + $this->assertTrue(!empty($form[$this->field_name][$langcode][2]), 'Option 2 exists'); + $this->assertTrue(empty($form[$this->field_name][$langcode][3]), 'Option 3 does not exist'); // Completely new options appear. $this->field['settings']['allowed_values'] = array(10 => 'Update', 20 => 'Twenty'); field_update_field($this->field); $form = drupal_get_form('field_test_entity_form', $entity); - $this->assertTrue(empty($form[$this->field_name][$langcode][1]), t('Option 1 does not exist')); - $this->assertTrue(empty($form[$this->field_name][$langcode][2]), t('Option 2 does not exist')); - $this->assertTrue(empty($form[$this->field_name][$langcode][3]), t('Option 3 does not exist')); - $this->assertTrue(!empty($form[$this->field_name][$langcode][10]), t('Option 10 exists')); - $this->assertTrue(!empty($form[$this->field_name][$langcode][20]), t('Option 20 exists')); + $this->assertTrue(empty($form[$this->field_name][$langcode][1]), 'Option 1 does not exist'); + $this->assertTrue(empty($form[$this->field_name][$langcode][2]), 'Option 2 does not exist'); + $this->assertTrue(empty($form[$this->field_name][$langcode][3]), 'Option 3 does not exist'); + $this->assertTrue(!empty($form[$this->field_name][$langcode][10]), 'Option 10 exists'); + $this->assertTrue(!empty($form[$this->field_name][$langcode][20]), 'Option 20 exists'); // Options are reset when a new field with the same name is created. field_delete_field($this->field_name); @@ -91,15 +107,98 @@ $this->instance = field_create_instance($this->instance); $entity = field_test_create_stub_entity(); $form = drupal_get_form('field_test_entity_form', $entity); - $this->assertTrue(!empty($form[$this->field_name][$langcode][1]), t('Option 1 exists')); - $this->assertTrue(!empty($form[$this->field_name][$langcode][2]), t('Option 2 exists')); - $this->assertTrue(!empty($form[$this->field_name][$langcode][3]), t('Option 3 exists')); + $this->assertTrue(!empty($form[$this->field_name][$langcode][1]), 'Option 1 exists'); + $this->assertTrue(!empty($form[$this->field_name][$langcode][2]), 'Option 2 exists'); + $this->assertTrue(!empty($form[$this->field_name][$langcode][3]), 'Option 3 exists'); + } +} + +/** + * Sets up a List field for testing allowed values functions. + */ +class ListDynamicValuesTestCase extends FieldTestCase { + function setUp() { + parent::setUp(array('list', 'field_test', 'list_test')); + + $this->field_name = 'test_list'; + $this->field = array( + 'field_name' => $this->field_name, + 'type' => 'list_text', + 'cardinality' => 1, + 'settings' => array( + 'allowed_values_function' => 'list_test_dynamic_values_callback', + ), + ); + $this->field = field_create_field($this->field); + + $this->instance = array( + 'field_name' => $this->field_name, + 'entity_type' => 'test_entity', + 'bundle' => 'test_bundle', + 'required' => TRUE, + 'widget' => array( + 'type' => 'options_select', + ), + ); + $this->instance = field_create_instance($this->instance); + $this->test = array( + 'id' => mt_rand(1, 10), + // Make sure this does not equal the ID so that + // list_test_dynamic_values_callback() always returns 4 values. + 'vid' => mt_rand(20, 30), + 'bundle' => 'test_bundle', + 'label' => $this->randomName(), + ); + $this->entity = call_user_func_array('field_test_create_stub_entity', $this->test); + } +} + +/** + * Tests the List field allowed values function. + */ +class ListDynamicValuesValidationTestCase extends ListDynamicValuesTestCase { + public static function getInfo() { + return array( + 'name' => 'List field dynamic values', + 'description' => 'Test the List field allowed values function.', + 'group' => 'Field types', + ); + } + + /** + * Test that allowed values function gets the entity. + */ + function testDynamicAllowedValues() { + // Verify that the test passes against every value we had. + foreach ($this->test as $key => $value) { + $this->entity->test_list[LANGUAGE_NONE][0]['value'] = $value; + try { + field_attach_validate('test_entity', $this->entity); + $this->pass("$key should pass"); + } + catch (FieldValidationException $e) { + // This will display as an exception, no need for a separate error. + throw($e); + } + } + // Now verify that the test does not pass against anything else. + foreach ($this->test as $key => $value) { + $this->entity->test_list[LANGUAGE_NONE][0]['value'] = is_numeric($value) ? (100 - $value) : ('X' . $value); + $pass = FALSE; + try { + field_attach_validate('test_entity', $this->entity); + } + catch (FieldValidationException $e) { + $pass = TRUE; + } + $this->assertTrue($pass, $key . ' should not pass'); + } } } /** -* List module UI tests. -*/ + * List module UI tests. + */ class ListFieldUITestCase extends FieldTestCase { public static function getInfo() { return array( @@ -113,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. @@ -134,20 +233,20 @@ // Flat list of textual values. $string = "Zero\nOne"; $array = array('0' => 'Zero', '1' => 'One'); - $this->assertAllowedValuesInput($string, $array, t('Unkeyed lists are accepted.')); + $this->assertAllowedValuesInput($string, $array, 'Unkeyed lists are accepted.'); // Explicit integer keys. $string = "0|Zero\n2|Two"; $array = array('0' => 'Zero', '2' => 'Two'); - $this->assertAllowedValuesInput($string, $array, t('Integer keys are accepted.')); + $this->assertAllowedValuesInput($string, $array, 'Integer keys are accepted.'); // Check that values can be added and removed. $string = "0|Zero\n1|One"; $array = array('0' => 'Zero', '1' => 'One'); - $this->assertAllowedValuesInput($string, $array, t('Values can be added and removed.')); + $this->assertAllowedValuesInput($string, $array, 'Values can be added and removed.'); // Non-integer keys. - $this->assertAllowedValuesInput("1.1|One", 'keys must be integers', t('Non integer keys are rejected.')); - $this->assertAllowedValuesInput("abc|abc", 'keys must be integers', t('Non integer keys are rejected.')); + $this->assertAllowedValuesInput("1.1|One", 'keys must be integers', 'Non integer keys are rejected.'); + $this->assertAllowedValuesInput("abc|abc", 'keys must be integers', 'Non integer keys are rejected.'); // Mixed list of keyed and unkeyed values. - $this->assertAllowedValuesInput("Zero\n1|One", 'invalid input', t('Mixed lists are rejected.')); + $this->assertAllowedValuesInput("Zero\n1|One", 'invalid input', 'Mixed lists are rejected.'); // Create a node with actual data for the field. $settings = array( @@ -157,22 +256,22 @@ $node = $this->drupalCreateNode($settings); // Check that a flat list of values is rejected once the field has data. - $this->assertAllowedValuesInput( "Zero\nOne", 'invalid input', t('Unkeyed lists are rejected once the field has data.')); + $this->assertAllowedValuesInput( "Zero\nOne", 'invalid input', 'Unkeyed lists are rejected once the field has data.'); // Check that values can be added but values in use cannot be removed. $string = "0|Zero\n1|One\n2|Two"; $array = array('0' => 'Zero', '1' => 'One', '2' => 'Two'); - $this->assertAllowedValuesInput($string, $array, t('Values can be added.')); + $this->assertAllowedValuesInput($string, $array, 'Values can be added.'); $string = "0|Zero\n1|One"; $array = array('0' => 'Zero', '1' => 'One'); - $this->assertAllowedValuesInput($string, $array, t('Values not in use can be removed.')); - $this->assertAllowedValuesInput("0|Zero", 'some values are being removed while currently in use', t('Values in use cannot be removed.')); + $this->assertAllowedValuesInput($string, $array, 'Values not in use can be removed.'); + $this->assertAllowedValuesInput("0|Zero", 'some values are being removed while currently in use', 'Values in use cannot be removed.'); // Delete the node, remove the value. node_delete($node->nid); $string = "0|Zero"; $array = array('0' => 'Zero'); - $this->assertAllowedValuesInput($string, $array, t('Values not in use can be removed.')); + $this->assertAllowedValuesInput($string, $array, 'Values not in use can be removed.'); } /** @@ -185,19 +284,19 @@ // Flat list of textual values. $string = "Zero\nOne"; $array = array('0' => 'Zero', '1' => 'One'); - $this->assertAllowedValuesInput($string, $array, t('Unkeyed lists are accepted.')); + $this->assertAllowedValuesInput($string, $array, 'Unkeyed lists are accepted.'); // Explicit numeric keys. $string = "0|Zero\n.5|Point five"; $array = array('0' => 'Zero', '0.5' => 'Point five'); - $this->assertAllowedValuesInput($string, $array, t('Integer keys are accepted.')); + $this->assertAllowedValuesInput($string, $array, 'Integer keys are accepted.'); // Check that values can be added and removed. $string = "0|Zero\n.5|Point five\n1.0|One"; $array = array('0' => 'Zero', '0.5' => 'Point five', '1' => 'One'); - $this->assertAllowedValuesInput($string, $array, t('Values can be added and removed.')); + $this->assertAllowedValuesInput($string, $array, 'Values can be added and removed.'); // Non-numeric keys. - $this->assertAllowedValuesInput("abc|abc\n", 'each key must be a valid integer or decimal', t('Non numeric keys are rejected.')); + $this->assertAllowedValuesInput("abc|abc\n", 'each key must be a valid integer or decimal', 'Non numeric keys are rejected.'); // Mixed list of keyed and unkeyed values. - $this->assertAllowedValuesInput("Zero\n1|One\n", 'invalid input', t('Mixed lists are rejected.')); + $this->assertAllowedValuesInput("Zero\n1|One\n", 'invalid input', 'Mixed lists are rejected.'); // Create a node with actual data for the field. $settings = array( @@ -207,22 +306,22 @@ $node = $this->drupalCreateNode($settings); // Check that a flat list of values is rejected once the field has data. - $this->assertAllowedValuesInput("Zero\nOne", 'invalid input', t('Unkeyed lists are rejected once the field has data.')); + $this->assertAllowedValuesInput("Zero\nOne", 'invalid input', 'Unkeyed lists are rejected once the field has data.'); // Check that values can be added but values in use cannot be removed. $string = "0|Zero\n.5|Point five\n2|Two"; $array = array('0' => 'Zero', '0.5' => 'Point five', '2' => 'Two'); - $this->assertAllowedValuesInput($string, $array, t('Values can be added.')); + $this->assertAllowedValuesInput($string, $array, 'Values can be added.'); $string = "0|Zero\n.5|Point five"; $array = array('0' => 'Zero', '0.5' => 'Point five'); - $this->assertAllowedValuesInput($string, $array, t('Values not in use can be removed.')); - $this->assertAllowedValuesInput("0|Zero", 'some values are being removed while currently in use', t('Values in use cannot be removed.')); + $this->assertAllowedValuesInput($string, $array, 'Values not in use can be removed.'); + $this->assertAllowedValuesInput("0|Zero", 'some values are being removed while currently in use', 'Values in use cannot be removed.'); // Delete the node, remove the value. node_delete($node->nid); $string = "0|Zero"; $array = array('0' => 'Zero'); - $this->assertAllowedValuesInput($string, $array, t('Values not in use can be removed.')); + $this->assertAllowedValuesInput($string, $array, 'Values not in use can be removed.'); } /** @@ -235,21 +334,21 @@ // Flat list of textual values. $string = "Zero\nOne"; $array = array('Zero' => 'Zero', 'One' => 'One'); - $this->assertAllowedValuesInput($string, $array, t('Unkeyed lists are accepted.')); + $this->assertAllowedValuesInput($string, $array, 'Unkeyed lists are accepted.'); // Explicit keys. $string = "zero|Zero\none|One"; $array = array('zero' => 'Zero', 'one' => 'One'); - $this->assertAllowedValuesInput($string, $array, t('Explicit keys are accepted.')); + $this->assertAllowedValuesInput($string, $array, 'Explicit keys are accepted.'); // Check that values can be added and removed. $string = "zero|Zero\ntwo|Two"; $array = array('zero' => 'Zero', 'two' => 'Two'); - $this->assertAllowedValuesInput($string, $array, t('Values can be added and removed.')); + $this->assertAllowedValuesInput($string, $array, 'Values can be added and removed.'); // Mixed list of keyed and unkeyed values. $string = "zero|Zero\nOne\n"; $array = array('zero' => 'Zero', 'One' => 'One'); - $this->assertAllowedValuesInput($string, $array, t('Mixed lists are accepted.')); + $this->assertAllowedValuesInput($string, $array, 'Mixed lists are accepted.'); // Overly long keys. - $this->assertAllowedValuesInput("zero|Zero\n" . $this->randomName(256) . "|One", 'each key must be a string at most 255 characters long', t('Overly long keys are rejected.')); + $this->assertAllowedValuesInput("zero|Zero\n" . $this->randomName(256) . "|One", 'each key must be a string at most 255 characters long', 'Overly long keys are rejected.'); // Create a node with actual data for the field. $settings = array( @@ -262,22 +361,22 @@ // data. $string = "Zero\nOne"; $array = array('Zero' => 'Zero', 'One' => 'One'); - $this->assertAllowedValuesInput($string, $array, t('Unkeyed lists are still accepted once the field has data.')); + $this->assertAllowedValuesInput($string, $array, 'Unkeyed lists are still accepted once the field has data.'); // Check that values can be added but values in use cannot be removed. $string = "Zero\nOne\nTwo"; $array = array('Zero' => 'Zero', 'One' => 'One', 'Two' => 'Two'); - $this->assertAllowedValuesInput($string, $array, t('Values can be added.')); + $this->assertAllowedValuesInput($string, $array, 'Values can be added.'); $string = "Zero\nOne"; $array = array('Zero' => 'Zero', 'One' => 'One'); - $this->assertAllowedValuesInput($string, $array, t('Values not in use can be removed.')); - $this->assertAllowedValuesInput("Zero", 'some values are being removed while currently in use', t('Values in use cannot be removed.')); + $this->assertAllowedValuesInput($string, $array, 'Values not in use can be removed.'); + $this->assertAllowedValuesInput("Zero", 'some values are being removed while currently in use', 'Values in use cannot be removed.'); // Delete the node, remove the value. node_delete($node->nid); $string = "Zero"; $array = array('Zero' => 'Zero'); - $this->assertAllowedValuesInput($string, $array, t('Values not in use can be removed.')); + $this->assertAllowedValuesInput($string, $array, 'Values not in use can be removed.'); } /** @@ -287,7 +386,7 @@ $this->field_name = 'field_list_boolean'; $this->createListField('list_boolean'); - // Check that the seperate 'On' and 'Off' form fields work. + // Check that the separate 'On' and 'Off' form fields work. $on = $this->randomName(); $off = $this->randomName(); $allowed_values = array(1 => $on, 0 => $off); @@ -296,15 +395,15 @@ 'off' => $off, ); $this->drupalPost($this->admin_path, $edit, t('Save settings')); - $this->assertText("Saved field_list_boolean configuration.", t("The 'On' and 'Off' form fields work for boolean fields.")); + $this->assertText("Saved field_list_boolean configuration.", "The 'On' and 'Off' form fields work for boolean fields."); // Test the allowed_values on the field settings form. $this->drupalGet($this->admin_path); - $this->assertFieldByName('on', $on, t("The 'On' value is stored correctly.")); - $this->assertFieldByName('off', $off, t("The 'Off' value is stored correctly.")); + $this->assertFieldByName('on', $on, "The 'On' value is stored correctly."); + $this->assertFieldByName('off', $off, "The 'Off' value is stored correctly."); $field = field_info_field($this->field_name); - $this->assertEqual($field['settings']['allowed_values'], $allowed_values, t('The allowed value is correct')); - $this->assertFalse(isset($field['settings']['on']), t('The on value is not saved into settings')); - $this->assertFalse(isset($field['settings']['off']), t('The off value is not saved into settings')); + $this->assertEqual($field['settings']['allowed_values'], $allowed_values, 'The allowed value is correct'); + $this->assertFalse(isset($field['settings']['on']), 'The on value is not saved into settings'); + $this->assertFalse(isset($field['settings']['off']), 'The off value is not saved into settings'); } /** diff -Naur drupal-7.0/modules/field/modules/list/tests/list_test.info drupal-7.66/modules/field/modules/list/tests/list_test.info --- drupal-7.0/modules/field/modules/list/tests/list_test.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/field/modules/list/tests/list_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -;$Id: list_test.info,v 1.2 2010/12/20 19:59:41 webchick Exp $ name = "List test" description = "Support module for the List module tests." core = 7.x @@ -6,8 +5,7 @@ version = VERSION hidden = TRUE -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/field/modules/list/tests/list_test.module drupal-7.66/modules/field/modules/list/tests/list_test.module --- drupal-7.0/modules/field/modules/list/tests/list_test.module 2009-12-14 21:18:55.000000000 +0100 +++ drupal-7.66/modules/field/modules/list/tests/list_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ ftlabel), entity_extract_ids($entity_type, $entity))); +} diff -Naur drupal-7.0/modules/field/modules/number/number.info drupal-7.66/modules/field/modules/number/number.info --- drupal-7.0/modules/field/modules/number/number.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/field/modules/number/number.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: number.info,v 1.9 2010/12/20 19:59:41 webchick Exp $ name = Number description = Defines numeric field types. package = Core @@ -7,8 +6,7 @@ dependencies[] = field files[] = number.test -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/field/modules/number/number.install drupal-7.66/modules/field/modules/number/number.install --- drupal-7.0/modules/field/modules/number/number.install 2010-09-04 17:40:51.000000000 +0200 +++ drupal-7.66/modules/field/modules/number/number.install 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ t('Minimum'), '#default_value' => $settings['min'], '#description' => t('The minimum value that should be allowed in this field. Leave blank for no minimum.'), - '#element_validate' => array('_element_validate_number'), + '#element_validate' => array('element_validate_number'), ); $form['max'] = array( '#type' => 'textfield', '#title' => t('Maximum'), '#default_value' => $settings['max'], '#description' => t('The maximum value that should be allowed in this field. Leave blank for no maximum.'), - '#element_validate' => array('_element_validate_number'), + '#element_validate' => array('element_validate_number'), ); $form['prefix'] = array( '#type' => 'textfield', @@ -139,13 +138,13 @@ if (is_numeric($instance['settings']['min']) && $item['value'] < $instance['settings']['min']) { $errors[$field['field_name']][$langcode][$delta][] = array( 'error' => 'number_min', - 'message' => t('%name: the value may be no less than %min.', array('%name' => t($instance['label']), '%min' => $instance['settings']['min'])), + 'message' => t('%name: the value may be no less than %min.', array('%name' => $instance['label'], '%min' => $instance['settings']['min'])), ); } if (is_numeric($instance['settings']['max']) && $item['value'] > $instance['settings']['max']) { $errors[$field['field_name']][$langcode][$delta][] = array( 'error' => 'number_max', - 'message' => t('%name: the value may be no greater than %max.', array('%name' => t($instance['label']), '%max' => $instance['settings']['max'])), + 'message' => t('%name: the value may be no greater than %max.', array('%name' => $instance['label'], '%max' => $instance['settings']['max'])), ); } } @@ -165,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']); + } + } + } } /** @@ -182,11 +190,18 @@ */ function number_field_formatter_info() { return array( + // The 'Default' formatter is different for integer fields on the one hand, + // and for decimal and float fields on the other hand, in order to be able + // to use different default values for the settings. 'number_integer' => array( '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' + // formatters can use the same code. 'decimal_separator' => '.', 'scale' => 0, 'prefix_suffix' => TRUE, @@ -196,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, @@ -216,40 +231,44 @@ $display = $instance['display'][$view_mode]; $settings = $display['settings']; - $options = array( - '' => t(''), - '.' => t('Decimal point'), - ',' => t('Comma'), - ' ' => t('Space'), - ); - $element['thousand_separator'] = array( - '#type' => 'select', - '#title' => t('Thousand marker'), - '#options' => $options, - '#default_value' => $settings['thousand_separator'], - ); + $element = array(); - if ($display['type'] == 'number_decimal' || $display['type'] == 'number_float') { - $element['decimal_separator'] = array( - '#type' => 'select', - '#title' => t('Decimal marker'), - '#options' => array('.' => t('Decimal point'), ',' => t('Comma')), - '#default_value' => $settings['decimal_separator'], + if ($display['type'] == 'number_decimal' || $display['type'] == 'number_integer') { + $options = array( + '' => t(''), + '.' => t('Decimal point'), + ',' => t('Comma'), + ' ' => t('Space'), ); - $element['scale'] = array( + $element['thousand_separator'] = array( '#type' => 'select', - '#title' => t('Scale'), - '#options' => drupal_map_assoc(range(0, 10)), - '#default_value' => $settings['scale'], - '#description' => t('The number of digits to the right of the decimal.'), + '#title' => t('Thousand marker'), + '#options' => $options, + '#default_value' => $settings['thousand_separator'], ); - } - $element['prefix_suffix'] = array( - '#type' => 'checkbox', - '#title' => t('Display prefix and suffix.'), - '#default_value' => $settings['prefix_suffix'], - ); + if ($display['type'] == 'number_decimal') { + $element['decimal_separator'] = array( + '#type' => 'select', + '#title' => t('Decimal marker'), + '#options' => array('.' => t('Decimal point'), ',' => t('Comma')), + '#default_value' => $settings['decimal_separator'], + ); + $element['scale'] = array( + '#type' => 'select', + '#title' => t('Scale'), + '#options' => drupal_map_assoc(range(0, 10)), + '#default_value' => $settings['scale'], + '#description' => t('The number of digits to the right of the decimal.'), + ); + } + + $element['prefix_suffix'] = array( + '#type' => 'checkbox', + '#title' => t('Display prefix and suffix.'), + '#default_value' => $settings['prefix_suffix'], + ); + } return $element; } @@ -262,9 +281,11 @@ $settings = $display['settings']; $summary = array(); - $summary[] = number_format(1234.1234567890, $settings['scale'], $settings['decimal_separator'], $settings['thousand_separator']); - if ($settings['prefix_suffix']) { - $summary[] = t('Display with prefix and suffix.'); + if ($display['type'] == 'number_decimal' || $display['type'] == 'number_integer') { + $summary[] = number_format(1234.1234567890, $settings['scale'], $settings['decimal_separator'], $settings['thousand_separator']); + if ($settings['prefix_suffix']) { + $summary[] = t('Display with prefix and suffix.'); + } } return implode('
', $summary); @@ -367,22 +388,34 @@ switch ($type) { case 'float': case 'decimal': - $regexp = '@[^-0-9\\' . $field['settings']['decimal_separator'] . ']@'; - $message = t('Only numbers and the decimal separator (@separator) allowed in %field.', array('%field' => t($instance['label']), '@separator' => $field['settings']['decimal_separator'])); + $regexp = '@([^-0-9\\' . $field['settings']['decimal_separator'] . '])|(.-)@'; + $message = t('Only numbers and the decimal separator (@separator) allowed in %field.', array('%field' => $instance['label'], '@separator' => $field['settings']['decimal_separator'])); break; case 'integer': - $regexp = '@[^-0-9]@'; - $message = t('Only numbers are allowed in %field.', array('%field' => t($instance['label']))); + $regexp = '@([^-0-9])|(.-)@'; + $message = t('Only numbers are allowed in %field.', array('%field' => $instance['label'])); break; } if ($value != preg_replace($regexp, '', $value)) { form_error($element, $message); } else { - // Substitute the decimal separator, if ($type == 'decimal' || $type == 'float') { - $value = strtr($value, $field['settings']['decimal_separator'], '.'); + // Verify that only one decimal separator exists in the field. + if (substr_count($value, $field['settings']['decimal_separator']) > 1) { + $message = t('%field: There should only be one decimal separator (@separator).', + array( + '%field' => t($instance['label']), + '@separator' => $field['settings']['decimal_separator'], + ) + ); + form_error($element, $message); + } + else { + // Substitute the decimal separator; things should be fine. + $value = strtr($value, $field['settings']['decimal_separator'], '.'); + } } form_set_value($element, $value, $form_state); } diff -Naur drupal-7.0/modules/field/modules/number/number.test drupal-7.66/modules/field/modules/number/number.test --- drupal-7.0/modules/field/modules/number/number.test 2010-10-23 23:03:22.000000000 +0200 +++ drupal-7.66/modules/field/modules/number/number.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,9 +1,8 @@ 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 content types', 'administer fields')); $this->drupalLogin($this->web_user); } @@ -59,7 +58,7 @@ // Display creation form. $this->drupalGet('test-entity/add/test-bundle'); $langcode = LANGUAGE_NONE; - $this->assertFieldByName("{$this->field['field_name']}[$langcode][0][value]", '', t('Widget is displayed')); + $this->assertFieldByName("{$this->field['field_name']}[$langcode][0][value]", '', 'Widget is displayed'); // Submit a signed decimal value within the allowed precision and scale. $value = '-1234.5678'; @@ -69,7 +68,134 @@ $this->drupalPost(NULL, $edit, t('Save')); 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)), t('Entity was created')); - $this->assertRaw(round($value, 2), t('Value is displayed.')); + $this->assertRaw(t('test_entity @id has been created.', array('@id' => $id)), 'Entity was created'); + $this->assertRaw($value, 'Value is displayed.'); + + // Try to create entries with more than one decimal separator; assert fail. + $wrong_entries = array( + '3.14.159', + '0..45469', + '..4589', + '6.459.52', + '6.3..25', + ); + + foreach ($wrong_entries as $wrong_entry) { + $this->drupalGet('test-entity/add/test-bundle'); + $edit = array( + "{$this->field['field_name']}[$langcode][0][value]" => $wrong_entry, + ); + $this->drupalPost(NULL, $edit, t('Save')); + $this->assertText( + t('There should only be one decimal separator (@separator)', + array('@separator' => $this->field['settings']['decimal_separator'])), + 'Correctly failed to save decimal value with more than one decimal point.' + ); + } + + // Try to create entries with minus sign not in the first position. + $wrong_entries = array( + '3-3', + '4-', + '1.3-', + '1.2-4', + '-10-10', + ); + + foreach ($wrong_entries as $wrong_entry) { + $this->drupalGet('test-entity/add/test-bundle'); + $edit = array( + "{$this->field['field_name']}[$langcode][0][value]" => $wrong_entry, + ); + $this->drupalPost(NULL, $edit, t('Save')); + $this->assertText( + t('Only numbers and the decimal separator (@separator) allowed in ', + array('@separator' => $this->field['settings']['decimal_separator'])), + 'Correctly failed to save decimal value with minus sign in the wrong position.' + ); + } + } + + /** + * Test number_integer field. + */ + function testNumberIntegerField() { + // Display the "Add content type" form. + $this->drupalGet('admin/structure/types/add'); + + // Add a content type. + $name = $this->randomName(); + $type = drupal_strtolower($name); + $edit = array('name' => $name, 'type' => $type); + $this->drupalPost(NULL, $edit, t('Save and add fields')); + + // Add an integer field to the newly-created type. + $label = $this->randomName(); + $field_name = drupal_strtolower($label); + $edit = array( + 'fields[_add_new_field][label]'=> $label, + 'fields[_add_new_field][field_name]' => $field_name, + 'fields[_add_new_field][type]' => 'number_integer', + 'fields[_add_new_field][widget_type]' => 'number', + ); + $this->drupalPost(NULL, $edit, t('Save')); + + // Set the formatter to "number_integer" and to "unformatted", and just + // check that the settings summary does not generate warnings. + $this->drupalGet("admin/structure/types/manage/$type/display"); + $edit = array( + "fields[field_$field_name][type]" => 'number_integer', + ); + $this->drupalPost(NULL, $edit, t('Save')); + $edit = array( + "fields[field_$field_name][type]" => 'number_unformatted', + ); + $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.0/modules/field/modules/options/options.api.php drupal-7.66/modules/field/modules/options/options.api.php --- drupal-7.0/modules/field/modules/options/options.api.php 2009-12-14 21:18:55.000000000 +0100 +++ drupal-7.66/modules/field/modules/options/options.api.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ t('Zero'), diff -Naur drupal-7.0/modules/field/modules/options/options.info drupal-7.66/modules/field/modules/options/options.info --- drupal-7.0/modules/field/modules/options/options.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/field/modules/options/options.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: options.info,v 1.8 2010/12/20 19:59:41 webchick Exp $ name = Options description = Defines selection, check box and radio button widgets for text and numeric fields. package = Core @@ -7,8 +6,7 @@ dependencies[] = field files[] = options.test -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/field/modules/options/options.module drupal-7.66/modules/field/modules/options/options.module --- drupal-7.0/modules/field/modules/options/options.module 2010-12-06 19:04:27.000000000 +0100 +++ drupal-7.66/modules/field/modules/options/options.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ $multiple ? 'checkboxes' : 'radios', // Radio buttons need a scalar value. - '#default_value' => $multiple ? $default_value : reset($default_value), + '#default_value' => $default_value, '#options' => $options, ); break; @@ -175,6 +185,7 @@ $base = array( 'filter_xss' => FALSE, 'strip_tags' => FALSE, + 'strip_tags_and_unescape' => FALSE, 'empty_option' => FALSE, 'optgroups' => FALSE, ); @@ -185,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) { @@ -201,7 +212,7 @@ if (!$required) { $properties['empty_option'] = 'option_none'; } - else if (!$has_value) { + elseif (!$has_value) { $properties['empty_option'] = 'option_select'; } } @@ -230,9 +241,9 @@ /** * Collects the options for a field. */ -function _options_get_options($field, $instance, $properties) { +function _options_get_options($field, $instance, $properties, $entity_type, $entity) { // Get the list of options. - $options = (array) module_invoke($field['module'], 'options_list', $field); + $options = (array) module_invoke($field['module'], 'options_list', $field, $instance, $entity_type, $entity); // Sanitize the options. _options_prepare_options($options, $properties); @@ -261,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.0/modules/field/modules/options/options.test drupal-7.66/modules/field/modules/options/options.test --- drupal-7.0/modules/field/modules/options/options.test 2010-12-18 01:50:03.000000000 +0100 +++ drupal-7.66/modules/field/modules/options/options.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,9 @@ '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); @@ -31,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); @@ -43,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); } @@ -81,7 +101,7 @@ $this->assertNoFieldChecked("edit-card-1-$langcode-0"); $this->assertNoFieldChecked("edit-card-1-$langcode-1"); $this->assertNoFieldChecked("edit-card-1-$langcode-2"); - $this->assertRaw('Some dangerous & unescaped markup', t('Option text was properly filtered.')); + $this->assertRaw('Some dangerous & unescaped markup', 'Option text was properly filtered.'); // Select first option. $edit = array("card_1[$langcode]" => 0); @@ -135,7 +155,7 @@ $this->assertNoFieldChecked("edit-card-2-$langcode-0"); $this->assertNoFieldChecked("edit-card-2-$langcode-1"); $this->assertNoFieldChecked("edit-card-2-$langcode-2"); - $this->assertRaw('Some dangerous & unescaped markup', t('Option text was properly filtered.')); + $this->assertRaw('Some dangerous & unescaped markup', 'Option text was properly filtered.'); // Submit form: select first and third options. $edit = array( @@ -174,7 +194,7 @@ "card_2[$langcode][2]" => TRUE, ); $this->drupalPost(NULL, $edit, t('Save')); - $this->assertText('this field cannot hold more than 2 values', t('Validation error was displayed.')); + $this->assertText('this field cannot hold more than 2 values', 'Validation error was displayed.'); // Submit form: uncheck all options. $edit = array( @@ -221,19 +241,20 @@ // Display form. $this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit'); // A required field without any value has a "none" option. - $this->assertTrue($this->xpath('//select[@id=:id]//option[@value="_none" and text()=:label]', array(':id' => 'edit-card-1-' . $langcode, ':label' => t('- Select a value -'))), t('A required select list has a "Select a value" choice.')); + $this->assertTrue($this->xpath('//select[@id=:id]//option[@value="_none" and text()=:label]', array(':id' => 'edit-card-1-' . $langcode, ':label' => t('- Select a value -'))), 'A required select list has a "Select a value" choice.'); // With no field data, nothing is selected. $this->assertNoOptionSelected("edit-card-1-$langcode", '_none'); $this->assertNoOptionSelected("edit-card-1-$langcode", 0); $this->assertNoOptionSelected("edit-card-1-$langcode", 1); $this->assertNoOptionSelected("edit-card-1-$langcode", 2); - $this->assertRaw('Some dangerous & unescaped markup', t('Option text was properly filtered.')); + $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'); $this->drupalPost(NULL, $edit, t('Save')); - $this->assertRaw(t('!title field is required.', array('!title' => $instance['field_name'])), t('Cannot save a required field when selecting "none" from the select list.')); + $this->assertRaw(t('!title field is required.', array('!title' => $instance['field_name'])), 'Cannot save a required field when selecting "none" from the select list.'); // Submit form: select first option. $edit = array("card_1[$langcode]" => 0); @@ -243,7 +264,7 @@ // Display form: check that the right options are selected. $this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit'); // A required field with a value has no 'none' option. - $this->assertFalse($this->xpath('//select[@id=:id]//option[@value="_none"]', array(':id' => 'edit-card-1-' . $langcode)), t('A required select list with an actual value has no "none" choice.')); + $this->assertFalse($this->xpath('//select[@id=:id]//option[@value="_none"]', array(':id' => 'edit-card-1-' . $langcode)), 'A required select list with an actual value has no "none" choice.'); $this->assertOptionSelected("edit-card-1-$langcode", 0); $this->assertNoOptionSelected("edit-card-1-$langcode", 1); $this->assertNoOptionSelected("edit-card-1-$langcode", 2); @@ -255,7 +276,7 @@ // Display form. $this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit'); // A non-required field has a 'none' option. - $this->assertTrue($this->xpath('//select[@id=:id]//option[@value="_none" and text()=:label]', array(':id' => 'edit-card-1-' . $langcode, ':label' => t('- None -'))), t('A non-required select list has a "None" choice.')); + $this->assertTrue($this->xpath('//select[@id=:id]//option[@value="_none" and text()=:label]', array(':id' => 'edit-card-1-' . $langcode, ':label' => t('- None -'))), 'A non-required select list has a "None" choice.'); // Submit form: Unselect the option. $edit = array("card_1[$langcode]" => '_none'); $this->drupalPost('test-entity/manage/' . $entity->ftid . '/edit', $edit, t('Save')); @@ -272,8 +293,8 @@ $this->assertNoOptionSelected("edit-card-1-$langcode", 0); $this->assertNoOptionSelected("edit-card-1-$langcode", 1); $this->assertNoOptionSelected("edit-card-1-$langcode", 2); - $this->assertRaw('Some dangerous & unescaped markup', t('Option text was properly filtered.')); - $this->assertRaw('Group 1', t('Option groups are displayed.')); + $this->assertRaw('Some dangerous & unescaped markup', 'Option text was properly filtered.'); + $this->assertRaw('Group 1', 'Option groups are displayed.'); // Submit form: select first option. $edit = array("card_1[$langcode]" => 0); @@ -319,7 +340,7 @@ $this->assertNoOptionSelected("edit-card-2-$langcode", 0); $this->assertNoOptionSelected("edit-card-2-$langcode", 1); $this->assertNoOptionSelected("edit-card-2-$langcode", 2); - $this->assertRaw('Some dangerous & unescaped markup', t('Option text was properly filtered.')); + $this->assertRaw('Some dangerous & unescaped markup', 'Option text was properly filtered.'); // Submit form: select first and third options. $edit = array("card_2[$langcode][]" => array(0 => 0, 2 => 2)); @@ -346,7 +367,7 @@ // Submit form: select the three options while the field accepts only 2. $edit = array("card_2[$langcode][]" => array(0 => 0, 1 => 1, 2 => 2)); $this->drupalPost(NULL, $edit, t('Save')); - $this->assertText('this field cannot hold more than 2 values', t('Validation error was displayed.')); + $this->assertText('this field cannot hold more than 2 values', 'Validation error was displayed.'); // Submit form: uncheck all options. $edit = array("card_2[$langcode][]" => array()); @@ -355,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')); @@ -370,7 +391,7 @@ $instance['required'] = TRUE; field_update_instance($instance); $this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit'); - $this->assertFalse($this->xpath('//select[@id=:id]//option[@value=""]', array(':id' => 'edit-card-2-' . $langcode)), t('A required select list does not have an empty key.')); + $this->assertFalse($this->xpath('//select[@id=:id]//option[@value=""]', array(':id' => 'edit-card-2-' . $langcode)), 'A required select list does not have an empty key.'); // We do not have to test that a required select list with one option is // auto-selected because the browser does it for us. @@ -389,8 +410,8 @@ $this->assertNoOptionSelected("edit-card-2-$langcode", 0); $this->assertNoOptionSelected("edit-card-2-$langcode", 1); $this->assertNoOptionSelected("edit-card-2-$langcode", 2); - $this->assertRaw('Some dangerous & unescaped markup', t('Option text was properly filtered.')); - $this->assertRaw('Group 1', t('Option groups are displayed.')); + $this->assertRaw('Some dangerous & unescaped markup', 'Option text was properly filtered.'); + $this->assertRaw('Group 1', 'Option groups are displayed.'); // Submit form: select first option. $edit = array("card_2[$langcode][]" => array(0 => 0)); @@ -434,7 +455,7 @@ // Display form: with no field data, option is unchecked. $this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit'); $this->assertNoFieldChecked("edit-bool-$langcode"); - $this->assertRaw('Some dangerous & unescaped markup', t('Option text was properly filtered.')); + $this->assertRaw('Some dangerous & unescaped markup', 'Option text was properly filtered.'); // Submit form: check the option. $edit = array("bool[$langcode]" => TRUE); @@ -455,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. @@ -479,13 +500,13 @@ $this->assertText( 'Use field label instead of the "On value" as label ', - t('Display setting checkbox available.') + 'Display setting checkbox available.' ); $this->assertFieldByXPath( '*//label[@for="edit-' . $this->bool['field_name'] . '-und" and text()="MyOnValue "]', TRUE, - t('Default case shows "On value"') + 'Default case shows "On value"' ); // Enable setting @@ -498,17 +519,52 @@ $this->drupalGet($fieldEditUrl); $this->assertText( 'Use field label instead of the "On value" as label ', - t('Display setting checkbox is available') + 'Display setting checkbox is available' ); $this->assertFieldChecked( 'edit-instance-widget-settings-display-label', - t('Display settings checkbox checked') + 'Display settings checkbox checked' ); $this->assertFieldByXPath( '*//label[@for="edit-' . $this->bool['field_name'] . '-und" and text()="' . $this->bool['field_name'] . ' "]', TRUE, - t('Display label changes label of the checkbox') + 'Display label changes label of the checkbox' ); } } +/** + * Test an options select on a list field with a dynamic allowed values function. + */ +class OptionsSelectDynamicValuesTestCase extends ListDynamicValuesTestCase { + public static function getInfo() { + return array( + 'name' => 'Options select dynamic values', + 'description' => 'Test an options select on a list field with a dynamic allowed values function.', + 'group' => 'Field types', + ); + } + + /** + * Tests the 'options_select' widget (single select). + */ + function testSelectListDynamic() { + // Create an entity. + $this->entity->is_new = TRUE; + field_test_entity_save($this->entity); + // Create a web user. + $web_user = $this->drupalCreateUser(array('access field_test content', 'administer field_test content')); + $this->drupalLogin($web_user); + + // Display form. + $this->drupalGet('test-entity/manage/' . $this->entity->ftid . '/edit'); + $options = $this->xpath('//select[@id="edit-test-list-und"]/option'); + $this->assertEqual(count($options), count($this->test) + 1); + foreach ($options as $option) { + $value = (string) $option['value']; + if ($value != '_none') { + $this->assertTrue(array_search($value, $this->test)); + } + } + } +} diff -Naur drupal-7.0/modules/field/modules/text/text.info drupal-7.66/modules/field/modules/text/text.info --- drupal-7.0/modules/field/modules/text/text.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/field/modules/text/text.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: text.info,v 1.9 2010/12/20 19:59:41 webchick Exp $ name = Text description = Defines simple text field types. package = Core @@ -8,8 +7,7 @@ files[] = text.test required = TRUE -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/field/modules/text/text.install drupal-7.66/modules/field/modules/text/text.install --- drupal-7.0/modules/field/modules/text/text.install 2010-10-20 17:57:42.000000000 +0200 +++ drupal-7.66/modules/field/modules/text/text.install 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ 7010, - ); - - return $dependencies; -} - -/** * Change text field 'format' columns into varchar. */ function text_update_7000() { @@ -93,7 +79,7 @@ 'module' => 'text', 'storage_type' => 'field_sql_storage', )); - foreach ($fields as $field_name => $field) { + foreach ($fields as $field) { if ($field['deleted']) { $table = "field_deleted_data_{$field['id']}"; $revision_table = "field_deleted_revision_{$field['id']}"; diff -Naur drupal-7.0/modules/field/modules/text/text.js drupal-7.66/modules/field/modules/text/text.js --- drupal-7.0/modules/field/modules/text/text.js 2010-10-24 06:18:10.000000000 +0200 +++ drupal-7.66/modules/field/modules/text/text.js 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -// $Id: text.js,v 1.5 2010/10/24 04:18:10 webchick Exp $ (function ($) { @@ -13,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. @@ -24,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.0/modules/field/modules/text/text.module drupal-7.66/modules/field/modules/text/text.module --- drupal-7.0/modules/field/modules/text/text.module 2010-11-13 08:39:35.000000000 +0100 +++ drupal-7.66/modules/field/modules/text/text.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ $settings['max_length'], '#required' => TRUE, '#description' => t('The maximum length of the field in characters.'), - '#element_validate' => array('_element_validate_integer_positive'), + '#element_validate' => array('element_validate_integer_positive'), // @todo: If $has_data, add a validate handler that only allows // max_length to increase. '#disabled' => $has_data, @@ -224,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'), + '#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, ); } @@ -246,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; @@ -481,7 +482,7 @@ '#title' => t('Size of textfield'), '#default_value' => $settings['size'], '#required' => TRUE, - '#element_validate' => array('_element_validate_integer_positive'), + '#element_validate' => array('element_validate_integer_positive'), ); } else { @@ -490,7 +491,7 @@ '#title' => t('Rows'), '#default_value' => $settings['rows'], '#required' => TRUE, - '#element_validate' => array('_element_validate_integer_positive'), + '#element_validate' => array('element_validate_integer_positive'), ); } diff -Naur drupal-7.0/modules/field/modules/text/text.test drupal-7.66/modules/field/modules/text/text.test --- drupal-7.0/modules/field/modules/text/text.test 2010-10-25 17:51:21.000000000 +0200 +++ drupal-7.66/modules/field/modules/text/text.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,9 @@ drupalGet('test-entity/add/test-bundle'); - $this->assertFieldByName("{$this->field_name}[$langcode][0][value]", '', t('Widget is displayed')); - $this->assertNoFieldByName("{$this->field_name}[$langcode][0][format]", '1', t('Format selector is not displayed')); + $this->assertFieldByName("{$this->field_name}[$langcode][0][value]", '', 'Widget is displayed'); + $this->assertNoFieldByName("{$this->field_name}[$langcode][0][format]", '1', 'Format selector is not displayed'); // Submit with some value. $value = $this->randomName(); @@ -117,7 +121,7 @@ $this->drupalPost(NULL, $edit, t('Save')); 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)), t('Entity was created')); + $this->assertRaw(t('test_entity @id has been created.', array('@id' => $id)), 'Entity was created'); // Display the entity. $entity = field_test_entity_test_load($id); @@ -175,8 +179,8 @@ // Display the creation form. Since the user only has access to one format, // no format selector will be displayed. $this->drupalGet('test-entity/add/test-bundle'); - $this->assertFieldByName("{$this->field_name}[$langcode][0][value]", '', t('Widget is displayed')); - $this->assertNoFieldByName("{$this->field_name}[$langcode][0][format]", '', t('Format selector is not displayed')); + $this->assertFieldByName("{$this->field_name}[$langcode][0][value]", '', 'Widget is displayed'); + $this->assertNoFieldByName("{$this->field_name}[$langcode][0][format]", '', 'Format selector is not displayed'); // Submit with data that should be filtered. $value = '' . $this->randomName() . ''; @@ -186,14 +190,14 @@ $this->drupalPost(NULL, $edit, t('Save')); 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)), t('Entity was created')); + $this->assertRaw(t('test_entity @id has been created.', array('@id' => $id)), 'Entity was created'); // Display the entity. $entity = field_test_entity_test_load($id); $entity->content = field_attach_view($entity_type, $entity, 'full'); $this->content = drupal_render($entity->content); - $this->assertNoRaw($value, t('HTML tags are not displayed.')); - $this->assertRaw(check_plain($value), t('Escaped HTML is displayed correctly.')); + $this->assertNoRaw($value, 'HTML tags are not displayed.'); + $this->assertRaw(check_plain($value), 'Escaped HTML is displayed correctly.'); // Create a new text format that does not escape HTML, and grant the user // access to it. @@ -215,21 +219,21 @@ // Display edition form. // We should now have a 'text format' selector. $this->drupalGet('test-entity/manage/' . $id . '/edit'); - $this->assertFieldByName("{$this->field_name}[$langcode][0][value]", NULL, t('Widget is displayed')); - $this->assertFieldByName("{$this->field_name}[$langcode][0][format]", NULL, t('Format selector is displayed')); + $this->assertFieldByName("{$this->field_name}[$langcode][0][value]", NULL, 'Widget is displayed'); + $this->assertFieldByName("{$this->field_name}[$langcode][0][format]", NULL, 'Format selector is displayed'); // Edit and change the text format to the new one that was created. $edit = array( "{$this->field_name}[$langcode][0][format]" => $format_id, ); $this->drupalPost(NULL, $edit, t('Save')); - $this->assertRaw(t('test_entity @id has been updated.', array('@id' => $id)), t('Entity was updated')); + $this->assertRaw(t('test_entity @id has been updated.', array('@id' => $id)), 'Entity was updated'); // Display the entity. $entity = field_test_entity_test_load($id); $entity->content = field_attach_view($entity_type, $entity, 'full'); $this->content = drupal_render($entity->content); - $this->assertRaw($value, t('Value is displayed unfiltered')); + $this->assertRaw($value, 'Value is displayed unfiltered'); } } @@ -379,7 +383,7 @@ */ function callTextSummary($text, $expected, $format = NULL, $size = NULL) { $summary = text_summary($text, $format, $size); - $this->assertIdentical($summary, $expected, t('Generated summary "@summary" matches expected "@expected".', array('@summary' => $summary, '@expected' => $expected))); + $this->assertIdentical($summary, $expected, format_string('Generated summary "@summary" matches expected "@expected".', array('@summary' => $summary, '@expected' => $expected))); } /** @@ -397,7 +401,7 @@ $this->drupalPost('node/add/article', $edit, t('Save')); $node = $this->drupalGetNodeByTitle($edit['title']); - $this->assertIdentical($node->body['und'][0]['summary'], $summary, t('Article with with summary and no body has been submitted.')); + $this->assertIdentical($node->body['und'][0]['summary'], $summary, 'Article with with summary and no body has been submitted.'); } } @@ -420,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')); @@ -432,7 +437,7 @@ // Set "Article" content type to use multilingual support with translation. $edit = array('language_content_type' => 2); $this->drupalPost('admin/structure/types/manage/article', $edit, t('Save content type')); - $this->assertRaw(t('The content type %type has been updated.', array('%type' => 'Article')), t('Article content type has been updated.')); + $this->assertRaw(t('The content type %type has been updated.', array('%type' => 'Article')), 'Article content type has been updated.'); } /** @@ -460,7 +465,7 @@ $node = $this->drupalGetNodeByTitle($edit['title']); $this->drupalGet("node/$node->nid/translate"); $this->clickLink(t('add translation')); - $this->assertFieldByXPath("//textarea[@name='body[fr][0][value]']", $body, t('The textfield widget is populated.')); + $this->assertFieldByXPath("//textarea[@name='body[$langcode][0][value]']", $body, 'The textfield widget is populated.'); } /** @@ -472,7 +477,7 @@ $edit = array('field[cardinality]' => -1); $this->drupalPost('admin/structure/types/manage/article/fields/body', $edit, t('Save settings')); $this->drupalGet('node/add/article'); - $this->assertFieldByXPath("//input[@name='body_add_more']", t('Add another item'), t('Body field cardinality set to multiple.')); + $this->assertFieldByXPath("//input[@name='body_add_more']", t('Add another item'), 'Body field cardinality set to multiple.'); $body = array( $this->randomName(), @@ -480,24 +485,24 @@ ); // Create an article with the first body input format set to "Full HTML". - $langcode = 'en'; $title = $this->randomName(); $edit = array( 'title' => $title, - 'language' => $langcode, + 'language' => 'en', ); $this->drupalPost('node/add/article', $edit, t('Save')); // Populate the body field: the first item gets the "Full HTML" input // format, the second one "Filtered HTML". $formats = array('full_html', 'filtered_html'); + $langcode = LANGUAGE_NONE; foreach ($body as $delta => $value) { $edit = array( "body[$langcode][$delta][value]" => $value, "body[$langcode][$delta][format]" => array_shift($formats), ); $this->drupalPost('node/1/edit', $edit, t('Save')); - $this->assertText($body[$delta], t('The body field with delta @delta has been saved.', array('@delta' => $delta))); + $this->assertText($body[$delta], format_string('The body field with delta @delta has been saved.', array('@delta' => $delta))); } // Login as translator. @@ -507,7 +512,7 @@ $node = $this->drupalGetNodeByTitle($title); $this->drupalGet("node/$node->nid/translate"); $this->clickLink(t('add translation')); - $this->assertNoText($body[0], t('The body field with delta @delta is hidden.', array('@delta' => 0))); - $this->assertText($body[1], t('The body field with delta @delta is shown.', array('@delta' => 1))); + $this->assertNoText($body[0], format_string('The body field with delta @delta is hidden.', array('@delta' => 0))); + $this->assertText($body[1], format_string('The body field with delta @delta is shown.', array('@delta' => 1))); } } diff -Naur drupal-7.0/modules/field/tests/field.test drupal-7.66/modules/field/tests/field.test --- drupal-7.0/modules/field/tests/field.test 2010-12-15 05:13:48.000000000 +0100 +++ drupal-7.66/modules/field/tests/field.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,9 +1,8 @@ ftid => $e)); $values = isset($e->{$field_name}[$langcode]) ? $e->{$field_name}[$langcode] : array(); - $this->assertEqual(count($values), count($expected_values), t('Expected number of values were saved.')); + $this->assertEqual(count($values), count($expected_values), 'Expected number of values were saved.'); foreach ($expected_values as $key => $value) { - $this->assertEqual($values[$key][$column], $value, t('Value @value was saved correctly.', array('@value' => $value))); + $this->assertEqual($values[$key][$column], $value, format_string('Value @value was saved correctly.', array('@value' => $value))); } } } class FieldAttachTestCase extends FieldTestCase { - function setUp($modules = array()) { + function setUp() { // Since this is a base class for many test cases, support the same // flexibility that DrupalWebTestCase::setUp() has for the modules to be // passed in as either an array or a variable number of string arguments. - if (!is_array($modules)) { - $modules = func_get_args(); + $modules = func_get_args(); + if (isset($modules[0]) && is_array($modules[0])) { + $modules = $modules[0]; } if (!in_array('field_test', $modules)) { $modules[] = 'field_test'; } parent::setUp($modules); - $this->field_name = drupal_strtolower($this->randomName() . '_field_name'); - $this->field = array('field_name' => $this->field_name, 'type' => 'test_field', 'cardinality' => 4); - $this->field = field_create_field($this->field); - $this->field_id = $this->field['id']; - $this->instance = array( - 'field_name' => $this->field_name, + $this->createFieldWithInstance(); + } + + /** + * Create a field and an instance of it. + * + * @param string $suffix + * (optional) A string that should only contain characters that are valid in + * PHP variable names as well. + */ + function createFieldWithInstance($suffix = '') { + $field_name = 'field_name' . $suffix; + $field = 'field' . $suffix; + $field_id = 'field_id' . $suffix; + $instance = 'instance' . $suffix; + + $this->$field_name = drupal_strtolower($this->randomName() . '_field_name' . $suffix); + $this->$field = array('field_name' => $this->$field_name, 'type' => 'test_field', 'cardinality' => 4); + $this->$field = field_create_field($this->$field); + $this->$field_id = $this->{$field}['id']; + $this->$instance = array( + 'field_name' => $this->$field_name, 'entity_type' => 'test_entity', 'bundle' => 'test_bundle', 'label' => $this->randomName() . '_label', @@ -107,7 +123,7 @@ ) ) ); - field_create_instance($this->instance); + field_create_instance($this->$instance); } } @@ -166,12 +182,12 @@ $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); field_attach_load($entity_type, array(0 => $entity)); // Number of values per field loaded equals the field cardinality. - $this->assertEqual(count($entity->{$this->field_name}[$langcode]), $this->field['cardinality'], t('Current revision: expected number of values')); + $this->assertEqual(count($entity->{$this->field_name}[$langcode]), $this->field['cardinality'], 'Current revision: expected number of values'); for ($delta = 0; $delta < $this->field['cardinality']; $delta++) { // The field value loaded matches the one inserted or updated. - $this->assertEqual($entity->{$this->field_name}[$langcode][$delta]['value'] , $values[$current_revision][$delta]['value'], t('Current revision: expected value %delta was found.', array('%delta' => $delta))); + $this->assertEqual($entity->{$this->field_name}[$langcode][$delta]['value'] , $values[$current_revision][$delta]['value'], format_string('Current revision: expected value %delta was found.', array('%delta' => $delta))); // The value added in hook_field_load() is found. - $this->assertEqual($entity->{$this->field_name}[$langcode][$delta]['additional_key'], 'additional_value', t('Current revision: extra information for value %delta was found', array('%delta' => $delta))); + $this->assertEqual($entity->{$this->field_name}[$langcode][$delta]['additional_key'], 'additional_value', format_string('Current revision: extra information for value %delta was found', array('%delta' => $delta))); } // Confirm each revision loads the correct data. @@ -179,12 +195,12 @@ $entity = field_test_create_stub_entity(0, $revision_id, $this->instance['bundle']); field_attach_load_revision($entity_type, array(0 => $entity)); // Number of values per field loaded equals the field cardinality. - $this->assertEqual(count($entity->{$this->field_name}[$langcode]), $this->field['cardinality'], t('Revision %revision_id: expected number of values.', array('%revision_id' => $revision_id))); + $this->assertEqual(count($entity->{$this->field_name}[$langcode]), $this->field['cardinality'], format_string('Revision %revision_id: expected number of values.', array('%revision_id' => $revision_id))); for ($delta = 0; $delta < $this->field['cardinality']; $delta++) { // The field value loaded matches the one inserted or updated. - $this->assertEqual($entity->{$this->field_name}[$langcode][$delta]['value'], $values[$revision_id][$delta]['value'], t('Revision %revision_id: expected value %delta was found.', array('%revision_id' => $revision_id, '%delta' => $delta))); + $this->assertEqual($entity->{$this->field_name}[$langcode][$delta]['value'], $values[$revision_id][$delta]['value'], format_string('Revision %revision_id: expected value %delta was found.', array('%revision_id' => $revision_id, '%delta' => $delta))); // The value added in hook_field_load() is found. - $this->assertEqual($entity->{$this->field_name}[$langcode][$delta]['additional_key'], 'additional_value', t('Revision %revision_id: extra information for value %delta was found', array('%revision_id' => $revision_id, '%delta' => $delta))); + $this->assertEqual($entity->{$this->field_name}[$langcode][$delta]['additional_key'], 'additional_value', format_string('Revision %revision_id: extra information for value %delta was found', array('%revision_id' => $revision_id, '%delta' => $delta))); } } } @@ -250,19 +266,19 @@ $instances = field_info_instances($entity_type, $bundles[$index]); foreach ($instances as $field_name => $instance) { // The field value loaded matches the one inserted. - $this->assertEqual($entity->{$field_name}[$langcode][0]['value'], $values[$index][$field_name], t('Entity %index: expected value was found.', array('%index' => $index))); + $this->assertEqual($entity->{$field_name}[$langcode][0]['value'], $values[$index][$field_name], format_string('Entity %index: expected value was found.', array('%index' => $index))); // The value added in hook_field_load() is found. - $this->assertEqual($entity->{$field_name}[$langcode][0]['additional_key'], 'additional_value', t('Entity %index: extra information was found', array('%index' => $index))); + $this->assertEqual($entity->{$field_name}[$langcode][0]['additional_key'], 'additional_value', format_string('Entity %index: extra information was found', array('%index' => $index))); } } // Check that the single-field load option works. $entity = field_test_create_stub_entity(1, 1, $bundles[1]); field_attach_load($entity_type, array(1 => $entity), FIELD_LOAD_CURRENT, array('field_id' => $field_ids[1])); - $this->assertEqual($entity->{$field_names[1]}[$langcode][0]['value'], $values[1][$field_names[1]], t('Entity %index: expected value was found.', array('%index' => 1))); - $this->assertEqual($entity->{$field_names[1]}[$langcode][0]['additional_key'], 'additional_value', t('Entity %index: extra information was found', array('%index' => 1))); - $this->assert(!isset($entity->{$field_names[2]}), t('Entity %index: field %field_name is not loaded.', array('%index' => 2, '%field_name' => $field_names[2]))); - $this->assert(!isset($entity->{$field_names[3]}), t('Entity %index: field %field_name is not loaded.', array('%index' => 3, '%field_name' => $field_names[3]))); + $this->assertEqual($entity->{$field_names[1]}[$langcode][0]['value'], $values[1][$field_names[1]], format_string('Entity %index: expected value was found.', array('%index' => 1))); + $this->assertEqual($entity->{$field_names[1]}[$langcode][0]['additional_key'], 'additional_value', format_string('Entity %index: extra information was found', array('%index' => 1))); + $this->assert(!isset($entity->{$field_names[2]}), format_string('Entity %index: field %field_name is not loaded.', array('%index' => 2, '%field_name' => $field_names[2]))); + $this->assert(!isset($entity->{$field_names[3]}), format_string('Entity %index: field %field_name is not loaded.', array('%index' => 3, '%field_name' => $field_names[3]))); } /** @@ -312,7 +328,7 @@ $entity = clone($entity_init); field_attach_load($entity_type, array($entity->ftid => $entity)); foreach ($fields as $field) { - $this->assertEqual($values[$field['field_name']], $entity->{$field['field_name']}[$langcode], t('%storage storage: expected values were found.', array('%storage' => $field['storage']['type']))); + $this->assertEqual($values[$field['field_name']], $entity->{$field['field_name']}[$langcode], format_string('%storage storage: expected values were found.', array('%storage' => $field['storage']['type']))); } } @@ -341,20 +357,20 @@ $instance = field_info_instance($instance['entity_type'], $instance['field_name'], $instance['bundle']); // The storage details are indexed by a storage engine type. - $this->assertTrue(array_key_exists('drupal_variables', $field['storage']['details']), t('The storage type is Drupal variables.')); + $this->assertTrue(array_key_exists('drupal_variables', $field['storage']['details']), 'The storage type is Drupal variables.'); $details = $field['storage']['details']['drupal_variables']; // The field_test storage details are indexed by variable name. The details // are altered, so moon and mars are correct for this test. - $this->assertTrue(array_key_exists('moon', $details[FIELD_LOAD_CURRENT]), t('Moon is available in the instance array.')); - $this->assertTrue(array_key_exists('mars', $details[FIELD_LOAD_REVISION]), t('Mars is available in the instance array.')); + $this->assertTrue(array_key_exists('moon', $details[FIELD_LOAD_CURRENT]), 'Moon is available in the instance array.'); + $this->assertTrue(array_key_exists('mars', $details[FIELD_LOAD_REVISION]), 'Mars is available in the instance array.'); // Test current and revision storage details together because the columns // are the same. foreach ((array) $field['columns'] as $column_name => $attributes) { - $this->assertEqual($details[FIELD_LOAD_CURRENT]['moon'][$column_name], $column_name, t('Column name %value matches the definition in %bin.', array('%value' => $column_name, '%bin' => 'moon[FIELD_LOAD_CURRENT]'))); - $this->assertEqual($details[FIELD_LOAD_REVISION]['mars'][$column_name], $column_name, t('Column name %value matches the definition in %bin.', array('%value' => $column_name, '%bin' => 'mars[FIELD_LOAD_REVISION]'))); + $this->assertEqual($details[FIELD_LOAD_CURRENT]['moon'][$column_name], $column_name, format_string('Column name %value matches the definition in %bin.', array('%value' => $column_name, '%bin' => 'moon[FIELD_LOAD_CURRENT]'))); + $this->assertEqual($details[FIELD_LOAD_REVISION]['mars'][$column_name], $column_name, format_string('Column name %value matches the definition in %bin.', array('%value' => $column_name, '%bin' => 'mars[FIELD_LOAD_REVISION]'))); } } @@ -372,7 +388,7 @@ $entity = clone($entity_init); field_attach_load($entity_type, array($entity->ftid => $entity)); - $this->assertTrue(empty($entity->{$this->field_name}), t('Insert: missing field results in no value saved')); + $this->assertTrue(empty($entity->{$this->field_name}), 'Insert: missing field results in no value saved'); // Insert: Field is NULL. field_cache_clear(); @@ -382,7 +398,7 @@ $entity = clone($entity_init); field_attach_load($entity_type, array($entity->ftid => $entity)); - $this->assertTrue(empty($entity->{$this->field_name}), t('Insert: NULL field results in no value saved')); + $this->assertTrue(empty($entity->{$this->field_name}), 'Insert: NULL field results in no value saved'); // Add some real data. field_cache_clear(); @@ -393,7 +409,7 @@ $entity = clone($entity_init); field_attach_load($entity_type, array($entity->ftid => $entity)); - $this->assertEqual($entity->{$this->field_name}[$langcode], $values, t('Field data saved')); + $this->assertEqual($entity->{$this->field_name}[$langcode], $values, 'Field data saved'); // Update: Field is missing. Data should survive. field_cache_clear(); @@ -402,7 +418,7 @@ $entity = clone($entity_init); field_attach_load($entity_type, array($entity->ftid => $entity)); - $this->assertEqual($entity->{$this->field_name}[$langcode], $values, t('Update: missing field leaves existing values in place')); + $this->assertEqual($entity->{$this->field_name}[$langcode], $values, 'Update: missing field leaves existing values in place'); // Update: Field is NULL. Data should be wiped. field_cache_clear(); @@ -412,7 +428,7 @@ $entity = clone($entity_init); field_attach_load($entity_type, array($entity->ftid => $entity)); - $this->assertTrue(empty($entity->{$this->field_name}), t('Update: NULL field removes existing values')); + $this->assertTrue(empty($entity->{$this->field_name}), 'Update: NULL field removes existing values'); // Re-add some data. field_cache_clear(); @@ -423,7 +439,7 @@ $entity = clone($entity_init); field_attach_load($entity_type, array($entity->ftid => $entity)); - $this->assertEqual($entity->{$this->field_name}[$langcode], $values, t('Field data saved')); + $this->assertEqual($entity->{$this->field_name}[$langcode], $values, 'Field data saved'); // Update: Field is empty array. Data should be wiped. field_cache_clear(); @@ -433,7 +449,7 @@ $entity = clone($entity_init); field_attach_load($entity_type, array($entity->ftid => $entity)); - $this->assertTrue(empty($entity->{$this->field_name}), t('Update: empty array removes existing values')); + $this->assertTrue(empty($entity->{$this->field_name}), 'Update: empty array removes existing values'); } /** @@ -455,7 +471,7 @@ $entity = clone($entity_init); field_attach_load($entity_type, array($entity->ftid => $entity)); - $this->assertTrue(empty($entity->{$this->field_name}[$langcode]), t('Insert: NULL field results in no value saved')); + $this->assertTrue(empty($entity->{$this->field_name}[$langcode]), 'Insert: NULL field results in no value saved'); // Insert: Field is missing. field_cache_clear(); @@ -465,7 +481,67 @@ $entity = clone($entity_init); field_attach_load($entity_type, array($entity->ftid => $entity)); $values = field_test_default_value($entity_type, $entity, $this->field, $this->instance); - $this->assertEqual($entity->{$this->field_name}[$langcode], $values, t('Insert: missing field results in default value saved')); + $this->assertEqual($entity->{$this->field_name}[$langcode], $values, 'Insert: missing field results in default value saved'); + } + + /** + * 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."); } /** @@ -520,7 +596,7 @@ } $read = field_test_create_stub_entity(0, 2, $this->instance['bundle']); field_attach_load($entity_type, array(0 => $read)); - $this->assertIdentical($read->{$this->field_name}, array(), t('The test entity current revision is deleted.')); + $this->assertIdentical($read->{$this->field_name}, array(), 'The test entity current revision is deleted.'); } /** @@ -641,13 +717,18 @@ * Test field_attach_view() and field_attach_prepare_view(). */ function testFieldAttachView() { + $this->createFieldWithInstance('_2'); + $entity_type = 'test_entity'; $entity_init = field_test_create_stub_entity(); $langcode = LANGUAGE_NONE; + $options = array('field_name' => $this->field_name_2); // Populate values to be displayed. $values = $this->_generateTestFieldValues($this->field['cardinality']); $entity_init->{$this->field_name}[$langcode] = $values; + $values_2 = $this->_generateTestFieldValues($this->field_2['cardinality']); + $entity_init->{$this->field_name_2}[$langcode] = $values_2; // Simple formatter, label displayed. $entity = clone($entity_init); @@ -662,15 +743,47 @@ ), ); field_update_instance($this->instance); + $formatter_setting_2 = $this->randomName(); + $this->instance_2['display'] = array( + 'full' => array( + 'label' => 'above', + 'type' => 'field_test_default', + 'settings' => array( + 'test_formatter_setting' => $formatter_setting_2, + ) + ), + ); + field_update_instance($this->instance_2); + // View all fields. field_attach_prepare_view($entity_type, array($entity->ftid => $entity), 'full'); $entity->content = field_attach_view($entity_type, $entity, 'full'); $output = drupal_render($entity->content); $this->content = $output; - $this->assertRaw($this->instance['label'], "Label is displayed."); + $this->assertRaw($this->instance['label'], "First field's label is displayed."); foreach ($values as $delta => $value) { $this->content = $output; $this->assertRaw("$formatter_setting|{$value['value']}", "Value $delta is displayed, formatter settings are applied."); } + $this->assertRaw($this->instance_2['label'], "Second field's label is displayed."); + foreach ($values_2 as $delta => $value) { + $this->content = $output; + $this->assertRaw("$formatter_setting_2|{$value['value']}", "Value $delta is displayed, formatter settings are applied."); + } + // View single field (the second field). + field_attach_prepare_view($entity_type, array($entity->ftid => $entity), 'full', $langcode, $options); + $entity->content = field_attach_view($entity_type, $entity, 'full', $langcode, $options); + $output = drupal_render($entity->content); + $this->content = $output; + $this->assertNoRaw($this->instance['label'], "First field's label is not displayed."); + foreach ($values as $delta => $value) { + $this->content = $output; + $this->assertNoRaw("$formatter_setting|{$value['value']}", "Value $delta is displayed, formatter settings are applied."); + } + $this->assertRaw($this->instance_2['label'], "Second field's label is displayed."); + foreach ($values_2 as $delta => $value) { + $this->content = $output; + $this->assertRaw("$formatter_setting_2|{$value['value']}", "Value $delta is displayed, formatter settings are applied."); + } // Label hidden. $entity = clone($entity_init); @@ -697,7 +810,7 @@ $this->content = $output; $this->assertNoRaw($this->instance['label'], "Hidden field: label is not displayed."); foreach ($values as $delta => $value) { - $this->assertNoRaw($value['value'], "Hidden field: value $delta is not displayed."); + $this->assertNoRaw("$formatter_setting|{$value['value']}", "Hidden field: value $delta is not displayed."); } // Multiple formatter. @@ -759,7 +872,56 @@ break; } } - $this->assertTrue($result, t('Variable $@field_name correctly populated.', array('@field_name' => $this->field_name))); + $this->assertTrue($result, format_string('Variable $@field_name correctly populated.', array('@field_name' => $this->field_name))); + } + + /** + * Tests the 'multiple entity' behavior of field_attach_prepare_view(). + */ + function testFieldAttachPrepareViewMultiple() { + $entity_type = 'test_entity'; + $langcode = LANGUAGE_NONE; + + // Set the instance to be hidden. + $this->instance['display']['full']['type'] = 'hidden'; + field_update_instance($this->instance); + + // Set up a second instance on another bundle, with a formatter that uses + // hook_field_formatter_prepare_view(). + field_test_create_bundle('test_bundle_2'); + $formatter_setting = $this->randomName(); + $this->instance2 = $this->instance; + $this->instance2['bundle'] = 'test_bundle_2'; + $this->instance2['display']['full'] = array( + 'type' => 'field_test_with_prepare_view', + 'settings' => array( + 'test_formatter_setting_additional' => $formatter_setting, + ) + ); + field_create_instance($this->instance2); + + // Create one entity in each bundle. + $entity1_init = field_test_create_stub_entity(1, 1, 'test_bundle'); + $values1 = $this->_generateTestFieldValues($this->field['cardinality']); + $entity1_init->{$this->field_name}[$langcode] = $values1; + + $entity2_init = field_test_create_stub_entity(2, 2, 'test_bundle_2'); + $values2 = $this->_generateTestFieldValues($this->field['cardinality']); + $entity2_init->{$this->field_name}[$langcode] = $values2; + + // Run prepare_view, and check that the entities come out as expected. + $entity1 = clone($entity1_init); + $entity2 = clone($entity2_init); + field_attach_prepare_view($entity_type, array($entity1->ftid => $entity1, $entity2->ftid => $entity2), 'full'); + $this->assertFalse(isset($entity1->{$this->field_name}[$langcode][0]['additional_formatter_value']), 'Entity 1 did not run through the prepare_view hook.'); + $this->assertTrue(isset($entity2->{$this->field_name}[$langcode][0]['additional_formatter_value']), 'Entity 2 ran through the prepare_view hook.'); + + // Same thing, reversed order. + $entity1 = clone($entity1_init); + $entity2 = clone($entity2_init); + field_attach_prepare_view($entity_type, array($entity2->ftid => $entity2, $entity1->ftid => $entity1), 'full'); + $this->assertFalse(isset($entity1->{$this->field_name}[$langcode][0]['additional_formatter_value']), 'Entity 1 did not run through the prepare_view hook.'); + $this->assertTrue(isset($entity2->{$this->field_name}[$langcode][0]['additional_formatter_value']), 'Entity 2 ran through the prepare_view hook.'); } /** @@ -776,18 +938,18 @@ $cid = "field:$entity_type:{$entity_init->ftid}"; // Check that no initial cache entry is present. - $this->assertFalse(cache_get($cid, 'cache_field'), t('Non-cached: no initial cache entry')); + $this->assertFalse(cache_get($cid, 'cache_field'), 'Non-cached: no initial cache entry'); // Save, and check that no cache entry is present. $entity = clone($entity_init); $entity->{$this->field_name}[$langcode] = $values; field_attach_insert($entity_type, $entity); - $this->assertFalse(cache_get($cid, 'cache_field'), t('Non-cached: no cache entry on insert')); + $this->assertFalse(cache_get($cid, 'cache_field'), 'Non-cached: no cache entry on insert'); // Load, and check that no cache entry is present. $entity = clone($entity_init); field_attach_load($entity_type, array($entity->ftid => $entity)); - $this->assertFalse(cache_get($cid, 'cache_field'), t('Non-cached: no cache entry on load')); + $this->assertFalse(cache_get($cid, 'cache_field'), 'Non-cached: no cache entry on load'); // Cacheable entity type. @@ -798,38 +960,38 @@ field_create_instance($instance); // Check that no initial cache entry is present. - $this->assertFalse(cache_get($cid, 'cache_field'), t('Cached: no initial cache entry')); + $this->assertFalse(cache_get($cid, 'cache_field'), 'Cached: no initial cache entry'); // Save, and check that no cache entry is present. $entity = clone($entity_init); $entity->{$this->field_name}[$langcode] = $values; field_attach_insert($entity_type, $entity); - $this->assertFalse(cache_get($cid, 'cache_field'), t('Cached: no cache entry on insert')); + $this->assertFalse(cache_get($cid, 'cache_field'), 'Cached: no cache entry on insert'); // Load a single field, and check that no cache entry is present. $entity = clone($entity_init); field_attach_load($entity_type, array($entity->ftid => $entity), FIELD_LOAD_CURRENT, array('field_id' => $this->field_id)); $cache = cache_get($cid, 'cache_field'); - $this->assertFalse(cache_get($cid, 'cache_field'), t('Cached: no cache entry on loading a single field')); + $this->assertFalse(cache_get($cid, 'cache_field'), 'Cached: no cache entry on loading a single field'); // Load, and check that a cache entry is present with the expected values. $entity = clone($entity_init); field_attach_load($entity_type, array($entity->ftid => $entity)); $cache = cache_get($cid, 'cache_field'); - $this->assertEqual($cache->data[$this->field_name][$langcode], $values, t('Cached: correct cache entry on load')); + $this->assertEqual($cache->data[$this->field_name][$langcode], $values, 'Cached: correct cache entry on load'); // Update with different values, and check that the cache entry is wiped. $values = $this->_generateTestFieldValues($this->field['cardinality']); $entity = clone($entity_init); $entity->{$this->field_name}[$langcode] = $values; field_attach_update($entity_type, $entity); - $this->assertFalse(cache_get($cid, 'cache_field'), t('Cached: no cache entry on update')); + $this->assertFalse(cache_get($cid, 'cache_field'), 'Cached: no cache entry on update'); // Load, and check that a cache entry is present with the expected values. $entity = clone($entity_init); field_attach_load($entity_type, array($entity->ftid => $entity)); $cache = cache_get($cid, 'cache_field'); - $this->assertEqual($cache->data[$this->field_name][$langcode], $values, t('Cached: correct cache entry on load')); + $this->assertEqual($cache->data[$this->field_name][$langcode], $values, 'Cached: correct cache entry on load'); // Create a new revision, and check that the cache entry is wiped. $entity_init = field_test_create_stub_entity(1, 2, $this->instance['bundle']); @@ -838,17 +1000,17 @@ $entity->{$this->field_name}[$langcode] = $values; field_attach_update($entity_type, $entity); $cache = cache_get($cid, 'cache_field'); - $this->assertFalse(cache_get($cid, 'cache_field'), t('Cached: no cache entry on new revision creation')); + $this->assertFalse(cache_get($cid, 'cache_field'), 'Cached: no cache entry on new revision creation'); // Load, and check that a cache entry is present with the expected values. $entity = clone($entity_init); field_attach_load($entity_type, array($entity->ftid => $entity)); $cache = cache_get($cid, 'cache_field'); - $this->assertEqual($cache->data[$this->field_name][$langcode], $values, t('Cached: correct cache entry on load')); + $this->assertEqual($cache->data[$this->field_name][$langcode], $values, 'Cached: correct cache entry on load'); // Delete, and check that the cache entry is wiped. field_attach_delete($entity_type, $entity); - $this->assertFalse(cache_get($cid, 'cache_field'), t('Cached: no cache entry after delete')); + $this->assertFalse(cache_get($cid, 'cache_field'), 'Cached: no cache entry after delete'); } /** @@ -858,11 +1020,13 @@ * hook_field_validate. */ function testFieldAttachValidate() { + $this->createFieldWithInstance('_2'); + $entity_type = 'test_entity'; $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); $langcode = LANGUAGE_NONE; - // Set up values to generate errors + // Set up all but one values of the first field to generate errors. $values = array(); for ($delta = 0; $delta < $this->field['cardinality']; $delta++) { $values[$delta]['value'] = -1; @@ -871,6 +1035,14 @@ $values[1]['value'] = 1; $entity->{$this->field_name}[$langcode] = $values; + // Set up all values of the second field to generate errors. + $values_2 = array(); + for ($delta = 0; $delta < $this->field_2['cardinality']; $delta++) { + $values_2[$delta]['value'] = -1; + } + $entity->{$this->field_name_2}[$langcode] = $values_2; + + // Validate all fields. try { field_attach_validate($entity_type, $entity); } @@ -880,26 +1052,57 @@ foreach ($values as $delta => $value) { if ($value['value'] != 1) { - $this->assertIdentical($errors[$this->field_name][$langcode][$delta][0]['error'], 'field_test_invalid', "Error set on value $delta"); - $this->assertEqual(count($errors[$this->field_name][$langcode][$delta]), 1, "Only one error set on value $delta"); + $this->assertIdentical($errors[$this->field_name][$langcode][$delta][0]['error'], 'field_test_invalid', "Error set on first field's value $delta"); + $this->assertEqual(count($errors[$this->field_name][$langcode][$delta]), 1, "Only one error set on first field's value $delta"); unset($errors[$this->field_name][$langcode][$delta]); } else { - $this->assertFalse(isset($errors[$this->field_name][$langcode][$delta]), "No error set on value $delta"); + $this->assertFalse(isset($errors[$this->field_name][$langcode][$delta]), "No error set on first field's value $delta"); } } - $this->assertEqual(count($errors[$this->field_name][$langcode]), 0, 'No extraneous errors set'); + foreach ($values_2 as $delta => $value) { + $this->assertIdentical($errors[$this->field_name_2][$langcode][$delta][0]['error'], 'field_test_invalid', "Error set on second field's value $delta"); + $this->assertEqual(count($errors[$this->field_name_2][$langcode][$delta]), 1, "Only one error set on second field's value $delta"); + unset($errors[$this->field_name_2][$langcode][$delta]); + } + $this->assertEqual(count($errors[$this->field_name][$langcode]), 0, 'No extraneous errors set for first field'); + $this->assertEqual(count($errors[$this->field_name_2][$langcode]), 0, 'No extraneous errors set for second field'); + + // Validate a single field. + $options = array('field_name' => $this->field_name_2); + try { + field_attach_validate($entity_type, $entity, $options); + } + catch (FieldValidationException $e) { + $errors = $e->errors; + } + + foreach ($values_2 as $delta => $value) { + $this->assertIdentical($errors[$this->field_name_2][$langcode][$delta][0]['error'], 'field_test_invalid', "Error set on second field's value $delta"); + $this->assertEqual(count($errors[$this->field_name_2][$langcode][$delta]), 1, "Only one error set on second field's value $delta"); + unset($errors[$this->field_name_2][$langcode][$delta]); + } + $this->assertFalse(isset($errors[$this->field_name]), 'No validation errors are set for the first field, despite it having errors'); + $this->assertEqual(count($errors[$this->field_name_2][$langcode]), 0, 'No extraneous errors set for second field'); // Check that cardinality is validated. - $entity->{$this->field_name}[$langcode] = $this->_generateTestFieldValues($this->field['cardinality'] + 1); + $entity->{$this->field_name_2}[$langcode] = $this->_generateTestFieldValues($this->field_2['cardinality'] + 1); + // When validating all fields. try { field_attach_validate($entity_type, $entity); } catch (FieldValidationException $e) { $errors = $e->errors; } - $this->assertEqual($errors[$this->field_name][$langcode][0][0]['error'], 'field_cardinality', t('Cardinality validation failed.')); - + $this->assertEqual($errors[$this->field_name_2][$langcode][0][0]['error'], 'field_cardinality', 'Cardinality validation failed.'); + // When validating a single field (the second field). + try { + field_attach_validate($entity_type, $entity, $options); + } + catch (FieldValidationException $e) { + $errors = $e->errors; + } + $this->assertEqual($errors[$this->field_name_2][$langcode][0][0]['error'], 'field_cardinality', 'Cardinality validation failed.'); } /** @@ -909,34 +1112,59 @@ * widgets show up. */ function testFieldAttachForm() { + $this->createFieldWithInstance('_2'); + $entity_type = 'test_entity'; $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); + $langcode = LANGUAGE_NONE; + // When generating form for all fields. $form = array(); $form_state = form_state_defaults(); field_attach_form($entity_type, $entity, $form, $form_state); - $langcode = LANGUAGE_NONE; - $this->assertEqual($form[$this->field_name][$langcode]['#title'], $this->instance['label'], "Form title is {$this->instance['label']}"); + $this->assertEqual($form[$this->field_name][$langcode]['#title'], $this->instance['label'], "First field's form title is {$this->instance['label']}"); + $this->assertEqual($form[$this->field_name_2][$langcode]['#title'], $this->instance_2['label'], "Second field's form title is {$this->instance_2['label']}"); for ($delta = 0; $delta < $this->field['cardinality']; $delta++) { // field_test_widget uses 'textfield' - $this->assertEqual($form[$this->field_name][$langcode][$delta]['value']['#type'], 'textfield', "Form delta $delta widget is textfield"); - } + $this->assertEqual($form[$this->field_name][$langcode][$delta]['value']['#type'], 'textfield', "First field's form delta $delta widget is textfield"); + } + for ($delta = 0; $delta < $this->field_2['cardinality']; $delta++) { + // field_test_widget uses 'textfield' + $this->assertEqual($form[$this->field_name_2][$langcode][$delta]['value']['#type'], 'textfield', "Second field's form delta $delta widget is textfield"); + } + + // When generating form for a single field (the second field). + $options = array('field_name' => $this->field_name_2); + $form = array(); + $form_state = form_state_defaults(); + field_attach_form($entity_type, $entity, $form, $form_state, NULL, $options); + + $this->assertFalse(isset($form[$this->field_name]), 'The first field does not exist in the form'); + $this->assertEqual($form[$this->field_name_2][$langcode]['#title'], $this->instance_2['label'], "Second field's form title is {$this->instance_2['label']}"); + for ($delta = 0; $delta < $this->field_2['cardinality']; $delta++) { + // field_test_widget uses 'textfield' + $this->assertEqual($form[$this->field_name_2][$langcode][$delta]['value']['#type'], 'textfield', "Second field's form delta $delta widget is textfield"); + } } /** * Test field_attach_submit(). */ function testFieldAttachSubmit() { + $this->createFieldWithInstance('_2'); + $entity_type = 'test_entity'; - $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); + $entity_init = field_test_create_stub_entity(0, 0, $this->instance['bundle']); + $langcode = LANGUAGE_NONE; - // Build the form. + // Build the form for all fields. $form = array(); $form_state = form_state_defaults(); - field_attach_form($entity_type, $entity, $form, $form_state); + field_attach_form($entity_type, $entity_init, $form, $form_state); // Simulate incoming values. + // First field. $values = array(); $weights = array(); for ($delta = 0; $delta < $this->field['cardinality']; $delta++) { @@ -950,22 +1178,59 @@ } // Leave an empty value. 'field_test' fields are empty if empty(). $values[1]['value'] = 0; - - $langcode = LANGUAGE_NONE; + // Second field. + $values_2 = array(); + $weights_2 = array(); + for ($delta = 0; $delta < $this->field_2['cardinality']; $delta++) { + $values_2[$delta]['value'] = mt_rand(1, 127); + // Assign random weight. + do { + $weight = mt_rand(0, $this->field_2['cardinality']); + } while (in_array($weight, $weights_2)); + $weights_2[$delta] = $weight; + $values_2[$delta]['_weight'] = $weight; + } + // Leave an empty value. 'field_test' fields are empty if empty(). + $values_2[1]['value'] = 0; // Pretend the form has been built. drupal_prepare_form('field_test_entity_form', $form, $form_state); drupal_process_form('field_test_entity_form', $form, $form_state); $form_state['values'][$this->field_name][$langcode] = $values; + $form_state['values'][$this->field_name_2][$langcode] = $values_2; + + // Call field_attach_submit() for all fields. + $entity = clone($entity_init); field_attach_submit($entity_type, $entity, $form, $form_state); asort($weights); + asort($weights_2); $expected_values = array(); + $expected_values_2 = array(); foreach ($weights as $key => $value) { if ($key != 1) { $expected_values[] = array('value' => $values[$key]['value']); } } $this->assertIdentical($entity->{$this->field_name}[$langcode], $expected_values, 'Submit filters empty values'); + foreach ($weights_2 as $key => $value) { + if ($key != 1) { + $expected_values_2[] = array('value' => $values_2[$key]['value']); + } + } + $this->assertIdentical($entity->{$this->field_name_2}[$langcode], $expected_values_2, 'Submit filters empty values'); + + // Call field_attach_submit() for a single field (the second field). + $options = array('field_name' => $this->field_name_2); + $entity = clone($entity_init); + field_attach_submit($entity_type, $entity, $form, $form_state, $options); + $expected_values_2 = array(); + foreach ($weights_2 as $key => $value) { + if ($key != 1) { + $expected_values_2[] = array('value' => $values_2[$key]['value']); + } + } + $this->assertFalse(isset($entity->{$this->field_name}), 'The first field does not exist in the entity object'); + $this->assertIdentical($entity->{$this->field_name_2}[$langcode], $expected_values_2, 'Submit filters empty values'); } } @@ -997,60 +1262,63 @@ $info = field_info_field_types(); foreach ($field_test_info as $t_key => $field_type) { foreach ($field_type as $key => $val) { - $this->assertEqual($info[$t_key][$key], $val, t("Field type $t_key key $key is $val")); + $this->assertEqual($info[$t_key][$key], $val, format_string('Field type %t_key key %key is %value', array('%t_key' => $t_key, '%key' => $key, '%value' => print_r($val, TRUE)))); } - $this->assertEqual($info[$t_key]['module'], 'field_test', t("Field type field_test module appears")); + $this->assertEqual($info[$t_key]['module'], 'field_test', "Field type field_test module appears"); } $formatter_info = field_test_field_formatter_info(); $info = field_info_formatter_types(); foreach ($formatter_info as $f_key => $formatter) { foreach ($formatter as $key => $val) { - $this->assertEqual($info[$f_key][$key], $val, t("Formatter type $f_key key $key is $val")); + $this->assertEqual($info[$f_key][$key], $val, format_string('Formatter type %f_key key %key is %value', array('%f_key' => $f_key, '%key' => $key, '%value' => print_r($val, TRUE)))); } - $this->assertEqual($info[$f_key]['module'], 'field_test', t("Formatter type field_test module appears")); + $this->assertEqual($info[$f_key]['module'], 'field_test', "Formatter type field_test module appears"); } $widget_info = field_test_field_widget_info(); $info = field_info_widget_types(); foreach ($widget_info as $w_key => $widget) { foreach ($widget as $key => $val) { - $this->assertEqual($info[$w_key][$key], $val, t("Widget type $w_key key $key is $val")); + $this->assertEqual($info[$w_key][$key], $val, format_string('Widget type %w_key key %key is %value', array('%w_key' => $w_key, '%key' => $key, '%value' => print_r($val, TRUE)))); } - $this->assertEqual($info[$w_key]['module'], 'field_test', t("Widget type field_test module appears")); + $this->assertEqual($info[$w_key]['module'], 'field_test', "Widget type field_test module appears"); } $storage_info = field_test_field_storage_info(); $info = field_info_storage_types(); foreach ($storage_info as $s_key => $storage) { foreach ($storage as $key => $val) { - $this->assertEqual($info[$s_key][$key], $val, t("Storage type $s_key key $key is $val")); + $this->assertEqual($info[$s_key][$key], $val, format_string('Storage type %s_key key %key is %value', array('%s_key' => $s_key, '%key' => $key, '%value' => print_r($val, TRUE)))); } - $this->assertEqual($info[$s_key]['module'], 'field_test', t("Storage type field_test module appears")); + $this->assertEqual($info[$s_key]['module'], 'field_test', "Storage type field_test module appears"); } // Verify that no unexpected instances exist. - $core_fields = field_info_fields(); + $instances = field_info_instances('test_entity'); + $expected = array('test_bundle' => array()); + $this->assertIdentical($instances, $expected, format_string("field_info_instances('test_entity') returns %expected.", array('%expected' => var_export($expected, TRUE)))); $instances = field_info_instances('test_entity', 'test_bundle'); - $this->assertTrue(empty($instances), t('With no instances, info bundles is empty.')); + $this->assertIdentical($instances, array(), "field_info_instances('test_entity', 'test_bundle') returns an empty array."); // Create a field, verify it shows up. + $core_fields = field_info_fields(); $field = array( 'field_name' => drupal_strtolower($this->randomName()), 'type' => 'test_field', ); field_create_field($field); $fields = field_info_fields(); - $this->assertEqual(count($fields), count($core_fields) + 1, t('One new field exists')); - $this->assertEqual($fields[$field['field_name']]['field_name'], $field['field_name'], t('info fields contains field name')); - $this->assertEqual($fields[$field['field_name']]['type'], $field['type'], t('info fields contains field type')); - $this->assertEqual($fields[$field['field_name']]['module'], 'field_test', t('info fields contains field module')); + $this->assertEqual(count($fields), count($core_fields) + 1, 'One new field exists'); + $this->assertEqual($fields[$field['field_name']]['field_name'], $field['field_name'], 'info fields contains field name'); + $this->assertEqual($fields[$field['field_name']]['type'], $field['type'], 'info fields contains field type'); + $this->assertEqual($fields[$field['field_name']]['module'], 'field_test', 'info fields contains field module'); $settings = array('test_field_setting' => 'dummy test string'); foreach ($settings as $key => $val) { - $this->assertEqual($fields[$field['field_name']]['settings'][$key], $val, t("Field setting $key has correct default value $val")); + $this->assertEqual($fields[$field['field_name']]['settings'][$key], $val, format_string('Field setting %key has correct default value %value', array('%key' => $key, '%value' => $val))); } - $this->assertEqual($fields[$field['field_name']]['cardinality'], 1, t('info fields contains cardinality 1')); - $this->assertEqual($fields[$field['field_name']]['active'], 1, t('info fields contains active 1')); + $this->assertEqual($fields[$field['field_name']]['cardinality'], 1, 'info fields contains cardinality 1'); + $this->assertEqual($fields[$field['field_name']]['active'], 1, 'info fields contains active 1'); // Create an instance, verify that it shows up $instance = array( @@ -1067,9 +1335,41 @@ 'test_setting' => 999))); field_create_instance($instance); + $info = entity_get_info('test_entity'); $instances = field_info_instances('test_entity', $instance['bundle']); - $this->assertEqual(count($instances), 1, t('One instance shows up in info when attached to a bundle.')); - $this->assertTrue($instance < $instances[$instance['field_name']], t('Instance appears in info correctly')); + $this->assertEqual(count($instances), 1, format_string('One instance shows up in info when attached to a bundle on a @label.', array( + '@label' => $info['label'] + ))); + $this->assertTrue($instance < $instances[$instance['field_name']], 'Instance appears in info correctly'); + + // Test a valid entity type but an invalid bundle. + $instances = field_info_instances('test_entity', 'invalid_bundle'); + $this->assertIdentical($instances, array(), "field_info_instances('test_entity', 'invalid_bundle') returns an empty array."); + + // Test invalid entity type and bundle. + $instances = field_info_instances('invalid_entity', $instance['bundle']); + $this->assertIdentical($instances, array(), "field_info_instances('invalid_entity', 'test_bundle') returns an empty array."); + + // Test invalid entity type, no bundle provided. + $instances = field_info_instances('invalid_entity'); + $this->assertIdentical($instances, array(), "field_info_instances('invalid_entity') returns an empty array."); + + // Test with an entity type that has no bundles. + $instances = field_info_instances('user'); + $expected = array('user' => array()); + $this->assertIdentical($instances, $expected, format_string("field_info_instances('user') returns %expected.", array('%expected' => var_export($expected, TRUE)))); + $instances = field_info_instances('user', 'user'); + $this->assertIdentical($instances, array(), "field_info_instances('user', 'user') returns an empty array."); + + // Test that querying for invalid entity types does not add entries in the + // list returned by field_info_instances(). + field_info_cache_clear(); + field_info_instances('invalid_entity', 'invalid_bundle'); + // Simulate new request by clearing static caches. + drupal_static_reset(); + field_info_instances('invalid_entity', 'invalid_bundle'); + $instances = field_info_instances(); + $this->assertFalse(isset($instances['invalid_entity']), 'field_info_instances() does not contain entries for the invalid entity type that was queried before'); } /** @@ -1100,7 +1400,7 @@ // Check that all expected settings are in place. $field_type = field_info_field_types($field_definition['type']); - $this->assertIdentical($field['settings'], $field_type['settings'], t('All expected default field settings are present.')); + $this->assertIdentical($field['settings'], $field_type['settings'], 'All expected default field settings are present.'); } /** @@ -1142,18 +1442,18 @@ // Check that all expected instance settings are in place. $field_type = field_info_field_types($field_definition['type']); - $this->assertIdentical($instance['settings'], $field_type['instance_settings'] , t('All expected instance settings are present.')); + $this->assertIdentical($instance['settings'], $field_type['instance_settings'] , 'All expected instance settings are present.'); // Check that the default widget is used and expected settings are in place. - $this->assertIdentical($instance['widget']['type'], $field_type['default_widget'], t('Unavailable widget replaced with default widget.')); + $this->assertIdentical($instance['widget']['type'], $field_type['default_widget'], 'Unavailable widget replaced with default widget.'); $widget_type = field_info_widget_types($instance['widget']['type']); - $this->assertIdentical($instance['widget']['settings'], $widget_type['settings'] , t('All expected widget settings are present.')); + $this->assertIdentical($instance['widget']['settings'], $widget_type['settings'] , 'All expected widget settings are present.'); // Check that display settings are set for the 'default' mode. $display = $instance['display']['default']; - $this->assertIdentical($display['type'], $field_type['default_formatter'], t("Formatter is set for the 'default' view mode")); + $this->assertIdentical($display['type'], $field_type['default_formatter'], "Formatter is set for the 'default' view mode"); $formatter_type = field_info_formatter_types($display['type']); - $this->assertIdentical($display['settings'], $formatter_type['settings'] , t("Formatter settings are set for the 'default' view mode")); + $this->assertIdentical($display['settings'], $formatter_type['settings'] , "Formatter settings are set for the 'default' view mode"); } /** @@ -1176,7 +1476,81 @@ // Disable coment module. This clears field_info cache. module_disable(array('comment')); - $this->assertNull(field_info_instance('comment', 'field', 'comment_node_article'), t('No instances are returned on disabled entity types.')); + $this->assertNull(field_info_instance('comment', 'field', 'comment_node_article'), 'No instances are returned on disabled entity types.'); + } + + /** + * Test field_info_field_map(). + */ + function testFieldMap() { + // We will overlook fields created by the 'standard' install profile. + $exclude = field_info_field_map(); + + // Create a new bundle for 'test_entity' entity type. + field_test_create_bundle('test_bundle_2'); + + // Create a couple fields. + $fields = array( + array( + 'field_name' => 'field_1', + 'type' => 'test_field', + ), + array( + 'field_name' => 'field_2', + 'type' => 'hidden_test_field', + ), + ); + foreach ($fields as $field) { + field_create_field($field); + } + + // Create a couple instances. + $instances = array( + array( + 'field_name' => 'field_1', + 'entity_type' => 'test_entity', + 'bundle' => 'test_bundle', + ), + array( + 'field_name' => 'field_1', + 'entity_type' => 'test_entity', + 'bundle' => 'test_bundle_2', + ), + array( + 'field_name' => 'field_2', + 'entity_type' => 'test_entity', + 'bundle' => 'test_bundle', + ), + array( + 'field_name' => 'field_2', + 'entity_type' => 'test_cacheable_entity', + 'bundle' => 'test_bundle', + ), + ); + foreach ($instances as $instance) { + field_create_instance($instance); + } + + $expected = array( + 'field_1' => array( + 'type' => 'test_field', + 'bundles' => array( + 'test_entity' => array('test_bundle', 'test_bundle_2'), + ), + ), + 'field_2' => array( + 'type' => 'hidden_test_field', + 'bundles' => array( + 'test_entity' => array('test_bundle'), + 'test_cacheable_entity' => array('test_bundle'), + ), + ), + ); + + // Check that the field map is correct. + $map = field_info_field_map(); + $map = array_diff_key($map, $exclude); + $this->assertEqual($map, $expected); } /** @@ -1189,20 +1563,45 @@ $info[$name]['instance_settings']['user_register_form'] = FALSE; } foreach ($info as $type => $data) { - $this->assertIdentical(field_info_field_settings($type), $data['settings'], "field_info_field_settings returns {$type}'s field settings"); - $this->assertIdentical(field_info_instance_settings($type), $data['instance_settings'], "field_info_field_settings returns {$type}'s field instance settings"); + $this->assertIdentical(field_info_field_settings($type), $data['settings'], format_string("field_info_field_settings returns %type's field settings", array('%type' => $type))); + $this->assertIdentical(field_info_instance_settings($type), $data['instance_settings'], format_string("field_info_field_settings returns %type's field instance settings", array('%type' => $type))); } $info = field_test_field_widget_info(); foreach ($info as $type => $data) { - $this->assertIdentical(field_info_widget_settings($type), $data['settings'], "field_info_widget_settings returns {$type}'s widget settings"); + $this->assertIdentical(field_info_widget_settings($type), $data['settings'], format_string("field_info_widget_settings returns %type's widget settings", array('%type' => $type))); } $info = field_test_field_formatter_info(); foreach ($info as $type => $data) { - $this->assertIdentical(field_info_formatter_settings($type), $data['settings'], "field_info_formatter_settings returns {$type}'s formatter settings"); + $this->assertIdentical(field_info_formatter_settings($type), $data['settings'], format_string("field_info_formatter_settings returns %type's formatter settings", array('%type' => $type))); } } + + /** + * Tests that the field info cache can be built correctly. + */ + function testFieldInfoCache() { + // Create a test field and ensure it's in the array returned by + // field_info_fields(). + $field_name = drupal_strtolower($this->randomName()); + $field = array( + 'field_name' => $field_name, + 'type' => 'test_field', + ); + field_create_field($field); + $fields = field_info_fields(); + $this->assertTrue(isset($fields[$field_name]), 'The test field is initially found in the array returned by field_info_fields().'); + + // Now rebuild the field info cache, and set a variable which will cause + // the cache to be cleared while it's being rebuilt; see + // field_test_entity_info(). Ensure the test field is still in the returned + // array. + field_info_cache_clear(); + variable_set('field_test_clear_info_cache_in_hook_entity_info', TRUE); + $fields = field_info_fields(); + $this->assertTrue(isset($fields[$field_name]), 'The test field is found in the array returned by field_info_fields() even if its cache is cleared while being rebuilt.'); + } } class FieldFormTestCase extends FieldTestCase { @@ -1411,6 +1810,51 @@ // Test with several multiple fields in a form } + /** + * Tests widget handling of multiple required radios. + */ + function testFieldFormMultivalueWithRequiredRadio() { + // Create a multivalue test field. + $this->field = $this->field_unlimited; + $this->field_name = $this->field['field_name']; + $this->instance['field_name'] = $this->field_name; + field_create_field($this->field); + field_create_instance($this->instance); + $langcode = LANGUAGE_NONE; + + // Add a required radio field. + field_create_field(array( + 'field_name' => 'required_radio_test', + 'type' => 'list_text', + 'settings' => array( + 'allowed_values' => array('yes' => 'yes', 'no' => 'no'), + ), + )); + field_create_instance(array( + 'field_name' => 'required_radio_test', + 'entity_type' => 'test_entity', + 'bundle' => 'test_bundle', + 'required' => TRUE, + 'widget' => array( + 'type' => 'options_buttons', + ), + )); + + // Display creation form. + $this->drupalGet('test-entity/add/test-bundle'); + + // Press the 'Add more' button. + $this->drupalPost(NULL, array(), t('Add another item')); + + // Verify that no error is thrown by the radio element. + $this->assertNoFieldByXpath('//div[contains(@class, "error")]', FALSE, 'No error message is displayed.'); + + // Verify that the widget is added. + $this->assertFieldByName("{$this->field_name}[$langcode][0][value]", '', 'Widget 1 is displayed'); + $this->assertFieldByName("{$this->field_name}[$langcode][1][value]", '', 'New widget is displayed'); + $this->assertNoField("{$this->field_name}[$langcode][2][value]", 'No extraneous widget is displayed'); + } + function testFieldFormJSAddMore() { $this->field = $this->field_unlimited; $this->field_name = $this->field['field_name']; @@ -1448,7 +1892,7 @@ $field_values[$weight]['value'] = (string) $value; $pattern[$weight] = "]*value=\"$value\" [^>]*"; } - // Press 'add more' button through AJAX, and place the expected HTML result + // Press 'add more' button through Ajax, and place the expected HTML result // as the tested content. $commands = $this->drupalPostAJAX(NULL, $edit, $this->field_name . '_add_more'); $this->content = $commands[1]['data']; @@ -1481,7 +1925,7 @@ // Display creation form. $this->drupalGet('test-entity/add/test-bundle'); - $this->assertFieldByName("{$this->field_name}[$langcode]", '', t('Widget is displayed.')); + $this->assertFieldByName("{$this->field_name}[$langcode]", '', 'Widget is displayed.'); // Create entity with three values. $edit = array("{$this->field_name}[$langcode]" => '1, 2, 3'); @@ -1495,12 +1939,12 @@ // Display the form, check that the values are correctly filled in. $this->drupalGet('test-entity/manage/' . $id . '/edit'); - $this->assertFieldByName("{$this->field_name}[$langcode]", '1, 2, 3', t('Widget is displayed.')); + $this->assertFieldByName("{$this->field_name}[$langcode]", '1, 2, 3', 'Widget is displayed.'); // Submit the form with more values than the field accepts. $edit = array("{$this->field_name}[$langcode]" => '1, 2, 3, 4, 5'); $this->drupalPost(NULL, $edit, t('Save')); - $this->assertRaw('this field cannot hold more than 4 values', t('Form validation failed.')); + $this->assertRaw('this field cannot hold more than 4 values', 'Form validation failed.'); // Check that the field values were not submitted. $this->assertFieldValues($entity_init, $this->field_name, $langcode, array(1, 2, 3)); } @@ -1534,9 +1978,21 @@ $langcode = LANGUAGE_NONE; + // Test that the form structure includes full information for each delta + // apart from #access. + $entity_type = 'test_entity'; + $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); + + $form = array(); + $form_state = form_state_defaults(); + field_attach_form($entity_type, $entity, $form, $form_state); + + $this->assertEqual($form[$field_name_no_access][$langcode][0]['value']['#entity_type'], $entity_type, 'The correct entity type is set in the field structure.'); + $this->assertFalse($form[$field_name_no_access]['#access'], 'Field #access is FALSE for the field without edit access.'); + // Display creation form. $this->drupalGet('test-entity/add/test-bundle'); - $this->assertNoFieldByName("{$field_name_no_access}[$langcode][0][value]", '', t('Widget is not displayed if field access is denied.')); + $this->assertNoFieldByName("{$field_name_no_access}[$langcode][0][value]", '', 'Widget is not displayed if field access is denied.'); // Create entity. $edit = array("{$field_name}[$langcode][0][value]" => 1); @@ -1546,8 +2002,8 @@ // Check that the default value was saved. $entity = field_test_entity_test_load($id); - $this->assertEqual($entity->{$field_name_no_access}[$langcode][0]['value'], 99, t('Default value was saved for the field with no edit access.')); - $this->assertEqual($entity->{$field_name}[$langcode][0]['value'], 1, t('Entered value vas saved for the field with edit access.')); + $this->assertEqual($entity->{$field_name_no_access}[$langcode][0]['value'], 99, 'Default value was saved for the field with no edit access.'); + $this->assertEqual($entity->{$field_name}[$langcode][0]['value'], 1, 'Entered value vas saved for the field with edit access.'); // Create a new revision. $edit = array("{$field_name}[$langcode][0][value]" => 2, 'revision' => TRUE); @@ -1555,122 +2011,122 @@ // Check that the new revision has the expected values. $entity = field_test_entity_test_load($id); - $this->assertEqual($entity->{$field_name_no_access}[$langcode][0]['value'], 99, t('New revision has the expected value for the field with no edit access.')); - $this->assertEqual($entity->{$field_name}[$langcode][0]['value'], 2, t('New revision has the expected value for the field with edit access.')); + $this->assertEqual($entity->{$field_name_no_access}[$langcode][0]['value'], 99, 'New revision has the expected value for the field with no edit access.'); + $this->assertEqual($entity->{$field_name}[$langcode][0]['value'], 2, 'New revision has the expected value for the field with edit access.'); // Check that the revision is also saved in the revisions table. $entity = field_test_entity_test_load($id, $entity->ftvid); - $this->assertEqual($entity->{$field_name_no_access}[$langcode][0]['value'], 99, t('New revision has the expected value for the field with no edit access.')); - $this->assertEqual($entity->{$field_name}[$langcode][0]['value'], 2, t('New revision has the expected value for the field with edit access.')); + $this->assertEqual($entity->{$field_name_no_access}[$langcode][0]['value'], 99, 'New revision has the expected value for the field with no edit access.'); + $this->assertEqual($entity->{$field_name}[$langcode][0]['value'], 2, 'New revision has the expected value for the field with edit access.'); } /** * Tests Field API form integration within a subform. */ function testNestedFieldForm() { - // Add two instances on the 'test_bundle' - field_create_field($this->field_single); - field_create_field($this->field_unlimited); - $this->instance['field_name'] = 'field_single'; - $this->instance['label'] = 'Single field'; - field_create_instance($this->instance); - $this->instance['field_name'] = 'field_unlimited'; - $this->instance['label'] = 'Unlimited field'; - field_create_instance($this->instance); - - // Create two entities. - $entity_1 = field_test_create_stub_entity(1, 1); - $entity_1->is_new = TRUE; - $entity_1->field_single[LANGUAGE_NONE][] = array('value' => 0); - $entity_1->field_unlimited[LANGUAGE_NONE][] = array('value' => 1); - field_test_entity_save($entity_1); - - $entity_2 = field_test_create_stub_entity(2, 2); - $entity_2->is_new = TRUE; - $entity_2->field_single[LANGUAGE_NONE][] = array('value' => 10); - $entity_2->field_unlimited[LANGUAGE_NONE][] = array('value' => 11); - field_test_entity_save($entity_2); - - // Display the 'combined form'. - $this->drupalGet('test-entity/nested/1/2'); - $this->assertFieldByName('field_single[und][0][value]', 0, t('Entity 1: field_single value appears correctly is the form.')); - $this->assertFieldByName('field_unlimited[und][0][value]', 1, t('Entity 1: field_unlimited value 0 appears correctly is the form.')); - $this->assertFieldByName('entity_2[field_single][und][0][value]', 10, t('Entity 2: field_single value appears correctly is the form.')); - $this->assertFieldByName('entity_2[field_unlimited][und][0][value]', 11, t('Entity 2: field_unlimited value 0 appears correctly is the form.')); - - // Submit the form and check that the entities are updated accordingly. - $edit = array( - 'field_single[und][0][value]' => 1, - 'field_unlimited[und][0][value]' => 2, - 'field_unlimited[und][1][value]' => 3, - 'entity_2[field_single][und][0][value]' => 11, - 'entity_2[field_unlimited][und][0][value]' => 12, - 'entity_2[field_unlimited][und][1][value]' => 13, - ); - $this->drupalPost(NULL, $edit, t('Save')); - field_cache_clear(); - $entity_1 = field_test_create_stub_entity(1); - $entity_2 = field_test_create_stub_entity(2); - $this->assertFieldValues($entity_1, 'field_single', LANGUAGE_NONE, array(1)); - $this->assertFieldValues($entity_1, 'field_unlimited', LANGUAGE_NONE, array(2, 3)); - $this->assertFieldValues($entity_2, 'field_single', LANGUAGE_NONE, array(11)); - $this->assertFieldValues($entity_2, 'field_unlimited', LANGUAGE_NONE, array(12, 13)); - - // Submit invalid values and check that errors are reported on the - // correct widgets. - $edit = array( - 'field_unlimited[und][1][value]' => -1, - ); - $this->drupalPost('test-entity/nested/1/2', $edit, t('Save')); - $this->assertRaw(t('%label does not accept the value -1', array('%label' => 'Unlimited field')), t('Entity 1: the field validation error was reported.')); - $error_field = $this->xpath('//input[@id=:id and contains(@class, "error")]', array(':id' => 'edit-field-unlimited-und-1-value')); - $this->assertTrue($error_field, t('Entity 1: the error was flagged on the correct element.')); - $edit = array( - 'entity_2[field_unlimited][und][1][value]' => -1, - ); - $this->drupalPost('test-entity/nested/1/2', $edit, t('Save')); - $this->assertRaw(t('%label does not accept the value -1', array('%label' => 'Unlimited field')), t('Entity 2: the field validation error was reported.')); - $error_field = $this->xpath('//input[@id=:id and contains(@class, "error")]', array(':id' => 'edit-entity-2-field-unlimited-und-1-value')); - $this->assertTrue($error_field, t('Entity 2: the error was flagged on the correct element.')); - - // Test that reordering works on both entities. - $edit = array( - 'field_unlimited[und][0][_weight]' => 0, - 'field_unlimited[und][1][_weight]' => -1, - 'entity_2[field_unlimited][und][0][_weight]' => 0, - 'entity_2[field_unlimited][und][1][_weight]' => -1, - ); - $this->drupalPost('test-entity/nested/1/2', $edit, t('Save')); - field_cache_clear(); - $this->assertFieldValues($entity_1, 'field_unlimited', LANGUAGE_NONE, array(3, 2)); - $this->assertFieldValues($entity_2, 'field_unlimited', LANGUAGE_NONE, array(13, 12)); - - // Test the 'add more' buttons. Only AJAX submission is tested, because - // the two 'add more' buttons present in the form have the same #value, - // which confuses drupalPost(). - // 'Add more' button in the first entity: - $this->drupalGet('test-entity/nested/1/2'); - $this->drupalPostAJAX(NULL, array(), 'field_unlimited_add_more'); - $this->assertFieldByName('field_unlimited[und][0][value]', 3, t('Entity 1: field_unlimited value 0 appears correctly is the form.')); - $this->assertFieldByName('field_unlimited[und][1][value]', 2, t('Entity 1: field_unlimited value 1 appears correctly is the form.')); - $this->assertFieldByName('field_unlimited[und][2][value]', '', t('Entity 1: field_unlimited value 2 appears correctly is the form.')); - $this->assertFieldByName('field_unlimited[und][3][value]', '', t('Entity 1: an empty widget was added for field_unlimited value 3.')); - // 'Add more' button in the first entity (changing field values): - $edit = array( - 'entity_2[field_unlimited][und][0][value]' => 13, - 'entity_2[field_unlimited][und][1][value]' => 14, - 'entity_2[field_unlimited][und][2][value]' => 15, - ); - $this->drupalPostAJAX(NULL, $edit, 'entity_2_field_unlimited_add_more'); - $this->assertFieldByName('entity_2[field_unlimited][und][0][value]', 13, t('Entity 2: field_unlimited value 0 appears correctly is the form.')); - $this->assertFieldByName('entity_2[field_unlimited][und][1][value]', 14, t('Entity 2: field_unlimited value 1 appears correctly is the form.')); - $this->assertFieldByName('entity_2[field_unlimited][und][2][value]', 15, t('Entity 2: field_unlimited value 2 appears correctly is the form.')); - $this->assertFieldByName('entity_2[field_unlimited][und][3][value]', '', t('Entity 2: an empty widget was added for field_unlimited value 3.')); - // Save the form and check values are saved correclty. - $this->drupalPost(NULL, array(), t('Save')); - field_cache_clear(); - $this->assertFieldValues($entity_1, 'field_unlimited', LANGUAGE_NONE, array(3, 2)); - $this->assertFieldValues($entity_2, 'field_unlimited', LANGUAGE_NONE, array(13, 14, 15)); + // Add two instances on the 'test_bundle' + field_create_field($this->field_single); + field_create_field($this->field_unlimited); + $this->instance['field_name'] = 'field_single'; + $this->instance['label'] = 'Single field'; + field_create_instance($this->instance); + $this->instance['field_name'] = 'field_unlimited'; + $this->instance['label'] = 'Unlimited field'; + field_create_instance($this->instance); + + // Create two entities. + $entity_1 = field_test_create_stub_entity(1, 1); + $entity_1->is_new = TRUE; + $entity_1->field_single[LANGUAGE_NONE][] = array('value' => 0); + $entity_1->field_unlimited[LANGUAGE_NONE][] = array('value' => 1); + field_test_entity_save($entity_1); + + $entity_2 = field_test_create_stub_entity(2, 2); + $entity_2->is_new = TRUE; + $entity_2->field_single[LANGUAGE_NONE][] = array('value' => 10); + $entity_2->field_unlimited[LANGUAGE_NONE][] = array('value' => 11); + field_test_entity_save($entity_2); + + // Display the 'combined form'. + $this->drupalGet('test-entity/nested/1/2'); + $this->assertFieldByName('field_single[und][0][value]', 0, 'Entity 1: field_single value appears correctly is the form.'); + $this->assertFieldByName('field_unlimited[und][0][value]', 1, 'Entity 1: field_unlimited value 0 appears correctly is the form.'); + $this->assertFieldByName('entity_2[field_single][und][0][value]', 10, 'Entity 2: field_single value appears correctly is the form.'); + $this->assertFieldByName('entity_2[field_unlimited][und][0][value]', 11, 'Entity 2: field_unlimited value 0 appears correctly is the form.'); + + // Submit the form and check that the entities are updated accordingly. + $edit = array( + 'field_single[und][0][value]' => 1, + 'field_unlimited[und][0][value]' => 2, + 'field_unlimited[und][1][value]' => 3, + 'entity_2[field_single][und][0][value]' => 11, + 'entity_2[field_unlimited][und][0][value]' => 12, + 'entity_2[field_unlimited][und][1][value]' => 13, + ); + $this->drupalPost(NULL, $edit, t('Save')); + field_cache_clear(); + $entity_1 = field_test_create_stub_entity(1); + $entity_2 = field_test_create_stub_entity(2); + $this->assertFieldValues($entity_1, 'field_single', LANGUAGE_NONE, array(1)); + $this->assertFieldValues($entity_1, 'field_unlimited', LANGUAGE_NONE, array(2, 3)); + $this->assertFieldValues($entity_2, 'field_single', LANGUAGE_NONE, array(11)); + $this->assertFieldValues($entity_2, 'field_unlimited', LANGUAGE_NONE, array(12, 13)); + + // Submit invalid values and check that errors are reported on the + // correct widgets. + $edit = array( + 'field_unlimited[und][1][value]' => -1, + ); + $this->drupalPost('test-entity/nested/1/2', $edit, t('Save')); + $this->assertRaw(t('%label does not accept the value -1', array('%label' => 'Unlimited field')), 'Entity 1: the field validation error was reported.'); + $error_field = $this->xpath('//input[@id=:id and contains(@class, "error")]', array(':id' => 'edit-field-unlimited-und-1-value')); + $this->assertTrue($error_field, 'Entity 1: the error was flagged on the correct element.'); + $edit = array( + 'entity_2[field_unlimited][und][1][value]' => -1, + ); + $this->drupalPost('test-entity/nested/1/2', $edit, t('Save')); + $this->assertRaw(t('%label does not accept the value -1', array('%label' => 'Unlimited field')), 'Entity 2: the field validation error was reported.'); + $error_field = $this->xpath('//input[@id=:id and contains(@class, "error")]', array(':id' => 'edit-entity-2-field-unlimited-und-1-value')); + $this->assertTrue($error_field, 'Entity 2: the error was flagged on the correct element.'); + + // Test that reordering works on both entities. + $edit = array( + 'field_unlimited[und][0][_weight]' => 0, + 'field_unlimited[und][1][_weight]' => -1, + 'entity_2[field_unlimited][und][0][_weight]' => 0, + 'entity_2[field_unlimited][und][1][_weight]' => -1, + ); + $this->drupalPost('test-entity/nested/1/2', $edit, t('Save')); + field_cache_clear(); + $this->assertFieldValues($entity_1, 'field_unlimited', LANGUAGE_NONE, array(3, 2)); + $this->assertFieldValues($entity_2, 'field_unlimited', LANGUAGE_NONE, array(13, 12)); + + // Test the 'add more' buttons. Only Ajax submission is tested, because + // the two 'add more' buttons present in the form have the same #value, + // which confuses drupalPost(). + // 'Add more' button in the first entity: + $this->drupalGet('test-entity/nested/1/2'); + $this->drupalPostAJAX(NULL, array(), 'field_unlimited_add_more'); + $this->assertFieldByName('field_unlimited[und][0][value]', 3, 'Entity 1: field_unlimited value 0 appears correctly is the form.'); + $this->assertFieldByName('field_unlimited[und][1][value]', 2, 'Entity 1: field_unlimited value 1 appears correctly is the form.'); + $this->assertFieldByName('field_unlimited[und][2][value]', '', 'Entity 1: field_unlimited value 2 appears correctly is the form.'); + $this->assertFieldByName('field_unlimited[und][3][value]', '', 'Entity 1: an empty widget was added for field_unlimited value 3.'); + // 'Add more' button in the first entity (changing field values): + $edit = array( + 'entity_2[field_unlimited][und][0][value]' => 13, + 'entity_2[field_unlimited][und][1][value]' => 14, + 'entity_2[field_unlimited][und][2][value]' => 15, + ); + $this->drupalPostAJAX(NULL, $edit, 'entity_2_field_unlimited_add_more'); + $this->assertFieldByName('entity_2[field_unlimited][und][0][value]', 13, 'Entity 2: field_unlimited value 0 appears correctly is the form.'); + $this->assertFieldByName('entity_2[field_unlimited][und][1][value]', 14, 'Entity 2: field_unlimited value 1 appears correctly is the form.'); + $this->assertFieldByName('entity_2[field_unlimited][und][2][value]', 15, 'Entity 2: field_unlimited value 2 appears correctly is the form.'); + $this->assertFieldByName('entity_2[field_unlimited][und][3][value]', '', 'Entity 2: an empty widget was added for field_unlimited value 3.'); + // Save the form and check values are saved correclty. + $this->drupalPost(NULL, array(), t('Save')); + field_cache_clear(); + $this->assertFieldValues($entity_1, 'field_unlimited', LANGUAGE_NONE, array(3, 2)); + $this->assertFieldValues($entity_2, 'field_unlimited', LANGUAGE_NONE, array(13, 14, 15)); } } @@ -1736,9 +2192,9 @@ $this->drupalSetContent(drupal_render($output)); $settings = field_info_formatter_settings('field_test_default'); $setting = $settings['test_formatter_setting']; - $this->assertText($this->label, t('Label was displayed.')); + $this->assertText($this->label, 'Label was displayed.'); foreach ($this->values as $delta => $value) { - $this->assertText($setting . '|' . $value['value'], t('Value @delta was displayed with expected setting.', array('@delta' => $delta))); + $this->assertText($setting . '|' . $value['value'], format_string('Value @delta was displayed with expected setting.', array('@delta' => $delta))); } // Check that explicit display settings are used. @@ -1750,16 +2206,17 @@ '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, t('Label was not displayed.')); - $this->assertText('field_test_field_attach_view_alter', t('Alter fired, display passed.')); + $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']; } - $this->assertText($setting . '|' . implode('|', $array), t('Values were displayed with expected setting.')); + $this->assertText($setting . '|' . implode('|', $array), 'Values were displayed with expected setting.'); // Check the prepare_view steps are invoked. $display = array( @@ -1773,10 +2230,10 @@ $view = drupal_render($output); $this->drupalSetContent($view); $setting = $display['settings']['test_formatter_setting_additional']; - $this->assertNoText($this->label, t('Label was not displayed.')); - $this->assertNoText('field_test_field_attach_view_alter', t('Alter not fired.')); + $this->assertNoText($this->label, 'Label was not displayed.'); + $this->assertNoText('field_test_field_attach_view_alter', 'Alter not fired.'); foreach ($this->values as $delta => $value) { - $this->assertText($setting . '|' . $value['value'] . '|' . ($value['value'] + 1), t('Value @delta was displayed with expected setting.', array('@delta' => $delta))); + $this->assertText($setting . '|' . $value['value'] . '|' . ($value['value'] + 1), format_string('Value @delta was displayed with expected setting.', array('@delta' => $delta))); } // View mode: check that display settings specified in the instance are @@ -1784,9 +2241,9 @@ $output = field_view_field('test_entity', $this->entity, $this->field_name, 'teaser'); $this->drupalSetContent(drupal_render($output)); $setting = $this->instance['display']['teaser']['settings']['test_formatter_setting']; - $this->assertText($this->label, t('Label was displayed.')); + $this->assertText($this->label, 'Label was displayed.'); foreach ($this->values as $delta => $value) { - $this->assertText($setting . '|' . $value['value'], t('Value @delta was displayed with expected setting.', array('@delta' => $delta))); + $this->assertText($setting . '|' . $value['value'], format_string('Value @delta was displayed with expected setting.', array('@delta' => $delta))); } // Unknown view mode: check that display settings for 'default' view mode @@ -1794,9 +2251,9 @@ $output = field_view_field('test_entity', $this->entity, $this->field_name, 'unknown_view_mode'); $this->drupalSetContent(drupal_render($output)); $setting = $this->instance['display']['default']['settings']['test_formatter_setting']; - $this->assertText($this->label, t('Label was displayed.')); + $this->assertText($this->label, 'Label was displayed.'); foreach ($this->values as $delta => $value) { - $this->assertText($setting . '|' . $value['value'], t('Value @delta was displayed with expected setting.', array('@delta' => $delta))); + $this->assertText($setting . '|' . $value['value'], format_string('Value @delta was displayed with expected setting.', array('@delta' => $delta))); } } @@ -1811,7 +2268,7 @@ $item = $this->entity->{$this->field_name}[LANGUAGE_NONE][$delta]; $output = field_view_value('test_entity', $this->entity, $this->field_name, $item); $this->drupalSetContent(drupal_render($output)); - $this->assertText($setting . '|' . $value['value'], t('Value @delta was displayed with expected setting.', array('@delta' => $delta))); + $this->assertText($setting . '|' . $value['value'], format_string('Value @delta was displayed with expected setting.', array('@delta' => $delta))); } // Check that explicit display settings are used. @@ -1827,7 +2284,7 @@ $item = $this->entity->{$this->field_name}[LANGUAGE_NONE][$delta]; $output = field_view_value('test_entity', $this->entity, $this->field_name, $item, $display); $this->drupalSetContent(drupal_render($output)); - $this->assertText($setting . '|0:' . $value['value'], t('Value @delta was displayed with expected setting.', array('@delta' => $delta))); + $this->assertText($setting . '|0:' . $value['value'], format_string('Value @delta was displayed with expected setting.', array('@delta' => $delta))); } // Check that prepare_view steps are invoked. @@ -1843,7 +2300,7 @@ $item = $this->entity->{$this->field_name}[LANGUAGE_NONE][$delta]; $output = field_view_value('test_entity', $this->entity, $this->field_name, $item, $display); $this->drupalSetContent(drupal_render($output)); - $this->assertText($setting . '|' . $value['value'] . '|' . ($value['value'] + 1), t('Value @delta was displayed with expected setting.', array('@delta' => $delta))); + $this->assertText($setting . '|' . $value['value'] . '|' . ($value['value'] + 1), format_string('Value @delta was displayed with expected setting.', array('@delta' => $delta))); } // View mode: check that display settings specified in the instance are @@ -1853,7 +2310,7 @@ $item = $this->entity->{$this->field_name}[LANGUAGE_NONE][$delta]; $output = field_view_value('test_entity', $this->entity, $this->field_name, $item, 'teaser'); $this->drupalSetContent(drupal_render($output)); - $this->assertText($setting . '|' . $value['value'], t('Value @delta was displayed with expected setting.', array('@delta' => $delta))); + $this->assertText($setting . '|' . $value['value'], format_string('Value @delta was displayed with expected setting.', array('@delta' => $delta))); } // Unknown view mode: check that display settings for 'default' view mode @@ -1863,7 +2320,7 @@ $item = $this->entity->{$this->field_name}[LANGUAGE_NONE][$delta]; $output = field_view_value('test_entity', $this->entity, $this->field_name, $item, 'unknown_view_mode'); $this->drupalSetContent(drupal_render($output)); - $this->assertText($setting . '|' . $value['value'], t('Value @delta was displayed with expected setting.', array('@delta' => $delta))); + $this->assertText($setting . '|' . $value['value'], format_string('Value @delta was displayed with expected setting.', array('@delta' => $delta))); } } } @@ -1906,18 +2363,18 @@ $record['data'] = unserialize($record['data']); // Ensure that basic properties are preserved. - $this->assertEqual($record['field_name'], $field_definition['field_name'], t('The field name is properly saved.')); - $this->assertEqual($record['type'], $field_definition['type'], t('The field type is properly saved.')); + $this->assertEqual($record['field_name'], $field_definition['field_name'], 'The field name is properly saved.'); + $this->assertEqual($record['type'], $field_definition['type'], 'The field type is properly saved.'); // Ensure that cardinality defaults to 1. - $this->assertEqual($record['cardinality'], 1, t('Cardinality defaults to 1.')); + $this->assertEqual($record['cardinality'], 1, 'Cardinality defaults to 1.'); // Ensure that default settings are present. $field_type = field_info_field_types($field_definition['type']); - $this->assertIdentical($record['data']['settings'], $field_type['settings'], t('Default field settings have been written.')); + $this->assertIdentical($record['data']['settings'], $field_type['settings'], 'Default field settings have been written.'); // Ensure that default storage was set. - $this->assertEqual($record['storage_type'], variable_get('field_storage_default'), t('The field type is properly saved.')); + $this->assertEqual($record['storage_type'], variable_get('field_storage_default'), 'The field type is properly saved.'); // Guarantee that the name is unique. try { @@ -2044,7 +2501,42 @@ // Read the field back. $field = field_read_field($field_definition['field_name']); - $this->assertTrue($field_definition < $field, t('The field was properly read.')); + $this->assertTrue($field_definition < $field, 'The field was properly read.'); + } + + /** + * Tests reading field definitions. + */ + function testReadFields() { + $field_definition = array( + 'field_name' => 'field_1', + 'type' => 'test_field', + ); + field_create_field($field_definition); + + // Check that 'single column' criteria works. + $fields = field_read_fields(array('field_name' => $field_definition['field_name'])); + $this->assertTrue(count($fields) == 1 && isset($fields[$field_definition['field_name']]), 'The field was properly read.'); + + // Check that 'multi column' criteria works. + $fields = field_read_fields(array('field_name' => $field_definition['field_name'], 'type' => $field_definition['type'])); + $this->assertTrue(count($fields) == 1 && isset($fields[$field_definition['field_name']]), 'The field was properly read.'); + $fields = field_read_fields(array('field_name' => $field_definition['field_name'], 'type' => 'foo')); + $this->assertTrue(empty($fields), 'No field was found.'); + + // Create an instance of the field. + $instance_definition = array( + 'field_name' => $field_definition['field_name'], + 'entity_type' => 'test_entity', + 'bundle' => 'test_bundle', + ); + field_create_instance($instance_definition); + + // Check that criteria spanning over the field_config_instance table work. + $fields = field_read_fields(array('entity_type' => $instance_definition['entity_type'], 'bundle' => $instance_definition['bundle'])); + $this->assertTrue(count($fields) == 1 && isset($fields[$field_definition['field_name']]), 'The field was properly read.'); + $fields = field_read_fields(array('entity_type' => $instance_definition['entity_type'], 'field_name' => $instance_definition['field_name'])); + $this->assertTrue(count($fields) == 1 && isset($fields[$field_definition['field_name']]), 'The field was properly read.'); } /** @@ -2059,7 +2551,7 @@ field_create_field($field_definition); $field = field_read_field($field_definition['field_name']); $expected_indexes = array('value' => array('value')); - $this->assertEqual($field['indexes'], $expected_indexes, t('Field type indexes saved by default')); + $this->assertEqual($field['indexes'], $expected_indexes, 'Field type indexes saved by default'); // Check that indexes specified by the field definition override the field // type indexes. @@ -2073,7 +2565,7 @@ field_create_field($field_definition); $field = field_read_field($field_definition['field_name']); $expected_indexes = array('value' => array()); - $this->assertEqual($field['indexes'], $expected_indexes, t('Field definition indexes override field type indexes')); + $this->assertEqual($field['indexes'], $expected_indexes, 'Field definition indexes override field type indexes'); // Check that indexes specified by the field definition add to the field // type indexes. @@ -2087,7 +2579,7 @@ field_create_field($field_definition); $field = field_read_field($field_definition['field_name']); $expected_indexes = array('value' => array('value'), 'value_2' => array('value')); - $this->assertEqual($field['indexes'], $expected_indexes, t('Field definition indexes are merged with field type indexes')); + $this->assertEqual($field['indexes'], $expected_indexes, 'Field definition indexes are merged with field type indexes'); } /** @@ -2118,41 +2610,41 @@ // Test that the first field is not deleted, and then delete it. $field = field_read_field($this->field['field_name'], array('include_deleted' => TRUE)); - $this->assertTrue(!empty($field) && empty($field['deleted']), t('A new field is not marked for deletion.')); + $this->assertTrue(!empty($field) && empty($field['deleted']), 'A new field is not marked for deletion.'); field_delete_field($this->field['field_name']); // Make sure that the field is marked as deleted when it is specifically // loaded. $field = field_read_field($this->field['field_name'], array('include_deleted' => TRUE)); - $this->assertTrue(!empty($field['deleted']), t('A deleted field is marked for deletion.')); + $this->assertTrue(!empty($field['deleted']), 'A deleted field is marked for deletion.'); // Make sure that this field's instance is marked as deleted when it is // specifically loaded. $instance = field_read_instance('test_entity', $this->instance_definition['field_name'], $this->instance_definition['bundle'], array('include_deleted' => TRUE)); - $this->assertTrue(!empty($instance['deleted']), t('An instance for a deleted field is marked for deletion.')); + $this->assertTrue(!empty($instance['deleted']), 'An instance for a deleted field is marked for deletion.'); // Try to load the field normally and make sure it does not show up. $field = field_read_field($this->field['field_name']); - $this->assertTrue(empty($field), t('A deleted field is not loaded by default.')); + $this->assertTrue(empty($field), 'A deleted field is not loaded by default.'); // Try to load the instance normally and make sure it does not show up. $instance = field_read_instance('test_entity', $this->instance_definition['field_name'], $this->instance_definition['bundle']); - $this->assertTrue(empty($instance), t('An instance for a deleted field is not loaded by default.')); + $this->assertTrue(empty($instance), 'An instance for a deleted field is not loaded by default.'); // Make sure the other field (and its field instance) are not deleted. $another_field = field_read_field($this->another_field['field_name']); - $this->assertTrue(!empty($another_field) && empty($another_field['deleted']), t('A non-deleted field is not marked for deletion.')); + $this->assertTrue(!empty($another_field) && empty($another_field['deleted']), 'A non-deleted field is not marked for deletion.'); $another_instance = field_read_instance('test_entity', $this->another_instance_definition['field_name'], $this->another_instance_definition['bundle']); - $this->assertTrue(!empty($another_instance) && empty($another_instance['deleted']), t('An instance of a non-deleted field is not marked for deletion.')); + $this->assertTrue(!empty($another_instance) && empty($another_instance['deleted']), 'An instance of a non-deleted field is not marked for deletion.'); // Try to create a new field the same name as a deleted field and // write data into it. field_create_field($this->field); field_create_instance($this->instance_definition); $field = field_read_field($this->field['field_name']); - $this->assertTrue(!empty($field) && empty($field['deleted']), t('A new field with a previously used name is created.')); + $this->assertTrue(!empty($field) && empty($field['deleted']), 'A new field with a previously used name is created.'); $instance = field_read_instance('test_entity', $this->instance_definition['field_name'], $this->instance_definition['bundle']); - $this->assertTrue(!empty($instance) && empty($instance['deleted']), t('A new instance for a previously used field name is created.')); + $this->assertTrue(!empty($instance) && empty($instance['deleted']), 'A new instance for a previously used field name is created.'); // Save an entity with data for the field $entity = field_test_create_stub_entity(0, 0, $instance['bundle']); @@ -2305,18 +2797,18 @@ // Read the field. $field = field_read_field($field_name); - $this->assertTrue($field_definition <= $field, t('The field was properly read.')); + $this->assertTrue($field_definition <= $field, 'The field was properly read.'); module_disable($modules, FALSE); $fields = field_read_fields(array('field_name' => $field_name), array('include_inactive' => TRUE)); - $this->assertTrue(isset($fields[$field_name]) && $field_definition < $field, t('The field is properly read when explicitly fetching inactive fields.')); + $this->assertTrue(isset($fields[$field_name]) && $field_definition < $field, 'The field is properly read when explicitly fetching inactive fields.'); // Re-enable modules one by one, and check that the field is still inactive // while some modules remain disabled. while ($modules) { $field = field_read_field($field_name); - $this->assertTrue(empty($field), t('%modules disabled. The field is marked inactive.', array('%modules' => implode(', ', $modules)))); + $this->assertTrue(empty($field), format_string('%modules disabled. The field is marked inactive.', array('%modules' => implode(', ', $modules)))); $module = array_shift($modules); module_enable(array($module), FALSE); @@ -2325,7 +2817,7 @@ // Check that the field is active again after all modules have been // enabled. $field = field_read_field($field_name); - $this->assertTrue($field_definition <= $field, t('The field was was marked active.')); + $this->assertTrue($field_definition <= $field, 'The field was was marked active.'); } } @@ -2377,17 +2869,17 @@ $formatter_type = field_info_formatter_types($field_type['default_formatter']); // Check that default values are set. - $this->assertIdentical($record['data']['required'], FALSE, t('Required defaults to false.')); - $this->assertIdentical($record['data']['label'], $this->instance_definition['field_name'], t('Label defaults to field name.')); - $this->assertIdentical($record['data']['description'], '', t('Description defaults to empty string.')); - $this->assertIdentical($record['data']['widget']['type'], $field_type['default_widget'], t('Default widget has been written.')); - $this->assertTrue(isset($record['data']['display']['default']), t('Display for "full" view_mode has been written.')); - $this->assertIdentical($record['data']['display']['default']['type'], $field_type['default_formatter'], t('Default formatter for "full" view_mode has been written.')); + $this->assertIdentical($record['data']['required'], FALSE, 'Required defaults to false.'); + $this->assertIdentical($record['data']['label'], $this->instance_definition['field_name'], 'Label defaults to field name.'); + $this->assertIdentical($record['data']['description'], '', 'Description defaults to empty string.'); + $this->assertIdentical($record['data']['widget']['type'], $field_type['default_widget'], 'Default widget has been written.'); + $this->assertTrue(isset($record['data']['display']['default']), 'Display for "full" view_mode has been written.'); + $this->assertIdentical($record['data']['display']['default']['type'], $field_type['default_formatter'], 'Default formatter for "full" view_mode has been written.'); // Check that default settings are set. - $this->assertIdentical($record['data']['settings'], $field_type['instance_settings'] , t('Default instance settings have been written.')); - $this->assertIdentical($record['data']['widget']['settings'], $widget_type['settings'] , t('Default widget settings have been written.')); - $this->assertIdentical($record['data']['display']['default']['settings'], $formatter_type['settings'], t('Default formatter settings for "full" view_mode have been written.')); + $this->assertIdentical($record['data']['settings'], $field_type['instance_settings'] , 'Default instance settings have been written.'); + $this->assertIdentical($record['data']['widget']['settings'], $widget_type['settings'] , 'Default widget settings have been written.'); + $this->assertIdentical($record['data']['display']['default']['settings'], $formatter_type['settings'], 'Default formatter settings for "full" view_mode have been written.'); // Guarantee that the field/bundle combination is unique. try { @@ -2452,7 +2944,7 @@ // Read the instance back. $instance = field_read_instance('test_entity', $this->instance_definition['field_name'], $this->instance_definition['bundle']); - $this->assertTrue($this->instance_definition < $instance, t('The field was properly read.')); + $this->assertTrue($this->instance_definition < $instance, 'The field was properly read.'); } /** @@ -2475,13 +2967,13 @@ field_update_instance($instance); $instance_new = field_read_instance('test_entity', $this->instance_definition['field_name'], $this->instance_definition['bundle']); - $this->assertEqual($instance['required'], $instance_new['required'], t('"required" change is saved')); - $this->assertEqual($instance['label'], $instance_new['label'], t('"label" change is saved')); - $this->assertEqual($instance['description'], $instance_new['description'], t('"description" change is saved')); - $this->assertEqual($instance['widget']['settings']['test_widget_setting'], $instance_new['widget']['settings']['test_widget_setting'], t('Widget setting change is saved')); - $this->assertEqual($instance['widget']['weight'], $instance_new['widget']['weight'], t('Widget weight change is saved')); - $this->assertEqual($instance['display']['default']['settings']['test_formatter_setting'], $instance_new['display']['default']['settings']['test_formatter_setting'], t('Formatter setting change is saved')); - $this->assertEqual($instance['display']['default']['weight'], $instance_new['display']['default']['weight'], t('Widget weight change is saved')); + $this->assertEqual($instance['required'], $instance_new['required'], '"required" change is saved'); + $this->assertEqual($instance['label'], $instance_new['label'], '"label" change is saved'); + $this->assertEqual($instance['description'], $instance_new['description'], '"description" change is saved'); + $this->assertEqual($instance['widget']['settings']['test_widget_setting'], $instance_new['widget']['settings']['test_widget_setting'], 'Widget setting change is saved'); + $this->assertEqual($instance['widget']['weight'], $instance_new['widget']['weight'], 'Widget weight change is saved'); + $this->assertEqual($instance['display']['default']['settings']['test_formatter_setting'], $instance_new['display']['default']['settings']['test_formatter_setting'], 'Formatter setting change is saved'); + $this->assertEqual($instance['display']['default']['weight'], $instance_new['display']['default']['weight'], 'Widget weight change is saved'); // Check that changing widget and formatter types updates the default settings. $instance = field_read_instance('test_entity', $this->instance_definition['field_name'], $this->instance_definition['bundle']); @@ -2490,13 +2982,13 @@ field_update_instance($instance); $instance_new = field_read_instance('test_entity', $this->instance_definition['field_name'], $this->instance_definition['bundle']); - $this->assertEqual($instance['widget']['type'], $instance_new['widget']['type'] , t('Widget type change is saved.')); + $this->assertEqual($instance['widget']['type'], $instance_new['widget']['type'] , 'Widget type change is saved.'); $settings = field_info_widget_settings($instance_new['widget']['type']); - $this->assertIdentical($settings, array_intersect_key($instance_new['widget']['settings'], $settings) , t('Widget type change updates default settings.')); - $this->assertEqual($instance['display']['default']['type'], $instance_new['display']['default']['type'] , t('Formatter type change is saved.')); + $this->assertIdentical($settings, array_intersect_key($instance_new['widget']['settings'], $settings) , 'Widget type change updates default settings.'); + $this->assertEqual($instance['display']['default']['type'], $instance_new['display']['default']['type'] , 'Formatter type change is saved.'); $info = field_info_formatter_types($instance_new['display']['default']['type']); $settings = $info['settings']; - $this->assertIdentical($settings, array_intersect_key($instance_new['display']['default']['settings'], $settings) , t('Changing formatter type updates default settings.')); + $this->assertIdentical($settings, array_intersect_key($instance_new['display']['default']['settings'], $settings) , 'Changing formatter type updates default settings.'); // Check that adding a new view mode is saved and gets default settings. $instance = field_read_instance('test_entity', $this->instance_definition['field_name'], $this->instance_definition['bundle']); @@ -2504,11 +2996,11 @@ field_update_instance($instance); $instance_new = field_read_instance('test_entity', $this->instance_definition['field_name'], $this->instance_definition['bundle']); - $this->assertTrue(isset($instance_new['display']['teaser']), t('Display for the new view_mode has been written.')); - $this->assertIdentical($instance_new['display']['teaser']['type'], $field_type['default_formatter'], t('Default formatter for the new view_mode has been written.')); + $this->assertTrue(isset($instance_new['display']['teaser']), 'Display for the new view_mode has been written.'); + $this->assertIdentical($instance_new['display']['teaser']['type'], $field_type['default_formatter'], 'Default formatter for the new view_mode has been written.'); $info = field_info_formatter_types($instance_new['display']['teaser']['type']); $settings = $info['settings']; - $this->assertIdentical($settings, $instance_new['display']['teaser']['settings'] , t('Default formatter settings for the new view_mode have been written.')); + $this->assertIdentical($settings, $instance_new['display']['teaser']['settings'] , 'Default formatter settings for the new view_mode have been written.'); // TODO: test failures. } @@ -2530,26 +3022,26 @@ // Test that the first instance is not deleted, and then delete it. $instance = field_read_instance('test_entity', $this->instance_definition['field_name'], $this->instance_definition['bundle'], array('include_deleted' => TRUE)); - $this->assertTrue(!empty($instance) && empty($instance['deleted']), t('A new field instance is not marked for deletion.')); + $this->assertTrue(!empty($instance) && empty($instance['deleted']), 'A new field instance is not marked for deletion.'); field_delete_instance($instance); // Make sure the instance is marked as deleted when the instance is // specifically loaded. $instance = field_read_instance('test_entity', $this->instance_definition['field_name'], $this->instance_definition['bundle'], array('include_deleted' => TRUE)); - $this->assertTrue(!empty($instance['deleted']), t('A deleted field instance is marked for deletion.')); + $this->assertTrue(!empty($instance['deleted']), 'A deleted field instance is marked for deletion.'); // Try to load the instance normally and make sure it does not show up. $instance = field_read_instance('test_entity', $this->instance_definition['field_name'], $this->instance_definition['bundle']); - $this->assertTrue(empty($instance), t('A deleted field instance is not loaded by default.')); + $this->assertTrue(empty($instance), 'A deleted field instance is not loaded by default.'); // Make sure the other field instance is not deleted. $another_instance = field_read_instance('test_entity', $this->another_instance_definition['field_name'], $this->another_instance_definition['bundle']); - $this->assertTrue(!empty($another_instance) && empty($another_instance['deleted']), t('A non-deleted field instance is not marked for deletion.')); + $this->assertTrue(!empty($another_instance) && empty($another_instance['deleted']), 'A non-deleted field instance is not marked for deletion.'); // Make sure the field is deleted when its last instance is deleted. field_delete_instance($another_instance); $field = field_read_field($another_instance['field_name'], array('include_deleted' => TRUE)); - $this->assertTrue(!empty($field['deleted']), t('A deleted field is marked for deletion after all its instances have been marked for deletion.')); + $this->assertTrue(!empty($field['deleted']), 'A deleted field is marked for deletion after all its instances have been marked for deletion.'); } } @@ -2616,23 +3108,26 @@ $available_languages = field_available_languages($this->entity_type, $this->field); foreach ($available_languages as $delta => $langcode) { if ($langcode != 'xx' && $langcode != 'en') { - $this->assertTrue(in_array($langcode, $enabled_languages), t('%language is an enabled language.', array('%language' => $langcode))); + $this->assertTrue(in_array($langcode, $enabled_languages), format_string('%language is an enabled language.', array('%language' => $langcode))); } } - $this->assertTrue(in_array('xx', $available_languages), t('%language was made available.', array('%language' => 'xx'))); - $this->assertFalse(in_array('en', $available_languages), t('%language was made unavailable.', array('%language' => 'en'))); + $this->assertTrue(in_array('xx', $available_languages), format_string('%language was made available.', array('%language' => 'xx'))); + $this->assertFalse(in_array('en', $available_languages), format_string('%language was made unavailable.', array('%language' => 'en'))); // Test field_available_languages() behavior for untranslatable fields. $this->field['translatable'] = FALSE; - $this->field_name = $this->field['field_name'] = $this->instance['field_name'] = drupal_strtolower($this->randomName() . '_field_name'); + field_update_field($this->field); $available_languages = field_available_languages($this->entity_type, $this->field); - $this->assertTrue(count($available_languages) == 1 && $available_languages[0] === LANGUAGE_NONE, t('For untranslatable fields only LANGUAGE_NONE is available.')); + $this->assertTrue(count($available_languages) == 1 && $available_languages[0] === LANGUAGE_NONE, 'For untranslatable fields only LANGUAGE_NONE is available.'); } /** * Test the multilanguage logic of _field_invoke(). */ function testFieldInvoke() { + // Enable field translations for the entity. + field_test_entity_info_translatable('test_entity', TRUE); + $entity_type = 'test_entity'; $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); @@ -2642,7 +3137,7 @@ $extra_languages = mt_rand(1, 4); $languages = $available_languages = field_available_languages($this->entity_type, $this->field); for ($i = 0; $i < $extra_languages; ++$i) { - $languages[] = $this->randomString(2); + $languages[] = $this->randomName(2); } // For each given language provide some random values. @@ -2658,19 +3153,24 @@ $hash = hash('sha256', serialize(array($entity_type, $entity, $this->field_name, $langcode, $values[$langcode]))); // Check whether the parameters passed to _field_invoke() were correctly // forwarded to the callback function. - $this->assertEqual($hash, $result, t('The result for %language is correctly stored.', array('%language' => $langcode))); + $this->assertEqual($hash, $result, format_string('The result for %language is correctly stored.', array('%language' => $langcode))); } - $this->assertEqual(count($results), count($available_languages), t('No unavailable language has been processed.')); + + $this->assertEqual(count($results), count($available_languages), 'No unavailable language has been processed.'); } /** * Test the multilanguage logic of _field_invoke_multiple(). */ function testFieldInvokeMultiple() { + // Enable field translations for the entity. + field_test_entity_info_translatable('test_entity', TRUE); + $values = array(); + $options = array(); $entities = array(); $entity_type = 'test_entity'; - $entity_count = mt_rand(1, 5); + $entity_count = 5; $available_languages = field_available_languages($this->entity_type, $this->field); for ($id = 1; $id <= $entity_count; ++$id) { @@ -2681,28 +3181,54 @@ // correctly uses the result of field_available_languages(). $extra_languages = mt_rand(1, 4); for ($i = 0; $i < $extra_languages; ++$i) { - $languages[] = $this->randomString(2); + $languages[] = $this->randomName(2); } // For each given language provide some random values. - foreach ($languages as $langcode) { - for ($delta = 0; $delta < $this->field['cardinality']; $delta++) { - $values[$id][$langcode][$delta]['value'] = mt_rand(1, 127); + $language_count = count($languages); + for ($i = 0; $i < $language_count; ++$i) { + $langcode = $languages[$i]; + // Avoid to populate at least one field translation to check that + // per-entity language suggestions work even when available field values + // are different for each language. + if ($i !== $id) { + for ($delta = 0; $delta < $this->field['cardinality']; $delta++) { + $values[$id][$langcode][$delta]['value'] = mt_rand(1, 127); + } + } + // Ensure that a language for which there is no field translation is + // used as display language to prepare per-entity language suggestions. + elseif (!isset($display_language)) { + $display_language = $langcode; } } + $entity->{$this->field_name} = $values[$id]; $entities[$id] = $entity; + + // Store per-entity language suggestions. + $options['language'][$id] = field_language($entity_type, $entity, NULL, $display_language); } $grouped_results = _field_invoke_multiple('test_op_multiple', $entity_type, $entities); foreach ($grouped_results as $id => $results) { foreach ($results as $langcode => $result) { - $hash = hash('sha256', serialize(array($entity_type, $entities[$id], $this->field_name, $langcode, $values[$id][$langcode]))); - // Check whether the parameters passed to _field_invoke() were correctly - // forwarded to the callback function. - $this->assertEqual($hash, $result, t('The result for entity %id/%language is correctly stored.', array('%id' => $id, '%language' => $langcode))); + if (isset($values[$id][$langcode])) { + $hash = hash('sha256', serialize(array($entity_type, $entities[$id], $this->field_name, $langcode, $values[$id][$langcode]))); + // Check whether the parameters passed to _field_invoke_multiple() + // were correctly forwarded to the callback function. + $this->assertEqual($hash, $result, format_string('The result for entity %id/%language is correctly stored.', array('%id' => $id, '%language' => $langcode))); + } + } + $this->assertEqual(count($results), count($available_languages), format_string('No unavailable language has been processed for entity %id.', array('%id' => $id))); + } + + $null = NULL; + $grouped_results = _field_invoke_multiple('test_op_multiple', $entity_type, $entities, $null, $null, $options); + foreach ($grouped_results as $id => $results) { + foreach ($results as $langcode => $result) { + $this->assertTrue(isset($options['language'][$id]), format_string('The result language %language for entity %id was correctly suggested (display language: %display_language).', array('%id' => $id, '%language' => $langcode, '%display_language' => $display_language))); } - $this->assertEqual(count($results), count($available_languages), t('No unavailable language has been processed for entity %id.', array('%id' => $id))); } } @@ -2713,7 +3239,7 @@ // Enable field translations for nodes. field_test_entity_info_translatable('node', TRUE); $entity_info = entity_get_info('node'); - $this->assertTrue(count($entity_info['translation']), t('Nodes are translatable.')); + $this->assertTrue(count($entity_info['translation']), 'Nodes are translatable.'); // Prepare the field translations. field_test_entity_info_translatable('test_entity', TRUE); @@ -2722,7 +3248,7 @@ $entity = field_test_create_stub_entity($eid, $evid, $this->instance['bundle']); $field_translations = array(); $available_languages = field_available_languages($entity_type, $this->field); - $this->assertTrue(count($available_languages) > 1, t('Field is translatable.')); + $this->assertTrue(count($available_languages) > 1, 'Field is translatable.'); foreach ($available_languages as $langcode) { $field_translations[$langcode] = $this->_generateTestFieldValues($this->field['cardinality']); } @@ -2739,7 +3265,7 @@ foreach ($items as $delta => $item) { $result = $result && $item['value'] == $entity->{$this->field_name}[$langcode][$delta]['value']; } - $this->assertTrue($result, t('%language translation correctly handled.', array('%language' => $langcode))); + $this->assertTrue($result, format_string('%language translation correctly handled.', array('%language' => $langcode))); } } @@ -2795,7 +3321,7 @@ $display_language = field_language($entity_type, $entity, NULL, $requested_language); foreach ($instances as $instance) { $field_name = $instance['field_name']; - $this->assertTrue($display_language[$field_name] == LANGUAGE_NONE, t('The display language for field %field_name is %language.', array('%field_name' => $field_name, '%language' => LANGUAGE_NONE))); + $this->assertTrue($display_language[$field_name] == LANGUAGE_NONE, format_string('The display language for field %field_name is %language.', array('%field_name' => $field_name, '%language' => LANGUAGE_NONE))); } // Test multiple-fields display languages for translatable entities. @@ -2809,20 +3335,20 @@ // As the requested language was not assinged to any field, if the // returned language is defined for the current field, core fallback rules // were successfully applied. - $this->assertTrue(isset($entity->{$field_name}[$langcode]) && $langcode != $requested_language, t('The display language for the field %field_name is %language.', array('%field_name' => $field_name, '%language' => $langcode))); + $this->assertTrue(isset($entity->{$field_name}[$langcode]) && $langcode != $requested_language, format_string('The display language for the field %field_name is %language.', array('%field_name' => $field_name, '%language' => $langcode))); } // Test single-field display language. drupal_static_reset('field_language'); $langcode = field_language($entity_type, $entity, $this->field_name, $requested_language); - $this->assertTrue(isset($entity->{$this->field_name}[$langcode]) && $langcode != $requested_language, t('The display language for the (single) field %field_name is %language.', array('%field_name' => $field_name, '%language' => $langcode))); + $this->assertTrue(isset($entity->{$this->field_name}[$langcode]) && $langcode != $requested_language, format_string('The display language for the (single) field %field_name is %language.', array('%field_name' => $field_name, '%language' => $langcode))); // Test field_language() basic behavior without language fallback. variable_set('field_test_language_fallback', FALSE); $entity->{$this->field_name}[$requested_language] = mt_rand(1, 127); drupal_static_reset('field_language'); $display_language = field_language($entity_type, $entity, $this->field_name, $requested_language); - $this->assertEqual($display_language, $requested_language, t('Display language behave correctly when language fallback is disabled')); + $this->assertEqual($display_language, $requested_language, 'Display language behave correctly when language fallback is disabled'); } /** @@ -2866,7 +3392,7 @@ $entity = field_test_entity_test_load($eid, $evid); foreach ($available_languages as $langcode => $value) { $passed = isset($entity->{$field_name}[$langcode]) && $entity->{$field_name}[$langcode][0]['value'] == $value + 1; - $this->assertTrue($passed, t('The @language translation for revision @revision was correctly stored', array('@language' => $langcode, '@revision' => $entity->ftvid))); + $this->assertTrue($passed, format_string('The @language translation for revision @revision was correctly stored', array('@language' => $langcode, '@revision' => $entity->ftvid))); } } } @@ -2880,7 +3406,7 @@ public static function getInfo() { return array( 'name' => 'Field bulk delete tests', - 'description'=> 'Bulk delete fields and instances, and clean up afterwards.', + 'description' => 'Bulk delete fields and instances, and clean up afterwards.', 'group' => 'Field API', ); } @@ -2905,22 +3431,54 @@ */ function _generateStubEntities($entity_type, $entities, $field_name = NULL) { $stubs = array(); - foreach ($entities as $entity) { + foreach ($entities as $id => $entity) { $stub = entity_create_stub_entity($entity_type, entity_extract_ids($entity_type, $entity)); if (isset($field_name)) { $stub->{$field_name} = $entity->{$field_name}; } - $stubs[] = $stub; + $stubs[$id] = $stub; } return $stubs; } + /** + * Tests that the expected hooks have been invoked on the expected entities. + * + * @param $expected_hooks + * An array keyed by hook name, with one entry per expected invocation. + * Each entry is the value of the "$entity" parameter the hook is expected + * to have been passed. + * @param $actual_hooks + * The array of actual hook invocations recorded by field_test_memorize(). + */ + function checkHooksInvocations($expected_hooks, $actual_hooks) { + foreach ($expected_hooks as $hook => $invocations) { + $actual_invocations = $actual_hooks[$hook]; + + // Check that the number of invocations is correct. + $this->assertEqual(count($actual_invocations), count($invocations), "$hook() was called the expected number of times."); + + // Check that the hook was called for each expected argument. + foreach ($invocations as $argument) { + $found = FALSE; + foreach ($actual_invocations as $actual_arguments) { + if ($actual_arguments[1] == $argument) { + $found = TRUE; + break; + } + } + $this->assertTrue($found, "$hook() was called on expected argument"); + } + } + } + function setUp() { parent::setUp('field_test'); - // Clean up data from previous test cases. $this->fields = array(); $this->instances = array(); + $this->entities = array(); + $this->entities_by_bundles = array(); // Create two bundles. $this->bundles = array('bb_1' => 'bb_1', 'bb_2' => 'bb_2'); @@ -2956,7 +3514,10 @@ foreach ($this->fields as $field) { $entity->{$field['field_name']}[LANGUAGE_NONE] = $this->_generateTestFieldValues($field['cardinality']); } + $this->entities[$id] = $entity; + // Also keep track of the entities per bundle. + $this->entities_by_bundles[$bundle][$id] = $entity; field_attach_insert($this->entity_type, $entity); $id++; } @@ -3021,6 +3582,7 @@ * instance is deleted. */ function testPurgeInstance() { + // Start recording hook invocations. field_test_memorize(); $bundle = reset($this->bundles); @@ -3035,7 +3597,7 @@ $this->assertEqual(count($mem), 0, 'No field hooks were called'); $batch_size = 2; - for ($count = 8; $count >= 0; $count -= 2) { + for ($count = 8; $count >= 0; $count -= $batch_size) { // Purge two entities. field_purge_batch($batch_size); @@ -3049,19 +3611,21 @@ $this->assertEqual($count ? count($found['test_entity']) : count($found), $count, 'Correct number of entities found after purging 2'); } - // hook_field_delete() was called on a pseudo-entity for each entity. Each - // pseudo entity has a $field property that matches the original entity, - // but no others. - $mem = field_test_memorize(); - $this->assertEqual(count($mem['field_test_field_delete']), 10, 'hook_field_delete was called for the right number of entities'); - $stubs = $this->_generateStubEntities($this->entity_type, $this->entities, $field['field_name']); - $count = count($stubs); - foreach ($mem['field_test_field_delete'] as $args) { - $entity = $args[1]; - $this->assertEqual($stubs[$entity->ftid], $entity, 'hook_field_delete() called with the correct stub'); - unset($stubs[$entity->ftid]); + // Check hooks invocations. + // - hook_field_load() (multiple hook) should have been called on all + // entities by pairs of two. + // - hook_field_delete() should have been called once for each entity in the + // bundle. + $actual_hooks = field_test_memorize(); + $hooks = array(); + $stubs = $this->_generateStubEntities($this->entity_type, $this->entities_by_bundles[$bundle], $field['field_name']); + foreach (array_chunk($stubs, $batch_size, TRUE) as $chunk) { + $hooks['field_test_field_load'][] = $chunk; } - $this->assertEqual(count($stubs), $count-10, 'hook_field_delete was called with each entity once'); + foreach ($stubs as $stub) { + $hooks['field_test_field_delete'][] = $stub; + } + $this->checkHooksInvocations($hooks, $actual_hooks); // The instance still exists, deleted. $instances = field_read_instances(array('field_id' => $field['id'], 'deleted' => 1), array('include_deleted' => 1, 'include_inactive' => 1)); @@ -3084,15 +3648,37 @@ * instances are deleted and purged. */ function testPurgeField() { + // Start recording hook invocations. + field_test_memorize(); + $field = reset($this->fields); // Delete the first instance. - $instance = field_info_instance($this->entity_type, $field['field_name'], 'bb_1'); + $bundle = reset($this->bundles); + $instance = field_info_instance($this->entity_type, $field['field_name'], $bundle); field_delete_instance($instance); + // Assert that hook_field_delete() was not called yet. + $mem = field_test_memorize(); + $this->assertEqual(count($mem), 0, 'No field hooks were called.'); + // Purge the data. field_purge_batch(10); + // Check hooks invocations. + // - hook_field_load() (multiple hook) should have been called once, for all + // entities in the bundle. + // - hook_field_delete() should have been called once for each entity in the + // bundle. + $actual_hooks = field_test_memorize(); + $hooks = array(); + $stubs = $this->_generateStubEntities($this->entity_type, $this->entities_by_bundles[$bundle], $field['field_name']); + $hooks['field_test_field_load'][] = $stubs; + foreach ($stubs as $stub) { + $hooks['field_test_field_delete'][] = $stub; + } + $this->checkHooksInvocations($hooks, $actual_hooks); + // Purge again to purge the instance. field_purge_batch(0); @@ -3101,12 +3687,27 @@ $this->assertTrue(isset($fields[$field['id']]) && !$fields[$field['id']]['deleted'], 'The field exists and is not deleted'); // Delete the second instance. - $instance = field_info_instance($this->entity_type, $field['field_name'], 'bb_2'); + $bundle = next($this->bundles); + $instance = field_info_instance($this->entity_type, $field['field_name'], $bundle); field_delete_instance($instance); + // Assert that hook_field_delete() was not called yet. + $mem = field_test_memorize(); + $this->assertEqual(count($mem), 0, 'No field hooks were called.'); + // Purge the data. field_purge_batch(10); + // Check hooks invocations (same as above, for the 2nd bundle). + $actual_hooks = field_test_memorize(); + $hooks = array(); + $stubs = $this->_generateStubEntities($this->entity_type, $this->entities_by_bundles[$bundle], $field['field_name']); + $hooks['field_test_field_load'][] = $stubs; + foreach ($stubs as $stub) { + $hooks['field_test_field_delete'][] = $stub; + } + $this->checkHooksInvocations($hooks, $actual_hooks); + // The field still exists, deleted. $fields = field_read_fields(array('id' => $field['id']), array('include_deleted' => 1)); $this->assertTrue(isset($fields[$field['id']]) && $fields[$field['id']]['deleted'], 'The field exists and is deleted'); @@ -3127,7 +3728,7 @@ public static function getInfo() { return array( 'name' => 'Entity properties', - 'description'=> 'Tests entity properties.', + 'description' => 'Tests entity properties.', 'group' => 'Entity API', ); } diff -Naur drupal-7.0/modules/field/tests/field_test.entity.inc drupal-7.66/modules/field/tests/field_test.entity.inc --- drupal-7.0/modules/field/tests/field_test.entity.inc 2010-11-20 20:57:01.000000000 +0100 +++ drupal-7.66/modules/field/tests/field_test.entity.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ array('label' => 'Test Bundle'))); $test_entity_modes = array( 'full' => array( @@ -24,7 +29,7 @@ return array( 'test_entity' => array( - 'name' => t('Test Entity'), + 'label' => t('Test Entity'), 'fieldable' => TRUE, 'field cache' => FALSE, 'base table' => 'test_entity', @@ -39,7 +44,7 @@ ), // This entity type doesn't get form handling for now... 'test_cacheable_entity' => array( - 'name' => t('Test Entity, cacheable'), + 'label' => t('Test Entity, cacheable'), 'fieldable' => TRUE, 'field cache' => TRUE, 'entity keys' => array( @@ -51,7 +56,7 @@ 'view modes' => $test_entity_modes, ), 'test_entity_bundle_key' => array( - 'name' => t('Test Entity with a bundle key.'), + 'label' => t('Test Entity with a bundle key.'), 'base table' => 'test_entity_bundle_key', 'fieldable' => TRUE, 'field cache' => FALSE, @@ -59,12 +64,12 @@ 'id' => 'ftid', 'bundle' => 'fttype', ), - 'bundles' => array('bundle1' => array('label' => 'Bundle1'), 'bundle2' => array('label' => 'Bundle2')), + 'bundles' => array('bundle1' => array('label' => 'Bundle1'), 'bundle2' => array('label' => 'Bundle2')) + $bundles, 'view modes' => $test_entity_modes, ), // In this case, the bundle key is not stored in the database. 'test_entity_bundle' => array( - 'name' => t('Test Entity with a specified bundle.'), + 'label' => t('Test Entity with a specified bundle.'), 'base table' => 'test_entity_bundle', 'fieldable' => TRUE, 'controller class' => 'TestEntityBundleController', @@ -73,12 +78,12 @@ 'id' => 'ftid', 'bundle' => 'fttype', ), - 'bundles' => array('test_entity_2' => array('label' => 'Test entity 2')), + 'bundles' => array('test_entity_2' => array('label' => 'Test entity 2')) + $bundles, 'view modes' => $test_entity_modes, ), // @see EntityPropertiesTestCase::testEntityLabel() 'test_entity_no_label' => array( - 'name' => t('Test entity without label'), + 'label' => t('Test entity without label'), 'fieldable' => TRUE, 'field cache' => FALSE, 'base table' => 'test_entity', @@ -91,7 +96,7 @@ 'view modes' => $test_entity_modes, ), 'test_entity_label' => array( - 'name' => t('Test entity label'), + 'label' => t('Test entity label'), 'fieldable' => TRUE, 'field cache' => FALSE, 'base table' => 'test_entity', @@ -105,7 +110,7 @@ 'view modes' => $test_entity_modes, ), 'test_entity_label_callback' => array( - 'name' => t('Test entity label callback'), + 'label' => t('Test entity label callback'), 'fieldable' => TRUE, 'field cache' => FALSE, 'base table' => 'test_entity', diff -Naur drupal-7.0/modules/field/tests/field_test.field.inc drupal-7.66/modules/field/tests/field_test.field.inc --- drupal-7.0/modules/field/tests/field_test.field.inc 2010-10-20 02:13:33.000000000 +0200 +++ drupal-7.66/modules/field/tests/field_test.field.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ array( 'label' => t('Shape'), 'description' => t('Another dummy field type.'), - 'settings' => array(), + 'settings' => array( + 'foreign_key_name' => 'shape', + ), 'instance_settings' => array(), 'default_widget' => 'test_field_widget', 'default_formatter' => 'field_test_default', @@ -59,6 +60,9 @@ * Implements hook_field_load(). */ function field_test_field_load($entity_type, $entities, $field, $instances, $langcode, &$items, $age) { + $args = func_get_args(); + field_test_memorize(__FUNCTION__, $args); + foreach ($items as $id => $item) { // To keep the test non-intrusive, only act for instances with the // test_hook_field_load setting explicitly set to TRUE. @@ -74,12 +78,39 @@ } /** + * Implements hook_field_insert(). + */ +function field_test_field_insert($entity_type, $entity, $field, $instance, $items) { + $args = func_get_args(); + field_test_memorize(__FUNCTION__, $args); +} + +/** + * Implements hook_field_update(). + */ +function field_test_field_update($entity_type, $entity, $field, $instance, $items) { + $args = func_get_args(); + field_test_memorize(__FUNCTION__, $args); +} + +/** + * Implements hook_field_delete(). + */ +function field_test_field_delete($entity_type, $entity, $field, $instance, $items) { + $args = func_get_args(); + field_test_memorize(__FUNCTION__, $args); +} + +/** * Implements hook_field_validate(). * * Possible error codes: * - 'field_test_invalid': The value is invalid. */ function field_test_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) { + $args = func_get_args(); + field_test_memorize(__FUNCTION__, $args); + foreach ($items as $delta => $item) { if ($item['value'] == -1) { $errors[$field['field_name']][$langcode][$delta][] = array( @@ -351,11 +382,13 @@ break; case 'field_test_multiple': - $array = array(); - foreach ($items as $delta => $item) { - $array[] = $delta . ':' . $item['value']; + if (!empty($items)) { + $array = array(); + foreach ($items as $delta => $item) { + $array[] = $delta . ':' . $item['value']; + } + $element[0] = array('#markup' => $settings['test_formatter_setting_multiple'] . '|' . implode('|', $array)); } - $element[0] = array('#markup' => $settings['test_formatter_setting_multiple'] . '|' . implode('|', $array)); break; } diff -Naur drupal-7.0/modules/field/tests/field_test.info drupal-7.66/modules/field/tests/field_test.info --- drupal-7.0/modules/field/tests/field_test.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/field/tests/field_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -;$Id: field_test.info,v 1.2 2010/12/20 19:59:41 webchick Exp $ name = "Field API Test" description = "Support module for the Field API tests." core = 7.x @@ -7,8 +6,7 @@ version = VERSION hidden = TRUE -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/field/tests/field_test.install drupal-7.66/modules/field/tests/field_test.install --- drupal-7.0/modules/field/tests/field_test.install 2010-09-11 08:03:11.000000000 +0200 +++ drupal-7.66/modules/field/tests/field_test.install 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ '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, @@ -80,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, @@ -133,6 +132,18 @@ ); } else { + $foreign_keys = array(); + // The 'foreign keys' key is not always used in tests. + if (!empty($field['settings']['foreign_key_name'])) { + $foreign_keys['foreign keys'] = array( + // This is a dummy foreign key definition, references a table that + // doesn't exist, but that's not a problem. + $field['settings']['foreign_key_name'] => array( + 'table' => $field['settings']['foreign_key_name'], + 'columns' => array($field['settings']['foreign_key_name'] => 'id'), + ), + ); + } return array( 'columns' => array( 'shape' => array( @@ -146,6 +157,6 @@ 'not null' => FALSE, ), ), - ); + ) + $foreign_keys; } } diff -Naur drupal-7.0/modules/field/tests/field_test.module drupal-7.66/modules/field/tests/field_test.module --- drupal-7.0/modules/field/tests/field_test.module 2010-12-07 06:09:58.000000000 +0100 +++ drupal-7.66/modules/field/tests/field_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ $entity) { - $result[$id] = array($langcode => hash('sha256', serialize(array($entity_type, $entity, $field['field_name'], $langcode, $items[$id])))); + // Entities, instances and items are assumed to be consistently grouped by + // language. To verify this we try to access all the passed data structures + // by entity id. If they are grouped correctly, one entity, one instance and + // one array of items should be available for each entity id. + $field_name = $instances[$id]['field_name']; + $result[$id] = array($langcode => hash('sha256', serialize(array($entity_type, $entity, $field_name, $langcode, $items[$id])))); } return $result; } @@ -179,30 +183,6 @@ } /** - * Memorize calls to hook_field_insert(). - */ -function field_test_field_insert($entity_type, $entity, $field, $instance, $items) { - $args = func_get_args(); - field_test_memorize(__FUNCTION__, $args); -} - -/** - * Memorize calls to hook_field_update(). - */ -function field_test_field_update($entity_type, $entity, $field, $instance, $items) { - $args = func_get_args(); - field_test_memorize(__FUNCTION__, $args); -} - -/** - * Memorize calls to hook_field_delete(). - */ -function field_test_field_delete($entity_type, $entity, $field, $instance, $items) { - $args = func_get_args(); - field_test_memorize(__FUNCTION__, $args); -} - -/** * Implements hook_entity_query_alter(). */ function field_test_entity_query_alter(&$query) { @@ -224,10 +204,7 @@ } /** - * Entity label callback. - * - * @param $entity - * The entity object. + * Implements callback_entity_info_label(). * * @return * The label of the entity prefixed with "label callback". @@ -243,4 +220,65 @@ 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']); + } +} + +/** + * Implements hook_field_widget_properties_alter(). + */ +function field_test_field_widget_properties_alter(&$widget, $context) { + // Make the alter_test_text field 42 characters for nodes and comments. + if (in_array($context['entity_type'], array('node', 'comment')) && ($context['field']['field_name'] == 'alter_test_text')) { + $widget['settings']['size'] = 42; + } +} + +/** + * Implements hook_field_widget_properties_ENTITY_TYPE_alter(). + */ +function field_test_field_widget_properties_user_alter(&$widget, $context) { + // Always use buttons for the alter_test_options field on user forms. + if ($context['field']['field_name'] == 'alter_test_options') { + $widget['type'] = 'options_buttons'; + } +} + +/** + * Implements hook_field_widget_form_alter(). + */ +function field_test_field_widget_form_alter(&$element, &$form_state, $context) { + switch ($context['field']['field_name']) { + case 'alter_test_text': + drupal_set_message('Field size: ' . $context['instance']['widget']['settings']['size']); + break; + + case 'alter_test_options': + drupal_set_message('Widget type: ' . $context['instance']['widget']['type']); + break; + } +} + +/** + * Implements hook_query_TAG_alter() for tag 'efq_table_prefixing_test'. + * + * @see EntityFieldQueryTestCase::testTablePrefixing() + */ +function field_test_query_efq_table_prefixing_test_alter(&$query) { + // Add an additional join onto the entity base table. This will cause an + // 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.0/modules/field/tests/field_test.storage.inc drupal-7.66/modules/field/tests/field_test.storage.inc --- drupal-7.0/modules/field/tests/field_test.storage.inc 2010-10-13 07:19:26.000000000 +0200 +++ drupal-7.66/modules/field/tests/field_test.storage.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ {$column} == $value; break; - case '!=': + case '<>': case '<': case '<=': case '>': case '>=': - eval('$match = $match && '. $row->{$column} . ' ' . $operator . ' '. $value); + eval('$match = $match && ' . $row->{$column} . ' ' . $operator . ' '. $value); break; case 'IN': $match = $match && in_array($row->{$column}, $value); diff -Naur drupal-7.0/modules/field/theme/field-rtl.css drupal-7.66/modules/field/theme/field-rtl.css --- drupal-7.0/modules/field/theme/field-rtl.css 2010-05-22 22:23:01.000000000 +0200 +++ drupal-7.66/modules/field/theme/field-rtl.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -/* $Id: field-rtl.css,v 1.2 2010/05/22 20:23:01 dries Exp $ */ form .field-multiple-table th.field-label { padding-right: 0; diff -Naur drupal-7.0/modules/field/theme/field.css drupal-7.66/modules/field/theme/field.css --- drupal-7.0/modules/field/theme/field.css 2010-05-22 22:23:01.000000000 +0200 +++ drupal-7.66/modules/field/theme/field.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -/* $Id: field.css,v 1.9 2010/05/22 20:23:01 dries Exp $ */ /* Field display */ .field .field-label { diff -Naur drupal-7.0/modules/field/theme/field.tpl.php drupal-7.66/modules/field/theme/field.tpl.php --- drupal-7.0/modules/field/theme/field.tpl.php 2010-03-26 18:14:45.000000000 +0100 +++ drupal-7.66/modules/field/theme/field.tpl.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,12 +1,13 @@ -
> - +
> +
>
> - $item) : ?> + $item): ?>
>
diff -Naur drupal-7.0/modules/field_ui/field_ui-rtl.css drupal-7.66/modules/field_ui/field_ui-rtl.css --- drupal-7.0/modules/field_ui/field_ui-rtl.css 2010-09-11 02:03:42.000000000 +0200 +++ drupal-7.66/modules/field_ui/field_ui-rtl.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,7 @@ -/* $Id: field_ui-rtl.css,v 1.3 2010/09/11 00:03:42 webchick Exp $ */ +/** + * @file + * Right-to-left specific stylesheet for the Field UI module. + */ /* 'Manage fields' overview */ table.field-ui-overview tr.add-new .label-input { diff -Naur drupal-7.0/modules/field_ui/field_ui.admin.inc drupal-7.66/modules/field_ui/field_ui.admin.inc --- drupal-7.0/modules/field_ui/field_ui.admin.inc 2010-12-15 05:13:48.000000000 +0100 +++ drupal-7.66/modules/field_ui/field_ui.admin.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ $type_bundles) { foreach ($type_bundles as $bundle => $bundle_instances) { foreach ($bundle_instances as $field_name => $instance) { $field = field_info_field($field_name); + + // Initialize the row if we encounter the field for the first time. + if (!isset($rows[$field_name])) { + $rows[$field_name]['class'] = $field['locked'] ? array('menu-disabled') : array(''); + $rows[$field_name]['data'][0] = $field['locked'] ? t('@field_name (Locked)', array('@field_name' => $field_name)) : $field_name; + $module_name = $field_types[$field['type']]['module']; + $rows[$field_name]['data'][1] = $field_types[$field['type']]['label'] . ' ' . t('(module: !module)', array('!module' => $modules[$module_name]->info['name'])); + } + + // Add the current instance. $admin_path = _field_ui_bundle_admin_path($entity_type, $bundle); - $rows[$field_name]['data'][0] = $field['locked'] ? t('@field_name (Locked)', array('@field_name' => $field_name)) : $field_name; - $rows[$field_name]['data'][1] = $field_types[$field['type']]['label']; $rows[$field_name]['data'][2][] = $admin_path ? l($bundles[$entity_type][$bundle]['label'], $admin_path . '/fields') : $bundles[$entity_type][$bundle]['label']; - $rows[$field_name]['class'] = $field['locked'] ? array('menu-disabled') : array(''); } } } @@ -42,7 +51,7 @@ } /** - * Helper function to display a message about inactive fields. + * Displays a message listing the inactive fields of a given bundle. */ function field_ui_inactive_message($entity_type, $bundle) { $inactive_instances = field_ui_inactive_instances($entity_type, $bundle); @@ -63,9 +72,9 @@ } /** - * Helper function: determines the rendering order of a tree array. + * Determines the rendering order of an array representing a tree. * - * This is intended as a callback for array_reduce(). + * Callback for array_reduce() within field_ui_table_pre_render(). */ function _field_ui_reduce_order($array, $a) { $array = !isset($array) ? array() : $array; @@ -82,8 +91,9 @@ /** * Returns the region to which a row in the 'Manage fields' screen belongs. * - * This function is used as a #row_callback in field_ui_field_overview_form(), - * and is called during field_ui_table_pre_render(). + * This function is used as a #region_callback in + * field_ui_field_overview_form(). It is called during + * field_ui_table_pre_render(). */ function field_ui_field_overview_row_region($row) { switch ($row['#row_type']) { @@ -100,8 +110,9 @@ /** * Returns the region to which a row in the 'Manage display' screen belongs. * - * This function is used as a #row_callback in field_ui_field_overview_form(), - * and is called during field_ui_table_pre_render(). + * This function is used as a #region_callback in + * field_ui_field_overview_form(), and is called during + * field_ui_table_pre_render(). */ function field_ui_display_overview_row_region($row) { switch ($row['#row_type']) { @@ -151,7 +162,8 @@ // Add tabledrag indentation to the first row cell. if ($depth = count($parents[$name])) { - $cell = current(element_children($row)); + $children = element_children($row); + $cell = current($children); $row[$cell]['#prefix'] = theme('indentation', array('size' => $depth)) . (isset($row[$cell]['#prefix']) ? $row[$cell]['#prefix'] : ''); } @@ -206,9 +218,9 @@ // Determine the colspan to use for region rows, by checking the number of // columns in the headers. - $colums_count = 0; + $columns_count = 0; foreach ($table['header'] as $header) { - $colums_count += (is_array($header) && isset($header['colspan']) ? $header['colspan'] : 1); + $columns_count += (is_array($header) && isset($header['colspan']) ? $header['colspan'] : 1); } // Render rows, region by region. @@ -221,7 +233,7 @@ 'class' => array('region-title', 'region-' . $region_name_class . '-title'), 'no_striping' => TRUE, 'data' => array( - array('data' => $region['title'], 'colspan' => $colums_count), + array('data' => $region['title'], 'colspan' => $columns_count), ), ); } @@ -231,7 +243,7 @@ 'class' => array('region-message', 'region-' . $region_name_class . '-message', $class), 'no_striping' => TRUE, 'data' => array( - array('data' => $region['message'], 'colspan' => $colums_count), + array('data' => $region['message'], 'colspan' => $columns_count), ), ); } @@ -245,12 +257,17 @@ $row += $element['#attributes']; } + // Render children as table cells. foreach (element_children($element) as $cell_key) { - $cell = array('data' => drupal_render($element[$cell_key])); - if (isset($element[$cell_key]['#cell_attributes'])) { - $cell += $element[$cell_key]['#cell_attributes']; + $child = &$element[$cell_key]; + // Do not render a cell for children of #type 'value'. + if (!(isset($child['#type']) && $child['#type'] == 'value')) { + $cell = array('data' => drupal_render($child)); + if (isset($child['#cell_attributes'])) { + $cell += $child['#cell_attributes']; + } + $row['data'][] = $cell; } - $row['data'][] = $cell; } $table['rows'][] = $row; } @@ -260,9 +277,13 @@ } /** - * Menu callback; listing of fields for a bundle. + * Form constructor for the 'Manage fields' form of a bundle. * * Allows fields and pseudo-fields to be re-ordered. + * + * @see field_ui_field_overview_form_validate() + * @see field_ui_field_overview_form_submit() + * @ingroup forms */ function field_ui_field_overview_form($form, &$form_state, $entity_type, $bundle) { $bundle = field_extract_bundle($entity_type, $bundle); @@ -296,8 +317,8 @@ t('Label'), t('Weight'), t('Parent'), - t('Name'), - t('Field'), + t('Machine name'), + t('Field type'), t('Widget'), array('data' => t('Operations'), 'colspan' => 2), ), @@ -478,16 +499,24 @@ ), ), 'field_name' => array( - '#type' => 'textfield', + '#type' => 'machine_name', '#title' => t('New field name'), '#title_display' => 'invisible', // This field should stay LTR even for RTL languages. '#field_prefix' => 'field_', '#field_suffix' => '‎', - '#attributes' => array('dir'=>'ltr'), - '#size' => 10, - '#description' => t('Field name (a-z, 0-9, _)'), + '#size' => 15, + '#description' => t('A unique machine-readable name containing letters, numbers, and underscores.'), + // 32 characters minus the 'field_' prefix. + '#maxlength' => 26, '#prefix' => '
 
', + '#machine_name' => array( + 'source' => array('fields', $name, 'label'), + 'exists' => '_field_ui_field_name_exists', + 'standalone' => TRUE, + 'label' => '', + ), + '#required' => FALSE, ), 'type' => array( '#type' => 'select', @@ -510,12 +539,29 @@ '#cell_attributes' => array('colspan' => 3), '#prefix' => '
 
', ), + // Place the 'translatable' property as an explicit value so that contrib + // modules can form_alter() the value for newly created fields. + 'translatable' => array( + '#type' => 'value', + '#value' => FALSE, + ), ); } // Additional row: add existing field. - $existing_field_options = field_ui_existing_field_options($entity_type, $bundle); - if ($existing_field_options && $widget_type_options) { + $existing_fields = field_ui_existing_field_options($entity_type, $bundle); + if ($existing_fields && $widget_type_options) { + // Build list of options. + $existing_field_options = array(); + foreach ($existing_fields as $field_name => $info) { + $text = t('@type: @field (@label)', array( + '@type' => $info['type_label'], + '@label' => $info['label'], + '@field' => $info['field'], + )); + $existing_field_options[$field_name] = truncate_utf8($text, 80, FALSE, TRUE); + } + asort($existing_field_options); $name = '_add_existing_field'; $table[$name] = array( '#attributes' => array('class' => array('draggable', 'tabledrag-leaf', 'add-new')), @@ -591,10 +637,8 @@ // Add settings for the update selects behavior. $js_fields = array(); - foreach ($existing_field_options as $field_name => $fields) { - $field = field_info_field($field_name); - $instance = field_info_instance($form['#entity_type'], $field_name, $form['#bundle']); - $js_fields[$field_name] = array('label' => $instance['label'], 'type' => $field['type'], 'widget' => $instance['widget']['type']); + foreach ($existing_fields as $field_name => $info) { + $js_fields[$field_name] = array('label' => $info['label'], 'type' => $info['type'], 'widget' => $info['widget_type']); } $form['#attached']['js'][] = array( @@ -610,7 +654,9 @@ } /** - * Validate handler for the field overview form. + * Form validation handler for field_ui_field_overview_form(). + * + * @see field_ui_field_overview_form_submit() */ function field_ui_field_overview_form_validate($form, &$form_state) { _field_ui_field_overview_form_validate_add_new($form, $form_state); @@ -618,9 +664,9 @@ } /** - * Helper function for field_ui_field_overview_form_validate. + * Validates the 'add new field' row of field_ui_field_overview_form(). * - * Validate the 'add new field' row. + * @see field_ui_field_overview_form_validate() */ function _field_ui_field_overview_form_validate_add_new($form, &$form_state) { $field = $form_state['values']['fields']['_add_new_field']; @@ -641,25 +687,8 @@ $field_name = $field['field_name']; // Add the 'field_' prefix. - if (substr($field_name, 0, 6) != 'field_') { - $field_name = 'field_' . $field_name; - form_set_value($form['fields']['_add_new_field']['field_name'], $field_name, $form_state); - } - - // Invalid field name. - if (!preg_match('!^field_[a-z0-9_]+$!', $field_name)) { - form_set_error('fields][_add_new_field][field_name', t('Add new field: the field name %field_name is invalid. The name must include only lowercase unaccentuated letters, numbers, and underscores.', array('%field_name' => $field_name))); - } - if (strlen($field_name) > 32) { - form_set_error('fields][_add_new_field][field_name', t("Add new field: the field name %field_name is too long. The name is limited to 32 characters, including the 'field_' prefix.", array('%field_name' => $field_name))); - } - - // Field name already exists. We need to check inactive fields as well, so - // we can't use field_info_fields(). - $fields = field_read_fields(array('field_name' => $field_name), array('include_inactive' => TRUE)); - if ($fields) { - form_set_error('fields][_add_new_field][field_name', t('Add new field: the field name %field_name already exists.', array('%field_name' => $field_name))); - } + $field_name = 'field_' . $field_name; + form_set_value($form['fields']['_add_new_field']['field_name'], $field_name, $form_state); } // Missing field type. @@ -682,9 +711,27 @@ } /** - * Helper function for field_ui_field_overview_form_validate. + * Render API callback: Checks if a field machine name is taken. + * + * @param $value + * The machine name, not prefixed with 'field_'. + * + * @return + * Whether or not the field machine name is taken. + */ +function _field_ui_field_name_exists($value) { + // Prefix with 'field_'. + $field_name = 'field_' . $value; + + // We need to check inactive fields as well, so we can't use + // field_info_fields(). + return (bool) field_read_fields(array('field_name' => $field_name), array('include_inactive' => TRUE)); +} + +/** + * Validates the 'add existing field' row of field_ui_field_overview_form(). * - * Validate the 'add existing field' row. + * @see field_ui_field_overview_form_validate() */ function _field_ui_field_overview_form_validate_add_existing($form, &$form_state) { // The form element might be absent if no existing fields can be added to @@ -720,7 +767,9 @@ } /** - * Submit handler for the field overview form. + * Form submission handler for field_ui_field_overview_form(). + * + * @see field_ui_field_overview_form_validate() */ function field_ui_field_overview_form_submit($form, &$form_state) { $form_values = $form_state['values']['fields']; @@ -754,7 +803,7 @@ $field = array( 'field_name' => $values['field_name'], 'type' => $values['type'], - 'translatable' => TRUE, + 'translatable' => $values['translatable'], ); $instance = array( 'field_name' => $field['field_name'], @@ -779,7 +828,7 @@ $form_state['fields_added']['_add_new_field'] = $field['field_name']; } catch (Exception $e) { - drupal_set_message(t('There was a problem creating field %label: @message.', array('%label' => $instance['label'], '@message' => $e->getMessage()))); + drupal_set_message(t('There was a problem creating field %label: !message', array('%label' => $instance['label'], '!message' => $e->getMessage())), 'error'); } } @@ -788,7 +837,7 @@ $values = $form_values['_add_existing_field']; $field = field_info_field($values['field_name']); if (!empty($field['locked'])) { - drupal_set_message(t('The field %label cannot be added because it is locked.', array('%label' => $values['label']))); + drupal_set_message(t('The field %label cannot be added because it is locked.', array('%label' => $values['label'])), 'error'); } else { $instance = array( @@ -809,7 +858,7 @@ $form_state['fields_added']['_add_existing_field'] = $instance['field_name']; } catch (Exception $e) { - drupal_set_message(t('There was a problem creating field instance %label: @message.', array('%label' => $instance['label'], '@message' => $e->getMessage()))); + drupal_set_message(t('There was a problem creating field instance %label: @message.', array('%label' => $instance['label'], '@message' => $e->getMessage())), 'error'); } } } @@ -826,7 +875,11 @@ } /** - * Menu callback; presents field display settings for a given view mode. + * Form constructor for the field display settings for a given view mode. + * + * @see field_ui_display_overview_multistep_submit() + * @see field_ui_display_overview_form_submit() + * @ingroup forms */ function field_ui_display_overview_form($form, &$form_state, $entity_type, $bundle, $view_mode) { $bundle = field_extract_bundle($entity_type, $bundle); @@ -875,7 +928,7 @@ 'class' => array('field-ui-overview'), 'id' => 'field-display-overview', ), - // Add AJAX wrapper. + // Add Ajax wrapper. '#prefix' => '
', '#suffix' => '
', ); @@ -883,7 +936,7 @@ $field_label_options = array( 'above' => t('Above'), 'inline' => t('Inline'), - 'hidden' => t(''), + 'hidden' => '<' . t('Hidden') . '>', ); $extra_visibility_options = array( 'visible' => t('Visible'), @@ -939,7 +992,7 @@ ); $formatter_options = field_ui_formatter_options($field['type']); - $formatter_options['hidden'] = t(''); + $formatter_options['hidden'] = '<' . t('Hidden') . '>'; $table[$name]['format'] = array( 'type' => array( '#type' => 'select', @@ -1157,7 +1210,7 @@ 'callback' => 'field_ui_display_overview_multistep_js', 'wrapper' => 'field-display-overview-wrapper', 'effect' => 'fade', - // The button stays hidden, so we hide the AJAX spinner too. Ad-hoc + // The button stays hidden, so we hide the Ajax spinner too. Ad-hoc // spinners will be added manually by the client-side script. 'progress' => 'none', ), @@ -1178,7 +1231,7 @@ /** - * Form submit handler for multistep buttons on the 'Manage display' screen. + * Form submission handler for buttons in field_ui_display_overview_form(). */ function field_ui_display_overview_multistep_submit($form, &$form_state) { $trigger = $form_state['triggering_element']; @@ -1218,13 +1271,13 @@ } /** - * AJAX handler for multistep buttons on the 'Manage display' screen. + * Ajax handler for multistep buttons on the 'Manage display' screen. */ function field_ui_display_overview_multistep_js($form, &$form_state) { $trigger = $form_state['triggering_element']; $op = $trigger['#op']; - // Pick the elements that need ro receive the ajax-new-content effect. + // Pick the elements that need to receive the ajax-new-content effect. switch ($op) { case 'edit': $updated_rows = array($trigger['#field_name']); @@ -1256,7 +1309,7 @@ } /** - * Submit handler for the display overview form. + * Form submission handler for field_ui_display_overview_form(). */ function field_ui_display_overview_form_submit($form, &$form_state) { $form_values = $form_state['values']; @@ -1334,7 +1387,7 @@ } /** - * Helper function for field_ui_display_overview_form_submit(). + * Populates display settings for a new view mode from the default view mode. * * When an administrator decides to use custom display settings for a view mode, * that view mode needs to be initialized with the display settings for the @@ -1343,8 +1396,6 @@ * them. It also modifies the passed-in $settings array, which the caller can * then save using field_bundle_settings(). * - * @see field_bundle_settings() - * * @param $entity_type * The bundle's entity type. * @param $bundle @@ -1354,6 +1405,9 @@ * @param $settings * An associative array of bundle settings, as expected by * field_bundle_settings(). + * + * @see field_ui_display_overview_form_submit(). + * @see field_bundle_settings() */ function _field_ui_add_default_view_mode_settings($entity_type, $bundle, $view_mode, &$settings) { // Update display settings for field instances. @@ -1378,7 +1432,7 @@ } /** - * Return an array of field_type options. + * Returns an array of field_type options. */ function field_ui_field_type_options() { $options = &drupal_static(__FUNCTION__); @@ -1400,7 +1454,7 @@ } /** - * Return an array of widget type options for a field type. + * Returns an array of widget type options for a field type. * * If no field type is provided, returns a nested array of all widget types, * keyed by field type human name. @@ -1436,7 +1490,7 @@ } /** - * Return an array of formatter options for a field type. + * Returns an array of formatter options for a field type. * * If no field type is provided, returns a nested array of all formatters, keyed * by field type. @@ -1464,10 +1518,10 @@ } /** - * Return an array of existing field to be added to a bundle. + * Returns an array of existing fields to be added to a bundle. */ function field_ui_existing_field_options($entity_type, $bundle) { - $options = array(); + $info = array(); $field_types = field_info_field_types(); foreach (field_info_instances() as $existing_entity_type => $bundles) { @@ -1480,29 +1534,32 @@ // - locked fields, // - fields already in the current bundle, // - fields that cannot be added to the entity type, - // - fields that that shoud not be added via user interface. + // - fields that should not be added via user interface. if (empty($field['locked']) && !field_info_instance($entity_type, $field['field_name'], $bundle) && (empty($field['entity_types']) || in_array($entity_type, $field['entity_types'])) && empty($field_types[$field['type']]['no_ui'])) { - $text = t('@type: @field (@label)', array( - '@type' => $field_types[$field['type']]['label'], - '@label' => t($instance['label']), '@field' => $instance['field_name'], - )); - $options[$instance['field_name']] = (drupal_strlen($text) > 80 ? truncate_utf8($text, 77) . '...' : $text); + $info[$instance['field_name']] = array( + 'type' => $field['type'], + 'type_label' => $field_types[$field['type']]['label'], + 'field' => $field['field_name'], + 'label' => $instance['label'], + 'widget_type' => $instance['widget']['type'], + ); } } } } } - // Sort the list by field name. - asort($options); - return $options; + return $info; } /** - * Menu callback; presents the field settings edit page. + * Form constructor for the field settings edit page. + * + * @see field_ui_field_settings_form_submit() + * @ingroup forms */ function field_ui_field_settings_form($form, &$form_state, $instance) { $bundle = $instance['bundle']; @@ -1556,7 +1613,7 @@ } /** - * Save a field's settings after editing. + * Form submission handler for field_ui_field_settings_form(). */ function field_ui_field_settings_form_submit($form, &$form_state) { $form_values = $form_state['values']; @@ -1577,15 +1634,21 @@ drupal_set_message(t('Updated field %label field settings.', array('%label' => $instance['label']))); $form_state['redirect'] = field_ui_next_destination($entity_type, $bundle); } - catch (FieldException $e) { + catch (Exception $e) { drupal_set_message(t('Attempt to update field %label failed: %message.', array('%label' => $instance['label'], '%message' => $e->getMessage())), 'error'); - // TODO: Where do we go from here? - $form_state['redirect'] = field_ui_next_destination($entity_type, $bundle); } } /** - * Menu callback; select a widget for the field. + * Form constructor for the widget selection form. + * + * Path: BUNDLE_ADMIN_PATH/fields/%field/widget-type, where BUNDLE_ADMIN_PATH is + * the path stored in the ['admin']['info'] property in the return value of + * hook_entity_info(). + * + * @see field_ui_menu() + * @see field_ui_widget_type_form_submit() + * @ingroup forms */ function field_ui_widget_type_form($form, &$form_state, $instance) { drupal_set_title($instance['label']); @@ -1629,7 +1692,7 @@ } /** - * Submit the change in widget type. + * Form submission handler for field_ui_widget_type_form(). */ function field_ui_widget_type_form_submit($form, &$form_state) { $form_values = $form_state['values']; @@ -1651,15 +1714,18 @@ field_update_instance($instance); drupal_set_message(t('Changed the widget for field %label.', array('%label' => $instance['label']))); } - catch (FieldException $e) { - drupal_set_message(t('There was a problem changing the widget for field %label.', array('%label' => $instance['label']))); + catch (Exception $e) { + drupal_set_message(t('There was a problem changing the widget for field %label.', array('%label' => $instance['label'])), 'error'); } $form_state['redirect'] = field_ui_next_destination($entity_type, $bundle); } /** - * Menu callback; present a form for removing a field instance from a bundle. + * Form constructor for removing a field instance from a bundle. + * + * @see field_ui_field_delete_form_submit() + * @ingroup forms */ function field_ui_field_delete_form($form, &$form_state, $instance) { $bundle = $instance['bundle']; @@ -1689,9 +1755,10 @@ } /** - * Removes a field instance from a bundle. + * Form submission handler for field_ui_field_delete_form(). * - * If the field has no more instances, it will be marked as deleted too. + * Removes a field instance from a bundle. If the field has no more instances, + * it will be marked as deleted too. */ function field_ui_field_delete_form_submit($form, &$form_state) { $form_values = $form_state['values']; @@ -1709,15 +1776,27 @@ drupal_set_message(t('The field %field has been deleted from the %type content type.', array('%field' => $instance['label'], '%type' => $bundle_label))); } else { - drupal_set_message(t('There was a problem removing the %field from the %type content type.', array('%field' => $instance['label'], '%type' => $bundle_label))); + drupal_set_message(t('There was a problem removing the %field from the %type content type.', array('%field' => $instance['label'], '%type' => $bundle_label)), 'error'); } $admin_path = _field_ui_bundle_admin_path($entity_type, $bundle); $form_state['redirect'] = field_ui_get_destinations(array($admin_path . '/fields')); + + // Fields are purged on cron. However field module prevents disabling modules + // when field types they provided are used in a field until it is fully + // purged. In the case that a field has minimal or no content, a single call + // to field_purge_batch() will remove it from the system. Call this with a + // low batch limit to avoid administrators having to wait for cron runs when + // removing instances that meet this criteria. + field_purge_batch(10); } /** - * Menu callback; presents the field instance edit page. + * Form constructor for the field instance settings form. + * + * @see field_ui_field_edit_form_validate() + * @see field_ui_field_edit_form_submit() + * @ingroup forms */ function field_ui_field_edit_form($form, &$form_state, $instance) { $bundle = $instance['bundle']; @@ -1793,7 +1872,7 @@ '#default_value' => !empty($instance['description']) ? $instance['description'] : '', '#rows' => 5, '#description' => t('Instructions to present to the user below this field on the editing form.
Allowed HTML tags: @tags', array('@tags' => _field_filter_xss_display_allowed_tags())), - '#weight' => 0, + '#weight' => -5, ); // Build the widget component of the instance. @@ -1898,7 +1977,7 @@ } /** - * Build default value fieldset. + * Builds the default value fieldset for a given field instance. */ function field_ui_default_value_widget($field, $instance, &$form, &$form_state) { $field_name = $field['field_name']; @@ -1921,13 +2000,15 @@ $instance['description'] = ''; // @todo Allow multiple values (requires more work on 'add more' JS handler). - $element += field_default_form(NULL, NULL, $field, $instance, LANGUAGE_NONE, $items, $element, $form_state, 0); + $element += field_default_form($instance['entity_type'], NULL, $field, $instance, LANGUAGE_NONE, $items, $element, $form_state, 0); return $element; } /** - * Form validation handler for field instance settings form. + * Form validation handler for field_ui_field_edit_form(). + * + * @see field_ui_field_edit_form_submit(). */ function field_ui_field_edit_form_validate($form, &$form_state) { // Take the incoming values as the $instance definition, so that the 'default @@ -1962,7 +2043,9 @@ } /** - * Form submit handler for field instance settings form. + * Form submission handler for field_ui_field_edit_form(). + * + * @see field_ui_field_edit_form_validate(). */ function field_ui_field_edit_form_submit($form, &$form_state) { $instance = $form_state['values']['instance']; @@ -1971,7 +2054,13 @@ // Update any field settings that have changed. $field_source = field_info_field($instance['field_name']); $field = array_merge($field_source, $field); - field_update_field($field); + try { + field_update_field($field); + } + catch (Exception $e) { + drupal_set_message(t('Attempt to update field %label failed: %message.', array('%label' => $instance['label'], '%message' => $e->getMessage())), 'error'); + return; + } // Handle the default value. if (isset($form['instance']['default_value_widget'])) { @@ -1996,7 +2085,9 @@ } /** - * Helper functions to handle multipage redirects. + * Extracts next redirect path from an array of multiple destinations. + * + * @see field_ui_next_destination() */ function field_ui_get_destinations($destinations) { $path = array_shift($destinations); @@ -2008,12 +2099,16 @@ } /** - * Return the next redirect path in a multipage sequence. + * Returns the next redirect path in a multipage sequence. */ function field_ui_next_destination($entity_type, $bundle) { $destinations = !empty($_REQUEST['destinations']) ? $_REQUEST['destinations'] : array(); if (!empty($destinations)) { unset($_REQUEST['destinations']); + } + // Remove any external URLs. + $destinations = array_diff($destinations, array_filter($destinations, 'url_is_external')); + if ($destinations) { return field_ui_get_destinations($destinations); } $admin_path = _field_ui_bundle_admin_path($entity_type, $bundle); diff -Naur drupal-7.0/modules/field_ui/field_ui.api.php drupal-7.66/modules/field_ui/field_ui.api.php --- drupal-7.0/modules/field_ui/field_ui.api.php 2010-11-12 04:10:38.000000000 +0100 +++ drupal-7.66/modules/field_ui/field_ui.api.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ t('Maximum length'), '#default_value' => $settings['max_length'], '#required' => FALSE, - '#element_validate' => array('_element_validate_integer_positive'), + '#element_validate' => array('element_validate_integer_positive'), '#description' => t('The maximum length of the field in characters. Leave blank for an unlimited size.'), ); return $form; @@ -83,7 +82,7 @@ t('No'), t('Yes'), ), - '#description' => t('Display the summary to allow the user to input a summary value. Hide the summary to automatically fill it with a trimmed portion from the main post. '), + '#description' => t('Display the summary to allow the user to input a summary value. Hide the summary to automatically fill it with a trimmed portion from the main post.'), '#default_value' => !empty($settings['display_summary']) ? $settings['display_summary'] : 0, ); } @@ -114,7 +113,7 @@ '#type' => 'textfield', '#title' => t('Size of textfield'), '#default_value' => $settings['size'], - '#element_validate' => array('_element_validate_integer_positive'), + '#element_validate' => array('element_validate_integer_positive'), '#required' => TRUE, ); } @@ -123,7 +122,7 @@ '#type' => 'textfield', '#title' => t('Rows'), '#default_value' => $settings['rows'], - '#element_validate' => array('_element_validate_integer_positive'), + '#element_validate' => array('element_validate_integer_positive'), '#required' => TRUE, ); } @@ -133,7 +132,10 @@ /** - * Returns form elements for a formatter's settings. + * Specify the form elements for a formatter's settings. + * + * This hook is only invoked if hook_field_formatter_settings_summary() + * returns a non-empty value. * * @param $field * The field structure being configured. @@ -161,7 +163,7 @@ '#type' => 'textfield', '#size' => 20, '#default_value' => $settings['trim_length'], - '#element_validate' => array('_element_validate_integer_positive'), + '#element_validate' => array('element_validate_integer_positive'), '#required' => TRUE, ); } @@ -171,7 +173,7 @@ } /** - * Returns a short summary for the current formatter settings of an instance. + * Return a short summary for the current formatter settings of an instance. * * If an empty result is returned, the formatter is assumed to have no * configurable settings, and no UI will be provided to display a settings @@ -201,5 +203,5 @@ } /** - * @} End of "ingroup field_ui_field_type" + * @} End of "addtogroup field_types". */ diff -Naur drupal-7.0/modules/field_ui/field_ui.css drupal-7.66/modules/field_ui/field_ui.css --- drupal-7.0/modules/field_ui/field_ui.css 2010-11-20 10:06:32.000000000 +0100 +++ drupal-7.66/modules/field_ui/field_ui.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,8 @@ -/* $Id: field_ui.css,v 1.6 2010/11/20 09:06:32 webchick Exp $ */ - +/** + * @file + * Stylesheet for the Field UI module. + */ + /* 'Manage fields' and 'Manage display' overviews */ table.field-ui-overview tr.add-new .label-input { float: left; /* LTR */ @@ -9,6 +12,10 @@ } table.field-ui-overview tr.add-new .description { margin-bottom: 0; + max-width: 250px; +} +table.field-ui-overview tr.add-new .form-type-machine-name .description { + white-space: normal; } table.field-ui-overview tr.add-new .add-new-placeholder { font-weight: bold; @@ -26,6 +33,10 @@ table.field-ui-overview tr.region-add-new-title { display: none; } +table.field-ui-overview tr.add-new td { + vertical-align: top; + white-space: nowrap; +} /* 'Manage display' overview */ #field-display-overview .field-formatter-summary-cell { diff -Naur drupal-7.0/modules/field_ui/field_ui.info drupal-7.66/modules/field_ui/field_ui.info --- drupal-7.0/modules/field_ui/field_ui.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/field_ui/field_ui.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: field_ui.info,v 1.5 2010/12/20 19:59:41 webchick Exp $ name = Field UI description = User interface for the Field API. package = Core @@ -7,8 +6,7 @@ dependencies[] = field files[] = field_ui.test -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/field_ui/field_ui.js drupal-7.66/modules/field_ui/field_ui.js --- drupal-7.0/modules/field_ui/field_ui.js 2010-11-21 09:50:49.000000000 +0100 +++ drupal-7.66/modules/field_ui/field_ui.js 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,8 @@ -// $Id: field_ui.js,v 1.8 2010/11/21 08:50:49 webchick Exp $ - +/** + * @file + * Attaches the behaviors for the Field UI module. + */ + (function($) { Drupal.behaviors.fieldUIFieldOverview = { @@ -25,7 +28,7 @@ // 'Field type' select updates its 'Widget' select. $('.field-type-select', table).each(function () { - this.targetSelect = $('.widget-type-select', $(this).parents('tr').eq(0)); + this.targetSelect = $('.widget-type-select', $(this).closest('tr')); $(this).bind('change keyup', function () { var selectedFieldType = this.options[this.selectedIndex].value; @@ -40,8 +43,13 @@ // 'Existing field' select updates its 'Widget' select and 'Label' textfield. $('.field-select', table).each(function () { - this.targetSelect = $('.widget-type-select', $(this).parents('tr').eq(0)); - this.targetTextfield = $('.label-textfield', $(this).parents('tr').eq(0)); + this.targetSelect = $('.widget-type-select', $(this).closest('tr')); + this.targetTextfield = $('.label-textfield', $(this).closest('tr')); + this.targetTextfield + .data('field_ui_edited', false) + .bind('keyup', function (e) { + $(this).data('field_ui_edited', $(this).val() != ''); + }); $(this).bind('change keyup', function (e, updateText) { var updateText = (typeof updateText == 'undefined' ? true : updateText); @@ -51,8 +59,10 @@ var options = (selectedFieldType && (selectedFieldType in widgetTypes) ? widgetTypes[selectedFieldType] : []); this.targetSelect.fieldUIPopulateOptions(options, selectedFieldWidget); - if (updateText) { - $(this.targetTextfield).attr('value', (selectedField in fields ? fields[selectedField].label : '')); + // Only overwrite the "Label" input if it has not been manually + // changed, or if it is empty. + if (updateText && !this.targetTextfield.data('field_ui_edited')) { + this.targetTextfield.val(selectedField in fields ? fields[selectedField].label : ''); } }); @@ -87,7 +97,7 @@ html += ''; }); - $(this).html(html).attr('disabled', disabled ? 'disabled' : ''); + $(this).html(html).attr('disabled', disabled ? 'disabled' : false); }); }; @@ -119,7 +129,7 @@ data.tableDrag = tableDrag; // Create the row handler, make it accessible from the DOM row element. - var rowHandler = eval('new rowHandlers.' + data.rowHandler + '(row, data);'); + var rowHandler = new rowHandlers[data.rowHandler](row, data); $(row).data('fieldUIRowHandler', rowHandler); } }); @@ -130,7 +140,7 @@ */ onChange: function () { var $trigger = $(this); - var row = $trigger.parents('tr:first').get(0); + var row = $trigger.closest('tr').get(0); var rowHandler = $(row).data('fieldUIRowHandler'); var refreshRows = {}; @@ -158,7 +168,7 @@ var dragObject = this; var row = dragObject.rowObject.element; var rowHandler = $(row).data('fieldUIRowHandler'); - if (rowHandler !== undefined) { + if (typeof rowHandler !== 'undefined') { var regionRow = $(row).prevAll('tr.region-message').get(0); var region = regionRow.className.replace(/([^ ]+[ ]+)*region-([^ ]+)-message([ ]+[^ ]+)*/, '$2'); @@ -167,7 +177,7 @@ refreshRows = rowHandler.regionChange(region); // Update the row region. rowHandler.region = region; - // AJAX-update the rows. + // Ajax-update the rows. Drupal.fieldUIOverview.AJAXRefreshRows(refreshRows); } } @@ -206,7 +216,7 @@ }, /** - * Triggers AJAX refresh of selected rows. + * Triggers Ajax refresh of selected rows. * * The 'format type' selects can trigger a series of changes in child rows. * The #ajax behavior is therefore not attached directly to the selects, but @@ -215,7 +225,7 @@ * @param rows * A hash object, whose keys are the names of the rows to refresh (they * will receive the 'ajax-new-content' effect on the server side), and - * whose values are the DOM element in the row that should get an AJAX + * whose values are the DOM element in the row that should get an Ajax * throbber. */ AJAXRefreshRows: function (rows) { @@ -234,7 +244,7 @@ .addClass('progress-disabled') .after($throbber); - // Fire the AJAX update. + // Fire the Ajax update. $('input[name=refresh_rows]').val(rowNames.join(' ')); $('input#edit-refresh').mousedown(); @@ -295,7 +305,7 @@ * @param region * The name of the new region for the row. * @return - * A hash object indicating which rows should be AJAX-updated as a result + * A hash object indicating which rows should be Ajax-updated as a result * of the change, in the format expected by * Drupal.displayOverview.AJAXRefreshRows(). */ @@ -309,7 +319,7 @@ if (currentValue == 'hidden') { // Restore the formatter back to the default formatter. Pseudo-fields do // not have default formatters, we just return to 'visible' for those. - var value = (this.defaultFormatter != undefined) ? this.defaultFormatter : 'visible'; + var value = (typeof this.defaultFormatter !== 'undefined') ? this.defaultFormatter : this.$formatSelect.find('option').val(); } break; diff -Naur drupal-7.0/modules/field_ui/field_ui.module drupal-7.66/modules/field_ui/field_ui.module --- drupal-7.0/modules/field_ui/field_ui.module 2010-11-21 08:28:39.000000000 +0100 +++ drupal-7.66/modules/field_ui/field_ui.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,9 +1,7 @@ ' . t('About') . ''; - $output .= '

' . t('The Field UI module provides an administrative user interface (UI) for attaching and managing fields. Fields can be defined at the content-type level for content items and comments, at the vocabulary level for taxonomy terms, and at the site level for user accounts. Other modules may also enable fields to be defined for their data. Field types (text, image, number, etc.) are defined by modules, and collected and managed by the Field module. For more information, see the online handbook entry for Field UI module.', array('@field' => url('admin/help/field'), '@field_ui' => 'http://drupal.org/handbook/modules/field-ui')) . '

'; + $output .= '

' . t('The Field UI module provides an administrative user interface (UI) for attaching and managing fields. Fields can be defined at the content-type level for content items and comments, at the vocabulary level for taxonomy terms, and at the site level for user accounts. Other modules may also enable fields to be defined for their data. Field types (text, image, number, etc.) are defined by modules, and collected and managed by the Field module. For more information, see the online handbook entry for Field UI module.', array('@field' => url('admin/help/field'), '@field_ui' => 'http://drupal.org/documentation/modules/field-ui')) . '

'; $output .= '

' . t('Uses') . '

'; $output .= '
'; $output .= '
' . t('Planning fields') . '
'; - $output .= '
' . t('There are several decisions you will need to make before definining a field for content, comments, etc.:') . '
'; + $output .= '
' . t('There are several decisions you will need to make before defining a field for content, comments, etc.:') . '
'; $output .= '
' . t('What the field will be called') . '
'; $output .= '
' . t('A field has a label (the name displayed in the user interface) and a machine name (the name used internally). The label can be changed after you create the field, if needed, but the machine name cannot be changed after you have created the field.') . ''; $output .= '
' . t('What type of data the field will store') . '
'; $output .= '
' . t('Each field can store one type of data (text, number, file, etc.). When you define a field, you choose a particular field type, which corresponds to the type of data you want to store. The field type cannot be changed after you have created the field.') . '
'; $output .= '
' . t('How the data will be input and displayed') . '
'; - $output .= '
' . t('Each field type has one or more available widgets associated with it; each widget provides a mechanism for data input when you are editing (text box, select list, file upload, etc.). Each field type also has one or more display options, which determine how the field is displayed to site visitors. The widget and display display options can be changed after you have created the field.') . '
'; + $output .= '
' . t('Each field type has one or more available widgets associated with it; each widget provides a mechanism for data input when you are editing (text box, select list, file upload, etc.). Each field type also has one or more display options, which determine how the field is displayed to site visitors. The widget and display options can be changed after you have created the field.') . '
'; $output .= '
' . t('How many values the field will store') . '
'; $output .= '
' . t('You can store one value, a specific maximum number of values, or an unlimited number of values in each field. For example, an employee identification number field might store a single number, whereas a phone number field might store multiple phone numbers. This setting can be changed after you have created the field, but if you reduce the maximum number of values, you may lose information.') . '
'; $output .= '
'; @@ -49,6 +47,17 @@ } /** + * Implements hook_field_attach_rename_bundle(). + */ +function field_ui_field_attach_rename_bundle($entity_type, $bundle_old, $bundle_new) { + // The Field UI relies on entity_get_info() to build menu items for entity + // field administration pages. Clear the entity info cache and ensure that + // the menu is rebuilt. + entity_info_cache_clear(); + menu_rebuild(); +} + +/** * Implements hook_menu(). */ function field_ui_menu() { @@ -97,9 +106,19 @@ $access = array_intersect_key($bundle_info['admin'], drupal_map_assoc(array('access callback', 'access arguments'))); $access += array( 'access callback' => 'user_access', - 'access arguments' => array('administer site configuration'), + 'access arguments' => array('administer fields'), ); + // Add the "administer fields" permission on top of the access + // restriction because the field UI should only be accessible to + // trusted users. + if ($access['access callback'] != 'user_access' || $access['access arguments'] != array('administer fields')) { + $access = array( + 'access callback' => 'field_ui_admin_access', + 'access arguments' => array($access['access callback'], $access['access arguments']), + ); + } + $items["$path/fields"] = array( 'title' => 'Manage fields', 'page callback' => 'drupal_get_form', @@ -203,6 +222,11 @@ * The position of $bundle_name in $map. * @param $map * The translated menu router path argument map. + * + * @return + * The field instance array. + * + * @ingroup field */ function field_ui_menu_load($field_name, $entity_type, $bundle_name, $bundle_pos, $map) { // Extract the actual bundle name from the translated argument map. @@ -234,7 +258,7 @@ * Menu title callback. */ function field_ui_menu_title($instance) { - return t($instance['label']); + return $instance['label']; } /** @@ -251,7 +275,8 @@ // part of _menu_check_access(). if ($visibility) { // Grab the variable 'access arguments' part. - $args = array_slice(func_get_args(), 4); + $all_args = func_get_args(); + $args = array_slice($all_args, 4); $callback = empty($access_callback) ? 0 : trim($access_callback); if (is_numeric($callback)) { return (bool) $callback; @@ -303,7 +328,7 @@ } /** - * Helper function to create the right administration path for a bundle. + * Determines the administration path for a bundle. */ function _field_ui_bundle_admin_path($entity_type, $bundle_name) { $bundles = field_info_bundles($entity_type); @@ -314,26 +339,33 @@ } /** - * Helper function to identify inactive fields within a bundle. + * Identifies inactive fields within a bundle. */ function field_ui_inactive_instances($entity_type, $bundle_name = NULL) { - if (!empty($bundle_name)) { - $inactive = array($bundle_name => array()); - $params = array('bundle' => $bundle_name); + $params = array('entity_type' => $entity_type); + + if (empty($bundle_name)) { + $active = field_info_instances($entity_type); + $inactive = array(); } else { - $inactive = array(); - $params = array(); + // Restrict to the specified bundle. For consistency with the case where + // $bundle_name is NULL, the $active and $inactive arrays are keyed by + // bundle name first. + $params['bundle'] = $bundle_name; + $active = array($bundle_name => field_info_instances($entity_type, $bundle_name)); + $inactive = array($bundle_name => array()); } - $params['entity_type'] = $entity_type; - $active_instances = field_info_instances($entity_type); + // Iterate on existing definitions, and spot those that do not appear in the + // $active list collected earlier. $all_instances = field_read_instances($params, array('include_inactive' => TRUE)); foreach ($all_instances as $instance) { - if (!isset($active_instances[$instance['bundle']][$instance['field_name']])) { + if (!isset($active[$instance['bundle']][$instance['field_name']])) { $inactive[$instance['bundle']][$instance['field_name']] = $instance; } } + if (!empty($bundle_name)) { return $inactive[$bundle_name]; } @@ -341,7 +373,12 @@ } /** - * Add a button Save and add fields to Create content type form. + * Implements hook_form_FORM_ID_alter(). + * + * Adds a button 'Save and add fields' to the 'Create content type' form. + * + * @see node_type_form() + * @see field_ui_form_node_type_form_submit() */ function field_ui_form_node_type_form_alter(&$form, $form_state) { // We want to display the button only on add page. @@ -356,11 +393,22 @@ } /** - * Redirect to manage fields form. + * Form submission handler for the 'Save and add fields' button. + * + * @see field_ui_form_node_type_form_alter() */ function field_ui_form_node_type_form_submit($form, &$form_state) { - if ($form_state['clicked_button']['#parents'][0] === 'save_continue') { + if ($form_state['triggering_element']['#parents'][0] === 'save_continue') { $form_state['redirect'] = _field_ui_bundle_admin_path('node', $form_state['values']['type']) .'/fields'; } } +/** + * Access callback to determine if a user is allowed to use the field UI. + * + * Only grant access if the user has both the "administer fields" permission and + * is granted access by the entity specific restrictions. + */ +function field_ui_admin_access($access_callback, $access_arguments) { + return user_access('administer fields') && call_user_func_array($access_callback, $access_arguments); +} diff -Naur drupal-7.0/modules/field_ui/field_ui.test drupal-7.66/modules/field_ui/field_ui.test --- drupal-7.0/modules/field_ui/field_ui.test 2010-10-27 20:29:17.000000000 +0200 +++ drupal-7.66/modules/field_ui/field_ui.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,26 +1,32 @@ 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. - $type_name = strtolower($this->randomName(8)) . '_' .'test'; + $type_name = strtolower($this->randomName(8)) . '_test'; $type = $this->drupalCreateContentType(array('name' => $type_name, 'type' => $type_name)); $this->type = $type->type; // Store a valid URL name, with hyphens instead of underscores. @@ -28,10 +34,10 @@ } /** - * Create a new field through the Field UI. + * Creates a new field through the Field UI. * * @param $bundle_path - * Path of the 'Manage fields' page for the bundle. + * Admin path of the bundle that the new field is to be attached to. * @param $initial_edit * $edit parameter for drupalPost() on the first step ('Manage fields' * screen). @@ -53,25 +59,25 @@ // First step : 'Add new field' on the 'Manage fields' page. $this->drupalPost("$bundle_path/fields", $initial_edit, t('Save')); - $this->assertRaw(t('These settings apply to the %label field everywhere it is used.', array('%label' => $label)), t('Field settings page was displayed.')); + $this->assertRaw(t('These settings apply to the %label field everywhere it is used.', array('%label' => $label)), 'Field settings page was displayed.'); // Second step : 'Field settings' form. $this->drupalPost(NULL, $field_edit, t('Save field settings')); - $this->assertRaw(t('Updated field %label field settings.', array('%label' => $label)), t('Redirected to instance and widget settings page.')); + $this->assertRaw(t('Updated field %label field settings.', array('%label' => $label)), 'Redirected to instance and widget settings page.'); // Third step : 'Instance settings' form. $this->drupalPost(NULL, $instance_edit, t('Save settings')); - $this->assertRaw(t('Saved %label configuration.', array('%label' => $label)), t('Redirected to "Manage fields" page.')); + $this->assertRaw(t('Saved %label configuration.', array('%label' => $label)), 'Redirected to "Manage fields" page.'); // Check that the field appears in the overview form. - $this->assertFieldByXPath('//table[@id="field-overview"]//td[1]', $label, t('Field was created and appears in the overview page.')); + $this->assertFieldByXPath('//table[@id="field-overview"]//td[1]', $label, 'Field was created and appears in the overview page.'); } /** - * Add an existing field through the Field UI. + * Adds an existing field through the Field UI. * * @param $bundle_path - * Path of the 'Manage fields' page for the bundle. + * Admin path of the bundle that the field is to be attached to. * @param $initial_edit * $edit parameter for drupalPost() on the first step ('Manage fields' * screen). @@ -92,17 +98,17 @@ // Second step : 'Instance settings' form. $this->drupalPost(NULL, $instance_edit, t('Save settings')); - $this->assertRaw(t('Saved %label configuration.', array('%label' => $label)), t('Redirected to "Manage fields" page.')); + $this->assertRaw(t('Saved %label configuration.', array('%label' => $label)), 'Redirected to "Manage fields" page.'); // Check that the field appears in the overview form. - $this->assertFieldByXPath('//table[@id="field-overview"]//td[1]', $label, t('Field was created and appears in the overview page.')); + $this->assertFieldByXPath('//table[@id="field-overview"]//td[1]', $label, 'Field was created and appears in the overview page.'); } /** - * Delete a field instance through the Field UI. + * Deletes a field instance through the Field UI. * * @param $bundle_path - * Path of the 'Manage fields' page for the bundle. + * Admin path of the bundle that the field instance is to be deleted from. * @param $field_name * The name of the field. * @param $label @@ -113,19 +119,19 @@ function fieldUIDeleteField($bundle_path, $field_name, $label, $bundle_label) { // Display confirmation form. $this->drupalGet("$bundle_path/fields/$field_name/delete"); - $this->assertRaw(t('Are you sure you want to delete the field %label', array('%label' => $label)), t('Delete confirmation was found.')); + $this->assertRaw(t('Are you sure you want to delete the field %label', array('%label' => $label)), 'Delete confirmation was found.'); // Submit confirmation form. $this->drupalPost(NULL, array(), t('Delete')); - $this->assertRaw(t('The field %label has been deleted from the %type content type.', array('%label' => $label, '%type' => $bundle_label)), t('Delete message was found.')); + $this->assertRaw(t('The field %label has been deleted from the %type content type.', array('%label' => $label, '%type' => $bundle_label)), 'Delete message was found.'); // Check that the field does not appear in the overview form. - $this->assertNoFieldByXPath('//table[@id="field-overview"]//span[@class="label-field"]', $label, t('Field does not appear in the overview page.')); + $this->assertNoFieldByXPath('//table[@id="field-overview"]//span[@class="label-field"]', $label, 'Field does not appear in the overview page.'); } } /** - * Field UI tests for the 'Manage fields' screen. + * Tests the functionality of the 'Manage fields' screen. */ class FieldUIManageFieldsTestCase extends FieldUITestCase { public static function getInfo() { @@ -146,7 +152,7 @@ } /** - * Main entry point for the field CRUD tests. + * Runs the field CRUD tests. * * In order to act on the same fields, and not create the fields over and over * again the following tests create, update and delete the same fields. @@ -159,32 +165,32 @@ } /** - * Test the manage fields page. + * Tests the manage fields page. */ function manageFieldsPage() { $this->drupalGet('admin/structure/types/manage/' . $this->hyphen_type . '/fields'); // Check all table columns. $table_headers = array( t('Label'), - t('Name'), - t('Field'), + t('Machine name'), + t('Field type'), t('Widget'), t('Operations'), ); foreach ($table_headers as $table_header) { // We check that the label appear in the table headings. - $this->assertRaw($table_header . '', t('%table_header table header was found.', array('%table_header' => $table_header))); + $this->assertRaw($table_header . '', format_string('%table_header table header was found.', array('%table_header' => $table_header))); } // "Add new field" and "Add existing field" aren't a table heading so just // test the text. foreach (array('Add new field', 'Add existing field') as $element) { - $this->assertText($element, t('"@element" was found.', array('@element' => $element))); + $this->assertText($element, format_string('"@element" was found.', array('@element' => $element))); } } /** - * Test adding a new field. + * Tests adding a new field. * * @todo Assert properties can bet set in the form and read back in $field and * $instances. @@ -202,11 +208,11 @@ // should also appear in the 'taxonomy term' entity. $vocabulary = taxonomy_vocabulary_load(1); $this->drupalGet('admin/structure/taxonomy/' . $vocabulary->machine_name . '/fields'); - $this->assertTrue($this->xpath('//select[@name="fields[_add_existing_field][field_name]"]//option[@value="' . $this->field_name . '"]'), t('Existing field was found in account settings.')); + $this->assertTrue($this->xpath('//select[@name="fields[_add_existing_field][field_name]"]//option[@value="' . $this->field_name . '"]'), 'Existing field was found in account settings.'); } /** - * Test editing an existing field. + * Tests editing an existing field. */ function updateField() { // Go to the field edit page. @@ -225,21 +231,21 @@ $this->assertFieldSettings($this->type, $this->field_name, $string); // Assert redirection back to the "manage fields" page. - $this->assertText(t('Saved @label configuration.', array('@label' => $this->field_label)), t('Redirected to "Manage fields" page.')); + $this->assertText(t('Saved @label configuration.', array('@label' => $this->field_label)), 'Redirected to "Manage fields" page.'); } /** - * Test adding an existing field in another content type. + * Tests adding an existing field in another content type. */ function addExistingField() { // Check "Add existing field" appears. $this->drupalGet('admin/structure/types/manage/page/fields'); - $this->assertRaw(t('Add existing field'), t('"Add existing field" was found.')); + $this->assertRaw(t('Add existing field'), '"Add existing field" was found.'); // Check that the list of options respects entity type restrictions on // fields. The 'comment' field is restricted to the 'comment' entity type // and should not appear in the list. - $this->assertFalse($this->xpath('//select[@id="edit-add-existing-field-field-name"]//option[@value="comment"]'), t('The list of options respects entity type restrictions.')); + $this->assertFalse($this->xpath('//select[@id="edit-add-existing-field-field-name"]//option[@value="comment"]'), 'The list of options respects entity type restrictions.'); // Add a new field based on an existing field. $edit = array( @@ -250,7 +256,7 @@ } /** - * Assert the field settings. + * Asserts field settings are as expected. * * @param $bundle * The bundle name for the instance. @@ -263,15 +269,15 @@ */ function assertFieldSettings($bundle, $field_name, $string = 'dummy test string', $entity_type = 'node') { // Reset the fields info. - _field_info_collate_fields(TRUE); + field_info_cache_clear(); // Assert field settings. $field = field_info_field($field_name); - $this->assertTrue($field['settings']['test_field_setting'] == $string, t('Field settings were found.')); + $this->assertTrue($field['settings']['test_field_setting'] == $string, 'Field settings were found.'); // Assert instance and widget settings. $instance = field_info_instance($entity_type, $field_name, $bundle); - $this->assertTrue($instance['settings']['test_instance_setting'] == $string, t('Field instance settings were found.')); - $this->assertTrue($instance['widget']['settings']['test_widget_setting'] == $string, t('Field widget settings were found.')); + $this->assertTrue($instance['settings']['test_instance_setting'] == $string, 'Field instance settings were found.'); + $this->assertTrue($instance['widget']['settings']['test_widget_setting'] == $string, 'Field widget settings were found.'); } /** @@ -297,31 +303,31 @@ $element_id = "edit-$field_name-$langcode-0-value"; $element_name = "{$field_name}[$langcode][0][value]"; $this->drupalGet($admin_path); - $this->assertFieldById($element_id, '', t('The default value widget was empty.')); + $this->assertFieldById($element_id, '', 'The default value widget was empty.'); // Check that invalid default values are rejected. $edit = array($element_name => '-1'); $this->drupalPost($admin_path, $edit, t('Save settings')); - $this->assertText("$field_name does not accept the value -1", t('Form vaildation failed.')); + $this->assertText("$field_name does not accept the value -1", 'Form vaildation failed.'); // Check that the default value is saved. $edit = array($element_name => '1'); $this->drupalPost($admin_path, $edit, t('Save settings')); - $this->assertText("Saved $field_name configuration", t('The form was successfully submitted.')); + $this->assertText("Saved $field_name configuration", 'The form was successfully submitted.'); $instance = field_info_instance('node', $field_name, $this->type); - $this->assertEqual($instance['default_value'], array(array('value' => 1)), t('The default value was correctly saved.')); + $this->assertEqual($instance['default_value'], array(array('value' => 1)), 'The default value was correctly saved.'); // Check that the default value shows up in the form $this->drupalGet($admin_path); - $this->assertFieldById($element_id, '1', t('The default value widget was displayed with the correct value.')); + $this->assertFieldById($element_id, '1', 'The default value widget was displayed with the correct value.'); // Check that the default value can be emptied. $edit = array($element_name => ''); $this->drupalPost(NULL, $edit, t('Save settings')); - $this->assertText("Saved $field_name configuration", t('The form was successfully submitted.')); + $this->assertText("Saved $field_name configuration", 'The form was successfully submitted.'); field_info_cache_clear(); $instance = field_info_instance('node', $field_name, $this->type); - $this->assertEqual($instance['default_value'], NULL, t('The default value was correctly saved.')); + $this->assertEqual($instance['default_value'], NULL, 'The default value was correctly saved.'); } /** @@ -332,12 +338,12 @@ $bundle_path1 = 'admin/structure/types/manage/' . $this->hyphen_type; $edit1 = array( 'fields[_add_new_field][label]' => $this->field_label, - 'fields[_add_new_field][field_name]' => $this->field_name, + 'fields[_add_new_field][field_name]' => $this->field_name_input, ); $this->fieldUIAddNewField($bundle_path1, $edit1); // Create an additional node type. - $type_name2 = strtolower($this->randomName(8)) . '_' .'test'; + $type_name2 = strtolower($this->randomName(8)) . '_test'; $type2 = $this->drupalCreateContentType(array('name' => $type_name2, 'type' => $type_name2)); $type_name2 = $type2->type; $hyphen_type2 = str_replace('_', '-', $type_name2); @@ -354,32 +360,32 @@ $this->fieldUIDeleteField($bundle_path1, $this->field_name, $this->field_label, $this->type); // Reset the fields info. - _field_info_collate_fields(TRUE); + field_info_cache_clear(); // Check that the field instance was deleted. - $this->assertNull(field_info_instance('node', $this->field_name, $this->type), t('Field instance was deleted.')); + $this->assertNull(field_info_instance('node', $this->field_name, $this->type), 'Field instance was deleted.'); // Check that the field was not deleted - $this->assertNotNull(field_info_field($this->field_name), t('Field was not deleted.')); + $this->assertNotNull(field_info_field($this->field_name), 'Field was not deleted.'); // Delete the second instance. $this->fieldUIDeleteField($bundle_path2, $this->field_name, $this->field_label, $type_name2); // Reset the fields info. - _field_info_collate_fields(TRUE); + field_info_cache_clear(); // Check that the field instance was deleted. - $this->assertNull(field_info_instance('node', $this->field_name, $type_name2), t('Field instance was deleted.')); + $this->assertNull(field_info_instance('node', $this->field_name, $type_name2), 'Field instance was deleted.'); // Check that the field was deleted too. - $this->assertNull(field_info_field($this->field_name), t('Field was deleted.')); + $this->assertNull(field_info_field($this->field_name), 'Field was deleted.'); } /** - * Test that Field UI respects the 'no_ui' option in hook_field_info(). + * Tests that Field UI respects the 'no_ui' option in hook_field_info(). */ function testHiddenFields() { $bundle_path = 'admin/structure/types/manage/' . $this->hyphen_type . '/fields/'; // Check that the field type is not available in the 'add new field' row. $this->drupalGet($bundle_path); - $this->assertFalse($this->xpath('//select[@id="edit-add-new-field-type"]//option[@value="hidden_test_field"]'), t("The 'add new field' select respects field types 'no_ui' property.")); + $this->assertFalse($this->xpath('//select[@id="edit-add-new-field-type"]//option[@value="hidden_test_field"]'), "The 'add new field' select respects field types 'no_ui' property."); // Create a field and an instance programmatically. $field_name = 'hidden_test_field'; @@ -389,26 +395,73 @@ 'bundle' => $this->type, 'entity_type' => 'node', 'label' => t('Hidden field'), - 'widget_type' => 'test_field_widget', + 'widget' => array('type' => 'test_field_widget'), ); field_create_instance($instance); - $this->assertTrue(field_read_instance('node', $field_name, $this->type), t('An instance of the field %field was created programmatically.', array('%field' => $field_name))); + $this->assertTrue(field_read_instance('node', $field_name, $this->type), format_string('An instance of the field %field was created programmatically.', array('%field' => $field_name))); // Check that the newly added instance appears on the 'Manage Fields' // screen. $this->drupalGet($bundle_path); - $this->assertFieldByXPath('//table[@id="field-overview"]//td[1]', $instance['label'], t('Field was created and appears in the overview page.')); + $this->assertFieldByXPath('//table[@id="field-overview"]//td[1]', $instance['label'], 'Field was created and appears in the overview page.'); // Check that the instance does not appear in the 'add existing field' row // on other bundles. $bundle_path = 'admin/structure/types/manage/article/fields/'; $this->drupalGet($bundle_path); - $this->assertFalse($this->xpath('//select[@id="edit-add-existing-field-field-name"]//option[@value=:field_name]', array(':field_name' => $field_name)), t("The 'add existing field' select respects field types 'no_ui' property.")); + $this->assertFalse($this->xpath('//select[@id="edit-add-existing-field-field-name"]//option[@value=:field_name]', array(':field_name' => $field_name)), "The 'add existing field' select respects field types 'no_ui' property."); + } + + /** + * Tests renaming a bundle. + */ + function testRenameBundle() { + $type2 = strtolower($this->randomName(8)) . '_' .'test'; + $hyphen_type2 = str_replace('_', '-', $type2); + + $options = array( + 'type' => $type2, + ); + $this->drupalPost('admin/structure/types/manage/' . $this->hyphen_type, $options, t('Save content type')); + + $this->drupalGet('admin/structure/types/manage/' . $hyphen_type2 . '/fields'); + } + + /** + * Tests that a duplicate field name is caught by validation. + */ + function testDuplicateFieldName() { + // field_tags already exists, so we're expecting an error when trying to + // create a new field with the same name. + $edit = array( + 'fields[_add_new_field][field_name]' => 'tags', + 'fields[_add_new_field][label]' => $this->randomName(), + 'fields[_add_new_field][type]' => 'taxonomy_term_reference', + 'fields[_add_new_field][widget_type]' => 'options_select', + ); + $url = 'admin/structure/types/manage/' . $this->hyphen_type . '/fields'; + $this->drupalPost($url, $edit, t('Save')); + + $this->assertText(t('The machine-readable name is already in use. It must be unique.')); + $this->assertUrl($url, array(), 'Stayed on the same page.'); + } + + /** + * Tests that external URLs in the 'destinations' query parameter are blocked. + */ + function testExternalDestinations() { + $path = 'admin/structure/types/manage/article/fields/field_tags/field-settings'; + $options = array( + 'query' => array('destinations' => array('http://example.com')), + ); + $this->drupalPost($path, NULL, t('Save field settings'), $options); + + $this->assertUrl('admin/structure/types/manage/article/fields', array(), 'Stayed on the same site.'); } } /** - * Field UI tests for the 'Manage display' screens. + * Tests the functionality of the 'Manage display' screens. */ class FieldUIManageDisplayTestCase extends FieldUITestCase { public static function getInfo() { @@ -424,7 +477,7 @@ } /** - * Test formatter formatter settings. + * Tests formatter settings. */ function testFormatterUI() { $manage_fields = 'admin/structure/types/manage/' . $this->hyphen_type; @@ -433,7 +486,7 @@ // Create a field, and a node with some data for the field. $edit = array( 'fields[_add_new_field][label]' => 'Test field', - 'fields[_add_new_field][field_name]' => 'field_test', + 'fields[_add_new_field][field_name]' => 'test', ); $this->fieldUIAddNewField($manage_fields, $edit); @@ -448,8 +501,8 @@ // Display the "Manage display" screen and check that the expected formatter is // selected. $this->drupalGet($manage_display); - $this->assertFieldByName('fields[field_test][type]', $format, t('The expected formatter is selected.')); - $this->assertText("$setting_name: $setting_value", t('The expected summary is displayed.')); + $this->assertFieldByName('fields[field_test][type]', $format, 'The expected formatter is selected.'); + $this->assertText("$setting_name: $setting_value", 'The expected summary is displayed.'); // Change the formatter and check that the summary is updated. $edit = array('fields[field_test][type]' => 'field_test_multiple', 'refresh_rows' => 'field_test'); @@ -458,8 +511,8 @@ $default_settings = field_info_formatter_settings($format); $setting_name = key($default_settings); $setting_value = $default_settings[$setting_name]; - $this->assertFieldByName('fields[field_test][type]', $format, t('The expected formatter is selected.')); - $this->assertText("$setting_name: $setting_value", t('The expected summary is displayed.')); + $this->assertFieldByName('fields[field_test][type]', $format, 'The expected formatter is selected.'); + $this->assertText("$setting_name: $setting_value", 'The expected summary is displayed.'); // Submit the form and check that the instance is updated. $this->drupalPost(NULL, array(), t('Save')); @@ -467,18 +520,18 @@ $instance = field_info_instance('node', 'field_test', $this->type); $current_format = $instance['display']['default']['type']; $current_setting_value = $instance['display']['default']['settings'][$setting_name]; - $this->assertEqual($current_format, $format, t('The formatter was updated.')); - $this->assertEqual($current_setting_value, $setting_value, t('The setting was updated.')); + $this->assertEqual($current_format, $format, 'The formatter was updated.'); + $this->assertEqual($current_setting_value, $setting_value, 'The setting was updated.'); } /** - * Test switching view modes to use custom or 'default' settings'. + * Tests switching view modes to use custom or 'default' settings'. */ function testViewModeCustom() { // Create a field, and a node with some data for the field. $edit = array( 'fields[_add_new_field][label]' => 'Test field', - 'fields[_add_new_field][field_name]' => 'field_test', + 'fields[_add_new_field][field_name]' => 'test', ); $this->fieldUIAddNewField('admin/structure/types/manage/' . $this->hyphen_type, $edit); // For this test, use a formatter setting value that is an integer unlikely @@ -500,8 +553,8 @@ // Check that the field is displayed with the default formatter in 'rss' // mode (uses 'default'), and hidden in 'teaser' mode (uses custom settings). - $this->assertNodeViewText($node, 'rss', $output['field_test_default'], t("The field is displayed as expected in view modes that use 'default' settings.")); - $this->assertNodeViewNoText($node, 'teaser', $value, t("The field is hidden in view modes that use custom settings.")); + $this->assertNodeViewText($node, 'rss', $output['field_test_default'], "The field is displayed as expected in view modes that use 'default' settings."); + $this->assertNodeViewNoText($node, 'teaser', $value, "The field is hidden in view modes that use custom settings."); // Change fomatter for 'default' mode, check that the field is displayed // accordingly in 'rss' mode. @@ -509,14 +562,14 @@ 'fields[field_test][type]' => 'field_test_with_prepare_view', ); $this->drupalPost('admin/structure/types/manage/' . $this->hyphen_type . '/display', $edit, t('Save')); - $this->assertNodeViewText($node, 'rss', $output['field_test_with_prepare_view'], t("The field is displayed as expected in view modes that use 'default' settings.")); + $this->assertNodeViewText($node, 'rss', $output['field_test_with_prepare_view'], "The field is displayed as expected in view modes that use 'default' settings."); // Specialize the 'rss' mode, check that the field is displayed the same. $edit = array( "view_modes_custom[rss]" => TRUE, ); $this->drupalPost('admin/structure/types/manage/' . $this->hyphen_type . '/display', $edit, t('Save')); - $this->assertNodeViewText($node, 'rss', $output['field_test_with_prepare_view'], t("The field is displayed as expected in newly specialized 'rss' mode.")); + $this->assertNodeViewText($node, 'rss', $output['field_test_with_prepare_view'], "The field is displayed as expected in newly specialized 'rss' mode."); // Set the field to 'hidden' in the view mode, check that the field is // hidden. @@ -524,7 +577,7 @@ 'fields[field_test][type]' => 'hidden', ); $this->drupalPost('admin/structure/types/manage/' . $this->hyphen_type . '/display/rss', $edit, t('Save')); - $this->assertNodeViewNoText($node, 'rss', $value, t("The field is hidden in 'rss' mode.")); + $this->assertNodeViewNoText($node, 'rss', $value, "The field is hidden in 'rss' mode."); // Set the view mode back to 'default', check that the field is displayed // accordingly. @@ -532,7 +585,7 @@ "view_modes_custom[rss]" => FALSE, ); $this->drupalPost('admin/structure/types/manage/' . $this->hyphen_type . '/display', $edit, t('Save')); - $this->assertNodeViewText($node, 'rss', $output['field_test_with_prepare_view'], t("The field is displayed as expected when 'rss' mode is set back to 'default' settings.")); + $this->assertNodeViewText($node, 'rss', $output['field_test_with_prepare_view'], "The field is displayed as expected when 'rss' mode is set back to 'default' settings."); // Specialize the view mode again. $edit = array( @@ -540,11 +593,11 @@ ); $this->drupalPost('admin/structure/types/manage/' . $this->hyphen_type . '/display', $edit, t('Save')); // Check that the previous settings for the view mode have been kept. - $this->assertNodeViewNoText($node, 'rss', $value, t("The previous settings are kept when 'rss' mode is specialized again.")); + $this->assertNodeViewNoText($node, 'rss', $value, "The previous settings are kept when 'rss' mode is specialized again."); } /** - * Pass if the text is found in the rendered node in a given view mode. + * Asserts that a string is found in the rendered node in a view mode. * * @param $node * The node. @@ -563,7 +616,7 @@ } /** - * Pass if the text is node found in the rendered node in a given view mode. + * Asserts that a string is not found in the rendered node in a view mode. * * @param $node * The node. @@ -581,7 +634,10 @@ } /** - * Helper for assertNodeViewText and assertNodeViewNoText. + * Asserts that a string is (not) found in the rendered nodein a view mode. + * + * This helper function is used by assertNodeViewText() and + * assertNodeViewNoText(). * * @param $node * The node. @@ -607,7 +663,8 @@ // Render a cloned node, so that we do not alter the original. $clone = clone $node; - $output = drupal_render(node_view($clone, $view_mode)); + $element = node_view($clone, $view_mode); + $output = drupal_render($element); $this->verbose(t('Rendered node - view mode: @view_mode', array('@view_mode' => $view_mode)) . '
'. $output); // Assign content so that DrupalWebTestCase functions can be used. @@ -621,3 +678,87 @@ return $return; } } + +/** + * Tests custom widget hooks and callbacks on the field administration pages. + */ +class FieldUIAlterTestCase extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'Widget customization', + 'description' => 'Test custom field widget hooks and callbacks on field administration pages.', + 'group' => 'Field UI', + ); + } + + function setUp() { + parent::setUp(array('field_test')); + + // Create test user. + $admin_user = $this->drupalCreateUser(array('access content', 'administer content types', 'administer users', 'administer fields')); + $this->drupalLogin($admin_user); + } + + /** + * Tests hook_field_widget_properties_alter() on the default field widget. + * + * @see field_test_field_widget_properties_alter() + * @see field_test_field_widget_properties_user_alter() + * @see field_test_field_widget_form_alter() + */ + function testDefaultWidgetPropertiesAlter() { + // Create the alter_test_text field and an instance on article nodes. + field_create_field(array( + 'field_name' => 'alter_test_text', + 'type' => 'text', + )); + field_create_instance(array( + 'field_name' => 'alter_test_text', + 'entity_type' => 'node', + 'bundle' => 'article', + 'widget' => array( + 'type' => 'text_textfield', + 'size' => 60, + ), + )); + + // Test that field_test_field_widget_properties_alter() sets the size to + // 42 and that field_test_field_widget_form_alter() reports the correct + // size when the form is displayed. + $this->drupalGet('admin/structure/types/manage/article/fields/alter_test_text'); + $this->assertText('Field size: 42', 'Altered field size is found in hook_field_widget_form_alter().'); + + // Create the alter_test_options field. + field_create_field(array( + 'field_name' => 'alter_test_options', + 'type' => 'list_text' + )); + // Create instances on users and page nodes. + field_create_instance(array( + 'field_name' => 'alter_test_options', + 'entity_type' => 'user', + 'bundle' => 'user', + 'widget' => array( + 'type' => 'options_select', + ) + )); + field_create_instance(array( + 'field_name' => 'alter_test_options', + 'entity_type' => 'node', + 'bundle' => 'page', + 'widget' => array( + 'type' => 'options_select', + ) + )); + + // Test that field_test_field_widget_properties_user_alter() replaces + // the widget and that field_test_field_widget_form_alter() reports the + // correct widget name when the form is displayed. + $this->drupalGet('admin/config/people/accounts/fields/alter_test_options'); + $this->assertText('Widget type: options_buttons', 'Widget type is altered for users in hook_field_widget_form_alter().'); + + // Test that the widget is not altered on page nodes. + $this->drupalGet('admin/structure/types/manage/page/fields/alter_test_options'); + $this->assertText('Widget type: options_select', 'Widget type is not altered for pages in hook_field_widget_form_alter().'); + } +} diff -Naur drupal-7.0/modules/file/file.api.php drupal-7.66/modules/file/file.api.php --- drupal-7.0/modules/file/file.api.php 2010-08-23 16:53:50.000000000 +0200 +++ drupal-7.66/modules/file/file.api.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ $grants['node']); diff -Naur drupal-7.0/modules/file/file.css drupal-7.66/modules/file/file.css --- drupal-7.0/modules/file/file.css 2009-08-29 14:52:32.000000000 +0200 +++ drupal-7.66/modules/file/file.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,7 @@ -/* $Id: file.css,v 1.1 2009/08/29 12:52:32 dries Exp $ */ +/** + * @file + * Admin stylesheet for file module. + */ /** * Managed file element styles. @@ -20,10 +23,6 @@ padding: 1px 5px 2px 5px; } -.form-managed-file div.ajax-progress div { - display: inline; -} - .form-managed-file div.ajax-progress-bar { display: none; margin-top: 4px; diff -Naur drupal-7.0/modules/file/file.field.inc drupal-7.66/modules/file/file.field.inc --- drupal-7.0/modules/file/file.field.inc 2010-12-24 16:25:28.000000000 +0100 +++ drupal-7.66/modules/file/file.field.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ t('Separate extensions with a space or comma and do not include the leading dot.'), '#element_validate' => array('_file_generic_settings_extensions'), '#weight' => 1, + '#maxlength' => 256, // By making this field required, we prevent a potential security issue // that would allow files of any type to be uploaded. '#required' => TRUE, @@ -187,7 +187,7 @@ $items[$id][$delta] = NULL; } else { - $items[$id][$delta] = array_merge($item, (array) $files[$item['fid']]); + $items[$id][$delta] = array_merge((array) $files[$item['fid']], $item); } } } @@ -203,9 +203,9 @@ if (!file_field_displayed($item, $field)) { unset($items[$id][$delta]); } - // Ensure consecutive deltas. - $items[$id] = array_values($items[$id]); } + // Ensure consecutive deltas. + $items[$id] = array_values($items[$id]); } } @@ -216,8 +216,16 @@ // Make sure that each file which will be saved with this object has a // permanent status, so that it will not be removed when temporary files are // cleaned up. - foreach ($items as $item) { + foreach ($items as $delta => $item) { + if (empty($item['fid'])) { + unset($items[$delta]); + continue; + } $file = file_load($item['fid']); + if (empty($file)) { + unset($items[$delta]); + continue; + } if (!$file->status) { $file->status = FILE_STATUS_PERMANENT; file_save($file); @@ -244,6 +252,12 @@ * Checks for files that have been removed from the object. */ function file_field_update($entity_type, $entity, $field, $instance, $langcode, &$items) { + // Check whether the field is defined on the object. + if (!isset($entity->{$field['field_name']})) { + // We cannot check for removed files if the field is not defined. + return; + } + list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity); // On new revisions, all files are considered to be a new usage and no @@ -262,11 +276,16 @@ $current_fids[] = $item['fid']; } - // Create a bare-bones entity so that we can load its previous values. - $original = entity_create_stub_entity($entity_type, array($id, $vid, $bundle)); - field_attach_load($entity_type, array($id => $original), FIELD_LOAD_CURRENT, array('field_id' => $field['id'])); - - // Compare the original field values with the ones that are being saved. + // Compare the original field values with the ones that are being saved. Use + // $entity->original to check this when possible, but if it isn't available, + // create a bare-bones entity and load its previous values instead. + if (isset($entity->original)) { + $original = $entity->original; + } + else { + $original = entity_create_stub_entity($entity_type, array($id, $vid, $bundle)); + field_attach_load($entity_type, array($id => $original), FIELD_LOAD_CURRENT, array('field_id' => $field['id'])); + } $original_fids = array(); if (!empty($original->{$field['field_name']}[$langcode])) { foreach ($original->{$field['field_name']}[$langcode] as $original_item) { @@ -313,7 +332,7 @@ } /** - * Decrements a file usage count and attempts to delete it. + * Decrements the usage count for a file and attempts to delete it. * * This function only has an effect if the file being deleted is used only by * File module. @@ -359,12 +378,13 @@ } /** - * Determine whether a file should be displayed when outputting field content. + * Determines whether a file should be displayed when outputting field content. * * @param $item * A field item array. * @param $field * A field array. + * * @return * Boolean TRUE if the file will be displayed, FALSE if the file is hidden. */ @@ -429,7 +449,7 @@ 'bar' => t('Bar with progress meter'), ), '#default_value' => $settings['progress_indicator'], - '#description' => t('The throbber display does not show the status of uploads but takes up space. The progress bar is helpful for monitoring progress on large uploads.'), + '#description' => t('The throbber display does not show the status of uploads but takes up less space. The progress bar is helpful for monitoring progress on large uploads.'), '#weight' => 16, '#access' => file_progress_implementation(), ); @@ -448,46 +468,30 @@ 'description' => '', ); - // Retrieve any values set in $form_state, as will be the case during AJAX - // rebuilds of this form. - if (isset($form_state['values'])) { - $path = array_merge($element['#field_parents'], array($field['field_name'], $langcode)); - $path_exists = FALSE; - $values = drupal_array_get_nested_value($form_state['values'], $path, $path_exists); - if ($path_exists) { - $items = $values; - drupal_array_set_nested_value($form_state['values'], $path, NULL); - } + // Load the items for form rebuilds from the field state as they might not be + // in $form_state['values'] because of validation limitations. Also, they are + // only passed in as $items when editing existing entities. + $field_state = field_form_get_state($element['#field_parents'], $field['field_name'], $langcode, $form_state); + if (isset($field_state['items'])) { + $items = $field_state['items']; } - foreach ($items as $delta => $item) { - $items[$delta] = array_merge($defaults, $items[$delta]); - // Remove any items from being displayed that are not needed. - if ($items[$delta]['fid'] == 0) { - unset($items[$delta]); - } - } - - // Re-index deltas after removing empty items. - $items = array_values($items); - - // Update order according to weight. - $items = _field_sort_items($field, $items); - // Essentially we use the managed_file type, extended with some enhancements. $element_info = element_info('managed_file'); $element += array( '#type' => 'managed_file', - '#default_value' => isset($items[$delta]) ? $items[$delta] : $defaults, '#upload_location' => file_field_widget_uri($field, $instance), '#upload_validators' => file_field_widget_upload_validators($field, $instance), '#value_callback' => 'file_field_widget_value', '#process' => array_merge($element_info['#process'], array('file_field_widget_process')), + '#progress_indicator' => $instance['widget']['settings']['progress_indicator'], // Allows this field to return an array instead of a single value. '#extended' => TRUE, ); if ($field['cardinality'] == 1) { + // Set the default value. + $element['#default_value'] = !empty($items) ? $items[0] : $defaults; // If there's only one field, return it as delta 0. if (empty($element['#default_value']['fid'])) { $element['#description'] = theme('file_upload_help', array('description' => $element['#description'], 'upload_validators' => $element['#upload_validators'])); @@ -496,15 +500,15 @@ } else { // If there are multiple values, add an element for each existing one. - $delta = -1; - foreach ($items as $delta => $item) { + foreach ($items as $item) { $elements[$delta] = $element; $elements[$delta]['#default_value'] = $item; $elements[$delta]['#weight'] = $delta; + $delta++; } - // And then add one more empty row for new uploads. - $delta++; - if ($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED || $delta < $field['cardinality']) { + // And then add one more empty row for new uploads except when this is a + // programmed form as it is not necessary. + if (($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED || $delta < $field['cardinality']) && empty($form_state['programmed'])) { $elements[$delta] = $element; $elements[$delta]['#default_value'] = $defaults; $elements[$delta]['#weight'] = $delta; @@ -533,10 +537,11 @@ } /** - * Get the upload validators for a file field. + * Retrieves the upload validators for a file field. * * @param $field * A field array. + * * @return * An array suitable for passing to file_save_upload() or the file field * element's '#upload_validators' property. @@ -562,20 +567,24 @@ } /** - * Determine the URI for a file field instance. + * Determines the URI for a file field instance. * * @param $field * A field array. * @param $instance * A field instance array. + * @param $data + * An array of token objects to pass to token_replace(). + * * @return * A file directory URI with tokens replaced. + * + * @see token_replace() */ -function file_field_widget_uri($field, $instance, $account = NULL) { +function file_field_widget_uri($field, $instance, $data = array()) { $destination = trim($instance['settings']['file_directory'], '/'); // Replace tokens. - $data = array('user' => isset($account) ? $account : $GLOBALS['user']); $destination = token_replace($destination, $data); return $field['settings']['uri_scheme'] . '://' . $destination; @@ -590,7 +599,7 @@ // If the display field is present make sure its unchecked value is saved. $field = field_widget_field($element, $form_state); if (empty($input['display'])) { - $input['display'] = $field['settings']['display_field'] ? 0 : 1; + $input['display'] = !empty($field['settings']['display_field']) ? 0 : 1; } } @@ -623,7 +632,7 @@ $element['#theme'] = 'file_widget'; // Add the display field if enabled. - if (!empty($field['settings']['display_field']) && $item['fid']) { + if (!empty($field['settings']['display_field'])) { $element['display'] = array( '#type' => empty($item['fid']) ? 'hidden' : 'checkbox', '#title' => t('Include file in display'), @@ -641,16 +650,15 @@ // Add the description field if enabled. if (!empty($instance['settings']['description_field']) && $item['fid']) { $element['description'] = array( - '#type' => 'textfield', + '#type' => variable_get('file_description_type', 'textfield'), '#title' => t('Description'), '#value' => isset($item['description']) ? $item['description'] : '', - '#type' => variable_get('file_description_type', 'textfield'), '#maxlength' => variable_get('file_description_length', 128), '#description' => t('The description may be used as the label of the link to the file.'), ); } - // Adjust the AJAX settings so that on upload and remove of any individual + // Adjust the Ajax settings so that on upload and remove of any individual // file, the entire group of file fields is updated together. if ($field['cardinality'] != 1) { $parents = array_slice($element['#array_parents'], 0, -1); @@ -681,7 +689,7 @@ /** * An element #process callback for a group of file_generic fields. * - * Adds the weight field to each row so it can be ordered and adds a new AJAX + * Adds the weight field to each row so it can be ordered and adds a new Ajax * wrapper around the entire group so it can be replaced all at once. */ function file_field_widget_process_multiple($element, &$form_state, $form) { @@ -710,7 +718,7 @@ } } - // Add a new wrapper around all the elements for AJAX replacement. + // Add a new wrapper around all the elements for Ajax replacement. $element['#prefix'] = '
'; $element['#suffix'] = '
'; @@ -718,10 +726,13 @@ } /** - * Helper function for file_field_widget_process_multiple(). + * Retrieves the file description from a field field element. + * + * This helper function is used by file_field_widget_process_multiple(). * * @param $element * The element being processed. + * * @return * A description of the file suitable for use in the administrative interface. */ @@ -739,7 +750,7 @@ } /** - * Submit handler for upload and remove buttons of file_generic fields. + * Form submission handler for upload/remove button of file_field_widget_form(). * * This runs in addition to and after file_managed_file_submit(). * @@ -755,6 +766,32 @@ // so nothing is lost in doing this. $parents = array_slice($form_state['triggering_element']['#parents'], 0, -2); drupal_array_set_nested_value($form_state['input'], $parents, NULL); + + $button = $form_state['triggering_element']; + + // Go one level up in the form, to the widgets container. + $element = drupal_array_get_nested_value($form, array_slice($button['#array_parents'], 0, -1)); + $field_name = $element['#field_name']; + $langcode = $element['#language']; + $parents = $element['#field_parents']; + + $submitted_values = drupal_array_get_nested_value($form_state['values'], array_slice($button['#parents'], 0, -2)); + foreach ($submitted_values as $delta => $submitted_value) { + if (!$submitted_value['fid']) { + unset($submitted_values[$delta]); + } + } + + // Re-index deltas after removing empty items. + $submitted_values = array_values($submitted_values); + + // Update form_state values. + drupal_array_set_nested_value($form_state['values'], array_slice($button['#parents'], 0, -2), $submitted_values); + + // Update items. + $field_state = field_form_get_state($parents, $field_name, $langcode, $form_state); + $field_state['items'] = $submitted_values; + field_form_set_state($parents, $field_name, $langcode, $form_state, $field_state); } /** @@ -770,7 +807,7 @@ $element = $variables['element']; $output = ''; - // The "form-managed-file" class is required for proper AJAX functionality. + // The "form-managed-file" class is required for proper Ajax functionality. $output .= '
'; if ($element['fid']['#value'] != 0) { // Add the file size after the file name. @@ -958,11 +995,13 @@ break; case 'file_table': - // Display all values in a single element.. - $element[0] = array( - '#theme' => 'file_formatter_table', - '#items' => $items, - ); + if (!empty($items)) { + // Display all values in a single element.. + $element[0] = array( + '#theme' => 'file_formatter_table', + '#items' => $items, + ); + } break; } diff -Naur drupal-7.0/modules/file/file.info drupal-7.66/modules/file/file.info --- drupal-7.0/modules/file/file.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/file/file.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: file.info,v 1.3 2010/12/20 19:59:41 webchick Exp $ name = File description = Defines a file field type. package = Core @@ -7,8 +6,7 @@ dependencies[] = field files[] = tests/file.test -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/file/file.install drupal-7.66/modules/file/file.install --- drupal-7.0/modules/file/file.install 2010-11-21 10:24:41.000000000 +0100 +++ drupal-7.66/modules/file/file.install 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ ' + error + '
'); + $(this).closest('div.form-managed-file').prepend('
' + error + '
'); this.value = ''; return false; } @@ -90,29 +95,29 @@ disableFields: function (event){ var clickedButton = this; - // Only disable upload fields for AJAX buttons. + // Only disable upload fields for Ajax buttons. if (!$(clickedButton).hasClass('ajax-processed')) { return; } // Check if we're working with an "Upload" button. var $enabledFields = []; - if ($(this).parents('div.form-managed-file').size() > 0) { - $enabledFields = $(this).parents('div.form-managed-file').find('input.form-file'); + if ($(this).closest('div.form-managed-file').length > 0) { + $enabledFields = $(this).closest('div.form-managed-file').find('input.form-file'); } // Temporarily disable upload fields other than the one we're currently // working with. Filter out fields that are already disabled so that they // do not get enabled when we re-enable these fields at the end of behavior // processing. Re-enable in a setTimeout set to a relatively short amount - // of time (1 second). All the other mousedown handlers (like Drupal's AJAX + // of time (1 second). All the other mousedown handlers (like Drupal's Ajax // behaviors) are excuted before any timeout functions are called, so we // don't have to worry about the fields being re-enabled too soon. // @todo If the previous sentence is true, why not set the timeout to 0? var $fieldsToTemporarilyDisable = $('div.form-managed-file input.form-file').not($enabledFields).not(':disabled'); $fieldsToTemporarilyDisable.attr('disabled', 'disabled'); setTimeout(function (){ - $fieldsToTemporarilyDisable.attr('disabled', ''); + $fieldsToTemporarilyDisable.attr('disabled', false); }, 1000); }, /** @@ -120,8 +125,8 @@ */ progressBar: function (event) { var clickedButton = this; - var $progressId = $(clickedButton).parents('div.form-managed-file').find('input.file-progress'); - if ($progressId.size()) { + var $progressId = $(clickedButton).closest('div.form-managed-file').find('input.file-progress'); + if ($progressId.length) { var originalName = $progressId.attr('name'); // Replace the name with the required identifier. @@ -134,7 +139,7 @@ } // Show the progress bar if the upload takes longer than half a second. setTimeout(function () { - $(clickedButton).parents('div.form-managed-file').find('div.ajax-progress-bar').slideDown(); + $(clickedButton).closest('div.form-managed-file').find('div.ajax-progress-bar').slideDown(); }, 500); }, /** diff -Naur drupal-7.0/modules/file/file.module drupal-7.66/modules/file/file.module --- drupal-7.0/modules/file/file.module 2010-12-29 05:35:23.000000000 +0100 +++ drupal-7.66/modules/file/file.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ ' . t('About') . ''; - $output .= '

' . t('The File module defines a File field type for the Field module, which lets you manage and validate uploaded files attached to content on your site (see the Field module help page for more information about fields). For more information, see the online handbook entry for File module.', array('@field-help' => url('admin/help/field'), '@file' => 'http://drupal.org/handbook/modules/file')) . '

'; + $output .= '

' . t('The File module defines a File field type for the Field module, which lets you manage and validate uploaded files attached to content on your site (see the Field module help page for more information about fields). For more information, see the online handbook entry for File module.', array('@field-help' => url('admin/help/field'), '@file' => 'http://drupal.org/documentation/modules/file')) . '

'; $output .= '

' . t('Uses') . '

'; $output .= '
'; $output .= '
' . t('Attaching files to content') . '
'; @@ -46,7 +45,6 @@ ); $items['file/progress'] = array( 'page callback' => 'file_ajax_progress', - 'delivery callback' => 'ajax_deliver', 'access arguments' => array('access content'), 'theme callback' => 'ajax_base_page_theme', 'type' => MENU_CALLBACK, @@ -58,7 +56,7 @@ /** * Implements hook_element_info(). * - * The managed file element may be used independently anywhere in Drupal. + * The managed file element may be used anywhere in Drupal. */ function file_element_info() { $file_path = drupal_get_path('module', 'file'); @@ -74,6 +72,7 @@ '#progress_message' => NULL, '#upload_validators' => array(), '#upload_location' => NULL, + '#size' => 22, '#extended' => FALSE, '#attached' => array( 'css' => array($file_path . '/file.css'), @@ -93,7 +92,7 @@ 'variables' => array('file' => NULL, 'icon_directory' => NULL), ), 'file_icon' => array( - 'variables' => array('file' => NULL, 'icon_directory' => NULL), + 'variables' => array('file' => NULL, 'icon_directory' => NULL, 'alt' => ''), ), 'file_managed_file' => array( 'render element' => 'element', @@ -141,12 +140,16 @@ } // Find out which (if any) fields of this type contain the file. - $references = file_get_file_references($file, NULL, FIELD_LOAD_CURRENT, $field_type); + $references = file_get_file_references($file, NULL, FIELD_LOAD_CURRENT, $field_type, FALSE); - // If there are no references, stop processing, to avoid returning headers - // for files controlled by other modules. - if (empty($references)) { - return; + // Stop processing if there are no references in order to avoid returning + // headers for files controlled by other modules. Make an exception for + // temporary files where the host entity has not yet been saved (for example, + // an image preview on a node/add form) in which case, allow download by the + // file's owner. For anonymous file owners, only the browser session that + // uploaded the file should be granted access. + if (empty($references) && ($file->status == FILE_STATUS_PERMANENT || $file->uid != $user->uid || (!$user->uid && empty($_SESSION['anonymous_allowed_file_ids'][$file->fid])))) { + return; } // Default to allow access. @@ -161,25 +164,29 @@ foreach ($field_references as $entity_type => $type_references) { foreach ($type_references as $id => $reference) { // Try to load $entity and $field. - $entity = reset(entity_load($entity_type, array($id))); - $field = NULL; + $entity = entity_load($entity_type, array($id)); + $entity = reset($entity); + $field = field_info_field($field_name); + + // Load the field item that references the file. + $field_item = NULL; if ($entity) { - // Load all fields for that entity. + // Load all field items for that entity. $field_items = field_get_items($entity_type, $entity, $field_name); // Find the field item with the matching URI. - foreach ($field_items as $field_item) { - if ($field_item['uri'] == $uri) { - $field = $field_item; + foreach ($field_items as $item) { + if ($item['uri'] == $uri) { + $field_item = $item; break; } } } - // Check that $entity and $field were loaded successfully and check if - // access to that field is not disallowed. If any of these checks fail, - // stop checking access for this reference. - if (empty($entity) || empty($field) || !field_access('view', $field, $entity_type, $entity)) { + // Check that $entity, $field and $field_item were loaded successfully + // and check if access to that field is not disallowed. If any of these + // checks fail, stop checking access for this reference. + if (empty($entity) || empty($field) || empty($field_item) || !field_access('view', $field, $entity_type, $entity)) { $denied = TRUE; break; } @@ -188,10 +195,10 @@ // Default to FALSE and let entities overrule this ruling. $grants = array('system' => FALSE); foreach (module_implements('file_download_access') as $module) { - $grants = array_merge($grants, array($module => module_invoke($module, 'file_download_access', $field, $entity_type, $entity))); + $grants = array_merge($grants, array($module => module_invoke($module, 'file_download_access', $field_item, $entity_type, $entity))); } // Allow other modules to alter the returned grants/denies. - drupal_alter('file_download_access', $grants, $field, $entity_type, $entity); + drupal_alter('file_download_access', $grants, $field_item, $entity_type, $entity); if (in_array(TRUE, $grants)) { // If TRUE is returned, access is granted and no further checks are @@ -221,7 +228,7 @@ } /** - * Menu callback; Shared AJAX callback for file uploads and deletions. + * Menu callback; Shared Ajax callback for file uploads and deletions. * * This rebuilds the form element for a particular field item. As long as the * form processing is properly encapsulated in the widget element the form @@ -232,6 +239,9 @@ $form_parents = func_get_args(); $form_build_id = (string) array_pop($form_parents); + // Sanitize form parents before using them. + $form_parents = array_filter($form_parents, 'element_child'); + if (empty($_POST['form_build_id']) || $form_build_id != $_POST['form_build_id']) { // Invalid request. drupal_set_message(t('An unrecoverable error occurred. The uploaded file likely exceeded the maximum file size (@size) that this server supports.', array('@size' => format_size(file_upload_max_size()))), 'error'); @@ -240,7 +250,7 @@ return array('#type' => 'ajax', '#commands' => $commands); } - list($form, $form_state) = ajax_get_form(); + list($form, $form_state, $form_id, $form_build_id, $commands) = ajax_get_form(); if (!$form) { // Invalid form_build_id. @@ -265,7 +275,7 @@ $form = $form[$parent]; } - // Add the special AJAX class if a new file was added. + // Add the special Ajax class if a new file was added. if (isset($form['#file_upload_delta']) && $current_file_count < $form['#file_upload_delta']) { $form[$current_file_count]['#attributes']['class'][] = 'ajax-new-content'; } @@ -274,11 +284,11 @@ $form['#suffix'] .= ''; } - $output = theme('status_messages') . drupal_render($form); + $form['#prefix'] .= theme('status_messages'); + $output = drupal_render($form); $js = drupal_add_js(); - $settings = call_user_func_array('array_merge_recursive', $js['settings']['data']); + $settings = drupal_array_merge_deep_array($js['settings']['data']); - $commands = array(); $commands[] = ajax_command_replace(NULL, $output, $settings); return array('#type' => 'ajax', '#commands' => $commands); } @@ -315,7 +325,7 @@ } /** - * Determine the preferred upload progress implementation. + * Determines the preferred upload progress implementation. * * @return * A string indicating which upload progress system is available. Either "apc" @@ -352,6 +362,10 @@ * support for a default value. */ function file_managed_file_process($element, &$form_state, $form) { + // Append the '-upload' to the #id so the field label's 'for' attribute + // corresponds with the file element. + $original_id = $element['#id']; + $element['#id'] .= '-upload'; $fid = isset($element['#value']['fid']) ? $element['#value']['fid'] : 0; // Set some default element properties. @@ -361,7 +375,7 @@ $ajax_settings = array( 'path' => 'file/ajax/' . implode('/', $element['#array_parents']) . '/' . $form['form_build_id']['#value'], - 'wrapper' => $element['#id'] . '-ajax-wrapper', + 'wrapper' => $original_id . '-ajax-wrapper', 'effect' => 'fade', 'progress' => array( 'type' => $element['#progress_indicator'], @@ -381,7 +395,9 @@ '#weight' => -5, ); - $ajax_settings['progress']['type'] ? $ajax_settings['progress']['type'] == 'bar' : 'throbber'; + // Force the progress indicator for the remove button to be either 'none' or + // 'throbber', even if the upload button is using something else. + $ajax_settings['progress']['type'] = ($element['#progress_indicator'] == 'none') ? 'none' : 'throbber'; $ajax_settings['progress']['message'] = NULL; $ajax_settings['effect'] = 'none'; $element['remove_button'] = array( @@ -409,6 +425,9 @@ '#type' => 'hidden', '#value' => $upload_progress_key, '#attributes' => array('class' => array('file-progress')), + // Uploadprogress extension requires this field to be at the top of the + // form. + '#weight' => -20, ); } elseif ($implementation == 'apc') { @@ -416,6 +435,9 @@ '#type' => 'hidden', '#value' => $upload_progress_key, '#attributes' => array('class' => array('file-progress')), + // Uploadprogress extension requires this field to be at the top of the + // form. + '#weight' => -20, ); } @@ -429,7 +451,7 @@ '#type' => 'file', '#title' => t('Choose a file'), '#title_display' => 'invisible', - '#size' => 22, + '#size' => $element['#size'], '#theme_wrappers' => array(), '#weight' => -10, ); @@ -440,6 +462,17 @@ '#markup' => theme('file_link', array('file' => $element['#file'])) . ' ', '#weight' => -10, ); + // Anonymous users who have uploaded a temporary file need a + // non-session-based token added so file_managed_file_value() can check + // that they have permission to use this file on subsequent submissions of + // the same form (for example, after an Ajax upload or form validation + // error). + if (!$GLOBALS['user']->uid && $element['#file']->status != FILE_STATUS_PERMANENT) { + $element['fid_token'] = array( + '#type' => 'hidden', + '#value' => drupal_hmac_base64('file-' . $fid, drupal_get_private_key() . drupal_get_hash_salt()), + ); + } } // Add the extension list to the page as JavaScript settings. @@ -448,13 +481,13 @@ $element['upload']['#attached']['js'] = array( array( 'type' => 'setting', - 'data' => array('file' => array('elements' => array('#' . $element['#id'] . '-upload' => $extension_list))) + 'data' => array('file' => array('elements' => array('#' . $element['#id'] => $extension_list))) ) ); } - // Prefix and suffix used for AJAX replacement. - $element['#prefix'] = '
'; + // Prefix and suffix used for Ajax replacement. + $element['#prefix'] = '
'; $element['#suffix'] = '
'; return $element; @@ -465,6 +498,7 @@ */ function file_managed_file_value(&$element, $input = FALSE, $form_state = NULL) { $fid = 0; + $force_default = FALSE; // Find the current value of this field from the form state. $form_state_fid = $form_state['values']; @@ -497,15 +531,51 @@ $callback($element, $input, $form_state); } } - // Load file if the FID has changed to confirm it exists. - if (isset($input['fid']) && $file = file_load($input['fid'])) { - $fid = $file->fid; + // If a FID was submitted, load the file (and check access if it's not a + // public file) to confirm it exists and that the current user has access + // to it. + if (isset($input['fid']) && ($file = file_load($input['fid']))) { + // By default the public:// file scheme provided by Drupal core is the + // only one that allows files to be publicly accessible to everyone, so + // it is the only one for which the file access checks are bypassed. + // Other modules which provide publicly accessible streams of their own + // in hook_stream_wrappers() can add the corresponding scheme to the + // 'file_public_schema' variable to bypass file access checks for those + // as well. This should only be done for schemes that are completely + // publicly accessible, with no download restrictions; for security + // reasons all other schemes must go through the file_download_access() + // check. + if (!in_array(file_uri_scheme($file->uri), variable_get('file_public_schema', array('public'))) && !file_download_access($file->uri)) { + $force_default = TRUE; + } + // Temporary files that belong to other users should never be allowed. + elseif ($file->status != FILE_STATUS_PERMANENT) { + if ($GLOBALS['user']->uid && $file->uid != $GLOBALS['user']->uid) { + $force_default = TRUE; + } + // Since file ownership can't be determined for anonymous users, they + // are not allowed to reuse temporary files at all. But they do need + // to be able to reuse their own files from earlier submissions of + // the same form, so to allow that, check for the token added by + // file_managed_file_process(). + elseif (!$GLOBALS['user']->uid) { + $token = drupal_array_get_nested_value($form_state['input'], array_merge($element['#parents'], array('fid_token'))); + if ($token !== drupal_hmac_base64('file-' . $file->fid, drupal_get_private_key() . drupal_get_hash_salt())) { + $force_default = TRUE; + } + } + } + // If all checks pass, allow the file to be changed. + if (!$force_default) { + $fid = $file->fid; + } } } } - // If there is no input, set the default value. - else { + // If there is no input or if the default value was requested above, use the + // default value. + if ($input === FALSE || $force_default) { if ($element['#extended']) { $default_fid = isset($element['#default_value']['fid']) ? $element['#default_value']['fid'] : 0; $return = isset($element['#default_value']) ? $element['#default_value'] : array('fid' => 0); @@ -533,7 +603,7 @@ // If referencing an existing file, only allow if there are existing // references. This prevents unmanaged files from being deleted if this // item were to be deleted. - $clicked_button = end($form_state['clicked_button']['#parents']); + $clicked_button = end($form_state['triggering_element']['#parents']); if ($clicked_button != 'remove_button' && !empty($element['fid']['#value'])) { if ($file = file_load($element['fid']['#value'])) { if ($file->status == FILE_STATUS_PERMANENT) { @@ -560,7 +630,9 @@ } /** - * Submit handler for upload and remove buttons of managed_file elements. + * Form submission handler for upload / remove buttons of managed_file elements. + * + * @see file_managed_file_process() */ function file_managed_file_submit($form, &$form_state) { // Determine whether it was the upload or the remove button that was clicked, @@ -601,10 +673,11 @@ } /** - * Given a managed_file element, save any files that have been uploaded into it. + * Saves any files that have been uploaded into a managed_file element. * * @param $element * The FAPI element whose values are being saved. + * * @return * The file object representing the file that was saved, or FALSE if no file * was saved. @@ -643,9 +716,18 @@ function theme_file_managed_file($variables) { $element = $variables['element']; + $attributes = array(); + if (isset($element['#id'])) { + $attributes['id'] = $element['#id']; + } + if (!empty($element['#attributes']['class'])) { + $attributes['class'] = (array) $element['#attributes']['class']; + } + $attributes['class'][] = 'form-managed-file'; + // This wrapper is required to apply JS behaviors and CSS styling. $output = ''; - $output .= '
'; + $output .= ''; $output .= drupal_render_children($element); $output .= '
'; return $output; @@ -699,7 +781,32 @@ $icon_directory = $variables['icon_directory']; $url = file_create_url($file->uri); - $icon = theme('file_icon', array('file' => $file, 'icon_directory' => $icon_directory)); + + // Human-readable names, for use as text-alternatives to icons. + $mime_name = array( + 'application/msword' => t('Microsoft Office document icon'), + 'application/vnd.ms-excel' => t('Office spreadsheet icon'), + 'application/vnd.ms-powerpoint' => t('Office presentation icon'), + 'application/pdf' => t('PDF icon'), + 'video/quicktime' => t('Movie icon'), + 'audio/mpeg' => t('Audio icon'), + 'audio/wav' => t('Audio icon'), + 'image/jpeg' => t('Image icon'), + 'image/png' => t('Image icon'), + 'image/gif' => t('Image icon'), + 'application/zip' => t('Package icon'), + 'text/html' => t('HTML icon'), + 'text/plain' => t('Plain text icon'), + 'application/octet-stream' => t('Binary Data'), + ); + + $mimetype = file_get_mimetype($file->uri); + + $icon = theme('file_icon', array( + 'file' => $file, + 'icon_directory' => $icon_directory, + 'alt' => !empty($mime_name[$mimetype]) ? $mime_name[$mimetype] : t('File'), + )); // Set options as per anchor format described at // http://microformats.org/wiki/file-format-examples @@ -729,26 +836,30 @@ * - file: A file object for which to make an icon. * - icon_directory: (optional) A path to a directory of icons to be used for * files. Defaults to the value of the "file_icon_directory" variable. + * - alt: (optional) The alternative text to represent the icon in text-based + * browsers. Defaults to an empty string. * * @ingroup themeable */ function theme_file_icon($variables) { $file = $variables['file']; + $alt = $variables['alt']; $icon_directory = $variables['icon_directory']; $mime = check_plain($file->filemime); $icon_url = file_icon_url($file, $icon_directory); - return ''; + return '' . check_plain($alt) . ''; } /** - * Given a file object, create a URL to a matching icon. + * Creates a URL to the icon for a file object. * * @param $file * A file object. * @param $icon_directory * (optional) A path to a directory of icons to be used for files. Defaults to * the value of the "file_icon_directory" variable. + * * @return * A URL string to the icon, or FALSE if an appropriate icon cannot be found. */ @@ -760,13 +871,14 @@ } /** - * Given a file object, create a path to a matching icon. + * Creates a path to the icon for a file object. * * @param $file * A file object. * @param $icon_directory * (optional) A path to a directory of icons to be used for files. Defaults to * the value of the "file_icon_directory" variable. + * * @return * A string to the icon as a local path, or FALSE if an appropriate icon could * not be found. @@ -812,10 +924,11 @@ } /** - * Determine the generic icon MIME package based on a file's MIME type. + * Determines the generic icon MIME package based on a file's MIME type. * * @param $file * A file object. + * * @return * The generic icon MIME package expected for this file. */ @@ -943,7 +1056,7 @@ */ /** - * Gets a list of references to a file. + * Retrieves a list of references to a file. * * @param $file * A file object. @@ -957,11 +1070,18 @@ * @param $field_type * (optional) The name of a field type. If given, limits the reference check * to fields of the given type. + * @param $check_access + * (optional) A boolean that specifies whether the permissions of the current + * user should be checked when retrieving references. If FALSE, all + * references to the file are returned. If TRUE, only references from + * entities that the current user has access to are returned. Defaults to + * TRUE for backwards compatibility reasons, but FALSE is recommended for + * most situations. * * @return * An integer value. */ -function file_get_file_references($file, $field = NULL, $age = FIELD_LOAD_REVISION, $field_type = 'file') { +function file_get_file_references($file, $field = NULL, $age = FIELD_LOAD_REVISION, $field_type = 'file', $check_access = TRUE) { $references = drupal_static(__FUNCTION__, array()); $fields = isset($field) ? array($field['field_name'] => $field) : field_info_fields(); @@ -972,11 +1092,16 @@ $query ->fieldCondition($file_field, 'fid', $file->fid) ->age($age); + if (!$check_access) { + // Neutralize the 'entity_field_access' query tag added by + // field_sql_storage_field_storage_query(). + $query->addTag('DANGEROUS_ACCESS_CHECK_OPT_OUT'); + } $references[$field_name] = $query->execute(); } } - return isset($field) ? $references[$field['field_name']] : $references; + return isset($field) ? $references[$field['field_name']] : array_filter($references); } /** diff -Naur drupal-7.0/modules/file/tests/file.test drupal-7.66/modules/file/tests/file.test --- drupal-7.0/modules/file/tests/file.test 2010-12-11 02:32:20.000000000 +0100 +++ drupal-7.66/modules/file/tests/file.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,25 +1,33 @@ admin_user = $this->drupalCreateUser(array('access content', 'access administration pages', 'administer site configuration', 'administer users', 'administer permissions', 'administer content types', 'administer nodes', 'bypass node access')); + // Since this is a base class for many test cases, support the same + // flexibility that DrupalWebTestCase::setUp() has for the modules to be + // passed in as either an array or a variable number of string arguments. + $modules = func_get_args(); + if (isset($modules[0]) && is_array($modules[0])) { + $modules = $modules[0]; + } + $modules[] = 'file'; + $modules[] = 'file_module_test'; + parent::setUp($modules); + $this->admin_user = $this->drupalCreateUser(array('access content', 'access administration pages', 'administer site configuration', 'administer users', 'administer permissions', 'administer content types', 'administer nodes', 'bypass node access', 'administer fields')); $this->drupalLogin($this->admin_user); } /** - * Get a sample file of the specified type. + * Retrieves a sample file of the specified type. */ function getTestFile($type_name, $size = NULL) { // Get a file to upload. @@ -32,14 +40,14 @@ } /** - * Get the fid of the last inserted file. + * Retrieves the fid of the last inserted file. */ function getLastFileId() { return (int) db_query('SELECT MAX(fid) FROM {file_managed}')->fetchField(); } /** - * Create a new file field. + * Creates a new file field. * * @param $name * The name of the new field (all lowercase), exclude the "field_" prefix. @@ -66,7 +74,7 @@ } /** - * Attach a file field to an entity. + * Attaches a file field to an entity. * * @param $name * The name of the new field (all lowercase), exclude the "field_" prefix. @@ -100,7 +108,7 @@ } /** - * Update an existing file field with new settings. + * Updates an existing file field with new settings. */ function updateFileField($name, $type_name, $instance_settings = array(), $widget_settings = array()) { $instance = field_info_instance('node', $name, $type_name); @@ -111,9 +119,9 @@ } /** - * Upload a file to a node. + * Uploads a file to a node. */ - function uploadNodeFile($file, $field_name, $nid_or_type, $new_revision = TRUE) { + function uploadNodeFile($file, $field_name, $nid_or_type, $new_revision = TRUE, $extras = array()) { $langcode = LANGUAGE_NONE; $edit = array( "title" => $this->randomName(), @@ -125,12 +133,13 @@ } else { // Add a new node. - $node = $this->drupalCreateNode(array('type' => $nid_or_type)); + $extras['type'] = $nid_or_type; + $node = $this->drupalCreateNode($extras); $nid = $node->nid; // Save at least one revision to better simulate a real site. $this->drupalCreateNode(get_object_vars($node)); $node = node_load($nid, NULL, TRUE); - $this->assertNotEqual($nid, $node->vid, t('Node revision exists.')); + $this->assertNotEqual($nid, $node->vid, 'Node revision exists.'); } // Attach a file to the node. @@ -141,7 +150,7 @@ } /** - * Remove a file from a node. + * Removes a file from a node. * * Note that if replacing a file, it must first be removed then added again. */ @@ -155,7 +164,7 @@ } /** - * Replace a file within a node. + * Replaces a file within a node. */ function replaceNodeFile($file, $field_name, $nid, $new_revision = TRUE) { $edit = array( @@ -168,51 +177,197 @@ } /** - * Assert that a file exists physically on disk. + * Asserts that a file exists physically on disk. */ function assertFileExists($file, $message = NULL) { - $message = isset($message) ? $message : t('File %file exists on the disk.', array('%file' => $file->uri)); + $message = isset($message) ? $message : format_string('File %file exists on the disk.', array('%file' => $file->uri)); $this->assertTrue(is_file($file->uri), $message); } /** - * Assert that a file exists in the database. + * Asserts that a file exists in the database. */ function assertFileEntryExists($file, $message = NULL) { entity_get_controller('file')->resetCache(); $db_file = file_load($file->fid); - $message = isset($message) ? $message : t('File %file exists in database at the correct path.', array('%file' => $file->uri)); + $message = isset($message) ? $message : format_string('File %file exists in database at the correct path.', array('%file' => $file->uri)); $this->assertEqual($db_file->uri, $file->uri, $message); } /** - * Assert that a file does not exist on disk. + * Asserts that a file does not exist on disk. */ function assertFileNotExists($file, $message = NULL) { - $message = isset($message) ? $message : t('File %file exists on the disk.', array('%file' => $file->uri)); + $message = isset($message) ? $message : format_string('File %file exists on the disk.', array('%file' => $file->uri)); $this->assertFalse(is_file($file->uri), $message); } /** - * Assert that a file does not exist in the database. + * Asserts that a file does not exist in the database. */ function assertFileEntryNotExists($file, $message) { entity_get_controller('file')->resetCache(); - $message = isset($message) ? $message : t('File %file exists in database at the correct path.', array('%file' => $file->uri)); + $message = isset($message) ? $message : format_string('File %file exists in database at the correct path.', array('%file' => $file->uri)); $this->assertFalse(file_load($file->fid), $message); } /** - * Assert that a file's status is set to permanent in the database. + * Asserts that a file's status is set to permanent in the database. */ function assertFileIsPermanent($file, $message = NULL) { - $message = isset($message) ? $message : t('File %file is permanent.', array('%file' => $file->uri)); + $message = isset($message) ? $message : format_string('File %file is permanent.', array('%file' => $file->uri)); $this->assertTrue($file->status == FILE_STATUS_PERMANENT, $message); } + + /** + * Creates a temporary file, for a specific user. + * + * @param string $data + * A string containing the contents of the file. + * @param int $uid + * The user ID of the file owner. + * + * @return object + * A file object, or FALSE on error. + */ + function createTemporaryFile($data, $uid = NULL) { + $file = file_save_data($data, NULL, NULL); + + if ($file) { + $file->uid = isset($uid) ? $uid : $this->admin_user->uid; + // Change the file status to be temporary. + $file->status = NULL; + return file_save($file); + } + + return $file; + } } /** - * Test class for testing the 'managed_file' element type on its own, not as part of a file field. + * Tests adding a file to a non-node entity. + */ +class FileTaxonomyTermTestCase extends DrupalWebTestCase { + protected $admin_user; + + public static function getInfo() { + return array( + 'name' => 'Taxonomy term file test', + 'description' => 'Tests adding a file to a non-node entity.', + 'group' => 'File', + ); + } + + public function setUp() { + $modules[] = 'file'; + $modules[] = 'taxonomy'; + parent::setUp($modules); + $this->admin_user = $this->drupalCreateUser(array('access content', 'access administration pages', 'administer site configuration', 'administer taxonomy')); + $this->drupalLogin($this->admin_user); + } + + /** + * Creates a file field and attaches it to the "Tags" taxonomy vocabulary. + * + * @param $name + * The field name of the file field to create. + * @param $uri_scheme + * The URI scheme to use for the file field (for example, "private" to + * create a field that stores private files or "public" to create a field + * that stores public files). + */ + protected function createAttachFileField($name, $uri_scheme) { + $field = array( + 'field_name' => $name, + 'type' => 'file', + 'settings' => array( + 'uri_scheme' => $uri_scheme, + ), + 'cardinality' => 1, + ); + field_create_field($field); + // Attach an instance of it. + $instance = array( + 'field_name' => $name, + 'label' => 'File', + 'entity_type' => 'taxonomy_term', + 'bundle' => 'tags', + 'required' => FALSE, + 'settings' => array(), + 'widget' => array( + 'type' => 'file_generic', + 'settings' => array(), + ), + ); + field_create_instance($instance); + } + + /** + * Tests that a public file can be attached to a taxonomy term. + * + * This is a regression test for https://www.drupal.org/node/2305017. + */ + public function testTermFilePublic() { + $this->_testTermFile('public'); + } + + /** + * Tests that a private file can be attached to a taxonomy term. + * + * This is a regression test for https://www.drupal.org/node/2305017. + */ + public function testTermFilePrivate() { + $this->_testTermFile('private'); + } + + /** + * Runs tests for attaching a file field to a taxonomy term. + * + * @param $uri_scheme + * The URI scheme to use for the file field, either "public" or "private". + */ + protected function _testTermFile($uri_scheme) { + $field_name = strtolower($this->randomName()); + $this->createAttachFileField($field_name, $uri_scheme); + // Get a file to upload. + $file = current($this->drupalGetTestFiles('text')); + // Add a filesize property to files as would be read by file_load(). + $file->filesize = filesize($file->uri); + $langcode = LANGUAGE_NONE; + $edit = array( + "name" => $this->randomName(), + ); + // Attach a file to the term. + $edit['files[' . $field_name . '_' . $langcode . '_0]'] = drupal_realpath($file->uri); + $this->drupalPost("admin/structure/taxonomy/tags/add", $edit, t('Save')); + // Find the term ID we just created. + $tid = db_query_range('SELECT tid FROM {taxonomy_term_data} ORDER BY tid DESC', 0, 1)->fetchField(); + $terms = entity_load('taxonomy_term', array($tid)); + $term = $terms[$tid]; + $fid = $term->{$field_name}[LANGUAGE_NONE][0]['fid']; + // Check that the uploaded file is present on the edit form. + $this->drupalGet("taxonomy/term/$tid/edit"); + $file_input_name = $field_name . '[' . LANGUAGE_NONE . '][0][fid]'; + $this->assertFieldByXpath('//input[@type="hidden" and @name="' . $file_input_name . '"]', $fid, 'File is attached on edit form.'); + // Edit the term and change name without changing the file. + $edit = array( + "name" => $this->randomName(), + ); + $this->drupalPost("taxonomy/term/$tid/edit", $edit, t('Save')); + // Check that the uploaded file is still present on the edit form. + $this->drupalGet("taxonomy/term/$tid/edit"); + $file_input_name = $field_name . '[' . LANGUAGE_NONE . '][0][fid]'; + $this->assertFieldByXpath('//input[@type="hidden" and @name="' . $file_input_name . '"]', $fid, 'File is attached on edit form.'); + // Load term while resetting the cache. + $terms = entity_load('taxonomy_term', array($tid), array(), TRUE); + $term = $terms[$tid]; + $this->assertTrue(!empty($term->{$field_name}[LANGUAGE_NONE]), 'Term has attached files.'); + $this->assertEqual($term->{$field_name}[LANGUAGE_NONE][0]['fid'], $fid, 'Same File ID is attached to the term.'); + } +} + +/** + * Tests the 'managed_file' element type. * * @todo Create a FileTestCase base class and move FileFieldTestCase methods * that aren't related to fields into it. @@ -230,6 +385,10 @@ * Tests the managed_file element type. */ function testManagedFile() { + // Check that $element['#size'] is passed to the child upload element. + $this->drupalGet('file/test'); + $this->assertFieldByXpath('//input[@name="files[nested_file]" and @size="13"]', NULL, 'The custom #size attribute is passed to the child upload element.'); + // Perform the tests with all permutations of $form['#tree'] and // $element['#extended']. foreach (array(0, 1) as $tree) { @@ -240,21 +399,33 @@ // Submit without a file. $this->drupalPost($path, array(), t('Save')); - $this->assertRaw(t('The file id is %fid.', array('%fid' => 0)), t('Submitted without a file.')); + $this->assertRaw(t('The file id is %fid.', array('%fid' => 0)), 'Submitted without a file.'); + + // Submit with a file, but with an invalid form token. Ensure the file + // was not saved. + $last_fid_prior = $this->getLastFileId(); + $edit = array( + 'files[' . $input_base_name . ']' => drupal_realpath($test_file->uri), + 'form_token' => 'invalid token', + ); + $this->drupalPost($path, $edit, t('Save')); + $this->assertText('The form has become outdated. Copy any unsaved work in the form below'); + $last_fid = $this->getLastFileId(); + $this->assertEqual($last_fid_prior, $last_fid, 'File was not saved when uploaded with an invalid form token.'); // Submit a new file, without using the Upload button. $last_fid_prior = $this->getLastFileId(); $edit = array('files[' . $input_base_name . ']' => drupal_realpath($test_file->uri)); $this->drupalPost($path, $edit, t('Save')); $last_fid = $this->getLastFileId(); - $this->assertTrue($last_fid > $last_fid_prior, t('New file got saved.')); - $this->assertRaw(t('The file id is %fid.', array('%fid' => $last_fid)), t('Submit handler has correct file info.')); + $this->assertTrue($last_fid > $last_fid_prior, 'New file got saved.'); + $this->assertRaw(t('The file id is %fid.', array('%fid' => $last_fid)), 'Submit handler has correct file info.'); // Submit no new input, but with a default file. $this->drupalPost($path . '/' . $last_fid, array(), t('Save')); - $this->assertRaw(t('The file id is %fid.', array('%fid' => $last_fid)), t('Empty submission did not change an existing file.')); + $this->assertRaw(t('The file id is %fid.', array('%fid' => $last_fid)), 'Empty submission did not change an existing file.'); - // Now, test the Upload and Remove buttons, with and without AJAX. + // Now, test the Upload and Remove buttons, with and without Ajax. foreach (array(FALSE, TRUE) as $ajax) { // Upload, then Submit. $last_fid_prior = $this->getLastFileId(); @@ -267,9 +438,9 @@ $this->drupalPost(NULL, $edit, t('Upload')); } $last_fid = $this->getLastFileId(); - $this->assertTrue($last_fid > $last_fid_prior, t('New file got uploaded.')); + $this->assertTrue($last_fid > $last_fid_prior, 'New file got uploaded.'); $this->drupalPost(NULL, array(), t('Save')); - $this->assertRaw(t('The file id is %fid.', array('%fid' => $last_fid)), t('Submit handler has correct file info.')); + $this->assertRaw(t('The file id is %fid.', array('%fid' => $last_fid)), 'Submit handler has correct file info.'); // Remove, then Submit. $this->drupalGet($path . '/' . $last_fid); @@ -280,7 +451,7 @@ $this->drupalPost(NULL, array(), t('Remove')); } $this->drupalPost(NULL, array(), t('Save')); - $this->assertRaw(t('The file id is %fid.', array('%fid' => 0)), t('Submission after file removal was successful.')); + $this->assertRaw(t('The file id is %fid.', array('%fid' => 0)), 'Submission after file removal was successful.'); // Upload, then Remove, then Submit. $this->drupalGet($path); @@ -294,7 +465,7 @@ $this->drupalPost(NULL, array(), t('Remove')); } $this->drupalPost(NULL, array(), t('Save')); - $this->assertRaw(t('The file id is %fid.', array('%fid' => 0)), t('Submission after file upload and removal was successful.')); + $this->assertRaw(t('The file id is %fid.', array('%fid' => 0)), 'Submission after file upload and removal was successful.'); } } } @@ -302,7 +473,7 @@ } /** - * Test class to test file field widget, single and multi-valued, with and without AJAX, with public and private files. + * Tests file field widget. */ class FileFieldWidgetTestCase extends FileFieldTestCase { public static function getInfo() { @@ -314,7 +485,7 @@ } /** - * Tests upload and remove buttons, with and without AJAX, for a single-valued File field. + * Tests upload and remove buttons for a single-valued File field. */ function testSingleValuedWidget() { // Use 'page' instead of 'article', so that the 'article' image field does @@ -337,16 +508,25 @@ $nid = $this->uploadNodeFile($test_file, $field_name, $type_name); $node = node_load($nid, NULL, TRUE); $node_file = (object) $node->{$field_name}[LANGUAGE_NONE][0]; - $this->assertFileExists($node_file, t('New file saved to disk on node creation.')); + $this->assertFileExists($node_file, 'New file saved to disk on node creation.'); + + // Test that running field_attach_update() leaves the file intact. + $field = new stdClass(); + $field->type = $type_name; + $field->nid = $nid; + field_attach_update('node', $field); + $node = node_load($nid); + $node_file = (object) $node->{$field_name}[LANGUAGE_NONE][0]; + $this->assertFileExists($node_file, 'New file still saved to disk on field update.'); // Ensure the file can be downloaded. $this->drupalGet(file_create_url($node_file->uri)); - $this->assertResponse(200, t('Confirmed that the generated URL is correct by downloading the shipped file.')); + $this->assertResponse(200, 'Confirmed that the generated URL is correct by downloading the shipped file.'); // Ensure the edit page has a remove button instead of an upload button. $this->drupalGet("node/$nid/edit"); - $this->assertNoFieldByXPath('//input[@type="submit"]', t('Upload'), t('Node with file does not display the "Upload" button.')); - $this->assertFieldByXpath('//input[@type="submit"]', t('Remove'), t('Node with file displays the "Remove" button.')); + $this->assertNoFieldByXPath('//input[@type="submit"]', t('Upload'), 'Node with file does not display the "Upload" button.'); + $this->assertFieldByXpath('//input[@type="submit"]', t('Remove'), 'Node with file displays the "Remove" button.'); // "Click" the remove button (emulating either a nojs or js submission). switch ($type) { @@ -360,18 +540,182 @@ } // Ensure the page now has an upload button instead of a remove button. - $this->assertNoFieldByXPath('//input[@type="submit"]', t('Remove'), t('After clicking the "Remove" button, it is no longer displayed.')); - $this->assertFieldByXpath('//input[@type="submit"]', t('Upload'), t('After clicking the "Remove" button, the "Upload" button is displayed.')); + $this->assertNoFieldByXPath('//input[@type="submit"]', t('Remove'), 'After clicking the "Remove" button, it is no longer displayed.'); + $this->assertFieldByXpath('//input[@type="submit"]', t('Upload'), 'After clicking the "Remove" button, the "Upload" button is displayed.'); // Save the node and ensure it does not have the file. $this->drupalPost(NULL, array(), t('Save')); $node = node_load($nid, NULL, TRUE); - $this->assertTrue(empty($node->{$field_name}[LANGUAGE_NONE][0]['fid']), t('File was successfully removed from the node.')); + $this->assertTrue(empty($node->{$field_name}[LANGUAGE_NONE][0]['fid']), 'File was successfully removed from the node.'); } } /** - * Tests upload and remove buttons, with and without AJAX, for a multi-valued File field. + * Tests exploiting the temporary file removal of another user using fid. + */ + function testTemporaryFileRemovalExploit() { + // Create a victim user. + $victim_user = $this->drupalCreateUser(); + + // Create an attacker user. + $attacker_user = $this->drupalCreateUser(array( + 'access content', + 'create page content', + 'edit any page content', + )); + + // Log in as the attacker user. + $this->drupalLogin($attacker_user); + + // Perform tests using the newly created users. + $this->doTestTemporaryFileRemovalExploit($victim_user->uid, $attacker_user->uid); + } + + /** + * Tests exploiting the temporary file removal for anonymous users using fid. + */ + public function testTemporaryFileRemovalExploitAnonymous() { + // Set up an anonymous victim user. + $victim_uid = 0; + + // Set up an anonymous attacker user. + $attacker_uid = 0; + + // Set up permissions for anonymous attacker user. + user_role_change_permissions(DRUPAL_ANONYMOUS_RID, array( + 'access content' => TRUE, + 'create page content' => TRUE, + 'edit any page content' => TRUE, + )); + + // In order to simulate being the anonymous attacker user, we need to log + // out here since setUp() has logged in the admin. + $this->drupalLogout(); + + // Perform tests using the newly set up users. + $this->doTestTemporaryFileRemovalExploit($victim_uid, $attacker_uid); + } + + /** + * Tests validation with the Upload button. + */ + function testWidgetValidation() { + $type_name = 'article'; + $field_name = strtolower($this->randomName()); + $this->createFileField($field_name, $type_name); + $this->updateFileField($field_name, $type_name, array('file_extensions' => 'txt')); + + foreach (array('nojs', 'js') as $type) { + // Create node and prepare files for upload. + $node = $this->drupalCreateNode(array('type' => 'article')); + $nid = $node->nid; + $this->drupalGet("node/$nid/edit"); + $test_file_text = $this->getTestFile('text'); + $test_file_image = $this->getTestFile('image'); + $field = field_info_field($field_name); + $name = 'files[' . $field_name . '_' . LANGUAGE_NONE . '_0]'; + + // Upload file with incorrect extension, check for validation error. + $edit[$name] = drupal_realpath($test_file_image->uri); + switch ($type) { + case 'nojs': + $this->drupalPost(NULL, $edit, t('Upload')); + break; + + case 'js': + $button = $this->xpath('//input[@type="submit" and @value="' . t('Upload') . '"]'); + $this->drupalPostAJAX(NULL, $edit, array((string) $button[0]['name'] => (string) $button[0]['value'])); + break; + } + $error_message = t('Only files with the following extensions are allowed: %files-allowed.', array('%files-allowed' => 'txt')); + $this->assertRaw($error_message, t('Validation error when file with wrong extension uploaded (JSMode=%type).', array('%type' => $type))); + + // Upload file with correct extension, check that error message is removed. + $edit[$name] = drupal_realpath($test_file_text->uri); + switch ($type) { + case 'nojs': + $this->drupalPost(NULL, $edit, t('Upload')); + break; + + case 'js': + $button = $this->xpath('//input[@type="submit" and @value="' . t('Upload') . '"]'); + $this->drupalPostAJAX(NULL, $edit, array((string) $button[0]['name'] => (string) $button[0]['value'])); + break; + } + $this->assertNoRaw($error_message, t('Validation error removed when file with correct extension uploaded (JSMode=%type).', array('%type' => $type))); + } + } + + /** + * Helper for testing exploiting the temporary file removal using fid. + * + * @param int $victim_uid + * The victim user ID. + * @param int $attacker_uid + * The attacker user ID. + */ + protected function doTestTemporaryFileRemovalExploit($victim_uid, $attacker_uid) { + // Use 'page' instead of 'article', so that the 'article' image field does + // not conflict with this test. If in the future the 'page' type gets its + // own default file or image field, this test can be made more robust by + // using a custom node type. + $type_name = 'page'; + $field_name = 'test_file_field'; + $this->createFileField($field_name, $type_name); + + $test_file = $this->getTestFile('text'); + foreach (array('nojs', 'js') as $type) { + // Create a temporary file owned by the anonymous victim user. This will be + // as if they had uploaded the file, but not saved the node they were + // editing or creating. + $victim_tmp_file = $this->createTemporaryFile('some text', $victim_uid); + $victim_tmp_file = file_load($victim_tmp_file->fid); + $this->assertTrue($victim_tmp_file->status != FILE_STATUS_PERMANENT, 'New file saved to disk is temporary.'); + $this->assertFalse(empty($victim_tmp_file->fid), 'New file has a fid'); + $this->assertEqual($victim_uid, $victim_tmp_file->uid, 'New file belongs to the victim user'); + + // Have attacker create a new node with a different uploaded file and + // ensure it got uploaded successfully. + // @todo Can we test AJAX? See https://www.drupal.org/node/2538260 + $edit = array( + 'title' => $type . '-title', + ); + + // Attach a file to a node. + $langcode = LANGUAGE_NONE; + $edit['files[' . $field_name . '_' . $langcode . '_0]'] = drupal_realpath($test_file->uri); + $this->drupalPost("node/add/$type_name", $edit, 'Save'); + $node = $this->drupalGetNodeByTitle($edit['title']); + $node_file = file_load($node->{$field_name}[$langcode][0]['fid']); + $this->assertFileExists($node_file, 'New file saved to disk on node creation.'); + $this->assertEqual($attacker_uid, $node_file->uid, 'New file belongs to the attacker.'); + + // Ensure the file can be downloaded. + $this->drupalGet(file_create_url($node_file->uri)); + $this->assertResponse(200, 'Confirmed that the generated URL is correct by downloading the shipped file.'); + + // "Click" the remove button (emulating either a nojs or js submission). + // In this POST request, the attacker "guesses" the fid of the victim's + // temporary file and uses that to remove this file. + $this->drupalGet('node/' . $node->nid . '/edit'); + switch ($type) { + case 'nojs': + $this->drupalPost(NULL, array("{$field_name}[$langcode][0][fid]" => (string) $victim_tmp_file->fid), 'Remove'); + break; + case 'js': + $button = $this->xpath('//input[@type="submit" and @value="Remove"]'); + $this->drupalPostAJAX(NULL, array("{$field_name}[$langcode][0][fid]" => (string) $victim_tmp_file->fid), array((string) $button[0]['name'] => (string) $button[0]['value'])); + break; + } + + // The victim's temporary file should not be removed by the attacker's + // POST request. + $this->assertFileExists($victim_tmp_file); + } + } + + /** + * Tests upload and remove buttons for multiple multi-valued File fields. */ function testMultiValuedWidget() { // Use 'page' instead of 'article', so that the 'article' image field does @@ -380,77 +724,106 @@ // using a custom node type. $type_name = 'page'; $field_name = strtolower($this->randomName()); + $field_name2 = strtolower($this->randomName()); $this->createFileField($field_name, $type_name, array('cardinality' => 3)); + $this->createFileField($field_name2, $type_name, array('cardinality' => 3)); + $field = field_info_field($field_name); $instance = field_info_instance('node', $field_name, $type_name); + $field2 = field_info_field($field_name2); + $instance2 = field_info_instance('node', $field_name2, $type_name); + $test_file = $this->getTestFile('text'); foreach (array('nojs', 'js') as $type) { - // Visit the node creation form, and upload 3 files. Since the field has - // cardinality of 3, ensure the "Upload" button is displayed until after - // the 3rd file, and after that, isn't displayed. - // @todo This is only testing a non-AJAX upload, because drupalPostAJAX() + // Visit the node creation form, and upload 3 files for each field. Since + // the field has cardinality of 3, ensure the "Upload" button is displayed + // until after the 3rd file, and after that, isn't displayed. Because + // SimpleTest triggers the last button with a given name, so upload to the + // second field first. + // @todo This is only testing a non-Ajax upload, because drupalPostAJAX() // does not yet emulate jQuery's file upload. + // $this->drupalGet("node/add/$type_name"); - for ($delta = 0; $delta < 3; $delta++) { - $edit = array('files[' . $field_name . '_' . LANGUAGE_NONE . '_' . $delta . ']' => drupal_realpath($test_file->uri)); - // If the Upload button doesn't exist, drupalPost() will automatically - // fail with an assertion message. - $this->drupalPost(NULL, $edit, t('Upload')); + foreach (array($field_name2, $field_name) as $each_field_name) { + for ($delta = 0; $delta < 3; $delta++) { + $edit = array('files[' . $each_field_name . '_' . LANGUAGE_NONE . '_' . $delta . ']' => drupal_realpath($test_file->uri)); + // If the Upload button doesn't exist, drupalPost() will automatically + // fail with an assertion message. + $this->drupalPost(NULL, $edit, t('Upload')); + } } - $this->assertNoFieldByXpath('//input[@type="submit"]', t('Upload'), t('After uploading 3 files, the "Upload" button is no longer displayed.')); + $this->assertNoFieldByXpath('//input[@type="submit"]', t('Upload'), 'After uploading 3 files for each field, the "Upload" button is no longer displayed.'); - // Test clicking each "Remove" button. For extra robustness, test them out - // of sequential order. They are 0-indexed, and get renumbered after each - // iteration, so array(1, 1, 0) means: - // - First remove the 2nd file. - // - Then remove what is then the 2nd file (was originally the 3rd file). - // - Then remove the first file. - $num_expected_remove_buttons = 3; - foreach (array(1, 1, 0) as $delta) { - // Ensure we have the expected number of Remove buttons, and that they - // are numbered sequentially. - $buttons = $this->xpath('//input[@type="submit" and @value="Remove"]'); - $this->assertTrue(is_array($buttons) && count($buttons) === $num_expected_remove_buttons, t('There are %n "Remove" buttons displayed (JSMode=%type).', array('%n' => $num_expected_remove_buttons, '%type' => $type))); - foreach ($buttons as $i => $button) { - $this->assertIdentical((string) $button['name'], $field_name . '_' . LANGUAGE_NONE . '_' . $i . '_remove_button'); - } + $num_expected_remove_buttons = 6; - // "Click" the remove button (emulating either a nojs or js submission). - $button_name = $field_name . '_' . LANGUAGE_NONE . '_' . $delta . '_remove_button'; - switch ($type) { - case 'nojs': - // drupalPost() takes a $submit parameter that is the value of the - // button whose click we want to emulate. Since we have multiple - // buttons with the value "Remove", and want to control which one we - // use, we change the value of the other ones to something else. - // Since non-clicked buttons aren't included in the submitted POST - // data, and since drupalPost() will result in $this being updated - // with a newly rebuilt form, this doesn't cause problems. - foreach ($buttons as $button) { - if ($button['name'] != $button_name) { - $button['value'] = 'DUMMY'; - } + foreach (array($field_name, $field_name2) as $current_field_name) { + // How many uploaded files for the current field are remaining. + $remaining = 3; + // Test clicking each "Remove" button. For extra robustness, test them out + // of sequential order. They are 0-indexed, and get renumbered after each + // iteration, so array(1, 1, 0) means: + // - First remove the 2nd file. + // - Then remove what is then the 2nd file (was originally the 3rd file). + // - Then remove the first file. + foreach (array(1,1,0) as $delta) { + // Ensure we have the expected number of Remove buttons, and that they + // are numbered sequentially. + $buttons = $this->xpath('//input[@type="submit" and @value="Remove"]'); + $this->assertTrue(is_array($buttons) && count($buttons) === $num_expected_remove_buttons, format_string('There are %n "Remove" buttons displayed (JSMode=%type).', array('%n' => $num_expected_remove_buttons, '%type' => $type))); + foreach ($buttons as $i => $button) { + $key = $i >= $remaining ? $i - $remaining : $i; + $check_field_name = $field_name2; + if ($current_field_name == $field_name && $i < $remaining) { + $check_field_name = $field_name; } - $this->drupalPost(NULL, array(), t('Remove')); - break; - case 'js': - // drupalPostAJAX() lets us target the button precisely, so we don't - // require the workaround used above for nojs. - $this->drupalPostAJAX(NULL, array(), array($button_name => t('Remove'))); - break; - } - $num_expected_remove_buttons--; - // Ensure we have a single Upload button, and that it is numbered - // sequentially after the Remove buttons. - $buttons = $this->xpath('//input[@type="submit" and @value="Upload"]'); - $this->assertTrue(is_array($buttons) && count($buttons) == 1 && ((string) $buttons[0]['name'] === ($field_name . '_' . LANGUAGE_NONE . '_' . $num_expected_remove_buttons . '_upload_button')), t('After removing a file, an "Upload" button is displayed (JSMode=%type).')); + $this->assertIdentical((string) $button['name'], $check_field_name . '_' . LANGUAGE_NONE . '_' . $key. '_remove_button'); + } + + // "Click" the remove button (emulating either a nojs or js submission). + $button_name = $current_field_name . '_' . LANGUAGE_NONE . '_' . $delta . '_remove_button'; + switch ($type) { + case 'nojs': + // drupalPost() takes a $submit parameter that is the value of the + // button whose click we want to emulate. Since we have multiple + // buttons with the value "Remove", and want to control which one we + // use, we change the value of the other ones to something else. + // Since non-clicked buttons aren't included in the submitted POST + // data, and since drupalPost() will result in $this being updated + // with a newly rebuilt form, this doesn't cause problems. + foreach ($buttons as $button) { + if ($button['name'] != $button_name) { + $button['value'] = 'DUMMY'; + } + } + $this->drupalPost(NULL, array(), t('Remove')); + break; + case 'js': + // drupalPostAJAX() lets us target the button precisely, so we don't + // require the workaround used above for nojs. + $this->drupalPostAJAX(NULL, array(), array($button_name => t('Remove'))); + break; + } + $num_expected_remove_buttons--; + $remaining--; + + // Ensure an "Upload" button for the current field is displayed with the + // correct name. + $upload_button_name = $current_field_name . '_' . LANGUAGE_NONE . '_' . $remaining . '_upload_button'; + $buttons = $this->xpath('//input[@type="submit" and @value="Upload" and @name=:name]', array(':name' => $upload_button_name)); + $this->assertTrue(is_array($buttons) && count($buttons) == 1, format_string('The upload button is displayed with the correct name (JSMode=%type).', array('%type' => $type))); + + // Ensure only at most one button per field is displayed. + $buttons = $this->xpath('//input[@type="submit" and @value="Upload"]'); + $expected = $current_field_name == $field_name ? 1 : 2; + $this->assertTrue(is_array($buttons) && count($buttons) == $expected, format_string('After removing a file, only one "Upload" button for each possible field is displayed (JSMode=%type).', array('%type' => $type))); + } } // Ensure the page now has no Remove buttons. - $this->assertNoFieldByXPath('//input[@type="submit"]', t('Remove'), t('After removing all files, there is no "Remove" button displayed.', array('%n' => $num_expected_remove_buttons, '%type' => $type))); + $this->assertNoFieldByXPath('//input[@type="submit"]', t('Remove'), format_string('After removing all files, there is no "Remove" button displayed (JSMode=%type).', array('%type' => $type))); // Save the node and ensure it does not have any files. $this->drupalPost(NULL, array('title' => $this->randomName()), t('Save')); @@ -458,7 +831,7 @@ preg_match('/node\/([0-9]+)/', $this->getUrl(), $matches); $nid = $matches[1]; $node = node_load($nid, NULL, TRUE); - $this->assertTrue(empty($node->{$field_name}[LANGUAGE_NONE][0]['fid']), t('Node was successfully saved without any files.')); + $this->assertTrue(empty($node->{$field_name}[LANGUAGE_NONE][0]['fid']), 'Node was successfully saved without any files.'); } } @@ -484,21 +857,21 @@ $nid = $this->uploadNodeFile($test_file, $field_name, $type_name); $node = node_load($nid, NULL, TRUE); $node_file = (object) $node->{$field_name}[LANGUAGE_NONE][0]; - $this->assertFileExists($node_file, t('New file saved to disk on node creation.')); + $this->assertFileExists($node_file, 'New file saved to disk on node creation.'); // Ensure the private file is available to the user who uploaded it. $this->drupalGet(file_create_url($node_file->uri)); - $this->assertResponse(200, t('Confirmed that the generated URL is correct by downloading the shipped file.')); + $this->assertResponse(200, 'Confirmed that the generated URL is correct by downloading the shipped file.'); // Ensure we can't change 'uri_scheme' field settings while there are some // entities with uploaded files. $this->drupalGet("admin/structure/types/manage/$type_name/fields/$field_name"); - $this->assertFieldByXpath('//input[@id="edit-field-settings-uri-scheme-public" and @disabled="disabled"]', 'public', t('Upload destination setting disabled.')); + $this->assertFieldByXpath('//input[@id="edit-field-settings-uri-scheme-public" and @disabled="disabled"]', 'public', 'Upload destination setting disabled.'); // Delete node and confirm that setting could be changed. node_delete($nid); $this->drupalGet("admin/structure/types/manage/$type_name/fields/$field_name"); - $this->assertFieldByXpath('//input[@id="edit-field-settings-uri-scheme-public" and not(@disabled)]', 'public', t('Upload destination setting enabled.')); + $this->assertFieldByXpath('//input[@id="edit-field-settings-uri-scheme-public" and not(@disabled)]', 'public', 'Upload destination setting enabled.'); } /** @@ -509,7 +882,7 @@ // Remove access comments permission from anon user. $edit = array( - '1[access comments]' => FALSE, + DRUPAL_ANONYMOUS_RID . '[access comments]' => FALSE, ); $this->drupalPost('admin/people/permissions', $edit, t('Save permissions')); @@ -531,6 +904,7 @@ 'title' => $this->randomName(), ); $this->drupalPost('node/add/article', $edit, t('Save')); + $node = $this->drupalGetNodeByTitle($edit['title']); // Add a comment with a file. $text_file = $this->getTestFile('text'); @@ -549,23 +923,35 @@ $comment = comment_load($cid); $comment_file = (object) $comment->{'field_' . $name}[LANGUAGE_NONE][0]; - $this->assertFileExists($comment_file, t('New file saved to disk on node creation.')); + $this->assertFileExists($comment_file, 'New file saved to disk on node creation.'); // Test authenticated file download. $url = file_create_url($comment_file->uri); - $this->assertNotEqual($url, NULL, t('Confirmed that the URL is valid')); + $this->assertNotEqual($url, NULL, 'Confirmed that the URL is valid'); $this->drupalGet(file_create_url($comment_file->uri)); - $this->assertResponse(200, t('Confirmed that the generated URL is correct by downloading the shipped file.')); + $this->assertResponse(200, 'Confirmed that the generated URL is correct by downloading the shipped file.'); // Test anonymous file download. $this->drupalLogout(); $this->drupalGet(file_create_url($comment_file->uri)); - $this->assertResponse(403, t('Confirmed that access is denied for the file without the needed permission.')); + $this->assertResponse(403, 'Confirmed that access is denied for the file without the needed permission.'); + + // Unpublishes node. + $this->drupalLogin($this->admin_user); + $edit = array( + 'status' => FALSE, + ); + $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save')); + + // Ensures normal user can no longer download the file. + $this->drupalLogin($user); + $this->drupalGet(file_create_url($comment_file->uri)); + $this->assertResponse(403, 'Confirmed that access is denied for the file without the needed permission.'); } } /** - * Test class to test file handling with node revisions. + * Tests file handling with node revisions. */ class FileFieldRevisionTestCase extends FileFieldTestCase { public static function getInfo() { @@ -577,7 +963,7 @@ } /** - * Test creating multiple revisions of a node and managing the attached files. + * Tests creating multiple revisions of a node and managing attached files. * * Expected behaviors: * - Adding a new revision will make another entry in the field table, but @@ -606,25 +992,25 @@ $node = node_load($nid, NULL, TRUE); $node_file_r1 = (object) $node->{$field_name}[LANGUAGE_NONE][0]; $node_vid_r1 = $node->vid; - $this->assertFileExists($node_file_r1, t('New file saved to disk on node creation.')); - $this->assertFileEntryExists($node_file_r1, t('File entry exists in database on node creation.')); - $this->assertFileIsPermanent($node_file_r1, t('File is permanent.')); + $this->assertFileExists($node_file_r1, 'New file saved to disk on node creation.'); + $this->assertFileEntryExists($node_file_r1, 'File entry exists in database on node creation.'); + $this->assertFileIsPermanent($node_file_r1, 'File is permanent.'); // Upload another file to the same node in a new revision. $this->replaceNodeFile($test_file, $field_name, $nid); $node = node_load($nid, NULL, TRUE); $node_file_r2 = (object) $node->{$field_name}[LANGUAGE_NONE][0]; $node_vid_r2 = $node->vid; - $this->assertFileExists($node_file_r2, t('Replacement file exists on disk after creating new revision.')); - $this->assertFileEntryExists($node_file_r2, t('Replacement file entry exists in database after creating new revision.')); - $this->assertFileIsPermanent($node_file_r2, t('Replacement file is permanent.')); + $this->assertFileExists($node_file_r2, 'Replacement file exists on disk after creating new revision.'); + $this->assertFileEntryExists($node_file_r2, 'Replacement file entry exists in database after creating new revision.'); + $this->assertFileIsPermanent($node_file_r2, 'Replacement file is permanent.'); // Check that the original file is still in place on the first revision. $node = node_load($nid, $node_vid_r1, TRUE); - $this->assertEqual($node_file_r1, (object) $node->{$field_name}[LANGUAGE_NONE][0], t('Original file still in place after replacing file in new revision.')); - $this->assertFileExists($node_file_r1, t('Original file still in place after replacing file in new revision.')); - $this->assertFileEntryExists($node_file_r1, t('Original file entry still in place after replacing file in new revision')); - $this->assertFileIsPermanent($node_file_r1, t('Original file is still permanent.')); + $this->assertEqual($node_file_r1, (object) $node->{$field_name}[LANGUAGE_NONE][0], 'Original file still in place after replacing file in new revision.'); + $this->assertFileExists($node_file_r1, 'Original file still in place after replacing file in new revision.'); + $this->assertFileEntryExists($node_file_r1, 'Original file entry still in place after replacing file in new revision'); + $this->assertFileIsPermanent($node_file_r1, 'Original file is still permanent.'); // Save a new version of the node without any changes. // Check that the file is still the same as the previous revision. @@ -632,23 +1018,23 @@ $node = node_load($nid, NULL, TRUE); $node_file_r3 = (object) $node->{$field_name}[LANGUAGE_NONE][0]; $node_vid_r3 = $node->vid; - $this->assertEqual($node_file_r2, $node_file_r3, t('Previous revision file still in place after creating a new revision without a new file.')); - $this->assertFileIsPermanent($node_file_r3, t('New revision file is permanent.')); + $this->assertEqual($node_file_r2, $node_file_r3, 'Previous revision file still in place after creating a new revision without a new file.'); + $this->assertFileIsPermanent($node_file_r3, 'New revision file is permanent.'); // Revert to the first revision and check that the original file is active. $this->drupalPost('node/' . $nid . '/revisions/' . $node_vid_r1 . '/revert', array(), t('Revert')); $node = node_load($nid, NULL, TRUE); $node_file_r4 = (object) $node->{$field_name}[LANGUAGE_NONE][0]; $node_vid_r4 = $node->vid; - $this->assertEqual($node_file_r1, $node_file_r4, t('Original revision file still in place after reverting to the original revision.')); - $this->assertFileIsPermanent($node_file_r4, t('Original revision file still permanent after reverting to the original revision.')); + $this->assertEqual($node_file_r1, $node_file_r4, 'Original revision file still in place after reverting to the original revision.'); + $this->assertFileIsPermanent($node_file_r4, 'Original revision file still permanent after reverting to the original revision.'); // Delete the second revision and check that the file is kept (since it is // still being used by the third revision). $this->drupalPost('node/' . $nid . '/revisions/' . $node_vid_r2 . '/delete', array(), t('Delete')); - $this->assertFileExists($node_file_r3, t('Second file is still available after deleting second revision, since it is being used by the third revision.')); - $this->assertFileEntryExists($node_file_r3, t('Second file entry is still available after deleting second revision, since it is being used by the third revision.')); - $this->assertFileIsPermanent($node_file_r3, t('Second file entry is still permanent after deleting second revision, since it is being used by the third revision.')); + $this->assertFileExists($node_file_r3, 'Second file is still available after deleting second revision, since it is being used by the third revision.'); + $this->assertFileEntryExists($node_file_r3, 'Second file entry is still available after deleting second revision, since it is being used by the third revision.'); + $this->assertFileIsPermanent($node_file_r3, 'Second file entry is still permanent after deleting second revision, since it is being used by the third revision.'); // Attach the second file to a user. $user = $this->drupalCreateUser(); @@ -659,9 +1045,9 @@ // Delete the third revision and check that the file is not deleted yet. $this->drupalPost('node/' . $nid . '/revisions/' . $node_vid_r3 . '/delete', array(), t('Delete')); - $this->assertFileExists($node_file_r3, t('Second file is still available after deleting third revision, since it is being used by the user.')); - $this->assertFileEntryExists($node_file_r3, t('Second file entry is still available after deleting third revision, since it is being used by the user.')); - $this->assertFileIsPermanent($node_file_r3, t('Second file entry is still permanent after deleting third revision, since it is being used by the user.')); + $this->assertFileExists($node_file_r3, 'Second file is still available after deleting third revision, since it is being used by the user.'); + $this->assertFileEntryExists($node_file_r3, 'Second file entry is still available after deleting third revision, since it is being used by the user.'); + $this->assertFileIsPermanent($node_file_r3, 'Second file entry is still permanent after deleting third revision, since it is being used by the user.'); // Delete the user and check that the file is also deleted. user_delete($user->uid); @@ -669,18 +1055,18 @@ // not be necessary here. The file really is deleted, but stream wrappers // doesn't seem to think so unless we clear the PHP file stat() cache. clearstatcache(); - $this->assertFileNotExists($node_file_r3, t('Second file is now deleted after deleting third revision, since it is no longer being used by any other nodes.')); - $this->assertFileEntryNotExists($node_file_r3, t('Second file entry is now deleted after deleting third revision, since it is no longer being used by any other nodes.')); + $this->assertFileNotExists($node_file_r3, 'Second file is now deleted after deleting third revision, since it is no longer being used by any other nodes.'); + $this->assertFileEntryNotExists($node_file_r3, 'Second file entry is now deleted after deleting third revision, since it is no longer being used by any other nodes.'); // Delete the entire node and check that the original file is deleted. $this->drupalPost('node/' . $nid . '/delete', array(), t('Delete')); - $this->assertFileNotExists($node_file_r1, t('Original file is deleted after deleting the entire node with two revisions remaining.')); - $this->assertFileEntryNotExists($node_file_r1, t('Original file entry is deleted after deleting the entire node with two revisions remaining.')); + $this->assertFileNotExists($node_file_r1, 'Original file is deleted after deleting the entire node with two revisions remaining.'); + $this->assertFileEntryNotExists($node_file_r1, 'Original file entry is deleted after deleting the entire node with two revisions remaining.'); } } /** - * Test class to check that formatters are working properly. + * Tests that formatters are working properly. */ class FileFieldDisplayTestCase extends FileFieldTestCase { public static function getInfo() { @@ -692,7 +1078,7 @@ } /** - * Test normal formatter display on node display. + * Tests normal formatter display on node display. */ function testNodeDisplay() { $field_name = strtolower($this->randomName()); @@ -700,6 +1086,7 @@ $field_settings = array( 'display_field' => '1', 'display_default' => '1', + 'cardinality' => FIELD_CARDINALITY_UNLIMITED, ); $instance_settings = array( 'description_field' => '1', @@ -709,6 +1096,19 @@ $field = field_info_field($field_name); $instance = field_info_instance('node', $field_name, $type_name); + // Create a new node *without* the file field set, and check that the field + // is not shown for each node display. + $node = $this->drupalCreateNode(array('type' => $type_name)); + $file_formatters = array('file_default', 'file_table', 'file_url_plain', 'hidden'); + foreach ($file_formatters as $formatter) { + $edit = array( + "fields[$field_name][type]" => $formatter, + ); + $this->drupalPost("admin/structure/types/manage/$type_name/display", $edit, t('Save')); + $this->drupalGet('node/' . $node->nid); + $this->assertNoText($field_name, format_string('Field label is hidden when no file attached for formatter %formatter', array('%formatter' => $formatter))); + } + $test_file = $this->getTestFile('text'); // Create a new node with the uploaded file. @@ -719,19 +1119,58 @@ $node = node_load($nid, NULL, TRUE); $node_file = (object) $node->{$field_name}[LANGUAGE_NONE][0]; $default_output = theme('file_link', array('file' => $node_file)); - $this->assertRaw($default_output, t('Default formatter displaying correctly on full node view.')); + $this->assertRaw($default_output, 'Default formatter displaying correctly on full node view.'); // Turn the "display" option off and check that the file is no longer displayed. $edit = array($field_name . '[' . LANGUAGE_NONE . '][0][display]' => FALSE); $this->drupalPost('node/' . $nid . '/edit', $edit, t('Save')); - $this->assertNoRaw($default_output, t('Field is hidden when "display" option is unchecked.')); + $this->assertNoRaw($default_output, 'Field is hidden when "display" option is unchecked.'); + + // Test that fields appear as expected during the preview. + // Add a second file. + $name = 'files[' . $field_name . '_' . LANGUAGE_NONE . '_1]'; + $edit[$name] = drupal_realpath($test_file->uri); + + // Uncheck the display checkboxes and go to the preview. + $edit[$field_name . '[' . LANGUAGE_NONE . '][0][display]'] = FALSE; + $edit[$field_name . '[' . LANGUAGE_NONE . '][1][display]'] = FALSE; + $this->drupalPost('node/' . $nid . '/edit', $edit, t('Preview')); + $this->assertRaw($field_name . '[' . LANGUAGE_NONE . '][0][display]', 'First file appears as expected.'); + $this->assertRaw($field_name . '[' . LANGUAGE_NONE . '][1][display]', 'Second file appears as expected.'); + } + + /** + * Tests default display of File Field. + */ + function testDefaultFileFieldDisplay() { + $field_name = strtolower($this->randomName()); + $type_name = 'article'; + $field_settings = array( + 'display_field' => '1', + 'display_default' => '0', + ); + $instance_settings = array( + 'description_field' => '1', + ); + $widget_settings = array(); + $this->createFileField($field_name, $type_name, $field_settings, $instance_settings, $widget_settings); + $field = field_info_field($field_name); + $instance = field_info_instance('node', $field_name, $type_name); + + $test_file = $this->getTestFile('text'); + + // Create a new node with the uploaded file. + $nid = $this->uploadNodeFile($test_file, $field_name, $type_name); + $this->drupalGet('node/' . $nid . '/edit'); + $this->assertFieldByXPath('//input[@type="checkbox" and @name="' . $field_name . '[und][0][display]"]', NULL, 'Default file display checkbox field exists.'); + $this->assertFieldByXPath('//input[@type="checkbox" and @name="' . $field_name . '[und][0][display]" and not(@checked)]', NULL, 'Default file display is off.'); } } /** - * Test class to check for various validations. + * Tests various validations. */ class FileFieldValidateTestCase extends FileFieldTestCase { protected $field; @@ -746,7 +1185,7 @@ } /** - * Test required property on file fields. + * Tests the required property on file fields. */ function testRequired() { $type_name = 'article'; @@ -761,17 +1200,17 @@ $langcode = LANGUAGE_NONE; $edit = array("title" => $this->randomName()); $this->drupalPost('node/add/' . $type_name, $edit, t('Save')); - $this->assertRaw(t('!title field is required.', array('!title' => $instance['label'])), t('Node save failed when required file field was empty.')); + $this->assertRaw(t('!title field is required.', array('!title' => $instance['label'])), 'Node save failed when required file field was empty.'); // Create a new node with the uploaded file. $nid = $this->uploadNodeFile($test_file, $field_name, $type_name); - $this->assertTrue($nid !== FALSE, t('uploadNodeFile(@test_file, @field_name, @type_name) succeeded', array('@test_file' => $test_file->uri, '@field_name' => $field_name, '@type_name' => $type_name))); + $this->assertTrue($nid !== FALSE, format_string('uploadNodeFile(@test_file, @field_name, @type_name) succeeded', array('@test_file' => $test_file->uri, '@field_name' => $field_name, '@type_name' => $type_name))); $node = node_load($nid, NULL, TRUE); $node_file = (object) $node->{$field_name}[LANGUAGE_NONE][0]; - $this->assertFileExists($node_file, t('File exists after uploading to the required field.')); - $this->assertFileEntryExists($node_file, t('File entry exists after uploading to the required field.')); + $this->assertFileExists($node_file, 'File exists after uploading to the required field.'); + $this->assertFileEntryExists($node_file, 'File entry exists after uploading to the required field.'); // Try again with a multiple value field. field_delete_field($field_name); @@ -780,21 +1219,21 @@ // Try to post a new node without uploading a file in the multivalue field. $edit = array('title' => $this->randomName()); $this->drupalPost('node/add/' . $type_name, $edit, t('Save')); - $this->assertRaw(t('!title field is required.', array('!title' => $instance['label'])), t('Node save failed when required multiple value file field was empty.')); + $this->assertRaw(t('!title field is required.', array('!title' => $instance['label'])), 'Node save failed when required multiple value file field was empty.'); // Create a new node with the uploaded file into the multivalue field. $nid = $this->uploadNodeFile($test_file, $field_name, $type_name); $node = node_load($nid, NULL, TRUE); $node_file = (object) $node->{$field_name}[LANGUAGE_NONE][0]; - $this->assertFileExists($node_file, t('File exists after uploading to the required multiple value field.')); - $this->assertFileEntryExists($node_file, t('File entry exists after uploading to the required multipel value field.')); + $this->assertFileExists($node_file, 'File exists after uploading to the required multiple value field.'); + $this->assertFileEntryExists($node_file, 'File entry exists after uploading to the required multipel value field.'); // Remove our file field. field_delete_field($field_name); } /** - * Test the max file size validator. + * Tests the max file size validator. */ function testFileMaxSize() { $type_name = 'article'; @@ -822,13 +1261,13 @@ $nid = $this->uploadNodeFile($small_file, $field_name, $type_name); $node = node_load($nid, NULL, TRUE); $node_file = (object) $node->{$field_name}[LANGUAGE_NONE][0]; - $this->assertFileExists($node_file, t('File exists after uploading a file (%filesize) under the max limit (%maxsize).', array('%filesize' => format_size($small_file->filesize), '%maxsize' => $max_filesize))); - $this->assertFileEntryExists($node_file, t('File entry exists after uploading a file (%filesize) under the max limit (%maxsize).', array('%filesize' => format_size($small_file->filesize), '%maxsize' => $max_filesize))); + $this->assertFileExists($node_file, format_string('File exists after uploading a file (%filesize) under the max limit (%maxsize).', array('%filesize' => format_size($small_file->filesize), '%maxsize' => $max_filesize))); + $this->assertFileEntryExists($node_file, format_string('File entry exists after uploading a file (%filesize) under the max limit (%maxsize).', array('%filesize' => format_size($small_file->filesize), '%maxsize' => $max_filesize))); // Check that uploading the large file fails (1M limit). $nid = $this->uploadNodeFile($large_file, $field_name, $type_name); $error_message = t('The file is %filesize exceeding the maximum file size of %maxsize.', array('%filesize' => format_size($large_file->filesize), '%maxsize' => format_size($file_limit))); - $this->assertRaw($error_message, t('Node save failed when file (%filesize) exceeded the max upload size (%maxsize).', array('%filesize' => format_size($large_file->filesize), '%maxsize' => $max_filesize))); + $this->assertRaw($error_message, format_string('Node save failed when file (%filesize) exceeded the max upload size (%maxsize).', array('%filesize' => format_size($large_file->filesize), '%maxsize' => $max_filesize))); } // Turn off the max filesize. @@ -838,15 +1277,15 @@ $nid = $this->uploadNodeFile($large_file, $field_name, $type_name); $node = node_load($nid, NULL, TRUE); $node_file = (object) $node->{$field_name}[LANGUAGE_NONE][0]; - $this->assertFileExists($node_file, t('File exists after uploading a file (%filesize) with no max limit.', array('%filesize' => format_size($large_file->filesize)))); - $this->assertFileEntryExists($node_file, t('File entry exists after uploading a file (%filesize) with no max limit.', array('%filesize' => format_size($large_file->filesize)))); + $this->assertFileExists($node_file, format_string('File exists after uploading a file (%filesize) with no max limit.', array('%filesize' => format_size($large_file->filesize)))); + $this->assertFileEntryExists($node_file, format_string('File entry exists after uploading a file (%filesize) with no max limit.', array('%filesize' => format_size($large_file->filesize)))); // Remove our file field. field_delete_field($field_name); } /** - * Test the file extension, do additional checks if mimedetect is installed. + * Tests file extension checking. */ function testFileExtension() { $type_name = 'article'; @@ -865,8 +1304,8 @@ $nid = $this->uploadNodeFile($test_file, $field_name, $type_name); $node = node_load($nid, NULL, TRUE); $node_file = (object) $node->{$field_name}[LANGUAGE_NONE][0]; - $this->assertFileExists($node_file, t('File exists after uploading a file with no extension checking.')); - $this->assertFileEntryExists($node_file, t('File entry exists after uploading a file with no extension checking.')); + $this->assertFileExists($node_file, 'File exists after uploading a file with no extension checking.'); + $this->assertFileEntryExists($node_file, 'File entry exists after uploading a file with no extension checking.'); // Enable extension checking for text files. $this->updateFileField($field_name, $type_name, array('file_extensions' => 'txt')); @@ -874,7 +1313,7 @@ // Check that the file with the wrong extension cannot be uploaded. $nid = $this->uploadNodeFile($test_file, $field_name, $type_name); $error_message = t('Only files with the following extensions are allowed: %files-allowed.', array('%files-allowed' => 'txt')); - $this->assertRaw($error_message, t('Node save failed when file uploaded with the wrong extension.')); + $this->assertRaw($error_message, 'Node save failed when file uploaded with the wrong extension.'); // Enable extension checking for text and image files. $this->updateFileField($field_name, $type_name, array('file_extensions' => "txt $test_file_extension")); @@ -883,8 +1322,8 @@ $nid = $this->uploadNodeFile($test_file, $field_name, $type_name); $node = node_load($nid, NULL, TRUE); $node_file = (object) $node->{$field_name}[LANGUAGE_NONE][0]; - $this->assertFileExists($node_file, t('File exists after uploading a file with extension checking.')); - $this->assertFileEntryExists($node_file, t('File entry exists after uploading a file with extension checking.')); + $this->assertFileExists($node_file, 'File exists after uploading a file with extension checking.'); + $this->assertFileEntryExists($node_file, 'File entry exists after uploading a file with extension checking.'); // Remove our file field. field_delete_field($field_name); @@ -892,7 +1331,7 @@ } /** - * Test class to check that files are uploaded to proper locations. + * Tests that files are uploaded to proper locations. */ class FileFieldPathTestCase extends FileFieldTestCase { public static function getInfo() { @@ -904,7 +1343,7 @@ } /** - * Test normal formatter display on node display. + * Tests the normal formatter display on node display. */ function testUploadPath() { $field_name = strtolower($this->randomName()); @@ -918,7 +1357,7 @@ // Check that the file was uploaded to the file root. $node = node_load($nid, NULL, TRUE); $node_file = (object) $node->{$field_name}[LANGUAGE_NONE][0]; - $this->assertPathMatch('public://' . $test_file->filename, $node_file->uri, t('The file %file was uploaded to the correct path.', array('%file' => $node_file->uri))); + $this->assertPathMatch('public://' . $test_file->filename, $node_file->uri, format_string('The file %file was uploaded to the correct path.', array('%file' => $node_file->uri))); // Change the path to contain multiple subdirectories. $field = $this->updateFileField($field_name, $type_name, array('file_directory' => 'foo/bar/baz')); @@ -929,11 +1368,11 @@ // Check that the file was uploaded into the subdirectory. $node = node_load($nid, NULL, TRUE); $node_file = (object) $node->{$field_name}[LANGUAGE_NONE][0]; - $this->assertPathMatch('public://foo/bar/baz/' . $test_file->filename, $node_file->uri, t('The file %file was uploaded to the correct path.', array('%file' => $node_file->uri))); + $this->assertPathMatch('public://foo/bar/baz/' . $test_file->filename, $node_file->uri, format_string('The file %file was uploaded to the correct path.', array('%file' => $node_file->uri))); // Check the path when used with tokens. // Change the path to contain multiple token directories. - $field = $this->updateFileField($field_name, $type_name, array('file_directory' => '[user:uid]/[user:name]')); + $field = $this->updateFileField($field_name, $type_name, array('file_directory' => '[current-user:uid]/[current-user:name]')); // Upload a new file into the token subdirectories. $nid = $this->uploadNodeFile($test_file, $field_name, $type_name); @@ -941,13 +1380,15 @@ // Check that the file was uploaded into the subdirectory. $node = node_load($nid, NULL, TRUE); $node_file = (object) $node->{$field_name}[LANGUAGE_NONE][0]; + // Do token replacement using the same user which uploaded the file, not + // the user running the test case. $data = array('user' => $this->admin_user); $subdirectory = token_replace('[user:uid]/[user:name]', $data); - $this->assertPathMatch('public://' . $subdirectory . '/' . $test_file->filename, $node_file->uri, t('The file %file was uploaded to the correct path with token replacements.', array('%file' => $node_file->uri))); + $this->assertPathMatch('public://' . $subdirectory . '/' . $test_file->filename, $node_file->uri, format_string('The file %file was uploaded to the correct path with token replacements.', array('%file' => $node_file->uri))); } /** - * A loose assertion to check that a file is uploaded to the right location. + * Asserts that a file is uploaded to the right location. * * @param $expected_path * The location where the file is expected to be uploaded. Duplicate file @@ -970,7 +1411,7 @@ } /** - * Test file token replacement in strings. + * Tests the file token replacement in strings. */ class FileTokenReplaceTestCase extends FileFieldTestCase { public static function getInfo() { @@ -999,47 +1440,495 @@ $instance = field_info_instance('node', $field_name, $type_name); $test_file = $this->getTestFile('text'); + // Coping a file to test uploads with non-latin filenames. + $filename = drupal_dirname($test_file->uri) . '/текстовый файл.txt'; + $test_file = file_copy($test_file, $filename); // Create a new node with the uploaded file. $nid = $this->uploadNodeFile($test_file, $field_name, $type_name); // Load the node and the file. $node = node_load($nid, NULL, TRUE); - $file = (object) $node->{$field_name}[LANGUAGE_NONE][0]; - $file->description = 'File description.'; + $file = file_load($node->{$field_name}[LANGUAGE_NONE][0]['fid']); // Generate and test sanitized tokens. $tests = array(); $tests['[file:fid]'] = $file->fid; $tests['[file:name]'] = check_plain($file->filename); - $tests['[file:description]'] = filter_xss($file->description); - $tests['[file:path]'] = filter_xss($file->uri); - $tests['[file:mime]'] = filter_xss($file->filemime); + $tests['[file:path]'] = check_plain($file->uri); + $tests['[file:mime]'] = check_plain($file->filemime); $tests['[file:size]'] = format_size($file->filesize); - $tests['[file:url]'] = url(file_create_url($file->uri), $url_options); + $tests['[file:url]'] = check_plain(file_create_url($file->uri)); $tests['[file:timestamp]'] = format_date($file->timestamp, 'medium', '', NULL, $language->language); $tests['[file:timestamp:short]'] = format_date($file->timestamp, 'short', '', NULL, $language->language); - $tests['[file:owner]'] = $this->admin_user->name; + $tests['[file:owner]'] = check_plain(format_username($this->admin_user)); $tests['[file:owner:uid]'] = $file->uid; // Test to make sure that we generated something for each token. - $this->assertFalse(in_array(0, array_map('strlen', $tests)), t('No empty tokens generated.')); + $this->assertFalse(in_array(0, array_map('strlen', $tests)), 'No empty tokens generated.'); foreach ($tests as $input => $expected) { $output = token_replace($input, array('file' => $file), array('language' => $language)); - $this->assertFalse(strcmp($output, $expected), t('Sanitized file token %token replaced.', array('%token' => $input))); + $this->assertEqual($output, $expected, format_string('Sanitized file token %token replaced.', array('%token' => $input))); } // Generate and test unsanitized tokens. $tests['[file:name]'] = $file->filename; - $tests['[file:description]'] = $file->description; $tests['[file:path]'] = $file->uri; $tests['[file:mime]'] = $file->filemime; $tests['[file:size]'] = format_size($file->filesize); foreach ($tests as $input => $expected) { $output = token_replace($input, array('file' => $file), array('language' => $language, 'sanitize' => FALSE)); - $this->assertFalse(strcmp($output, $expected), t('Unsanitized file token %token replaced.', array('%token' => $input))); + $this->assertEqual($output, $expected, format_string('Unsanitized file token %token replaced.', array('%token' => $input))); } } } + +/** + * Tests file access on private nodes. + */ +class FilePrivateTestCase extends FileFieldTestCase { + public static function getInfo() { + return array( + 'name' => 'Private file test', + 'description' => 'Uploads a test to a private node and checks access.', + 'group' => 'File', + ); + } + + function setUp() { + parent::setUp(array('node_access_test', 'field_test')); + node_access_rebuild(); + variable_set('node_access_test_private', TRUE); + } + + /** + * Tests file access for file uploaded to a private node. + */ + function testPrivateFile() { + // Use 'page' instead of 'article', so that the 'article' image field does + // not conflict with this test. If in the future the 'page' type gets its + // own default file or image field, this test can be made more robust by + // using a custom node type. + $type_name = 'page'; + $field_name = strtolower($this->randomName()); + $this->createFileField($field_name, $type_name, array('uri_scheme' => 'private')); + + // Create a field with no view access - see field_test_field_access(). + $no_access_field_name = 'field_no_view_access'; + $this->createFileField($no_access_field_name, $type_name, array('uri_scheme' => 'private')); + + $test_file = $this->getTestFile('text'); + $nid = $this->uploadNodeFile($test_file, $field_name, $type_name, TRUE, array('private' => TRUE)); + $node = node_load($nid, NULL, TRUE); + $node_file = (object) $node->{$field_name}[LANGUAGE_NONE][0]; + // Ensure the file can be downloaded. + $this->drupalGet(file_create_url($node_file->uri)); + $this->assertResponse(200, 'Confirmed that the generated URL is correct by downloading the shipped file.'); + $this->drupalLogOut(); + $this->drupalGet(file_create_url($node_file->uri)); + $this->assertResponse(403, 'Confirmed that access is denied for the file without the needed permission.'); + + // Test with the field that should deny access through field access. + $this->drupalLogin($this->admin_user); + $nid = $this->uploadNodeFile($test_file, $no_access_field_name, $type_name, TRUE, array('private' => TRUE)); + $node = node_load($nid, NULL, TRUE); + $node_file = (object) $node->{$no_access_field_name}[LANGUAGE_NONE][0]; + // Ensure the file cannot be downloaded. + $this->drupalGet(file_create_url($node_file->uri)); + $this->assertResponse(403, 'Confirmed that access is denied for the file without view field access permission.'); + + // Attempt to reuse the existing file when creating a new node, and confirm + // that access is still denied. + $edit = array(); + $edit['title'] = $this->randomName(8); + $edit[$field_name . '[' . LANGUAGE_NONE . '][0][fid]'] = $node_file->fid; + $this->drupalPost('node/add/page', $edit, t('Save')); + $new_node = $this->drupalGetNodeByTitle($edit['title']); + $this->assertTrue(!empty($new_node), 'Node was created.'); + $this->assertUrl('node/' . $new_node->nid); + $this->assertNoRaw($node_file->filename, 'File without view field access permission does not appear after attempting to attach it to a new node.'); + $this->drupalGet(file_create_url($node_file->uri)); + $this->assertResponse(403, 'Confirmed that access is denied for the file without view field access permission after attempting to attach it to a new node.'); + + // As an anonymous user, create a temporary file with no references and + // confirm that only the session that uploaded it may view it. + $this->drupalLogout(); + user_role_grant_permissions(DRUPAL_ANONYMOUS_RID, array( + "create $type_name content", + 'access content', + )); + $test_file = $this->getTestFile('text'); + $this->drupalGet('node/add/' . $type_name); + $edit = array('files[' . $field_name . '_' . LANGUAGE_NONE . '_0]' => drupal_realpath($test_file->uri)); + $this->drupalPost(NULL, $edit, t('Upload')); + $files = file_load_multiple(array(), array('uid' => 0)); + $this->assertEqual(1, count($files), 'Loaded one anonymous file.'); + $file = end($files); + $this->assertNotEqual($file->status, FILE_STATUS_PERMANENT, 'File is temporary.'); + $usage = file_usage_list($file); + $this->assertFalse($usage, 'No file usage found.'); + $file_url = file_create_url($file->uri); + $this->drupalGet($file_url); + $this->assertResponse(200, 'Confirmed that the anonymous uploader has access to the temporary file.'); + // Close the prior connection and remove the session cookie. + $this->curlClose(); + $this->cookies = array(); + $this->drupalGet($file_url); + $this->assertResponse(403, 'Confirmed that another anonymous user cannot access the temporary file.'); + + // As an anonymous user, create a permanent file that is referenced by a + // published node and confirm that all anonymous users may view it. + $test_file = $this->getTestFile('text'); + $this->drupalGet('node/add/' . $type_name); + $edit = array(); + $edit['title'] = $this->randomName(); + $edit['files[' . $field_name . '_' . LANGUAGE_NONE . '_0]'] = drupal_realpath($test_file->uri); + $this->drupalPost(NULL, $edit, t('Save')); + $new_node = $this->drupalGetNodeByTitle($edit['title']); + $file = file_load($new_node->{$field_name}[LANGUAGE_NONE][0]['fid']); + $this->assertEqual($file->status, FILE_STATUS_PERMANENT, 'File is permanent.'); + $usage = file_usage_list($file); + $this->assertTrue($usage, 'File usage found.'); + $file_url = file_create_url($file->uri); + $this->drupalGet($file_url); + $this->assertResponse(200, 'Confirmed that the anonymous uploader has access to the permanent file that is referenced by a published node.'); + // Close the prior connection and remove the session cookie. + $this->curlClose(); + $this->cookies = array(); + $this->drupalGet($file_url); + $this->assertResponse(200, 'Confirmed that another anonymous user also has access to the permanent file that is referenced by a published node.'); + + // As an anonymous user, create a permanent file that is referenced by an + // unpublished node and confirm that no anonymous users may view it (even + // the session that uploaded the file) because they cannot view the + // unpublished node. + $test_file = $this->getTestFile('text'); + $this->drupalGet('node/add/' . $type_name); + $edit = array(); + $edit['title'] = $this->randomName(); + $edit['files[' . $field_name . '_' . LANGUAGE_NONE . '_0]'] = drupal_realpath($test_file->uri); + $this->drupalPost(NULL, $edit, t('Save')); + $new_node = $this->drupalGetNodeByTitle($edit['title']); + $new_node->status = NODE_NOT_PUBLISHED; + node_save($new_node); + $file = file_load($new_node->{$field_name}[LANGUAGE_NONE][0]['fid']); + $this->assertEqual($file->status, FILE_STATUS_PERMANENT, 'File is permanent.'); + $usage = file_usage_list($file); + $this->assertTrue($usage, 'File usage found.'); + $file_url = file_create_url($file->uri); + $this->drupalGet($file_url); + $this->assertResponse(403, 'Confirmed that the anonymous uploader cannot access the permanent file when it is referenced by an unpublished node.'); + // Close the prior connection and remove the session cookie. + $this->curlClose(); + $this->cookies = array(); + $this->drupalGet($file_url); + $this->assertResponse(403, 'Confirmed that another anonymous user cannot access the permanent file when it is referenced by an unpublished node.'); + } + + /** + * Tests file access for private nodes when file download access is granted. + */ + function testPrivateFileDownloadAccessGranted() { + // Tell file_module_test to attempt to grant access to all private files, + // and ensure that it is doing so correctly. + $test_file = $this->getTestFile('text'); + $uri = file_unmanaged_move($test_file->uri, 'private://'); + $file_url = file_create_url($uri); + $this->drupalGet($file_url); + $this->assertResponse(403, 'Access is not granted to an arbitrary private file by default.'); + variable_set('file_module_test_grant_download_access', TRUE); + $this->drupalGet($file_url); + $this->assertResponse(200, 'Access is granted to an arbitrary private file after a module grants access to all private files in hook_file_download().'); + + // Create a public node with a file attached. + $type_name = 'page'; + $field_name = strtolower($this->randomName()); + $this->createFileField($field_name, $type_name, array('uri_scheme' => 'private')); + $test_file = $this->getTestFile('text'); + $nid = $this->uploadNodeFile($test_file, $field_name, $type_name, TRUE, array('private' => FALSE)); + $node = node_load($nid, NULL, TRUE); + $file_url = file_create_url($node->{$field_name}[LANGUAGE_NONE][0]['uri']); + + // Unpublish the node and ensure that only administrators (not anonymous + // users) can access the node and download the file; the expectation is + // that the File module's hook_file_download() implementation will deny + // access and thereby override the file_module_test module's access grant. + $node->status = NODE_NOT_PUBLISHED; + node_save($node); + $this->drupalLogin($this->admin_user); + $this->drupalGet("node/$nid"); + $this->assertResponse(200, 'Administrator can access the unpublished node.'); + $this->drupalGet($file_url); + $this->assertResponse(200, 'Administrator can download the file attached to the unpublished node.'); + $this->drupalLogOut(); + $this->drupalGet("node/$nid"); + $this->assertResponse(403, 'Anonymous user cannot access the unpublished node.'); + $this->drupalGet($file_url); + $this->assertResponse(403, 'Anonymous user cannot download the file attached to the unpublished node.'); + + // Re-publish the node and ensure that the node and file can be accessed by + // everyone. + $node->status = NODE_PUBLISHED; + node_save($node); + $this->drupalLogin($this->admin_user); + $this->drupalGet("node/$nid"); + $this->assertResponse(200, 'Administrator can access the published node.'); + $this->drupalGet($file_url); + $this->assertResponse(200, 'Administrator can download the file attached to the published node.'); + $this->drupalLogOut(); + $this->drupalGet("node/$nid"); + $this->assertResponse(200, 'Anonymous user can access the published node.'); + $this->drupalGet($file_url); + $this->assertResponse(200, 'Anonymous user can download the file attached to the published node.'); + + // Make the node private via the node access system and test that only + // administrators (not anonymous users) can access the node and download + // the file. + $node->private = TRUE; + node_save($node); + $this->drupalLogin($this->admin_user); + $this->drupalGet("node/$nid"); + $this->assertResponse(200, 'Administrator can access the private node.'); + $this->drupalGet($file_url); + $this->assertResponse(200, 'Administrator can download the file attached to the private node.'); + $this->drupalLogOut(); + $this->drupalGet("node/$nid"); + $this->assertResponse(403, 'Anonymous user cannot access the private node.'); + $this->drupalGet($file_url); + $this->assertResponse(403, 'Anonymous user cannot download the file attached to the private node.'); + } +} + +/** + * Confirm that file field submissions work correctly for anonymous visitors. + */ +class FileFieldAnonymousSubmission extends FileFieldTestCase { + + public static function getInfo() { + return array( + 'name' => 'File form anonymous submission', + 'description' => 'Test anonymous form submission.', + 'group' => 'File', + ); + } + + function setUp() { + parent::setUp(); + + // Allow node submissions by anonymous users. + user_role_grant_permissions(DRUPAL_ANONYMOUS_RID, array( + 'create article content', + 'access content', + )); + } + + /** + * Tests the basic node submission for an anonymous visitor. + */ + function testAnonymousNode() { + $bundle_label = 'Article'; + $node_title = 'Test page'; + + // Load the node form. + $this->drupalGet('node/add/article'); + $this->assertResponse(200, 'Loaded the article node form.'); + $this->assertText(strip_tags(t('Create @name', array('@name' => $bundle_label)))); + + $edit = array( + 'title' => $node_title, + 'body[und][0][value]' => 'Test article', + 'body[und][0][format]' => 'filtered_html', + ); + $this->drupalPost(NULL, $edit, t('Save')); + $this->assertResponse(200); + $t_args = array('@type' => $bundle_label, '%title' => $node_title); + $this->assertText(strip_tags(t('@type %title has been created.', $t_args)), 'The node was created.'); + $matches = array(); + if (preg_match('@node/(\d+)$@', $this->getUrl(), $matches)) { + $nid = end($matches); + $this->assertNotEqual($nid, 0, 'The node ID was extracted from the URL.'); + $node = node_load($nid); + $this->assertNotEqual($node, NULL, 'The node was loaded successfully.'); + } + } + + /** + * Tests file submission for an anonymous visitor. + */ + function testAnonymousNodeWithFile() { + $bundle_label = 'Article'; + $node_title = 'Test page'; + + // Load the node form. + $this->drupalGet('node/add/article'); + $this->assertResponse(200, 'Loaded the article node form.'); + $this->assertText(strip_tags(t('Create @name', array('@name' => $bundle_label)))); + + // Generate an image file. + $image = $this->getTestImage(); + + // Submit the form. + $edit = array( + 'title' => $node_title, + 'body[und][0][value]' => 'Test article', + 'body[und][0][format]' => 'filtered_html', + 'files[field_image_und_0]' => drupal_realpath($image->uri), + ); + $this->drupalPost(NULL, $edit, t('Save')); + $this->assertResponse(200); + $t_args = array('@type' => $bundle_label, '%title' => $node_title); + $this->assertText(strip_tags(t('@type %title has been created.', $t_args)), 'The node was created.'); + $matches = array(); + if (preg_match('@node/(\d+)$@', $this->getUrl(), $matches)) { + $nid = end($matches); + $this->assertNotEqual($nid, 0, 'The node ID was extracted from the URL.'); + $node = node_load($nid); + $this->assertNotEqual($node, NULL, 'The node was loaded successfully.'); + $this->assertEqual($node->field_image[LANGUAGE_NONE][0]['filename'], $image->filename, 'The image was uploaded successfully.'); + } + } + + /** + * Tests file submission for an anonymous visitor with a missing node title. + */ + function testAnonymousNodeWithFileWithoutTitle() { + $this->drupalLogout(); + $this->_testNodeWithFileWithoutTitle(); + } + + /** + * Tests file submission for an authenticated user with a missing node title. + */ + function testAuthenticatedNodeWithFileWithoutTitle() { + $admin_user = $this->drupalCreateUser(array( + 'bypass node access', + 'access content overview', + 'administer nodes', + )); + $this->drupalLogin($admin_user); + $this->_testNodeWithFileWithoutTitle(); + } + + /** + * Helper method to test file submissions with missing node titles. + */ + protected function _testNodeWithFileWithoutTitle() { + $bundle_label = 'Article'; + $node_title = 'Test page'; + + // Load the node form. + $this->drupalGet('node/add/article'); + $this->assertResponse(200, 'Loaded the article node form.'); + $this->assertText(strip_tags(t('Create @name', array('@name' => $bundle_label)))); + + // Generate an image file. + $image = $this->getTestImage(); + + // Submit the form but exclude the title field. + $edit = array( + 'body[und][0][value]' => 'Test article', + 'body[und][0][format]' => 'filtered_html', + 'files[field_image_und_0]' => drupal_realpath($image->uri), + ); + $this->drupalPost(NULL, $edit, t('Save')); + $this->assertResponse(200); + $t_args = array('@type' => $bundle_label, '%title' => $node_title); + $this->assertNoText(strip_tags(t('@type %title has been created.', $t_args)), 'The node was created.'); + $this->assertText(t('!name field is required.', array('!name' => t('Title')))); + + // Submit the form again but this time with the missing title field. This + // should still work. + $edit = array( + 'title' => $node_title, + ); + $this->drupalPost(NULL, $edit, t('Save')); + + // Confirm the final submission actually worked. + $t_args = array('@type' => $bundle_label, '%title' => $node_title); + $this->assertText(strip_tags(t('@type %title has been created.', $t_args)), 'The node was created.'); + $matches = array(); + if (preg_match('@node/(\d+)$@', $this->getUrl(), $matches)) { + $nid = end($matches); + $this->assertNotEqual($nid, 0, 'The node ID was extracted from the URL.'); + $node = node_load($nid); + $this->assertNotEqual($node, NULL, 'The node was loaded successfully.'); + $this->assertEqual($node->field_image[LANGUAGE_NONE][0]['filename'], $image->filename, 'The image was uploaded successfully.'); + } + } + + /** + * Generates a test image. + * + * @return stdClass + * A file object. + */ + function getTestImage() { + // Get a file to upload. + $file = current($this->drupalGetTestFiles('image')); + + // Add a filesize property to files as would be read by file_load(). + $file->filesize = filesize($file->uri); + + return $file; + } + +} + +/** + * Tests the file_scan_directory() function. + */ +class FileScanDirectory extends FileFieldTestCase { + + /** + * @var string + */ + protected $path; + + /** + * {@inheritdoc} + */ + public static function getInfo() { + return array( + 'name' => 'File ScanDirectory', + 'description' => 'Tests the file_scan_directory() function.', + 'group' => 'File', + ); + } + + /** + * {@inheritdoc} + */ + function setUp() { + parent::setUp(); + + $this->path = 'modules/file/tests/fixtures/file_scan_ignore'; + } + + /** + * Tests file_scan_directory() obeys 'file_scan_ignore_directories' setting. + * If nomask is not passed as argument, it should use the default settings. + * If nomask is passed as argument, it should obey this rule. + */ + public function testNoMask() { + $files = file_scan_directory($this->path, '/\.txt$/'); + $this->assertEqual(3, count($files), '3 text files found when not ignoring directories.'); + + global $conf; + $conf['file_scan_ignore_directories'] = array('frontend_framework'); + + $files = file_scan_directory($this->path, '/\.txt$/'); + $this->assertEqual(1, count($files), '1 text files found when ignoring directories called "frontend_framework".'); + + // Make that directories specified by default still work when a new nomask is provided. + $files = file_scan_directory($this->path, '/\.txt$/', array('nomask' => '/^c.txt/')); + $this->assertEqual(2, count($files), '2 text files found when an "nomask" option is passed in.'); + + // Ensure that the directories in file_scan_ignore_directories are escaped using preg_quote. + $conf['file_scan_ignore_directories'] = array('frontend.*'); + $files = file_scan_directory($this->path, '/\.txt$/'); + $this->assertEqual(3, count($files), '2 text files found when ignoring a directory that is not there.'); + } + +} diff -Naur drupal-7.0/modules/file/tests/file_module_test.info drupal-7.66/modules/file/tests/file_module_test.info --- drupal-7.0/modules/file/tests/file_module_test.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/file/tests/file_module_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: file_module_test.info,v 1.2 2010/12/20 19:59:41 webchick Exp $ name = File test description = Provides hooks for testing File module functionality. package = Core @@ -6,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/file/tests/file_module_test.module drupal-7.66/modules/file/tests/file_module_test.module --- drupal-7.0/modules/file/tests/file_module_test.module 2010-11-13 15:04:08.000000000 +0100 +++ drupal-7.66/modules/file/tests/file_module_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ 'public://test', '#progress_message' => t('Please wait...'), '#extended' => (bool) $extended, + '#size' => 13, ); if ($default_fid) { $form['nested']['file']['#default_value'] = $extended ? array('fid' => $default_fid) : $default_fid; @@ -64,3 +67,18 @@ } drupal_set_message(t('The file id is %fid.', array('%fid' => $fid))); } + +/** + * Implements hook_file_download(). + */ +function file_module_test_file_download($uri) { + if (variable_get('file_module_test_grant_download_access')) { + // Mimic what file_get_content_headers() would do if we had a full $file + // object to pass to it. + return array( + 'Content-Type' => mime_header_encode(file_get_mimetype($uri)), + 'Content-Length' => filesize($uri), + 'Cache-Control' => 'private', + ); + } +} diff -Naur drupal-7.0/modules/filter/filter.admin.inc drupal-7.66/modules/filter/filter.admin.inc --- drupal-7.0/modules/filter/filter.admin.inc 2010-12-14 03:40:15.000000000 +0100 +++ drupal-7.66/modules/filter/filter.admin.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,15 +1,15 @@ $data) { if (is_array($data) && isset($data['weight'])) { @@ -96,7 +99,26 @@ } /** - * Menu callback; Display a text format form. + * Page callback: Displays the text format add/edit form. + * + * @param object|null $format + * (optional) An object representing a format, with the following properties: + * - format: A machine-readable name representing the ID of the text format + * to save. If this corresponds to an existing text format, that format + * will be updated; otherwise, a new format will be created. + * - name: The title of the text format. + * - cache: (optional) An integer indicating whether the text format is + * cacheable (1) or not (0). Defaults to 1. + * - status: (optional) An integer indicating whether the text format is + * enabled (1) or not (0). Defaults to 1. + * - weight: (optional) The weight of the text format, which controls its + * placement in text format lists. If omitted, the weight is set to 0. + * Defaults to NULL. + * + * @return + * A form array. + * + * @see filter_menu() */ function filter_admin_format_page($format = NULL) { if (!isset($format->name)) { @@ -110,11 +132,24 @@ } /** - * Generate a text format form. + * Form constructor for the text format add/edit form. + * + * @param $format + * A format object having the properties: + * - format: A machine-readable name representing the ID of the text format to + * save. If this corresponds to an existing text format, that format will be + * updated; otherwise, a new format will be created. + * - name: The title of the text format. + * - cache: An integer indicating whether the text format is cacheable (1) or + * not (0). Defaults to 1. + * - status: (optional) An integer indicating whether the text format is + * enabled (1) or not (0). Defaults to 1. + * - weight: (optional) The weight of the text format, which controls its + * placement in text format lists. If omitted, the weight is set to 0. * - * @ingroup forms * @see filter_admin_format_form_validate() * @see filter_admin_format_form_submit() + * @ingroup forms */ function filter_admin_format_form($form, &$form_state, $format) { $is_fallback = ($format->format == filter_fallback_format()); @@ -288,7 +323,9 @@ } /** - * Validate text format form submissions. + * Form validation handler for filter_admin_format_form(). + * + * @see filter_admin_format_form_submit() */ function filter_admin_format_form_validate($form, &$form_state) { $format_format = trim($form_state['values']['format']); @@ -305,7 +342,9 @@ } /** - * Process text format form submissions. + * Form submission handler for filter_admin_format_form(). + * + * @see filter_admin_format_form_validate() */ function filter_admin_format_form_submit($form, &$form_state) { // Remove unnecessary values. @@ -337,10 +376,14 @@ } /** - * Menu callback; confirm deletion of a format. + * Form constructor for the text format deletion confirmation form. * - * @ingroup forms + * @param $format + * An object representing a text format. + * + * @see filter_menu() * @see filter_admin_disable_submit() + * @ingroup forms */ function filter_admin_disable($form, &$form_state, $format) { $form['#format'] = $format; @@ -354,7 +397,7 @@ } /** - * Process filter disable form submission. + * Form submission handler for filter_admin_disable(). */ function filter_admin_disable_submit($form, &$form_state) { $format = $form['#format']; @@ -363,4 +406,3 @@ $form_state['redirect'] = 'admin/config/content/formats'; } - diff -Naur drupal-7.0/modules/filter/filter.admin.js drupal-7.66/modules/filter/filter.admin.js --- drupal-7.0/modules/filter/filter.admin.js 2010-04-16 15:55:06.000000000 +0200 +++ drupal-7.66/modules/filter/filter.admin.js 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -// $Id: filter.admin.js,v 1.4 2010/04/16 13:55:06 dries Exp $ (function ($) { Drupal.behaviors.filterStatus = { diff -Naur drupal-7.0/modules/filter/filter.api.php drupal-7.66/modules/filter/filter.api.php --- drupal-7.0/modules/filter/filter.api.php 2010-12-09 03:04:16.000000000 +0100 +++ drupal-7.66/modules/filter/filter.api.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ settings and $defaults. + * + * @ingroup callbacks */ -function hook_filter_FILTER_settings($form, &$form_state, $filter, $format, $defaults, $filters) { +function callback_filter_settings($form, &$form_state, $filter, $format, $defaults, $filters) { $filter->settings += $defaults; $elements = array(); @@ -173,11 +172,9 @@ } /** - * Prepare callback for hook_filter_info(). + * Provide prepared text with special characters escaped. * - * Note: This is not really a hook. The function name is manually specified via - * 'prepare callback' in hook_filter_info(), with this recommended callback - * name pattern. It is called from check_markup(). + * Callback for hook_filter_info(). * * See hook_filter_info() for a description of the filtering process. Filters * should not use the 'prepare callback' step for anything other than escaping, @@ -200,19 +197,19 @@ * * @return * The prepared, escaped text. + * + * @ingroup callbacks */ -function hook_filter_FILTER_prepare($text, $filter, $format, $langcode, $cache, $cache_id) { +function callback_filter_prepare($text, $filter, $format, $langcode, $cache, $cache_id) { // Escape and tags. $text = preg_replace('|(.+?)|se', "[codefilter_code]$1[/codefilter_code]", $text); return $text; } /** - * Process callback for hook_filter_info(). + * Provide text filtered to conform to the supplied format. * - * Note: This is not really a hook. The function name is manually specified via - * 'process callback' in hook_filter_info(), with this recommended callback - * name pattern. It is called from check_markup(). + * Callback for hook_filter_info(). * * See hook_filter_info() for a description of the filtering process. This step * is where the filter actually transforms the text. @@ -233,19 +230,19 @@ * * @return * The filtered text. + * + * @ingroup callbacks */ -function hook_filter_FILTER_process($text, $filter, $format, $langcode, $cache, $cache_id) { +function callback_filter_process($text, $filter, $format, $langcode, $cache, $cache_id) { $text = preg_replace('|\[codefilter_code\](.+?)\[/codefilter_code\]|se', "
$1
", $text); return $text; } /** - * Tips callback for hook_filter_info(). + * Return help text for a filter. * - * Note: This is not really a hook. The function name is manually specified via - * 'tips callback' in hook_filter_info(), with this recommended callback - * name pattern. It is called from _filter_tips(). + * Callback for hook_filter_info(). * * A filter's tips should be informative and to the point. Short tips are * preferably one-liners. @@ -261,8 +258,10 @@ * * @return * Translated text to display as a tip. + * + * @ingroup callbacks */ -function hook_filter_FILTER_tips($filter, $format, $long) { +function callback_filter_tips($filter, $format, $long) { if ($long) { return t('Lines and paragraphs are automatically recognized. The <br /> line break, <p> paragraph and </p> close paragraph tags are inserted automatically. If paragraphs are not recognized simply add a couple blank lines.'); } diff -Naur drupal-7.0/modules/filter/filter.css drupal-7.66/modules/filter/filter.css --- drupal-7.0/modules/filter/filter.css 2010-09-19 20:10:41.000000000 +0200 +++ drupal-7.66/modules/filter/filter.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -/* $Id: filter.css,v 1.6 2010/09/19 18:10:41 dries Exp $ */ .text-format-wrapper .form-item { margin-bottom: 0; diff -Naur drupal-7.0/modules/filter/filter.info drupal-7.66/modules/filter/filter.info --- drupal-7.0/modules/filter/filter.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/filter/filter.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: filter.info,v 1.14 2010/12/20 19:59:42 webchick Exp $ name = Filter description = Filters content in preparation for display. package = Core @@ -8,8 +7,7 @@ required = TRUE configure = admin/config/content/formats -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/filter/filter.install drupal-7.66/modules/filter/filter.install --- drupal-7.0/modules/filter/filter.install 2011-01-02 18:26:39.000000000 +0100 +++ drupal-7.66/modules/filter/filter.install 2019-04-17 22:20:46.000000000 +0200 @@ -1,9 +1,8 @@ 7007, ); @@ -490,5 +490,5 @@ } /** - * @} End of "addtogroup updates-6.x-to-7.x" + * @} End of "addtogroup updates-6.x-to-7.x". */ diff -Naur drupal-7.0/modules/filter/filter.js drupal-7.66/modules/filter/filter.js --- drupal-7.0/modules/filter/filter.js 2010-09-13 02:59:47.000000000 +0200 +++ drupal-7.66/modules/filter/filter.js 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -// $Id: filter.js,v 1.3 2010/09/13 00:59:47 dries Exp $ (function ($) { /** @@ -8,9 +7,9 @@ attach: function (context) { $('.filter-guidelines', context).once('filter-guidelines') .find(':header').hide() - .parents('.filter-wrapper').find('select.filter-list') + .closest('.filter-wrapper').find('select.filter-list') .bind('change', function () { - $(this).parents('.filter-wrapper') + $(this).closest('.filter-wrapper') .find('.filter-guidelines-item').hide() .siblings('.filter-guidelines-' + this.value).show(); }) diff -Naur drupal-7.0/modules/filter/filter.module drupal-7.66/modules/filter/filter.module --- drupal-7.0/modules/filter/filter.module 2010-12-09 03:04:16.000000000 +0100 +++ drupal-7.66/modules/filter/filter.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,9 +1,8 @@ ' . t('About') . ''; - $output .= '

' . t('The Filter module allows administrators to configure text formats. A text format defines the HTML tags, codes, and other input allowed in content and comments, and is a key feature in guarding against potentially damaging input from malicious users. For more information, see the online handbook entry for Filter module.', array('@filter' => 'http://drupal.org/handbook/modules/filter/')) . '

'; + $output .= '

' . t('The Filter module allows administrators to configure text formats. A text format defines the HTML tags, codes, and other input allowed in content and comments, and is a key feature in guarding against potentially damaging input from malicious users. For more information, see the online handbook entry for Filter module.', array('@filter' => 'http://drupal.org/documentation/modules/filter/')) . '

'; $output .= '

' . t('Uses') . '

'; $output .= '
'; $output .= '
' . t('Configuring text formats') . '
'; @@ -72,6 +71,7 @@ * Implements hook_element_info(). * * @see filter_process_format() + * @see text_format_wrapper() */ function filter_element_info() { $type['text_format'] = array( @@ -93,6 +93,14 @@ 'type' => MENU_SUGGESTED_ITEM, 'file' => 'filter.pages.inc', ); + $items['filter/tips/%filter_format'] = array( + 'title' => 'Compose tips', + 'page callback' => 'filter_tips_long', + 'page arguments' => array(2), + 'access callback' => 'filter_access', + 'access arguments' => array(2), + 'file' => 'filter.pages.inc', + ); $items['admin/config/content/formats'] = array( 'title' => 'Text formats', 'description' => 'Configure how content input by users is filtered, including allowed HTML tags. Also allows enabling of module-provided filters.', @@ -133,13 +141,16 @@ } /** - * Access callback for deleting text formats. + * Access callback: Checks access for disabling text formats. * * @param $format * A text format object. + * * @return * TRUE if the text format can be disabled by the current user, FALSE * otherwise. + * + * @see filter_menu() */ function _filter_disable_format_access($format) { // The fallback format can never be disabled. @@ -147,7 +158,7 @@ } /** - * Load a text format object from the database. + * Loads a text format object from the database. * * @param $format_id * The format ID. @@ -165,29 +176,32 @@ } /** - * Save a text format object to the database. + * Saves a text format object to the database. * * @param $format - * A format object using the properties: - * - 'format': A machine-readable name representing the ID of the text format + * A format object having the properties: + * - format: A machine-readable name representing the ID of the text format * to save. If this corresponds to an existing text format, that format * will be updated; otherwise, a new format will be created. - * - 'name': The title of the text format. - * - 'status': (optional) An integer indicating whether the text format is + * - name: The title of the text format. + * - status: (optional) An integer indicating whether the text format is * enabled (1) or not (0). Defaults to 1. - * - 'weight': (optional) The weight of the text format, which controls its + * - weight: (optional) The weight of the text format, which controls its * placement in text format lists. If omitted, the weight is set to 0. - * - 'filters': (optional) An associative, multi-dimensional array of filters + * - filters: (optional) An associative, multi-dimensional array of filters * assigned to the text format, keyed by the name of each filter and using * the properties: - * - 'weight': (optional) The weight of the filter in the text format. If + * - weight: (optional) The weight of the filter in the text format. If * omitted, either the currently stored weight is retained (if there is * one), or the filter is assigned a weight of 10, which will usually * put it at the bottom of the list. - * - 'status': (optional) A boolean indicating whether the filter is + * - status: (optional) A boolean indicating whether the filter is * enabled in the text format. If omitted, the filter will be disabled. - * - 'settings': (optional) An array of configured settings for the filter. + * - settings: (optional) An array of configured settings for the filter. * See hook_filter_info() for details. + * + * @return + * SAVED_NEW or SAVED_UPDATED. */ function filter_format_save($format) { $format->name = trim($format->name); @@ -216,9 +230,11 @@ } $filter_info = filter_get_filters(); foreach ($filter_info as $name => $filter) { - // Add new filters without weight to the bottom. + // If the format does not specify an explicit weight for a filter, assign + // a default weight, either defined in hook_filter_info(), or the default of + // 0 by filter_get_filters() if (!isset($format->filters[$name]['weight'])) { - $format->filters[$name]['weight'] = 10; + $format->filters[$name]['weight'] = $filter['weight']; } $format->filters[$name]['status'] = isset($format->filters[$name]['status']) ? $format->filters[$name]['status'] : 0; $format->filters[$name]['module'] = $filter['module']; @@ -270,7 +286,7 @@ } /** - * Disable a text format. + * Disables a text format. * * There is no core facility to re-enable a disabled format. It is not deleted * to keep information for contrib and to make sure the format ID is never @@ -312,7 +328,15 @@ } /** - * Display a text format form title. + * Displays a text format form title. + * + * @param object $format + * A format object. + * + * @return string + * The name of the format. + * + * @see filter_menu() */ function filter_admin_format_title($format) { return $format->name; @@ -324,6 +348,7 @@ function filter_permission() { $perms['administer filters'] = array( 'title' => t('Administer text formats and filters'), + 'description' => t('Define how text is handled by combining filters into text formats.', array('@url' => url('admin/config/content/formats'))), 'restrict access' => TRUE, ); @@ -332,9 +357,7 @@ foreach (filter_formats() as $format) { $permission = filter_permission_name($format); if (!empty($permission)) { - // Only link to the text format configuration page if the user who is - // viewing this will have access to that page. - $format_name_replacement = user_access('administer filters') ? l($format->name, 'admin/config/content/formats/' . $format->format) : drupal_placeholder($format->name); + $format_name_replacement = l($format->name, 'admin/config/content/formats/' . $format->format); $perms[$permission] = array( 'title' => t("Use the !text_format text format", array('!text_format' => $format_name_replacement,)), 'description' => drupal_placeholder(t('Warning: This permission may have security implications depending on how the text format is configured.')), @@ -349,6 +372,7 @@ * * @param $format * An object representing a text format. + * * @return * The machine-readable permission name, or FALSE if the provided text format * is malformed or is the fallback format (which is available to all users). @@ -379,11 +403,13 @@ } /** - * Retrieve a list of text formats, ordered by weight. + * Retrieves a list of text formats, ordered by weight. * * @param $account * (optional) If provided, only those formats that are allowed for this user - * account will be returned. All formats will be returned otherwise. + * account will be returned. All formats will be returned otherwise. Defaults + * to NULL. + * * @return * An array of text format objects, keyed by the format ID and ordered by * weight. @@ -426,7 +452,7 @@ } /** - * Resets text format caches. + * Resets the text format caches. * * @see filter_formats() */ @@ -442,6 +468,7 @@ * * @param $format * An object representing the text format. + * * @return * An array of role names, keyed by role ID. */ @@ -460,6 +487,7 @@ * * @param $rid * The user role ID to retrieve text formats for. + * * @return * An array of text format objects that are allowed for the role, keyed by * the text format ID and ordered by weight. @@ -493,7 +521,8 @@ * * @param $account * (optional) The user account to check. Defaults to the currently logged-in - * user. + * user. Defaults to NULL. + * * @return * The ID of the user's default text format. * @@ -524,15 +553,18 @@ * format is initialized to output plain text. Installation profiles and site * administrators have the freedom to configure it further. * - * Note that the fallback format is completely distinct from the default - * format, which differs per user and is simply the first format which that - * user has access to. The default and fallback formats are only guaranteed to - * be the same for users who do not have access to any other format; otherwise, - * the fallback format's weight determines its placement with respect to the - * user's other formats. + * Note that the fallback format is completely distinct from the default format, + * which differs per user and is simply the first format which that user has + * access to. The default and fallback formats are only guaranteed to be the + * same for users who do not have access to any other format; otherwise, the + * fallback format's weight determines its placement with respect to the user's + * other formats. * - * Any modules implementing a format deletion functionality must not delete - * this format. + * Any modules implementing a format deletion functionality must not delete this + * format. + * + * @return + * The ID of the fallback text format. * * @see hook_filter_format_disable() * @see filter_default_format() @@ -549,6 +581,9 @@ /** * Returns the title of the fallback text format. + * + * @return string + * The title of the fallback text format. */ function filter_fallback_format_title() { $fallback_format = filter_format_load(filter_fallback_format()); @@ -556,7 +591,10 @@ } /** - * Return a list of all filters provided by modules. + * Returns a list of all filters provided by modules. + * + * @return array + * An array of filter formats. */ function filter_get_filters() { $filters = &drupal_static(__FUNCTION__, array()); @@ -587,14 +625,16 @@ } /** - * Helper function for sorting the filter list by filter name. + * Sorts an array of filters by filter name. + * + * Callback for uasort() within filter_get_filters(). */ function _filter_list_cmp($a, $b) { return strcmp($a['title'], $b['title']); } /** - * Check if text in a certain text format is allowed to be cached. + * Checks if the text in a certain text format is allowed to be cached. * * This function can be used to check whether the result of the filtering * process can be cached. A text format may allow caching depending on the @@ -602,6 +642,7 @@ * * @param $format_id * The text format ID to check. + * * @return * TRUE if the given text format allows caching, FALSE otherwise. */ @@ -618,6 +659,7 @@ * * @param $format * The text format object to check. + * * @return * TRUE if all the filters enabled in the given text format allow caching, * FALSE otherwise. @@ -639,7 +681,7 @@ } /** - * Retrieve a list of filters for a given text format. + * Retrieves a list of filters for a given text format. * * Note that this function returns all associated filters regardless of whether * they are enabled or disabled. All functions working with the filter @@ -672,7 +714,8 @@ if (!isset($filters[$format_id])) { $format_filters = array(); - foreach ($filters['all'][$format_id] as $name => $filter) { + $filter_map = isset($filters['all'][$format_id]) ? $filters['all'][$format_id] : array(); + foreach ($filter_map as $name => $filter) { if (isset($filter_info[$name])) { $filter->title = $filter_info[$name]['title']; // Unpack stored filter settings. @@ -692,7 +735,7 @@ } /** - * Run all the enabled filters on a piece of text. + * Runs all the enabled filters on a piece of text. * * Note: Because filters can inject JavaScript or execute PHP code, security is * vital here. When a user supplies a text format, you should validate it using @@ -703,16 +746,20 @@ * @param $text * The text to be filtered. * @param $format_id - * The format id of the text to be filtered. If no format is assigned, the - * fallback format will be used. + * (optional) The machine name of the filter format to be used to filter the + * text. Defaults to the fallback format. See filter_fallback_format(). * @param $langcode - * Optional: the language code of the text to be filtered, e.g. 'en' for + * (optional) The language code of the text to be filtered, e.g. 'en' for * English. This allows filters to be language aware so language specific - * text replacement can be implemented. + * text replacement can be implemented. Defaults to an empty string. * @param $cache - * Boolean whether to cache the filtered output in the {cache_filter} table. - * The caller may set this to FALSE when the output is already cached - * elsewhere to avoid duplicate cache lookups and storage. + * (optional) A Boolean indicating whether to cache the filtered output in the + * {cache_filter} table. The caller may set this to FALSE when the output is + * already cached elsewhere to avoid duplicate cache lookups and storage. + * Defaults to FALSE. + * + * @return + * The filtered text. * * @ingroup sanitization */ @@ -760,9 +807,12 @@ } } - // Store in cache with a minimum expiration time of 1 day. + // Cache the filtered text. This cache is infinitely valid. It becomes + // obsolete when $text changes (which leads to a new $cache_id). It is + // automatically flushed when the text format is updated. + // @see filter_format_save() if ($cache) { - cache_set($cache_id, $text, 'cache_filter', REQUEST_TIME + (60 * 60 * 24)); + cache_set($cache_id, $text, 'cache_filter'); } return $text; @@ -779,8 +829,8 @@ * the text format id specified in #format or the user's default format by * default, if NULL. * - * The resulting value for the element will be an array holding the value and the - * format. For example, the value for the body element will be: + * The resulting value for the element will be an array holding the value and + * the format. For example, the value for the body element will be: * @code * $form_state['values']['body']['value'] = 'foo'; * $form_state['values']['body']['format'] = 'foo'; @@ -790,7 +840,7 @@ * The form element to process. Properties used: * - #base_type: The form element #type to use for the 'value' element. * 'textarea' by default. - * - #format: (optional) The text format id to preselect. If NULL or not set, + * - #format: (optional) The text format ID to preselect. If NULL or not set, * the default format for the current user will be used. * * @return @@ -928,7 +978,7 @@ } /** - * #pre_render callback for #type 'text_format' to hide field value from prying eyes. + * Render API callback: Hides the field value of 'text_format' elements. * * To not break form processing and previews if a user does not have access to a * stored text format, the expanded form elements in filter_process_format() are @@ -971,7 +1021,7 @@ * An object representing the text format. * @param $account * (optional) The user account to check access for; if omitted, the currently - * logged-in user is used. + * logged-in user is used. Defaults to NULL. * * @return * Boolean TRUE if the user is allowed to access the given format. @@ -993,7 +1043,20 @@ } /** - * Helper function for fetching filter tips. + * Retrieves the filter tips. + * + * @param $format_id + * The ID of the text format for which to retrieve tips, or -1 to return tips + * for all formats accessible to the current user. + * @param $long + * (optional) Boolean indicating whether the long form of tips should be + * returned. Defaults to FALSE. + * + * @return + * An associative array of filtering tips, keyed by filter name. Each + * filtering tip is an associative array with elements: + * - tip: Tip text. + * - id: Filter ID. */ function _filter_tips($format_id, $long = FALSE) { global $user; @@ -1027,14 +1090,14 @@ /** * Parses an HTML snippet and returns it as a DOM object. * - * This function loads the body part of a partial (X)HTML document - * and returns a full DOMDocument object that represents this document. - * You can use filter_dom_serialize() to serialize this DOMDocument - * back to a XHTML snippet. + * This function loads the body part of a partial (X)HTML document and returns + * a full DOMDocument object that represents this document. You can use + * filter_dom_serialize() to serialize this DOMDocument back to a XHTML + * snippet. * * @param $text - * The partial (X)HTML snippet to load. Invalid mark-up - * will be corrected on import. + * The partial (X)HTML snippet to load. Invalid mark-up will be corrected on + * import. * @return * A DOMDocument that represents the loaded (X)HTML snippet. */ @@ -1049,15 +1112,14 @@ /** * Converts a DOM object back to an HTML snippet. * - * The function serializes the body part of a DOMDocument - * back to an XHTML snippet. - * - * The resulting XHTML snippet will be properly formatted - * to be compatible with HTML user agents. + * The function serializes the body part of a DOMDocument back to an XHTML + * snippet. The resulting XHTML snippet will be properly formatted to be + * compatible with HTML user agents. * * @param $dom_document * A DOMDocument object to serialize, only the tags below * the first node will be converted. + * * @return * A valid (X)HTML snippet, as a string. */ @@ -1065,18 +1127,23 @@ $body_node = $dom_document->getElementsByTagName('body')->item(0); $body_content = ''; - foreach ($body_node->getElementsByTagName('script') as $node) { - filter_dom_serialize_escape_cdata_element($dom_document, $node); - } + if ($body_node !== NULL) { + foreach ($body_node->getElementsByTagName('script') as $node) { + filter_dom_serialize_escape_cdata_element($dom_document, $node); + } - foreach ($body_node->getElementsByTagName('style') as $node) { - filter_dom_serialize_escape_cdata_element($dom_document, $node, '/*', '*/'); - } + foreach ($body_node->getElementsByTagName('style') as $node) { + filter_dom_serialize_escape_cdata_element($dom_document, $node, '/*', '*/'); + } - foreach ($body_node->childNodes as $child_node) { - $body_content .= $dom_document->saveXML($child_node); + foreach ($body_node->childNodes as $child_node) { + $body_content .= $dom_document->saveXML($child_node); + } + return preg_replace('|<([^> ]*)/>|i', '<$1 />', $body_content); + } + else { + return $body_content; } - return preg_replace('|<([^> ]*)/>|i', '<$1 />', $body_content); } /** @@ -1087,16 +1154,18 @@ * throw exceptions. * * This function attempts to solve the problem by creating a DocumentFragment - * and immitating the behavior in drupal_get_js(), commenting the CDATA tag. + * and imitating the behavior in drupal_get_js(), commenting the CDATA tag. * * @param $dom_document * The DOMDocument containing the $dom_element. * @param $dom_element * The element potentially containing a CDATA node. * @param $comment_start - * String to use as a comment start marker to escape the CDATA declaration. + * (optional) A string to use as a comment start marker to escape the CDATA + * declaration. Defaults to '//'. * @param $comment_end - * String to use as a comment end marker to escape the CDATA declaration. + * (optional) A string to use as a comment end marker to escape the CDATA + * declaration. Defaults to an empty string. */ function filter_dom_serialize_escape_cdata_element($dom_document, $dom_element, $comment_start = '//', $comment_end = '') { foreach ($dom_element->childNodes as $node) { @@ -1104,8 +1173,15 @@ // See drupal_get_js(). This code is more or less duplicated there. $embed_prefix = "\n{$comment_end}\n"; + + // Prevent invalid cdata escaping as this would throw a DOM error. + // This is the same behavior as found in libxml2. + // Related W3C standard: http://www.w3.org/TR/REC-xml/#dt-cdsection + // Fix explanation: http://en.wikipedia.org/wiki/CDATA#Nesting + $data = str_replace(']]>', ']]]]>', $node->data); + $fragment = $dom_document->createDocumentFragment(); - $fragment->appendXML($embed_prefix . $node->data . $embed_suffix); + $fragment->appendXML($embed_prefix . $data . $embed_suffix); $dom_element->appendChild($fragment); $dom_element->removeChild($node); } @@ -1118,7 +1194,7 @@ * @ingroup themeable */ function theme_filter_tips_more_info() { - return '

' . l(t('More information about text formats'), 'filter/tips') . '

'; + return '

' . l(t('More information about text formats'), 'filter/tips', array('attributes' => array('target' => '_blank'))) . '

'; } /** @@ -1144,7 +1220,7 @@ /** * @defgroup standard_filters Standard filters * @{ - * Filters implemented by the filter.module. + * Filters implemented by the Filter module. */ /** @@ -1192,7 +1268,9 @@ } /** - * Settings callback for the HTML filter. + * Implements callback_filter_settings(). + * + * Filter settings callback for the HTML content filter. */ function _filter_html_settings($form, &$form_state, $filter, $format, $defaults) { $filter->settings += $defaults; @@ -1218,7 +1296,9 @@ } /** - * HTML filter. Provides filtering of input into accepted HTML. + * Implements callback_filter_process(). + * + * Provides filtering of input into accepted HTML. */ function _filter_html($text, $filter) { $allowed_tags = preg_split('/\s+|<|>/', $filter->settings['allowed_html'], -1, PREG_SPLIT_NO_EMPTY); @@ -1237,7 +1317,11 @@ } /** - * Filter tips callback for HTML filter. + * Implements callback_filter_tips(). + * + * Provides help for the HTML filter. + * + * @see filter_filter_info() */ function _filter_html_tips($filter, $format, $long = FALSE) { global $base_url; @@ -1335,7 +1419,11 @@ } /** - * Settings callback for URL filter. + * Implements callback_filter_settings(). + * + * Provides settings for the URL filter. + * + * @see filter_filter_info() */ function _filter_url_settings($form, &$form_state, $filter, $format, $defaults) { $filter->settings += $defaults; @@ -1348,12 +1436,15 @@ '#maxlength' => 4, '#field_suffix' => t('characters'), '#description' => t('URLs longer than this number of characters will be truncated to prevent long strings that break formatting. The link itself will be retained; just the text portion of the link will be truncated.'), + '#element_validate' => array('element_validate_integer_positive'), ); return $settings; } /** - * URL filter. Automatically converts text into hyperlinks. + * Implements callback_filter_process(). + * + * Converts text into hyperlinks automatically. * * This filter identifies and makes clickable three types of "links". * - URLs like http://example.com. @@ -1382,7 +1473,7 @@ // we cannot cleanly differ between protocols here without hard-coding MAILTO, // so '//' is optional for all protocols. // @see filter_xss_bad_protocol() - $protocols = variable_get('filter_allowed_protocols', array('http', 'https', 'ftp', 'news', 'nntp', 'telnet', 'mailto', 'irc', 'ssh', 'sftp', 'webcal', 'rtsp')); + $protocols = variable_get('filter_allowed_protocols', array('ftp', 'http', 'https', 'irc', 'mailto', 'news', 'nntp', 'rtsp', 'sftp', 'ssh', 'tel', 'telnet', 'webcal')); $protocols = implode(':(?://)?|', $protocols) . ':(?://)?'; // Prepare domain name pattern. @@ -1392,7 +1483,7 @@ $domain = '(?:[A-Za-z0-9._+-]+\.)?[A-Za-z]{2,64}\b'; $ip = '(?:[0-9]{1,3}\.){3}[0-9]{1,3}'; $auth = '[a-zA-Z0-9:%_+*~#?&=.,/;-]+@'; - $trail = '[a-zA-Z0-9:%_+*~#&\[\]=/;?\.,-]*[a-zA-Z0-9:%_+*~#&\[\]=/;-]'; + $trail = '[a-zA-Z0-9:%_+*~#&\[\]=/;?!\.,-]*[a-zA-Z0-9:%_+*~#&\[\]=/;-]'; // Prepare pattern for optional trailing punctuation. // Even these characters could have a valid meaning for the URL, such usage is @@ -1406,7 +1497,7 @@ $tasks['_filter_url_parse_full_links'] = $pattern; // Match e-mail addresses. - $url_pattern = "[A-Za-z0-9._-]+@(?:$domain)"; + $url_pattern = "[A-Za-z0-9._+-]{1,254}@(?:$domain)"; $pattern = "`($url_pattern)`"; $tasks['_filter_url_parse_email_links'] = $pattern; @@ -1476,7 +1567,9 @@ } /** - * preg_replace callback to make links out of absolute URLs. + * Makes links out of absolute URLs. + * + * Callback for preg_replace_callback() within _filter_url(). */ function _filter_url_parse_full_links($match) { // The $i:th parenthesis in the regexp contains the URL. @@ -1489,7 +1582,9 @@ } /** - * preg_replace callback to make links out of e-mail addresses. + * Makes links out of e-mail addresses. + * + * Callback for preg_replace_callback() within _filter_url(). */ function _filter_url_parse_email_links($match) { // The $i:th parenthesis in the regexp contains the URL. @@ -1502,7 +1597,9 @@ } /** - * preg_replace callback to make links out of domain names starting with "www." + * Makes links out of domain names starting with "www." + * + * Callback for preg_replace_callback() within _filter_url(). */ function _filter_url_parse_partial_links($match) { // The $i:th parenthesis in the regexp contains the URL. @@ -1515,14 +1612,17 @@ } /** - * preg_replace callback to escape contents of HTML comments + * Escapes the contents of HTML comments. + * + * Callback for preg_replace_callback() within _filter_url(). * * @param $match * An array containing matches to replace from preg_replace_callback(), * whereas $match[1] is expected to contain the content to be filtered. * @param $escape - * (optional) Boolean whether to escape (TRUE) or unescape comments (FALSE). - * Defaults to neither. If TRUE, statically cached $comments are reset. + * (optional) A Boolean indicating whether to escape (TRUE) or unescape + * comments (FALSE). Defaults to NULL, indicating neither. If TRUE, statically + * cached $comments are reset. */ function _filter_url_escape_comments($match, $escape = NULL) { static $mode, $comments = array(); @@ -1538,7 +1638,7 @@ // Replace all HTML coments with a '' placeholder. if ($mode) { $content = $match[1]; - $hash = md5($content); + $hash = hash('sha256', $content); $comments[$hash] = $content; return ""; } @@ -1569,32 +1669,42 @@ } /** - * Filter tips callback for URL filter. + * Implements callback_filter_tips(). + * + * Provides help for the URL filter. + * + * @see filter_filter_info() */ function _filter_url_tips($filter, $format, $long = FALSE) { return t('Web page addresses and e-mail addresses turn into links automatically.'); } /** - * Scan input and make sure that all HTML tags are properly closed and nested. + * Implements callback_filter_process(). + * + * Scans the input and makes sure that HTML tags are properly closed. */ function _filter_htmlcorrector($text) { return filter_dom_serialize(filter_dom_load($text)); } /** - * Convert line breaks into

and
in an intelligent fashion. + * Implements callback_filter_process(). + * + * Converts line breaks into

and
in an intelligent fashion. + * * Based on: http://photomatt.net/scripts/autop */ function _filter_autop($text) { // All block level tags $block = '(?:table|thead|tfoot|caption|colgroup|tbody|tr|td|th|div|dl|dd|dt|ul|ol|li|pre|select|form|blockquote|address|p|h[1-6]|hr)'; - // Split at opening and closing PRE, SCRIPT, STYLE, OBJECT tags and comments. - // We don't apply any processing to the contents of these tags to avoid messing - // up code. We look for matched pairs and allow basic nesting. For example: + // Split at opening and closing PRE, SCRIPT, STYLE, OBJECT, IFRAME tags + // and comments. We don't apply any processing to the contents of these tags + // to avoid messing up code. We look for matched pairs and allow basic + // nesting. For example: // "processed

 ignored  ignored 
processed" - $chunks = preg_split('@(|]*>)@i', $text, -1, PREG_SPLIT_DELIM_CAPTURE); + $chunks = preg_split('@(|]*>)@i', $text, -1, PREG_SPLIT_DELIM_CAPTURE); // Note: PHP ensures the array consists of alternating delimiters and literals // and begins and ends with a literal (inserting NULL as required). $ignore = FALSE; @@ -1602,9 +1712,14 @@ $output = ''; foreach ($chunks as $i => $chunk) { if ($i % 2) { - // Opening or closing tag? - $open = ($chunk[1] != '/' || $chunk[1] != '!'); $comment = (substr($chunk, 0, 4) == '\n\nbbb\n\nccc\n\nddd\n\neee\n\nfff" => array( + "

aaa

\n

\nbbb

\n

ccc

\n

ddd

" => TRUE, + "

\neee

\n

fff

" => TRUE, + ), + // Check that a comment in a PRE will result that the text after + // the comment, but still in PRE, is not transformed. + "
aaa\nbbb\n\nccc
\nddd" => array( + "
aaa\nbbb\n\nccc
" => TRUE, + ), + // Bug 810824, paragraphs were appearing around iframe tags. + "\n\n" => array( + "

" => FALSE, + ), ); $this->assertFilteredString($filter, $tests); @@ -835,7 +962,7 @@ $limit = max(ini_get('pcre.backtrack_limit'), ini_get('pcre.recursion_limit')); $source = $this->randomName($limit); $result = _filter_autop($source); - $success = $this->assertEqual($result, '

' . $source . "

\n", t('Line break filter can process very long strings.')); + $success = $this->assertEqual($result, '

' . $source . "

\n", 'Line break filter can process very long strings.'); if (!$success) { $this->verbose("\n" . $source . "\n
\n" . $result); } @@ -856,176 +983,180 @@ function testFilterXSS() { // Tag stripping, different ways to work around removal of HTML tags. $f = filter_xss(''); - $this->assertNoNormalized($f, 'script', t('HTML tag stripping -- simple script without special characters.')); + $this->assertNoNormalized($f, 'script', 'HTML tag stripping -- simple script without special characters.'); $f = filter_xss(''); - $this->assertNoNormalized($f, 'script', t('HTML tag stripping evasion -- non whitespace character after tag name.')); + $this->assertNoNormalized($f, 'script', 'HTML tag stripping evasion -- non whitespace character after tag name.'); $f = filter_xss(''); - $this->assertNoNormalized($f, 'script', t('HTML tag stripping evasion -- no space between tag and attribute.')); + $this->assertNoNormalized($f, 'script', 'HTML tag stripping evasion -- no space between tag and attribute.'); // Null between < and tag name works at least with IE6. $f = filter_xss("<\0scr\0ipt>alert(0)"); - $this->assertNoNormalized($f, 'ipt', t('HTML tag stripping evasion -- breaking HTML with nulls.')); + $this->assertNoNormalized($f, 'ipt', 'HTML tag stripping evasion -- breaking HTML with nulls.'); $f = filter_xss(""); - $this->assertNoNormalized($f, 'script', t('HTML tag stripping evasion -- filter just removing "script".')); + $this->assertNoNormalized($f, 'script', 'HTML tag stripping evasion -- filter just removing "script".'); $f = filter_xss('<'); - $this->assertNoNormalized($f, 'script', t('HTML tag stripping evasion -- double opening brackets.')); + $this->assertNoNormalized($f, 'script', 'HTML tag stripping evasion -- double opening brackets.'); $f = filter_xss('', array('img')); - $this->assertNoNormalized($f, 'script', t('HTML tag stripping evasion -- a malformed image tag.')); + $this->assertNoNormalized($f, 'script', 'HTML tag stripping evasion -- a malformed image tag.'); $f = filter_xss('
', array('blockquote')); - $this->assertNoNormalized($f, 'script', t('HTML tag stripping evasion -- script in a blockqoute.')); + $this->assertNoNormalized($f, 'script', 'HTML tag stripping evasion -- script in a blockqoute.'); $f = filter_xss(""); - $this->assertNoNormalized($f, 'script', t('HTML tag stripping evasion -- script within a comment.')); + $this->assertNoNormalized($f, 'script', 'HTML tag stripping evasion -- script within a comment.'); // Dangerous attributes removal. $f = filter_xss('

', array('p')); - $this->assertNoNormalized($f, 'onmouseover', t('HTML filter attributes removal -- events, no evasion.')); + $this->assertNoNormalized($f, 'onmouseover', 'HTML filter attributes removal -- events, no evasion.'); $f = filter_xss('

  • ', array('li')); - $this->assertNoNormalized($f, 'style', t('HTML filter attributes removal -- style, no evasion.')); + $this->assertNoNormalized($f, 'style', 'HTML filter attributes removal -- style, no evasion.'); $f = filter_xss('', array('img')); - $this->assertNoNormalized($f, 'onerror', t('HTML filter attributes removal evasion -- spaces before equals sign.')); + $this->assertNoNormalized($f, 'onerror', 'HTML filter attributes removal evasion -- spaces before equals sign.'); $f = filter_xss('', array('img')); - $this->assertNoNormalized($f, 'onabort', t('HTML filter attributes removal evasion -- non alphanumeric characters before equals sign.')); + $this->assertNoNormalized($f, 'onabort', 'HTML filter attributes removal evasion -- non alphanumeric characters before equals sign.'); $f = filter_xss('', array('img')); - $this->assertNoNormalized($f, 'onmediaerror', t('HTML filter attributes removal evasion -- varying case.')); + $this->assertNoNormalized($f, 'onmediaerror', 'HTML filter attributes removal evasion -- varying case.'); // Works at least with IE6. $f = filter_xss("", array('img')); - $this->assertNoNormalized($f, 'focus', t('HTML filter attributes removal evasion -- breaking with nulls.')); + $this->assertNoNormalized($f, 'focus', 'HTML filter attributes removal evasion -- breaking with nulls.'); // Only whitelisted scheme names allowed in attributes. $f = filter_xss('', array('img')); - $this->assertNoNormalized($f, 'javascript', t('HTML scheme clearing -- no evasion.')); + $this->assertNoNormalized($f, 'javascript', 'HTML scheme clearing -- no evasion.'); $f = filter_xss('', array('img')); - $this->assertNoNormalized($f, 'javascript', t('HTML scheme clearing evasion -- no quotes.')); + $this->assertNoNormalized($f, 'javascript', 'HTML scheme clearing evasion -- no quotes.'); // A bit like CVE-2006-0070. $f = filter_xss('', array('img')); - $this->assertNoNormalized($f, 'javascript', t('HTML scheme clearing evasion -- no alert ;)')); + $this->assertNoNormalized($f, 'javascript', 'HTML scheme clearing evasion -- no alert ;)'); $f = filter_xss('', array('img')); - $this->assertNoNormalized($f, 'javascript', t('HTML scheme clearing evasion -- grave accents.')); + $this->assertNoNormalized($f, 'javascript', 'HTML scheme clearing evasion -- grave accents.'); $f = filter_xss('', array('img')); - $this->assertNoNormalized($f, 'javascript', t('HTML scheme clearing -- rare attribute.')); + $this->assertNoNormalized($f, 'javascript', 'HTML scheme clearing -- rare attribute.'); $f = filter_xss('', array('table')); - $this->assertNoNormalized($f, 'javascript', t('HTML scheme clearing -- another tag.')); + $this->assertNoNormalized($f, 'javascript', 'HTML scheme clearing -- another tag.'); $f = filter_xss('', array('base')); - $this->assertNoNormalized($f, 'javascript', t('HTML scheme clearing -- one more attribute and tag.')); + $this->assertNoNormalized($f, 'javascript', 'HTML scheme clearing -- one more attribute and tag.'); $f = filter_xss('', array('img')); - $this->assertNoNormalized($f, 'javascript', t('HTML scheme clearing evasion -- varying case.')); + $this->assertNoNormalized($f, 'javascript', 'HTML scheme clearing evasion -- varying case.'); $f = filter_xss('', array('img')); - $this->assertNoNormalized($f, 'javascript', t('HTML scheme clearing evasion -- UTF-8 decimal encoding.')); + $this->assertNoNormalized($f, 'javascript', 'HTML scheme clearing evasion -- UTF-8 decimal encoding.'); $f = filter_xss('', array('img')); - $this->assertNoNormalized($f, 'javascript', t('HTML scheme clearing evasion -- long UTF-8 encoding.')); + $this->assertNoNormalized($f, 'javascript', 'HTML scheme clearing evasion -- long UTF-8 encoding.'); $f = filter_xss('', array('img')); - $this->assertNoNormalized($f, 'javascript', t('HTML scheme clearing evasion -- UTF-8 hex encoding.')); + $this->assertNoNormalized($f, 'javascript', 'HTML scheme clearing evasion -- UTF-8 hex encoding.'); $f = filter_xss("", array('img')); - $this->assertNoNormalized($f, 'script', t('HTML scheme clearing evasion -- an embedded tab.')); + $this->assertNoNormalized($f, 'script', 'HTML scheme clearing evasion -- an embedded tab.'); $f = filter_xss('', array('img')); - $this->assertNoNormalized($f, 'script', t('HTML scheme clearing evasion -- an encoded, embedded tab.')); + $this->assertNoNormalized($f, 'script', 'HTML scheme clearing evasion -- an encoded, embedded tab.'); $f = filter_xss('', array('img')); - $this->assertNoNormalized($f, 'script', t('HTML scheme clearing evasion -- an encoded, embedded newline.')); + $this->assertNoNormalized($f, 'script', 'HTML scheme clearing evasion -- an encoded, embedded newline.'); // With this test would fail, but the entity gets turned into // &#xD;, so it's OK. $f = filter_xss('', array('img')); - $this->assertNoNormalized($f, 'script', t('HTML scheme clearing evasion -- an encoded, embedded carriage return.')); + $this->assertNoNormalized($f, 'script', 'HTML scheme clearing evasion -- an encoded, embedded carriage return.'); $f = filter_xss("", array('img')); - $this->assertNoNormalized($f, 'cript', t('HTML scheme clearing evasion -- broken into many lines.')); + $this->assertNoNormalized($f, 'cript', 'HTML scheme clearing evasion -- broken into many lines.'); $f = filter_xss("", array('img')); - $this->assertNoNormalized($f, 'cript', t('HTML scheme clearing evasion -- embedded nulls.')); + $this->assertNoNormalized($f, 'cript', 'HTML scheme clearing evasion -- embedded nulls.'); - $f = filter_xss('', array('img')); - $this->assertNoNormalized($f, 'javascript', t('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', t('HTML scheme clearing evasion -- another scheme.')); + $this->assertNoNormalized($f, 'vbscript', 'HTML scheme clearing evasion -- another scheme.'); $f = filter_xss('', array('img')); - $this->assertNoNormalized($f, 'nosuchscheme', t('HTML scheme clearing evasion -- unknown scheme.')); + $this->assertNoNormalized($f, 'nosuchscheme', 'HTML scheme clearing evasion -- unknown scheme.'); // Netscape 4.x javascript entities. $f = filter_xss('
    ', array('br')); - $this->assertNoNormalized($f, 'alert', t('Netscape 4.x javascript entities.')); + $this->assertNoNormalized($f, 'alert', 'Netscape 4.x javascript entities.'); // DRUPAL-SA-2008-006: Invalid UTF-8, these only work as reflected XSS with // Internet Explorer 6. $f = filter_xss("

    \" style=\"background-image: url(javascript:alert(0));\"\xe0

    ", array('p')); - $this->assertNoNormalized($f, 'style', t('HTML filter -- invalid UTF-8.')); + $this->assertNoNormalized($f, 'style', 'HTML filter -- invalid UTF-8.'); $f = filter_xss("\xc0aaa"); - $this->assertEqual($f, '', t('HTML filter -- overlong UTF-8 sequences.')); + $this->assertEqual($f, '', 'HTML filter -- overlong UTF-8 sequences.'); $f = filter_xss("Who's Online"); - $this->assertNormalized($f, "who's online", t('HTML filter -- html entity number')); + $this->assertNormalized($f, "who's online", 'HTML filter -- html entity number'); $f = filter_xss("Who&#039;s Online"); - $this->assertNormalized($f, "who's online", t('HTML filter -- encoded html entity number')); + $this->assertNormalized($f, "who's online", 'HTML filter -- encoded html entity number'); $f = filter_xss("Who&amp;#039; Online"); - $this->assertNormalized($f, "who&#039; online", t('HTML filter -- double encoded html entity number')); + $this->assertNormalized($f, "who&#039; online", 'HTML filter -- double encoded html entity number'); } /** - * Test filter settings, defaults, access restrictions and similar. + * Tests filter settings, defaults, access restrictions and similar. * * @todo This is for functions like filter_filter and check_markup, whose * functionality is not completely focused on filtering. Some ideas: @@ -1042,7 +1173,7 @@ // Setup dummy filter object. $filter = new stdClass(); $filter->settings = array( - 'allowed_html' => '

    @@ -49,6 +51,9 @@ * left-margin for indenting. */ ?> ', $forum->depth); ?> +
    + icon_title; ?> +
    description): ?>
    description; ?>
    diff -Naur drupal-7.0/modules/forum/forum-rtl.css drupal-7.66/modules/forum/forum-rtl.css --- drupal-7.0/modules/forum/forum-rtl.css 2007-11-27 13:09:26.000000000 +0100 +++ drupal-7.66/modules/forum/forum-rtl.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,9 +1,15 @@ -/* $Id: forum-rtl.css,v 1.3 2007/11/27 12:09:26 goba Exp $ */ +/** + * @file + * Right-to-left styling for the Forum module. + */ -#forum tr td.forum { - padding-left: 0.5em; - padding-right: 25px; - background-position: 98% 2px; +#forum td.forum .icon { + float: right; + margin: 0 0 0 9px; +} +#forum div.indent { + margin-left: 0; + margin-right: 20px; } .forum-topic-navigation { padding: 1em 3em 0 0; diff -Naur drupal-7.0/modules/forum/forum-submitted.tpl.php drupal-7.66/modules/forum/forum-submitted.tpl.php --- drupal-7.0/modules/forum/forum-submitted.tpl.php 2009-07-28 12:41:20.000000000 +0200 +++ drupal-7.66/modules/forum/forum-submitted.tpl.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,20 +1,21 @@ diff -Naur drupal-7.0/modules/forum/forum-topic-list.tpl.php drupal-7.66/modules/forum/forum-topic-list.tpl.php --- drupal-7.0/modules/forum/forum-topic-list.tpl.php 2010-04-28 22:25:21.000000000 +0200 +++ drupal-7.66/modules/forum/forum-topic-list.tpl.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,37 +1,40 @@ icon: The icon to display. - * - $topic->moved: A flag to indicate whether the topic has been moved to - * another forum. - * - $topic->title: The title of the topic. Safe to output. - * - $topic->message: If the topic has been moved, this contains an - * explanation and a link. - * - $topic->zebra: 'even' or 'odd' string used for row class. - * - $topic->comment_count: The number of replies on this topic. - * - $topic->new_replies: A flag to indicate whether there are unread comments. - * - $topic->new_url: If there are unread replies, this is a link to them. - * - $topic->new_text: Text containing the translated, properly pluralized count. - * - $topic->created: An outputtable string represented when the topic was posted. - * - $topic->last_reply: An outputtable string representing when the topic was - * last replied to. - * - $topic->timestamp: The raw timestamp this topic was posted. + * - $topics: An array of topics to be displayed. Each $topic in $topics + * contains: + * - $topic->icon: The icon to display. + * - $topic->moved: A flag to indicate whether the topic has been moved to + * another forum. + * - $topic->title: The title of the topic. Safe to output. + * - $topic->message: If the topic has been moved, this contains an + * explanation and a link. + * - $topic->zebra: 'even' or 'odd' string used for row class. + * - $topic->comment_count: The number of replies on this topic. + * - $topic->new_replies: A flag to indicate whether there are unread + * comments. + * - $topic->new_url: If there are unread replies, this is a link to them. + * - $topic->new_text: Text containing the translated, properly pluralized + * count. + * - $topic->created: A string representing when the topic was posted. Safe + * to output. + * - $topic->last_reply: An outputtable string representing when the topic was + * last replied to. + * - $topic->timestamp: The raw timestamp this topic was posted. + * - $topic_id: Numeric ID for the current forum topic. * * @see template_preprocess_forum_topic_list() * @see theme_forum_topic_list() + * + * @ingroup themeable */ ?>
    diff -Naur drupal-7.0/modules/forum/forum.admin.inc drupal-7.66/modules/forum/forum.admin.inc --- drupal-7.0/modules/forum/forum.admin.inc 2010-08-08 21:21:25.000000000 +0200 +++ drupal-7.66/modules/forum/forum.admin.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,9 +1,23 @@ 'hidden', '#value' => variable_get('forum_nav_vocabulary', '')); $form['actions'] = array('#type' => 'actions'); - $form['actions']['submit' ] = array('#type' => 'submit', '#value' => t('Save')); + $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save')); if ($edit['tid']) { $form['actions']['delete'] = array('#type' => 'submit', '#value' => t('Delete')); $form['tid'] = array('#type' => 'hidden', '#value' => $edit['tid']); @@ -68,7 +85,7 @@ } /** - * Process forum form and container form submissions. + * Form submission handler for forum_form_forum() and forum_form_container(). */ function forum_form_submit($form, &$form_state) { if ($form['form_id']['#value'] == 'forum_form_container') { @@ -105,8 +122,8 @@ /** * Returns HTML for a forum form. * - * By default this does not alter the appearance of a form at all, - * but is provided as a convenience for themers. + * By default this does not alter the appearance of a form at all, but is + * provided as a convenience for themers. * * @param $variables * An associative array containing: @@ -119,11 +136,14 @@ } /** - * Returns a form for adding a container to the forum vocabulary + * Form constructor for adding and editing forum containers. + * + * @param $edit + * (optional) Associative array containing a container term to be added or edited. + * Defaults to an empty array. * - * @param $edit Associative array containing a container term to be added or edited. - * @ingroup forms * @see forum_form_submit() + * @ingroup forms */ function forum_form_container($form, &$form_state, $edit = array()) { $edit += array( @@ -177,9 +197,13 @@ } /** - * Returns a confirmation page for deleting a forum taxonomy term. + * Form constructor for confirming deletion of a forum taxonomy term. + * + * @param $tid + * ID of the term to be deleted. * - * @param $tid ID of the term to be deleted + * @see forum_confirm_delete_submit() + * @ingroup forms */ function forum_confirm_delete($form, &$form_state, $tid) { $term = taxonomy_term_load($tid); @@ -191,7 +215,7 @@ } /** - * Implement forms api _submit call. Deletes a forum after confirmation. + * Form submission handler for forum_confirm_delete(). */ function forum_confirm_delete_submit($form, &$form_state) { taxonomy_term_delete($form_state['values']['tid']); @@ -203,9 +227,11 @@ } /** - * Form builder for the forum settings page. + * Form constructor for the forum settings page. * + * @see forum_menu() * @see system_settings_form() + * @ingroup forms */ function forum_admin_settings($form) { $number = drupal_map_assoc(array(5, 10, 15, 20, 25, 30, 35, 40, 50, 60, 80, 100, 150, 200, 250, 300, 350, 400, 500)); @@ -233,7 +259,13 @@ } /** - * Returns an overview list of existing forums and containers + * Form constructor for the forum overview form. + * + * Returns a form for controlling the hierarchy of existing forums and + * containers. + * + * @see forum_menu() + * @ingroup forms */ function forum_overview($form, &$form_state) { module_load_include('inc', 'taxonomy', 'taxonomy.admin'); @@ -268,11 +300,17 @@ } /** - * Returns a select box for available parent terms + * Returns a select box for available parent terms. + * + * @param $tid + * ID of the term that is being added or edited. + * @param $title + * Title for the select box. + * @param $child_type + * Whether the child is a forum or a container. * - * @param $tid ID of the term which is being added or edited - * @param $title Title to display the select box with - * @param $child_type Whether the child is forum or container + * @return + * A select form element. */ function _forum_parent_select($tid, $title, $child_type) { diff -Naur drupal-7.0/modules/forum/forum.css drupal-7.66/modules/forum/forum.css --- drupal-7.0/modules/forum/forum.css 2010-10-03 02:41:14.000000000 +0200 +++ drupal-7.66/modules/forum/forum.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,7 @@ -/* $Id: forum.css,v 1.9 2010/10/03 00:41:14 dries Exp $ */ +/** + * @file + * Styling for the Forum module. + */ #forum .description { font-size: 0.9em; @@ -12,17 +15,21 @@ #forum td.pager { white-space: nowrap; } -#forum tr td.forum { - padding-left: 25px; /* LTR */ - background-position: 2px 2px; /* LTR */ - background-image: url(../../misc/forum-default.png); + +#forum td.forum .icon { + background-image: url(../../misc/forum-icons.png); background-repeat: no-repeat; + float: left; /* LTR */ + height: 24px; + margin: 0 9px 0 0; /* LTR */ + width: 24px; } -#forum tr.new-topics td.forum { - background-image: url(../../misc/forum-new.png); +#forum td.forum .forum-status-new { + background-position: -24px 0; } + #forum div.indent { - margin-left: 20px; + margin-left: 20px; /* LTR */ } #forum .icon div { background-image: url(../../misc/forum-icons.png); diff -Naur drupal-7.0/modules/forum/forum.info drupal-7.66/modules/forum/forum.info --- drupal-7.0/modules/forum/forum.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/forum/forum.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: forum.info,v 1.15 2010/12/20 19:59:42 webchick Exp $ name = Forum description = Provides discussion forums. dependencies[] = taxonomy @@ -10,8 +9,7 @@ configure = admin/structure/forum stylesheets[all][] = forum.css -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/forum/forum.install drupal-7.66/modules/forum/forum.install --- drupal-7.0/modules/forum/forum.install 2010-10-07 02:28:19.000000000 +0200 +++ drupal-7.66/modules/forum/forum.install 2019-04-17 22:20:46.000000000 +0200 @@ -1,9 +1,8 @@ 'taxonomy_' . $vocabulary->machine_name, + 'field_name' => 'taxonomy_forums', 'type' => 'taxonomy_term_reference', 'settings' => array( 'allowed_values' => array( @@ -60,8 +59,6 @@ ); field_create_field($field); - variable_set('forum_nav_vocabulary', $vocabulary->vid); - // Create a default forum so forum posts can be created. $edit = array( 'name' => t('General discussion'), @@ -74,7 +71,7 @@ // Create the instance on the bundle. $instance = array( - 'field_name' => 'taxonomy_' . $vocabulary->machine_name, + 'field_name' => 'taxonomy_forums', 'entity_type' => 'node', 'label' => $vocabulary->name, 'bundle' => 'forum', @@ -116,6 +113,11 @@ variable_del('forum_block_num_active'); variable_del('forum_block_num_new'); variable_del('node_options_forum'); + + field_delete_field('taxonomy_forums'); + // Purge field data now to allow taxonomy module to be uninstalled + // if this is the only field remaining. + field_purge_batch(10); } /** @@ -216,7 +218,9 @@ ), ), 'indexes' => array( - 'forum_topics' => array('tid', 'sticky', 'last_comment_timestamp'), + 'forum_topics' => array('nid', 'tid', 'sticky', 'last_comment_timestamp'), + 'created' => array('created'), + 'last_comment_timestamp' => array('last_comment_timestamp'), ), 'foreign keys' => array( 'tracked_node' => array( @@ -237,6 +241,21 @@ } /** + * Implements hook_update_dependencies(). + */ +function forum_update_dependencies() { + $dependencies['forum'][7003] = array( + // Forum update 7003 uses field API update functions, so must run after + // Field API has been enabled. + 'system' => 7020, + // Forum update 7003 relies on updated taxonomy module schema. Ensure it + // runs after all taxonomy updates. + 'taxonomy' => 7010, + ); + return $dependencies; +} + +/** * Add new index to forum table. */ function forum_update_7000() { @@ -331,3 +350,118 @@ ->from($select) ->execute(); } + +/** + * @addtogroup updates-7.x-extra + * @{ + */ + +/** + * Add new index to forum_index table. + */ +function forum_update_7002() { + db_drop_index('forum_index', 'forum_topics'); + db_add_index('forum_index', 'forum_topics', array('nid', 'tid', 'sticky', 'last_comment_timestamp')); +} + +/** + * Rename field to 'taxonomy_forums'. + */ +function forum_update_7003() { + $messages = array(); + + $new_field_name = 'taxonomy_forums'; + + // Test to see if the taxonomy_forums field exists. + $fields = _update_7000_field_read_fields(array('field_name' => $new_field_name)); + if ($fields) { + // Since the field exists, we're done. + return; + } + + // Calculate the old field name. + $vid = variable_get('forum_nav_vocabulary', 0); + $vocabulary_machine_name = db_select('taxonomy_vocabulary', 'tv') + ->fields('tv', array('machine_name')) + ->condition('vid', $vid) + ->execute() + ->fetchField(); + $old_field_name = 'taxonomy_' . $vocabulary_machine_name; + + // Read the old fields. + $old_fields = _update_7000_field_read_fields(array('field_name' => $old_field_name)); + foreach ($old_fields as $old_field) { + if ($old_field['storage']['type'] != 'field_sql_storage') { + $messages[] = t('Cannot rename field %id (%old_field_name) to %new_field_name because it does not use the field_sql_storage storage type.', array( + '%id' => $old_field['id'], + '%old_field_name' => $old_field_name, + '%new_field_name' => $new_field_name, + )); + continue; + } + + // Update {field_config}. + db_update('field_config') + ->fields(array('field_name' => $new_field_name)) + ->condition('id', $old_field['id']) + ->execute(); + + // Update {field_config_instance}. + db_update('field_config_instance') + ->fields(array('field_name' => $new_field_name)) + ->condition('field_id', $old_field['id']) + ->execute(); + + // The tables that need updating in the form 'old_name' => 'new_name'. + $tables = array( + 'field_data_' . $old_field_name => 'field_data_' . $new_field_name, + 'field_revision_' . $old_field_name => 'field_revision_' . $new_field_name, + ); + foreach ($tables as $old_table => $new_table) { + $old_column_name = $old_field_name . '_tid'; + $new_column_name = $new_field_name . '_tid'; + + // Rename the column. + db_drop_index($old_table, $old_column_name); + db_change_field($old_table, $old_column_name, $new_column_name, array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => FALSE, + )); + db_drop_index($old_table, $new_column_name); + db_add_index($old_table, $new_column_name, array($new_column_name)); + + // Rename the table. + db_rename_table($old_table, $new_table); + } + } + + cache_clear_all('*', 'cache_field', TRUE); + + return $messages; +} + +/** + * Update {forum_index} so that only published nodes are indexed. + */ +function forum_update_7011() { + $select = db_select('node', 'n') + ->fields('n', array('nid')) + ->condition('status', 0 ); + + db_delete('forum_index') + ->condition('nid', $select, 'IN') + ->execute(); +} + +/** + * Add 'created' and 'last_comment_timestamp' indexes. + */ +function forum_update_7012() { + db_add_index('forum_index', 'created', array('created')); + db_add_index('forum_index', 'last_comment_timestamp', array('last_comment_timestamp')); +} + +/** + * @} End of "addtogroup updates-7.x-extra". + */ diff -Naur drupal-7.0/modules/forum/forum.module drupal-7.66/modules/forum/forum.module --- drupal-7.0/modules/forum/forum.module 2010-11-29 05:53:32.000000000 +0100 +++ drupal-7.66/modules/forum/forum.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ '; $output .= ''; $output .= ''; - $output .= '

    ' . t('For more information, see the online handbook entry for Forum module.', array('@forum' => 'http://drupal.org/handbook/modules/forum')) . '

    '; + $output .= '

    ' . t('For more information, see the online handbook entry for Forum module.', array('@forum' => 'http://drupal.org/documentation/modules/forum')) . '

    '; $output .= '

    ' . t('Uses') . '

    '; $output .= '
    '; $output .= '
    ' . t('Setting up forum structure') . '
    '; @@ -170,12 +169,9 @@ $tid = (isset($router_item['page_arguments'][0]) ? $router_item['page_arguments'][0]->tid : 0); $forum_term = forum_forum_load($tid); if ($forum_term) { - $vid = variable_get('forum_nav_vocabulary', 0); - $vocabulary = taxonomy_vocabulary_load($vid); - $links = array(); // Loop through all bundles for forum taxonomy vocabulary field. - $field = field_info_field('taxonomy_' . $vocabulary->machine_name); + $field = field_info_field('taxonomy_forums'); foreach ($field['bundles']['node'] as $type) { if (node_access('create', $type)) { $links[$type] = array( @@ -219,7 +215,7 @@ * Implements hook_entity_info_alter(). */ function forum_entity_info_alter(&$info) { - // Take over URI constuction for taxonomy terms that are forums. + // Take over URI construction for taxonomy terms that are forums. if ($vid = variable_get('forum_nav_vocabulary', 0)) { // Within hook_entity_info(), we can't invoke entity_load() as that would // cause infinite recursion, so we call taxonomy_vocabulary_get_names() @@ -237,7 +233,9 @@ } /** - * Entity URI callback. + * Implements callback_entity_info_uri(). + * + * Entity URI callback used in forum_entity_info_alter(). */ function forum_uri($forum) { return array( @@ -246,7 +244,7 @@ } /** - * Check whether a content type can be used in a forum. + * Checks whether a node can be used in a forum, based on its content type. * * @param $node * A node object. @@ -265,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'); @@ -287,7 +285,8 @@ /** * Implements hook_node_validate(). * - * Check in particular that only a "leaf" term in the associated taxonomy. + * Checks in particular that the node is assigned only a "leaf" term in the + * forum taxonomy. */ function forum_node_validate($node, $form) { if (_forum_node_check_node_type($node)) { @@ -323,7 +322,7 @@ /** * Implements hook_node_presave(). * - * Assign forum taxonomy when adding a topic from within a forum. + * Assigns the forum taxonomy when adding a topic from within a forum. */ function forum_node_presave($node) { if (_forum_node_check_node_type($node)) { @@ -333,10 +332,12 @@ $langcode = key($node->taxonomy_forums); if (!empty($node->taxonomy_forums[$langcode])) { $node->forum_tid = $node->taxonomy_forums[$langcode][0]['tid']; - $old_tid = db_query_range("SELECT f.tid FROM {forum} f INNER JOIN {node} n ON f.vid = n.vid WHERE n.nid = :nid ORDER BY f.vid DESC", 0, 1, array(':nid' => $node->nid))->fetchField(); - if ($old_tid && isset($node->forum_tid) && ($node->forum_tid != $old_tid) && !empty($node->shadow)) { - // A shadow copy needs to be created. Retain new term and add old term. - $node->taxonomy_forums[$langcode][] = array('tid' => $old_tid); + if (isset($node->nid)) { + $old_tid = db_query_range("SELECT f.tid FROM {forum} f INNER JOIN {node} n ON f.vid = n.vid WHERE n.nid = :nid ORDER BY f.vid DESC", 0, 1, array(':nid' => $node->nid))->fetchField(); + if ($old_tid && isset($node->forum_tid) && ($node->forum_tid != $old_tid) && !empty($node->shadow)) { + // A shadow copy needs to be created. Retain new term and add old term. + $node->taxonomy_forums[$langcode][] = array('tid' => $old_tid); + } } } } @@ -374,7 +375,7 @@ } // If the node has a shadow forum topic, update the record for this // revision. - if ($node->shadow) { + if (!empty($node->shadow)) { db_delete('forum') ->condition('nid', $node->nid) ->condition('vid', $node->vid) @@ -472,10 +473,10 @@ /** * Implements hook_taxonomy_term_delete(). */ -function forum_taxonomy_term_delete($tid) { +function forum_taxonomy_term_delete($term) { // For containers, remove the tid from the forum_containers variable. $containers = variable_get('forum_containers', array()); - $key = array_search($tid, $containers); + $key = array_search($term->tid, $containers); if ($key !== FALSE) { unset($containers[$key]); } @@ -485,7 +486,7 @@ /** * Implements hook_comment_publish(). * - * This actually handles the insert and update of published nodes since + * This actually handles the insertion and update of published nodes since * comment_save() calls hook_comment_publish() for all published comments. */ function forum_comment_publish($comment) { @@ -495,12 +496,12 @@ /** * Implements hook_comment_update(). * - * Comment module doesn't call hook_comment_unpublish() when saving individual - * comments so we need to check for those here. + * The Comment module doesn't call hook_comment_unpublish() when saving + * individual comments, so we need to check for those here. */ function forum_comment_update($comment) { - // comment_save() calls hook_comment_publish() for all published comments - // so we to handle all other values here. + // comment_save() calls hook_comment_publish() for all published comments, + // so we need to handle all other values here. if (!$comment->status) { _forum_update_forum_index($comment->nid); } @@ -549,16 +550,18 @@ function forum_field_storage_pre_update($entity_type, $entity, &$skip_fields) { $first_call = &drupal_static(__FUNCTION__, array()); - if ($entity_type == 'node' && $entity->status && _forum_node_check_node_type($entity)) { - // We don't maintain data for old revisions, so clear all previous values - // from the table. Since this hook runs once per field, per object, make - // sure we only wipe values once. - if (!isset($first_call[$entity->nid])) { - $first_call[$entity->nid] = FALSE; - db_delete('forum_index')->condition('nid', $entity->nid)->execute(); - } - // Only save data to the table if the node is published. + if ($entity_type == 'node' && _forum_node_check_node_type($entity)) { + + // If the node is published, update the forum index. if ($entity->status) { + + // We don't maintain data for old revisions, so clear all previous values + // from the table. Since this hook runs once per field, per object, make + // sure we only wipe values once. + if (!isset($first_call[$entity->nid])) { + $first_call[$entity->nid] = FALSE; + db_delete('forum_index')->condition('nid', $entity->nid)->execute(); + } $query = db_insert('forum_index')->fields(array('nid', 'title', 'tid', 'sticky', 'created', 'comment_count', 'last_comment_timestamp')); foreach ($entity->taxonomy_forums as $language) { foreach ($language as $item) { @@ -578,30 +581,50 @@ // call _forum_update_forum_index() too. _forum_update_forum_index($entity->nid); } + + // When a forum node is unpublished, remove it from the forum_index table. + else { + db_delete('forum_index')->condition('nid', $entity->nid)->execute(); + } + } } /** - * Implements hook_form_alter(). + * Implements hook_form_FORM_ID_alter() for taxonomy_form_vocabulary(). */ -function forum_form_alter(&$form, $form_state, $form_id) { +function forum_form_taxonomy_form_vocabulary_alter(&$form, &$form_state, $form_id) { $vid = variable_get('forum_nav_vocabulary', 0); - if (isset($form['vid']) && $form['vid']['#value'] == $vid) { - // Hide critical options from forum vocabulary. - if ($form_id == 'taxonomy_form_vocabulary') { - $form['help_forum_vocab'] = array( - '#markup' => t('This is the designated forum vocabulary. Some of the normal vocabulary options have been removed.'), - '#weight' => -1, - ); - $form['hierarchy'] = array('#type' => 'value', '#value' => 1); - $form['delete']['#access'] = FALSE; - } + if (isset($form['vid']['#value']) && $form['vid']['#value'] == $vid) { + $form['help_forum_vocab'] = array( + '#markup' => t('This is the designated forum vocabulary. Some of the normal vocabulary options have been removed.'), + '#weight' => -1, + ); + // Forum's vocabulary always has single hierarchy. Forums and containers + // have only one parent or no parent for root items. By default this value + // is 0. + $form['hierarchy']['#value'] = 1; + // Do not allow to delete forum's vocabulary. + $form['actions']['delete']['#access'] = FALSE; + } +} + +/** + * Implements hook_form_FORM_ID_alter() for taxonomy_form_term(). + */ +function forum_form_taxonomy_form_term_alter(&$form, &$form_state, $form_id) { + $vid = variable_get('forum_nav_vocabulary', 0); + if (isset($form['vid']['#value']) && $form['vid']['#value'] == $vid) { // Hide multiple parents select from forum terms. - elseif ($form_id == 'taxonomy_form_term') { - $form['advanced']['parent']['#access'] = FALSE; - } + $form['relations']['parent']['#access'] = FALSE; } - if (!empty($form['#node_edit_form']) && isset($form['taxonomy_forums'])) { +} + +/** + * Implements hook_form_BASE_FORM_ID_alter() for node_form(). + */ +function forum_form_node_form_alter(&$form, &$form_state, $form_id) { + if (isset($form['taxonomy_forums'])) { $langcode = $form['taxonomy_forums']['#language']; // Make the vocabulary required for 'real' forum-nodes. $form['taxonomy_forums'][$langcode]['#required'] = TRUE; @@ -611,7 +634,7 @@ // ID from the URL (e.g., if we are on a page like node/add/forum/2, we // expect "2" to be the ID of the forum that was requested). $requested_forum_id = arg(3); - $form['taxonomy_forums'][$langcode]['#default_value'] = is_numeric($requested_forum_id) ? $requested_forum_id : NULL; + $form['taxonomy_forums'][$langcode]['#default_value'] = is_numeric($requested_forum_id) ? $requested_forum_id : ''; } } } @@ -637,7 +660,12 @@ * Implements hook_block_configure(). */ function forum_block_configure($delta = '') { - $form['forum_block_num_' . $delta] = array('#type' => 'select', '#title' => t('Number of topics'), '#default_value' => variable_get('forum_block_num_' . $delta, '5'), '#options' => drupal_map_assoc(array(2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20))); + $form['forum_block_num_' . $delta] = array( + '#type' => 'select', + '#title' => t('Number of topics'), + '#default_value' => variable_get('forum_block_num_' . $delta, '5'), + '#options' => drupal_map_assoc(range(2, 20)) + ); return $form; } @@ -651,8 +679,8 @@ /** * Implements hook_block_view(). * - * Generates a block containing the currently active forum topics and the - * most recently added forum topics. + * Generates a block containing the currently active forum topics and the most + * recently added forum topics. */ function forum_block_view($delta = '') { $query = db_select('forum_index', 'f') @@ -682,13 +710,12 @@ } /** -* A #pre_render callback. Lists nodes based on the element's #query property. -* -* @see forum_block_view() -* -* @return -* A renderable array. -*/ + * Render API callback: Lists nodes based on the element's #query property. + * + * This function can be used as a #pre_render callback. + * + * @see forum_block_view() + */ function forum_block_view_pre_render($elements) { $result = $elements['#query']->execute(); if ($node_title_list = node_title_list($result)) { @@ -712,7 +739,7 @@ if (!empty($node->nid)) { $forum_terms = $node->taxonomy_forums; - // If editing, give option to leave shadows + // If editing, give option to leave shadows. $shadow = (count($forum_terms) > 1); $form['shadow'] = array('#type' => 'checkbox', '#title' => t('Leave shadow copy'), '#default_value' => $shadow, '#description' => t('If you move this topic, you can leave a link in the old forum to the new forum.')); $form['forum_tid'] = array('#type' => 'value', '#value' => $node->forum_tid); @@ -725,13 +752,15 @@ * Returns a tree of all forums for a given taxonomy term ID. * * @param $tid - * (optional) Taxonomy ID of the forum, if not givin all forums will be returned. + * (optional) Taxonomy term ID of the forum. If not given all forums will be + * returned. + * * @return * A tree of taxonomy objects, with the following additional properties: - * - 'num_topics': Number of topics in the forum - * - 'num_posts': Total number of posts in all topics - * - 'last_post': Most recent post for the forum - * - 'forums': An array of child forums + * - num_topics: Number of topics in the forum. + * - num_posts: Total number of posts in all topics. + * - last_post: Most recent post for the forum. + * - forums: An array of child forums. */ function forum_forum_load($tid = NULL) { $cache = &drupal_static(__FUNCTION__, array()); @@ -780,7 +809,7 @@ $query->addExpression('SUM(ncs.comment_count)', 'comment_count'); $counts = $query ->fields('f', array('tid')) - ->condition('status', 1) + ->condition('n.status', 1) ->groupBy('tid') ->addTag('node_access') ->execute() @@ -839,8 +868,17 @@ } /** - * Calculate the number of nodes the user has not yet read and are newer - * than NODE_NEW_LIMIT. + * Calculates the number of new posts in a forum that the user has not yet read. + * + * Nodes are new if they are newer than NODE_NEW_LIMIT. + * + * @param $term + * The term ID of the forum. + * @param $uid + * The user ID. + * + * @return + * The number of new posts in the forum that have not been read by the user. */ function _forum_topics_unread($term, $uid) { $query = db_select('node', 'n'); @@ -856,6 +894,23 @@ ->fetchField(); } +/** + * Gets all the topics in a forum. + * + * @param $tid + * The term ID of the forum. + * @param $sortby + * One of the following integers indicating the sort criteria: + * - 1: Date - newest first. + * - 2: Date - oldest first. + * - 3: Posts with the most comments first. + * - 4: Posts with the least comments first. + * @param $forum_per_page + * The maximum number of topics to display per page. + * + * @return + * A list of all the topics in a forum. + */ function forum_get_topics($tid, $sortby, $forum_per_page) { global $user, $forum_topic_list_header; @@ -880,7 +935,6 @@ ->addTag('node_access') ->orderBy('f.sticky', 'DESC') ->orderByHeader($forum_topic_list_header) - ->orderBy('f.last_comment_timestamp', 'DESC') ->limit($forum_per_page); $count_query = db_select('forum_index', 'f'); @@ -895,7 +949,29 @@ $nids[] = $record->nid; } if ($nids) { - $result = db_query("SELECT n.title, n.nid, n.type, n.sticky, n.created, n.uid, n.comment AS comment_mode, ncs.*, f.tid AS forum_tid, u.name, CASE ncs.last_comment_uid WHEN 0 THEN ncs.last_comment_name ELSE u2.name END AS last_comment_name FROM {node} n INNER JOIN {node_comment_statistics} ncs ON n.nid = ncs.nid INNER JOIN {forum} f ON n.vid = f.vid INNER JOIN {users} u ON n.uid = u.uid INNER JOIN {users} u2 ON ncs.last_comment_uid = u2.uid WHERE n.nid IN (:nids)", array(':nids' => $nids)); + $query = db_select('node', 'n')->extend('TableSort'); + $query->fields('n', array('title', 'nid', 'type', 'sticky', 'created', 'uid')); + $query->addField('n', 'comment', 'comment_mode'); + + $query->join('node_comment_statistics', 'ncs', 'n.nid = ncs.nid'); + $query->fields('ncs', array('cid', 'last_comment_uid', 'last_comment_timestamp', 'comment_count')); + + $query->join('forum_index', 'f', 'f.nid = ncs.nid'); + $query->addField('f', 'tid', 'forum_tid'); + + $query->join('users', 'u', 'n.uid = u.uid'); + $query->addField('u', 'name'); + + $query->join('users', 'u2', 'ncs.last_comment_uid = u2.uid'); + + $query->addExpression('CASE ncs.last_comment_uid WHEN 0 THEN ncs.last_comment_name ELSE u2.name END', 'last_comment_name'); + + $query + ->orderBy('f.sticky', 'DESC') + ->orderByHeader($forum_topic_list_header) + ->condition('n.nid', $nids); + + $result = $query->execute(); } else { $result = array(); @@ -905,7 +981,8 @@ $first_new_found = FALSE; foreach ($result as $topic) { if ($user->uid) { - // folder is new if topic is new or there are new comments since last visit + // A forum is new if the topic is new, or if there are new comments since + // the user's last visit. if ($topic->forum_tid != $tid) { $topic->new = 0; } @@ -942,15 +1019,22 @@ } /** - * Process variables for forums.tpl.php + * Preprocesses variables for forums.tpl.php. * - * The $variables array contains the following arguments: - * - $forums - * - $topics - * - $parents - * - $tid - * - $sortby - * - $forum_per_page + * @param $variables + * An array containing the following elements: + * - forums: An array of all forum objects to display for the given taxonomy + * term ID. If tid = 0 then all the top-level forums are displayed. + * - topics: An array of all the topics in the current forum. + * - parents: An array of taxonomy term objects that are ancestors of the + * current term ID. + * - tid: Taxonomy term ID of the current forum. + * - sortby: One of the following integers indicating the sort criteria: + * - 1: Date - newest first. + * - 2: Date - oldest first. + * - 3: Posts with the most comments first. + * - 4: Posts with the least comments first. + * - forum_per_page: The maximum number of topics to display per page. * * @see forums.tpl.php */ @@ -1021,12 +1105,15 @@ } /** - * Process variables to format a forum listing. + * Preprocesses variables for forum-list.tpl.php. * - * $variables contains the following information: - * - $forums - * - $parents - * - $tid + * @param $variables + * An array containing the following elements: + * - forums: An array of all forum objects to display for the given taxonomy + * term ID. If tid = 0 then all the top-level forums are displayed. + * - parents: An array of taxonomy term objects that are ancestors of the + * current term ID. + * - tid: Taxonomy term ID of the current forum. * * @see forum-list.tpl.php * @see theme_forum_list() @@ -1047,11 +1134,15 @@ $variables['forums'][$id]->new_url = ''; $variables['forums'][$id]->new_topics = 0; $variables['forums'][$id]->old_topics = $forum->num_topics; + $variables['forums'][$id]->icon_class = 'default'; + $variables['forums'][$id]->icon_title = t('No new posts'); if ($user->uid) { $variables['forums'][$id]->new_topics = _forum_topics_unread($forum->tid, $user->uid); if ($variables['forums'][$id]->new_topics) { $variables['forums'][$id]->new_text = format_plural($variables['forums'][$id]->new_topics, '1 new', '@count new'); $variables['forums'][$id]->new_url = url("forum/$forum->tid", array('fragment' => 'new')); + $variables['forums'][$id]->icon_class = 'new'; + $variables['forums'][$id]->icon_title = t('New posts'); } $variables['forums'][$id]->old_topics = $forum->num_topics - $variables['forums'][$id]->new_topics; } @@ -1063,13 +1154,13 @@ } /** - * Preprocess variables to format the topic listing. + * Preprocesses variables for forum-topic-list.tpl.php. * - * $variables contains the following data: - * - $tid - * - $topics - * - $sortby - * - $forum_per_page + * @param $variables + * An array containing the following elements: + * - tid: Taxonomy term ID of the current forum. + * - topics: An array of all the topics in the current forum. + * - forum_per_page: The maximum number of topics to display per page. * * @see forum-topic-list.tpl.php * @see theme_forum_topic_list() @@ -1106,7 +1197,6 @@ $variables['topics'][$id]->title = l($topic->title, "node/$topic->nid"); $variables['topics'][$id]->message = ''; } - $topic->uid = $topic->last_comment_uid ? $topic->last_comment_uid : $topic->uid; $variables['topics'][$id]->created = theme('forum_submitted', array('topic' => $topic)); $variables['topics'][$id]->last_reply = theme('forum_submitted', array('topic' => isset($topic->last_reply) ? $topic->last_reply : NULL)); @@ -1120,7 +1210,7 @@ } } else { - // Make this safe for the template + // Make this safe for the template. $variables['topics'] = array(); } // Give meaning to $tid for themers. $tid actually stands for term id. @@ -1131,14 +1221,16 @@ } /** - * Process variables to format the icon for each individual topic. + * Preprocesses variables for forum-icon.tpl.php. * - * $variables contains the following data: - * - $new_posts - * - $num_posts = 0 - * - $comment_mode = 0 - * - $sticky = 0 - * - $first_new + * @param $variables + * An array containing the following elements: + * - new_posts: Indicates whether or not the topic contains new posts. + * - num_posts: The total number of posts in all topics. + * - comment_mode: An integer indicating whether comments are open, closed, + * or hidden. + * - sticky: Indicates whether the topic is sticky. + * - first_new: Indicates whether this is the first topic with new posts. * * @see forum-icon.tpl.php * @see theme_forum_icon() @@ -1166,9 +1258,14 @@ } /** - * Process variables to format submission info for display in the forum list and topic list. + * Preprocesses variables for forum-submitted.tpl.php. + * + * The submission information will be displayed in the forum list and topic + * list. * - * $variables will contain: $topic + * @param $variables + * An array containing the following elements: + * - topic: The topic object. * * @see forum-submitted.tpl.php * @see theme_forum_submitted() @@ -1178,6 +1275,16 @@ $variables['time'] = isset($variables['topic']->created) ? format_interval(REQUEST_TIME - $variables['topic']->created) : ''; } +/** + * Gets the last time the user viewed a node. + * + * @param $nid + * The node ID. + * + * @return + * The timestamp when the user last viewed this node, if the user has + * previously viewed the node; otherwise NODE_NEW_LIMIT. + */ function _forum_user_last_visit($nid) { global $user; $history = &drupal_static(__FUNCTION__, array()); @@ -1191,19 +1298,34 @@ return isset($history[$nid]) ? $history[$nid] : NODE_NEW_LIMIT; } +/** + * Gets topic sorting information based on an integer code. + * + * @param $sortby + * One of the following integers indicating the sort criteria: + * - 1: Date - newest first. + * - 2: Date - oldest first. + * - 3: Posts with the most comments first. + * - 4: Posts with the least comments first. + * + * @return + * An array with the following values: + * - field: A field for an SQL query. + * - sort: 'asc' or 'desc'. + */ function _forum_get_topic_order($sortby) { switch ($sortby) { case 1: - return array('field' => 'ncs.last_comment_timestamp', 'sort' => 'desc'); + return array('field' => 'f.last_comment_timestamp', 'sort' => 'desc'); break; case 2: - return array('field' => 'ncs.last_comment_timestamp', 'sort' => 'asc'); + return array('field' => 'f.last_comment_timestamp', 'sort' => 'asc'); break; case 3: - return array('field' => 'ncs.comment_count', 'sort' => 'desc'); + return array('field' => 'f.comment_count', 'sort' => 'desc'); break; case 4: - return array('field' => 'ncs.comment_count', 'sort' => 'asc'); + return array('field' => 'f.comment_count', 'sort' => 'asc'); break; } } @@ -1215,7 +1337,7 @@ * The ID of the node to update. */ function _forum_update_forum_index($nid) { - $count = db_query('SELECT COUNT(cid) FROM {comment} WHERE nid = :nid AND status = :status', array( + $count = db_query('SELECT COUNT(cid) FROM {comment} c INNER JOIN {forum_index} i ON c.nid = i.nid WHERE c.nid = :nid AND c.status = :status', array( ':nid' => $nid, ':status' => COMMENT_PUBLISHED, ))->fetchField(); diff -Naur drupal-7.0/modules/forum/forum.pages.inc drupal-7.66/modules/forum/forum.pages.inc --- drupal-7.0/modules/forum/forum.pages.inc 2010-04-28 07:54:55.000000000 +0200 +++ drupal-7.66/modules/forum/forum.pages.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,13 +1,21 @@ admin_user = $this->drupalCreateUser(array( 'access administration pages', + 'administer modules', 'administer blocks', 'administer forums', 'administer menu', @@ -48,7 +85,31 @@ } /** - * Login users, create forum nodes, and test forum functionality through the admin and user interfaces. + * Tests disabling and re-enabling the Forum module. + */ + function testEnableForumField() { + $this->drupalLogin($this->admin_user); + + // Disable the Forum module. + $edit = array(); + $edit['modules[Core][forum][enable]'] = FALSE; + $this->drupalPost('admin/modules', $edit, t('Save configuration')); + $this->assertText(t('The configuration options have been saved.'), 'Modules status has been updated.'); + module_list(TRUE); + $this->assertFalse(module_exists('forum'), 'Forum module is not enabled.'); + + // Attempt to re-enable the Forum module and ensure it does not try to + // recreate the taxonomy_forums field. + $edit = array(); + $edit['modules[Core][forum][enable]'] = 'forum'; + $this->drupalPost('admin/modules', $edit, t('Save configuration')); + $this->assertText(t('The configuration options have been saved.'), 'Modules status has been updated.'); + module_list(TRUE); + $this->assertTrue(module_exists('forum'), 'Forum module is enabled.'); + } + + /** + * Tests forum functionality through the admin and user interfaces. */ function testForum() { //Check that the basic forum install creates a default forum topic @@ -105,17 +166,17 @@ $xpath = $this->buildXPathQuery('//tr[@id=:forum]//td[@class="topics"]', $forum_arg); $topics = $this->xpath($xpath); $topics = trim($topics[0]); - $this->assertEqual($topics, '6', t('Number of topics found.')); + $this->assertEqual($topics, '6', 'Number of topics found.'); // Verify the number of unread topics. $unread_topics = _forum_topics_unread($this->forum['tid'], $this->edit_any_topics_user->uid); $unread_topics = format_plural($unread_topics, '1 new', '@count new'); $xpath = $this->buildXPathQuery('//tr[@id=:forum]//td[@class="topics"]//a', $forum_arg); - $this->assertFieldByXPath($xpath, $unread_topics, t('Number of unread topics found.')); + $this->assertFieldByXPath($xpath, $unread_topics, 'Number of unread topics found.'); // Verify total number of posts in forum. $xpath = $this->buildXPathQuery('//tr[@id=:forum]//td[@class="posts"]', $forum_arg); - $this->assertFieldByXPath($xpath, '6', t('Number of posts found.')); + $this->assertFieldByXPath($xpath, '6', 'Number of posts found.'); // Test loading multiple forum nodes on the front page. $this->drupalLogin($this->drupalCreateUser(array('administer content types', 'create forum content'))); @@ -136,10 +197,21 @@ $this->drupalGet('forum/' . $this->forum['tid']); $this->drupalPost("node/$node->nid/edit", array(), t('Save')); $this->assertResponse(200); + + // Make sure constructing a forum node programmatically produces no notices. + $node = new stdClass; + $node->type = 'forum'; + $node->title = 'Test forum notices'; + $node->uid = 1; + $node->taxonomy_forums[LANGUAGE_NONE][0]['tid'] = $this->root_forum['tid']; + node_save($node); } /** - * Forum nodes should not be created without choosing forum from select list. + * Tests that forum nodes can't be added without a parent. + * + * Verifies that forum nodes are not created without choosing "forum" from the + * select list. */ function testAddOrphanTopic() { // Must remove forum topics to test creating orphan topics. @@ -154,16 +226,17 @@ $this->drupalPost('node/add/forum', array('title' => $this->randomName(10), 'body[' . LANGUAGE_NONE .'][0][value]' => $this->randomName(120)), t('Save')); $nid_count = db_query('SELECT COUNT(nid) FROM {node}')->fetchField(); - $this->assertEqual(0, $nid_count, t('A forum node was not created when missing a forum vocabulary.')); + $this->assertEqual(0, $nid_count, 'A forum node was not created when missing a forum vocabulary.'); // Reset the defaults for future tests. module_enable(array('forum')); } /** - * Run admin tests on the admin user. + * Runs admin tests on the admin user. * - * @param object $user The logged in user. + * @param object $user + * The logged in user. */ private function doAdminTests($user) { // Login the user. @@ -174,14 +247,14 @@ $edit['blocks[forum_active][region]'] = 'sidebar_second'; $this->drupalPost('admin/structure/block', $edit, t('Save blocks')); $this->assertResponse(200); - $this->assertText(t('The block settings have been updated.'), t('Active forum topics forum block was enabled')); + $this->assertText(t('The block settings have been updated.'), 'Active forum topics forum block was enabled'); // Enable the new forum block. $edit = array(); $edit['blocks[forum_new][region]'] = 'sidebar_second'; $this->drupalPost('admin/structure/block', $edit, t('Save blocks')); $this->assertResponse(200); - $this->assertText(t('The block settings have been updated.'), t('[New forum topics] Forum block was enabled')); + $this->assertText(t('The block settings have been updated.'), '[New forum topics] Forum block was enabled'); // Retrieve forum menu id. $mlid = db_query_range("SELECT mlid FROM {menu_links} WHERE link_path = 'forum' AND menu_name = 'navigation' AND module = 'system' ORDER BY mlid ASC", 0, 1)->fetchField(); @@ -199,13 +272,13 @@ // Verify "edit container" link exists and functions correctly. $this->drupalGet('admin/structure/forum'); $this->clickLink('edit container'); - $this->assertRaw('Edit container', t('Followed the link to edit the container')); + $this->assertRaw('Edit container', 'Followed the link to edit the container'); // Create forum inside the forum container. $this->forum = $this->createForum('forum', $this->container['tid']); // Verify the "edit forum" link exists and functions correctly. $this->drupalGet('admin/structure/forum'); $this->clickLink('edit forum'); - $this->assertRaw('Edit forum', t('Followed the link to edit the forum')); + $this->assertRaw('Edit forum', 'Followed the link to edit the forum'); // Navigate back to forum structure page. $this->drupalGet('admin/structure/forum'); // Create second forum in container. @@ -213,14 +286,35 @@ // Save forum overview. $this->drupalPost('admin/structure/forum/', array(), t('Save')); $this->assertRaw(t('The configuration options have been saved.')); - // Delete this second form. + // Delete this second forum. $this->deleteForum($this->delete_forum['tid']); // Create forum at the top (root) level. $this->root_forum = $this->createForum('forum'); + + // Test vocabulary form alterations. + $this->drupalGet('admin/structure/taxonomy/forums/edit'); + $this->assertFieldByName('op', t('Save'), 'Save button found.'); + $this->assertNoFieldByName('op', t('Delete'), 'Delete button not found.'); + + // Test term edit form alterations. + $this->drupalGet('taxonomy/term/' . $this->container['tid'] . '/edit'); + // Test parent field been hidden by forum module. + $this->assertNoField('parent[]', 'Parent field not found.'); + + // Test tags vocabulary form is not affected. + $this->drupalGet('admin/structure/taxonomy/tags/edit'); + $this->assertFieldByName('op', t('Save'), 'Save button found.'); + $this->assertFieldByName('op', t('Delete'), 'Delete button found.'); + // Test tags vocabulary term form is not affected. + $this->drupalGet('admin/structure/taxonomy/tags/add'); + $this->assertField('parent[]', 'Parent field found.'); + // Test relations fieldset exists. + $relations_fieldset = $this->xpath("//fieldset[@id='edit-relations']"); + $this->assertTrue(isset($relations_fieldset[0]), 'Relations fieldset element found.'); } /** - * Edit the forum taxonomy. + * Edits the forum taxonomy. */ function editForumTaxonomy() { // Backup forum taxonomy. @@ -240,15 +334,15 @@ // Edit the vocabulary. $this->drupalPost('admin/structure/taxonomy/' . $original_settings->machine_name . '/edit', $edit, t('Save')); $this->assertResponse(200); - $this->assertRaw(t('Updated vocabulary %name.', array('%name' => $title)), t('Vocabulary was edited')); + $this->assertRaw(t('Updated vocabulary %name.', array('%name' => $title)), 'Vocabulary was edited'); // Grab the newly edited vocabulary. entity_get_controller('taxonomy_vocabulary')->resetCache(); $current_settings = taxonomy_vocabulary_load($vid); // Make sure we actually edited the vocabulary properly. - $this->assertEqual($current_settings->name, $title, t('The name was updated')); - $this->assertEqual($current_settings->description, $description, t('The description was updated')); + $this->assertEqual($current_settings->name, $title, 'The name was updated'); + $this->assertEqual($current_settings->description, $description, 'The description was updated'); // Restore the original vocabulary. taxonomy_vocabulary_save($original_settings); @@ -258,15 +352,16 @@ } /** - * Create a forum container or a forum. + * Creates a forum container or a forum. * * @param $type - * Forum type (forum container or forum). + * The forum type (forum container or forum). * @param $parent - * Forum parent (default = 0 = a root forum; >0 = a forum container or + * The forum parent. This defaults to 0, indicating a root forum. * another forum). + * * @return - * taxonomy_term_data created. + * The created taxonomy term data. */ function createForum($type, $parent = 0) { // Generate a random name/description. @@ -284,7 +379,7 @@ $this->drupalPost('admin/structure/forum/add/' . $type, $edit, t('Save')); $this->assertResponse(200); $type = ($type == 'container') ? 'forum container' : 'forum'; - $this->assertRaw(t('Created new @type %term.', array('%term' => $name, '@type' => t($type))), t(ucfirst($type) . ' was created')); + $this->assertRaw(t('Created new @type %term.', array('%term' => $name, '@type' => t($type))), format_string('@type was created', array('@type' => ucfirst($type)))); // Verify forum. $term = db_query("SELECT * FROM {taxonomy_term_data} t WHERE t.vid = :vid AND t.name = :name AND t.description = :desc", array(':vid' => variable_get('forum_nav_vocabulary', ''), ':name' => $name, ':desc' => $description))->fetchAssoc(); @@ -299,7 +394,7 @@ } /** - * Delete a forum. + * Deletes a forum. * * @param $tid * The forum ID. @@ -312,10 +407,15 @@ // Assert that the forum no longer exists. $this->drupalGet('forum/' . $tid); $this->assertResponse(404, 'The forum was not found'); + + // Assert that the associated term has been removed from the + // forum_containers variable. + $containers = variable_get('forum_containers', array()); + $this->assertFalse(in_array($tid, $containers), 'The forum_containers variable has been updated.'); } /** - * Run basic tests on the indicated user. + * Runs basic tests on the indicated user. * * @param $user * The logged in user. @@ -334,15 +434,15 @@ } /** - * Create forum topic. + * Creates forum topic. * * @param array $forum - * Forum array. + * A forum array. * @param boolean $container - * True if $forum is a container. + * TRUE if $forum is a container; FALSE otherwise. * * @return object - * Topic node created. + * The created topic node. */ function createForumTopic($forum, $container = FALSE) { // Generate a random subject/body. @@ -361,30 +461,30 @@ $type = t('Forum topic'); if ($container) { - $this->assertNoRaw(t('@type %title has been created.', array('@type' => $type, '%title' => $title)), t('Forum topic was not created')); - $this->assertRaw(t('The item %title is a forum container, not a forum.', array('%title' => $forum['name'])), t('Error message was shown')); + $this->assertNoRaw(t('@type %title has been created.', array('@type' => $type, '%title' => $title)), 'Forum topic was not created'); + $this->assertRaw(t('The item %title is a forum container, not a forum.', array('%title' => $forum['name'])), 'Error message was shown'); return; } else { - $this->assertRaw(t('@type %title has been created.', array('@type' => $type, '%title' => $title)), t('Forum topic was created')); - $this->assertNoRaw(t('The item %title is a forum container, not a forum.', array('%title' => $forum['name'])), t('No error message was shown')); + $this->assertRaw(t('@type %title has been created.', array('@type' => $type, '%title' => $title)), 'Forum topic was created'); + $this->assertNoRaw(t('The item %title is a forum container, not a forum.', array('%title' => $forum['name'])), 'No error message was shown'); } // Retrieve node object, ensure that the topic was created and in the proper forum. $node = $this->drupalGetNodeByTitle($title); - $this->assertTrue($node != NULL, t('Node @title was loaded', array('@title' => $title))); + $this->assertTrue($node != NULL, format_string('Node @title was loaded', array('@title' => $title))); $this->assertEqual($node->taxonomy_forums[LANGUAGE_NONE][0]['tid'], $tid, 'Saved forum topic was in the expected forum'); // View forum topic. $this->drupalGet('node/' . $node->nid); - $this->assertRaw($title, t('Subject was found')); - $this->assertRaw($body, t('Body was found')); + $this->assertRaw($title, 'Subject was found'); + $this->assertRaw($body, 'Body was found'); return $node; } /** - * Verify the logged in user has access to a forum nodes. + * Verifies that the logged in user has access to a forum nodes. * * @param $node_user * The user who creates the node. @@ -402,14 +502,14 @@ $this->drupalGet('admin/help/forum'); $this->assertResponse($response2); if ($response2 == 200) { - $this->assertTitle(t('Forum | Drupal'), t('Forum help title was displayed')); - $this->assertText(t('Forum'), t('Forum help node was displayed')); + $this->assertTitle(t('Forum | Drupal'), 'Forum help title was displayed'); + $this->assertText(t('Forum'), 'Forum help node was displayed'); } // Verify the forum blocks were displayed. $this->drupalGet(''); $this->assertResponse(200); - $this->assertText(t('New forum topics'), t('[New forum topics] Forum block was displayed')); + $this->assertText(t('New forum topics'), '[New forum topics] Forum block was displayed'); // View forum container page. $this->verifyForumView($this->container); @@ -421,20 +521,20 @@ // View forum node. $this->drupalGet('node/' . $node->nid); $this->assertResponse(200); - $this->assertTitle($node->title . ' | Drupal', t('Forum node was displayed')); + $this->assertTitle($node->title . ' | Drupal', 'Forum node was displayed'); $breadcrumb = array( l(t('Home'), NULL), l(t('Forums'), 'forum'), l($this->container['name'], 'forum/' . $this->container['tid']), l($this->forum['name'], 'forum/' . $this->forum['tid']), ); - $this->assertRaw(theme('breadcrumb', array('breadcrumb' => $breadcrumb)), t('Breadcrumbs were displayed')); + $this->assertRaw(theme('breadcrumb', array('breadcrumb' => $breadcrumb)), 'Breadcrumbs were displayed'); // View forum edit node. $this->drupalGet('node/' . $node->nid . '/edit'); $this->assertResponse($response); if ($response == 200) { - $this->assertTitle('Edit Forum topic ' . $node->title . ' | Drupal', t('Forum edit node was displayed')); + $this->assertTitle('Edit Forum topic ' . $node->title . ' | Drupal', 'Forum edit node was displayed'); } if ($response == 200) { @@ -447,7 +547,7 @@ $edit["taxonomy_forums[$langcode]"] = $this->root_forum['tid']; $edit['shadow'] = TRUE; $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save')); - $this->assertRaw(t('Forum topic %title has been updated.', array('%title' => $edit["title"])), t('Forum node was edited')); + $this->assertRaw(t('Forum topic %title has been updated.', array('%title' => $edit["title"])), 'Forum node was edited'); // Verify topic was moved to a different forum. $forum_tid = db_query("SELECT tid FROM {forum} WHERE nid = :nid AND vid = :vid", array( @@ -459,21 +559,23 @@ // Delete forum node. $this->drupalPost('node/' . $node->nid . '/delete', array(), t('Delete')); $this->assertResponse($response); - $this->assertRaw(t('Forum topic %title has been deleted.', array('%title' => $edit['title'])), t('Forum node was deleted')); + $this->assertRaw(t('Forum topic %title has been deleted.', array('%title' => $edit['title'])), 'Forum node was deleted'); } } /** - * Verify display of forum page. + * Verifies display of forum page. * * @param $forum - * A row from taxonomy_term_data table in array. + * A row from the taxonomy_term_data table in an array. + * @param $parent + * (optional) An array representing the forum's parent. */ private function verifyForumView($forum, $parent = NULL) { // View forum page. $this->drupalGet('forum/' . $forum['tid']); $this->assertResponse(200); - $this->assertTitle($forum['name'] . ' | Drupal', t('Forum name was displayed')); + $this->assertTitle($forum['name'] . ' | Drupal', 'Forum name was displayed'); $breadcrumb = array( l(t('Home'), NULL), @@ -483,13 +585,14 @@ $breadcrumb[] = l($parent['name'], 'forum/' . $parent['tid']); } - $this->assertRaw(theme('breadcrumb', array('breadcrumb' => $breadcrumb)), t('Breadcrumbs were displayed')); + $this->assertRaw(theme('breadcrumb', array('breadcrumb' => $breadcrumb)), 'Breadcrumbs were displayed'); } /** - * Generate forum topics to test display of active forum block. + * Generates forum topics to test the display of an active forum block. * - * @param array $forum Forum array (a row from taxonomy_term_data table). + * @param array $forum + * The foorum array (a row from taxonomy_term_data table). */ private function generateForumTopics($forum) { $this->nids = array(); @@ -500,10 +603,10 @@ } /** - * View forum topics to test display of active forum block. + * Views forum topics to test the display of an active forum block. * - * @todo The logic here is completely incorrect, since the active - * forum topics block is determined by comments on the node, not by views. + * @todo The logic here is completely incorrect, since the active forum topics + * block is determined by comments on the node, not by views. * @todo DIE * * @param $nids @@ -519,3 +622,66 @@ } } } + +/** + * Tests the forum index listing. + */ +class ForumIndexTestCase extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Forum index', + 'description' => 'Tests the forum index listing.', + 'group' => 'Forum', + ); + } + + function setUp() { + parent::setUp('taxonomy', 'comment', 'forum'); + + // Create a test user. + $web_user = $this->drupalCreateUser(array('create forum content', 'edit own forum content', 'edit any forum content', 'administer nodes')); + $this->drupalLogin($web_user); + } + + /** + * Tests the forum index for published and unpublished nodes. + */ + function testForumIndexStatus() { + + $langcode = LANGUAGE_NONE; + + // The forum ID to use. + $tid = 1; + + // Create a test node. + $title = $this->randomName(20); + $edit = array( + "title" => $title, + "body[$langcode][0][value]" => $this->randomName(200), + ); + + // Create the forum topic, preselecting the forum ID via a URL parameter. + $this->drupalPost('node/add/forum/' . $tid, $edit, t('Save')); + + // Check that the node exists in the database. + $node = $this->drupalGetNodeByTitle($title); + $this->assertTrue(!empty($node), 'New forum node found in database.'); + + // Verify that the node appears on the index. + $this->drupalGet('forum/' . $tid); + $this->assertText($title, 'Published forum topic appears on index.'); + + // Unpublish the node. + $edit = array( + 'status' => FALSE, + ); + $this->drupalPost("node/{$node->nid}/edit", $edit, t('Save')); + $this->drupalGet("node/{$node->nid}"); + $this->assertText(t('Access denied'), 'Unpublished node is no longer accessible.'); + + // Verify that the node no longer appears on the index. + $this->drupalGet('forum/' . $tid); + $this->assertNoText($title, 'Unpublished forum topic no longer appears on index.'); + } +} diff -Naur drupal-7.0/modules/forum/forums.tpl.php drupal-7.66/modules/forum/forums.tpl.php --- drupal-7.0/modules/forum/forums.tpl.php 2009-12-03 21:21:50.000000000 +0100 +++ drupal-7.66/modules/forum/forums.tpl.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,18 +1,19 @@ diff -Naur drupal-7.0/modules/help/help-rtl.css drupal-7.66/modules/help/help-rtl.css --- drupal-7.0/modules/help/help-rtl.css 2007-11-27 13:09:26.000000000 +0100 +++ drupal-7.66/modules/help/help-rtl.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -/* $Id: help-rtl.css,v 1.2 2007/11/27 12:09:26 goba Exp $ */ .help-items { float: right; diff -Naur drupal-7.0/modules/help/help.admin.inc drupal-7.66/modules/help/help.admin.inc --- drupal-7.0/modules/help/help.admin.inc 2010-10-01 17:24:18.000000000 +0200 +++ drupal-7.66/modules/help/help.admin.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@
      '; diff -Naur drupal-7.0/modules/help/help.api.php drupal-7.66/modules/help/help.api.php --- drupal-7.0/modules/help/help.api.php 2010-10-06 05:43:01.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.0/modules/help/help.css drupal-7.66/modules/help/help.css --- drupal-7.0/modules/help/help.css 2007-05-27 19:57:48.000000000 +0200 +++ drupal-7.66/modules/help/help.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -/* $Id: help.css,v 1.2 2007/05/27 17:57:48 goba Exp $ */ .help-items { float: left; /* LTR */ diff -Naur drupal-7.0/modules/help/help.info drupal-7.66/modules/help/help.info --- drupal-7.0/modules/help/help.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/help/help.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: help.info,v 1.9 2010/12/20 19:59:42 webchick Exp $ name = Help description = Manages the display of online help. package = Core @@ -6,8 +5,7 @@ core = 7.x files[] = help.test -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/help/help.module drupal-7.66/modules/help/help.module --- drupal-7.0/modules/help/help.module 2010-10-04 16:54:10.000000000 +0200 +++ drupal-7.66/modules/help/help.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ ' . t('Customize your website design To change the "look and feel" of your website, visit the themes section. You may choose from one of the included themes or download additional themes from the Drupal themes download section.', array('@themes' => url('admin/appearance'), '@download_themes' => 'http://drupal.org/project/themes')) . ''; $output .= '
    • ' . t('Start posting content Finally, you can add new content for your website.', array('@content' => url('node/add'))) . '
    • '; $output .= ''; - $output .= '

      ' . t('For more information, refer to the specific topics listed in the next section or to the online Drupal handbooks. You may also post at the Drupal forum or view the wide range of other support options available.', array('@help' => url('admin/help'), '@handbook' => 'http://drupal.org/handbooks', '@forum' => 'http://drupal.org/forum', '@support' => 'http://drupal.org/support')) . '

      '; + $output .= '

      ' . t('For more information, refer to the specific topics listed in the next section or to the online Drupal handbooks. You may also post at the Drupal forum or view the wide range of other support options available.', array('@help' => url('admin/help'), '@handbook' => 'http://drupal.org/documentation', '@forum' => 'http://drupal.org/forum', '@support' => 'http://drupal.org/support')) . '

      '; return $output; case 'admin/help#help': $output = ''; $output .= '

      ' . t('About') . '

      '; - $output .= '

      ' . t('The Help module provides Help reference pages and context-sensitive advice to guide you through the use and configuration of modules. It is a starting point for the online Drupal handbooks. The handbooks contain more extensive and up-to-date information, are annotated with user-contributed comments, and serve as the definitive reference point for all Drupal documentation. For more information, see the online handbook entry for the Help module.', array('@help' => 'http://drupal.org/handbook/modules/help/', '@handbook' => 'http://drupal.org/handbook', '@help-page' => url('admin/help'))) . '

      '; + $output .= '

      ' . t('The Help module provides Help reference pages and context-sensitive advice to guide you through the use and configuration of modules. It is a starting point for the online Drupal handbooks. The handbooks contain more extensive and up-to-date information, are annotated with user-contributed comments, and serve as the definitive reference point for all Drupal documentation. For more information, see the online handbook entry for the Help module.', array('@help' => 'http://drupal.org/documentation/modules/help/', '@handbook' => 'http://drupal.org/documentation', '@help-page' => url('admin/help'))) . '

      '; $output .= '

      ' . t('Uses') . '

      '; $output .= '
      '; $output .= '
      ' . t('Providing a help reference') . '
      '; diff -Naur drupal-7.0/modules/help/help.test drupal-7.66/modules/help/help.test --- drupal-7.0/modules/help/help.test 2010-11-27 21:25:44.000000000 +0100 +++ drupal-7.66/modules/help/help.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,8 +1,22 @@ drupalLogin($this->big_user); $this->drupalGet('admin/help'); - $this->assertRaw(drupal_get_path('module', 'help') . '/help.css', t('The help.css file is present in the HTML.')); + $this->assertRaw(drupal_get_path('module', 'help') . '/help.css', 'The help.css file is present in the HTML.'); // Verify that introductory help text exists, goes for 100% module coverage. - $this->assertRaw(t('For more information, refer to the specific topics listed in the next section or to the online Drupal handbooks.', array('@drupal' => 'http://drupal.org/handbooks')), 'Help intro text correctly appears.'); + $this->assertRaw(t('For more information, refer to the specific topics listed in the next section or to the online Drupal handbooks.', array('@drupal' => 'http://drupal.org/documentation')), 'Help intro text correctly appears.'); // Verify that help topics text appears. - $this->assertRaw('

      ' . t('Help topics') . '

      ' . t('Help is available on the following items:') . '

      ', t('Help topics text correctly appears.')); + $this->assertRaw('

      ' . t('Help topics') . '

      ' . t('Help is available on the following items:') . '

      ', 'Help topics text correctly appears.'); // Make sure links are properly added for modules implementing hook_help(). foreach ($this->modules as $module => $name) { - $this->assertLink($name, 0, t('Link properly added to @name (admin/help/@module)', array('@module' => $module, '@name' => $name))); + $this->assertLink($name, 0, format_string('Link properly added to @name (admin/help/@module)', array('@module' => $module, '@name' => $name))); } } /** - * Verify the logged in user has the desired access to the various help nodes and the nodes display help. + * Verifies the logged in user has access to the various help nodes. * - * @param integer $response HTTP response code. + * @param integer $response + * An HTTP response code. */ protected function verifyHelp($response = 200) { foreach ($this->modules as $module => $name) { @@ -66,16 +78,17 @@ $this->drupalGet('admin/help/' . $module); $this->assertResponse($response); if ($response == 200) { - $this->assertTitle($name . ' | Drupal', t('[' . $module . '] Title was displayed')); - $this->assertRaw('

      ' . t($name) . '

      ', t('[' . $module . '] Heading was displayed')); + $this->assertTitle($name . ' | Drupal', format_string('%module title was displayed', array('%module' => $module))); + $this->assertRaw('

      ' . t($name) . '

      ', format_string('%module heading was displayed', array('%module' => $module))); } } } /** - * Get list of enabled modules that implement hook_help(). + * Gets the list of enabled modules that implement hook_help(). * - * @return array Enabled modules. + * @return array + * A list of enabled modules. */ protected function getModuleList() { $this->modules = array(); @@ -90,9 +103,12 @@ } /** - * Tests module without help to verify it is not listed in help page. + * Tests a module without help to verify it is not listed in the help page. */ class NoHelpTestCase extends DrupalWebTestCase { + /** + * The user who will be created. + */ protected $big_user; public static function getInfo() { @@ -110,12 +126,12 @@ } /** - * Ensure modules not implementing help do not appear on admin/help. + * Ensures modules not implementing help do not appear on admin/help. */ function testMainPageNoHelp() { $this->drupalLogin($this->big_user); $this->drupalGet('admin/help'); - $this->assertNoText('Hook menu tests', t('Making sure the test module menu_test does not display a help link in admin/help')); + $this->assertNoText('Hook menu tests', 'Making sure the test module menu_test does not display a help link in admin/help'); } } diff -Naur drupal-7.0/modules/image/image-rtl.css drupal-7.66/modules/image/image-rtl.css --- drupal-7.0/modules/image/image-rtl.css 2010-09-27 05:56:14.000000000 +0200 +++ drupal-7.66/modules/image/image-rtl.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -/* $Id: image-rtl.css,v 1.2 2010/09/27 03:56:14 webchick Exp $ */ /** * Image upload widget. diff -Naur drupal-7.0/modules/image/image.admin.css drupal-7.66/modules/image/image.admin.css --- drupal-7.0/modules/image/image.admin.css 2009-07-21 09:09:46.000000000 +0200 +++ drupal-7.66/modules/image/image.admin.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -/* $Id: image.admin.css,v 1.1 2009/07/21 07:09:46 webchick Exp $ */ /** * Image style configuration pages. diff -Naur drupal-7.0/modules/image/image.admin.inc drupal-7.66/modules/image/image.admin.inc --- drupal-7.0/modules/image/image.admin.inc 2010-11-21 08:24:53.000000000 +0100 +++ drupal-7.66/modules/image/image.admin.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ $style['name'])); + $title = t('Edit %name style', array('%name' => $style['label'])); drupal_set_title($title, PASS_THROUGH); // Adjust this form for styles that must be overridden to edit. @@ -57,27 +55,31 @@ '#markup' => theme('image_style_preview', array('style' => $style)), ); + // Show the Image Style label. + $form['label'] = array( + '#type' => 'textfield', + '#title' => t('Image style name'), + '#default_value' => $style['label'], + '#disabled' => !$editable, + '#required' => TRUE, + ); + // Allow the name of the style to be changed, unless this style is // provided by a module's hook_default_image_styles(). - if ($style['storage'] & IMAGE_STORAGE_MODULE) { - $form['name'] = array( - '#type' => 'item', - '#title' => t('Image style name'), - '#markup' => $style['name'], - '#description' => t('This image style is being provided by %module module and may not be renamed.', array('%module' => $style['module'])), - ); - } - else { - $form['name'] = array( - '#type' => 'textfield', - '#size' => '64', - '#title' => t('Image style name'), - '#default_value' => $style['name'], - '#description' => t('The name is used in URLs for generated images. Use only lowercase alphanumeric characters, underscores (_), and hyphens (-).'), - '#element_validate' => array('image_style_name_validate'), - '#required' => TRUE, - ); - } + $form['name'] = array( + '#type' => 'machine_name', + '#size' => '64', + '#default_value' => $style['name'], + '#disabled' => !$editable, + '#description' => t('The name is used in URLs for generated images. Use only lowercase alphanumeric characters, underscores (_), and hyphens (-).'), + '#required' => TRUE, + '#machine_name' => array( + 'exists' => 'image_style_load', + 'source' => array('label'), + 'replace_pattern' => '[^0-9a-z_\-]', + 'error' => t('Please only use lowercase alphanumeric characters, underscores (_), and hyphens (-) for style names.'), + ), + ); // Build the list of existing image effects for this image style. $form['effects'] = array( @@ -200,7 +202,7 @@ * Submit handler for overriding a module-defined style. */ function image_style_form_override_submit($form, &$form_state) { - drupal_set_message(t('The %style style has been overridden, allowing you to change its settings.', array('%style' => $form_state['image_style']['name']))); + drupal_set_message(t('The %style style has been overridden, allowing you to change its settings.', array('%style' => $form_state['image_style']['label']))); image_default_style_save($form_state['image_style']); } @@ -208,11 +210,10 @@ * Submit handler for saving an image style. */ function image_style_form_submit($form, &$form_state) { - // Update the image style name if it has changed. + // Update the image style. $style = $form_state['image_style']; - if (isset($form_state['values']['name']) && $style['name'] != $form_state['values']['name']) { - $style['name'] = $form_state['values']['name']; - } + $style['name'] = $form_state['values']['name']; + $style['label'] = $form_state['values']['label']; // Update image effect weights. if (!empty($form_state['values']['effects'])) { @@ -227,7 +228,7 @@ image_style_save($style); if ($form_state['values']['op'] == t('Update style')) { - drupal_set_message('Changes to the style have been saved.'); + drupal_set_message(t('Changes to the style have been saved.')); } $form_state['redirect'] = 'admin/config/media/image-styles/edit/' . $style['name']; } @@ -237,17 +238,25 @@ * * @ingroup forms * @see image_style_add_form_submit() - * @see image_style_name_validate() */ function image_style_add_form($form, &$form_state) { - $form['name'] = array( + $form['label'] = array( '#type' => 'textfield', - '#size' => '64', '#title' => t('Style name'), '#default_value' => '', + '#required' => TRUE, + ); + $form['name'] = array( + '#type' => 'machine_name', '#description' => t('The name is used in URLs for generated images. Use only lowercase alphanumeric characters, underscores (_), and hyphens (-).'), - '#element_validate' => array('image_style_name_validate'), + '#size' => '64', '#required' => TRUE, + '#machine_name' => array( + 'exists' => 'image_style_load', + 'source' => array('label'), + 'replace_pattern' => '[^0-9a-z_\-]', + 'error' => t('Please only use lowercase alphanumeric characters, underscores (_), and hyphens (-) for style names.'), + ), ); $form['submit'] = array( @@ -262,14 +271,22 @@ * Submit handler for adding a new image style. */ function image_style_add_form_submit($form, &$form_state) { - $style = array('name' => $form_state['values']['name']); + $style = array( + 'name' => $form_state['values']['name'], + 'label' => $form_state['values']['label'], + ); $style = image_style_save($style); - drupal_set_message(t('Style %name was created.', array('%name' => $style['name']))); + drupal_set_message(t('Style %name was created.', array('%name' => $style['label']))); $form_state['redirect'] = 'admin/config/media/image-styles/edit/' . $style['name']; } /** * Element validate function to ensure unique, URL safe style names. + * + * This function is no longer used in Drupal core since image style names are + * now validated using #machine_name functionality. It is kept for backwards + * compatibility (since non-core modules may be using it) and will be removed + * in Drupal 8. */ function image_style_name_validate($element, $form_state) { // Check for duplicates. @@ -293,10 +310,10 @@ * @ingroup forms * @see image_style_delete_form_submit() */ -function image_style_delete_form($form, $form_state, $style) { +function image_style_delete_form($form, &$form_state, $style) { $form_state['image_style'] = $style; - $replacement_styles = array_diff_key(image_style_options(), array($style['name'] => '')); + $replacement_styles = array_diff_key(image_style_options(TRUE, PASS_THROUGH), array($style['name'] => '')); $form['replacement'] = array( '#title' => t('Replacement style'), '#type' => 'select', @@ -306,7 +323,7 @@ return confirm_form( $form, - t('Optionally select a style before deleting %style', array('%style' => $style['name'])), + t('Optionally select a style before deleting %style', array('%style' => $style['label'])), 'admin/config/media/image-styles', t('If this style is in use on the site, you may select another style to replace it. All images that have been generated for this style will be permanently deleted.'), t('Delete'), t('Cancel') @@ -320,19 +337,19 @@ $style = $form_state['image_style']; image_style_delete($style, $form_state['values']['replacement']); - drupal_set_message(t('Style %name was deleted.', array('%name' => $style['name']))); + drupal_set_message(t('Style %name was deleted.', array('%name' => $style['label']))); $form_state['redirect'] = 'admin/config/media/image-styles'; } /** * Confirmation form to revert a database style to its default. */ -function image_style_revert_form($form, $form_state, $style) { +function image_style_revert_form($form, &$form_state, $style) { $form_state['image_style'] = $style; return confirm_form( $form, - t('Revert the %style style?', array('%style' => $style['name'])), + t('Revert the %style style?', array('%style' => $style['label'])), 'admin/config/media/image-styles', t('Reverting this style will delete the customized settings and restore the defaults provided by the @module module.', array('@module' => $style['module'])), t('Revert'), t('Cancel') @@ -343,7 +360,7 @@ * Submit handler to convert an overridden style to its default. */ function image_style_revert_form_submit($form, &$form_state) { - drupal_set_message(t('The %style style has been revert to its defaults.', array('%style' => $form_state['image_style']['name']))); + drupal_set_message(t('The %style style has been reverted to its defaults.', array('%style' => $form_state['image_style']['label']))); image_default_style_revert($form_state['image_style']); $form_state['redirect'] = 'admin/config/media/image-styles'; } @@ -440,7 +457,7 @@ $form_state['image_style'] = $style; $form_state['image_effect'] = $effect; - $question = t('Are you sure you want to delete the @effect effect from the %style style?', array('%style' => $style['name'], '@effect' => $effect['label'])); + $question = t('Are you sure you want to delete the @effect effect from the %style style?', array('%style' => $style['label'], '@effect' => $effect['label'])); return confirm_form($form, $question, 'admin/config/media/image-styles/edit/' . $style['name'], '', t('Delete')); } @@ -575,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'], @@ -651,7 +668,7 @@ $rows = array(); foreach ($styles as $style) { $row = array(); - $row[] = l($style['name'], 'admin/config/media/image-styles/edit/' . $style['name']); + $row[] = l($style['label'], 'admin/config/media/image-styles/edit/' . $style['name']); $link_attributes = array( 'attributes' => array( 'class' => array('image-style-link'), @@ -719,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(), ); } } @@ -806,7 +824,7 @@ // Build the preview of the image style. $preview_url = file_create_url($preview_file) . '?cache_bypass=' . REQUEST_TIME; $output .= '
      '; - $output .= check_plain($style['name']) . ' (' . l(t('view actual size'), file_create_url($preview_file) . '?' . time()) . ')'; + $output .= check_plain($style['label']) . ' (' . l(t('view actual size'), file_create_url($preview_file) . '?' . time()) . ')'; $output .= '
      '; $output .= '' . theme('image', array('path' => $preview_url, 'alt' => t('Sample modified image'), 'title' => '', 'attributes' => $preview_attributes)) . ''; $output .= '
      ' . $preview_image['height'] . 'px
      '; diff -Naur drupal-7.0/modules/image/image.api.php drupal-7.66/modules/image/image.api.php --- drupal-7.0/modules/image/image.api.php 2010-09-11 03:54:43.000000000 +0200 +++ drupal-7.66/modules/image/image.api.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ t('Resize'), 'help' => t('Resize an image to an exact set of dimensions, ignoring aspect ratio.'), - 'effect callback' => 'mymodule_resize_image', + 'effect callback' => 'mymodule_resize_effect', + 'dimensions callback' => 'mymodule_resize_dimensions', 'form callback' => 'mymodule_resize_form', 'summary theme' => 'mymodule_resize_summary', ); @@ -56,8 +60,9 @@ */ function hook_image_effect_info_alter(&$effects) { // Override the Image module's crop effect with more options. - $effect['image_crop']['effect callback'] = 'mymodule_crop_effect'; - $effect['image_crop']['form callback'] = 'mymodule_crop_form'; + $effects['image_crop']['effect callback'] = 'mymodule_crop_effect'; + $effects['image_crop']['dimensions callback'] = 'mymodule_crop_dimensions'; + $effects['image_crop']['form callback'] = 'mymodule_crop_form'; } /** @@ -172,6 +177,7 @@ $styles = array(); $styles['mymodule_preview'] = array( + 'label' => 'My module preview', 'effects' => array( array( 'name' => 'image_scale', diff -Naur drupal-7.0/modules/image/image.css drupal-7.66/modules/image/image.css --- drupal-7.0/modules/image/image.css 2010-09-27 05:56:14.000000000 +0200 +++ drupal-7.66/modules/image/image.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -/* $Id: image.css,v 1.2 2010/09/27 03:56:14 webchick Exp $ */ /** * Image upload widget. diff -Naur drupal-7.0/modules/image/image.effects.inc drupal-7.66/modules/image/image.effects.inc --- drupal-7.0/modules/image/image.effects.inc 2010-05-06 07:59:31.000000000 +0200 +++ drupal-7.66/modules/image/image.effects.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ t('Resize'), 'help' => t('Resizing will make images an exact set of dimensions. This may cause images to be stretched or shrunk disproportionately.'), 'effect callback' => 'image_resize_effect', + 'dimensions callback' => 'image_resize_dimensions', 'form callback' => 'image_resize_form', 'summary theme' => 'image_resize_summary', ), @@ -22,6 +22,7 @@ 'label' => t('Scale'), 'help' => t('Scaling will maintain the aspect-ratio of the original image. If only a single dimension is specified, the other dimension will be calculated.'), 'effect callback' => 'image_scale_effect', + 'dimensions callback' => 'image_scale_dimensions', 'form callback' => 'image_scale_form', 'summary theme' => 'image_scale_summary', ), @@ -29,6 +30,7 @@ 'label' => t('Scale and crop'), 'help' => t('Scale and crop will maintain the aspect-ratio of the original image, then crop the larger dimension. This is most useful for creating perfectly square thumbnails without stretching the image.'), 'effect callback' => 'image_scale_and_crop_effect', + 'dimensions callback' => 'image_resize_dimensions', 'form callback' => 'image_resize_form', 'summary theme' => 'image_resize_summary', ), @@ -36,6 +38,7 @@ 'label' => t('Crop'), 'help' => t('Cropping will remove portions of an image to make it the specified dimensions.'), 'effect callback' => 'image_crop_effect', + 'dimensions callback' => 'image_resize_dimensions', 'form callback' => 'image_crop_form', 'summary theme' => 'image_crop_summary', ), @@ -43,11 +46,13 @@ 'label' => t('Desaturate'), 'help' => t('Desaturate converts an image to grayscale.'), 'effect callback' => 'image_desaturate_effect', + 'dimensions passthrough' => TRUE, ), 'image_rotate' => array( 'label' => t('Rotate'), 'help' => t('Rotating an image may cause the dimensions of an image to increase to fit the diagonal.'), 'effect callback' => 'image_rotate_effect', + 'dimensions callback' => 'image_rotate_dimensions', 'form callback' => 'image_rotate_form', 'summary theme' => 'image_rotate_summary', ), @@ -74,13 +79,31 @@ */ function image_resize_effect(&$image, $data) { if (!image_resize($image, $data['width'], $data['height'])) { - watchdog('image', 'Image resize failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => $image->toolkit, '%path' => $image->source, '%mimetype' => $image->info['mime_type'], '%dimensions' => $image->info['height'] . 'x' . $image->info['height']), WATCHDOG_ERROR); + watchdog('image', 'Image resize failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => $image->toolkit, '%path' => $image->source, '%mimetype' => $image->info['mime_type'], '%dimensions' => $image->info['width'] . 'x' . $image->info['height']), WATCHDOG_ERROR); return FALSE; } return TRUE; } /** + * Image dimensions callback; Resize. + * + * @param $dimensions + * Dimensions to be modified - an array with components width and height, in + * pixels. + * @param $data + * An array of attributes to use when performing the resize effect with the + * following items: + * - "width": An integer representing the desired width in pixels. + * - "height": An integer representing the desired height in pixels. + */ +function image_resize_dimensions(array &$dimensions, array $data) { + // The new image will have the exact dimensions defined for the effect. + $dimensions['width'] = $data['width']; + $dimensions['height'] = $data['height']; +} + +/** * Image effect callback; Scale an image resource. * * @param $image @@ -90,8 +113,8 @@ * following items: * - "width": An integer representing the desired width in pixels. * - "height": An integer representing the desired height in pixels. - * - "upscale": A Boolean indicating that the image should be upscalled if - * the dimensions are larger than the original image. + * - "upscale": A boolean indicating that the image should be upscaled if the + * dimensions are larger than the original image. * * @return * TRUE on success. FALSE on failure to scale image. @@ -101,21 +124,39 @@ function image_scale_effect(&$image, $data) { // Set sane default values. $data += array( + 'width' => NULL, + 'height' => NULL, 'upscale' => FALSE, ); - // Set impossibly large values if the width and height aren't set. - $data['width'] = empty($data['width']) ? PHP_INT_MAX : $data['width']; - $data['height'] = empty($data['height']) ? PHP_INT_MAX : $data['height']; - if (!image_scale($image, $data['width'], $data['height'], $data['upscale'])) { - watchdog('image', 'Image scale failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => $image->toolkit, '%path' => $image->source, '%mimetype' => $image->info['mime_type'], '%dimensions' => $image->info['height'] . 'x' . $image->info['height']), WATCHDOG_ERROR); + watchdog('image', 'Image scale failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => $image->toolkit, '%path' => $image->source, '%mimetype' => $image->info['mime_type'], '%dimensions' => $image->info['width'] . 'x' . $image->info['height']), WATCHDOG_ERROR); return FALSE; } return TRUE; } /** + * Image dimensions callback; Scale. + * + * @param $dimensions + * Dimensions to be modified - an array with components width and height, in + * pixels. + * @param $data + * An array of attributes to use when performing the scale effect with the + * following items: + * - "width": An integer representing the desired width in pixels. + * - "height": An integer representing the desired height in pixels. + * - "upscale": A boolean indicating that the image should be upscaled if the + * dimensions are larger than the original image. + */ +function image_scale_dimensions(array &$dimensions, array $data) { + if ($dimensions['width'] && $dimensions['height']) { + image_dimensions_scale($dimensions, $data['width'], $data['height'], $data['upscale']); + } +} + +/** * Image effect callback; Crop an image resource. * * @param $image @@ -143,7 +184,7 @@ $x = image_filter_keyword($x, $image->info['width'], $data['width']); $y = image_filter_keyword($y, $image->info['height'], $data['height']); if (!image_crop($image, $x, $y, $data['width'], $data['height'])) { - watchdog('image', 'Image crop failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => $image->toolkit, '%path' => $image->source, '%mimetype' => $image->info['mime_type'], '%dimensions' => $image->info['height'] . 'x' . $image->info['height']), WATCHDOG_ERROR); + watchdog('image', 'Image crop failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => $image->toolkit, '%path' => $image->source, '%mimetype' => $image->info['mime_type'], '%dimensions' => $image->info['width'] . 'x' . $image->info['height']), WATCHDOG_ERROR); return FALSE; } return TRUE; @@ -165,7 +206,7 @@ */ function image_scale_and_crop_effect(&$image, $data) { if (!image_scale_and_crop($image, $data['width'], $data['height'])) { - watchdog('image', 'Image scale and crop failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => $image->toolkit, '%path' => $image->source, '%mimetype' => $image->info['mime_type'], '%dimensions' => $image->info['height'] . 'x' . $image->info['height']), WATCHDOG_ERROR); + watchdog('image', 'Image scale and crop failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => $image->toolkit, '%path' => $image->source, '%mimetype' => $image->info['mime_type'], '%dimensions' => $image->info['width'] . 'x' . $image->info['height']), WATCHDOG_ERROR); return FALSE; } return TRUE; @@ -184,7 +225,7 @@ */ function image_desaturate_effect(&$image, $data) { if (!image_desaturate($image)) { - watchdog('image', 'Image desaturate failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => $image->toolkit, '%path' => $image->source, '%mimetype' => $image->info['mime_type'], '%dimensions' => $image->info['height'] . 'x' . $image->info['height']), WATCHDOG_ERROR); + watchdog('image', 'Image desaturate failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => $image->toolkit, '%path' => $image->source, '%mimetype' => $image->info['mime_type'], '%dimensions' => $image->info['width'] . 'x' . $image->info['height']), WATCHDOG_ERROR); return FALSE; } return TRUE; @@ -199,7 +240,7 @@ * An array of attributes to use when performing the rotate effect containing * the following items: * - "degrees": The number of (clockwise) degrees to rotate the image. - * - "random": A Boolean indicating that a random rotation angle should be + * - "random": A boolean indicating that a random rotation angle should be * used for this image. The angle specified in "degrees" is used as a * positive and negative maximum. * - "bgcolor": The background color to use for exposed areas of the image. @@ -237,8 +278,37 @@ } if (!image_rotate($image, $data['degrees'], $data['bgcolor'])) { - watchdog('image', 'Image rotate failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => $image->toolkit, '%path' => $image->source, '%mimetype' => $image->info['mime_type'], '%dimensions' => $image->info['height'] . 'x' . $image->info['height']), WATCHDOG_ERROR); + watchdog('image', 'Image rotate failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => $image->toolkit, '%path' => $image->source, '%mimetype' => $image->info['mime_type'], '%dimensions' => $image->info['width'] . 'x' . $image->info['height']), WATCHDOG_ERROR); return FALSE; } return TRUE; } + +/** + * Image dimensions callback; Rotate. + * + * @param $dimensions + * Dimensions to be modified - an array with components width and height, in + * pixels. + * @param $data + * An array of attributes to use when performing the rotate effect containing + * the following items: + * - "degrees": The number of (clockwise) degrees to rotate the image. + * - "random": A boolean indicating that a random rotation angle should be + * used for this image. The angle specified in "degrees" is used as a + * positive and negative maximum. + */ +function image_rotate_dimensions(array &$dimensions, array $data) { + // If the rotate is not random and the angle is a multiple of 90 degrees, + // then the new dimensions can be determined. + if (!$data['random'] && ((int) ($data['degrees']) == $data['degrees']) && ($data['degrees'] % 90 == 0)) { + if ($data['degrees'] % 180 != 0) { + $temp = $dimensions['width']; + $dimensions['width'] = $dimensions['height']; + $dimensions['height'] = $temp; + } + } + else { + $dimensions['width'] = $dimensions['height'] = NULL; + } +} diff -Naur drupal-7.0/modules/image/image.field.inc drupal-7.66/modules/image/image.field.inc --- drupal-7.0/modules/image/image.field.inc 2010-10-31 13:12:00.000000000 +0100 +++ drupal-7.66/modules/image/image.field.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ 0, 'max_resolution' => '', 'min_resolution' => '', + 'default_image' => 0, ), 'default_widget' => 'image_image', 'default_formatter' => 'image', @@ -52,16 +52,18 @@ '#description' => t('Select where the final files should be stored. Private file storage has significantly more overhead than public files, but allows restricted access to files within this field.'), ); + // When the user sets the scheme on the UI, even for the first time, it's + // updating a field because fields are created on the "Manage fields" + // page. So image_field_update_field() can handle this change. $form['default_image'] = array( '#title' => t('Default image'), '#type' => 'managed_file', '#description' => t('If no image is uploaded, this image will be shown on display.'), '#default_value' => $field['settings']['default_image'], - '#upload_location' => 'public://default_images/', + '#upload_location' => $settings['uri_scheme'] . '://default_images/', ); return $form; - } /** @@ -151,6 +153,15 @@ '#weight' => 11, ); + // Add the default image to the instance. + $form['default_image'] = array( + '#title' => t('Default image'), + '#type' => 'managed_file', + '#description' => t("If no image is uploaded, this image will be shown on display and will override the field's default image."), + '#default_value' => $settings['default_image'], + '#upload_location' => $field['settings']['uri_scheme'] . '://default_images/', + ); + return $form; } @@ -190,8 +201,19 @@ function image_field_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items) { // If there are no files specified at all, use the default. foreach ($entities as $id => $entity) { - if (empty($items[$id]) && $field['settings']['default_image']) { - if ($file = file_load($field['settings']['default_image'])) { + if (empty($items[$id])) { + $fid = 0; + // Use the default for the instance if one is available. + if (!empty($instances[$id]['settings']['default_image'])) { + $fid = $instances[$id]['settings']['default_image']; + } + // Otherwise, use the default for the field. + elseif (!empty($field['settings']['default_image'])) { + $fid = $field['settings']['default_image']; + } + + // Add the default image if one is found. + if ($fid && ($file = file_load($fid))) { $items[$id][0] = (array) $file + array( 'is_default' => TRUE, 'alt' => '', @@ -207,6 +229,18 @@ */ function image_field_presave($entity_type, $entity, $field, $instance, $langcode, &$items) { file_field_presave($entity_type, $entity, $field, $instance, $langcode, $items); + + // Determine the dimensions if necessary. + foreach ($items as &$item) { + if (!isset($item['width']) || !isset($item['height'])) { + $info = image_get_info(file_load($item['fid'])->uri); + + if (is_array($info)) { + $item['width'] = $info['width']; + $item['height'] = $info['height']; + } + } + } } /** @@ -277,7 +311,7 @@ $form['preview_image_style'] = array( '#title' => t('Preview image style'), '#type' => 'select', - '#options' => image_style_options(FALSE), + '#options' => image_style_options(FALSE, PASS_THROUGH), '#empty_option' => '<' . t('no preview') . '>', '#default_value' => $settings['preview_image_style'], '#description' => t('The preview image will be shown while editing the content.'), @@ -317,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 { @@ -345,9 +379,42 @@ // Add the image preview. if ($element['#file'] && $widget_settings['preview_image_style']) { + $variables = array( + 'style_name' => $widget_settings['preview_image_style'], + 'path' => $element['#file']->uri, + ); + + // Determine image dimensions. + if (isset($element['#value']['width']) && isset($element['#value']['height'])) { + $variables['width'] = $element['#value']['width']; + $variables['height'] = $element['#value']['height']; + } + else { + $info = image_get_info($element['#file']->uri); + + if (is_array($info)) { + $variables['width'] = $info['width']; + $variables['height'] = $info['height']; + } + else { + $variables['width'] = $variables['height'] = NULL; + } + } + $element['preview'] = array( '#type' => 'markup', - '#markup' => theme('image_style', array('style_name' => $widget_settings['preview_image_style'], 'path' => $element['#file']->uri)), + '#markup' => theme('image_style', $variables), + ); + + // Store the dimensions in the form so the file doesn't have to be accessed + // again. This is important for remote files. + $element['width'] = array( + '#type' => 'hidden', + '#value' => $variables['width'], + ); + $element['height'] = array( + '#type' => 'hidden', + '#value' => $variables['height'], ); } @@ -357,7 +424,8 @@ '#type' => 'textfield', '#default_value' => isset($item['alt']) ? $item['alt'] : '', '#description' => t('This text will be used by screen readers, search engines, or when the image cannot be loaded.'), - '#maxlength' => variable_get('image_alt_length', 80), // See http://www.gawds.org/show.php?contentid=28. + // @see http://www.gawds.org/show.php?contentid=28 + '#maxlength' => 512, '#weight' => -2, '#access' => (bool) $item['fid'] && $settings['alt_field'], ); @@ -366,7 +434,7 @@ '#title' => t('Title'), '#default_value' => isset($item['title']) ? $item['title'] : '', '#description' => t('The title is used as a tool tip when the user hovers the mouse over the image.'), - '#maxlength' => variable_get('image_title_length', 500), + '#maxlength' => 1024, '#weight' => -1, '#access' => (bool) $item['fid'] && $settings['title_field'], ); @@ -427,7 +495,7 @@ $display = $instance['display'][$view_mode]; $settings = $display['settings']; - $image_styles = image_style_options(FALSE); + $image_styles = image_style_options(FALSE, PASS_THROUGH); $element['image_style'] = array( '#title' => t('Image style'), '#type' => 'select', @@ -460,7 +528,7 @@ $summary = array(); - $image_styles = image_style_options(FALSE); + $image_styles = image_style_options(FALSE, PASS_THROUGH); // Unset possible 'No defined styles' option. unset($image_styles['']); // Styles could be lost because of enabled/disabled modules that defines @@ -521,7 +589,8 @@ * * @param $variables * An associative array containing: - * - item: An array of image data. + * - item: Associative array of image data, which may include "uri", "alt", + * "width", "height", "title" and "attributes". * - image_style: An optional image style. * - path: An array containing the link 'path' and link 'options'. * @@ -531,10 +600,23 @@ $item = $variables['item']; $image = array( 'path' => $item['uri'], - 'alt' => $item['alt'], ); + + if (array_key_exists('alt', $item)) { + $image['alt'] = $item['alt']; + } + + if (isset($item['attributes'])) { + $image['attributes'] = $item['attributes']; + } + + if (isset($item['width']) && isset($item['height'])) { + $image['width'] = $item['width']; + $image['height'] = $item['height']; + } + // Do not output an empty 'title' attribute. - if (drupal_strlen($item['title']) > 0) { + if (isset($item['title']) && drupal_strlen($item['title']) > 0) { $image['title'] = $item['title']; } @@ -546,9 +628,11 @@ $output = theme('image', $image); } - if ($variables['path']) { + // The link path and link options are both optional, but for the options to be + // processed, the link path must at least be an empty string. + if (isset($variables['path']['path'])) { $path = $variables['path']['path']; - $options = $variables['path']['options']; + $options = isset($variables['path']['options']) ? $variables['path']['options'] : array(); // When displaying an image inside a link, the html option must be TRUE. $options['html'] = TRUE; $output = l($output, $path, $options); diff -Naur drupal-7.0/modules/image/image.info drupal-7.66/modules/image/image.info --- drupal-7.0/modules/image/image.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/image/image.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: image.info,v 1.6 2010/12/20 19:59:42 webchick Exp $ name = Image description = Provides image manipulation tools. package = Core @@ -8,8 +7,7 @@ files[] = image.test configure = admin/config/media/image-styles -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/image/image.install drupal-7.66/modules/image/image.install --- drupal-7.0/modules/image/image.install 2010-11-21 10:24:41.000000000 +0100 +++ drupal-7.66/modules/image/image.install 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ TRUE, ), 'name' => array( - 'description' => 'The style name.', + 'description' => 'The style machine name.', 'type' => 'varchar', 'length' => 255, 'not null' => TRUE, ), + 'label' => array( + 'description' => 'The style administrative name.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), ), 'primary key' => array('isid'), 'unique keys' => array( @@ -122,15 +128,25 @@ 'alt' => array( 'description' => "Alternative image text, for the image's 'alt' attribute.", 'type' => 'varchar', - 'length' => 128, + 'length' => 512, 'not null' => FALSE, ), 'title' => array( 'description' => "Image title text, for the image's 'title' attribute.", 'type' => 'varchar', - 'length' => 128, + 'length' => 1024, 'not null' => FALSE, ), + 'width' => array( + 'description' => 'The width of the image in pixels.', + 'type' => 'int', + 'unsigned' => TRUE, + ), + 'height' => array( + 'description' => 'The height of the image in pixels.', + 'type' => 'int', + 'unsigned' => TRUE, + ), ), 'indexes' => array( 'fid' => array('fid'), @@ -145,6 +161,18 @@ } /** + * Implements hook_update_dependencies(). + */ +function image_update_dependencies() { + $dependencies['image'][7002] = array( + // Image update 7002 uses field API functions, so must run after + // Field API has been enabled. + 'system' => 7020, + ); + return $dependencies; +} + +/** * Install the schema for users upgrading from the contributed module. */ function image_update_7000() { @@ -228,11 +256,234 @@ db_create_table('cache_image', $schema['cache_image']); db_create_table('image_styles', $schema['image_styles']); - db_create_table('image_effect', $schema['image_effects']); + db_create_table('image_effects', $schema['image_effects']); + } +} + +/** + * @addtogroup updates-7.x-extra + * @{ + */ + +/** + * Rename possibly misnamed {image_effect} table to {image_effects}. + */ +function image_update_7001() { + // Due to a bug in earlier versions of image_update_7000() it is possible + // to end up with an {image_effect} table where there should be an + // {image_effects} table. + if (!db_table_exists('image_effects') && db_table_exists('image_effect')) { + db_rename_table('image_effect', 'image_effects'); + } +} + +/** + * Add width and height columns to a specific table. + * + * @param $table + * The name of the database table to be updated. + * @param $columns + * Keyed array of columns this table is supposed to have. + */ +function _image_update_7002_add_columns($table, $field_name) { + $spec = array( + 'type' => 'int', + 'unsigned' => TRUE, + ); + + $spec['description'] = 'The width of the image in pixels.'; + db_add_field($table, $field_name . '_width', $spec); + + $spec['description'] = 'The height of the image in pixels.'; + db_add_field($table, $field_name . '_height', $spec); +} + +/** + * Populate image dimensions in a specific table. + * + * @param $table + * The name of the database table to be updated. + * @param $columns + * Keyed array of columns this table is supposed to have. + * @param $last_fid + * The fid of the last image to have been processed. + * + * @return + * The number of images that were processed. + */ +function _image_update_7002_populate_dimensions($table, $field_name, &$last_fid) { + // Define how many images to process per pass. + $images_per_pass = 100; + + // Query the database for fid / URI pairs. + $query = db_select($table, NULL, array('fetch' => PDO::FETCH_ASSOC)); + $query->join('file_managed', NULL, $table . '.' . $field_name . '_fid = file_managed.fid'); + + if ($last_fid) { + $query->condition('file_managed.fid', $last_fid, '>'); + } + + $result = $query->fields('file_managed', array('fid', 'uri')) + ->orderBy('file_managed.fid') + ->range(0, $images_per_pass) + ->execute(); + + $count = 0; + foreach ($result as $file) { + $count++; + $info = image_get_info($file['uri']); + + if (is_array($info)) { + db_update($table) + ->fields(array( + $field_name . '_width' => $info['width'], + $field_name . '_height' => $info['height'], + )) + ->condition($field_name . '_fid', $file['fid']) + ->execute(); + } } + + // If less than the requested number of rows were returned then this table + // has been fully processed. + $last_fid = ($count < $images_per_pass) ? NULL : $file['fid']; + return $count; +} + +/** + * Add width and height columns to image field schema and populate. + */ +function image_update_7002(array &$sandbox) { + if (empty($sandbox)) { + // Setup the sandbox. + $sandbox = array( + 'tables' => array(), + 'total' => 0, + 'processed' => 0, + 'last_fid' => NULL, + ); + + $fields = _update_7000_field_read_fields(array( + 'module' => 'image', + 'storage_type' => 'field_sql_storage', + 'deleted' => 0, + )); + + foreach ($fields as $field) { + $tables = array( + _field_sql_storage_tablename($field), + _field_sql_storage_revision_tablename($field), + ); + foreach ($tables as $table) { + // Add the width and height columns to the table. + _image_update_7002_add_columns($table, $field['field_name']); + + // How many rows need dimensions populated? + $count = db_select($table)->countQuery()->execute()->fetchField(); + + if (!$count) { + continue; + } + + $sandbox['total'] += $count; + $sandbox['tables'][$table] = $field['field_name']; + } + } + + // If no tables need rows populated with dimensions then we are done. + if (empty($sandbox['tables'])) { + $sandbox = array(); + return; + } + } + + // Process the table at the top of the list. + $keys = array_keys($sandbox['tables']); + $table = reset($keys); + $sandbox['processed'] += _image_update_7002_populate_dimensions($table, $sandbox['tables'][$table], $sandbox['last_fid']); + + // Has the table been fully processed? + if (!$sandbox['last_fid']) { + unset($sandbox['tables'][$table]); + } + + $sandbox['#finished'] = count($sandbox['tables']) ? ($sandbox['processed'] / $sandbox['total']) : 1; +} + +/** + * Remove the variables that set alt and title length since they were not + * used for database column size and could cause PDO exceptions. + */ +function image_update_7003() { + variable_del('image_alt_length'); + variable_del('image_title_length'); } /** + * Use a large setting (512 and 1024 characters) for the length of the image alt + * and title fields. + */ +function image_update_7004() { + $alt_spec = array( + 'type' => 'varchar', + 'length' => 512, + 'not null' => FALSE, + ); + + $title_spec = array( + 'type' => 'varchar', + 'length' => 1024, + 'not null' => FALSE, + ); + + $fields = _update_7000_field_read_fields(array( + 'module' => 'image', + 'storage_type' => 'field_sql_storage', + )); + + foreach ($fields as $field_name => $field) { + $tables = array( + _field_sql_storage_tablename($field), + _field_sql_storage_revision_tablename($field), + ); + $alt_column = $field['field_name'] . '_alt'; + $title_column = $field['field_name'] . '_title'; + foreach ($tables as $table) { + db_change_field($table, $alt_column, $alt_column, $alt_spec); + db_change_field($table, $title_column, $title_column, $title_spec); + } + } +} + +/** + * Add a column to the 'image_style' table to store administrative labels. + */ +function image_update_7005() { + $field = array( + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + 'description' => 'The style administrative name.', + ); + db_add_field('image_styles', 'label', $field); + + // Do a direct query here, rather than calling image_styles(), + // in case Image module is disabled. + $styles = db_query('SELECT name FROM {image_styles}')->fetchCol(); + foreach ($styles as $style) { + db_update('image_styles') + ->fields(array('label' => $style)) + ->condition('name', $style) + ->execute(); + } +} + +/** + * @} End of "addtogroup updates-7.x-extra". + */ + +/** * Implements hook_requirements() to check the PHP GD Library. * * @param $phase @@ -253,7 +504,7 @@ $requirements['image_gd']['severity'] = REQUIREMENT_OK; } else { - $requirements['image_gd']['severity'] = REQUIREMENT_ERROR; + $requirements['image_gd']['severity'] = REQUIREMENT_WARNING; $requirements['image_gd']['description'] = t('The GD Library for PHP is enabled, but was compiled without support for functions used by the rotate and desaturate effects. It was probably compiled using the official GD libraries from http://www.libgd.org instead of the GD library bundled with PHP. You should recompile PHP --with-gd using the bundled GD library. See the PHP manual.'); } } diff -Naur drupal-7.0/modules/image/image.module drupal-7.66/modules/image/image.module --- drupal-7.0/modules/image/image.module 2010-11-18 06:36:27.000000000 +0100 +++ drupal-7.66/modules/image/image.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ ' . t('About') . ''; - $output .= '

      ' . t('The Image module allows you to manipulate images on your website. It exposes a setting for using the Image toolkit, allows you to configure Image styles that can be used for resizing or adjusting images on display, and provides an Image field for attaching images to content. For more information, see the online handbook entry for Image module.', array('@image' => 'http://drupal.org/handbook/modules/image')) . '

      '; + $output .= '

      ' . t('The Image module allows you to manipulate images on your website. It exposes a setting for using the Image toolkit, allows you to configure Image styles that can be used for resizing or adjusting images on display, and provides an Image field for attaching images to content. For more information, see the online handbook entry for Image module.', array('@image' => 'http://drupal.org/documentation/modules/image')) . '

      '; $output .= '

      ' . t('Uses') . '

      '; $output .= '
      '; $output .= '
      ' . t('Manipulating images') . '
      '; @@ -60,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; } } @@ -186,10 +190,11 @@ 'variables' => array( 'style_name' => NULL, 'path' => NULL, + 'width' => NULL, + 'height' => NULL, 'alt' => '', 'title' => NULL, 'attributes' => array(), - 'getsize' => TRUE, ), ), @@ -249,7 +254,7 @@ } /** - * Submit handler for the file system settings form. + * Form submission handler for system_file_system_settings(). * * Adds a menu rebuild after the public file path has been changed, so that the * menu router item depending on that file path will be regenerated. @@ -292,26 +297,24 @@ if ($info = image_get_info($uri)) { // Check the permissions of the original to grant access to this image. $headers = module_invoke_all('file_download', $original_uri); - if (!in_array(-1, $headers)) { + // Confirm there's at least one module granting access and none denying access. + if (!empty($headers) && !in_array(-1, $headers)) { return array( // Send headers describing the image's size, and MIME-type... 'Content-Type' => $info['mime_type'], 'Content-Length' => $info['file_size'], - // ...and allow the file to be cached for two weeks (matching the - // value we/ use for the mod_expires settings in .htaccess) and - // ensure that caching proxies do not share the image with other - // users. - 'Expires' => gmdate(DATE_RFC1123, REQUEST_TIME + 1209600), - 'Cache-Control' => 'max-age=1209600, private, must-revalidate', + // By not explicitly setting them here, this uses normal Drupal + // Expires, Cache-Control and ETag headers to prevent proxy or + // browser caching of private images. ); } } return -1; } - // Private file access for the original files. Note that we only - // check access for non-temporary images, since file.module will - // grant access for all temporary files. + // Private file access for the original files. Note that we only check access + // for non-temporary images, since file.module will grant access for all + // temporary files. $files = file_load_multiple(array(), array('uri' => $uri)); if (count($files)) { $file = reset($files); @@ -326,7 +329,7 @@ */ function image_file_move($file, $source) { // Delete any image derivatives at the original image path. - image_path_flush($file->uri); + image_path_flush($source->uri); } /** @@ -344,6 +347,7 @@ $styles = array(); $styles['thumbnail'] = array( + 'label' => 'Thumbnail (100x100)', 'effects' => array( array( 'name' => 'image_scale', @@ -354,6 +358,7 @@ ); $styles['medium'] = array( + 'label' => 'Medium (220x220)', 'effects' => array( array( 'name' => 'image_scale', @@ -364,6 +369,7 @@ ); $styles['large'] = array( + 'label' => 'Large (480x480)', 'effects' => array( array( 'name' => 'image_scale', @@ -415,7 +421,126 @@ } /** - * Clear cached versions of a specific file in all styles. + * Implements hook_field_delete_field(). + */ +function image_field_delete_field($field) { + if ($field['type'] != 'image') { + return; + } + + // The value of a managed_file element can be an array if #extended == TRUE. + $fid = (is_array($field['settings']['default_image']) ? $field['settings']['default_image']['fid'] : $field['settings']['default_image']); + if ($fid && ($file = file_load($fid))) { + file_usage_delete($file, 'image', 'default_image', $field['id']); + } +} + +/** + * Implements hook_field_update_field(). + */ +function image_field_update_field($field, $prior_field, $has_data) { + if ($field['type'] != 'image') { + return; + } + + // The value of a managed_file element can be an array if #extended == TRUE. + $fid_new = (is_array($field['settings']['default_image']) ? $field['settings']['default_image']['fid'] : $field['settings']['default_image']); + $fid_old = (is_array($prior_field['settings']['default_image']) ? $prior_field['settings']['default_image']['fid'] : $prior_field['settings']['default_image']); + + $file_new = $fid_new ? file_load($fid_new) : FALSE; + + if ($fid_new != $fid_old) { + + // Is there a new file? + if ($file_new) { + $file_new->status = FILE_STATUS_PERMANENT; + file_save($file_new); + file_usage_add($file_new, 'image', 'default_image', $field['id']); + } + + // Is there an old file? + if ($fid_old && ($file_old = file_load($fid_old))) { + file_usage_delete($file_old, 'image', 'default_image', $field['id']); + } + } + + // If the upload destination changed, then move the file. + if ($file_new && (file_uri_scheme($file_new->uri) != $field['settings']['uri_scheme'])) { + $directory = $field['settings']['uri_scheme'] . '://default_images/'; + file_prepare_directory($directory, FILE_CREATE_DIRECTORY); + file_move($file_new, $directory . $file_new->filename); + } +} + +/** + * Implements hook_field_delete_instance(). + */ +function image_field_delete_instance($instance) { + // Only act on image fields. + $field = field_read_field($instance['field_name']); + if ($field['type'] != 'image') { + return; + } + + // The value of a managed_file element can be an array if the #extended + // property is set to TRUE. + $fid = $instance['settings']['default_image']; + if (is_array($fid)) { + $fid = $fid['fid']; + } + + // Remove the default image when the instance is deleted. + if ($fid && ($file = file_load($fid))) { + file_usage_delete($file, 'image', 'default_image', $instance['id']); + } +} + +/** + * Implements hook_field_update_instance(). + */ +function image_field_update_instance($instance, $prior_instance) { + // Only act on image fields. + $field = field_read_field($instance['field_name']); + if ($field['type'] != 'image') { + return; + } + + // The value of a managed_file element can be an array if the #extended + // property is set to TRUE. + $fid_new = $instance['settings']['default_image']; + if (is_array($fid_new)) { + $fid_new = $fid_new['fid']; + } + $fid_old = $prior_instance['settings']['default_image']; + if (is_array($fid_old)) { + $fid_old = $fid_old['fid']; + } + + // If the old and new files do not match, update the default accordingly. + $file_new = $fid_new ? file_load($fid_new) : FALSE; + if ($fid_new != $fid_old) { + // Save the new file, if present. + if ($file_new) { + $file_new->status = FILE_STATUS_PERMANENT; + file_save($file_new); + file_usage_add($file_new, 'image', 'default_image', $instance['id']); + } + // Delete the old file, if present. + if ($fid_old && ($file_old = file_load($fid_old))) { + file_usage_delete($file_old, 'image', 'default_image', $instance['id']); + } + } + + // If the upload destination changed, then move the file. + if ($file_new && (file_uri_scheme($file_new->uri) != $field['settings']['uri_scheme'])) { + $directory = $field['settings']['uri_scheme'] . '://default_images/'; + file_prepare_directory($directory, FILE_CREATE_DIRECTORY); + file_move($file_new, $directory . $file_new->filename); + } +} + +/** + * Clears cached versions of a specific file in all styles. * * @param $path * The Drupal file path to the original image. @@ -431,7 +556,7 @@ } /** - * Get an array of all styles and their settings. + * Gets an array of all styles and their settings. * * @return * An array of styles keyed by the image style ID (isid). @@ -453,6 +578,7 @@ $module_styles = module_invoke($module, 'image_default_styles'); foreach ($module_styles as $style_name => $style) { $style['name'] = $style_name; + $style['label'] = empty($style['label']) ? $style_name : $style['label']; $style['module'] = $module; $style['storage'] = IMAGE_STORAGE_DEFAULT; foreach ($style['effects'] as $key => $effect) { @@ -492,7 +618,9 @@ } /** - * Load a style by style name or ID. May be used as a loader for menu items. + * Loads a style by style name or ID. + * + * May be used as a loader for menu items. * * @param $name * The name of the style. @@ -501,6 +629,7 @@ * @param $include * If set, this loader will restrict to a specific type of image style, may be * one of the defined Image style storage constants. + * * @return * An image style array containing the following keys: * - "isid": The unique image style ID. @@ -538,12 +667,20 @@ } /** - * Save an image style. + * Saves an image style. * - * @param style - * An image style array. - * @return - * An image style array. In the case of a new style, 'isid' will be populated. + * @param array $style + * An image style array containing: + * - name: A unique name for the style. + * - isid: (optional) An image style ID. + * + * @return array + * An image style array containing: + * - name: An unique name for the style. + * - old_name: The original name for the style. + * - isid: An image style ID. + * - is_new: TRUE if this is a new style, and FALSE if it is an existing + * style. */ function image_style_save($style) { if (isset($style['isid']) && is_numeric($style['isid'])) { @@ -556,6 +693,10 @@ } } else { + // Add a default label when not given. + if (empty($style['label'])) { + $style['label'] = $style['name']; + } drupal_write_record('image_styles', $style); $style['is_new'] = TRUE; } @@ -570,13 +711,14 @@ } /** - * Delete an image style. + * Deletes an image style. * * @param $style * An image style array. * @param $replacement_style_name * (optional) When deleting a style, specify a replacement style name so * that existing settings (if any) may be converted to a new style. + * * @return * TRUE on success. */ @@ -595,14 +737,17 @@ } /** - * Load all the effects for an image style. + * Loads all the effects for an image style. * - * @param $style - * An image style array. - * @return + * @param array $style + * An image style array containing: + * - isid: The unique image style ID that contains this image effect. + * + * @return array * An array of image effects associated with specified image style in the * format array('isid' => array()), or an empty array if the specified style * has no effects. + * @see image_effects() */ function image_style_effects($style) { $effects = image_effects(); @@ -617,20 +762,32 @@ } /** - * Get an array of image styles suitable for using as select list options. + * Gets an array of image styles suitable for using as select list options. * * @param $include_empty * If TRUE a option will be inserted in the options array. + * @param $output + * Optional flag determining how the options will be sanitized on output. + * Leave this at the default (CHECK_PLAIN) if you are using the output of + * this function directly in an HTML context, such as for checkbox or radio + * button labels, and do not plan to sanitize it on your own. If using the + * output of this function as select list options (its primary use case), you + * should instead set this flag to PASS_THROUGH to avoid double-escaping of + * the output (the form API sanitizes select list options by default). + * * @return - * Array of image styles both key and value are set to style name. + * Array of image styles with the machine name as key and the label as value. */ -function image_style_options($include_empty = TRUE) { +function image_style_options($include_empty = TRUE, $output = CHECK_PLAIN) { $styles = image_styles(); $options = array(); if ($include_empty && !empty($styles)) { $options[''] = t(''); } - $options = array_merge($options, drupal_map_assoc(array_keys($styles))); + foreach ($styles as $name => $style) { + $options[$name] = ($output == PASS_THROUGH) ? $style['label'] : check_plain($style['label']); + } + if (empty($options)) { $options[''] = t('No defined styles'); } @@ -638,24 +795,36 @@ } /** - * Menu callback; Given a style and image path, generate a derivative. + * Page callback: Generates a derivative, given a style and image path. * * After generating an image, transfer it to the requesting agent. * * @param $style * The image style + * @param $scheme + * The file scheme, for example 'public' for public files. */ function image_style_deliver($style, $scheme) { - // Check that the style is defined and the scheme is valid. - if (!$style || !file_stream_wrapper_valid_scheme($scheme)) { - drupal_exit(); - } - $args = func_get_args(); array_shift($args); array_shift($args); $target = implode('/', $args); + // Check that the style is defined, the scheme is valid, and the image + // derivative token is valid. (Sites which require image derivatives to be + // generated without a token can set the 'image_allow_insecure_derivatives' + // variable to TRUE to bypass the latter check, but this will increase the + // site's vulnerability to denial-of-service attacks. To prevent this + // variable from leaving the site vulnerable to the most serious attacks, a + // token is always required when a derivative of a derivative is requested.) + $valid = !empty($style) && file_stream_wrapper_valid_scheme($scheme); + if (!variable_get('image_allow_insecure_derivatives', FALSE) || strpos(ltrim($target, '\/'), 'styles/') === 0) { + $valid = $valid && isset($_GET[IMAGE_DERIVATIVE_TOKEN]) && $_GET[IMAGE_DERIVATIVE_TOKEN] === image_style_path_token($style['name'], $scheme . '://' . $target); + } + if (!$valid) { + return MENU_ACCESS_DENIED; + } + $image_uri = $scheme . '://' . $target; $derivative_uri = image_style_path($style['name'], $image_uri); @@ -666,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) { @@ -678,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); @@ -687,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(); @@ -708,13 +884,18 @@ 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(); } } /** - * Create a new image based on an image style. + * Creates a new image derivative based on an image style. + * + * Generates an image derivative by creating the destination folder (if it does + * not already exist), applying all image effects defined in $style['effects'], + * and saving a cached version of the resulting image. * * @param $style * An image style array. @@ -722,11 +903,19 @@ * Path of the source file. * @param $destination * Path or URI of the destination file. + * * @return - * TRUE if an image derivative is generated, FALSE if no image derivative - * is generated. NULL if the derivative is being generated. + * TRUE if an image derivative was generated, or FALSE if the image derivative + * could not be generated. + * + * @see image_style_load() */ function image_style_create_derivative($style, $source, $destination) { + // If the source file doesn't exist, return FALSE without creating folders. + if (!$image = image_load($source)) { + return FALSE; + } + // Get the folder for the final location of this style. $directory = drupal_dirname($destination); @@ -736,10 +925,6 @@ return FALSE; } - if (!$image = image_load($source)) { - return FALSE; - } - foreach ($style['effects'] as $effect) { image_effect_apply($image, $effect); } @@ -755,15 +940,51 @@ } /** - * Flush cached media for a style. + * Determines the dimensions of the styled image. + * + * Applies all of an image style's effects to $dimensions. + * + * @param $style_name + * The name of the style to be applied. + * @param $dimensions + * Dimensions to be modified - an array with components width and height, in + * pixels. + */ +function image_style_transform_dimensions($style_name, array &$dimensions) { + module_load_include('inc', 'image', 'image.effects'); + $style = image_style_load($style_name); + + if (!is_array($style)) { + return; + } + + foreach ($style['effects'] as $effect) { + if (isset($effect['dimensions passthrough'])) { + continue; + } + + if (isset($effect['dimensions callback'])) { + $effect['dimensions callback']($dimensions, $effect['data']); + } + else { + $dimensions['width'] = $dimensions['height'] = NULL; + } + } +} + +/** + * Flushes cached media for a style. * * @param $style * An image style array. */ function image_style_flush($style) { - $style_directory = drupal_realpath(file_default_scheme() . '://styles/' . $style['name']); - if (is_dir($style_directory)) { - file_unmanaged_delete_recursive($style_directory); + // Delete the style directory in each registered wrapper. + $wrappers = file_get_stream_wrappers(STREAM_WRAPPERS_WRITE_VISIBLE); + foreach ($wrappers as $wrapper => $wrapper_data) { + if (file_exists($directory = $wrapper . '://styles/' . $style['name'])) { + file_unmanaged_delete_recursive($directory); + } } // Let other modules update as necessary on flush. @@ -787,32 +1008,78 @@ } /** - * Return the URL for an image derivative given a style and image path. + * Returns the URL for an image derivative given a style and image path. * * @param $style_name * The name of the style to be used with this image. * @param $path * The path to the image. + * * @return * The absolute URL where a style image can be downloaded, suitable for use * in an tag. Requesting the URL will cause the image to be created. * @see image_style_deliver() */ function image_style_url($style_name, $path) { - $scheme = file_uri_scheme($path); - if ($scheme === 'private') { - $target = file_uri_target($path); - $url = url('system/files/styles/' . $style_name . '/' . $scheme . '/' . $target, array('absolute' => TRUE)); + $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. + // 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)); } - else { - $destination = image_style_path($style_name, $path); - $url = url(file_stream_wrapper_get_instance_by_scheme($scheme)->getDirectoryPath() . '/' . file_uri_target($destination), array('absolute' => TRUE)); + + // 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 + // that it is included. Once the file exists it's fine to fall back to the + // actual file path, this avoids bootstrapping PHP once the files are built. + if (!variable_get('clean_url') && file_uri_scheme($uri) == 'public' && !file_exists($uri)) { + $directory_path = file_stream_wrapper_get_instance_by_uri($uri)->getDirectoryPath(); + return url($directory_path . '/' . file_uri_target($uri), array('absolute' => TRUE, 'query' => $token_query)); } - return $url; + + $file_url = file_create_url($uri); + // 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; +} + +/** + * Generates a token to protect an image style derivative. + * + * This prevents unauthorized generation of an image style derivative, + * which can be costly both in CPU time and disk space. + * + * @param $style_name + * The name of the image style. + * @param $uri + * The URI of the image for this style, for example as returned by + * image_style_path(). + * + * @return + * An eight-character token which can be used to protect image style + * derivatives against denial-of-service attacks. + */ +function image_style_path_token($style_name, $uri) { + // Return the first eight characters. + return substr(drupal_hmac_base64($style_name . ':' . $uri, drupal_get_private_key() . drupal_get_hash_salt()), 0, 8); } /** - * Return the URI of an image when using a style. + * Returns the URI of an image when using a style. * * The path returned by this function may not exist. The default generation * method only creates images when they are requested by a user's browser. @@ -821,6 +1088,7 @@ * The name of the style to be used with this image. * @param $uri * The URI or path to the image. + * * @return * The URI to an image style image. * @see image_style_url() @@ -838,10 +1106,11 @@ } /** - * Save a default image style to the database. + * Saves a default image style to the database. * * @param style * An image style array provided by a module. + * * @return * An image style array. The returned style array will include the new 'isid' * assigned to the style. @@ -859,7 +1128,7 @@ } /** - * Revert the changes made by users to a default image style. + * Reverts the changes made by users to a default image style. * * @param style * An image style array. @@ -876,7 +1145,10 @@ } /** - * Pull in image effects exposed by modules implementing hook_image_effect_info(). + * Returns a set of image effects. + * + * These image effects are exposed by modules implementing + * hook_image_effect_info(). * * @return * An array of image effects to be used when transforming images. @@ -893,7 +1165,7 @@ $effects = &drupal_static(__FUNCTION__); if (!isset($effects)) { - if ($cache = cache_get("image_effects:$langcode") && !empty($cache->data)) { + if ($cache = cache_get("image_effects:$langcode")) { $effects = $cache->data; } else { @@ -918,7 +1190,7 @@ } /** - * Load the definition for an image effect. + * Loads the definition for an image effect. * * The effect definition is a set of core properties for an image effect, not * containing any user-settings. The definition defines various functions to @@ -930,6 +1202,7 @@ * The name of the effect definition to load. * @param $style * An image style array to which this effect will be added. + * * @return * An array containing the image effect definition with the following keys: * - "effect": The unique name for the effect being performed. Usually prefixed @@ -957,7 +1230,7 @@ } /** - * Load all image effects from the database. + * Loads all image effects from the database. * * @return * An array of all image effects. @@ -989,7 +1262,7 @@ } /** - * Load a single image effect. + * Loads a single image effect. * * @param $ieid * The image effect ID. @@ -998,6 +1271,7 @@ * @param $include * If set, this loader will restrict to a specific type of image style, may be * one of the defined Image style storage constants. + * * @return * An image effect array, consisting of the following keys: * - "ieid": The unique image effect ID. @@ -1019,10 +1293,11 @@ } /** - * Save an image effect. + * Saves an image effect. * * @param $effect * An image effect array. + * * @return * An image effect array. In the case of a new effect, 'ieid' will be set. */ @@ -1039,7 +1314,7 @@ } /** - * Delete an image effect. + * Deletes an image effect. * * @param $effect * An image effect array. @@ -1051,12 +1326,13 @@ } /** - * Given an image object and effect, perform the effect on the file. + * Applies an image effect to the image object. * * @param $image * An image object returned by image_load(). * @param $effect * An image effect array. + * * @return * TRUE on success. FALSE if unable to perform the image effect on the image. */ @@ -1077,33 +1353,37 @@ * - style_name: The name of the style to be used to alter the original image. * - path: The path of the image file relative to the Drupal files directory. * This function does not work with images outside the files directory nor - * with remotely hosted images. + * with remotely hosted images. This should be in a format such as + * 'images/image.jpg', or using a stream wrapper such as + * 'public://images/image.jpg'. + * - width: The width of the source image (if known). + * - height: The height of the source image (if known). * - alt: The alternative text for text-based browsers. * - title: The title text is displayed when the image is hovered in some * popular browsers. * - attributes: Associative array of attributes to be placed in the img tag. - * - getsize: If set to TRUE, the image's dimension are fetched and added as - * width/height attributes. * * @ingroup themeable */ function theme_image_style($variables) { - $style_name = $variables['style_name']; - $path = $variables['path']; + // Determine the dimensions of the styled image. + $dimensions = array( + 'width' => $variables['width'], + 'height' => $variables['height'], + ); - // theme_image() can only honor the $getsize parameter with local file paths. - // The derivative image is not created until it has been requested so the file - // may not yet exist, in this case we just fallback to the URL. - $style_path = image_style_path($style_name, $path); - if (!file_exists($style_path)) { - $style_path = image_style_url($style_name, $path); - } - $variables['path'] = $style_path; + image_style_transform_dimensions($variables['style_name'], $dimensions); + + $variables['width'] = $dimensions['width']; + $variables['height'] = $dimensions['height']; + + // Determine the URL for the styled image. + $variables['path'] = image_style_url($variables['style_name'], $variables['path']); return theme('image', $variables); } /** - * Accept a keyword (center, top, left, etc) and return it as a pixel offset. + * Accepts a keyword (center, top, left, etc) and returns it as a pixel offset. * * @param $value * @param $current_pixels diff -Naur drupal-7.0/modules/image/image.test drupal-7.66/modules/image/image.test --- drupal-7.0/modules/image/image.test 2010-09-22 05:24:09.000000000 +0200 +++ drupal-7.66/modules/image/image.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,12 +1,10 @@ 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); } @@ -81,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 @@ -107,7 +122,7 @@ /** * Tests the functions for generating paths and URLs for image styles. */ -class ImageStylesPathAndUrlUnitTest extends DrupalWebTestCase { +class ImageStylesPathAndUrlTestCase extends DrupalWebTestCase { protected $style_name; protected $image_info; protected $image_filepath; @@ -124,7 +139,7 @@ parent::setUp('image_module_test'); $this->style_name = 'style_foo'; - image_style_save(array('name' => $this->style_name)); + image_style_save(array('name' => $this->style_name, 'label' => $this->randomString())); } /** @@ -134,11 +149,11 @@ $scheme = 'public'; $actual = image_style_path($this->style_name, "$scheme://foo/bar.gif"); $expected = "$scheme://styles/" . $this->style_name . "/$scheme/foo/bar.gif"; - $this->assertEqual($actual, $expected, t('Got the path for a file URI.')); + $this->assertEqual($actual, $expected, 'Got the path for a file URI.'); $actual = image_style_path($this->style_name, 'foo/bar.gif'); $expected = "$scheme://styles/" . $this->style_name . "/$scheme/foo/bar.gif"; - $this->assertEqual($actual, $expected, t('Got the path for a relative file path.')); + $this->assertEqual($actual, $expected, 'Got the path for a relative file path.'); } /** @@ -156,44 +171,213 @@ } /** + * Test image_style_url() with the "public://" scheme and unclean URLs. + */ + function testImageStylUrlAndPathPublicUnclean() { + $this->_testImageStyleUrlAndPath('public', FALSE); + } + + /** + * Test image_style_url() with the "private://" schema and unclean URLs. + */ + function testImageStyleUrlAndPathPrivateUnclean() { + $this->_testImageStyleUrlAndPath('private', FALSE); + } + + /** + * Test image_style_url() with a file URL that has an extra slash in it. + */ + function testImageStyleUrlExtraSlash() { + $this->_testImageStyleUrlAndPath('public', TRUE, TRUE); + } + + /** + * Test that an invalid source image returns a 404. + */ + function testImageStyleUrlForMissingSourceImage() { + $non_existent_uri = 'public://foo.png'; + $generated_url = image_style_url($this->style_name, $non_existent_uri); + $this->drupalGet($generated_url); + $this->assertResponse(404, 'Accessing an image style URL with a source image that does not exist provides a 404 error response.'); + } + + /** + * Test 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) { + function _testImageStyleUrlAndPath($scheme, $clean_url = TRUE, $extra_slash = FALSE) { // Make the default scheme neither "public" nor "private" to verify the // functions work for other than the default scheme. variable_set('file_default_scheme', 'temporary'); + variable_set('clean_url', $clean_url); // Create the directories for the styles. $directory = $scheme . '://styles/' . $this->style_name; $status = file_prepare_directory($directory, FILE_CREATE_DIRECTORY); - $this->assertNotIdentical(FALSE, $status, t('Created the directory for the generated images for the test style.')); + $this->assertNotIdentical(FALSE, $status, 'Created the directory for the generated images for the test style.'); // Create a working copy of the file. $files = $this->drupalGetTestFiles('image'); - $file = reset($files); + $file = array_shift($files); $image_info = image_get_info($file->uri); $original_uri = file_unmanaged_copy($file->uri, $scheme . '://', FILE_EXISTS_RENAME); // Let the image_module_test module know about this file, so it can claim // ownership in hook_file_download(). variable_set('image_module_test_file_download', $original_uri); - $this->assertNotIdentical(FALSE, $original_uri, t('Created the generated image file.')); + $this->assertNotIdentical(FALSE, $original_uri, 'Created the generated image file.'); // Get the URL of a file that has not been generated and try to create it. - $generated_uri = $scheme . '://styles/' . $this->style_name . '/' . $scheme . '/'. basename($original_uri); - $this->assertFalse(file_exists($generated_uri), t('Generated file does not exist.')); + $generated_uri = image_style_path($this->style_name, $original_uri); + $this->assertFalse(file_exists($generated_uri), 'Generated file does not exist.'); $generate_url = image_style_url($this->style_name, $original_uri); + // Ensure that the tests still pass when the file is generated by accessing + // a poorly constructed (but still valid) file URL that has an extra slash + // in it. + if ($extra_slash) { + $modified_uri = str_replace('://', ':///', $original_uri); + $this->assertNotEqual($original_uri, $modified_uri, 'An extra slash was added to the generated file URI.'); + $generate_url = image_style_url($this->style_name, $modified_uri); + } + + if (!$clean_url) { + $this->assertTrue(strpos($generate_url, '?q=') !== FALSE, 'When using non-clean URLS, the system path contains the query string.'); + } + // 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 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 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); - $this->assertResponse(200, t('Image was generated at the URL.')); - $this->assertTrue(file_exists($generated_uri), t('Generated file does exist after we accessed it.')); - $this->assertRaw(file_get_contents($generated_uri), t('URL returns expected file.')); + $this->assertResponse(200, 'Image was generated at the URL.'); + $this->assertTrue(file_exists($generated_uri), 'Generated file does exist after we accessed it.'); + $this->assertRaw(file_get_contents($generated_uri), 'URL returns expected file.'); $generated_image_info = image_get_info($generated_uri); - $this->assertEqual($this->drupalGetHeader('Content-Type'), $generated_image_info['mime_type'], t('Expected Content-Type was reported.')); - $this->assertEqual($this->drupalGetHeader('Content-Length'), $generated_image_info['file_size'], t('Expected Content-Length was reported.')); + $this->assertEqual($this->drupalGetHeader('Content-Type'), $generated_image_info['mime_type'], 'Expected Content-Type was reported.'); + $this->assertEqual($this->drupalGetHeader('Content-Length'), $generated_image_info['file_size'], 'Expected Content-Length was reported.'); if ($scheme == 'private') { - $this->assertEqual($this->drupalGetHeader('X-Image-Owned-By'), 'image_module_test', t('Expected custom header has been added.')); + $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', '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 + // too. + $this->drupalGet($generate_url); + $this->assertResponse(200, 'Image was generated at the URL.'); + + // Make sure that access is denied for existing style files if we do not + // have access. + variable_del('image_module_test_file_download'); + $this->drupalGet($generate_url); + $this->assertResponse(403, 'Confirmed that access is denied for the private image style.'); + + // Repeat this with a different file that we do not have access to and + // make sure that access is denied. + $file_noaccess = array_shift($files); + $original_uri_noaccess = file_unmanaged_copy($file_noaccess->uri, $scheme . '://', FILE_EXISTS_RENAME); + $generated_uri_noaccess = $scheme . '://styles/' . $this->style_name . '/' . $scheme . '/'. drupal_basename($original_uri_noaccess); + $this->assertFalse(file_exists($generated_uri_noaccess), 'Generated file does not exist.'); + $generate_url_noaccess = image_style_url($this->style_name, $original_uri_noaccess); + + $this->drupalGet($generate_url_noaccess); + $this->assertResponse(403, 'Confirmed that access is denied for the private image style.'); + // Verify that images are not appended to the response. Currently this test only uses PNG images. + if (strpos($generate_url, '.png') === FALSE ) { + $this->fail('Confirming that private image styles are not appended require PNG file.'); + } + else { + // Check for PNG-Signature (cf. http://www.libpng.org/pub/png/book/chapter08.html#png.ch08.div.2) in the + // response body. + $this->assertNoRaw( chr(137) . chr(80) . chr(78) . chr(71) . chr(13) . chr(10) . chr(26) . chr(10), 'No PNG signature found in the response body.'); + } } + 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 with an invalid token.'); + } + + // Allow insecure image derivatives to be created for the remainder of this + // test. + variable_set('image_allow_insecure_derivatives', TRUE); + + // Create another working copy of the file. + $files = $this->drupalGetTestFiles('image'); + $file = array_shift($files); + $image_info = image_get_info($file->uri); + $original_uri = file_unmanaged_copy($file->uri, $scheme . '://', FILE_EXISTS_RENAME); + // Let the image_module_test module know about this file, so it can claim + // ownership in hook_file_download(). + variable_set('image_module_test_file_download', $original_uri); + + // Get the URL of a file that has not been generated and try to create it. + $generated_uri = image_style_path($this->style_name, $original_uri); + $this->assertFalse(file_exists($generated_uri), 'Generated file does not exist.'); + $generate_url = image_style_url($this->style_name, $original_uri); + + // Check that the image is accessible even without the security token. + $this->drupalGet(str_replace(IMAGE_DERIVATIVE_TOKEN . '=', 'wrongparam=', $generate_url)); + $this->assertResponse(200, 'Image was accessible at the URL with a missing token.'); + + // Check that a security token is still required when generating a second + // image derivative using the first one as a source. + $nested_uri = image_style_path($this->style_name, $generated_uri); + $nested_url = image_style_url($this->style_name, $generated_uri); + $nested_url_with_wrong_token = str_replace(IMAGE_DERIVATIVE_TOKEN . '=', 'wrongparam=', $nested_url); + $this->drupalGet($nested_url_with_wrong_token); + $this->assertResponse(403, 'Image generated from an earlier derivative was inaccessible at the URL with a missing token.'); + // Check that this restriction cannot be bypassed by adding extra slashes + // to the URL. + $this->drupalGet(substr_replace($nested_url_with_wrong_token, '//styles/', strrpos($nested_url_with_wrong_token, '/styles/'), strlen('/styles/'))); + $this->assertResponse(403, 'Image generated from an earlier derivative was inaccessible at the URL with a missing token, even with an extra forward slash in the URL.'); + $this->drupalGet(substr_replace($nested_url_with_wrong_token, '/\styles/', strrpos($nested_url_with_wrong_token, '/styles/'), strlen('/styles/'))); + $this->assertResponse(403, 'Image generated from an earlier derivative was inaccessible at the URL with a missing token, even with an extra backslash in the URL.'); + // Make sure the image can still be generated if a correct token is used. + $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(); + $this->drupalGet(file_create_url($directory . '/' . $this->randomName())); + $this->assertFalse(file_exists($directory), 'New directory was not created in the filesystem when requesting an unauthorized image.'); } } @@ -211,7 +395,7 @@ } function setUp() { - parent::setUp('image_test'); + parent::setUp('image_module_test'); module_load_include('inc', 'image', 'image.effects'); } @@ -219,13 +403,13 @@ * Test the image_resize_effect() function. */ function testResizeEffect() { - $this->assertTrue(image_resize_effect($this->image, array('width' => 1, 'height' => 2)), t('Function returned the expected value.')); + $this->assertTrue(image_resize_effect($this->image, array('width' => 1, 'height' => 2)), 'Function returned the expected value.'); $this->assertToolkitOperationsCalled(array('resize')); // Check the parameters. $calls = image_test_get_all_calls(); - $this->assertEqual($calls['resize'][0][1], 1, t('Width was passed correctly')); - $this->assertEqual($calls['resize'][0][2], 2, t('Height was passed correctly')); + $this->assertEqual($calls['resize'][0][1], 1, 'Width was passed correctly'); + $this->assertEqual($calls['resize'][0][2], 2, 'Height was passed correctly'); } /** @@ -233,13 +417,13 @@ */ function testScaleEffect() { // @todo: need to test upscaling. - $this->assertTrue(image_scale_effect($this->image, array('width' => 10, 'height' => 10)), t('Function returned the expected value.')); + $this->assertTrue(image_scale_effect($this->image, array('width' => 10, 'height' => 10)), 'Function returned the expected value.'); $this->assertToolkitOperationsCalled(array('resize')); // Check the parameters. $calls = image_test_get_all_calls(); - $this->assertEqual($calls['resize'][0][1], 10, t('Width was passed correctly')); - $this->assertEqual($calls['resize'][0][2], 5, t('Height was based off aspect ratio and passed correctly')); + $this->assertEqual($calls['resize'][0][1], 10, 'Width was passed correctly'); + $this->assertEqual($calls['resize'][0][2], 5, 'Height was based off aspect ratio and passed correctly'); } /** @@ -247,42 +431,42 @@ */ function testCropEffect() { // @todo should test the keyword offsets. - $this->assertTrue(image_crop_effect($this->image, array('anchor' => 'top-1', 'width' => 3, 'height' => 4)), t('Function returned the expected value.')); + $this->assertTrue(image_crop_effect($this->image, array('anchor' => 'top-1', 'width' => 3, 'height' => 4)), 'Function returned the expected value.'); $this->assertToolkitOperationsCalled(array('crop')); // Check the parameters. $calls = image_test_get_all_calls(); - $this->assertEqual($calls['crop'][0][1], 0, t('X was passed correctly')); - $this->assertEqual($calls['crop'][0][2], 1, t('Y was passed correctly')); - $this->assertEqual($calls['crop'][0][3], 3, t('Width was passed correctly')); - $this->assertEqual($calls['crop'][0][4], 4, t('Height was passed correctly')); + $this->assertEqual($calls['crop'][0][1], 0, 'X was passed correctly'); + $this->assertEqual($calls['crop'][0][2], 1, 'Y was passed correctly'); + $this->assertEqual($calls['crop'][0][3], 3, 'Width was passed correctly'); + $this->assertEqual($calls['crop'][0][4], 4, 'Height was passed correctly'); } /** * Test the image_scale_and_crop_effect() function. */ function testScaleAndCropEffect() { - $this->assertTrue(image_scale_and_crop_effect($this->image, array('width' => 5, 'height' => 10)), t('Function returned the expected value.')); + $this->assertTrue(image_scale_and_crop_effect($this->image, array('width' => 5, 'height' => 10)), 'Function returned the expected value.'); $this->assertToolkitOperationsCalled(array('resize', 'crop')); // Check the parameters. $calls = image_test_get_all_calls(); - $this->assertEqual($calls['crop'][0][1], 7.5, t('X was computed and passed correctly')); - $this->assertEqual($calls['crop'][0][2], 0, t('Y was computed and passed correctly')); - $this->assertEqual($calls['crop'][0][3], 5, t('Width was computed and passed correctly')); - $this->assertEqual($calls['crop'][0][4], 10, t('Height was computed and passed correctly')); + $this->assertEqual($calls['crop'][0][1], 7.5, 'X was computed and passed correctly'); + $this->assertEqual($calls['crop'][0][2], 0, 'Y was computed and passed correctly'); + $this->assertEqual($calls['crop'][0][3], 5, 'Width was computed and passed correctly'); + $this->assertEqual($calls['crop'][0][4], 10, 'Height was computed and passed correctly'); } /** * Test the image_desaturate_effect() function. */ function testDesaturateEffect() { - $this->assertTrue(image_desaturate_effect($this->image, array()), t('Function returned the expected value.')); + $this->assertTrue(image_desaturate_effect($this->image, array()), 'Function returned the expected value.'); $this->assertToolkitOperationsCalled(array('desaturate')); // Check the parameters. $calls = image_test_get_all_calls(); - $this->assertEqual(count($calls['desaturate'][0]), 1, t('Only the image was passed.')); + $this->assertEqual(count($calls['desaturate'][0]), 1, 'Only the image was passed.'); } /** @@ -290,13 +474,84 @@ */ function testRotateEffect() { // @todo: need to test with 'random' => TRUE - $this->assertTrue(image_rotate_effect($this->image, array('degrees' => 90, 'bgcolor' => '#fff')), t('Function returned the expected value.')); + $this->assertTrue(image_rotate_effect($this->image, array('degrees' => 90, 'bgcolor' => '#fff')), 'Function returned the expected value.'); $this->assertToolkitOperationsCalled(array('rotate')); // Check the parameters. $calls = image_test_get_all_calls(); - $this->assertEqual($calls['rotate'][0][1], 90, t('Degrees were passed correctly')); - $this->assertEqual($calls['rotate'][0][2], 0xffffff, t('Background color was passed correctly')); + $this->assertEqual($calls['rotate'][0][1], 90, 'Degrees were passed correctly'); + $this->assertEqual($calls['rotate'][0][2], 0xffffff, 'Background color was passed correctly'); + } + + /** + * Test image effect caching. + */ + function testImageEffectsCaching() { + $image_effect_definitions_called = &drupal_static('image_module_test_image_effect_info_alter'); + + // First call should grab a fresh copy of the data. + $effects = image_effect_definitions(); + $this->assertTrue($image_effect_definitions_called === 1, 'image_effect_definitions() generated data.'); + + // Second call should come from cache. + drupal_static_reset('image_effect_definitions'); + drupal_static_reset('image_module_test_image_effect_info_alter'); + $cached_effects = image_effect_definitions(); + $this->assertTrue(is_null($image_effect_definitions_called), 'image_effect_definitions() returned data from cache.'); + + $this->assertTrue($effects == $cached_effects, 'Cached effects are the same as generated effects.'); + } +} + +/** + * 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.'); + } } } @@ -338,11 +593,29 @@ } /** + * Test creating an image style with a numeric name and ensuring it can be + * applied to an image. + */ + function testNumericStyleName() { + $style_name = rand(); + $style_label = $this->randomString(); + $edit = array( + 'name' => $style_name, + 'label' => $style_label, + ); + $this->drupalPost('admin/config/media/image-styles/add', $edit, t('Create new style')); + $this->assertRaw(t('Style %name was created.', array('%name' => $style_label)), 'Image style successfully created.'); + $options = image_style_options(); + $this->assertTrue(array_key_exists($style_name, $options), format_string('Array key %key exists.', array('%key' => $style_name))); + } + + /** * General test to add a style, add/remove/edit effects to it, then delete it. */ function testStyle() { // Setup a style to be created and effects to add to it. $style_name = strtolower($this->randomName(10)); + $style_label = $this->randomString(); $style_path = 'admin/config/media/image-styles/edit/' . $style_name; $effect_edits = array( 'image_resize' => array( @@ -377,9 +650,10 @@ $edit = array( 'name' => $style_name, + 'label' => $style_label, ); $this->drupalPost('admin/config/media/image-styles/add', $edit, t('Create new style')); - $this->assertRaw(t('Style %name was created.', array('%name' => $style_name)), t('Image style successfully created.')); + $this->assertRaw(t('Style %name was created.', array('%name' => $style_label)), 'Image style successfully created.'); // Add effect form. @@ -401,7 +675,7 @@ foreach ($style['effects'] as $ieid => $effect) { $this->drupalGet($style_path . '/effects/' . $ieid); foreach ($effect_edits[$effect['name']] as $field => $value) { - $this->assertFieldByName($field, $value, t('The %field field in the %effect effect has the correct value of %value.', array('%field' => $field, '%effect' => $effect['name'], '%value' => $value))); + $this->assertFieldByName($field, $value, format_string('The %field field in the %effect effect has the correct value of %value.', array('%field' => $field, '%effect' => $effect['name'], '%value' => $value))); } } @@ -417,14 +691,16 @@ $order_correct = FALSE; } } - $this->assertTrue($order_correct, t('The order of the effects is correctly set by default.')); + $this->assertTrue($order_correct, 'The order of the effects is correctly set by default.'); // Test the style overview form. // Change the name of the style and adjust the weights of effects. $style_name = strtolower($this->randomName(10)); + $style_label = $this->randomString(); $weight = count($effect_edits); $edit = array( 'name' => $style_name, + 'label' => $style_label, ); foreach ($style['effects'] as $ieid => $effect) { $edit['effects[' . $ieid . '][weight]'] = $weight; @@ -433,7 +709,7 @@ // Create an image to make sure it gets flushed after saving. $image_path = $this->createSampleImage($style); - $this->assertEqual($this->getImageCount($style), 1, t('Image style %style image %file successfully generated.', array('%style' => $style['name'], '%file' => $image_path))); + $this->assertEqual($this->getImageCount($style), 1, format_string('Image style %style image %file successfully generated.', array('%style' => $style['label'], '%file' => $image_path))); $this->drupalPost($style_path, $edit, t('Update style')); @@ -442,12 +718,12 @@ // Check that the URL was updated. $this->drupalGet($style_path); - $this->assertResponse(200, t('Image style %original renamed to %new', array('%original' => $style['name'], '%new' => $style_name))); + $this->assertResponse(200, format_string('Image style %original renamed to %new', array('%original' => $style['label'], '%new' => $style_label))); // Check that the image was flushed after updating the style. // This is especially important when renaming the style. Make sure that // the old image directory has been deleted. - $this->assertEqual($this->getImageCount($style), 0, t('Image style %style was flushed after renaming the style and updating the order of effects.', array('%style' => $style['name']))); + $this->assertEqual($this->getImageCount($style), 0, format_string('Image style %style was flushed after renaming the style and updating the order of effects.', array('%style' => $style['label']))); // Load the style by the new name with the new weights. drupal_static_reset('image_styles'); @@ -462,18 +738,18 @@ $order_correct = FALSE; } } - $this->assertTrue($order_correct, t('The order of the effects is correctly set by default.')); + $this->assertTrue($order_correct, 'The order of the effects is correctly set by default.'); // Image effect deletion form. // Create an image to make sure it gets flushed after deleting an effect. $image_path = $this->createSampleImage($style); - $this->assertEqual($this->getImageCount($style), 1, t('Image style %style image %file successfully generated.', array('%style' => $style['name'], '%file' => $image_path))); + $this->assertEqual($this->getImageCount($style), 1, format_string('Image style %style image %file successfully generated.', array('%style' => $style['label'], '%file' => $image_path))); // Test effect deletion form. $effect = array_pop($style['effects']); $this->drupalPost($style_path . '/effects/' . $effect['ieid'] . '/delete', array(), t('Delete')); - $this->assertRaw(t('The image effect %name has been deleted.', array('%name' => $effect['label'])), t('Image effect deleted.')); + $this->assertRaw(t('The image effect %name has been deleted.', array('%name' => $effect['label'])), 'Image effect deleted.'); // Style deletion form. @@ -482,10 +758,10 @@ // Confirm the style directory has been removed. $directory = file_default_scheme() . '://styles/' . $style_name; - $this->assertFalse(is_dir($directory), t('Image style %style directory removed on style deletion.', array('%style' => $style['name']))); + $this->assertFalse(is_dir($directory), format_string('Image style %style directory removed on style deletion.', array('%style' => $style['label']))); drupal_static_reset('image_styles'); - $this->assertFalse(image_style_load($style_name), t('Image style %style successfully deleted.', array('%style' => $style['name']))); + $this->assertFalse(image_style_load($style_name), format_string('Image style %style successfully deleted.', array('%style' => $style['label']))); } @@ -495,34 +771,36 @@ function testDefaultStyle() { // Setup a style to be created and effects to add to it. $style_name = 'thumbnail'; + $style_label = 'Thumbnail (100x100)'; $edit_path = 'admin/config/media/image-styles/edit/' . $style_name; $delete_path = 'admin/config/media/image-styles/delete/' . $style_name; $revert_path = 'admin/config/media/image-styles/revert/' . $style_name; // Ensure deleting a default is not possible. $this->drupalGet($delete_path); - $this->assertText(t('Page not found'), t('Default styles may not be deleted.')); + $this->assertText(t('Page not found'), 'Default styles may not be deleted.'); // Ensure that editing a default is not possible (without overriding). $this->drupalGet($edit_path); - $this->assertNoField('edit-name', t('Default styles may not be renamed.')); - $this->assertNoField('edit-submit', t('Default styles may not be edited.')); - $this->assertNoField('edit-add', t('Default styles may not have new effects added.')); + $disabled_field = $this->xpath('//input[@id=:id and @disabled="disabled"]', array(':id' => 'edit-name')); + $this->assertTrue($disabled_field, 'Default styles may not be renamed.'); + $this->assertNoField('edit-submit', 'Default styles may not be edited.'); + $this->assertNoField('edit-add', 'Default styles may not have new effects added.'); // Create an image to make sure the default works before overriding. drupal_static_reset('image_styles'); $style = image_style_load($style_name); $image_path = $this->createSampleImage($style); - $this->assertEqual($this->getImageCount($style), 1, t('Image style %style image %file successfully generated.', array('%style' => $style['name'], '%file' => $image_path))); + $this->assertEqual($this->getImageCount($style), 1, format_string('Image style %style image %file successfully generated.', array('%style' => $style['name'], '%file' => $image_path))); // Verify that effects attached to a default style do not have an ieid key. foreach ($style['effects'] as $effect) { - $this->assertFalse(isset($effect['ieid']), t('The %effect effect does not have an ieid.', array('%effect' => $effect['name']))); + $this->assertFalse(isset($effect['ieid']), format_string('The %effect effect does not have an ieid.', array('%effect' => $effect['name']))); } // Override the default. $this->drupalPost($edit_path, array(), t('Override defaults')); - $this->assertRaw(t('The %style style has been overridden, allowing you to change its settings.', array('%style' => $style_name)), t('Default image style may be overridden.')); + $this->assertRaw(t('The %style style has been overridden, allowing you to change its settings.', array('%style' => $style_label)), 'Default image style may be overridden.'); // Add sample effect to the overridden style. $this->drupalPost($edit_path, array('new' => 'image_desaturate'), t('Add')); @@ -531,22 +809,23 @@ // Verify that effects attached to the style have an ieid now. foreach ($style['effects'] as $effect) { - $this->assertTrue(isset($effect['ieid']), t('The %effect effect has an ieid.', array('%effect' => $effect['name']))); + $this->assertTrue(isset($effect['ieid']), format_string('The %effect effect has an ieid.', array('%effect' => $effect['name']))); } // The style should now have 2 effect, the original scale provided by core // and the desaturate effect we added in the override. $effects = array_values($style['effects']); - $this->assertEqual($effects[0]['name'], 'image_scale', t('The default effect still exists in the overridden style.')); - $this->assertEqual($effects[1]['name'], 'image_desaturate', t('The added effect exists in the overridden style.')); + $this->assertEqual($effects[0]['name'], 'image_scale', 'The default effect still exists in the overridden style.'); + $this->assertEqual($effects[1]['name'], 'image_desaturate', 'The added effect exists in the overridden style.'); - // Check that we are unable to rename an overridden style. + // Check that we are able to rename an overridden style. $this->drupalGet($edit_path); - $this->assertNoField('edit-name', t('Overridden styles may not be renamed.')); + $disabled_field = $this->xpath('//input[@id=:id and @disabled="disabled"]', array(':id' => 'edit-name')); + $this->assertFalse($disabled_field, 'Overridden styles may be renamed.'); // Create an image to ensure the override works properly. $image_path = $this->createSampleImage($style); - $this->assertEqual($this->getImageCount($style), 1, t('Image style %style image %file successfully generated.', array('%style' => $style['name'], '%file' => $image_path))); + $this->assertEqual($this->getImageCount($style), 1, format_string('Image style %style image %file successfully generated.', array('%style' => $style['label'], '%file' => $image_path))); // Revert the image style. $this->drupalPost($revert_path, array(), t('Revert')); @@ -555,8 +834,8 @@ // The style should now have the single effect for scale. $effects = array_values($style['effects']); - $this->assertEqual($effects[0]['name'], 'image_scale', t('The default effect still exists in the reverted style.')); - $this->assertFalse(array_key_exists(1, $effects), t('The added effect has been removed in the reverted style.')); + $this->assertEqual($effects[0]['name'], 'image_scale', 'The default effect still exists in the reverted style.'); + $this->assertFalse(array_key_exists(1, $effects), 'The added effect has been removed in the reverted style.'); } /** @@ -565,12 +844,14 @@ function testStyleReplacement() { // Create a new style. $style_name = strtolower($this->randomName(10)); - image_style_save(array('name' => $style_name)); + $style_label = $this->randomString(); + image_style_save(array('name' => $style_name, 'label' => $style_label)); $style_path = 'admin/config/media/image-styles/edit/' . $style_name; // Create an image field that uses the new style. $field_name = strtolower($this->randomName(10)); - $instance = $this->createImageField($field_name, 'article'); + $this->createImageField($field_name, 'article'); + $instance = field_info_instance('node', $field_name, 'article'); $instance['display']['default']['type'] = 'image'; $instance['display']['default']['settings']['image_style'] = $style_name; field_update_instance($instance); @@ -582,28 +863,30 @@ // Test that image is displayed using newly created style. $this->drupalGet('node/' . $nid); - $this->assertRaw(image_style_url($style_name, $node->{$field_name}[LANGUAGE_NONE][0]['uri']), t('Image displayed using style @style.', array('@style' => $style_name))); + $this->assertRaw(check_plain(image_style_url($style_name, $node->{$field_name}[LANGUAGE_NONE][0]['uri'])), format_string('Image displayed using style @style.', array('@style' => $style_name))); // Rename the style and make sure the image field is updated. $new_style_name = strtolower($this->randomName(10)); + $new_style_label = $this->randomString(); $edit = array( 'name' => $new_style_name, + 'label' => $new_style_label, ); $this->drupalPost('admin/config/media/image-styles/edit/' . $style_name, $edit, t('Update style')); - $this->assertText(t('Changes to the style have been saved.'), t('Style %name was renamed to %new_name.', array('%name' => $style_name, '%new_name' => $new_style_name))); + $this->assertText(t('Changes to the style have been saved.'), format_string('Style %name was renamed to %new_name.', array('%name' => $style_name, '%new_name' => $new_style_name))); $this->drupalGet('node/' . $nid); - $this->assertRaw(image_style_url($new_style_name, $node->{$field_name}[LANGUAGE_NONE][0]['uri']), t('Image displayed using style replacement style.')); + $this->assertRaw(check_plain(image_style_url($new_style_name, $node->{$field_name}[LANGUAGE_NONE][0]['uri'])), format_string('Image displayed using style replacement style.')); // Delete the style and choose a replacement style. $edit = array( 'replacement' => 'thumbnail', ); $this->drupalPost('admin/config/media/image-styles/delete/' . $new_style_name, $edit, t('Delete')); - $message = t('Style %name was deleted.', array('%name' => $new_style_name)); + $message = t('Style %name was deleted.', array('%name' => $new_style_label)); $this->assertRaw($message, $message); $this->drupalGet('node/' . $nid); - $this->assertRaw(image_style_url('thumbnail', $node->{$field_name}[LANGUAGE_NONE][0]['uri']), t('Image displayed using style replacement style.')); + $this->assertRaw(check_plain(image_style_url('thumbnail', $node->{$field_name}[LANGUAGE_NONE][0]['uri'])), format_string('Image displayed using style replacement style.')); } } @@ -650,9 +933,11 @@ $image_uri = $node->{$field_name}[LANGUAGE_NONE][0]['uri']; $image_info = array( 'path' => $image_uri, + 'width' => 40, + 'height' => 20, ); $default_output = theme('image', $image_info); - $this->assertRaw($default_output, t('Default formatter displaying correctly on full node view.')); + $this->assertRaw($default_output, 'Default formatter displaying correctly on full node view.'); // Test the image linked to file formatter. $instance = field_info_instance('node', $field_name, 'article'); @@ -661,20 +946,19 @@ field_update_instance($instance); $default_output = l(theme('image', $image_info), file_create_url($image_uri), array('html' => TRUE)); $this->drupalGet('node/' . $nid); - $this->assertRaw($default_output, t('Image linked to file formatter displaying correctly on full node view.')); + $this->assertRaw($default_output, 'Image linked to file formatter displaying correctly on full node view.'); // Verify that the image can be downloaded. - $this->assertEqual(file_get_contents($test_image->uri), $this->drupalGet(file_create_url($image_uri)), t('File was downloaded successfully.')); + $this->assertEqual(file_get_contents($test_image->uri), $this->drupalGet(file_create_url($image_uri)), 'File was downloaded successfully.'); if ($scheme == 'private') { // Only verify HTTP headers when using private scheme and the headers are // sent by Drupal. - $this->assertEqual($this->drupalGetHeader('Content-Type'), 'image/png; name="' . $test_image->filename . '"', t('Content-Type header was sent.')); - $this->assertEqual($this->drupalGetHeader('Content-Disposition'), 'inline; filename="' . $test_image->filename . '"', t('Content-Disposition header was sent.')); - $this->assertEqual($this->drupalGetHeader('Cache-Control'), 'private', t('Cache-Control header was sent.')); + $this->assertEqual($this->drupalGetHeader('Content-Type'), 'image/png', 'Content-Type header was sent.'); + $this->assertEqual($this->drupalGetHeader('Cache-Control'), 'private', 'Cache-Control header was sent.'); // Log out and try to access the file. $this->drupalLogout(); $this->drupalGet(file_create_url($image_uri)); - $this->assertResponse('403', t('Access denied to original image as anonymous user.')); + $this->assertResponse('403', 'Access denied to original image as anonymous user.'); // Log in again. $this->drupalLogin($this->admin_user); @@ -685,7 +969,7 @@ field_update_instance($instance); $default_output = l(theme('image', $image_info), 'node/' . $nid, array('html' => TRUE, 'attributes' => array('class' => 'active'))); $this->drupalGet('node/' . $nid); - $this->assertRaw($default_output, t('Image linked to content formatter displaying correctly on full node view.')); + $this->assertRaw($default_output, 'Image linked to content formatter displaying correctly on full node view.'); // Test the image style 'thumbnail' formatter. $instance['display']['default']['settings']['image_link'] = ''; @@ -694,17 +978,20 @@ // Ensure the derivative image is generated so we do not have to deal with // image style callback paths. $this->drupalGet(image_style_url('thumbnail', $image_uri)); - $image_info['path'] = image_style_path('thumbnail', $image_uri); - $image_info['getsize'] = FALSE; + // Need to create the URL again since it will change if clean URLs + // are disabled. + $image_info['path'] = image_style_url('thumbnail', $image_uri); + $image_info['width'] = 100; + $image_info['height'] = 50; $default_output = theme('image', $image_info); $this->drupalGet('node/' . $nid); - $this->assertRaw($default_output, t('Image style thumbnail formatter displaying correctly on full node view.')); + $this->assertRaw($default_output, 'Image style thumbnail formatter displaying correctly on full node view.'); if ($scheme == 'private') { // Log out and try to access the file. $this->drupalLogout(); $this->drupalGet(image_style_url('thumbnail', $image_uri)); - $this->assertResponse('403', t('Access denied to image style thumbnail as anonymous user.')); + $this->assertResponse('403', 'Access denied to image style thumbnail as anonymous user.'); } } @@ -726,34 +1013,41 @@ $widget_settings = array( 'preview_image_style' => 'medium', ); - $this->createImageField($field_name, 'article', array(), $instance_settings, $widget_settings); + $field = $this->createImageField($field_name, 'article', array(), $instance_settings, $widget_settings); + $field['deleted'] = 0; + $table = _field_sql_storage_tablename($field); + $schema = drupal_get_schema($table, TRUE); $instance = field_info_instance('node', $field_name, 'article'); $this->drupalGet('node/add/article'); - $this->assertText(t('Files must be less than 50 KB.'), t('Image widget max file size is displayed on article form.')); - $this->assertText(t('Allowed file types: ' . $test_image_extension . '.'), t('Image widget allowed file types displayed on article form.')); - $this->assertText(t('Images must be between 10x10 and 100x100 pixels.'), t('Image widget allowed resolution displayed on article form.')); + $this->assertText(t('Files must be less than 50 KB.'), 'Image widget max file size is displayed on article form.'); + $this->assertText(t('Allowed file types: ' . $test_image_extension . '.'), 'Image widget allowed file types displayed on article form.'); + $this->assertText(t('Images must be between 10x10 and 100x100 pixels.'), 'Image widget allowed resolution displayed on article form.'); // We have to create the article first and then edit it because the alt // and title fields do not display until the image has been attached. $nid = $this->uploadNodeImage($test_image, $field_name, 'article'); $this->drupalGet('node/' . $nid . '/edit'); - $this->assertFieldByName($field_name . '[' . LANGUAGE_NONE . '][0][alt]', '', t('Alt field displayed on article form.')); - $this->assertFieldByName($field_name . '[' . LANGUAGE_NONE . '][0][title]', '', t('Title field displayed on article form.')); + $this->assertFieldByName($field_name . '[' . LANGUAGE_NONE . '][0][alt]', '', 'Alt field displayed on article form.'); + $this->assertFieldByName($field_name . '[' . LANGUAGE_NONE . '][0][title]', '', 'Title field displayed on article form.'); // Verify that the attached image is being previewed using the 'medium' // style. $node = node_load($nid, NULL, TRUE); $image_info = array( 'path' => image_style_url('medium', $node->{$field_name}[LANGUAGE_NONE][0]['uri']), + 'width' => 220, + 'height' => 110, ); $default_output = theme('image', $image_info); - $this->assertRaw($default_output, t("Preview image is displayed using 'medium' style.")); + $this->assertRaw($default_output, "Preview image is displayed using 'medium' style."); // Add alt/title fields to the image and verify that they are displayed. $image_info = array( 'path' => $node->{$field_name}[LANGUAGE_NONE][0]['uri'], 'alt' => $this->randomName(), 'title' => $this->randomName(), + 'width' => 40, + 'height' => 20, ); $edit = array( $field_name . '[' . LANGUAGE_NONE . '][0][alt]' => $image_info['alt'], @@ -761,7 +1055,40 @@ ); $this->drupalPost('node/' . $nid . '/edit', $edit, t('Save')); $default_output = theme('image', $image_info); - $this->assertRaw($default_output, t('Image displayed using user supplied alt and title attributes.')); + $this->assertRaw($default_output, 'Image displayed using user supplied alt and title attributes.'); + + // Verify that alt/title longer than allowed results in a validation error. + $test_size = 2000; + $edit = array( + $field_name . '[' . LANGUAGE_NONE . '][0][alt]' => $this->randomName($test_size), + $field_name . '[' . LANGUAGE_NONE . '][0][title]' => $this->randomName($test_size), + ); + $this->drupalPost('node/' . $nid . '/edit', $edit, t('Save')); + $this->assertRaw(t('Alternate text cannot be longer than %max characters but is currently %length characters long.', array( + '%max' => $schema['fields'][$field_name .'_alt']['length'], + '%length' => $test_size, + ))); + $this->assertRaw(t('Title cannot be longer than %max characters but is currently %length characters long.', array( + '%max' => $schema['fields'][$field_name .'_title']['length'], + '%length' => $test_size, + ))); + } + + /** + * Test passing attributes into the image field formatters. + */ + function testImageFieldFormatterAttributes() { + $image = theme('image_formatter', array( + 'item' => array( + 'uri' => 'http://example.com/example.png', + 'attributes' => array( + 'data-image-field-formatter' => 'testFound', + ), + 'alt' => t('Image field formatter attribute test.'), + 'title' => t('Image field formatter'), + ), + )); + $this->assertTrue(stripos($image, 'testFound') > 0, 'Image field formatters can have attributes.'); } /** @@ -778,9 +1105,9 @@ $this->drupalGet('node/' . $node->nid); // Verify that no image is displayed on the page by checking for the class // that would be used on the image field. - $this->assertNoPattern('
      ', t('No image displayed when no image is attached and no default image specified.')); + $this->assertNoPattern('
      ', 'No image displayed when no image is attached and no default image specified.'); - // Add a default image to the imagefield instance. + // Add a default image to the public imagefield instance. $images = $this->drupalGetTestFiles('image'); $edit = array( 'files[field_settings_default_image]' => drupal_realpath($images[0]->uri), @@ -790,9 +1117,10 @@ field_info_cache_clear(); $field = field_info_field($field_name); $image = file_load($field['settings']['default_image']); + $this->assertTrue($image->status == FILE_STATUS_PERMANENT, 'The default image status is permanent.'); $default_output = theme('image', array('path' => $image->uri)); $this->drupalGet('node/' . $node->nid); - $this->assertRaw($default_output, t('Default image displayed when no user supplied image is present.')); + $this->assertRaw($default_output, 'Default image displayed when no user supplied image is present.'); // Create a node with an image attached and ensure that the default image // is not displayed. @@ -800,11 +1128,13 @@ $node = node_load($nid, NULL, TRUE); $image_info = array( 'path' => $node->{$field_name}[LANGUAGE_NONE][0]['uri'], + 'width' => 40, + 'height' => 20, ); $image_output = theme('image', $image_info); $this->drupalGet('node/' . $nid); - $this->assertNoRaw($default_output, t('Default image is not displayed when user supplied image is present.')); - $this->assertRaw($image_output, t('User supplied image is displayed.')); + $this->assertNoRaw($default_output, 'Default image is not displayed when user supplied image is present.'); + $this->assertRaw($image_output, 'User supplied image is displayed.'); // Remove default image from the field and make sure it is no longer used. $edit = array( @@ -814,7 +1144,26 @@ // Clear field info cache so the new default image is detected. field_info_cache_clear(); $field = field_info_field($field_name); - $this->assertFalse($field['settings']['default_image'], t('Default image removed from field.')); + $this->assertFalse($field['settings']['default_image'], 'Default image removed from field.'); + // Create an image field that uses the private:// scheme and test that the + // default image works as expected. + $private_field_name = strtolower($this->randomName()); + $this->createImageField($private_field_name, 'article', array('uri_scheme' => 'private')); + // Add a default image to the new field. + $edit = array( + 'files[field_settings_default_image]' => drupal_realpath($images[1]->uri), + ); + $this->drupalPost('admin/structure/types/manage/article/fields/' . $private_field_name, $edit, t('Save settings')); + $private_field = field_info_field($private_field_name); + $image = file_load($private_field['settings']['default_image']); + $this->assertEqual('private', file_uri_scheme($image->uri), 'Default image uses private:// scheme.'); + $this->assertTrue($image->status == FILE_STATUS_PERMANENT, 'The default image status is permanent.'); + // Create a new node with no image attached and ensure that default private + // image is displayed. + $node = $this->drupalCreateNode(array('type' => 'article')); + $default_output = theme('image', array('path' => $image->uri)); + $this->drupalGet('node/' . $node->nid); + $this->assertRaw($default_output, 'Default private image displayed when no user supplied image is present.'); } } @@ -860,8 +1209,762 @@ } } $nid = $this->uploadNodeImage($image_that_is_too_small, $field_name, 'article'); - $this->assertText(t('The specified file ' . $image_that_is_too_small->filename . ' could not be uploaded. The image is too small; the minimum dimensions are 50x50 pixels.'), t('Node save failed when minimum image resolution was not met.')); + $this->assertText(t('The specified file ' . $image_that_is_too_small->filename . ' could not be uploaded. The image is too small; the minimum dimensions are 50x50 pixels.'), 'Node save failed when minimum image resolution was not met.'); $nid = $this->uploadNodeImage($image_that_is_too_big, $field_name, 'article'); - $this->assertText(t('The image was resized to fit within the maximum allowed dimensions of 100x100 pixels.'), t('Image exceeding max resolution was properly resized.')); + $this->assertText(t('The image was resized to fit within the maximum allowed dimensions of 100x100 pixels.'), 'Image exceeding max resolution was properly resized.'); + } +} + +/** + * Tests that images have correct dimensions when styled. + */ +class ImageDimensionsTestCase extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Image dimensions', + 'description' => 'Tests that images have correct dimensions when styled.', + 'group' => 'Image', + ); + } + + function setUp() { + parent::setUp('image_module_test'); + } + + /** + * Test styled image dimensions cumulatively. + */ + function testImageDimensions() { + // Create a working copy of the file. + $files = $this->drupalGetTestFiles('image'); + $file = reset($files); + $original_uri = file_unmanaged_copy($file->uri, 'public://', FILE_EXISTS_RENAME); + + // Create a style. + $style = image_style_save(array('name' => 'test', 'label' => 'Test')); + $generated_uri = 'public://styles/test/public/'. drupal_basename($original_uri); + $url = image_style_url('test', $original_uri); + + $variables = array( + 'style_name' => 'test', + 'path' => $original_uri, + 'width' => 40, + 'height' => 20, + ); + + // Scale an image that is wider than it is high. + $effect = array( + 'name' => 'image_scale', + 'data' => array( + 'width' => 120, + 'height' => 90, + 'upscale' => TRUE, + ), + 'isid' => $style['isid'], + ); + + image_effect_save($effect); + $img_tag = theme_image_style($variables); + $this->assertEqual($img_tag, '', 'Expected img tag was found.'); + $this->assertFalse(file_exists($generated_uri), 'Generated file does not exist.'); + $this->drupalGet($url); + $this->assertResponse(200, 'Image was generated at the URL.'); + $this->assertTrue(file_exists($generated_uri), 'Generated file does exist after we accessed it.'); + $image_info = image_get_info($generated_uri); + $this->assertEqual($image_info['width'], 120, 'Expected width was found.'); + $this->assertEqual($image_info['height'], 60, 'Expected height was found.'); + + // Rotate 90 degrees anticlockwise. + $effect = array( + 'name' => 'image_rotate', + 'data' => array( + 'degrees' => -90, + 'random' => FALSE, + ), + 'isid' => $style['isid'], + ); + + image_effect_save($effect); + $img_tag = theme_image_style($variables); + $this->assertEqual($img_tag, '', 'Expected img tag was found.'); + $this->assertFalse(file_exists($generated_uri), 'Generated file does not exist.'); + $this->drupalGet($url); + $this->assertResponse(200, 'Image was generated at the URL.'); + $this->assertTrue(file_exists($generated_uri), 'Generated file does exist after we accessed it.'); + $image_info = image_get_info($generated_uri); + $this->assertEqual($image_info['width'], 60, 'Expected width was found.'); + $this->assertEqual($image_info['height'], 120, 'Expected height was found.'); + + // Scale an image that is higher than it is wide (rotated by previous effect). + $effect = array( + 'name' => 'image_scale', + 'data' => array( + 'width' => 120, + 'height' => 90, + 'upscale' => TRUE, + ), + 'isid' => $style['isid'], + ); + + image_effect_save($effect); + $img_tag = theme_image_style($variables); + $this->assertEqual($img_tag, '', 'Expected img tag was found.'); + $this->assertFalse(file_exists($generated_uri), 'Generated file does not exist.'); + $this->drupalGet($url); + $this->assertResponse(200, 'Image was generated at the URL.'); + $this->assertTrue(file_exists($generated_uri), 'Generated file does exist after we accessed it.'); + $image_info = image_get_info($generated_uri); + $this->assertEqual($image_info['width'], 45, 'Expected width was found.'); + $this->assertEqual($image_info['height'], 90, 'Expected height was found.'); + + // Test upscale disabled. + $effect = array( + 'name' => 'image_scale', + 'data' => array( + 'width' => 400, + 'height' => 200, + 'upscale' => FALSE, + ), + 'isid' => $style['isid'], + ); + + image_effect_save($effect); + $img_tag = theme_image_style($variables); + $this->assertEqual($img_tag, '', 'Expected img tag was found.'); + $this->assertFalse(file_exists($generated_uri), 'Generated file does not exist.'); + $this->drupalGet($url); + $this->assertResponse(200, 'Image was generated at the URL.'); + $this->assertTrue(file_exists($generated_uri), 'Generated file does exist after we accessed it.'); + $image_info = image_get_info($generated_uri); + $this->assertEqual($image_info['width'], 45, 'Expected width was found.'); + $this->assertEqual($image_info['height'], 90, 'Expected height was found.'); + + // Add a desaturate effect. + $effect = array( + 'name' => 'image_desaturate', + 'data' => array(), + 'isid' => $style['isid'], + ); + + image_effect_save($effect); + $img_tag = theme_image_style($variables); + $this->assertEqual($img_tag, '', 'Expected img tag was found.'); + $this->assertFalse(file_exists($generated_uri), 'Generated file does not exist.'); + $this->drupalGet($url); + $this->assertResponse(200, 'Image was generated at the URL.'); + $this->assertTrue(file_exists($generated_uri), 'Generated file does exist after we accessed it.'); + $image_info = image_get_info($generated_uri); + $this->assertEqual($image_info['width'], 45, 'Expected width was found.'); + $this->assertEqual($image_info['height'], 90, 'Expected height was found.'); + + // Add a random rotate effect. + $effect = array( + 'name' => 'image_rotate', + 'data' => array( + 'degrees' => 180, + 'random' => TRUE, + ), + 'isid' => $style['isid'], + ); + + image_effect_save($effect); + $img_tag = theme_image_style($variables); + $this->assertEqual($img_tag, '', 'Expected img tag was found.'); + $this->assertFalse(file_exists($generated_uri), 'Generated file does not exist.'); + $this->drupalGet($url); + $this->assertResponse(200, 'Image was generated at the URL.'); + $this->assertTrue(file_exists($generated_uri), 'Generated file does exist after we accessed it.'); + + + // Add a crop effect. + $effect = array( + 'name' => 'image_crop', + 'data' => array( + 'width' => 30, + 'height' => 30, + 'anchor' => 'center-center', + ), + 'isid' => $style['isid'], + ); + + image_effect_save($effect); + $img_tag = theme_image_style($variables); + $this->assertEqual($img_tag, '', 'Expected img tag was found.'); + $this->assertFalse(file_exists($generated_uri), 'Generated file does not exist.'); + $this->drupalGet($url); + $this->assertResponse(200, 'Image was generated at the URL.'); + $this->assertTrue(file_exists($generated_uri), 'Generated file does exist after we accessed it.'); + $image_info = image_get_info($generated_uri); + $this->assertEqual($image_info['width'], 30, 'Expected width was found.'); + $this->assertEqual($image_info['height'], 30, 'Expected height was found.'); + + // Rotate to a non-multiple of 90 degrees. + $effect = array( + 'name' => 'image_rotate', + 'data' => array( + 'degrees' => 57, + 'random' => FALSE, + ), + 'isid' => $style['isid'], + ); + + $effect = image_effect_save($effect); + $img_tag = theme_image_style($variables); + $this->assertEqual($img_tag, '', 'Expected img tag was found.'); + $this->assertFalse(file_exists($generated_uri), 'Generated file does not exist.'); + $this->drupalGet($url); + $this->assertResponse(200, 'Image was generated at the URL.'); + $this->assertTrue(file_exists($generated_uri), 'Generated file does exist after we accessed it.'); + + image_effect_delete($effect); + + // Ensure that an effect with no dimensions callback unsets the dimensions. + // This ensures compatibility with 7.0 contrib modules. + $effect = array( + 'name' => 'image_module_test_null', + 'data' => array(), + 'isid' => $style['isid'], + ); + + image_effect_save($effect); + $img_tag = theme_image_style($variables); + $this->assertEqual($img_tag, '', 'Expected img tag was found.'); + } +} + +/** + * Tests image_dimensions_scale(). + */ +class ImageDimensionsScaleTestCase extends DrupalUnitTestCase { + public static function getInfo() { + return array( + 'name' => 'image_dimensions_scale()', + 'description' => 'Tests all control flow branches in image_dimensions_scale().', + 'group' => 'Image', + ); + } + + /** + * Tests all control flow branches in image_dimensions_scale(). + */ + function testImageDimensionsScale() { + // Define input / output datasets to test different branch conditions. + $test = array(); + + // Test branch conditions: + // - No height. + // - Upscale, don't need to upscale. + $tests[] = array( + 'input' => array( + 'dimensions' => array( + 'width' => 1000, + 'height' => 2000, + ), + 'width' => 200, + 'height' => NULL, + 'upscale' => TRUE, + ), + 'output' => array( + 'dimensions' => array( + 'width' => 200, + 'height' => 400, + ), + 'return_value' => TRUE, + ), + ); + + // Test branch conditions: + // - No width. + // - Don't upscale, don't need to upscale. + $tests[] = array( + 'input' => array( + 'dimensions' => array( + 'width' => 1000, + 'height' => 800, + ), + 'width' => NULL, + 'height' => 140, + 'upscale' => FALSE, + ), + 'output' => array( + 'dimensions' => array( + 'width' => 175, + 'height' => 140, + ), + 'return_value' => TRUE, + ), + ); + + // Test branch conditions: + // - Source aspect ratio greater than target. + // - Upscale, need to upscale. + $tests[] = array( + 'input' => array( + 'dimensions' => array( + 'width' => 8, + 'height' => 20, + ), + 'width' => 200, + 'height' => 140, + 'upscale' => TRUE, + ), + 'output' => array( + 'dimensions' => array( + 'width' => 56, + 'height' => 140, + ), + 'return_value' => TRUE, + ), + ); + + // Test branch condition: target aspect ratio greater than source. + $tests[] = array( + 'input' => array( + 'dimensions' => array( + 'width' => 2000, + 'height' => 800, + ), + 'width' => 200, + 'height' => 140, + 'upscale' => FALSE, + ), + 'output' => array( + 'dimensions' => array( + 'width' => 200, + 'height' => 80, + ), + 'return_value' => TRUE, + ), + ); + + // Test branch condition: don't upscale, need to upscale. + $tests[] = array( + 'input' => array( + 'dimensions' => array( + 'width' => 100, + 'height' => 50, + ), + 'width' => 200, + 'height' => 140, + 'upscale' => FALSE, + ), + 'output' => array( + 'dimensions' => array( + 'width' => 100, + 'height' => 50, + ), + 'return_value' => FALSE, + ), + ); + + foreach ($tests as $test) { + // Process the test dataset. + $return_value = image_dimensions_scale($test['input']['dimensions'], $test['input']['width'], $test['input']['height'], $test['input']['upscale']); + + // Check the width. + $this->assertEqual($test['output']['dimensions']['width'], $test['input']['dimensions']['width'], format_string('Computed width (@computed_width) equals expected width (@expected_width)', array('@computed_width' => $test['output']['dimensions']['width'], '@expected_width' => $test['input']['dimensions']['width']))); + + // Check the height. + $this->assertEqual($test['output']['dimensions']['height'], $test['input']['dimensions']['height'], format_string('Computed height (@computed_height) equals expected height (@expected_height)', array('@computed_height' => $test['output']['dimensions']['height'], '@expected_height' => $test['input']['dimensions']['height']))); + + // Check the return value. + $this->assertEqual($test['output']['return_value'], $return_value, 'Correct return value.'); + } + } +} + +/** + * Tests default image settings. + */ +class ImageFieldDefaultImagesTestCase extends ImageFieldTestCase { + + public static function getInfo() { + return array( + 'name' => 'Image field default images tests', + 'description' => 'Tests setting up default images both to the field and field instance.', + 'group' => 'Image', + ); + } + + function setUp() { + parent::setUp(array('field_ui')); + } + + /** + * Tests CRUD for fields and fields instances with default images. + */ + function testDefaultImages() { + // Create files to use as the default images. + $files = $this->drupalGetTestFiles('image'); + $default_images = array(); + foreach (array('field', 'instance', 'instance2', 'field_new', 'instance_new') as $image_target) { + $file = array_pop($files); + $file = file_save($file); + $default_images[$image_target] = $file; + } + + // Create an image field and add an instance to the article content type. + $field_name = strtolower($this->randomName()); + $field_settings = array( + 'default_image' => $default_images['field']->fid, + ); + $instance_settings = array( + 'default_image' => $default_images['instance']->fid, + ); + $widget_settings = array( + 'preview_image_style' => 'medium', + ); + $this->createImageField($field_name, 'article', $field_settings, $instance_settings, $widget_settings); + $field = field_info_field($field_name); + $instance = field_info_instance('node', $field_name, 'article'); + + // Add another instance with another default image to the page content type. + $instance2 = array_merge($instance, array( + 'bundle' => 'page', + 'settings' => array( + 'default_image' => $default_images['instance2']->fid, + ), + )); + field_create_instance($instance2); + $instance2 = field_info_instance('node', $field_name, 'page'); + + + // Confirm the defaults are present on the article field admin form. + $this->drupalGet("admin/structure/types/manage/article/fields/$field_name"); + $this->assertFieldByXpath( + '//input[@name="field[settings][default_image][fid]"]', + $default_images['field']->fid, + format_string( + 'Article image field default equals expected file ID of @fid.', + array('@fid' => $default_images['field']->fid) + ) + ); + $this->assertFieldByXpath( + '//input[@name="instance[settings][default_image][fid]"]', + $default_images['instance']->fid, + format_string( + 'Article image field instance default equals expected file ID of @fid.', + array('@fid' => $default_images['instance']->fid) + ) + ); + + // Confirm the defaults are present on the page field admin form. + $this->drupalGet("admin/structure/types/manage/page/fields/$field_name"); + $this->assertFieldByXpath( + '//input[@name="field[settings][default_image][fid]"]', + $default_images['field']->fid, + format_string( + 'Page image field default equals expected file ID of @fid.', + array('@fid' => $default_images['field']->fid) + ) + ); + $this->assertFieldByXpath( + '//input[@name="instance[settings][default_image][fid]"]', + $default_images['instance2']->fid, + format_string( + 'Page image field instance default equals expected file ID of @fid.', + array('@fid' => $default_images['instance2']->fid) + ) + ); + + // Confirm that the image default is shown for a new article node. + $article = $this->drupalCreateNode(array('type' => 'article')); + $article_built = node_view($article); + $this->assertEqual( + $article_built[$field_name]['#items'][0]['fid'], + $default_images['instance']->fid, + format_string( + 'A new article node without an image has the expected default image file ID of @fid.', + array('@fid' => $default_images['instance']->fid) + ) + ); + + // Confirm that the image default is shown for a new page node. + $page = $this->drupalCreateNode(array('type' => 'page')); + $page_built = node_view($page); + $this->assertEqual( + $page_built[$field_name]['#items'][0]['fid'], + $default_images['instance2']->fid, + format_string( + 'A new page node without an image has the expected default image file ID of @fid.', + array('@fid' => $default_images['instance2']->fid) + ) + ); + + // Upload a new default for the field. + $field['settings']['default_image'] = $default_images['field_new']->fid; + field_update_field($field); + + // Confirm that the new field default is used on the article admin form. + $this->drupalGet("admin/structure/types/manage/article/fields/$field_name"); + $this->assertFieldByXpath( + '//input[@name="field[settings][default_image][fid]"]', + $default_images['field_new']->fid, + format_string( + 'Updated image field default equals expected file ID of @fid.', + array('@fid' => $default_images['field_new']->fid) + ) + ); + + // Reload the nodes and confirm the field instance defaults are used. + $article_built = node_view($article = node_load($article->nid, NULL, $reset = TRUE)); + $page_built = node_view($page = node_load($page->nid, NULL, $reset = TRUE)); + $this->assertEqual( + $article_built[$field_name]['#items'][0]['fid'], + $default_images['instance']->fid, + format_string( + 'An existing article node without an image has the expected default image file ID of @fid.', + array('@fid' => $default_images['instance']->fid) + ) + ); + $this->assertEqual( + $page_built[$field_name]['#items'][0]['fid'], + $default_images['instance2']->fid, + format_string( + 'An existing page node without an image has the expected default image file ID of @fid.', + array('@fid' => $default_images['instance2']->fid) + ) + ); + + // Upload a new default for the article's field instance. + $instance['settings']['default_image'] = $default_images['instance_new']->fid; + field_update_instance($instance); + + // Confirm the new field instance default is used on the article field + // admin form. + $this->drupalGet("admin/structure/types/manage/article/fields/$field_name"); + $this->assertFieldByXpath( + '//input[@name="instance[settings][default_image][fid]"]', + $default_images['instance_new']->fid, + format_string( + 'Updated article image field instance default equals expected file ID of @fid.', + array('@fid' => $default_images['instance_new']->fid) + ) + ); + + // Reload the nodes. + $article_built = node_view($article = node_load($article->nid, NULL, $reset = TRUE)); + $page_built = node_view($page = node_load($page->nid, NULL, $reset = TRUE)); + + // Confirm the article uses the new default. + $this->assertEqual( + $article_built[$field_name]['#items'][0]['fid'], + $default_images['instance_new']->fid, + format_string( + 'An existing article node without an image has the expected default image file ID of @fid.', + array('@fid' => $default_images['instance_new']->fid) + ) + ); + // Confirm the page remains unchanged. + $this->assertEqual( + $page_built[$field_name]['#items'][0]['fid'], + $default_images['instance2']->fid, + format_string( + 'An existing page node without an image has the expected default image file ID of @fid.', + array('@fid' => $default_images['instance2']->fid) + ) + ); + + // Remove the instance default from articles. + $instance['settings']['default_image'] = NULL; + field_update_instance($instance); + + // Confirm the article field instance default has been removed. + $this->drupalGet("admin/structure/types/manage/article/fields/$field_name"); + $this->assertFieldByXpath( + '//input[@name="instance[settings][default_image][fid]"]', + '', + 'Updated article image field instance default has been successfully removed.' + ); + + // Reload the nodes. + $article_built = node_view($article = node_load($article->nid, NULL, $reset = TRUE)); + $page_built = node_view($page = node_load($page->nid, NULL, $reset = TRUE)); + // Confirm the article uses the new field (not instance) default. + $this->assertEqual( + $article_built[$field_name]['#items'][0]['fid'], + $default_images['field_new']->fid, + format_string( + 'An existing article node without an image has the expected default image file ID of @fid.', + array('@fid' => $default_images['field_new']->fid) + ) + ); + // Confirm the page remains unchanged. + $this->assertEqual( + $page_built[$field_name]['#items'][0]['fid'], + $default_images['instance2']->fid, + format_string( + 'An existing page node without an image has the expected default image file ID of @fid.', + array('@fid' => $default_images['instance2']->fid) + ) + ); + } + +} + +/** + * Tests image theme functions. + */ +class ImageThemeFunctionWebTestCase extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Image theme functions', + 'description' => 'Test that the image theme functions work correctly.', + 'group' => 'Image', + ); + } + + function setUp() { + parent::setUp(array('image')); + } + + /** + * Tests usage of the image field formatters. + */ + function testImageFormatterTheme() { + // Create an image. + $files = $this->drupalGetTestFiles('image'); + $file = reset($files); + $original_uri = file_unmanaged_copy($file->uri, 'public://', FILE_EXISTS_RENAME); + + // Create a style. + image_style_save(array('name' => 'test', 'label' => 'Test')); + $url = image_style_url('test', $original_uri); + + // Test using theme_image_formatter() without an image title, alt text, or + // link options. + $path = $this->randomName(); + $element = array( + '#theme' => 'image_formatter', + '#image_style' => 'test', + '#item' => array( + 'uri' => $original_uri, + ), + '#path' => array( + 'path' => $path, + ), + ); + $rendered_element = render($element); + $expected_result = ''; + $this->assertEqual($expected_result, $rendered_element, 'theme_image_formatter() correctly renders without title, alt, or path options.'); + + // Link the image to a fragment on the page, and not a full URL. + $fragment = $this->randomName(); + $element['#path']['path'] = ''; + $element['#path']['options'] = array( + 'external' => TRUE, + 'fragment' => $fragment, + ); + $rendered_element = render($element); + $expected_result = ''; + $this->assertEqual($expected_result, $rendered_element, 'theme_image_formatter() correctly renders a link fragment.'); + } + +} + +/** + * Tests flushing of image styles. + */ +class ImageStyleFlushTest extends ImageFieldTestCase { + + public static function getInfo() { + return array( + 'name' => 'Image style flushing', + 'description' => 'Tests flushing of image styles.', + 'group' => 'Image', + ); + } + + /** + * Given an image style and a wrapper, generate an image. + */ + function createSampleImage($style, $wrapper) { + static $file; + + if (!isset($file)) { + $files = $this->drupalGetTestFiles('image'); + $file = reset($files); + } + + // Make sure we have an image in our wrapper testing file directory. + $source_uri = file_unmanaged_copy($file->uri, $wrapper . '://'); + // Build the derivative image. + $derivative_uri = image_style_path($style['name'], $source_uri); + $derivative = image_style_create_derivative($style, $source_uri, $derivative_uri); + + return $derivative ? $derivative_uri : FALSE; + } + + /** + * Count the number of images currently created for a style in a wrapper. + */ + function getImageCount($style, $wrapper) { + return count(file_scan_directory($wrapper . '://styles/' . $style['name'], '/.*/')); + } + + /** + * General test to flush a style. + */ + function testFlush() { + + // Setup a style to be created and effects to add to it. + $style_name = strtolower($this->randomName(10)); + $style_label = $this->randomString(); + $style_path = 'admin/config/media/image-styles/edit/' . $style_name; + $effect_edits = array( + 'image_resize' => array( + 'data[width]' => 100, + 'data[height]' => 101, + ), + 'image_scale' => array( + 'data[width]' => 110, + 'data[height]' => 111, + 'data[upscale]' => 1, + ), + ); + + // Add style form. + $edit = array( + 'name' => $style_name, + 'label' => $style_label, + ); + $this->drupalPost('admin/config/media/image-styles/add', $edit, t('Create new style')); + // Add each sample effect to the style. + foreach ($effect_edits as $effect => $edit) { + // Add the effect. + $this->drupalPost($style_path, array('new' => $effect), t('Add')); + if (!empty($edit)) { + $this->drupalPost(NULL, $edit, t('Add effect')); + } + } + + // Load the saved image style. + $style = image_style_load($style_name); + + // Create an image for the 'public' wrapper. + $image_path = $this->createSampleImage($style, 'public'); + // Expecting to find 2 images, one is the sample.png image shown in + // image style preview. + $this->assertEqual($this->getImageCount($style, 'public'), 2, format_string('Image style %style image %file successfully generated.', array('%style' => $style['name'], '%file' => $image_path))); + + // Create an image for the 'private' wrapper. + $image_path = $this->createSampleImage($style, 'private'); + $this->assertEqual($this->getImageCount($style, 'private'), 1, format_string('Image style %style image %file successfully generated.', array('%style' => $style['name'], '%file' => $image_path))); + + // Remove the 'image_scale' effect and updates the style, which in turn + // forces an image style flush. + $effect = array_pop($style['effects']); + $this->drupalPost($style_path . '/effects/' . $effect['ieid'] . '/delete', array(), t('Delete')); + $this->assertResponse(200); + $this->drupalPost($style_path, array(), t('Update style')); + $this->assertResponse(200); + + // Post flush, expected 1 image in the 'public' wrapper (sample.png). + $this->assertEqual($this->getImageCount($style, 'public'), 1, format_string('Image style %style flushed correctly for %wrapper wrapper.', array('%style' => $style['name'], '%wrapper' => 'public'))); + + // Post flush, expected no image in the 'private' wrapper. + $this->assertEqual($this->getImageCount($style, 'private'), 0, format_string('Image style %style flushed correctly for %wrapper wrapper.', array('%style' => $style['name'], '%wrapper' => 'private'))); } } diff -Naur drupal-7.0/modules/image/tests/image_module_test.info drupal-7.66/modules/image/tests/image_module_test.info --- drupal-7.0/modules/image/tests/image_module_test.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/image/tests/image_module_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: image_module_test.info,v 1.1 2010/08/23 09:04:57 dries Exp $ name = Image test description = Provides hook implementations for testing Image module functionality. package = Core @@ -7,8 +6,7 @@ files[] = image_module_test.module hidden = TRUE -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/image/tests/image_module_test.module drupal-7.66/modules/image/tests/image_module_test.module --- drupal-7.0/modules/image/tests/image_module_test.module 2010-08-23 11:04:57.000000000 +0200 +++ drupal-7.66/modules/image/tests/image_module_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ 'image_module_test'); } - return -1; + if (variable_get('image_module_test_invalid_headers', FALSE) == $uri) { + return array('Content-Type' => 'image/png'); + } +} + +/** + * Implements hook_image_effect_info(). + */ +function image_module_test_image_effect_info() { + $effects = array( + 'image_module_test_null' => array( + 'effect callback' => 'image_module_test_null_effect', + ), + ); + + return $effects; +} + +/** + * Image effect callback; Null. + * + * @param $image + * An image object returned by image_load(). + * @param $data + * An array with no attributes. + * + * @return + * TRUE + */ +function image_module_test_null_effect(array &$image, array $data) { + return TRUE; +} + +/** + * Implements hook_image_effect_info_alter(). + * + * Used to keep a count of cache misses in image_effect_definitions(). + */ +function image_module_test_image_effect_info_alter(&$effects) { + $image_effects_definition_called = &drupal_static(__FUNCTION__, 0); + $image_effects_definition_called++; } diff -Naur drupal-7.0/modules/locale/locale-rtl.css drupal-7.66/modules/locale/locale-rtl.css --- drupal-7.0/modules/locale/locale-rtl.css 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/locale/locale-rtl.css 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,12 @@ + +#locale-translation-filter-form .form-item-language, +#locale-translation-filter-form .form-item-translation, +#locale-translation-filter-form .form-item-group { + float: right; + padding-left: .8em; + padding-right: 0; +} +#locale-translation-filter-form .form-actions { + float: right; + padding: 3ex 1em 0 0; +} diff -Naur drupal-7.0/modules/locale/locale.admin.inc drupal-7.66/modules/locale/locale.admin.inc --- drupal-7.0/modules/locale/locale.admin.inc 2011-01-03 19:03:54.000000000 +0100 +++ drupal-7.66/modules/locale/locale.admin.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ language) { $form['enabled'][$key]['#attributes']['disabled'] = 'disabled'; } + + // Add invisible labels for the checkboxes and radio buttons in the table + // for accessibility. These changes are only required and valid when the + // form is themed as a table, so it would be wrong to perform them in the + // form constructor. + $title = drupal_render($form['name'][$key]); + $form['enabled'][$key]['#title'] = t('Enable !title', array('!title' => $title)); + $form['enabled'][$key]['#title_display'] = 'invisible'; + $form['site_default'][$key]['#title'] = t('Set !title as default', array('!title' => $title)); + $form['site_default'][$key]['#title_display'] = 'invisible'; $rows[] = array( 'data' => array( - '' . drupal_render($form['name'][$key]) . '', + '' . $title . '', drupal_render($form['native'][$key]), check_plain($key), drupal_render($form['direction'][$key]), @@ -266,8 +275,7 @@ else { $form['langcode'] = array('#type' => 'textfield', '#title' => t('Language code'), - '#size' => 12, - '#maxlength' => 60, + '#maxlength' => 12, '#required' => TRUE, '#default_value' => @$language->language, '#disabled' => (isset($language->language)), @@ -298,7 +306,7 @@ '#title' => t('Language domain'), '#maxlength' => 128, '#default_value' => @$language->domain, - '#description' => t('URL including protocol to use for this language, if your Detection and selection settings use URL domains. For the default language, this value may be left blank. Modifying this value may break existing URLs. Use with caution in a production environment. Example: Specifying "http://example.de" or "http://de.example.com" as language domains for German results in URLs like "http://example.de/contact" and "http://de.example.com/contact", respectively.'), + '#description' => t('The domain name to use for this language if URL domains are used for Detection and selection. Leave blank for the default language. Changing this value may break existing URLs. Example: Specifying "de.example.com" as language domain for German will result in an URL like "http://de.example.com/contact".'), ); $form['direction'] = array('#type' => 'radios', '#title' => t('Direction'), @@ -380,13 +388,13 @@ form_set_error('prefix', t('Domain and path prefix values should not be set at the same time.')); } if (!empty($form_state['values']['domain']) && $duplicate = db_query("SELECT language FROM {languages} WHERE domain = :domain AND language <> :language", array(':domain' => $form_state['values']['domain'], ':language' => $form_state['values']['langcode']))->fetchField()) { - form_set_error('domain', t('The domain (%domain) is already tied to a language (%language).', array('%domain' => $form_state['values']['domain'], '%language' => $duplicate->language))); + form_set_error('domain', t('The domain (%domain) is already tied to a language (%language).', array('%domain' => $form_state['values']['domain'], '%language' => $duplicate))); } if (empty($form_state['values']['prefix']) && language_default('language') != $form_state['values']['langcode'] && empty($form_state['values']['domain'])) { form_set_error('prefix', t('Only the default language can have both the domain and prefix empty.')); } if (!empty($form_state['values']['prefix']) && $duplicate = db_query("SELECT language FROM {languages} WHERE prefix = :prefix AND language <> :language", array(':prefix' => $form_state['values']['prefix'], ':language' => $form_state['values']['langcode']))->fetchField()) { - form_set_error('prefix', t('The prefix (%prefix) is already tied to a language (%language).', array('%prefix' => $form_state['values']['prefix'], '%language' => $duplicate->language))); + form_set_error('prefix', t('The prefix (%prefix) is already tied to a language (%language).', array('%prefix' => $form_state['values']['prefix'], '%language' => $duplicate))); } } @@ -468,6 +476,9 @@ ->fields(array('language' => '')) ->condition('language', $form_state['values']['langcode']) ->execute(); + if ($languages[$form_state['values']['langcode']]->enabled) { + variable_set('language_count', variable_get('language_count', 1) - 1); + } module_invoke_all('multilingual_settings_changed'); $variables = array('%locale' => $languages[$form_state['values']['langcode']]->name); drupal_set_message(t('The language %locale has been removed.', $variables)); @@ -539,6 +550,12 @@ asort($providers_weight); foreach ($providers_weight as $id => $weight) { + // A language provider might be no more available if the defining module has + // been disabled after the last configuration saving. + if (!isset($language_providers[$id])) { + continue; + } + $enabled = isset($enabled_providers[$id]); $provider = $language_providers[$id]; @@ -656,7 +673,6 @@ * Submit handler for language negotiation settings. */ function locale_languages_configure_form_submit($form, &$form_state) { - $language_types = array(); $configurable_types = $form['#language_types']; foreach ($configurable_types as $type) { @@ -664,7 +680,6 @@ $enabled_providers = $form_state['values'][$type]['enabled']; $enabled_providers[LANGUAGE_NEGOTIATION_DEFAULT] = TRUE; $providers_weight = $form_state['values'][$type]['weight']; - $language_types[$type] = TRUE; foreach ($providers_weight as $id => $weight) { if ($enabled_providers[$id]) { @@ -678,27 +693,11 @@ variable_set("locale_language_providers_weight_$type", $providers_weight); } - // Save non-configurable language types negotiation. - $language_types_info = language_types_info(); - $defined_providers = $form['#language_providers']; - foreach ($language_types_info as $type => $info) { - if (isset($info['fixed'])) { - $language_types[$type] = FALSE; - $negotiation = array(); - foreach ($info['fixed'] as $weight => $id) { - if (isset($defined_providers[$id])) { - $negotiation[$id] = $defined_providers[$id]; - $negotiation[$id]['weight'] = $weight; - } - } - language_negotiation_set($type, $negotiation); - } - } - - // Save language types. - variable_set('language_types', $language_types); + // Update non-configurable language types and the related language negotiation + // configuration. + language_types_set(); - $form_state['redirect'] = 'admin/config/regional/language'; + $form_state['redirect'] = 'admin/config/regional/language/configure'; drupal_set_message(t('Language negotiation configuration saved.')); } @@ -1140,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); @@ -1195,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, @@ -1243,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.0/modules/locale/locale.api.php drupal-7.66/modules/locale/locale.api.php --- drupal-7.0/modules/locale/locale.api.php 2010-07-16 04:37:06.000000000 +0200 +++ drupal-7.66/modules/locale/locale.api.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ language) { - case 'it': - $conf['site_name'] = 'Il mio sito Drupal'; - break; - - case 'fr': - $conf['site_name'] = 'Mon site Drupal'; - break; - } -} - -/** - * Perform alterations on language switcher links. - * - * A language switcher link may need to point to a different path or use a - * translated link text before going through l(), which will just handle the - * path aliases. - * - * @param $links - * Nested array of links keyed by language code. - * @param $type - * The language type the links will switch. - * @param $path - * The current path. - */ -function hook_language_switch_links_alter(array &$links, $type, $path) { - global $language; - - if ($type == LANGUAGE_TYPE_CONTENT && isset($links[$language->language])) { - foreach ($links[$language->language] as $link) { - $link['attributes']['class'][] = 'active-language'; - } - } -} - -/** - * Allow modules to define their own language types. - * - * @return - * An array of language type definitions. Each language type has an identifier - * key. The language type definition is an associative array that may contain - * the following key-value pairs: - * - "name": The human-readable language type identifier. - * - "description": A description of the language type. - * - "fixed": An array of language provider identifiers. Defining this key - * makes the language type non-configurable. - */ -function hook_language_types_info() { - return array( - 'custom_language_type' => array( - 'name' => t('Custom language'), - 'description' => t('A custom language type.'), - ), - 'fixed_custom_language_type' => array( - 'fixed' => array('custom_language_provider'), - ), - ); -} - -/** - * Perform alterations on language types. - * - * @param $language_types - * Array of language type definitions. - */ -function hook_language_types_info_alter(array &$language_types) { - if (isset($language_types['custom_language_type'])) { - $language_types['custom_language_type_custom']['description'] = t('A far better description.'); - } -} - -/** - * Allow modules to define their own language providers. - * - * @return - * An array of language provider definitions. Each language provider has an - * identifier key. The language provider definition is an associative array - * that may contain the following key-value pairs: - * - "types": An array of allowed language types. If a language provider does - * not specify which language types it should be used with, it will be - * available for all the configurable language types. - * - "callbacks": An array of functions that will be called to perform various - * tasks. Possible key-value pairs are: - * - "language": Required. The callback that will determine the language - * value. - * - "switcher": The callback that will determine the language switch links - * associated to the current language provider. - * - "url_rewrite": The callback that will provide URL rewriting. - * - "file": A file that will be included before the callback is invoked; this - * allows callback functions to be in separate files. - * - "weight": The default weight the language provider has. - * - "name": A human-readable identifier. - * - "description": A description of the language provider. - * - "config": An internal path pointing to the language provider - * configuration page. - * - "cache": The value Drupal's page cache should be set to for the current - * language provider to be invoked. - */ -function hook_language_negotiation_info() { - return array( - 'custom_language_provider' => array( - 'callbacks' => array( - 'language' => 'custom_language_provider_callback', - 'switcher' => 'custom_language_switcher_callback', - 'url_rewrite' => 'custom_language_url_rewrite_callback', - ), - 'file' => drupal_get_path('module', 'custom') . '/custom.module', - 'weight' => -4, - 'types' => array('custom_language_type'), - 'name' => t('Custom language provider'), - 'description' => t('This is a custom language provider.'), - 'cache' => 0, - ), - ); -} - -/** - * Perform alterations on language providers. - * - * @param $language_providers - * Array of language provider definitions. - */ -function hook_language_negotiation_info_alter(array &$language_providers) { - if (isset($language_providers['custom_language_provider'])) { - $language_providers['custom_language_provider']['config'] = 'admin/config/regional/language/configure/custom-language-provider'; - } -} - -/** * Allow modules to react to language settings changes. * * Every module needing to act when the number of enabled languages changes @@ -182,16 +37,5 @@ } /** - * Perform alterations on the language fallback candidates. - * - * @param $fallback_candidates - * An array of language codes whose order will determine the language fallback - * order. - */ -function hook_language_fallback_candidates_alter(array &$fallback_candidates) { - $fallback_candidates = array_reverse($fallback_candidates); -} - -/** * @} End of "addtogroup hooks". */ diff -Naur drupal-7.0/modules/locale/locale.css drupal-7.66/modules/locale/locale.css --- drupal-7.0/modules/locale/locale.css 2010-10-08 05:30:40.000000000 +0200 +++ drupal-7.66/modules/locale/locale.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,13 +1,12 @@ -/* $Id: locale.css,v 1.8 2010/10/08 03:30:40 webchick Exp $ */ .locale-untranslated { font-style: normal; text-decoration: line-through; } -.form-item-language, -.form-item-translation, -.form-item-group { +#locale-translation-filter-form .form-item-language, +#locale-translation-filter-form .form-item-translation, +#locale-translation-filter-form .form-item-group { float: left; /* LTR */ padding-right: .8em; /* LTR */ margin: 0.1em; @@ -22,14 +21,12 @@ width: 100%; } #locale-translation-filter-form .form-actions { - float: left; - padding: 3ex 0 0 1em; + float: left; /* LTR */ + padding: 3ex 0 0 1em; /* LTR */ } - .language-switcher-locale-session a.active { color: #0062A0; } - .language-switcher-locale-session a.session-active { color: #000000; } diff -Naur drupal-7.0/modules/locale/locale.datepicker.js drupal-7.66/modules/locale/locale.datepicker.js --- drupal-7.0/modules/locale/locale.datepicker.js 2010-04-29 07:15:43.000000000 +0200 +++ drupal-7.66/modules/locale/locale.datepicker.js 2019-04-17 22:20:46.000000000 +0200 @@ -1,70 +1,79 @@ -// $Id: locale.datepicker.js,v 1.1 2010/04/29 05:15:43 webchick Exp $ (function ($) { -$.datepicker.regional['drupal-locale'] = { - closeText: Drupal.t('Done'), - prevText: Drupal.t('Prev'), - nextText: Drupal.t('Next'), - currentText: Drupal.t('Today'), - monthNames: [ - Drupal.t('January'), - Drupal.t('February'), - Drupal.t('March'), - Drupal.t('April'), - Drupal.t('May'), - Drupal.t('June'), - Drupal.t('July'), - Drupal.t('August'), - Drupal.t('September'), - Drupal.t('October'), - Drupal.t('November'), - Drupal.t('December') - ], - monthNamesShort: [ - Drupal.t('Jan'), - Drupal.t('Feb'), - Drupal.t('Mar'), - Drupal.t('Apr'), - Drupal.t('May'), - Drupal.t('Jun'), - Drupal.t('Jul'), - Drupal.t('Aug'), - Drupal.t('Sep'), - Drupal.t('Oct'), - Drupal.t('Nov'), - Drupal.t('Dec') - ], - dayNames: [ - Drupal.t('Sunday'), - Drupal.t('Monday') - Drupal.t('Tuesday') - Drupal.t('Wednesday') - Drupal.t('Thursday') - Drupal.t('Friday') - Drupal.t('Saturday') - ], - dayNamesShort: [ - Drupal.t('Sun') - Drupal.t('Mon') - Drupal.t('Tue') - Drupal.t('Wed') - Drupal.t('Thu') - Drupal.t('Fri') - Drupal.t('Sat') - ], - dayNamesMin: [ - Drupal.t('Su') - Drupal.t('Mo') - Drupal.t('Tu') - Drupal.t('We') - Drupal.t('Th') - Drupal.t('Fr') - Drupal.t('Sa') - ], - dateFormat: Drupal.t('mm/dd/yy'), - firstDay: Drupal.settings.jqueryuidatepicker.firstDay, - isRTL: Drupal.settings.jqueryuidatepicker.rtl +/** + * Attaches language support to the jQuery UI datepicker component. + */ +Drupal.behaviors.localeDatepicker = { + attach: function(context, settings) { + // This code accesses Drupal.settings and localized strings via Drupal.t(). + // So this code should run after these are initialized. By placing it in an + // attach behavior this is assured. + $.datepicker.regional['drupal-locale'] = $.extend({ + closeText: Drupal.t('Done'), + prevText: Drupal.t('Prev'), + nextText: Drupal.t('Next'), + currentText: Drupal.t('Today'), + monthNames: [ + Drupal.t('January'), + Drupal.t('February'), + Drupal.t('March'), + Drupal.t('April'), + Drupal.t('May'), + Drupal.t('June'), + Drupal.t('July'), + Drupal.t('August'), + Drupal.t('September'), + Drupal.t('October'), + Drupal.t('November'), + Drupal.t('December') + ], + monthNamesShort: [ + Drupal.t('Jan'), + Drupal.t('Feb'), + Drupal.t('Mar'), + Drupal.t('Apr'), + Drupal.t('May'), + Drupal.t('Jun'), + Drupal.t('Jul'), + Drupal.t('Aug'), + Drupal.t('Sep'), + Drupal.t('Oct'), + Drupal.t('Nov'), + Drupal.t('Dec') + ], + dayNames: [ + Drupal.t('Sunday'), + Drupal.t('Monday'), + Drupal.t('Tuesday'), + Drupal.t('Wednesday'), + Drupal.t('Thursday'), + Drupal.t('Friday'), + Drupal.t('Saturday') + ], + dayNamesShort: [ + Drupal.t('Sun'), + Drupal.t('Mon'), + Drupal.t('Tue'), + Drupal.t('Wed'), + Drupal.t('Thu'), + Drupal.t('Fri'), + Drupal.t('Sat') + ], + dayNamesMin: [ + Drupal.t('Su'), + Drupal.t('Mo'), + Drupal.t('Tu'), + Drupal.t('We'), + Drupal.t('Th'), + Drupal.t('Fr'), + Drupal.t('Sa') + ], + dateFormat: Drupal.t('mm/dd/yy'), + firstDay: 0, + isRTL: 0 + }, Drupal.settings.jquery.ui.datepicker); + $.datepicker.setDefaults($.datepicker.regional['drupal-locale']); + } }; -$.datepicker.setDefaults($.datepicker.regional['drupal-locale']); })(jQuery); diff -Naur drupal-7.0/modules/locale/locale.info drupal-7.66/modules/locale/locale.info --- drupal-7.0/modules/locale/locale.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/locale/locale.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: locale.info,v 1.16 2010/12/20 19:59:42 webchick Exp $ name = Locale description = Adds language handling functionality and enables the translation of the user interface to languages other than English. package = Core @@ -7,8 +6,7 @@ files[] = locale.test configure = admin/config/regional/language -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/locale/locale.install drupal-7.66/modules/locale/locale.install --- drupal-7.0/modules/locale/locale.install 2011-01-02 18:26:39.000000000 +0100 +++ drupal-7.66/modules/locale/locale.install 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ 1"); + + $conflict = FALSE; + foreach ($result_source as $source) { + // Find all rows in {locales_target} that are translations of the same + // string (incl. context). + $result_target = db_query("SELECT t.lid, t.language, t.plural, t.translation FROM {locales_source} s JOIN {locales_target} t ON s.lid = t.lid WHERE s.source = :source AND s.context = :context AND s.textgroup = 'default' ORDER BY lid", array( + ':source' => $source->source, + ':context' => $source->context, + )); + + $translations = array(); + $keep_lids = array($source->lid); + foreach ($result_target as $target) { + if (!isset($translations[$target->language])) { + $translations[$target->language] = $target->translation; + if ($target->lid != $source->lid) { + // Move translation to the master lid. + db_query('UPDATE {locales_target} SET lid = :new_lid WHERE lid = :old_lid', array( + ':new_lid' => $source->lid, + ':old_lid' => $target->lid)); + } + } + elseif ($translations[$target->language] == $target->translation) { + // Delete duplicate translation. + db_query('DELETE FROM {locales_target} WHERE lid = :lid AND language = :language', array( + ':lid' => $target->lid, + ':language' => $target->language)); + } + else { + // The same string is translated into several different strings in one + // language. We do not know which is the preferred, so we keep them all. + $keep_lids[] = $target->lid; + $conflict = TRUE; + } + } + + // Delete rows in {locales_source} that are no longer referenced from + // {locales_target}. + db_delete('locales_source') + ->condition('source', $source->source) + ->condition('context', $source->context) + ->condition('textgroup', 'default') + ->condition('lid', $keep_lids, 'NOT IN') + ->execute(); + } + + if ($conflict) { + $url = 'http://drupal.org/node/746240'; + drupal_set_message('Your {locales_source} table contains duplicates that could not be removed automatically. See ' . $url . ' for more information.', 'warning'); + } +} + +/** + * Increase {locales_languages}.formula column's length. + */ +function locale_update_7005() { + db_change_field('languages', 'formula', 'formula', array( + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + 'description' => 'Plural formula in PHP code to evaluate to get plural indexes.', + )); +} + +/** + * @} End of "addtogroup updates-7.x-extra". */ /** @@ -228,7 +316,7 @@ ), 'formula' => array( 'type' => 'varchar', - 'length' => 128, + 'length' => 255, 'not null' => TRUE, 'default' => '', 'description' => 'Plural formula in PHP code to evaluate to get plural indexes.', diff -Naur drupal-7.0/modules/locale/locale.module drupal-7.66/modules/locale/locale.module --- drupal-7.0/modules/locale/locale.module 2010-11-21 08:02:46.000000000 +0100 +++ drupal-7.66/modules/locale/locale.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ ' . t('About') . ''; - $output .= '

      ' . t('The Locale module allows your Drupal site to be presented in languages other than the default English, and to be multilingual. The Locale module works by maintaining a database of translations, and examining text as it is about to be displayed. When a translation of the text is available in the language to be displayed, the translation is displayed rather than the original text. When a translation is unavailable, the original text is displayed, and then stored for review by a translator. For more information, see the online handbook entry for Locale module.', array('@locale' => 'http://drupal.org/handbook/modules/locale/')) . '

      '; + $output .= '

      ' . t('The Locale module allows your Drupal site to be presented in languages other than the default English, and to be multilingual. The Locale module works by maintaining a database of translations, and examining text as it is about to be displayed. When a translation of the text is available in the language to be displayed, the translation is displayed rather than the original text. When a translation is unavailable, the original text is displayed, and then stored for review by a translator. For more information, see the online handbook entry for Locale module.', array('@locale' => 'http://drupal.org/documentation/modules/locale/')) . '

      '; $output .= '

      ' . t('Uses') . '

      '; $output .= '
      '; $output .= '
      ' . t('Translating interface text') . '
      '; @@ -60,9 +59,9 @@ return '

      ' . t('This page exports the translated strings used by your site. An export file may be in Gettext Portable Object (.po) form, which includes both the original string and the translation (used to share translations with others), or in Gettext Portable Object Template (.pot) form, which includes the original strings only (used to create new translations with a Gettext translation editor).') . '

      '; case 'admin/config/regional/translate/translate': return '

      ' . t('This page allows a translator to search for specific translated and untranslated strings, and is used when creating or editing translations. (Note: For translation tasks involving many strings, it may be more convenient to export strings for offline editing in a desktop Gettext translation editor.) Searches may be limited to strings found within a specific text group or in a specific language.', array('@export' => url('admin/config/regional/translate/export'))) . '

      '; - case 'admin/structure/block/manage': - if ($arg[4] == 'locale' && $arg[5] == 0) { - return '

      ' . t('This block is only shown if at least two languages are enabled and language negotiation is set to something other than None.', array('@languages' => url('admin/config/regional/language'), '@configuration' => url('admin/config/regional/language/configure'))) . '

      '; + case 'admin/structure/block/manage/%/%': + if ($arg[4] == 'locale' && $arg[5] == 'language') { + return '

      ' . t('This block is only shown if at least two languages are enabled and language negotiation is set to URL or Session.', array('@languages' => url('admin/config/regional/language'), '@configuration' => url('admin/config/regional/language/configure'))) . '

      '; } break; } @@ -254,6 +253,7 @@ ), 'translate interface' => array( 'title' => t('Translate interface texts'), + 'restrict access' => TRUE, ), ); } @@ -386,28 +386,61 @@ /** * Form submit handler for node_form(). * - * Checks if Locale is registered as a translation handler and handle possible - * node language changes. - * * This submit handler needs to run before entity_form_submit_build_entity() * is invoked by node_form_submit_build_node(), because it alters the values of * attached fields. Therefore, it cannot be a hook_node_submit() implementation. */ function locale_field_node_form_submit($form, &$form_state) { - if (field_has_translation_handler('node', 'locale')) { - $node = (object) $form_state['values']; - $available_languages = field_content_languages(); - list(, , $bundle) = entity_extract_ids('node', $node); + locale_field_entity_form_submit('node', $form, $form_state); +} + +/** + * Implements hook_form_FORM_ID_alter(). + */ +function locale_form_comment_form_alter(&$form, &$form_state, $form_id) { + // If a content type has multilingual support we set the content language as + // comment language. + if ($form['language']['#value'] == LANGUAGE_NONE && locale_multilingual_node_type($form['#node']->type)) { + global $language_content; + $form['language']['#value'] = $language_content->language; + $submit_callback = 'locale_field_comment_form_submit'; + array_unshift($form['actions']['preview']['#submit'], $submit_callback); + array_unshift($form['#submit'], $submit_callback); + } +} + +/** + * Form submit handler for comment_form(). + * + * This submit handler needs to run before entity_form_submit_build_entity() + * is invoked by comment_form_submit_build_comment(), because it alters the + * values of attached fields. + */ +function locale_field_comment_form_submit($form, &$form_state) { + locale_field_entity_form_submit('comment', $form, $form_state); +} - foreach (field_info_instances('node', $bundle) as $instance) { +/** + * Handles field language on submit for the given entity type. + * + * Checks if Locale is registered as a translation handler and handle possible + * language changes. + */ +function locale_field_entity_form_submit($entity_type, $form, &$form_state ) { + if (field_has_translation_handler($entity_type, 'locale')) { + $entity = (object) $form_state['values']; + $current_language = entity_language($entity_type, $entity); + list(, , $bundle) = entity_extract_ids($entity_type, $entity); + + foreach (field_info_instances($entity_type, $bundle) as $instance) { $field_name = $instance['field_name']; $field = field_info_field($field_name); $previous_language = $form[$field_name]['#language']; // Handle a possible language change: new language values are inserted, // previous ones are deleted. - if ($field['translatable'] && $previous_language != $node->language) { - $form_state['values'][$field_name][$node->language] = $node->{$field_name}[$previous_language]; + if ($field['translatable'] && $previous_language != $current_language) { + $form_state['values'][$field_name][$current_language] = $entity->{$field_name}[$previous_language]; $form_state['values'][$field_name][$previous_language] = array(); } } @@ -491,6 +524,9 @@ */ function locale_entity_info_alter(&$entity_info) { $entity_info['node']['translation']['locale'] = TRUE; + if (isset($entity_info['comment'])) { + $entity_info['comment']['translation']['locale'] = TRUE; + } } /** @@ -514,6 +550,8 @@ 'description' => t('Order of language detection methods for user interface text. If a translation of user interface text is available in the detected language, it will be displayed.'), ), LANGUAGE_TYPE_CONTENT => array( + 'name' => t('Content'), + 'description' => t('Order of language detection methods for content. If a version of content is available in the detected language, it will be displayed.'), 'fixed' => array(LOCALE_LANGUAGE_NEGOTIATION_INTERFACE), ), LANGUAGE_TYPE_URL => array( @@ -594,6 +632,22 @@ return $providers; } +/** + * Implements hook_modules_enabled(). + */ +function locale_modules_enabled($modules) { + include_once DRUPAL_ROOT . '/includes/language.inc'; + language_types_set(); + language_negotiation_purge(); +} + +/** + * Implements hook_modules_disabled(). + */ +function locale_modules_disabled($modules) { + locale_modules_enabled($modules); +} + // --------------------------------------------------------------------------------- // Locale core functionality @@ -613,7 +667,14 @@ */ function locale($string = NULL, $context = NULL, $langcode = NULL) { global $language; - $locale_t = &drupal_static(__FUNCTION__); + + // Use the advanced drupal_static() pattern, since this is called very often. + static $drupal_static_fast; + if (!isset($drupal_static_fast)) { + $drupal_static_fast['locale'] = &drupal_static(__FUNCTION__); + } + $locale_t = &$drupal_static_fast['locale']; + if (!isset($string)) { // Return all cached strings if no string was specified @@ -675,13 +736,15 @@ } else { // We don't have the source string, cache this as untranslated. - db_insert('locales_source') - ->fields(array( + db_merge('locales_source') + ->insertFields(array( 'location' => request_uri(), + 'version' => VERSION, + )) + ->key(array( 'source' => $string, 'context' => (string) $context, 'textgroup' => 'default', - 'version' => VERSION, )) ->execute(); $locale_t[$langcode][$context][$string] = TRUE; @@ -710,30 +773,50 @@ * @param $langcode * Optional language code to translate to a language other than * what is used to display the page. + * + * @return + * The numeric index of the plural variant to use for this $langcode and + * $count combination or -1 if the language was not found or does not have a + * plural formula. */ function locale_get_plural($count, $langcode = NULL) { global $language; - $locale_formula = &drupal_static(__FUNCTION__, array()); - $plurals = &drupal_static(__FUNCTION__ . ':plurals', array()); + + // Used to locally cache the plural formulas for all languages. + $plural_formulas = &drupal_static(__FUNCTION__, array()); + + // Used to store precomputed plural indexes corresponding to numbers + // individually for each language. + $plural_indexes = &drupal_static(__FUNCTION__ . ':plurals', array()); $langcode = $langcode ? $langcode : $language->language; - if (!isset($plurals[$langcode][$count])) { - if (empty($locale_formula)) { - $language_list = language_list(); - $locale_formula[$langcode] = $language_list[$langcode]->formula; + if (!isset($plural_indexes[$langcode][$count])) { + // Retrieve and statically cache the plural formulas for all languages. + if (empty($plural_formulas)) { + foreach (language_list() as $installed_language) { + $plural_formulas[$installed_language->language] = $installed_language->formula; + } } - if ($locale_formula[$langcode]) { + // If there is a plural formula for the language, evaluate it for the given + // $count and statically cache the result for the combination of language + // and count, since the result will always be identical. + if (!empty($plural_formulas[$langcode])) { + // $n is used inside the expression in the eval(). $n = $count; - $plurals[$langcode][$count] = @eval('return intval(' . $locale_formula[$langcode] . ');'); - return $plurals[$langcode][$count]; + $plural_indexes[$langcode][$count] = @eval('return intval(' . $plural_formulas[$langcode] . ');'); + } + // In case there is no plural formula for English (no imported translation + // for English), use a default formula. + elseif ($langcode == 'en') { + $plural_indexes[$langcode][$count] = (int) ($count != 1); } + // Otherwise, return -1 (unknown). else { - $plurals[$langcode][$count] = -1; - return -1; + $plural_indexes[$langcode][$count] = -1; } } - return $plurals[$langcode][$count]; + return $plural_indexes[$langcode][$count]; } @@ -773,7 +856,24 @@ } /** - * Imports translations when new modules or themes are installed or enabled. + * Implements hook_modules_installed(). + */ +function locale_modules_installed($modules) { + locale_system_update($modules); +} + +/** + * Implements hook_themes_enabled(). + * + * @todo This is technically wrong. We must not import upon enabling, but upon + * initial installation. The theme system is missing an installation hook. + */ +function locale_themes_enabled($themes) { + locale_system_update($themes); +} + +/** + * Imports translations when new modules or themes are installed. * * This function will either import translation for the component change * right away, or start a batch if more files need to be imported. @@ -872,7 +972,7 @@ // Replicate the same item, but with the RTL path and a little larger // weight so that it appears directly after the original CSS file. $item['data'] = $rtl_path; - $item['weight'] += 0.01; + $item['weight'] += 0.0001; $css[$rtl_path] = $item; } } @@ -886,15 +986,22 @@ * Provides the language support for the jQuery UI Date Picker. */ function locale_library_alter(&$libraries, $module) { - global $language; - if ($module == 'system' && isset($libraries['system']['ui.datepicker'])) { + if ($module == 'system' && isset($libraries['ui.datepicker'])) { + global $language; + // locale.datepicker.js should be added in the JS_LIBRARY group, so that + // this attach behavior will execute early. JS_LIBRARY is the default for + // hook_library_info_alter(), thus does not have to be specified explicitly. $datepicker = drupal_get_path('module', 'locale') . '/locale.datepicker.js'; - $libraries['system']['ui.datepicker']['js'][$datepicker] = array('group' => JS_THEME); - $libraries['system']['ui.datepicker']['js'][] = array( + $libraries['ui.datepicker']['js'][$datepicker] = array(); + $libraries['ui.datepicker']['js'][] = array( 'data' => array( - 'jqueryuidatepicker' => array( - 'rtl' => $language->direction == LANGUAGE_RTL, - 'firstDay' => variable_get('date_first_day', 0), + 'jquery' => array( + 'ui' => array( + 'datepicker' => array( + 'isRTL' => $language->direction == LANGUAGE_RTL, + 'firstDay' => variable_get('date_first_day', 0), + ), + ), ), ), 'type' => 'setting', @@ -912,7 +1019,7 @@ include_once DRUPAL_ROOT . '/includes/language.inc'; $block = array(); $info = language_types_info(); - foreach (language_types_configurable() as $type) { + foreach (language_types_configurable(FALSE) as $type) { $block[$type] = array( 'info' => t('Language switcher (@type)', array('@type' => $info[$type]['name'])), // Not worth caching. @@ -962,17 +1069,16 @@ include_once DRUPAL_ROOT . '/includes/language.inc'; foreach (language_types_configurable() as $type) { - // Get url rewriter callbacks only from enabled language providers. + // Get URL rewriter callbacks only from enabled language providers. $negotiation = variable_get("language_negotiation_$type", array()); foreach ($negotiation as $id => $provider) { - if (isset($provider['file'])) { - require_once DRUPAL_ROOT . '/' . $provider['file']; - } - - // Avoid duplicate callback entries. if (isset($provider['callbacks']['url_rewrite'])) { - $callbacks[$provider['callbacks']['url_rewrite']] = NULL; + if (isset($provider['file'])) { + require_once DRUPAL_ROOT . '/' . $provider['file']; + } + // Avoid duplicate callback entries. + $callbacks[$provider['callbacks']['url_rewrite']] = TRUE; } } } @@ -990,15 +1096,3 @@ } } } - -/** - * Implements hook_form_FORM_ID_alter(). - */ -function locale_form_comment_form_alter(&$form, &$form_state, $form_id) { - // If a content type has multilingual support we set the content language as - // comment language. - if ($form['language']['#value'] == LANGUAGE_NONE && locale_multilingual_node_type($form['#node']->type)) { - global $language_content; - $form['language']['#value'] = $language_content->language; - } -} diff -Naur drupal-7.0/modules/locale/locale.test drupal-7.66/modules/locale/locale.test --- drupal-7.0/modules/locale/locale.test 2010-11-30 02:05:24.000000000 +0100 +++ drupal-7.66/modules/locale/locale.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,9 +1,8 @@ 'fr', ); $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language')); - $this->assertText('fr', t('Language added successfully.')); - $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), t('Correct page redirection.')); + $this->assertText('fr', 'Language added successfully.'); + $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), 'Correct page redirection.'); // Add custom language. // Code for the language. @@ -72,89 +72,233 @@ 'direction' => '0', ); $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language')); - $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), t('Correct page redirection.')); - $this->assertText($langcode, t('Language code found.')); - $this->assertText($name, t('Name found.')); - $this->assertText($native, t('Native found.')); - $this->assertText($native, t('Test language added.')); + $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), 'Correct page redirection.'); + $this->assertText($langcode, 'Language code found.'); + $this->assertText($name, 'Name found.'); + $this->assertText($native, 'Native found.'); + $this->assertText($native, 'Test language added.'); // Check if we can change the default language. $path = 'admin/config/regional/language'; $this->drupalGet($path); - $this->assertFieldChecked('edit-site-default-en', t('English is the default language.')); + $this->assertFieldChecked('edit-site-default-en', 'English is the default language.'); // Change the default language. $edit = array( 'site_default' => $langcode, ); $this->drupalPost(NULL, $edit, t('Save configuration')); - $this->assertNoFieldChecked('edit-site-default-en', t('Default language updated.')); - $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), t('Correct page redirection.')); + $this->assertNoFieldChecked('edit-site-default-en', 'Default language updated.'); + $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), 'Correct page redirection.'); // Check if a valid language prefix is added after changing the default // language. $this->drupalGet('admin/config/regional/language/edit/en'); - $this->assertFieldByXPath('//input[@name="prefix"]', 'en', t('A valid path prefix has been added to the previous default language.')); + $this->assertFieldByXPath('//input[@name="prefix"]', 'en', 'A valid path prefix has been added to the previous default language.'); // Ensure we can't delete the default language. $this->drupalGet('admin/config/regional/language/delete/' . $langcode); - $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), t('Correct page redirection.')); - $this->assertText(t('The default language cannot be deleted.'), t('Failed to delete the default language.')); + $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), 'Correct page redirection.'); + $this->assertText(t('The default language cannot be deleted.'), 'Failed to delete the default language.'); // Check if we can disable a language. $edit = array( 'enabled[en]' => FALSE, ); $this->drupalPost($path, $edit, t('Save configuration')); - $this->assertNoFieldChecked('edit-enabled-en', t('Language disabled.')); + $this->assertNoFieldChecked('edit-enabled-en', 'Language disabled.'); // Set disabled language to be the default and ensure it is re-enabled. $edit = array( 'site_default' => 'en', ); $this->drupalPost(NULL, $edit, t('Save configuration')); - $this->assertFieldChecked('edit-enabled-en', t('Default language re-enabled.')); + $this->assertFieldChecked('edit-enabled-en', 'Default language re-enabled.'); // Ensure 'edit' link works. $this->clickLink(t('edit')); - $this->assertTitle(t('Edit language | Drupal'), t('Page title is "Edit language".')); + $this->assertTitle(t('Edit language | Drupal'), 'Page title is "Edit language".'); // Edit a language. $name = $this->randomName(16); $edit = array( 'name' => $name, ); $this->drupalPost('admin/config/regional/language/edit/' . $langcode, $edit, t('Save language')); - $this->assertRaw($name, t('The language has been updated.')); - $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), t('Correct page redirection.')); + $this->assertRaw($name, 'The language has been updated.'); + $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), 'Correct page redirection.'); // Ensure 'delete' link works. $this->drupalGet('admin/config/regional/language'); $this->clickLink(t('delete')); - $this->assertText(t('Are you sure you want to delete the language'), t('"delete" link is correct.')); - // Delete the language. + $this->assertText(t('Are you sure you want to delete the language'), '"delete" link is correct.'); + // Delete an enabled language. $this->drupalGet('admin/config/regional/language/delete/' . $langcode); // First test the 'cancel' link. $this->clickLink(t('Cancel')); - $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), t('Correct page redirection.')); - $this->assertRaw($name, t('The language was not deleted.')); + $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), 'Correct page redirection.'); + $this->assertRaw($name, 'The language was not deleted.'); // Delete the language for real. This a confirm form, we do not need any // fields changed. $this->drupalPost('admin/config/regional/language/delete/' . $langcode, array(), t('Delete')); // We need raw here because %locale will add HTML. - $this->assertRaw(t('The language %locale has been removed.', array('%locale' => $name)), t('The test language has been removed.')); - $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), t('Correct page redirection.')); + $this->assertRaw(t('The language %locale has been removed.', array('%locale' => $name)), 'The test language has been removed.'); + $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), 'Correct page redirection.'); // Verify that language is no longer found. $this->drupalGet('admin/config/regional/language/delete/' . $langcode); - $this->assertResponse(404, t('Language no longer found.')); + $this->assertResponse(404, 'Language no longer found.'); + // Make sure the "language_count" variable has been updated correctly. + drupal_static_reset('language_list'); + $enabled = language_list('enabled'); + $this->assertEqual(variable_get('language_count', 1), count($enabled[1]), 'Language count is correct.'); + // Delete a disabled language. + // Disable an enabled language. + $edit = array( + 'enabled[fr]' => FALSE, + ); + $this->drupalPost($path, $edit, t('Save configuration')); + $this->assertNoFieldChecked('edit-enabled-fr', 'French language disabled.'); + // Get the count of enabled languages. + drupal_static_reset('language_list'); + $enabled = language_list('enabled'); + // Delete the disabled language. + $this->drupalPost('admin/config/regional/language/delete/fr', array(), t('Delete')); + // We need raw here because %locale will add HTML. + $this->assertRaw(t('The language %locale has been removed.', array('%locale' => 'French')), 'Disabled language has been removed.'); + $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), 'Correct page redirection.'); + // Verify that language is no longer found. + $this->drupalGet('admin/config/regional/language/delete/fr'); + $this->assertResponse(404, 'Language no longer found.'); + // Make sure the "language_count" variable has not changed. + $this->assertEqual(variable_get('language_count', 1), count($enabled[1]), 'Language count is correct.'); + // Ensure we can't delete the English language. $this->drupalGet('admin/config/regional/language/delete/en'); - $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), t('Correct page redirection.')); - $this->assertText(t('The English language cannot be deleted.'), t('Failed to delete English language.')); + $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), 'Correct page redirection.'); + $this->assertText(t('The English language cannot be deleted.'), 'Failed to delete English language.'); } } /** + * Tests localization of the JavaScript libraries. + * + * Currently, only the jQuery datepicker is localized using Drupal translations. + */ +class LocaleLibraryInfoAlterTest extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'Javascript library localisation', + 'description' => 'Tests the localisation of JavaScript libraries.', + 'group' => 'Locale', + ); + } + + function setUp() { + parent::setUp('locale', 'locale_test'); + } + + /** + * Verifies that the datepicker can be localized. + * + * @see locale_library_info_alter() + */ + public function testLibraryInfoAlter() { + drupal_add_library('system', 'ui.datepicker'); + $scripts = drupal_get_js(); + $this->assertTrue(strpos($scripts, 'locale.datepicker.js'), 'locale.datepicker.js added to scripts.'); + } +} + +/** + * Functional tests for JavaScript parsing for translatable strings. + */ +class LocaleJavascriptTranslationTest extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'Javascript translation', + 'description' => 'Tests parsing js files for translatable strings', + 'group' => 'Locale', + ); + } + + function setUp() { + parent::setUp('locale', 'locale_test'); + } + + function testFileParsing() { + + $filename = drupal_get_path('module', 'locale_test') . '/locale_test.js'; + + // Parse the file to look for source strings. + _locale_parse_js_file($filename); + + // Get all of the source strings that were found. + $source_strings = db_select('locales_source', 's') + ->fields('s', array('source', 'context')) + ->condition('s.location', $filename) + ->execute() + ->fetchAllKeyed(); + + // List of all strings that should be in the file. + $test_strings = array( + "Standard Call t" => '', + "Whitespace Call t" => '', + + "Single Quote t" => '', + "Single Quote \\'Escaped\\' t" => '', + "Single Quote Concat strings t" => '', + + "Double Quote t" => '', + "Double Quote \\\"Escaped\\\" t" => '', + "Double Quote Concat strings t" => '', + + "Context !key Args t" => "Context string", + + "Context Unquoted t" => "Context string unquoted", + "Context Single Quoted t" => "Context string single quoted", + "Context Double Quoted t" => "Context string double quoted", + + "Standard Call plural" => '', + "Standard Call @count plural" => '', + "Whitespace Call plural" => '', + "Whitespace Call @count plural" => '', + + "Single Quote plural" => '', + "Single Quote @count plural" => '', + "Single Quote \\'Escaped\\' plural" => '', + "Single Quote \\'Escaped\\' @count plural" => '', + + "Double Quote plural" => '', + "Double Quote @count plural" => '', + "Double Quote \\\"Escaped\\\" plural" => '', + "Double Quote \\\"Escaped\\\" @count plural" => '', + + "Context !key Args plural" => "Context string", + "Context !key Args @count plural" => "Context string", + + "Context Unquoted plural" => "Context string unquoted", + "Context Unquoted @count plural" => "Context string unquoted", + "Context Single Quoted plural" => "Context string single quoted", + "Context Single Quoted @count plural" => "Context string single quoted", + "Context Double Quoted plural" => "Context string double quoted", + "Context Double Quoted @count plural" => "Context string double quoted", + ); + + // Assert that all strings were found properly. + foreach ($test_strings as $str => $context) { + $args = array('%source' => $str, '%context' => $context); + + // Make sure that the string was found in the file. + $this->assertTrue(isset($source_strings[$str]), format_string('Found source string: %source', $args)); + + // Make sure that the proper context was matched. + $this->assertTrue(isset($source_strings[$str]) && $source_strings[$str] === $context, strlen($context) > 0 ? format_string('Context for %source is %context', $args) : format_string('Context for %source is blank', $args)); + } + + $this->assertEqual(count($source_strings), count($test_strings), 'Found correct number of source strings.'); + } +} +/** * Functional test for string translation and validation. */ class LocaleTranslationFunctionalTest extends DrupalWebTestCase { @@ -208,12 +352,12 @@ t($name, array(), array('langcode' => $langcode)); // Reset locale cache. locale_reset(); - $this->assertText($langcode, t('Language code found.')); - $this->assertText($name, t('Name found.')); - $this->assertText($native, t('Native found.')); + $this->assertText($langcode, 'Language code found.'); + $this->assertText($name, 'Name found.'); + $this->assertText($native, 'Native found.'); // No t() here, we do not want to add this string to the database and it's // surely not translated yet. - $this->assertText($native, t('Test language added.')); + $this->assertText($native, 'Test language added.'); $this->drupalLogout(); // Search for the name and translate it. @@ -228,8 +372,8 @@ // assertText() seems to remove the input field where $name always could be // found, so this is not a false assert. See how assertNoText succeeds // later. - $this->assertText($name, t('Search found the name.')); - $this->assertRaw($language_indicator, t('Name is untranslated.')); + $this->assertText($name, 'Search found the name.'); + $this->assertRaw($language_indicator, 'Name is untranslated.'); // Assume this is the only result, given the random name. $this->clickLink(t('edit')); // We save the lid from the path. @@ -237,24 +381,34 @@ preg_match('!admin/config/regional/translate/edit/(\d+)!', $this->getUrl(), $matches); $lid = $matches[1]; // No t() here, it's surely not translated yet. - $this->assertText($name, t('name found on edit screen.')); + $this->assertText($name, 'name found on edit screen.'); $edit = array( "translations[$langcode]" => $translation, ); $this->drupalPost(NULL, $edit, t('Save translations')); - $this->assertText(t('The string has been saved.'), t('The string has been saved.')); - $this->assertEqual($this->getUrl(), url('admin/config/regional/translate/translate', array('absolute' => TRUE)), t('Correct page redirection.')); - $this->assertTrue($name != $translation && t($name, array(), array('langcode' => $langcode)) == $translation, t('t() works.')); + $this->assertText(t('The string has been saved.'), 'The string has been saved.'); + $this->assertEqual($this->getUrl(), url('admin/config/regional/translate/translate', array('absolute' => TRUE)), 'Correct page redirection.'); + $this->assertTrue($name != $translation && t($name, array(), array('langcode' => $langcode)) == $translation, 't() works.'); $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); // The indicator should not be here. - $this->assertNoRaw($language_indicator, t('String is translated.')); + $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; $this->drupalGet('admin/config/regional/translate/edit/' . $random_lid); - $this->assertText(t('String not found'), t('String not found.')); - $this->assertEqual($this->getUrl(), url('admin/config/regional/translate/translate', array('absolute' => TRUE)), t('Correct page redirection.')); + $this->assertText(t('String not found'), 'String not found.'); + $this->assertEqual($this->getUrl(), url('admin/config/regional/translate/translate', array('absolute' => TRUE)), 'Correct page redirection.'); $this->drupalLogout(); // Delete the language. @@ -263,12 +417,11 @@ // This a confirm form, we do not need any fields changed. $this->drupalPost($path, array(), t('Delete')); // We need raw here because %locale will add HTML. - $this->assertRaw(t('The language %locale has been removed.', array('%locale' => $name)), t('The test language has been removed.')); + $this->assertRaw(t('The language %locale has been removed.', array('%locale' => $name)), 'The test language has been removed.'); // Reload to remove $name. $this->drupalGet($path); - $this->assertNoText($langcode, t('Language code not found.')); - $this->assertNoText($name, t('Name not found.')); - $this->assertNoText($native, t('Native not found.')); + // Verify that language is no longer found. + $this->assertResponse(404, 'Language no longer found.'); $this->drupalLogout(); // Delete the string. @@ -282,20 +435,20 @@ $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); // Assume this is the only result, given the random name. $this->clickLink(t('delete')); - $this->assertText(t('Are you sure you want to delete the string'), t('"delete" link is correct.')); + $this->assertText(t('Are you sure you want to delete the string'), '"delete" link is correct.'); // Delete the string. $path = 'admin/config/regional/translate/delete/' . $lid; $this->drupalGet($path); // First test the 'cancel' link. $this->clickLink(t('Cancel')); - $this->assertEqual($this->getUrl(), url('admin/config/regional/translate/translate', array('absolute' => TRUE)), t('Correct page redirection.')); - $this->assertRaw($name, t('The string was not deleted.')); + $this->assertEqual($this->getUrl(), url('admin/config/regional/translate/translate', array('absolute' => TRUE)), 'Correct page redirection.'); + $this->assertRaw($name, 'The string was not deleted.'); // Delete the name string. $this->drupalPost('admin/config/regional/translate/delete/' . $lid, array(), t('Delete')); - $this->assertText(t('The string has been removed.'), t('The string has been removed message.')); - $this->assertEqual($this->getUrl(), url('admin/config/regional/translate/translate', array('absolute' => TRUE)), t('Correct page redirection.')); + $this->assertText(t('The string has been removed.'), 'The string has been removed message.'); + $this->assertEqual($this->getUrl(), url('admin/config/regional/translate/translate', array('absolute' => TRUE)), 'Correct page redirection.'); $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); - $this->assertNoText($name, t('Search now can not find the name.')); + $this->assertNoText($name, 'Search now can not find the name.'); } /* @@ -351,14 +504,14 @@ ->execute() ->fetchObject(); $js_file = 'public://' . variable_get('locale_js_directory', 'languages') . '/' . $langcode . '_' . $file->javascript . '.js'; - $this->assertTrue($result = file_exists($js_file), t('JavaScript file created: %file', array('%file' => $result ? $js_file : t('not found')))); + $this->assertTrue($result = file_exists($js_file), format_string('JavaScript file created: %file', array('%file' => $result ? $js_file : 'not found'))); // Test JavaScript translation rebuilding. file_unmanaged_delete($js_file); - $this->assertTrue($result = !file_exists($js_file), t('JavaScript file deleted: %file', array('%file' => $result ? $js_file : t('found')))); + $this->assertTrue($result = !file_exists($js_file), format_string('JavaScript file deleted: %file', array('%file' => $result ? $js_file : 'found'))); cache_clear_all(); _locale_rebuild_js($langcode); - $this->assertTrue($result = file_exists($js_file), t('JavaScript file rebuilt: %file', array('%file' => $result ? $js_file : t('not found')))); + $this->assertTrue($result = file_exists($js_file), format_string('JavaScript file rebuilt: %file', array('%file' => $result ? $js_file : 'not found'))); } /** @@ -411,7 +564,7 @@ $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); // Find the edit path. $content = $this->drupalGetContent(); - $this->assertTrue(preg_match('@(admin/config/regional/translate/edit/[0-9]+)@', $content, $matches), t('Found the edit path.')); + $this->assertTrue(preg_match('@(admin/config/regional/translate/edit/[0-9]+)@', $content, $matches), 'Found the edit path.'); $path = $matches[0]; foreach ($bad_translations as $key => $translation) { $edit = array( @@ -420,8 +573,8 @@ $this->drupalPost($path, $edit, t('Save translations')); // Check for a form error on the textarea. $form_class = $this->xpath('//form[@id="locale-translate-edit-form"]//textarea/@class'); - $this->assertNotIdentical(FALSE, strpos($form_class[0], 'error'), t('The string was rejected as unsafe.')); - $this->assertNoText(t('The string has been saved.'), t('The string was not saved.')); + $this->assertNotIdentical(FALSE, strpos($form_class[0], 'error'), 'The string was rejected as unsafe.'); + $this->assertNoText(t('The string has been saved.'), 'The string was not saved.'); } } @@ -478,7 +631,7 @@ // assertText() seems to remove the input field where $name always could be // found, so this is not a false assert. See how assertNoText succeeds // later. - $this->assertText($name, t('Search found the string.')); + $this->assertText($name, 'Search found the string.'); // Ensure untranslated string doesn't appear if searching on 'only // translated strings'. @@ -489,10 +642,10 @@ 'group' => 'all', ); $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); - $this->assertText(t('No strings available.'), t("Search didn't find the string.")); + $this->assertText(t('No strings available.'), "Search didn't find the string."); // Ensure untranslated string appears if searching on 'only untranslated - // strings'. + // strings' in "all" (hasn't been translated to any language). $search = array( 'string' => $name, 'language' => 'all', @@ -500,7 +653,18 @@ 'group' => 'all', ); $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); - $this->assertNoText(t('No strings available.'), t('Search found the string.')); + $this->assertNoText(t('No strings available.'), 'Search found the string.'); + + // Ensure untranslated string appears if searching on 'only untranslated + // strings' in the custom language (hasn't been translated to that specific language). + $search = array( + 'string' => $name, + 'language' => $langcode, + 'translation' => 'untranslated', + 'group' => 'all', + ); + $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); + $this->assertNoText(t('No strings available.'), 'Search found the string.'); // Add translation. // Assume this is the only result, given the random name. @@ -523,7 +687,7 @@ 'group' => 'all', ); $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); - $this->assertNoText(t('No strings available.'), t('Search found the translation.')); + $this->assertNoText(t('No strings available.'), 'Search found the translation.'); // Ensure translated source string doesn't appear if searching on 'only // untranslated strings'. @@ -534,7 +698,7 @@ 'group' => 'all', ); $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); - $this->assertText(t('No strings available.'), t("Search didn't find the source string.")); + $this->assertText(t('No strings available.'), "Search didn't find the source string."); // Ensure translated string doesn't appear if searching on 'only // untranslated strings'. @@ -545,7 +709,7 @@ 'group' => 'all', ); $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); - $this->assertText(t('No strings available.'), t("Search didn't find the translation.")); + $this->assertText(t('No strings available.'), "Search didn't find the translation."); // Ensure translated string does appear if searching on the custom language. $search = array( @@ -555,7 +719,7 @@ 'group' => 'all', ); $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); - $this->assertNoText(t('No strings available.'), t('Search found the translation.')); + $this->assertNoText(t('No strings available.'), 'Search found the translation.'); // Ensure translated string doesn't appear if searching on English. $search = array( @@ -565,7 +729,7 @@ 'group' => 'all', ); $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); - $this->assertText(t('No strings available.'), t("Search didn't find the translation.")); + $this->assertText(t('No strings available.'), "Search didn't find the translation."); // Search for a string that isn't in the system. $unavailable_string = $this->randomName(16); @@ -576,7 +740,137 @@ 'group' => 'all', ); $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); - $this->assertText(t('No strings available.'), t("Search didn't find the invalid string.")); + $this->assertText(t('No strings available.'), "Search didn't find the invalid string."); + } +} + +/** + * Tests plural index computation functionality. + */ +class LocalePluralFormatTest extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'Plural formula evaluation', + 'description' => 'Tests plural formula evaluation for various languages.', + 'group' => 'Locale', + ); + } + + function setUp() { + parent::setUp('locale', 'locale_test'); + + $admin_user = $this->drupalCreateUser(array('administer languages', 'translate interface', 'access administration pages')); + $this->drupalLogin($admin_user); + + // Import some .po files with formulas to set up the environment. + // These will also add the languages to the system and enable them. + $this->importPoFile($this->getPoFileWithSimplePlural(), array( + 'langcode' => 'fr', + )); + $this->importPoFile($this->getPoFileWithComplexPlural(), array( + 'langcode' => 'hr', + )); + } + + /** + * Tests locale_get_plural() functionality. + */ + function testGetPluralFormat() { + $this->drupalGet('locale_test_plural_format_page'); + $tests = _locale_test_plural_format_tests(); + $result = array(); + foreach ($tests as $test) { + $this->assertPluralFormat($test['count'], $test['language'], $test['expected-result']); + } + } + + /** + * Helper assert to test locale_get_plural page. + * + * @param $count + * Number for testing. + * @param $lang + * Language for testing + * @param $expected_result + * Expected result. + * @param $message + */ + function assertPluralFormat($count, $lang, $expected_result) { + $message_param = array( + '@lang' => $lang, + '@count' => $count, + '@expected_result' => $expected_result, + ); + $message = t("Computed plural index for '@lang' with count @count is @expected_result.", $message_param); + + $message_param = array( + '@lang' => $lang, + '@expected_result' => $expected_result, + ); + $this->assertText(format_string('Language: @lang, locale_get_plural: @expected_result.', $message_param, $message)); + } + + /** + * Imports a standalone .po file in a given language. + * + * @param $contents + * Contents of the .po file to import. + * @param $options + * Additional options to pass to the translation import form. + */ + function importPoFile($contents, array $options = array()) { + $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')); + drupal_unlink($name); + } + + /** + * Returns a .po file with a simple plural formula. + */ + function getPoFileWithSimplePlural() { + return <<< EOF +msgid "" +msgstr "" +"Project-Id-Version: Drupal 7\\n" +"MIME-Version: 1.0\\n" +"Content-Type: text/plain; charset=UTF-8\\n" +"Content-Transfer-Encoding: 8bit\\n" +"Plural-Forms: nplurals=2; plural=(n!=1);\\n" + +msgid "One sheep" +msgid_plural "@count sheep" +msgstr[0] "un mouton" +msgstr[1] "@count moutons" + +msgid "Monday" +msgstr "lundi" +EOF; + } + + /** + * Returns a .po file with a complex plural formula. + */ + function getPoFileWithComplexPlural() { + return <<< EOF +msgid "" +msgstr "" +"Project-Id-Version: Drupal 7\\n" +"MIME-Version: 1.0\\n" +"Content-Type: text/plain; charset=UTF-8\\n" +"Content-Transfer-Encoding: 8bit\\n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\\n" + +msgid "1 hour" +msgid_plural "@count hours" +msgstr[0] "@count sat" +msgstr[1] "@count sata" +msgstr[2] "@count sati" + +msgid "Monday" +msgstr "Ponedjeljak" +EOF; } } @@ -614,16 +908,16 @@ )); // The import should automatically create the corresponding language. - $this->assertRaw(t('The language %language has been created.', array('%language' => 'French')), t('The language has been automatically created.')); + $this->assertRaw(t('The language %language has been created.', array('%language' => 'French')), 'The language has been automatically created.'); // The import should have created 7 strings. - $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 9, '%update' => 0, '%delete' => 0)), t('The translation file was successfully imported.')); + $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 9, '%update' => 0, '%delete' => 0)), 'The translation file was successfully imported.'); // This import should have saved plural forms to have 2 variants. - $this->assert(db_query("SELECT plurals FROM {languages} WHERE language = 'fr'")->fetchField() == 2, t('Plural number initialized.')); + $this->assert(db_query("SELECT plurals FROM {languages} WHERE language = 'fr'")->fetchField() == 2, 'Plural number initialized.'); // Ensure we were redirected correctly. - $this->assertEqual($this->getUrl(), url('admin/config/regional/translate', array('absolute' => TRUE)), t('Correct page redirection.')); + $this->assertEqual($this->getUrl(), url('admin/config/regional/translate', array('absolute' => TRUE)), 'Correct page redirection.'); // Try importing a .po file with invalid tags in the default text group. @@ -632,9 +926,9 @@ )); // The import should have created 1 string and rejected 2. - $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 1, '%update' => 0, '%delete' => 0)), t('The translation file was successfully imported.')); + $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 1, '%update' => 0, '%delete' => 0)), 'The translation file was successfully imported.'); $skip_message = format_plural(2, 'One translation string was skipped because it contains disallowed HTML.', '@count translation strings were skipped because they contain disallowed HTML.'); - $this->assertRaw($skip_message, t('Unsafe strings were skipped.')); + $this->assertRaw($skip_message, 'Unsafe strings were skipped.'); // Try importing a .po file with invalid tags in a non default text group. @@ -644,7 +938,7 @@ )); // The import should have created 3 strings. - $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 3, '%update' => 0, '%delete' => 0)), t('The translation file was successfully imported.')); + $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 3, '%update' => 0, '%delete' => 0)), 'The translation file was successfully imported.'); // Try importing a .po file which doesn't exist. @@ -654,8 +948,8 @@ 'files[file]' => $name, 'group' => 'custom', ), t('Import')); - $this->assertEqual($this->getUrl(), url('admin/config/regional/translate/import', array('absolute' => TRUE)), t('Correct page redirection.')); - $this->assertText(t('File to import not found.'), t('File to import not found message.')); + $this->assertEqual($this->getUrl(), url('admin/config/regional/translate/import', array('absolute' => TRUE)), 'Correct page redirection.'); + $this->assertText(t('File to import not found.'), 'File to import not found message.'); // Try importing a .po file with overriding strings, and ensure existing @@ -666,7 +960,7 @@ )); // The import should have created 1 string. - $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 1, '%update' => 0, '%delete' => 0)), t('The translation file was successfully imported.')); + $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 1, '%update' => 0, '%delete' => 0)), 'The translation file was successfully imported.'); // Ensure string wasn't overwritten. $search = array( 'string' => 'Montag', @@ -675,10 +969,29 @@ 'group' => 'all', ); $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); - $this->assertText(t('No strings available.'), t('String not overwritten by imported string.')); + $this->assertText(t('No strings available.'), 'String not overwritten by imported string.'); // This import should not have changed number of plural forms. - $this->assert(db_query("SELECT plurals FROM {languages} WHERE language = 'fr'")->fetchField() == 2, t('Plural numbers untouched.')); + $this->assert(db_query("SELECT plurals FROM {languages} WHERE language = 'fr'")->fetchField() == 2, 'Plural numbers untouched.'); + + $this->importPoFile($this->getPoFileWithBrokenPlural(), array( + 'langcode' => 'fr', + 'mode' => 1, // Existing strings are kept, only new strings are added. + )); + + // Attempt to import broken .po file as well to prove that this + // will not overwrite the proper plural formula imported above. + $this->assert(db_query("SELECT plurals FROM {languages} WHERE language = 'fr'")->fetchField() == 2, 'Broken plurals: plural numbers untouched.'); + + $this->importPoFile($this->getPoFileWithMissingPlural(), array( + 'langcode' => 'fr', + 'mode' => 1, // Existing strings are kept, only new strings are added. + )); + + // Attempt to import .po file which has no plurals and prove that this + // will not overwrite the proper plural formula imported above. + $this->assert(db_query("SELECT plurals FROM {languages} WHERE language = 'fr'")->fetchField() == 2, 'No plurals: plural numbers untouched.'); + // Try importing a .po file with overriding strings, and ensure existing // strings are overwritten. @@ -688,7 +1001,7 @@ )); // The import should have updated 2 strings. - $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 0, '%update' => 2, '%delete' => 0)), t('The translation file was successfully imported.')); + $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 0, '%update' => 2, '%delete' => 0)), 'The translation file was successfully imported.'); // Ensure string was overwritten. $search = array( 'string' => 'Montag', @@ -697,9 +1010,9 @@ 'group' => 'all', ); $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); - $this->assertNoText(t('No strings available.'), t('String overwritten by imported string.')); + $this->assertNoText(t('No strings available.'), 'String overwritten by imported string.'); // This import should have changed number of plural forms. - $this->assert(db_query("SELECT plurals FROM {languages} WHERE language = 'fr'")->fetchField() == 3, t('Plural numbers changed.')); + $this->assert(db_query("SELECT plurals FROM {languages} WHERE language = 'fr'")->fetchField() == 3, 'Plural numbers changed.'); } /** @@ -728,7 +1041,7 @@ // Ensure the translation file was automatically imported when language was // added. - $this->assertText(t('One translation file imported for the enabled modules.'), t('Language file automatically imported.')); + $this->assertText(t('One translation file imported for the enabled modules.'), 'Language file automatically imported.'); // Ensure strings were successfully imported. $search = array( @@ -738,7 +1051,7 @@ 'group' => 'all', ); $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); - $this->assertNoText(t('No strings available.'), t('String successfully imported.')); + $this->assertNoText(t('No strings available.'), 'String successfully imported.'); } /** @@ -750,8 +1063,8 @@ 'langcode' => 'hr', )); - $this->assertIdentical(t('May', array(), array('langcode' => 'hr', 'context' => 'Long month name')), 'Svibanj', t('Long month name context is working.')); - $this->assertIdentical(t('May', array(), array('langcode' => 'hr')), 'Svi.', t('Default context is working.')); + $this->assertIdentical(t('May', array(), array('langcode' => 'hr', 'context' => 'Long month name')), 'Svibanj', 'Long month name context is working.'); + $this->assertIdentical(t('May', array(), array('langcode' => 'hr')), 'Svi.', 'Default context is working.'); } /** @@ -765,15 +1078,15 @@ 'langcode' => $langcode, )); - $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 1, '%update' => 0, '%delete' => 0)), t('The translation file was successfully imported.')); - $this->assertIdentical(t('Operations', array(), array('langcode' => $langcode)), 'Műveletek', t('String imported and translated.')); + $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 1, '%update' => 0, '%delete' => 0)), 'The translation file was successfully imported.'); + $this->assertIdentical(t('Operations', array(), array('langcode' => $langcode)), 'Műveletek', 'String imported and translated.'); // Try importing a .po file. $this->importPoFile($this->getPoFileWithEmptyMsgstr(), array( 'langcode' => $langcode, 'mode' => 0, )); - $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 0, '%update' => 0, '%delete' => 1)), t('The translation file was successfully imported.')); + $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 0, '%update' => 0, '%delete' => 1)), 'The translation file was successfully imported.'); // This is the language indicator on the translation search screen for // untranslated strings. Copied straight from locale.inc. $language_indicator = "$langcode "; @@ -787,8 +1100,8 @@ $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); // assertText() seems to remove the input field where $str always could be // found, so this is not a false assert. - $this->assertText($str, t('Search found the string.')); - $this->assertRaw($language_indicator, t('String is untranslated again.')); + $this->assertText($str, 'Search found the string.'); + $this->assertRaw($language_indicator, 'String is untranslated again.'); } /** @@ -800,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')); @@ -899,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 "" @@ -959,6 +1272,42 @@ EOF; } + + /** + * Returns a .po file with a missing plural formula. + */ + function getPoFileWithMissingPlural() { + return <<< EOF +msgid "" +msgstr "" +"Project-Id-Version: Drupal 7\\n" +"MIME-Version: 1.0\\n" +"Content-Type: text/plain; charset=UTF-8\\n" +"Content-Transfer-Encoding: 8bit\\n" + +msgid "Monday" +msgstr "Ponedjeljak" +EOF; + } + + /** + * Returns a .po file with a broken plural formula. + */ + function getPoFileWithBrokenPlural() { + return <<< EOF +msgid "" +msgstr "" +"Project-Id-Version: Drupal 7\\n" +"MIME-Version: 1.0\\n" +"Content-Type: text/plain; charset=UTF-8\\n" +"Content-Transfer-Encoding: 8bit\\n" +"Plural-Forms: broken, will not parse\\n" + +msgid "Monday" +msgstr "lundi" +EOF; + } + } /** @@ -991,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', @@ -1005,9 +1354,9 @@ ), t('Export')); // Ensure we have a translation file. - $this->assertRaw('# French translation of Drupal', t('Exported French translation file.')); + $this->assertRaw('# French translation of Drupal', 'Exported French translation file.'); // Ensure our imported translations exist in the file. - $this->assertRaw('msgstr "lundi"', t('French translations present in exported file.')); + $this->assertRaw('msgstr "lundi"', 'French translations present in exported file.'); } /** @@ -1020,7 +1369,7 @@ // doesn't work. $this->drupalPost('admin/config/regional/translate/export', array(), t('Export')); // Ensure we have a translation file. - $this->assertRaw('# LANGUAGE translation of PROJECT', t('Exported translation template file.')); + $this->assertRaw('# LANGUAGE translation of PROJECT', 'Exported translation template file.'); } /** @@ -1068,7 +1417,7 @@ function testFunctionSignatures() { $reflector_t = new ReflectionFunction('t'); $reflector_st = new ReflectionFunction('st'); - $this->assertEqual($reflector_t->getParameters(), $reflector_st->getParameters(), t('Function signatures of t() and st() are equal.')); + $this->assertEqual($reflector_t->getParameters(), $reflector_st->getParameters(), 'Function signatures of t() and st() are equal.'); } } @@ -1107,7 +1456,7 @@ // Check the UI language. drupal_language_initialize(); global $language; - $this->assertEqual($language->language, $this->language, t('Current language: %lang', array('%lang' => $language->language))); + $this->assertEqual($language->language, $this->language, format_string('Current language: %lang', array('%lang' => $language->language))); // Enable multilingual workflow option for articles. variable_set('language_content_type_article', 1); @@ -1128,7 +1477,7 @@ _locale_rebuild_js('fr'); $file = db_query('SELECT javascript FROM {languages} WHERE language = :language', array(':language' => 'fr'))->fetchObject(); $js_file = 'public://' . variable_get('locale_js_directory', 'languages') . '/fr_' . $file->javascript . '.js'; - $this->assertTrue($result = file_exists($js_file), t('JavaScript file created: %file', array('%file' => $result ? $js_file : t('none')))); + $this->assertTrue($result = file_exists($js_file), format_string('JavaScript file created: %file', array('%file' => $result ? $js_file : 'none'))); // Disable string caching. variable_set('locale_cache_strings', 0); @@ -1153,44 +1502,44 @@ // Check the init language logic. drupal_language_initialize(); - $this->assertEqual($language->language, 'en', t('Language after uninstall: %lang', array('%lang' => $language->language))); + $this->assertEqual($language->language, 'en', format_string('Language after uninstall: %lang', array('%lang' => $language->language))); // Check JavaScript files deletion. - $this->assertTrue($result = !file_exists($js_file), t('JavaScript file deleted: %file', array('%file' => $result ? $js_file : t('found')))); + $this->assertTrue($result = !file_exists($js_file), format_string('JavaScript file deleted: %file', array('%file' => $result ? $js_file : 'found'))); // Check language count. $language_count = variable_get('language_count', 1); - $this->assertEqual($language_count, 1, t('Language count: %count', array('%count' => $language_count))); + $this->assertEqual($language_count, 1, format_string('Language count: %count', array('%count' => $language_count))); // Check language negotiation. require_once DRUPAL_ROOT . '/includes/language.inc'; - $this->assertTrue(count(language_types()) == count(drupal_language_types()), t('Language types reset')); + $this->assertTrue(count(language_types()) == count(drupal_language_types()), 'Language types reset'); $language_negotiation = language_negotiation_get(LANGUAGE_TYPE_INTERFACE) == LANGUAGE_NEGOTIATION_DEFAULT; - $this->assertTrue($language_negotiation, t('Interface language negotiation: %setting', array('%setting' => t($language_negotiation ? 'none' : 'set')))); + $this->assertTrue($language_negotiation, format_string('Interface language negotiation: %setting', array('%setting' => $language_negotiation ? 'none' : 'set'))); $language_negotiation = language_negotiation_get(LANGUAGE_TYPE_CONTENT) == LANGUAGE_NEGOTIATION_DEFAULT; - $this->assertTrue($language_negotiation, t('Content language negotiation: %setting', array('%setting' => t($language_negotiation ? 'none' : 'set')))); + $this->assertTrue($language_negotiation, format_string('Content language negotiation: %setting', array('%setting' => $language_negotiation ? 'none' : 'set'))); $language_negotiation = language_negotiation_get(LANGUAGE_TYPE_URL) == LANGUAGE_NEGOTIATION_DEFAULT; - $this->assertTrue($language_negotiation, t('URL language negotiation: %setting', array('%setting' => t($language_negotiation ? 'none' : 'set')))); + $this->assertTrue($language_negotiation, format_string('URL language negotiation: %setting', array('%setting' => $language_negotiation ? 'none' : 'set'))); // Check language providers settings. - $this->assertFalse(variable_get('locale_language_negotiation_url_part', FALSE), t('URL language provider indicator settings cleared.')); - $this->assertFalse(variable_get('locale_language_negotiation_session_param', FALSE), t('Visit language provider settings cleared.')); + $this->assertFalse(variable_get('locale_language_negotiation_url_part', FALSE), 'URL language provider indicator settings cleared.'); + $this->assertFalse(variable_get('locale_language_negotiation_session_param', FALSE), 'Visit language provider settings cleared.'); // Check JavaScript parsed. $javascript_parsed_count = count(variable_get('javascript_parsed', array())); - $this->assertEqual($javascript_parsed_count, 0, t('JavaScript parsed count: %count', array('%count' => $javascript_parsed_count))); + $this->assertEqual($javascript_parsed_count, 0, format_string('JavaScript parsed count: %count', array('%count' => $javascript_parsed_count))); // Check multilingual workflow option for articles. $multilingual = variable_get('language_content_type_article', 0); - $this->assertEqual($multilingual, 0, t('Multilingual workflow option: %status', array('%status' => t($multilingual ? 'enabled': 'disabled')))); + $this->assertEqual($multilingual, 0, format_string('Multilingual workflow option: %status', array('%status' => $multilingual ? 'enabled': 'disabled'))); // Check JavaScript translations directory. $locale_js_directory = variable_get('locale_js_directory', 'languages'); - $this->assertEqual($locale_js_directory, 'languages', t('JavaScript translations directory: %dir', array('%dir' => $locale_js_directory))); + $this->assertEqual($locale_js_directory, 'languages', format_string('JavaScript translations directory: %dir', array('%dir' => $locale_js_directory))); // Check string caching. $locale_cache_strings = variable_get('locale_cache_strings', 1); - $this->assertEqual($locale_cache_strings, 1, t('String caching: %status', array('%status' => t($locale_cache_strings ? 'enabled': 'disabled')))); + $this->assertEqual($locale_cache_strings, 1, format_string('String caching: %status', array('%status' => $locale_cache_strings ? 'enabled': 'disabled'))); } } @@ -1217,7 +1566,6 @@ } } - /** * Functional tests for the language switching feature. */ @@ -1262,7 +1610,7 @@ // Assert that the language switching block is displayed on the frontpage. $this->drupalGet(''); - $this->assertText(t('Languages'), t('Language switcher block found.')); + $this->assertText(t('Languages'), 'Language switcher block found.'); // Assert that only the current language is marked as active. list($language_switcher) = $this->xpath('//div[@id=:id]/div[@class="content"]', array(':id' => 'block-locale-' . $language_type)); @@ -1291,8 +1639,128 @@ $anchors['inactive'][] = $language; } } - $this->assertIdentical($links, array('active' => array('en'), 'inactive' => array('fr')), t('Only the current language list item is marked as active on the language switcher block.')); - $this->assertIdentical($anchors, array('active' => array('en'), 'inactive' => array('fr')), t('Only the current language anchor is marked as active on the language switcher block.')); + $this->assertIdentical($links, array('active' => array('en'), 'inactive' => array('fr')), 'Only the current language list item is marked as active on the language switcher block.'); + $this->assertIdentical($anchors, array('active' => array('en'), 'inactive' => array('fr')), 'Only the current language anchor is marked as active on the language switcher block.'); + } +} + +/** + * Test browser language detection. + */ +class LocaleBrowserDetectionTest extends DrupalUnitTestCase { + + public static function getInfo() { + return array( + 'name' => 'Browser language detection', + 'description' => 'Tests for the browser language detection.', + 'group' => 'Locale', + ); + } + + /** + * Unit tests for the locale_language_from_browser() function. + */ + function testLanguageFromBrowser() { + // Load the required functions. + require_once DRUPAL_ROOT . '/includes/locale.inc'; + + $languages = array( + // In our test case, 'en' has priority over 'en-US'. + 'en' => (object) array( + 'language' => 'en', + ), + 'en-US' => (object) array( + 'language' => 'en-US', + ), + // But 'fr-CA' has priority over 'fr'. + 'fr-CA' => (object) array( + 'language' => 'fr-CA', + ), + 'fr' => (object) array( + 'language' => 'fr', + ), + // 'es-MX' is alone. + 'es-MX' => (object) array( + 'language' => 'es-MX', + ), + // 'pt' is alone. + 'pt' => (object) array( + 'language' => 'pt', + ), + // Language codes with more then one dash are actually valid. + // eh-oh-laa-laa is the official language code of the Teletubbies. + 'eh-oh-laa-laa' => (object) array( + 'language' => 'eh-oh-laa-laa', + ), + ); + + $test_cases = array( + // 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', + 'en,fr' => 'en', + 'en-US,fr' => 'en', + 'fr,en-US' => 'en', + 'fr,fr-CA' => 'fr-CA', + 'fr-CA,fr' => 'fr-CA', + 'fr' => 'fr-CA', + 'fr;q=1' => 'fr-CA', + 'fr,es-MX' => 'fr-CA', + 'fr,es' => 'fr-CA', + 'es,fr' => 'fr-CA', + 'es-MX,de' => 'es-MX', + 'de,es-MX' => 'es-MX', + + // Different cases and whitespace. + 'en' => 'en', + 'En' => 'en', + 'EN' => 'en', + ' en' => 'en', + 'en ' => 'en', + 'en, fr' => 'en', + + // A less specific language from the browser matches a more specific one + // from the website, and the other way around for compatibility with + // some versions of Internet Explorer. + 'es' => 'es-MX', + 'es-MX' => 'es-MX', + 'pt' => 'pt', + 'pt-PT' => 'pt', + 'pt-PT;q=0.5,pt-BR;q=1,en;q=0.7' => 'en', + 'pt-PT;q=1,pt-BR;q=0.5,en;q=0.7' => 'en', + 'pt-PT;q=0.4,pt-BR;q=0.1,en;q=0.7' => 'en', + 'pt-PT;q=0.1,pt-BR;q=0.4,en;q=0.7' => 'en', + + // Language code with several dashes are valid. The less specific language + // from the browser matches the more specific one from the website. + 'eh-oh-laa-laa' => 'eh-oh-laa-laa', + 'eh-oh-laa' => 'eh-oh-laa-laa', + 'eh-oh' => 'eh-oh-laa-laa', + 'eh' => 'eh-oh-laa-laa', + + // Different qvalues. + 'fr,en;q=0.5' => 'fr-CA', + 'fr,en;q=0.5,fr-CA;q=0.25' => 'fr', + + // Silly wildcards are also valid. + '*,fr-CA;q=0.5' => 'en', + '*,en;q=0.25' => 'fr-CA', + 'en,en-US;q=0.5,fr;q=0.25' => 'en', + 'en-US,en;q=0.5,fr;q=0.25' => 'en-US', + + // Unresolvable cases. + '' => FALSE, + 'de,pl' => FALSE, + 'iecRswK4eh' => FALSE, + $this->randomName(10) => FALSE, + ); + + foreach ($test_cases as $accept_language => $expected_result) { + $_SERVER['HTTP_ACCEPT_LANGUAGE'] = $accept_language; + $result = locale_language_from_browser($languages); + $this->assertIdentical($result, $expected_result, format_string("Language selection '@accept-language' selects '@result', result = '@actual'", array('@accept-language' => $accept_language, '@result' => $expected_result, '@actual' => isset($result) ? $result : 'none'))); + } } } @@ -1371,21 +1839,21 @@ $path = 'user/' . $web_user->uid . '/edit'; $this->drupalGet($path); // Ensure language settings fieldset is available. - $this->assertText(t('Language settings'), t('Language settings available.')); + $this->assertText(t('Language settings'), 'Language settings available.'); // Ensure custom language is present. - $this->assertText($name, t('Language present on form.')); + $this->assertText($name, 'Language present on form.'); // Ensure disabled language isn't present. - $this->assertNoText($name_disabled, t('Disabled language not present on form.')); + $this->assertNoText($name_disabled, 'Disabled language not present on form.'); // Switch to our custom language. $edit = array( 'language' => $langcode, ); $this->drupalPost($path, $edit, t('Save')); // Ensure form was submitted successfully. - $this->assertText(t('The changes have been saved.'), t('Changes were saved.')); + $this->assertText(t('The changes have been saved.'), 'Changes were saved.'); // Check if language was changed. $elements = $this->xpath('//input[@id=:id]', array(':id' => 'edit-language-' . $langcode)); - $this->assertTrue(isset($elements[0]) && !empty($elements[0]['checked']), t('Default language successfully updated.')); + $this->assertTrue(isset($elements[0]) && !empty($elements[0]['checked']), 'Default language successfully updated.'); $this->drupalLogout(); } @@ -1423,20 +1891,20 @@ 'langcode' => 'fr', ); $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language')); - $this->assertText($langcode, t('Language added successfully.')); - $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), t('Correct page redirection.')); + $this->assertText($langcode, 'Language added successfully.'); + $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), 'Correct page redirection.'); // Set language negotiation. $edit = array( 'language[enabled][locale-url]' => TRUE, ); $this->drupalPost('admin/config/regional/language/configure', $edit, t('Save settings')); - $this->assertText(t('Language negotiation configuration saved.'), t('Set language negotiation.')); + $this->assertText(t('Language negotiation configuration saved.'), 'Set language negotiation.'); // Check if the language selector is available on admin/people/create and // set to the currently active language. $this->drupalGet($langcode . '/admin/people/create'); - $this->assertFieldChecked("edit-language-$langcode", t('Global language set in the language selector.')); + $this->assertFieldChecked("edit-language-$langcode", 'Global language set in the language selector.'); // Create a user with the admin/people/create form and check if the correct // language is set. @@ -1451,13 +1919,13 @@ $this->drupalPost($langcode . '/admin/people/create', $edit, t('Create new account')); $user = user_load_by_name($username); - $this->assertEqual($user->language, $langcode, t('New user has correct language set.')); + $this->assertEqual($user->language, $langcode, 'New user has correct language set.'); // Register a new user and check if the language selector is hidden. $this->drupalLogout(); $this->drupalGet($langcode . '/user/register'); - $this->assertNoFieldByName('language[fr]', t('Language selector is not accessible.')); + $this->assertNoFieldByName('language[fr]', 'Language selector is not accessible.'); $username = $this->randomName(10); $edit = array( @@ -1468,7 +1936,7 @@ $this->drupalPost($langcode . '/user/register', $edit, t('Create new account')); $user = user_load_by_name($username); - $this->assertEqual($user->language, $langcode, t('New user has correct language set.')); + $this->assertEqual($user->language, $langcode, 'New user has correct language set.'); // Test if the admin can use the language selector and if the // correct language is was saved. @@ -1476,7 +1944,7 @@ $this->drupalLogin($admin_user); $this->drupalGet($user_edit); - $this->assertFieldChecked("edit-language-$langcode", t('Language selector is accessible and correct language is selected.')); + $this->assertFieldChecked("edit-language-$langcode", 'Language selector is accessible and correct language is selected.'); // Set pass_raw so we can login the new user. $user->pass_raw = $this->randomName(10); @@ -1489,7 +1957,7 @@ $this->drupalLogin($user); $this->drupalGet($user_edit); - $this->assertFieldChecked("edit-language-$langcode", t('Language selector is accessible and correct language is selected.')); + $this->assertFieldChecked("edit-language-$langcode", 'Language selector is accessible and correct language is selected.'); } } @@ -1500,7 +1968,7 @@ public static function getInfo() { return array( 'name' => 'Path language settings', - 'description' => 'Checks you can configure a language for individual url aliases.', + 'description' => 'Checks you can configure a language for individual URL aliases.', 'group' => 'Locale', ); } @@ -1541,7 +2009,7 @@ // not enabled yet. $this->drupalPost('admin/config/regional/language/configure', array(), t('Save settings')); $this->drupalGet($prefix); - $this->assertResponse(404, t('The "xx" front page is not available yet.')); + $this->assertResponse(404, 'The "xx" front page is not available yet.'); // Enable URL language detection and selection. $edit = array('language[enabled][locale-url]' => 1); @@ -1571,11 +2039,11 @@ // Confirm English language path alias works. $this->drupalGet($english_path); - $this->assertText($node->title, t('English alias works.')); + $this->assertText($node->title, 'English alias works.'); // Confirm custom language path alias works. $this->drupalGet($prefix . '/' . $custom_language_path); - $this->assertText($node->title, t('Custom language alias works.')); + $this->assertText($node->title, 'Custom language alias works.'); // Create a custom path. $custom_path = $this->randomName(8); @@ -1588,10 +2056,10 @@ ); path_save($edit); $lookup_path = drupal_lookup_path('alias', 'node/' . $node->nid, 'en'); - $this->assertEqual($english_path, $lookup_path, t('English language alias has priority.')); + $this->assertEqual($english_path, $lookup_path, 'English language alias has priority.'); // Same check for language 'xx'. $lookup_path = drupal_lookup_path('alias', 'node/' . $node->nid, $prefix); - $this->assertEqual($custom_language_path, $lookup_path, t('Custom language alias has priority.')); + $this->assertEqual($custom_language_path, $lookup_path, 'Custom language alias has priority.'); path_delete($edit); // Create language nodes to check priority of aliases. @@ -1618,17 +2086,17 @@ $this->drupalGet(''); $custom_path_url = base_path() . (variable_get('clean_url', 0) ? $custom_path : '?q=' . $custom_path); $elements = $this->xpath('//a[@href=:href and .=:title]', array(':href' => $custom_path_url, ':title' => $first_node->title)); - $this->assertTrue(!empty($elements), t('First node links to the path alias.')); + $this->assertTrue(!empty($elements), 'First node links to the path alias.'); $elements = $this->xpath('//a[@href=:href and .=:title]', array(':href' => $custom_path_url, ':title' => $second_node->title)); - $this->assertTrue(!empty($elements), t('Second node links to the path alias.')); + $this->assertTrue(!empty($elements), 'Second node links to the path alias.'); // Confirm that the custom path leads to the first node. $this->drupalGet($custom_path); - $this->assertText($first_node->title, t('Custom alias returns first node.')); + $this->assertText($first_node->title, 'Custom alias returns first node.'); // Confirm that the custom path with prefix leads to the second node. $this->drupalGet($prefix . '/' . $custom_path); - $this->assertText($second_node->title, t('Custom alias with prefix returns second node.')); + $this->assertText($second_node->title, 'Custom alias with prefix returns second node.'); } } @@ -1649,6 +2117,34 @@ } /** + * Verifies that machine name fields are always LTR. + */ + function testMachineNameLTR() { + // User to add and remove language. + $admin_user = $this->drupalCreateUser(array('administer languages', 'administer content types', 'access administration pages')); + + // Log in as admin. + $this->drupalLogin($admin_user); + + // Verify that the machine name field is LTR for a new content type. + $this->drupalGet('admin/structure/types/add'); + $this->assertFieldByXpath('//input[@name="type" and @dir="ltr"]', NULL, 'The machine name field is LTR when no additional language is configured.'); + + // Install the Arabic language (which is RTL) and configure as the default. + $edit = array(); + $edit['langcode'] = 'ar'; + $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language')); + + $edit = array(); + $edit['site_default'] = 'ar'; + $this->drupalPost(NULL, $edit, t('Save configuration')); + + // Verify that the machine name field is still LTR for a new content type. + $this->drupalGet('admin/structure/types/add'); + $this->assertFieldByXpath('//input[@name="type" and @dir="ltr"]', NULL, 'The machine name field is LTR when the default language is RTL.'); + } + + /** * Test if a content type can be set to multilingual and language setting is * present on node add and edit forms. */ @@ -1705,28 +2201,28 @@ // Set "Basic page" content type to use multilingual support. $this->drupalGet('admin/structure/types/manage/page'); - $this->assertText(t('Multilingual support'), t('Multilingual support fieldset present on content type configuration form.')); + $this->assertText(t('Multilingual support'), 'Multilingual support fieldset present on content type configuration form.'); $edit = array( 'language_content_type' => 1, ); $this->drupalPost('admin/structure/types/manage/page', $edit, t('Save content type')); - $this->assertRaw(t('The content type %type has been updated.', array('%type' => 'Basic page')), t('Basic page content type has been updated.')); + $this->assertRaw(t('The content type %type has been updated.', array('%type' => 'Basic page')), 'Basic page content type has been updated.'); $this->drupalLogout(); // Verify language selection is not present on add article form. $this->drupalLogin($web_user); $this->drupalGet('node/add/article'); // Verify language select list is not present. - $this->assertNoFieldByName('language', NULL, t('Language select not present on add article form.')); + $this->assertNoFieldByName('language', NULL, 'Language select not present on add article form.'); // Verify language selection appears on add "Basic page" form. $this->drupalGet('node/add/page'); // Verify language select list is present. - $this->assertFieldByName('language', NULL, t('Language select present on add Basic page form.')); + $this->assertFieldByName('language', NULL, 'Language select present on add Basic page form.'); // Ensure enabled language appears. - $this->assertText($name, t('Enabled language present.')); + $this->assertText($name, 'Enabled language present.'); // Ensure disabled language doesn't appear. - $this->assertNoText($name_disabled, t('Disabled language not present.')); + $this->assertNoText($name_disabled, 'Disabled language not present.'); // Create "Basic page" content. $node_title = $this->randomName(); @@ -1741,16 +2237,47 @@ // Edit the content and ensure correct language is selected. $path = 'node/' . $node->nid . '/edit'; $this->drupalGet($path); - $this->assertRaw('', t('Correct language selected.')); + $this->assertRaw('', 'Correct language selected.'); // Ensure we can change the node language. $edit = array( 'language' => 'en', ); $this->drupalPost($path, $edit, t('Save')); - $this->assertRaw(t('%title has been updated.', array('%title' => $node_title)), t('Basic page content updated.')); + $this->assertRaw(t('%title has been updated.', array('%title' => $node_title)), 'Basic page content updated.'); $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))); + } + } + } /** @@ -1782,7 +2309,7 @@ public static function getInfo() { return array( 'name' => 'UI language negotiation', - 'description' => 'Test UI language switching by url path prefix and domain.', + 'description' => 'Test UI language switching by URL path prefix and domain.', 'group' => 'Locale', ); } @@ -1858,6 +2385,7 @@ 'language_negotiation' => array(LOCALE_LANGUAGE_NEGOTIATION_URL, LANGUAGE_NEGOTIATION_DEFAULT), 'path' => 'admin/config', 'expect' => $default_string, + 'expected_provider' => LANGUAGE_NEGOTIATION_DEFAULT, 'http_header' => $http_header_browser_fallback, 'message' => 'URL (PATH) > DEFAULT: no language prefix, UI language is default and the browser language preference setting is not used.', ), @@ -1866,6 +2394,7 @@ 'language_negotiation' => array(LOCALE_LANGUAGE_NEGOTIATION_URL, LANGUAGE_NEGOTIATION_DEFAULT), 'path' => "$language/admin/config", 'expect' => $language_string, + 'expected_provider' => LOCALE_LANGUAGE_NEGOTIATION_URL, 'http_header' => $http_header_browser_fallback, 'message' => 'URL (PATH) > DEFAULT: with language prefix, UI language is switched based on path prefix', ), @@ -1874,6 +2403,7 @@ 'language_negotiation' => array(LOCALE_LANGUAGE_NEGOTIATION_URL, LOCALE_LANGUAGE_NEGOTIATION_BROWSER), 'path' => 'admin/config', 'expect' => $language_browser_fallback_string, + 'expected_provider' => LOCALE_LANGUAGE_NEGOTIATION_BROWSER, 'http_header' => $http_header_browser_fallback, 'message' => 'URL (PATH) > BROWSER: no language prefix, UI language is determined by browser language preference', ), @@ -1882,6 +2412,7 @@ 'language_negotiation' => array(LOCALE_LANGUAGE_NEGOTIATION_URL, LOCALE_LANGUAGE_NEGOTIATION_BROWSER), 'path' => "$language/admin/config", 'expect' => $language_string, + 'expected_provider' => LOCALE_LANGUAGE_NEGOTIATION_URL, 'http_header' => $http_header_browser_fallback, 'message' => 'URL (PATH) > BROWSER: with langage prefix, UI language is based on path prefix', ), @@ -1890,6 +2421,7 @@ 'language_negotiation' => array(LOCALE_LANGUAGE_NEGOTIATION_URL, LOCALE_LANGUAGE_NEGOTIATION_BROWSER, LANGUAGE_NEGOTIATION_DEFAULT), 'path' => 'admin/config', 'expect' => $default_string, + 'expected_provider' => LANGUAGE_NEGOTIATION_DEFAULT, 'http_header' => $http_header_blah, 'message' => 'URL (PATH) > BROWSER > DEFAULT: no language prefix and browser language preference set to unknown language should use default language', ), @@ -1905,8 +2437,8 @@ $this->assertResponse(404, "Unknown language path prefix should return 404"); // Setup for domain negotiation, first configure the language to have domain - // URL. - $edit = array('prefix' => '', 'domain' => "http://$language_domain"); + // URL. We use HTTPS and a port to make sure that only the domain name is used. + $edit = array('prefix' => '', 'domain' => "https://$language_domain:99"); $this->drupalPost("admin/config/regional/language/edit/$language", $edit, t('Save language')); // Set the site to use domain language negotiation. @@ -1917,6 +2449,7 @@ 'locale_language_negotiation_url_part' => LOCALE_LANGUAGE_NEGOTIATION_URL_DOMAIN, 'path' => 'admin/config', 'expect' => $default_string, + 'expected_provider' => LANGUAGE_NEGOTIATION_DEFAULT, 'http_header' => $http_header_browser_fallback, 'message' => 'URL (DOMAIN) > DEFAULT: default domain should get default language', ), @@ -1925,9 +2458,10 @@ array( 'language_negotiation' => array(LOCALE_LANGUAGE_NEGOTIATION_URL, LANGUAGE_NEGOTIATION_DEFAULT), 'locale_language_negotiation_url_part' => LOCALE_LANGUAGE_NEGOTIATION_URL_DOMAIN, - 'locale_test_domain' => $language_domain, + 'locale_test_domain' => $language_domain . ':88', 'path' => 'admin/config', 'expect' => $language_string, + 'expected_provider' => LOCALE_LANGUAGE_NEGOTIATION_URL, 'http_header' => $http_header_browser_fallback, 'message' => 'URL (DOMAIN) > DEFAULT: domain example.cn should switch to Chinese', ), @@ -1951,6 +2485,7 @@ } $this->drupalGet($test['path'], array(), $test['http_header']); $this->assertText($test['expect'], $test['message']); + $this->assertText(t('Language negotiation provider: @name', array('@name' => $test['expected_provider']))); } /** @@ -1990,11 +2525,61 @@ // language. $args = array(':url' => base_path() . (!empty($GLOBALS['conf']['clean_url']) ? $language_browser_fallback : "?q=$language_browser_fallback")); $fields = $this->xpath('//div[@id="block-locale-language"]//a[@class="language-link active" and @href=:url]', $args); - $this->assertTrue($fields[0] == $languages[$language_browser_fallback]->native, t('The browser language is the URL active language')); + $this->assertTrue($fields[0] == $languages[$language_browser_fallback]->native, 'The browser language is the URL active language'); // Check that URLs are rewritten using the given browser language. $fields = $this->xpath('//div[@id="site-name"]//a[@rel="home" and @href=:url]//span', $args); - $this->assertTrue($fields[0] == 'Drupal', t('URLs are rewritten using the browser language.')); + $this->assertTrue($fields[0] == 'Drupal', 'URLs are rewritten using the browser language.'); + } + + /** + * Tests url() when separate domains are used for multiple languages. + */ + function testLanguageDomain() { + // Add the Italian language, without protocol. + $langcode = 'it'; + locale_add_language($langcode, 'Italian', 'Italian', LANGUAGE_LTR, 'it.example.com', '', TRUE, FALSE); + + // Add the French language, with protocol. + $langcode = 'fr'; + locale_add_language($langcode, 'French', 'French', LANGUAGE_LTR, 'http://fr.example.com', '', TRUE, FALSE); + + // Enable language URL detection. + $negotiation = array_flip(array(LOCALE_LANGUAGE_NEGOTIATION_URL, LANGUAGE_NEGOTIATION_DEFAULT)); + language_negotiation_set(LANGUAGE_TYPE_INTERFACE, $negotiation); + + variable_set('locale_language_negotiation_url_part', 1); + + global $is_https; + $languages = language_list(); + + foreach (array('it', 'fr') as $langcode) { + // Build the link we're going to test based on the clean URL setting. + $link = (!empty($GLOBALS['conf']['clean_url'])) ? $langcode . '.example.com/admin' : $langcode . '.example.com/?q=admin'; + + // Test URL in another language. + // Base path gives problems on the testbot, so $correct_link is hard-coded. + // @see UrlAlterFunctionalTest::assertUrlOutboundAlter (path.test). + $url = url('admin', array('language' => $languages[$langcode])); + $url_scheme = ($is_https) ? 'https://' : 'http://'; + $correct_link = $url_scheme . $link; + $this->assertTrue($url == $correct_link, format_string('The url() function returns the right url (@url) in accordance with the chosen language', array('@url' => $url . " == " . $correct_link))); + + // Test HTTPS via options. + variable_set('https', TRUE); + $url = url('admin', array('https' => TRUE, 'language' => $languages[$langcode])); + $correct_link = 'https://' . $link; + $this->assertTrue($url == $correct_link, format_string('The url() function returns the right https url (via options) (@url) in accordance with the chosen language', array('@url' => $url . " == " . $correct_link))); + variable_set('https', FALSE); + + // Test HTTPS via current URL scheme. + $temp_https = $is_https; + $is_https = TRUE; + $url = url('admin', array('language' => $languages[$langcode])); + $correct_link = 'https://' . $link; + $this->assertTrue($url == $correct_link, format_string('The url() function returns the right url (via current url scheme) (@url) in accordance with the chosen language', array('@url' => $url . " == " . $correct_link))); + $is_https = $temp_https; + } } } @@ -2047,13 +2632,13 @@ function testUrlRewritingEdgeCases() { // Check URL rewriting with a disabled language. $languages = language_list(); - $this->checkUrl($languages['it'], t('Path language is ignored if language is disabled.'), t('URL language negotiation does not work with disabled languages')); + $this->checkUrl($languages['it'], 'Path language is ignored if language is disabled.', 'URL language negotiation does not work with disabled languages'); // Check URL rewriting with a non-installed language. $non_existing = language_default(); $non_existing->language = $this->randomName(); $non_existing->prefix = $this->randomName(); - $this->checkUrl($non_existing, t('Path language is ignored if language is not installed.'), t('URL language negotiation does not work with non-installed languages')); + $this->checkUrl($non_existing, 'Path language is ignored if language is not installed.', 'URL language negotiation does not work with non-installed languages'); } /** @@ -2062,6 +2647,13 @@ * The test is performed with a fixed URL (the default front page) to simply * check that language prefixes are not added to it and that the prefixed URL * is actually not working. + * + * @param string $language + * The language prefix, e.g. 'es'. + * @param string $message1 + * Message to display in assertion that language prefixes are not added. + * @param string $message2 + * The message to display confirming prefixed URL is not working. */ private function checkUrl($language, $message1, $message2) { $options = array('language' => $language); @@ -2078,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; + } } /** @@ -2111,7 +2765,12 @@ 'language_content_type' => 1, ); $this->drupalPost('admin/structure/types/manage/page', $edit, t('Save content type')); - $this->assertRaw(t('The content type %type has been updated.', array('%type' => 'Basic page')), t('Basic page content type has been updated.')); + $this->assertRaw(t('The content type %type has been updated.', array('%type' => 'Basic page')), 'Basic page content type has been updated.'); + + // Make node body translatable. + $field = field_info_field('body'); + $field['translatable'] = TRUE; + field_update_field($field); } /** @@ -2134,10 +2793,10 @@ // Check that the node exists in the database. $node = $this->drupalGetNodeByTitle($edit[$title_key]); - $this->assertTrue($node, t('Node found in database.')); + $this->assertTrue($node, 'Node found in database.'); $assert = isset($node->body['en']) && !isset($node->body[LANGUAGE_NONE]) && $node->body['en'][0]['value'] == $body_value; - $this->assertTrue($assert, t('Field language correctly set.')); + $this->assertTrue($assert, 'Field language correctly set.'); // Change node language. $this->drupalGet("node/$node->nid/edit"); @@ -2147,20 +2806,20 @@ ); $this->drupalPost(NULL, $edit, t('Save')); $node = $this->drupalGetNodeByTitle($edit[$title_key]); - $this->assertTrue($node, t('Node found in database.')); + $this->assertTrue($node, 'Node found in database.'); $assert = isset($node->body['it']) && !isset($node->body['en']) && $node->body['it'][0]['value'] == $body_value; - $this->assertTrue($assert, t('Field language correctly changed.')); + $this->assertTrue($assert, 'Field language correctly changed.'); // Enable content language URL detection. language_negotiation_set(LANGUAGE_TYPE_CONTENT, array(LOCALE_LANGUAGE_NEGOTIATION_URL => 0)); // Test multilingual field language fallback logic. $this->drupalGet("it/node/$node->nid"); - $this->assertRaw($body_value, t('Body correctly displayed using Italian as requested language')); + $this->assertRaw($body_value, 'Body correctly displayed using Italian as requested language'); $this->drupalGet("node/$node->nid"); - $this->assertRaw($body_value, t('Body correctly displayed using English as requested language')); + $this->assertRaw($body_value, 'Body correctly displayed using English as requested language'); } /* @@ -2183,7 +2842,7 @@ // Check that the node exists in the database. $node = $this->drupalGetNodeByTitle($edit[$title_key]); - $this->assertTrue($node, t('Node found in database.')); + $this->assertTrue($node, 'Node found in database.'); // Check if node body is showed. $this->drupalGet("node/$node->nid"); @@ -2209,7 +2868,7 @@ parent::setUp('locale', 'locale_test'); // Create and login user. - $admin_user = $this->drupalCreateUser(array('administer site configuration', 'administer languages', 'access administration pages', 'administer content types', 'create article content')); + $admin_user = $this->drupalCreateUser(array('administer site configuration', 'administer languages', 'access administration pages', 'administer content types', 'administer comments', 'create article content')); $this->drupalLogin($admin_user); // Add language. @@ -2224,10 +2883,13 @@ variable_set('locale_test_content_language_type', TRUE); // Set interface language detection to user and content language detection - // to URL. + // to URL. Disable inheritance from interface language to ensure content + // language will fall back to the default language if no URL language can be + // detected. $edit = array( 'language[enabled][locale-user]' => TRUE, - 'language_content[enabled][locale-url]' => TRUE + 'language_content[enabled][locale-url]' => TRUE, + 'language_content[enabled][locale-interface]' => FALSE, ); $this->drupalPost('admin/config/regional/language/configure', $edit, t('Save settings')); @@ -2235,6 +2897,12 @@ // French no matter what path prefix the URLs have. $edit = array('language' => 'fr'); $this->drupalPost("user/{$admin_user->uid}/edit", $edit, t('Save')); + + // Make comment body translatable. + $field = field_info_field('comment_body'); + $field['translatable'] = TRUE; + field_update_field($field); + $this->assertTrue(field_is_translatable('comment', $field), 'Comment body is translatable.'); } /** @@ -2265,22 +2933,48 @@ foreach (language_list() as $langcode => $language) { // Post a comment with content language $langcode. $prefix = empty($language->prefix) ? '' : $language->prefix . '/'; - $edit = array("comment_body[$language_none][0][value]" => $this->randomName()); - $this->drupalPost("{$prefix}node/{$node->nid}", $edit, t('Save')); + $comment_values[$node_langcode][$langcode] = $this->randomName(); + // Initially field form widgets have no language. + $edit = array( + 'subject' => $this->randomName(), + "comment_body[$language_none][0][value]" => $comment_values[$node_langcode][$langcode], + ); + $this->drupalPost("{$prefix}node/{$node->nid}", $edit, t('Preview')); + // After the first submit the submitted entity language is taken into + // account. + $edit = array( + 'subject' => $edit['subject'], + "comment_body[$langcode][0][value]" => $comment_values[$node_langcode][$langcode], + ); + $this->drupalPost(NULL, $edit, t('Save')); // Check that comment language matches the current content language. - $comment = db_select('comment', 'c') - ->fields('c') + $cid = db_select('comment', 'c') + ->fields('c', array('cid')) ->condition('nid', $node->nid) ->orderBy('cid', 'DESC') + ->range(0, 1) ->execute() - ->fetchObject(); - $args = array('%node_language' => $node_langcode, '%comment_language' => $comment->language, '%langcode' => $langcode); - $this->assertEqual($comment->language, $langcode, t('The comment posted with content language %langcode and belonging to the node with language %node_language has language %comment_language', $args)); + ->fetchField(); + $comment = comment_load($cid); + $comment_langcode = entity_language('comment', $comment); + $args = array('%node_language' => $node_langcode, '%comment_language' => $comment_langcode, '%langcode' => $langcode); + $this->assertEqual($comment_langcode, $langcode, format_string('The comment posted with content language %langcode and belonging to the node with language %node_language has language %comment_language', $args)); + $this->assertEqual($comment->comment_body[$langcode][0]['value'], $comment_values[$node_langcode][$langcode], 'Comment body correctly stored.'); + } + } + + // Check that comment bodies appear in the administration UI. + $this->drupalGet('admin/content/comment'); + foreach ($comment_values as $node_values) { + foreach ($node_values as $value) { + $this->assertRaw($value); } } } + } + /** * Functional tests for localizing date formats. */ @@ -2343,9 +3037,249 @@ // Configure format for the node posted date changes with the language. $this->drupalGet('node/' . $node->nid); $english_date = format_date($node->created, 'custom', 'j M Y'); - $this->assertText($english_date, t('English date format appears')); + $this->assertText($english_date, 'English date format appears'); $this->drupalGet('fr/node/' . $node->nid); $french_date = format_date($node->created, 'custom', 'd.m.Y'); - $this->assertText($french_date, t('French date format appears')); + $this->assertText($french_date, 'French date format appears'); + } +} + +/** + * Functional test for language types/negotiation info. + */ +class LocaleLanguageNegotiationInfoFunctionalTest extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Language negotiation info', + 'description' => 'Tests alterations to language types/negotiation info.', + 'group' => 'Locale', + ); + } + + function setUp() { + parent::setUp('locale'); + require_once DRUPAL_ROOT .'/includes/language.inc'; + $admin_user = $this->drupalCreateUser(array('administer languages', 'access administration pages', 'view the administration theme')); + $this->drupalLogin($admin_user); + $this->drupalPost('admin/config/regional/language/add', array('langcode' => 'it'), t('Add language')); + } + + /** + * Tests alterations to language types/negotiation info. + */ + function testInfoAlterations() { + // Enable language type/negotiation info alterations. + variable_set('locale_test_language_types', TRUE); + variable_set('locale_test_language_negotiation_info', TRUE); + $this->languageNegotiationUpdate(); + + // Check that fixed language types are properly configured without the need + // of saving the language negotiation settings. + $this->checkFixedLanguageTypes(); + + // Make the content language type configurable by updating the language + // negotiation settings with the proper flag enabled. + variable_set('locale_test_content_language_type', TRUE); + $this->languageNegotiationUpdate(); + $type = LANGUAGE_TYPE_CONTENT; + $language_types = variable_get('language_types', drupal_language_types()); + $this->assertTrue($language_types[$type], 'Content language type is configurable.'); + + // Enable some core and custom language providers. The test language type is + // supposed to be configurable. + $test_type = 'test_language_type'; + $provider = LOCALE_LANGUAGE_NEGOTIATION_INTERFACE; + $test_provider = 'test_language_provider'; + $form_field = $type . '[enabled]['. $provider .']'; + $edit = array( + $form_field => TRUE, + $type . '[enabled][' . $test_provider . ']' => TRUE, + $test_type . '[enabled][' . $test_provider . ']' => TRUE, + ); + $this->drupalPost('admin/config/regional/language/configure', $edit, t('Save settings')); + + // Remove the interface language provider by updating the language + // negotiation settings with the proper flag enabled. + variable_set('locale_test_language_negotiation_info_alter', TRUE); + $this->languageNegotiationUpdate(); + $negotiation = variable_get("language_negotiation_$type", array()); + $this->assertFalse(isset($negotiation[$provider]), 'Interface language provider removed from the stored settings.'); + $this->assertNoFieldByXPath("//input[@name=\"$form_field\"]", NULL, 'Interface language provider unavailable.'); + + // Check that type-specific language providers can be assigned only to the + // corresponding language types. + foreach (language_types_configurable() as $type) { + $form_field = $type . '[enabled][test_language_provider_ts]'; + if ($type == $test_type) { + $this->assertFieldByXPath("//input[@name=\"$form_field\"]", NULL, format_string('Type-specific test language provider available for %type.', array('%type' => $type))); + } + else { + $this->assertNoFieldByXPath("//input[@name=\"$form_field\"]", NULL, format_string('Type-specific test language provider unavailable for %type.', array('%type' => $type))); + } + } + + // Check language negotiation results. + $this->drupalGet(''); + $last = variable_get('locale_test_language_negotiation_last', array()); + foreach (language_types() as $type) { + $langcode = $last[$type]; + $value = $type == LANGUAGE_TYPE_CONTENT || strpos($type, 'test') !== FALSE ? 'it' : 'en'; + $this->assertEqual($langcode, $value, format_string('The negotiated language for %type is %language', array('%type' => $type, '%language' => $langcode))); + } + + // Disable locale_test and check that everything is set back to the original + // status. + $this->languageNegotiationUpdate('disable'); + + // Check that only the core language types are available. + foreach (language_types() as $type) { + $this->assertTrue(strpos($type, 'test') === FALSE, format_string('The %type language is still available', array('%type' => $type))); + } + + // Check that fixed language types are properly configured, even those + // previously set to configurable. + $this->checkFixedLanguageTypes(); + + // Check that unavailable language providers are not present in the + // negotiation settings. + $negotiation = variable_get("language_negotiation_$type", array()); + $this->assertFalse(isset($negotiation[$test_provider]), 'The disabled test language provider is not part of the content language negotiation settings.'); + + // Check that configuration page presents the correct options and settings. + $this->assertNoRaw(t('Test language detection'), 'No test language type configuration available.'); + $this->assertNoRaw(t('This is a test language provider'), 'No test language provider available.'); + } + + /** + * Update language types/negotiation information. + * + * Manually invoke locale_modules_enabled()/locale_modules_disabled() since + * they would not be invoked after enabling/disabling locale_test the first + * time. + */ + private function languageNegotiationUpdate($op = 'enable') { + static $last_op = NULL; + $modules = array('locale_test'); + + // Enable/disable locale_test only if we did not already before. + if ($last_op != $op) { + $function = "module_{$op}"; + $function($modules); + // Reset hook implementation cache. + module_implements(NULL, FALSE, TRUE); + } + + drupal_static_reset('language_types_info'); + drupal_static_reset('language_negotiation_info'); + $function = "locale_modules_{$op}d"; + if (function_exists($function)) { + $function($modules); + } + + $this->drupalGet('admin/config/regional/language/configure'); + } + + /** + * Check that language negotiation for fixed types matches the stored one. + */ + private function checkFixedLanguageTypes() { + drupal_static_reset('language_types_info'); + foreach (language_types_info() as $type => $info) { + if (isset($info['fixed'])) { + $negotiation = variable_get("language_negotiation_$type", array()); + $equal = array_keys($negotiation) === array_values($info['fixed']); + $this->assertTrue($equal, format_string('language negotiation for %type is properly set up', array('%type' => $type))); + } + } + } +} + +/** + * Functional tests for CSS alter functions. + */ +class LocaleCSSAlterTest extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'CSS altering', + 'description' => 'Test CSS alter functions.', + 'group' => 'Locale', + ); + } + + function setUp() { + parent::setUp('locale'); + } + + /** + * Verifies that -rtl.css file is added directly after LTR .css file. + */ + function testCSSFilesOrderInRTLMode() { + global $base_url; + + // User to add and remove language. + $admin_user = $this->drupalCreateUser(array('administer languages', 'administer content types', 'access administration pages')); + + // Log in as admin. + $this->drupalLogin($admin_user); + + // Install the Arabic language (which is RTL) and configure as the default. + $edit = array(); + $edit['langcode'] = 'ar'; + $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language')); + + $edit = array(); + $edit['site_default'] = 'ar'; + $this->drupalPost(NULL, $edit, t('Save configuration')); + + // Verify that the -rtl.css file is added directly after LTR file. + $this->drupalGet(''); + $query_string = '?' . variable_get('css_js_query_string', '0'); + $this->assertRaw('@import url("' . $base_url . '/modules/system/system.base.css' . $query_string . '");' . "\n" . '@import url("' . $base_url . '/modules/system/system.base-rtl.css' . $query_string . '");' . "\n", 'CSS: system.base-rtl.css is added directly after system.base.css.'); + $this->assertRaw('@import url("' . $base_url . '/modules/system/system.menus.css' . $query_string . '");' . "\n" . '@import url("' . $base_url . '/modules/system/system.menus-rtl.css' . $query_string . '");' . "\n", 'CSS: system.menus-rtl.css is added directly after system.menus.css.'); + $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.0/modules/locale/tests/locale_test.info drupal-7.66/modules/locale/tests/locale_test.info --- drupal-7.0/modules/locale/tests/locale_test.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/locale/tests/locale_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: locale_test.info,v 1.3 2010/12/20 19:59:42 webchick Exp $ name = "Locale Test" description = "Support module for the locale layer tests." core = 7.x @@ -6,8 +5,7 @@ version = VERSION hidden = TRUE -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/locale/tests/locale_test.js drupal-7.66/modules/locale/tests/locale_test.js --- drupal-7.0/modules/locale/tests/locale_test.js 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/locale/tests/locale_test.js 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,46 @@ + +Drupal.t("Standard Call t"); +Drupal +. +t +( +"Whitespace Call t" +) +; + +Drupal.t('Single Quote t'); +Drupal.t('Single Quote \'Escaped\' t'); +Drupal.t('Single Quote ' + 'Concat ' + 'strings ' + 't'); + +Drupal.t("Double Quote t"); +Drupal.t("Double Quote \"Escaped\" t"); +Drupal.t("Double Quote " + "Concat " + "strings " + "t"); + +Drupal.t("Context Unquoted t", {}, {context: "Context string unquoted"}); +Drupal.t("Context Single Quoted t", {}, {'context': "Context string single quoted"}); +Drupal.t("Context Double Quoted t", {}, {"context": "Context string double quoted"}); + +Drupal.t("Context !key Args t", {'!key': 'value'}, {context: "Context string"}); + +Drupal.formatPlural(1, "Standard Call plural", "Standard Call @count plural"); +Drupal +. +formatPlural +( +1, +"Whitespace Call plural", +"Whitespace Call @count plural" +) +; + +Drupal.formatPlural(1, 'Single Quote plural', 'Single Quote @count plural'); +Drupal.formatPlural(1, 'Single Quote \'Escaped\' plural', 'Single Quote \'Escaped\' @count plural'); + +Drupal.formatPlural(1, "Double Quote plural", "Double Quote @count plural"); +Drupal.formatPlural(1, "Double Quote \"Escaped\" plural", "Double Quote \"Escaped\" @count plural"); + +Drupal.formatPlural(1, "Context Unquoted plural", "Context Unquoted @count plural", {}, {context: "Context string unquoted"}); +Drupal.formatPlural(1, "Context Single Quoted plural", "Context Single Quoted @count plural", {}, {'context': "Context string single quoted"}); +Drupal.formatPlural(1, "Context Double Quoted plural", "Context Double Quoted @count plural", {}, {"context": "Context string double quoted"}); + +Drupal.formatPlural(1, "Context !key Args plural", "Context !key Args @count plural", {'!key': 'value'}, {context: "Context string"}); diff -Naur drupal-7.0/modules/locale/tests/locale_test.module drupal-7.66/modules/locale/tests/locale_test.module --- drupal-7.0/modules/locale/tests/locale_test.module 2010-10-09 15:46:09.000000000 +0200 +++ drupal-7.66/modules/locale/tests/locale_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ provider)) { + drupal_set_message(t('Language negotiation provider: @name', array('@name' => $GLOBALS['language']->provider))); + } +} + +/** + * Implements hook_language_types_info(). + */ +function locale_test_language_types_info() { + if (variable_get('locale_test_language_types', FALSE)) { + return array( + 'test_language_type' => array( + 'name' => t('Test'), + 'description' => t('A test language type.'), + ), + 'fixed_test_language_type' => array( + 'fixed' => array('test_language_provider'), + ), + ); + } +} + +/** + * Implements hook_menu(). + * + * @return array + */ +function locale_test_menu() { + $items = array(); + $items['locale_test_plural_format_page'] = array( + 'page callback' => 'locale_test_plural_format_page', + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); + + return $items; +} + +/** * Implements hook_language_types_info_alter(). */ function locale_test_language_types_info_alter(array &$language_types) { if (variable_get('locale_test_content_language_type', FALSE)) { - $language_types[LANGUAGE_TYPE_CONTENT] = array( - 'name' => t('Content'), - 'description' => t('Order of language detection methods for content. If a version of content is available in the detected language, it will be displayed.'), + unset($language_types[LANGUAGE_TYPE_CONTENT]['fixed']); + } +} + +/** + * Implements hook_language_negotiation_info(). + */ +function locale_test_language_negotiation_info() { + if (variable_get('locale_test_language_negotiation_info', FALSE)) { + $info = array( + 'callbacks' => array( + 'language' => 'locale_test_language_provider', + ), + 'file' => drupal_get_path('module', 'locale_test') .'/locale_test.module', + 'weight' => -10, + 'description' => t('This is a test language provider.'), + ); + + return array( + 'test_language_provider' => array( + 'name' => t('Test'), + 'types' => array(LANGUAGE_TYPE_CONTENT, 'test_language_type', 'fixed_test_language_type'), + ) + $info, + 'test_language_provider_ts' => array( + 'name' => t('Type-specific test'), + 'types' => array('test_language_type'), + ) + $info, + ); + } +} + +/** + * Implements hook_language_negotiation_info_alter(). + */ +function locale_test_language_negotiation_info_alter(array &$language_providers) { + if (variable_get('locale_test_language_negotiation_info_alter', FALSE)) { + unset($language_providers[LOCALE_LANGUAGE_NEGOTIATION_INTERFACE]); + } +} + +/** + * Store the last negotiated languages. + */ +function locale_test_store_language_negotiation() { + $last = array(); + foreach (language_types() as $type) { + $last[$type] = $GLOBALS[$type]->language; + } + variable_set('locale_test_language_negotiation_last', $last); +} + +/** + * Test language provider. + */ +function locale_test_language_provider($languages) { + return 'it'; +} + +/** + * Returns markup for locale_get_plural testing. + * + * @return array + */ +function locale_test_plural_format_page() { + $tests = _locale_test_plural_format_tests(); + $result = array(); + foreach ($tests as $test) { + $string_param = array( + '@lang' => $test['language'], + '@locale_get_plural' => locale_get_plural($test['count'], $test['language']) + ); + $result[] = array( + '#prefix' => '
      ', + '#markup' => format_string('Language: @lang, locale_get_plural: @locale_get_plural.', $string_param), ); } + return $result; +} + +/** + * Helper function with list of test cases + * + * @return array + */ +function _locale_test_plural_format_tests() { + return array( + // Test data for English (no formula present). + array( + 'count' => 1, + 'language' => 'en', + 'expected-result' => 0, + ), + array( + 'count' => 0, + 'language' => 'en', + 'expected-result' => 1, + ), + array( + 'count' => 5, + 'language' => 'en', + 'expected-result' => 1, + ), + + // Test data for French (simpler formula). + array( + 'count' => 1, + 'language' => 'fr', + 'expected-result' => 0, + ), + array( + 'count' => 0, + 'language' => 'fr', + 'expected-result' => 1, + ), + array( + 'count' => 5, + 'language' => 'fr', + 'expected-result' => 1, + ), + + // Test data for Croatian (more complex formula). + array( + 'count' => 1, + 'language' => 'hr', + 'expected-result' => 0, + ), + array( + 'count' => 21, + 'language' => 'hr', + 'expected-result' => 0, + ), + array( + 'count' => 0, + 'language' => 'hr', + 'expected-result' => 2, + ), + array( + 'count' => 2, + 'language' => 'hr', + 'expected-result' => 1, + ), + array( + 'count' => 8, + 'language' => 'hr', + 'expected-result' => 2, + ), + + // Test data for Hungarian (nonexistent language). + array( + 'count' => 1, + 'language' => 'hu', + 'expected-result' => -1, + ), + array( + 'count' => 21, + 'language' => 'hu', + 'expected-result' => -1, + ), + array( + 'count' => 0, + 'language' => 'hu', + 'expected-result' => -1, + ), + ); } diff -Naur drupal-7.0/modules/menu/menu.admin.inc drupal-7.66/modules/menu/menu.admin.inc --- drupal-7.0/modules/menu/menu.admin.inc 2010-10-20 09:40:59.000000000 +0200 +++ drupal-7.66/modules/menu/menu.admin.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ array('menu-disabled')) : array('class' => array('menu-enabled')); - $form[$mlid]['title']['#markup'] = l($item['title'], $item['href'], $item['localized_options']) . ($item['hidden'] ? ' (' . t('disabled') . ')' : ''); + $form[$mlid]['title']['#markup'] = l($item['title'], $item['href'], $item['localized_options']); + if ($item['hidden']) { + $form[$mlid]['title']['#markup'] .= ' (' . t('disabled') . ')'; + } + elseif ($item['link_path'] == 'user' && $item['module'] == 'system') { + $form[$mlid]['title']['#markup'] .= ' (' . t('logged in users only') . ')'; + } + $form[$mlid]['hidden'] = array( '#type' => 'checkbox', '#title' => t('Enable @title menu link', array('@title' => $item['title'])), @@ -261,10 +267,21 @@ // This is an add form, initialize the menu link. $item = array('link_title' => '', 'mlid' => 0, 'plid' => 0, 'menu_name' => $menu['menu_name'], 'weight' => 0, 'link_path' => '', 'options' => array(), 'module' => 'menu', 'expanded' => 0, 'hidden' => 0, 'has_children' => 0); } + else { + // Get the human-readable menu title from the given menu name. + $titles = menu_get_menus(); + $current_title = $titles[$item['menu_name']]; + + // Get the current breadcrumb and add a link to that menu's overview page. + $breadcrumb = menu_get_active_breadcrumb(); + $breadcrumb[] = l($current_title, 'admin/structure/menu/manage/' . $item['menu_name']); + drupal_set_breadcrumb($breadcrumb); + } $form['actions'] = array('#type' => 'actions'); $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, @@ -287,8 +304,9 @@ $form['link_path'] = array( '#type' => 'textfield', '#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( @@ -378,7 +396,7 @@ else { unset($item['options']['fragment']); } - if ($item['link_path'] != $parsed_link['path']) { + if (isset($parsed_link['path']) && $item['link_path'] != $parsed_link['path']) { $item['link_path'] = $parsed_link['path']; } } @@ -453,7 +471,6 @@ '#machine_name' => array( 'exists' => 'menu_edit_menu_name_exists', 'source' => array('title'), - 'label' => t('URL path'), 'replace_pattern' => '[^a-z0-9-]+', 'replace' => '-', ), @@ -496,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); } @@ -606,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); } @@ -679,7 +694,7 @@ '#empty_option' => t('No Secondary links'), '#options' => $menu_options, '#tree' => FALSE, - '#description' => t('Select the source for the Secondary links. An advanced option allows you to use the same source for both Main links (currently %main) and Secondary links: if your source menu has two levels of hierarchy, the top level menu links will appear in the Main links, and the children of the active link will appear in the Secondary links.', array('%main' => $menu_options[$main])), + '#description' => t('Select the source for the Secondary links. An advanced option allows you to use the same source for both Main links (currently %main) and Secondary links: if your source menu has two levels of hierarchy, the top level menu links will appear in the Main links, and the children of the active link will appear in the Secondary links.', array('%main' => $main ? $menu_options[$main] : t('none'))), ); return system_settings_form($form); diff -Naur drupal-7.0/modules/menu/menu.admin.js drupal-7.66/modules/menu/menu.admin.js --- drupal-7.0/modules/menu/menu.admin.js 2009-10-13 03:25:58.000000000 +0200 +++ drupal-7.66/modules/menu/menu.admin.js 2019-04-17 22:20:46.000000000 +0200 @@ -1,48 +1,46 @@ -// $Id: menu.admin.js,v 1.1 2009/10/13 01:25:58 dries Exp $ - (function ($) { - Drupal.behaviors.menuChangeParentItems = { - attach: function (context, settings) { - $('fieldset#edit-menu input').each(function () { - $(this).change(function () { - // Update list of available parent menu items. - Drupal.menu_update_parent_list(); - }); +Drupal.behaviors.menuChangeParentItems = { + attach: function (context, settings) { + $('fieldset#edit-menu input').each(function () { + $(this).change(function () { + // Update list of available parent menu items. + Drupal.menu_update_parent_list(); }); - } - } - - /** - * Function to set the options of the menu parent item dropdown. - */ - Drupal.menu_update_parent_list = function () { - var values = []; - - $('input:checked', $('fieldset#edit-menu')).each(function () { - // Get the names of all checked menus. - values.push(Drupal.checkPlain($.trim($(this).val()))); - }); - - var url = Drupal.settings.basePath + 'admin/structure/menu/parents'; - $.ajax({ - url: location.protocol + '//' + location.host + url, - type: 'POST', - data: {'menus[]' : values}, - dataType: 'json', - success: function (options) { - // Save key of last selected element. - var selected = $('fieldset#edit-menu #edit-menu-parent :selected').val(); - // Remove all exisiting options from dropdown. - $('fieldset#edit-menu #edit-menu-parent').children().remove(); - // Add new options to dropdown. - jQuery.each(options, function(index, value) { - $('fieldset#edit-menu #edit-menu-parent').append( - $('').val(index).text(value) - ); - }); - } }); } +}; + +/** + * Function to set the options of the menu parent item dropdown. + */ +Drupal.menu_update_parent_list = function () { + var values = []; + + $('input:checked', $('fieldset#edit-menu')).each(function () { + // Get the names of all checked menus. + values.push(Drupal.checkPlain($.trim($(this).val()))); + }); + + var url = Drupal.settings.basePath + 'admin/structure/menu/parents'; + $.ajax({ + url: location.protocol + '//' + location.host + url, + type: 'POST', + data: {'menus[]' : values}, + dataType: 'json', + success: function (options) { + // Save key of last selected element. + var selected = $('fieldset#edit-menu #edit-menu-parent :selected').val(); + // Remove all exisiting options from dropdown. + $('fieldset#edit-menu #edit-menu-parent').children().remove(); + // Add new options to dropdown. + jQuery.each(options, function(index, value) { + $('fieldset#edit-menu #edit-menu-parent').append( + $('').val(index).text(value) + ); + }); + } + }); +}; })(jQuery); diff -Naur drupal-7.0/modules/menu/menu.api.php drupal-7.66/modules/menu/menu.api.php --- drupal-7.0/modules/menu/menu.api.php 2010-04-28 07:08:24.000000000 +0200 +++ drupal-7.66/modules/menu/menu.api.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ fetchAllAssoc('menu_name', PDO::FETCH_ASSOC); + // If the menu does not exist, do nothing; nodes will use the default D7 + // node menu settings. + if (!isset($defined_menus[$default_node_menu])) { + return; + } + + // Update the menu settings for each node type. + foreach (_update_7000_node_get_types() as $type => $type_object) { + $type_menus = variable_get('menu_options_' . $type); + // If the site already has a custom menu setting for this node type (set + // on the initial upgrade to Drupal 7.0), don't override it. + if (!isset($type_menus)) { + // Set up this node type so that the Drupal 6 "Default menu for content" + // is still available in the "Menu settings" section. + variable_set('menu_options_' . $type, array($default_node_menu)); + variable_set('menu_parent_' . $type, $default_node_menu . ':0'); + } + } + } +} + +/** + * Rename "Primary Links" and "Secondary Links" to their Drupal 7 equivalents. + */ +function menu_update_7001() { + // Migrate D6 menu_primary_links_source to D7 menu_main_links_source (without + // renaming). + if (variable_get('menu_primary_links_source') !== NULL) { + variable_set('menu_main_links_source', variable_get('menu_primary_links_source')); + variable_del('menu_primary_links_source'); + } + + // Rename each menu, and any settings that refer to the old menu name. + // - "Primary Links" has become system menu "Main menu". + // - "Secondary Links" has become a new custom menu "Secondary menu". + $rename = array( + 'primary-links' => array('main-menu', 'Main menu'), + 'secondary-links' => array('secondary-menu', 'Secondary menu'), + ); + foreach ($rename as $from_menu => $to) { + list($to_menu, $to_title) = $to; + // Rename the menu, and links in the menu. + db_update('menu_custom') + ->fields(array('menu_name' => $to_menu, 'title' => $to_title)) + ->condition('menu_name', $from_menu) + ->execute(); + db_update('menu_links') + ->fields(array('menu_name' => $to_menu)) + ->condition('menu_name', $from_menu) + ->execute(); + + // Update any content type that used this menu as a default menu. + // Note: these variables may be unset/default, in which case we leave them + // alone. See menu_update_7000() + foreach (_update_7000_node_get_types() as $type => $type_object) { + $menu_options = variable_get('menu_options_' . $type); + if ($menu_options !== NULL) { + variable_set('menu_options_' . $type, str_replace($from_menu, $to_menu, $menu_options)); + if (variable_get('menu_parent_' . $type) == $from_menu . ':0') { + variable_set('menu_parent_' . $type, $to_menu . ':0'); + } + } + } + + // Update the "source for primary links" and "source for secondary links" to + // follow. + if (variable_get('menu_main_links_source') == $from_menu) { + variable_set('menu_main_links_source', $to_menu); + } + if (variable_get('menu_secondary_links_source') == $from_menu) { + variable_set('menu_secondary_links_source', $to_menu); + } + } +} + +/** + * Rename the primary/secondary menu blocks to match previously renamed menus. + */ +function menu_update_7002(&$sandbox) { + // Check for the presence of old or new table names. + if (db_table_exists('blocks') || db_table_exists('block')) { + $renamed_deltas = array( + 'menu' => array( + 'primary-links' => 'main-menu', + 'secondary-links' => 'secondary-menu', + ), + ); + + $moved_deltas = array( + 'menu' => array('main-menu' => 'system'), + ); + + update_fix_d7_block_deltas($sandbox, $renamed_deltas, $moved_deltas); + } +} +/** + * Add missing custom menus to active menus list. + */ +function menu_update_7003(&$sandbox) { + // Make sure all custom menus are present in the active menus variable so that + // their items may appear in the active trail. + // @see menu_set_active_menu_names() + $active_menus = variable_get('menu_default_active_menus', array_keys(menu_list_system_menus())); + $update_variable = FALSE; + foreach (menu_get_names() as $menu_name) { + if (!in_array($menu_name, $active_menus) && (strpos($menu_name, 'menu-') === 0)) { + $active_menus[] = $menu_name; + $update_variable = TRUE; + } + } + if ($update_variable) { + variable_set('menu_default_active_menus', $active_menus); + } + // Clear the menu cache. + cache_clear_all(NULL, 'cache_menu'); +} + +/** + * @} End of "addtogroup updates-7.x-extra". + * The next series of updates should start at 8000. + */ diff -Naur drupal-7.0/modules/menu/menu.js drupal-7.66/modules/menu/menu.js --- drupal-7.0/modules/menu/menu.js 2010-11-05 20:47:20.000000000 +0100 +++ drupal-7.66/modules/menu/menu.js 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,3 @@ -// $Id: menu.js,v 1.7 2010/11/05 19:47:20 dries Exp $ - (function ($) { Drupal.behaviors.menuFieldsetSummaries = { diff -Naur drupal-7.0/modules/menu/menu.module drupal-7.66/modules/menu/menu.module --- drupal-7.0/modules/menu/menu.module 2010-09-24 02:37:43.000000000 +0200 +++ drupal-7.66/modules/menu/menu.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,9 +1,14 @@ ' . t('About') . ''; - $output .= '

      ' . t('The Menu module provides an interface for managing menus. A menu is a hierarchical collection of links, which can be within or external to the site, generally used for navigation. Each menu is rendered in a block that can be enabled and positioned through the Blocks administration page. You can view and manage menus on the Menus administration page. For more information, see the online handbook entry for the Menu module.', array('@blocks' => url('admin/structure/block'), '@menus' => url('admin/structure/menu'), '@menu' => 'http://drupal.org/handbook/modules/menu/')) . '

      '; + $output .= '

      ' . t('The Menu module provides an interface for managing menus. A menu is a hierarchical collection of links, which can be within or external to the site, generally used for navigation. Each menu is rendered in a block that can be enabled and positioned through the Blocks administration page. You can view and manage menus on the Menus administration page. For more information, see the online handbook entry for the Menu module.', array('@blocks' => url('admin/structure/block'), '@menus' => url('admin/structure/menu'), '@menu' => 'http://drupal.org/documentation/modules/menu/')) . '

      '; $output .= '

      ' . t('Uses') . '

      '; $output .= '
      '; $output .= '
      ' . t('Managing menus') . '
      '; @@ -64,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', @@ -241,7 +246,8 @@ * * @param $menu * An array representing a custom menu: - * - menu_name: The unique name of the custom menu. + * - menu_name: The unique name of the custom menu (composed of lowercase + * letters, numbers, and hyphens). * - title: The human readable menu title. * - description: The custom menu description. * @@ -262,6 +268,15 @@ switch ($status) { case SAVED_NEW: + // Make sure the menu is present in the active menus variable so that its + // items may appear in the menu active trail. + // @see menu_set_active_menu_names() + $active_menus = variable_get('menu_default_active_menus', array_keys(menu_get_menus())); + if (!in_array($menu['menu_name'], $active_menus)) { + $active_menus[] = $menu['menu_name']; + variable_set('menu_default_active_menus', $active_menus); + } + module_invoke_all('menu_insert', $menu); break; @@ -299,6 +314,15 @@ // Delete all links from the menu. menu_delete_links($menu['menu_name']); + // Remove menu from active menus variable. + $active_menus = variable_get('menu_default_active_menus', array_keys(menu_get_menus())); + foreach ($active_menus as $i => $menu_name) { + if ($menu['menu_name'] == $menu_name) { + unset($active_menus[$i]); + variable_set('menu_default_active_menus', $active_menus); + } + } + // Delete the custom menu. db_delete('menu_custom') ->condition('menu_name', $menu['menu_name']) @@ -316,6 +340,9 @@ * @param $item * The menu item or the node type for which to generate a list of parents. * If $item['mlid'] == 0 then the complete tree is returned. + * @param $type + * The node type for which to generate a list of parents. + * If $item itself is a node type then $type is ignored. * @return * An array of menu link titles keyed on the a string containing the menu name * and mlid. The list excludes the given item and its children. @@ -323,7 +350,7 @@ * @todo This has to be turned into a #process form element callback. The * 'menu_override_parent_selector' variable is entirely superfluous. */ -function menu_parent_options($menus, $item) { +function menu_parent_options($menus, $item, $type = '') { // The menu_links table can be practically any size and we need a way to // allow contrib modules to provide more scalable pattern choosers. // hook_form_alter is too late in itself because all the possible parents are @@ -333,18 +360,22 @@ } $available_menus = array(); - if (is_array($item)) { - // If $item is an array fill it with all menus given to this function. + if (!is_array($item)) { + // If $item is not an array then it is a node type. + // Use it as $type and prepare a dummy menu item for _menu_get_options(). + $type = $item; + $item = array('mlid' => 0); + } + if (empty($type)) { + // If no node type is set, use all menus given to this function. $available_menus = $menus; } else { - // If $item is a node type, get all available menus for this type and - // prepare a dummy menu item for _menu_parent_depth_limit(). - $type_menus = variable_get('menu_options_' . $item, array('main-menu' => 'main-menu')); + // If a node type is set, use all available menus for this type. + $type_menus = variable_get('menu_options_' . $type, array('main-menu' => 'main-menu')); foreach ($type_menus as $menu) { $available_menus[$menu] = $menu; } - $item = array('mlid' => 0); } return _menu_get_options($menus, $available_menus, $item); @@ -352,7 +383,7 @@ /** * Page callback. - * Get all available menus and menu items as Javascript array. + * Get all the available menus and menu items as a JavaScript array. */ function menu_parent_options_js() { $available_menus = array(); @@ -439,7 +470,6 @@ $blocks = array(); foreach ($menus as $name => $title) { - // Default "Navigation" block is handled by user.module. $blocks[$name]['info'] = check_plain($title); // Menu blocks can't be cached because each menu item can have // a custom access callback. menu.inc manages its own caching. @@ -535,18 +565,23 @@ function menu_node_prepare($node) { if (empty($node->menu)) { // Prepare the node for the edit form so that $node->menu always exists. - $menu_name = variable_get('menu_parent_' . $node->type, 'main-menu:0'); + $menu_name = strtok(variable_get('menu_parent_' . $node->type, 'main-menu:0'), ':'); $item = array(); if (isset($node->nid)) { + $mlid = FALSE; // Give priority to the default menu - $mlid = db_query_range("SELECT mlid FROM {menu_links} WHERE link_path = :path AND menu_name = :menu_name AND module = 'menu' ORDER BY mlid ASC", 0, 1, array( - ':path' => 'node/' . $node->nid, - ':menu_name' => $menu_name, - ))->fetchField(); - // Check all menus if a link does not exist in the default menu. - if (!$mlid) { - $mlid = db_query_range("SELECT mlid FROM {menu_links} WHERE link_path = :path AND module = 'menu' ORDER BY mlid ASC", 0, 1, array( + $type_menus = variable_get('menu_options_' . $node->type, array('main-menu' => 'main-menu')); + if (in_array($menu_name, $type_menus)) { + $mlid = db_query_range("SELECT mlid FROM {menu_links} WHERE link_path = :path AND menu_name = :menu_name AND module = 'menu' ORDER BY mlid ASC", 0, 1, array( + ':path' => 'node/' . $node->nid, + ':menu_name' => $menu_name, + ))->fetchField(); + } + // Check all allowed menus if a link does not exist in the default menu. + if (!$mlid && !empty($type_menus)) { + $mlid = db_query_range("SELECT mlid FROM {menu_links} WHERE link_path = :path AND module = 'menu' AND menu_name IN (:type_menus) ORDER BY mlid ASC", 0, 1, array( ':path' => 'node/' . $node->nid, + ':type_menus' => array_values($type_menus), ))->fetchField(); } if ($mlid) { @@ -589,15 +624,18 @@ * @see menu_node_submit() */ function menu_form_node_form_alter(&$form, $form_state) { - // Generate a list of possible parents. + // Generate a list of possible parents (not including this link or descendants). // @todo This must be handled in a #process handler. + $link = $form['#node']->menu; $type = $form['#node']->type; - $options = menu_parent_options(menu_get_menus(), $type); + // menu_parent_options() is goofy and can actually handle either a menu link + // or a node type both as second argument. Pick based on whether there is + // a link already (menu_node_prepare() sets mlid default to 0). + $options = menu_parent_options(menu_get_menus(), $link['mlid'] ? $link : $type, $type); // If no possible parent menu items were found, there is nothing to display. if (empty($options)) { return; } - $link = $form['#node']->menu; $form['menu'] = array( '#type' => 'fieldset', @@ -636,6 +674,7 @@ $form['menu']['link']['link_title'] = array( '#type' => 'textfield', '#title' => t('Menu link title'), + '#maxlength' => 255, '#default_value' => $link['link_title'], ); @@ -648,9 +687,13 @@ ); $default = ($link['mlid'] ? $link['menu_name'] . ':' . $link['plid'] : variable_get('menu_parent_' . $type, 'main-menu:0')); - // @todo This will fail with the new selective menus per content type. + // If the current parent menu item is not present in options, use the first + // available option as default value. + // @todo User should not be allowed to access menu link settings in such a + // case. if (!isset($options[$default])) { - $default = 'navigation:0'; + $array = array_keys($options); + $default = reset($array); } $form['menu']['link']['parent'] = array( '#type' => 'select', diff -Naur drupal-7.0/modules/menu/menu.test drupal-7.66/modules/menu/menu.test --- drupal-7.0/modules/menu/menu.test 2010-10-13 15:43:21.000000000 +0200 +++ drupal-7.66/modules/menu/menu.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,9 +1,8 @@ assertEqual($description, $saved_item['options']['attributes']['title'], t('Saving an existing link updates the description (title attribute)')); + $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'); } /** @@ -112,14 +122,14 @@ // Assert the new menu. $this->drupalGet('admin/structure/menu/manage/' . $menu_name . '/edit'); - $this->assertRaw($title, t('Custom menu was added.')); + $this->assertRaw($title, 'Custom menu was added.'); // Edit the menu. $new_title = $this->randomName(16); $menu['title'] = $new_title; menu_save($menu); $this->drupalGet('admin/structure/menu/manage/' . $menu_name . '/edit'); - $this->assertRaw($new_title, t('Custom menu was edited.')); + $this->assertRaw($new_title, 'Custom menu was edited.'); } /** @@ -168,7 +178,7 @@ $edit['blocks[menu_' . $menu_name . '][region]'] = 'sidebar_first'; $this->drupalPost('admin/structure/block', $edit, t('Save blocks')); $this->assertResponse(200); - $this->assertText(t('The block settings have been updated.'), t('Custom menu block was enabled')); + $this->assertText(t('The block settings have been updated.'), 'Custom menu block was enabled'); return menu_load($menu_name); } @@ -185,11 +195,11 @@ // Delete custom menu. $this->drupalPost("admin/structure/menu/manage/$menu_name/delete", array(), t('Delete')); $this->assertResponse(200); - $this->assertRaw(t('The custom menu %title has been deleted.', array('%title' => $title)), t('Custom menu was deleted')); + $this->assertRaw(t('The custom menu %title has been deleted.', array('%title' => $title)), 'Custom menu was deleted'); $this->assertFalse(menu_load($menu_name), 'Custom menu was deleted'); // Test if all menu links associated to the menu were removed from database. $result = db_query("SELECT menu_name FROM {menu_links} WHERE menu_name = :menu_name", array(':menu_name' => $menu_name))->fetchField(); - $this->assertFalse($result, t('All menu links associated to the custom menu were deleted.')); + $this->assertFalse($result, 'All menu links associated to the custom menu were deleted.'); } /** @@ -206,7 +216,7 @@ // Add menu links. $item1 = $this->addMenuLink(0, 'node/' . $node1->nid, $menu_name); - $item2 = $this->addMenuLink($item1['mlid'], 'node/' . $node2->nid, $menu_name); + $item2 = $this->addMenuLink($item1['mlid'], 'node/' . $node2->nid, $menu_name, FALSE); $item3 = $this->addMenuLink($item2['mlid'], 'node/' . $node3->nid, $menu_name); $this->assertMenuLink($item1['mlid'], array('depth' => 1, 'has_children' => 1, 'p1' => $item1['mlid'], 'p2' => 0)); $this->assertMenuLink($item2['mlid'], array('depth' => 2, 'has_children' => 1, 'p1' => $item1['mlid'], 'p2' => $item2['mlid'], 'p3' => 0)); @@ -267,13 +277,13 @@ $item = $this->addMenuLink(0, $path); $this->drupalGet('admin/structure/menu/item/' . $item['mlid'] . '/edit'); - $this->assertFieldByName('link_path', $path, t('Path is found with both query and fragment.')); + $this->assertFieldByName('link_path', $path, 'Path is found with both query and fragment.'); // Now change the path to something without query and fragment. $path = 'node'; $this->drupalPost('admin/structure/menu/item/' . $item['mlid'] . '/edit', array('link_path' => $path), t('Save')); $this->drupalGet('admin/structure/menu/item/' . $item['mlid'] . '/edit'); - $this->assertFieldByName('link_path', $path, t('Path no longer has query or fragment.')); + $this->assertFieldByName('link_path', $path, 'Path no longer has query or fragment.'); } /** @@ -284,7 +294,7 @@ * @param string $menu_name Menu name. * @return array Menu link created. */ - function addMenuLink($plid = 0, $link = '', $menu_name = 'navigation') { + function addMenuLink($plid = 0, $link = '', $menu_name = 'navigation', $expanded = TRUE) { // View add menu link page. $this->drupalGet("admin/structure/menu/manage/$menu_name/add"); $this->assertResponse(200); @@ -295,7 +305,7 @@ 'link_title' => $title, 'description' => '', 'enabled' => TRUE, // Use this to disable the menu and test. - 'expanded' => TRUE, // Setting this to true should test whether it works when we do the std_user tests. + 'expanded' => $expanded, // Setting this to true should test whether it works when we do the std_user tests. 'parent' => $menu_name . ':' . $plid, 'weight' => '0', ); @@ -319,7 +329,7 @@ * @param string $menu_name Menu name. */ function addInvalidMenuLink($menu_name = 'navigation') { - foreach (array('-&-', 'admin/people/permissions') as $link_path) { + foreach (array('-&-', 'admin/people/permissions', '#') as $link_path) { $edit = array( 'link_path' => $link_path, 'link_title' => 'title', @@ -346,22 +356,22 @@ if (isset($parent)) { // Verify menu link. $title = $parent['link_title']; - $this->assertText($title, 'Parent menu link was displayed'); + $this->assertLink($title, 0, 'Parent menu link was displayed'); // Verify menu link link. $this->clickLink($title); $title = $parent_node->title; - $this->assertTitle(t("@title | Drupal", array('@title' => $title)), t('Parent menu link link target was correct')); + $this->assertTitle(t("@title | Drupal", array('@title' => $title)), 'Parent menu link link target was correct'); } // Verify menu link. $title = $item['link_title']; - $this->assertText($title, 'Menu link was displayed'); + $this->assertLink($title, 0, 'Menu link was displayed'); // Verify menu link link. $this->clickLink($title); $title = $item_node->title; - $this->assertTitle(t("@title | Drupal", array('@title' => $title)), t('Menu link link target was correct')); + $this->assertTitle(t("@title | Drupal", array('@title' => $title)), 'Menu link link target was correct'); } /** @@ -380,7 +390,7 @@ /** * Modify a menu link using the menu module UI. * - * @param array &$item Menu link passed by reference. + * @param array $item Menu link passed by reference. */ function modifyMenuLink(&$item) { $item['link_title'] = $this->randomName(16); @@ -413,7 +423,7 @@ // Reset menu link. $this->drupalPost("admin/structure/menu/item/$mlid/reset", array(), t('Reset')); $this->assertResponse(200); - $this->assertRaw(t('The menu link was reset to its default settings.'), t('Menu link was reset')); + $this->assertRaw(t('The menu link was reset to its default settings.'), 'Menu link was reset'); // Verify menu link. $this->drupalGet(''); @@ -433,7 +443,7 @@ // Delete menu link. $this->drupalPost("admin/structure/menu/item/$mlid/delete", array(), t('Confirm')); $this->assertResponse(200); - $this->assertRaw(t('The menu link %title has been deleted.', array('%title' => $title)), t('Menu link was deleted')); + $this->assertRaw(t('The menu link %title has been deleted.', array('%title' => $title)), 'Menu link was deleted'); // Verify deletion. $this->drupalGet(''); @@ -510,11 +520,28 @@ $item['link_path'] .= '#' . $options['fragment']; } foreach ($expected_item as $key => $value) { - $this->assertEqual($item[$key], $value, t('Parameter %key had expected value.', array('%key' => $key))); + $this->assertEqual($item[$key], $value, format_string('Parameter %key had expected value.', array('%key' => $key))); } } /** + * 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() { @@ -538,21 +565,21 @@ $this->drupalGet('admin/help/menu'); $this->assertResponse($response); if ($response == 200) { - $this->assertText(t('Menu'), t('Menu help was displayed')); + $this->assertText(t('Menu'), 'Menu help was displayed'); } // View menu build overview node. $this->drupalGet('admin/structure/menu'); $this->assertResponse($response); if ($response == 200) { - $this->assertText(t('Menus'), t('Menu build overview node was displayed')); + $this->assertText(t('Menus'), 'Menu build overview node was displayed'); } // View navigation menu customization node. $this->drupalGet('admin/structure/menu/manage/navigation'); $this->assertResponse($response); if ($response == 200) { - $this->assertText(t('Navigation'), t('Navigation menu node was displayed')); + $this->assertText(t('Navigation'), 'Navigation menu node was displayed'); } // View menu edit node. @@ -560,21 +587,21 @@ $this->drupalGet('admin/structure/menu/item/' . $item['mlid'] . '/edit'); $this->assertResponse($response); if ($response == 200) { - $this->assertText(t('Edit menu item'), t('Menu edit node was displayed')); + $this->assertText(t('Edit menu item'), 'Menu edit node was displayed'); } // View menu settings node. $this->drupalGet('admin/structure/menu/settings'); $this->assertResponse($response); if ($response == 200) { - $this->assertText(t('Menus'), t('Menu settings node was displayed')); + $this->assertText(t('Menus'), 'Menu settings node was displayed'); } // View add menu node. $this->drupalGet('admin/structure/menu/add'); $this->assertResponse($response); if ($response == 200) { - $this->assertText(t('Menus'), t('Add menu node was displayed')); + $this->assertText(t('Menus'), 'Add menu node was displayed'); } } } @@ -621,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( @@ -655,7 +687,11 @@ $this->assertLink($node_title); $this->drupalGet('node/' . $node->nid . '/edit'); - $this->assertOptionSelected('edit-menu-weight', 17, t('Menu weight correct in edit form')); + $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( @@ -665,5 +701,61 @@ // Assert that there is no link for the node. $this->drupalGet(''); $this->assertNoLink($node_title); + + // Add a menu link to the Management menu. + $item = array( + 'link_path' => 'node/' . $node->nid, + 'link_title' => $this->randomName(16), + 'menu_name' => 'management', + ); + menu_link_save($item); + + // Assert that disabled Management menu is not shown on the node/$nid/edit page. + $this->drupalGet('node/' . $node->nid . '/edit'); + $this->assertText('Provide a menu link', 'Link in not allowed menu not shown in node edit form'); + // Assert that the link is still in the management menu after save. + $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save')); + $link = menu_link_load($item['mlid']); + $this->assertTrue($link, 'Link in not allowed menu still exists after saving node'); + + // Move the menu link back to the Navigation menu. + $item['menu_name'] = 'navigation'; + menu_link_save($item); + // Create a second node. + $child_node = $this->drupalCreateNode(array('type' => 'article')); + // Assign a menu link to the second node, being a child of the first one. + $child_item = array( + 'link_path' => 'node/'. $child_node->nid, + 'link_title' => $this->randomName(16), + 'plid' => $item['mlid'], + ); + menu_link_save($child_item); + // Edit the first node. + $this->drupalGet('node/'. $node->nid .'/edit'); + // Assert that it is not possible to set the parent of the first node to itself or the second node. + $this->assertNoOption('edit-menu-parent', 'navigation:'. $item['mlid']); + $this->assertNoOption('edit-menu-parent', 'navigation:'. $child_item['mlid']); + // Assert that unallowed Management menu is not available in options. + $this->assertNoOption('edit-menu-parent', 'management:0'); + } + + /** + * Asserts that a select option in the current page does not exist. + * + * @param $id + * Id of select field to assert. + * @param $option + * Option to assert. + * @param $message + * Message to display. + * @return + * TRUE on pass, FALSE on fail. + * + * @todo move to simpletest drupal_web_test_case.php. + */ + protected function assertNoOption($id, $option, $message = '') { + $selects = $this->xpath('//select[@id=:id]', array(':id' => $id)); + $options = $this->xpath('//select[@id=:id]//option[@value=:option]', array(':id' => $id, ':option' => $option)); + return $this->assertTrue(isset($selects[0]) && !isset($options[0]), $message ? $message : t('Option @option for field @id does not exist.', array('@option' => $option, '@id' => $id)), t('Browser')); } } diff -Naur drupal-7.0/modules/node/content_types.inc drupal-7.66/modules/node/content_types.inc --- drupal-7.0/modules/node/content_types.inc 2010-10-13 15:43:21.000000000 +0200 +++ drupal-7.66/modules/node/content_types.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,9 +1,8 @@ t('Operations'), 'colspan' => $field_ui ? '4' : '2')); $rows = array(); @@ -70,13 +69,21 @@ $type = $variables['type']; $output = check_plain($name); - $output .= ' (Machine name: ' . check_plain($type->type) . ')'; + $output .= ' ' . t('(Machine name: @type)', array('@type' => $type->type)) . ''; $output .= '
      ' . filter_xss_admin($type->description) . '
      '; return $output; } /** - * Generates the node type editing form. + * Form constructor for the node type editing form. + * + * @param $type + * (optional) An object representing the node type, when editing an existing + * node type. + * + * @see node_type_form_validate() + * @see node_type_form_submit() + * @ingroup forms */ function node_type_form($form, &$form_state, $type = NULL) { if (!isset($type->type)) { @@ -242,15 +249,17 @@ } /** - * Validates the content type submission form generated by node_type_form(). + * Form validation handler for node_type_form(). + * + * @see node_type_form_submit() */ 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(); @@ -270,14 +279,16 @@ } /** - * Implements hook_form_submit(). + * Form submission handler for node_type_form(). + * + * @see node_type_form_validate() */ function node_type_form_submit($form, &$form_state) { $op = isset($form_state['values']['op']) ? $form_state['values']['op'] : ''; $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; @@ -377,8 +388,7 @@ } /** - * Resets all of the relevant fields of a module-defined node type to their - * default values. + * Resets relevant fields of a module-defined node type to their default values. * * @param $type * The node type to reset. The node type is passed back by reference with its @@ -399,6 +409,8 @@ /** * Menu callback; delete a single content type. + * + * @ingroup forms */ function node_type_delete_confirm($form, &$form_state, $type) { $form['type'] = array('#type' => 'value', '#value' => $type->type); @@ -419,6 +431,8 @@ /** * Process content type delete confirm submissions. + * + * @see node_type_delete_confirm() */ function node_type_delete_confirm_submit($form, &$form_state) { node_type_delete($form_state['values']['type']); @@ -426,7 +440,7 @@ variable_del('node_preview_' . $form_state['values']['type']); $t_args = array('%name' => $form_state['values']['name']); drupal_set_message(t('The content type %name has been deleted.', $t_args)); - watchdog('menu', 'Deleted content type %name.', $t_args, WATCHDOG_NOTICE); + watchdog('node', 'Deleted content type %name.', $t_args, WATCHDOG_NOTICE); node_types_rebuild(); menu_rebuild(); diff -Naur drupal-7.0/modules/node/content_types.js drupal-7.66/modules/node/content_types.js --- drupal-7.0/modules/node/content_types.js 2010-05-05 08:55:25.000000000 +0200 +++ drupal-7.66/modules/node/content_types.js 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -// $Id: content_types.js,v 1.10 2010/05/05 06:55:25 webchick Exp $ (function ($) { Drupal.behaviors.contentTypes = { diff -Naur drupal-7.0/modules/node/node-rtl.css drupal-7.66/modules/node/node-rtl.css --- drupal-7.0/modules/node/node-rtl.css 2009-11-17 03:50:41.000000000 +0100 +++ drupal-7.66/modules/node/node-rtl.css 1970-01-01 01:00:00.000000000 +0100 @@ -1,15 +0,0 @@ -/* $Id: node-rtl.css,v 1.5 2009/11/17 02:50:41 webchick Exp $ */ - -#node-admin-content dl.multiselect dd .form-item label { - display: block; - float: right; - width: 6em; - font-weight: normal; -} - -#node-admin-buttons { - float: right; - margin-left: 0; - margin-right: 0.5em; - clear: left; -} diff -Naur drupal-7.0/modules/node/node.admin.inc drupal-7.66/modules/node/node.admin.inc --- drupal-7.0/modules/node/node.admin.inc 2010-12-09 03:16:21.000000000 +0100 +++ drupal-7.66/modules/node/node.admin.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ $filter) { list($key, $value) = $filter; switch ($key) { - case 'term': - $alias = $query->join('taxonomy_index', 'ti', "n.nid = %alias.nid"); - $query->condition($alias . '.tid', $value); - break; case 'status': // Note: no exploitable hole as $key/$value have already been checked when submitted list($key, $value) = explode('-', $value, 2); @@ -138,7 +142,16 @@ } /** - * Return form for node administration filters. + * Returns the node administration filters form array to node_admin_content(). + * + * @see node_admin_nodes() + * @see node_admin_nodes_submit() + * @see node_admin_nodes_validate() + * @see node_filter_form_submit() + * @see node_multiple_delete_confirm() + * @see node_multiple_delete_confirm_submit() + * + * @ingroup forms */ function node_filter_form() { $session = isset($_SESSION['node_overview_filter']) ? $_SESSION['node_overview_filter'] : array(); @@ -213,7 +226,15 @@ } /** - * Process result from node administration filter form. + * Form submission handler for node_filter_form(). + * + * @see node_admin_content() + * @see node_admin_nodes() + * @see node_admin_nodes_submit() + * @see node_admin_nodes_validate() + * @see node_filter_form() + * @see node_multiple_delete_confirm() + * @see node_multiple_delete_confirm_submit() */ function node_filter_form_submit($form, &$form_state) { $filters = node_filters(); @@ -245,15 +266,15 @@ * Make mass update of nodes, changing all nodes in the $nodes array * to update them with the field values in $updates. * - * IMPORTANT NOTE: This function is intended to work when called - * from a form submit handler. Calling it outside of the form submission - * process may not work correctly. + * IMPORTANT NOTE: This function is intended to work when called from a form + * submission handler. Calling it outside of the form submission process may not + * work correctly. * * @param array $nodes * Array of node nids to update. * @param array $updates - * Array of key/value pairs with node field names and the - * value to update that field to. + * Array of key/value pairs with node field names and the value to update that + * field to. */ function node_mass_update($nodes, $updates) { // We use batch processing to prevent timeout when updating a large number @@ -284,7 +305,17 @@ } /** - * Node Mass Update - helper function. + * Updates individual nodes when fewer than 10 are queued. + * + * @param $nid + * ID of node to update. + * @param $updates + * Associative array of updates. + * + * @return object + * An updated node object. + * + * @see node_mass_update() */ function _node_mass_update_helper($nid, $updates) { $node = node_load($nid, NULL, TRUE); @@ -298,7 +329,16 @@ } /** - * Node Mass Update Batch operation + * Implements callback_batch_operation(). + * + * Executes a batch operation for node_mass_update(). + * + * @param array $nodes + * An array of node IDs. + * @param array $updates + * Associative array of updates. + * @param array $context + * An array of contextual key/values. */ function _node_mass_update_batch_process($nodes, $updates, &$context) { if (!isset($context['sandbox']['progress'])) { @@ -329,7 +369,17 @@ } /** - * Node Mass Update Batch 'finished' callback. + * 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 + * concluded. + * @param int $results + * The number of nodes updated via the batch mode process. + * @param array $operations + * An array of function calls (not used in this function). */ function _node_mass_update_batch_finished($success, $results, $operations) { if ($success) { @@ -344,7 +394,17 @@ } /** - * Menu callback: content administration. + * Page callback: Form constructor for the content administration form. + * + * @see node_admin_nodes() + * @see node_admin_nodes_submit() + * @see node_admin_nodes_validate() + * @see node_filter_form() + * @see node_filter_form_submit() + * @see node_menu() + * @see node_multiple_delete_confirm() + * @see node_multiple_delete_confirm_submit() + * @ingroup forms */ function node_admin_content($form, $form_state) { if (isset($form_state['values']['operation']) && $form_state['values']['operation'] == 'delete') { @@ -359,6 +419,15 @@ /** * Form builder: Builds the node administration overview. + * + * @see node_admin_nodes_submit() + * @see node_admin_nodes_validate() + * @see node_filter_form() + * @see node_filter_form_submit() + * @see node_multiple_delete_confirm() + * @see node_multiple_delete_confirm_submit() + * + * @ingroup forms */ function node_admin_nodes() { $admin_access = user_access('administer nodes'); @@ -388,9 +457,9 @@ '#submit' => array('node_admin_nodes_submit'), ); - // Enable language column if translation module is enabled - // or if we have any node with language. - $multilanguage = (module_exists('translation') || db_query("SELECT COUNT(*) FROM {node} WHERE language <> :language", array(':language' => LANGUAGE_NONE))->fetchField()); + // Enable language column if translation module is enabled or if we have any + // node with language. + $multilanguage = (module_exists('translation') || db_query_range("SELECT 1 FROM {node} WHERE language <> :language", 0, 1, array(':language' => LANGUAGE_NONE))->fetchField()); // Build the sortable table header. $header = array( @@ -406,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')) { @@ -427,6 +497,7 @@ ->fields('n',array('nid')) ->limit(50) ->orderByHeader($header) + ->addTag('node_access') ->execute() ->fetchCol(); $nodes = node_load_multiple($nids); @@ -436,14 +507,18 @@ $destination = drupal_get_destination(); $options = array(); foreach ($nodes as $node) { - $l_options = $node->language != LANGUAGE_NONE && isset($languages[$node->language]) ? array('language' => $languages[$node->language]) : array(); + $langcode = entity_language('node', $node); + $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))), ), ), @@ -453,11 +528,11 @@ 'changed' => format_date($node->changed, 'short'), ); if ($multilanguage) { - if ($node->language == LANGUAGE_NONE || isset($languages[$node->language])) { - $options[$node->nid]['language'] = $node->language == LANGUAGE_NONE ? t('Language neutral') : t($languages[$node->language]->name); + if ($langcode == LANGUAGE_NONE || isset($languages[$langcode])) { + $options[$node->nid]['language'] = $langcode == LANGUAGE_NONE ? t('Language neutral') : t($languages[$langcode]->name); } else { - $options[$node->nid]['language'] = t('Undefined language (@langcode)', array('@langcode' => $node->language)); + $options[$node->nid]['language'] = t('Undefined language (@langcode)', array('@langcode' => $langcode)); } } // Build a list of all the accessible operations for the current node. @@ -528,8 +603,15 @@ /** * Validate node_admin_nodes form submissions. * - * Check if any nodes have been selected to perform the chosen - * 'Update option' on. + * Checks whether any nodes have been selected to perform the chosen 'Update + * option' on. + * + * @see node_admin_nodes() + * @see node_admin_nodes_submit() + * @see node_filter_form() + * @see node_filter_form_submit() + * @see node_multiple_delete_confirm() + * @see node_multiple_delete_confirm_submit() */ function node_admin_nodes_validate($form, &$form_state) { // Error if there are no items to select. @@ -541,7 +623,14 @@ /** * Process node_admin_nodes form submissions. * - * Execute the chosen 'Update option' on the selected nodes. + * Executes the chosen 'Update option' on the selected nodes. + * + * @see node_admin_nodes() + * @see node_admin_nodes_validate() + * @see node_filter_form() + * @see node_filter_form_submit() + * @see node_multiple_delete_confirm() + * @see node_multiple_delete_confirm_submit() */ function node_admin_nodes_submit($form, &$form_state) { $operations = module_invoke_all('node_operations'); @@ -567,6 +656,17 @@ } } +/** + * Multiple node deletion confirmation form for node_admin_content(). + * + * @see node_admin_nodes() + * @see node_admin_nodes_submit() + * @see node_admin_nodes_validate() + * @see node_filter_form() + * @see node_filter_form_submit() + * @see node_multiple_delete_confirm_submit() + * @ingroup forms + */ function node_multiple_delete_confirm($form, &$form_state, $nodes) { $form['nodes'] = array('#prefix' => '
        ', '#suffix' => '
      ', '#tree' => TRUE); // array_filter returns only elements with TRUE values @@ -590,9 +690,20 @@ t('Delete'), t('Cancel')); } +/** + * Form submission handler for node_multiple_delete_confirm(). + * + * @see node_admin_nodes() + * @see node_admin_nodes_submit() + * @see node_admin_nodes_validate() + * @see node_filter_form() + * @see node_filter_form_submit() + * @see node_multiple_delete_confirm() + */ 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.0/modules/node/node.api.php drupal-7.66/modules/node/node.api.php --- drupal-7.0/modules/node/node.api.php 2011-01-03 19:03:54.000000000 +0100 +++ drupal-7.66/modules/node/node.api.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ uid); + $grants['example_author'] = array($account->uid); return $grants; } @@ -257,6 +265,7 @@ * @return * An array of grants as defined above. * + * @see hook_node_access_records_alter() * @ingroup node_access */ function hook_node_access_records($node) { @@ -315,7 +324,7 @@ * @see hook_node_grants() * @see hook_node_grants_alter() * - * @param &$grants + * @param $grants * The $grants array returned by hook_node_access_records(). * @param $node * The node for which the grants were acquired. @@ -343,12 +352,11 @@ * Alter user access rules when trying to view, edit or delete a node. * * Node access modules establish rules for user access to content. - * hook_node_grants() defines permissions for a user to view, edit or - * delete nodes by building a $grants array that indicates the permissions - * assigned to the user by each node access module. This hook is called to allow - * modules to modify the $grants array by reference, so the interaction of - * multiple node access modules can be altered or advanced business logic can be - * applied. + * hook_node_grants() defines permissions for a user to view, edit or delete + * nodes by building a $grants array that indicates the permissions assigned to + * the user by each node access module. This hook is called to allow modules to + * modify the $grants array by reference, so the interaction of multiple node + * access modules can be altered or advanced business logic can be applied. * * @see hook_node_grants() * @@ -360,15 +368,15 @@ * @see hook_node_access_records() * @see hook_node_access_records_alter() * - * @param &$grants + * @param $grants * The $grants array returned by hook_node_grants(). * @param $account * The user account requesting access to content. * @param $op * The operation being performed, 'view', 'update' or 'delete'. * - * Developers may use this hook to either add additional grants to a user - * or to remove existing grants. These rules are typically based on either the + * Developers may use this hook to either add additional grants to a user or to + * remove existing grants. These rules are typically based on either the * permissions assigned to a user role, or specific attributes of a user * account. * @@ -386,7 +394,7 @@ if ($op != 'view' && !empty($restricted)) { // Now check the roles for this account against the restrictions. foreach ($restricted as $role_id) { - if (isset($user->roles[$role_id])) { + if (isset($account->roles[$role_id])) { $grants = array(); } } @@ -405,10 +413,10 @@ * @return * An array of operations. Each operation is an associative array that may * contain the following key-value pairs: - * - 'label': Required. The label for the operation, displayed in the dropdown + * - label: (required) The label for the operation, displayed in the dropdown * menu. - * - 'callback': Required. The function to call for the operation. - * - 'callback arguments': Optional. An array of additional arguments to pass + * - callback: (required) The function to call for the operation. + * - callback arguments: (optional) An array of additional arguments to pass * to the callback function. */ function hook_node_operations() { @@ -454,9 +462,10 @@ /** * Respond to node deletion. * - * This hook is invoked from node_delete_multiple() after the node has been - * removed from the node table in the database, after the type-specific - * hook_delete() has been invoked, and before field_attach_delete() is called. + * This hook is invoked from node_delete_multiple() after the type-specific + * hook_delete() has been invoked, but before hook_entity_delete and + * field_attach_delete() are called, and before the node is removed from the + * node table in the database. * * @param $node * The node that is being deleted. @@ -490,9 +499,18 @@ /** * Respond to creation of a new node. * - * This hook is invoked from node_save() after the node is inserted into the - * node table in the database, after the type-specific hook_insert() is invoked, - * and after field_attach_insert() is called. + * This hook is invoked from node_save() after the database query that will + * insert the node into the node table is scheduled for execution, after the + * type-specific hook_insert() is invoked, and after field_attach_insert() 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 save operation is entirely completed + * and node_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 node_save() and db_transaction() for more info. * * @param $node * The node that is being created. @@ -509,40 +527,42 @@ } /** - * Act on nodes being loaded from the database. + * Act on arbitrary nodes being loaded from the database. + * + * This hook should be used to add information that is not in the node or node + * revisions table, not to replace information that is in these tables (which + * could interfere with the entity cache). For performance reasons, information + * for all available nodes should be loaded in a single query where possible. * * This hook is invoked during node loading, which is handled by entity_load(), * via classes NodeController and DrupalDefaultEntityController. After the node * information is read from the database or the entity cache, hook_load() is - * invoked on the node's content type module, then field_attach_node_revision() + * invoked on the node's content type module, then field_attach_load_revision() * or field_attach_load() is called, then hook_entity_load() is invoked on all * implementing modules, and finally hook_node_load() is invoked on all * implementing modules. * - * This hook should only be used to add information that is not in the node or - * node revisions table, not to replace information that is in these tables - * (which could interfere with the entity cache). For performance reasons, - * information for all available nodes should be loaded in a single query where - * possible. - * - * The $types parameter allows for your module to have an early return (for - * efficiency) if your module only supports certain node types. However, if your - * module defines a content type, you can use hook_load() to respond to loading - * of just that content type. - * * @param $nodes * An array of the nodes being loaded, keyed by nid. * @param $types - * An array containing the types of the nodes. + * An array containing the node types present in $nodes. Allows for an early + * return for modules that only support certain node types. However, if your + * module defines a content type, you can use hook_load() to respond to + * loading of just that content type. * * For a detailed usage example, see nodeapi_example.module. * * @ingroup node_api_hooks */ function hook_node_load($nodes, $types) { - $result = db_query('SELECT nid, foo FROM {mytable} WHERE nid IN(:nids)', array(':nids' => array_keys($nodes))); - foreach ($result as $record) { - $nodes[$record->nid]->foo = $record->foo; + // Decide whether any of $types are relevant to our purposes. + if (count(array_intersect($types_we_want_to_process, $types))) { + // Gather our extra data for each of these nodes. + $result = db_query('SELECT nid, foo FROM {mytable} WHERE nid IN(:nids)', array(':nids' => array_keys($nodes))); + // Add our extra data to the node objects. + foreach ($result as $record) { + $nodes[$record->nid]->foo = $record->foo; + } } } @@ -552,21 +572,23 @@ * Modules may implement this hook if they want to have a say in whether or not * a given user has access to perform a given operation on a node. * - * The administrative account (user ID #1) always passes any access check, - * so this hook is not called in that case. Users with the "bypass node access" + * The administrative account (user ID #1) always passes any access check, so + * this hook is not called in that case. Users with the "bypass node access" * permission may always view and edit content through the administrative * interface. * - * Note that not all modules will want to influence access on all - * node types. If your module does not want to actively grant or - * block access, return NODE_ACCESS_IGNORE or simply return nothing. - * Blindly returning FALSE will break other node access modules. + * Note that not all modules will want to influence access on all node types. If + * your module does not want to actively grant or block access, return + * NODE_ACCESS_IGNORE or simply return nothing. Blindly returning FALSE will + * break other node access modules. + * + * Also note that this function isn't called for node listings (e.g., RSS feeds, + * the default home page at path 'node', a recent content block, etc.) See + * @link node_access Node access rights @endlink for a full explanation. * - * @link http://api.drupal.org/api/group/node_access/7 More on the node access system @endlink - * @ingroup node_access * @param $node - * The node on which the operation is to be performed, or, if it does - * not yet exist, the type of node to be created. + * Either a node object or the machine name of the content type on which to + * perform the access check. * @param $op * The operation to be performed. Possible values: * - "create" @@ -574,13 +596,14 @@ * - "update" * - "view" * @param $account - * A user object representing the user for whom the operation is to be - * performed. + * The user object to perform the access check operation on. * * @return - * NODE_ACCESS_ALLOW if the operation is to be allowed; - * NODE_ACCESS_DENY if the operation is to be denied; - * NODE_ACCESSS_IGNORE to not affect this operation at all. + * - NODE_ACCESS_ALLOW: if the operation is to be allowed. + * - NODE_ACCESS_DENY: if the operation is to be denied. + * - NODE_ACCESS_IGNORE: to not affect this operation at all. + * + * @ingroup node_access */ function hook_node_access($node, $op, $account) { $type = is_string($node) ? $node : $node->type; @@ -628,20 +651,26 @@ /** * Act on a node being displayed as a search result. * - * This hook is invoked from node_search_execute(), after node_load() - * and node_view() have been called. + * This hook is invoked from node_search_execute(), after node_load() and + * node_view() have been called. * * @param $node * The node being displayed in a search result. * - * @return - * Extra information to be displayed with search result. + * @return array + * Extra information to be displayed with search result. This information + * should be presented as an associative array. It will be concatenated with + * the post information (last updated, author) in the default search result + * theming. + * + * @see template_preprocess_search_result() + * @see search-result.tpl.php * * @ingroup node_api_hooks */ function hook_node_search_result($node) { $comments = db_query('SELECT comment_count FROM {node_comment_statistics} WHERE nid = :nid', array('nid' => $node->nid))->fetchField(); - return format_plural($comments, '1 comment', '@count comments'); + return array('comment' => format_plural($comments, '1 comment', '@count comments')); } /** @@ -667,9 +696,18 @@ /** * Respond to updates to a node. * - * This hook is invoked from node_save() after the node is updated in the node - * table in the database, after the type-specific hook_update() is invoked, and - * after field_attach_update() is called. + * This hook is invoked from node_save() after the database query that will + * update node in the node table is scheduled for execution, after the + * type-specific hook_update() is invoked, and after field_attach_update() 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 save operation is entirely completed + * and node_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 node_save() and db_transaction() for more info. * * @param $node * The node that is being updated. @@ -686,14 +724,14 @@ /** * Act on a node being indexed for searching. * - * This hook is invoked during search indexing, after node_load(), and after - * the result of node_view() is added as $node->rendered to the node object. + * This hook is invoked during search indexing, after node_load(), and after the + * result of node_view() is added as $node->rendered to the node object. * * @param $node * The node being indexed. * - * @return - * Array of additional information to be indexed. + * @return string + * Additional node information to be indexed. * * @ingroup node_api_hooks */ @@ -718,8 +756,8 @@ * * Note: Changes made to the $node object within your hook implementation will * have no effect. The preferred method to change a node's content is to use - * hook_node_presave() instead. If it is really necessary to change - * the node at the validate stage, you can use form_set_value(). + * hook_node_presave() instead. If it is really necessary to change the node at + * the validate stage, you can use form_set_value(). * * @param $node * The node being validated. @@ -750,7 +788,7 @@ * properties. * * @param $node - * The node being updated in response to a form submission. + * The node object being updated in response to a form submission. * @param $form * The form being used to edit the node. * @param $form_state @@ -836,8 +874,8 @@ * * This hook allows a module to define one or more of its own node types. For * example, the blog module uses it to define a blog node-type named "Blog - * entry." The name and attributes of each desired node type are specified in - * an array returned by the hook. + * entry." The name and attributes of each desired node type are specified in an + * array returned by the hook. * * Only module-provided node types should be defined through this hook. User- * provided (or 'custom') node types should be defined only in the 'node_type' @@ -849,26 +887,25 @@ * contains a sub-array for each node type, with the machine-readable type * name as the key. Each sub-array has up to 10 attributes. Possible * attributes: - * - "name": the human-readable name of the node type. Required. - * - "base": the base string used to construct callbacks corresponding to - * this node type. - * (i.e. 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. Required. - * - "description": a brief description of the node type. Required. - * - "help": help information shown to the user when creating a node of - * this type.. Optional (defaults to ''). - * - "has_title": boolean indicating whether or not this node type has a title - * field. Optional (defaults to TRUE). - * - "title_label": the label for the title field of this content type. - * Optional (defaults to 'Title'). - * - "locked": boolean indicating whether the administrator can change the - * machine name of this type. FALSE = changeable (not locked), - * TRUE = unchangeable (locked). Optional (defaults to TRUE). - * - * The machine-readable name of a node type should contain only letters, - * numbers, and underscores. Underscores will be converted into hyphens for the - * purpose of constructing URLs. + * - name: (required) The human-readable name of the node type. + * - 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. + * - has_title: (optional) A Boolean indicating whether or not this node type + * has a title field. + * - title_label: (optional) The label for the title field of this content + * type. + * - locked: (optional) A Boolean indicating whether the administrator can + * change the machine name of this type. FALSE = changeable (not locked), + * TRUE = unchangeable (locked). + * + * The machine name of a node type should contain only letters, numbers, and + * underscores. Underscores will be converted into hyphens for the purpose of + * constructing URLs. * * All attributes of a node type that are defined through this hook (except for * 'locked') can be edited by a site administrator. This includes the @@ -912,20 +949,20 @@ * corresponding to the internal name of the ranking mechanism, such as * 'recent', or 'comments'. The values should be arrays themselves, with the * following keys available: - * - "title": the human readable name of the ranking mechanism. Required. - * - "join": part of a query string to join to 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. Optional. - * - "score": part of a query string to calculate the score for the ranking - * mechanism based on values in the database. This does not need to be - * wrapped in parentheses, as it will be done automatically; it also does - * not need to take the weighted system into account, as it will be done - * automatically. It does, however, need to calculate a decimal between + * - title: (required) The human readable name of the ranking mechanism. + * - 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. + * - score: (required) The part of a query string to calculate the score for + * the ranking mechanism based on values in the database. This does not need + * to be wrapped in parentheses, as it will be done automatically; it also + * does not need to take the weighted system into account, as it will be + * done automatically. It does, however, need to calculate a decimal between * 0 and 1; be careful not to cast the entire score to an integer by - * inadvertently introducing a variable argument. Required. - * - "arguments": if any arguments are required for the score, they can be - * specified in an array here. + * inadvertently introducing a variable argument. + * - arguments: (optional) If any arguments are required for the score, they + * can be specified in an array here. * * @ingroup node_api_hooks */ @@ -937,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)', @@ -952,20 +994,21 @@ /** * Respond to node type creation. * - * This hook is invoked from node_type_save() after the node type is added - * to the database. + * This hook is invoked from node_type_save() after the node type is added to + * the database. * * @param $info * The node type object that is being created. */ function hook_node_type_insert($info) { + drupal_set_message(t('You have just created a content type with a machine name %type.', array('%type' => $info->type))); } /** * Respond to node type updates. * - * This hook is invoked from node_type_save() after the node type is updated - * in the database. + * This hook is invoked from node_type_save() after the node type is updated in + * the database. * * @param $info * The node type object that is being updated. @@ -994,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. @@ -1008,15 +1062,18 @@ */ function hook_delete($node) { db_delete('mytable') - ->condition('nid', $nid->nid) + ->condition('nid', $node->nid) ->execute(); } /** * 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. @@ -1027,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. @@ -1057,8 +1109,6 @@ * displayed automatically by the node module. This hook just needs to * return the node title and form editing fields specific to the node type. * - * For a detailed usage example, see node_example.module. - * * @param $node * The node being added or edited. * @param $form_state @@ -1104,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 @@ -1128,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 @@ -1162,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 @@ -1184,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 @@ -1218,31 +1280,38 @@ /** * Display a node. * - * This is a hook used by node modules. It allows a module to define a - * custom method of displaying its nodes, usually by displaying extra - * information particular to that node type. + * 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 + * the default display. * * @param $node * The node to be displayed, as returned by node_load(). * @param $view_mode * View mode, e.g. 'full', 'teaser', ... - * @return - * $node. The passed $node parameter should be modified as necessary and - * returned so it can be properly presented. Nodes are prepared for display - * by assembling a structured array, formatted as in the Form API, in - * $node->content. As with Form API arrays, the #weight property can be - * used to control the relative positions of added elements. After this - * hook is invoked, node_view() calls field_attach_view() to add field - * views to $node->content, and then invokes hook_node_view() and - * hook_node_view_alter(), so if you want to affect the final - * view of the node, you might consider implementing one of these hooks - * instead. + * @param $langcode + * (optional) A language code to use for rendering. Defaults to the global + * content language of the current request. * - * For a detailed usage example, see node_example.module. + * @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 + * a structured array, formatted as in the Form API, in $node->content. As + * with Form API arrays, the #weight property can be used to control the + * relative positions of added elements. After this hook is invoked, + * node_view() calls field_attach_view() to add field views to $node->content, + * and then invokes hook_node_view() and hook_node_view_alter(), so if you + * want to affect the final view of the node, you might consider implementing + * one of these hooks instead. * * @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.0/modules/node/node.css drupal-7.66/modules/node/node.css --- drupal-7.0/modules/node/node.css 2010-09-17 16:53:21.000000000 +0200 +++ drupal-7.66/modules/node/node.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -/* $Id: node.css,v 1.18 2010/09/17 14:53:21 dries Exp $ */ .node-unpublished { background-color: #fff4f4; diff -Naur drupal-7.0/modules/node/node.info drupal-7.66/modules/node/node.info --- drupal-7.0/modules/node/node.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/node/node.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: node.info,v 1.15 2010/12/20 19:59:42 webchick Exp $ name = Node description = Allows content to be submitted to the site and displayed on pages. package = Core @@ -10,8 +9,7 @@ configure = admin/structure/types stylesheets[all][] = node.css -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/node/node.install drupal-7.66/modules/node/node.install --- drupal-7.0/modules/node/node.install 2011-01-02 18:26:39.000000000 +0100 +++ drupal-7.66/modules/node/node.install 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ TRUE, 'not null' => TRUE, ), + // Defaults to NULL in order to avoid a brief period of potential + // deadlocks on the index. 'vid' => array( 'description' => 'The current {node_revision}.vid version identifier.', 'type' => 'int', 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, + 'not null' => FALSE, + 'default' => NULL, ), 'type' => array( 'description' => 'The {node_type}.type of this node.', @@ -113,6 +114,7 @@ 'uid' => array('uid'), 'tnid' => array('tnid'), 'translate' => array('translate'), + 'language' => array('language'), ), 'unique keys' => array( 'vid' => array('vid'), @@ -396,6 +398,35 @@ ), ); + $schema['history'] = array( + 'description' => 'A record of which {users} have read which {node}s.', + 'fields' => array( + 'uid' => array( + 'description' => 'The {users}.uid that read the {node} nid.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + 'nid' => array( + 'description' => 'The {node}.nid that was read.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'timestamp' => array( + 'description' => 'The Unix timestamp at which the read occurred.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + ), + 'primary key' => array('uid', 'nid'), + 'indexes' => array( + 'nid' => array('nid'), + ), + ); + return $schema; } @@ -420,17 +451,23 @@ * Implements hook_update_dependencies(). */ function node_update_dependencies() { - // Node update 7006 migrates node data to fields and therefore must run after - // the Field module has been enabled, but before upgrading field data. + // node_update_7006() migrates node data to fields and therefore must run + // after all Field modules have been enabled, which happens in + // system_update_7027(). It also needs to query the {filter_format} table to + // get a list of existing text formats, so it must run after + // filter_update_7000(), which creates that table. $dependencies['node'][7006] = array( - 'system' => 7049, - // It must also run after filter_update_7000() because it needs to query - // the list of existing text formats. + 'system' => 7027, 'filter' => 7000, ); - $dependencies['system'][7050] = array( - 'node' => 7006, + + // node_update_7008() migrates role permissions and therefore must run after + // the {role} and {role_permission} tables are properly set up, which happens + // in user_update_7007(). + $dependencies['node'][7008] = array( + 'user' => 7007, ); + return $dependencies; } @@ -439,10 +476,29 @@ * * This function is valid for a database schema version 7000. * - * @ingroup update-api-6.x-to-7.x + * @ingroup update_api */ function _update_7000_node_get_types() { - return db_query('SELECT * FROM {node_type}')->fetchAllAssoc('type', PDO::FETCH_OBJ); + $node_types = db_query('SELECT * FROM {node_type}')->fetchAllAssoc('type', PDO::FETCH_OBJ); + + // Create default settings for orphan nodes. + $all_types = db_query('SELECT DISTINCT type FROM {node}')->fetchCol(); + $extra_types = array_diff($all_types, array_keys($node_types)); + + foreach ($extra_types as $type) { + $type_object = new stdClass(); + $type_object->type = $type; + + // In Drupal 6, whether you have a body field or not is a flag in the node + // type table. If it's enabled, nodes may or may not have an empty string + // for the bodies. As we can't detect what this setting should be in + // Drupal 7 without access to the Drupal 6 node type settings, we assume + // the default, which is to enable the body field. + $type_object->has_body = 1; + $type_object->body_label = 'Body'; + $node_types[$type_object->type] = $type_object; + } + return $node_types; } /** @@ -573,19 +629,6 @@ // Get node type info, specifically the body field settings. $node_types = _update_7000_node_get_types(); - // Create default settings for orphan nodes. - $extra_types = db_query('SELECT DISTINCT type FROM {node} WHERE type NOT IN (:types)', array(':types' => array_keys($node_types)))->fetchCol(); - foreach ($extra_types as $type) { - $type_object = new stdClass; - $type_object->type = $type; - // Always create a body. Querying node_revisions for a non-empty body - // would skip creating body fields for types that have a body but - // the nodes of that type so far had empty bodies. - $type_object->has_body = 1; - $type_object->body_label = 'Body'; - $node_types[$type_object->type] = $type_object; - } - // Add body field instances for existing node types. foreach ($node_types as $node_type) { if ($node_type->has_body) { @@ -595,6 +638,8 @@ 'entity_type' => 'node', 'bundle' => $node_type->type, 'label' => $node_type->body_label, + 'description' => isset($node_type->description) ? $node_type->description : '', + 'required' => (isset($node_type->min_word_count) && $node_type->min_word_count > 0) ? 1 : 0, 'widget' => array( 'type' => 'text_textarea_with_summary', 'settings' => array( @@ -804,5 +849,118 @@ } /** - * @} End of "addtogroup updates-6.x-to-7.x" + * @} End of "addtogroup updates-6.x-to-7.x". + */ + +/** + * @addtogroup updates-7.x-extra + * @{ + */ + +/** + * Update the database from Drupal 6 to match the schema. + */ +function node_update_7011() { + // Drop node moderation field. + db_drop_field('node', 'moderate'); + db_drop_index('node', 'node_moderate'); + + // Change {node_revision}.status field to default to 1. + db_change_field('node_revision', 'status', 'status', array( + 'type' => 'int', + 'not null' => TRUE, + 'default' => 1, + )); + + // Change {node_type}.module field default. + db_change_field('node_type', 'module', 'module', array( + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + )); +} + +/** + * Switches body fields to untranslatable while upgrading from D6 and makes them language neutral. + */ +function node_update_7012() { + // If we are upgrading from D6, then body fields should be set back to + // untranslatable, as D6 did not know about the idea of translating fields, + // but only nodes. If a D7 > D7 update is running we need to skip this update, + // as it is a valid use case to have translatable body fields in this context. + if (variable_get('update_d6', FALSE)) { + // Make node bodies untranslatable: field_update_field() cannot be used + // throughout the upgrade process and we do not have an update counterpart + // for _update_7000_field_create_field(). Hence we are forced to update the + // 'field_config' table directly. This is a safe operation since it is + // being performed while upgrading from D6. Perfoming the same operation + // during a D7 update is highly discouraged. + db_update('field_config') + ->fields(array('translatable' => 0)) + ->condition('field_name', 'body') + ->execute(); + + // Switch field languages to LANGUAGE_NONE, since initially they were + // assigned the node language. + foreach (array('field_data_body', 'field_revision_body') as $table) { + db_update($table) + ->fields(array('language' => LANGUAGE_NONE)) + ->execute(); + } + + node_type_cache_reset(); + } +} + +/** + * Change {node}.vid default value from 0 to NULL to avoid deadlock issues on MySQL. + */ +function node_update_7013() { + db_drop_unique_key('node', 'vid'); + db_change_field('node', 'vid', 'vid', array( + 'description' => 'The current {node_revision}.vid version identifier.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => FALSE, + 'default' => NULL, + )); + db_add_unique_key('node', 'vid', array('vid')); +} + +/** + * 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.0/modules/node/node.js drupal-7.66/modules/node/node.js --- drupal-7.0/modules/node/node.js 2010-12-15 04:42:25.000000000 +0100 +++ drupal-7.66/modules/node/node.js 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -// $Id: node.js,v 1.8 2010/12/15 03:42:25 webchick Exp $ (function ($) { diff -Naur drupal-7.0/modules/node/node.module drupal-7.66/modules/node/node.module --- drupal-7.0/modules/node/node.module 2011-01-03 19:03:54.000000000 +0100 +++ drupal-7.66/modules/node/node.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ ' . t('About') . ''; - $output .= '

      ' . t('The Node module manages the creation, editing, deletion, settings, and display of the main site content. Content items managed by the Node module are typically displayed as pages on your site, and include a title, some meta-data (author, creation time, content type, etc.), and optional fields containing text or other data (fields are managed by the Field module). For more information, see the online handbook entry for Node module.', array('@node' => 'http://drupal.org/handbook/modules/node', '@field' => url('admin/help/field'))) . '

      '; + $output .= '

      ' . t('The Node module manages the creation, editing, deletion, settings, and display of the main site content. Content items managed by the Node module are typically displayed as pages on your site, and include a title, some meta-data (author, creation time, content type, etc.), and optional fields containing text or other data (fields are managed by the Field module). For more information, see the online handbook entry for Node module.', array('@node' => 'http://drupal.org/documentation/modules/node', '@field' => url('admin/help/field'))) . '

      '; $output .= '

      ' . t('Uses') . '

      '; $output .= '
      '; $output .= '
      ' . t('Creating content') . '
      '; @@ -142,6 +141,7 @@ ), 'node_admin_overview' => array( 'variables' => array('name' => NULL, 'type' => NULL), + 'file' => 'content_types.inc', ), 'node_recent_block' => array( 'variables' => array('nodes' => NULL), @@ -178,6 +178,7 @@ 'revision' => 'vid', 'bundle' => 'type', 'label' => 'title', + 'language' => 'language', ), 'bundle keys' => array( 'bundle' => 'type', @@ -200,8 +201,8 @@ ), ); - // Search integration is provided by node.module, so search-related - // view modes for nodes are defined here and not in search.module. + // Search integration is provided by node.module, so search-related view modes + // for nodes are defined here and not in search.module. if (module_exists('search')) { $return['node']['view modes'] += array( 'search_index' => array( @@ -209,7 +210,7 @@ 'custom settings' => FALSE, ), 'search_result' => array( - 'label' => t('Search result'), + 'label' => t('Search result highlighting input'), 'custom settings' => FALSE, ), ); @@ -243,7 +244,7 @@ } /** - * Entity uri callback. + * Implements callback_entity_info_uri(). */ function node_uri($node) { return array( @@ -273,10 +274,10 @@ * Gathers a listing of links to nodes. * * @param $result - * A DB result object from a query to fetch node entities. If your query - * joins the node_comment_statistics table so that the - * comment_count field is available, a title attribute will - * be added to show the number of comments. + * A database result object from a query to fetch node entities. If your + * query joins the {node_comment_statistics} table so that the comment_count + * field is available, a title attribute will be added to show the number of + * comments. * @param $title * A heading for the resulting list. * @@ -296,7 +297,7 @@ } /** - * Update the 'last viewed' timestamp of the specified node for current user. + * Updates the 'last viewed' timestamp of the specified node for current user. * * @param $node * A node object. @@ -315,8 +316,14 @@ } /** - * Retrieves the timestamp at which the current user last viewed the - * specified node. + * Retrieves the timestamp for the current user's last view of a specified node. + * + * @param $nid + * A node ID. + * + * @return + * If a node has been previously viewed by the user, the timestamp in seconds + * of when the last view occurred; otherwise, zero. */ function node_last_viewed($nid) { global $user; @@ -330,12 +337,13 @@ } /** - * Decide on the type of marker to be displayed for a given node. + * Determines the type of marker to be displayed for a given node. * * @param $nid * Node ID whose history supplies the "last viewed" timestamp. * @param $timestamp * Time which is compared against node's "last viewed" timestamp. + * * @return * One of the MARK constants. */ @@ -359,13 +367,13 @@ } /** - * Extract the type name. + * Extracts the type name. * * @param $node * Either a string or object, containing the node type information. * * @return - * Node type of the passed in data. + * Node type of the passed-in data. */ function _node_extract_type($node) { return is_object($node) ? $node->type : $node; @@ -461,6 +469,8 @@ * node_type_save(), and obsolete ones are deleted via a call to * node_type_delete(). See _node_types_build() for an explanation of the new * and obsolete types. + * + * @see _node_types_build() */ function node_types_rebuild() { _node_types_build(TRUE); @@ -483,14 +493,37 @@ /** * Saves a node type to the database. * - * @param $info - * The node type to save, as an object. - * - * @return - * Status flag indicating outcome of the operation. + * @param object $info + * The node type to save; an object with the following properties: + * - type: A string giving the machine name of the node type. + * - name: A string giving the human-readable name of the node type. + * - base: A string that indicates the base string for hook functions. For + * example, 'node_content' is the value used by the UI when creating a new + * node type. + * - description: A string that describes the node type. + * - help: A string giving the help information shown to the user when + * creating a node of this type. + * - 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. 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. + * - has_title: TRUE or FALSE indicating whether this type uses the node title + * field. + * - title_label: A string containing the label for the title. + * - module: A string giving the module defining this type of node. + * - orig_type: A string giving the original machine-readable name of this + * node type. This may be different from the current type name if the + * 'locked' key is FALSE. + * + * @return int + * A status flag indicating the outcome of the operation, either SAVED_NEW or + * SAVED_UPDATED. */ function node_type_save($info) { - $is_existing = FALSE; $existing_type = !empty($info->old_type) ? $info->old_type : $info->type; $is_existing = (bool) db_query_range('SELECT 1 FROM {node_type} WHERE type = :type', 0, 1, array(':type' => $existing_type))->fetchField(); $type = node_type_set_defaults($info); @@ -541,7 +574,7 @@ } /** - * Add default body field to a node type. + * Adds default body field to a node type. * * @param $type * A node type object. @@ -560,7 +593,6 @@ 'field_name' => 'body', 'type' => 'text_with_summary', 'entity_types' => array('node'), - 'translatable' => TRUE, ); $field = field_create_field($field); } @@ -570,7 +602,7 @@ 'entity_type' => 'node', 'bundle' => $type->type, 'label' => $label, - 'widget_type' => 'text_textarea_with_summary', + 'widget' => array('type' => 'text_textarea_with_summary'), 'settings' => array('display_summary' => TRUE), 'display' => array( 'default' => array( @@ -657,8 +689,9 @@ * * @param $rebuild * TRUE to rebuild node types. Equivalent to calling node_types_rebuild(). + * * @return - * Associative array with two components: + * An object with two properties: * - names: Associative array of the names of node types, keyed by the type. * - types: Associative array of node type objects, keyed by the type. * Both of these arrays will include new types that have been defined by @@ -754,17 +787,21 @@ } /** - * Set the default values for a node type. + * Sets the default values for a node type. * - * The defaults are for a type defined through hook_node_info(). - * When populating a custom node type $info should have the 'custom' - * key set to 1. + * The defaults are appropriate for a type defined through hook_node_info(), + * since 'custom' is TRUE for types defined in the user interface, and FALSE + * for types defined by modules. (The 'custom' flag prevents types from being + * deleted through the user interface.) Also, the default for 'locked' is TRUE, + * which prevents users from changing the machine name of the type. * * @param $info - * An object or array containing values to override the defaults. + * (optional) An object or array containing values to override the defaults. + * See hook_node_info() for details on what the array elements mean. Defaults + * to an empty array. * * @return - * A node type object. + * A node type object, with missing values in $info set to their defaults. */ function node_type_set_defaults($info = array()) { $info = (array) $info; @@ -844,12 +881,13 @@ } /** - * Determine whether a node hook exists. + * Determines whether a node hook exists. * * @param $node * A node object or a string containing the node type. * @param $hook * A string containing the name of the hook. + * * @return * TRUE if the $hook exists in the node type of $node. */ @@ -859,7 +897,7 @@ } /** - * Invoke a node hook. + * Invokes a node hook. * * @param $node * A node object or a string containing the node type. @@ -867,6 +905,7 @@ * A string containing the name of the hook. * @param $a2, $a3, $a4 * Arguments to pass on to the hook, after the $node argument. + * * @return * The returned value of the invoked hook. */ @@ -879,11 +918,11 @@ } /** - * Load node entities from the database. + * Loads node entities from the database. * * This function should be used whenever you need to load more than one node - * from the database. Nodes are loaded into memory and will not require - * database access if loaded again during the same page request. + * from the database. Nodes are loaded into memory and will not require database + * access if loaded again during the same page request. * * @see entity_load() * @see EntityFieldQuery @@ -909,7 +948,7 @@ } /** - * Load a node object from the database. + * Loads a node object from the database. * * @param $nid * The node ID. @@ -919,7 +958,7 @@ * Whether to reset the node_load_multiple cache. * * @return - * A fully-populated node object. + * A fully-populated node object, or FALSE if the node is not found. */ function node_load($nid = NULL, $vid = NULL, $reset = FALSE) { $nids = (isset($nid) ? array($nid) : array()); @@ -933,6 +972,9 @@ * * Fills in a few default values, and then invokes hook_prepare() on the node * type module, and hook_node_prepare() on all modules. + * + * @param $node + * A node object. */ function node_object_prepare($node) { // Set up default values, if required. @@ -962,11 +1004,11 @@ } /** - * Perform validation checks on the given node. + * Implements hook_validate(). + * + * Performs validation checks on the given node. */ function node_validate($node, $form, &$form_state) { - $type = node_type_get_type($node); - if (isset($node->nid) && (node_last_changed($node->nid) > $node->changed)) { form_set_error('changed', t('The content on this page has either been modified by another user, or you have already submitted modifications using this form. As a result, your changes cannot be saved.')); } @@ -999,11 +1041,15 @@ } /** - * Prepare node for saving by populating author and creation date. + * Prepares node for saving by populating author and creation date. + * + * @param $node + * A node object. + * + * @return + * An updated node object. */ function node_submit($node) { - global $user; - // A user might assign the node author by entering a user name in the node // form, which we then need to translate to a user ID. if (isset($node->name)) { @@ -1022,7 +1068,7 @@ } /** - * Save changes to a node or add a new node. + * Saves changes to a node or adds a new node. * * @param $node * The $node object to be saved. If $node->nid is @@ -1073,7 +1119,7 @@ $node->log = ''; } } - elseif (empty($node->log)) { + elseif (!isset($node->log) || $node->log === '') { // If we are updating an existing node without adding a new revision, we // need to make sure $node->log is unset whenever it is empty. As long as // $node->log is unset, drupal_write_record() will not attempt to update @@ -1134,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); @@ -1160,6 +1204,13 @@ * Helper function to save a revision with the uid of the current user. * * The resulting revision ID is available afterward in $node->vid. + * + * @param $node + * A node object. + * @param $uid + * The current user's UID. + * @param $update + * (optional) An array of primary keys' field names to update. */ function _node_save_revision($node, $uid, $update = NULL) { $temp_uid = $node->uid; @@ -1175,7 +1226,7 @@ } /** - * Delete a node. + * Deletes a node. * * @param $nid * A node ID. @@ -1185,7 +1236,7 @@ } /** - * Delete multiple nodes. + * Deletes multiple nodes. * * @param $nids * An array of node IDs. @@ -1238,7 +1289,7 @@ } /** - * Delete a node revision. + * Deletes a node revision. * * @param $revision_id * The revision ID to delete. @@ -1263,7 +1314,7 @@ } /** - * Generate an array for rendering the given node. + * Generates an array for rendering the given node. * * @param $node * A node object. @@ -1346,22 +1397,25 @@ // Remove previously built content, if exists. $node->content = array(); + // Allow modules to change the view mode. + $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. if (node_hook($node, 'view')) { - $node = node_invoke($node, 'view', $view_mode); + $node = node_invoke($node, 'view', $view_mode, $langcode); } // Build fields content. // In case of a multiple view, node_view_multiple() already ran the // 'prepare_view' step. An internal flag prevents the operation from running // twice. - field_attach_prepare_view('node', array($node->nid => $node), $view_mode); - entity_prepare_view('node', array($node->nid => $node)); + field_attach_prepare_view('node', array($node->nid => $node), $view_mode, $langcode); + entity_prepare_view('node', array($node->nid => $node), $langcode); $node->content += field_attach_view('node', $node, $view_mode, $langcode); - // Always display a read more link on teasers because we have no way - // to know when a teaser view is different than a full view. + // Always display a read more link on teasers because we have no way to know + // when a teaser view is different than a full view. $links = array(); $node->content['links'] = array( '#theme' => 'links__node', @@ -1386,35 +1440,45 @@ // Allow modules to make their own additions to the node. module_invoke_all('node_view', $node, $view_mode, $langcode); module_invoke_all('entity_view', $node, 'node', $view_mode, $langcode); + + // Make sure the current view mode is stored if no module has already + // populated the related key. + $node->content += array('#view_mode' => $view_mode); } /** - * Generate an array which displays a node detail page. + * Generates an array which displays a node detail page. * * @param $node * A node object. * @param $message * A flag which sets a page title relevant to the revision being viewed. + * * @return - * A $page element suitable for use by drupal_page_render(). + * A $page element suitable for use by drupal_render(). */ function node_show($node, $message = FALSE) { if ($message) { drupal_set_title(t('Revision of %title from %date', array('%title' => $node->title, '%date' => format_date($node->revision_timestamp))), PASS_THROUGH); } + // For markup consistency with other pages, use node_view_multiple() rather than node_view(). + $nodes = node_view_multiple(array($node->nid => $node), 'full'); + // Update the history table, stating that this user viewed this node. node_tag_new($node); - // For markup consistency with other pages, use node_view_multiple() rather than node_view(). - return node_view_multiple(array($node->nid => $node), 'full'); + return $nodes; } /** - * Returns whether the current page is the full page view of the passed in node. + * Returns whether the current page is the full page view of the passed-in node. * * @param $node * A node object. + * + * @return + * The ID of the node if this is a full page view, otherwise FALSE. */ function node_is_page($node) { $page_node = menu_get_object(); @@ -1422,7 +1486,7 @@ } /** - * Process variables for node.tpl.php + * Processes variables for node.tpl.php * * Most themes utilize their own copy of node.tpl.php. The default is located * inside "modules/node/node.tpl.php". Look in there for the full list of @@ -1450,14 +1514,11 @@ $variables['title'] = check_plain($node->title); $variables['page'] = $variables['view_mode'] == 'full' && node_is_page($node); - if (!empty($node->in_preview)) { - unset($node->content['links']); - } - // Flatten the node object's member fields. $variables = array_merge((array) $node, $variables); // Helpful $content variable for templates. + $variables += array('content' => array()); foreach (element_children($variables['elements']) as $key) { $variables['content'][$key] = $variables['elements'][$key]; } @@ -1520,6 +1581,7 @@ ), 'access content overview' => array( 'title' => t('Access the content overview page'), + 'description' => t('Get an overview of all content.', array('@url' => url('admin/content'))), ), 'access content' => array( 'title' => t('View published content'), @@ -1547,7 +1609,7 @@ } /** - * Gather 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. @@ -1615,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. @@ -1685,7 +1747,7 @@ 'extra' => $extra, 'score' => $item->calculated_score, 'snippet' => search_excerpt($keys, $node->rendered), - 'language' => $node->language, + 'language' => entity_language('node', $node), ); } return $results; @@ -1794,6 +1856,7 @@ * An associative array containing: * - form: A render element representing the form. * + * @see node_search_admin() * @ingroup themeable */ function theme_node_search_admin($variables) { @@ -1801,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']; @@ -1815,40 +1878,82 @@ return $output; } -function _node_revision_access($node, $op = 'view') { +/** + * Access callback: Checks node revision access. + * + * @param $node + * The node to check. + * @param $op + * (optional) The specific operation being checked. Defaults to 'view.' + * @param object $account + * (optional) A user object representing the user for whom the operation is + * to be performed. Determines access for a user other than the current user. + * + * @return + * TRUE if the operation may be performed, FALSE otherwise. + * + * @see node_menu() + */ +function _node_revision_access($node, $op = 'view', $account = NULL) { $access = &drupal_static(__FUNCTION__, array()); - if (!isset($access[$node->vid])) { - // To save additional calls to the database, return early if the user - // doesn't have the required permissions. - $map = array('view' => 'view revisions', 'update' => 'revert revisions', 'delete' => 'delete revisions'); - if (isset($map[$op]) && (!user_access($map[$op]) && !user_access('administer nodes'))) { - $access[$node->vid] = FALSE; - return FALSE; + + $map = array( + 'view' => 'view revisions', + 'update' => 'revert revisions', + 'delete' => 'delete revisions', + ); + + if (!$node || !isset($map[$op])) { + // If there was no node to check against, or the $op was not one of the + // supported ones, we return access denied. + return FALSE; + } + + if (!isset($account)) { + $account = $GLOBALS['user']; + } + + // Statically cache access by revision ID, user account ID, and operation. + $cid = $node->vid . ':' . $account->uid . ':' . $op; + + if (!isset($access[$cid])) { + // Perform basic permission checks first. + if (!user_access($map[$op], $account) && !user_access('administer nodes', $account)) { + return $access[$cid] = FALSE; } $node_current_revision = node_load($node->nid); $is_current_revision = $node_current_revision->vid == $node->vid; - // There should be at least two revisions. If the vid of the given node - // and the vid of the current revision differs, then we already have two + // There should be at least two revisions. If the vid of the given node and + // the vid of the current revision differ, then we already have two // different revisions so there is no need for a separate database check. - // Also, if you try to revert to or delete the current revision, that's - // not good. + // Also, if you try to revert to or delete the current revision, that's not + // good. if ($is_current_revision && (db_query('SELECT COUNT(vid) FROM {node_revision} WHERE nid = :nid', array(':nid' => $node->nid))->fetchField() == 1 || $op == 'update' || $op == 'delete')) { - $access[$node->vid] = FALSE; + $access[$cid] = FALSE; } - elseif (user_access('administer nodes')) { - $access[$node->vid] = TRUE; + elseif (user_access('administer nodes', $account)) { + $access[$cid] = TRUE; } else { - // First check the access to the current revision and finally, if the - // node passed in is not the current revision then access to that, too. - $access[$node->vid] = node_access($op, $node_current_revision) && ($is_current_revision || node_access($op, $node)); + // First check the access to the current revision and finally, if the node + // passed in is not the current revision then access to that, too. + $access[$cid] = node_access($op, $node_current_revision, $account) && ($is_current_revision || node_access($op, $node, $account)); } } - return $access[$node->vid]; + + return $access[$cid]; } +/** + * Access callback: Checks whether the user has permission to add a node. + * + * @return + * TRUE if the user has add permission, otherwise FALSE. + * + * @see node_menu() + */ function _node_add_access() { $types = node_type_get_types(); foreach ($types as $type) { @@ -1952,6 +2057,9 @@ 'page callback' => 'node_feed', 'access arguments' => array('access content'), 'type' => MENU_CALLBACK, + // Pass a FALSE and array argument to ensure that additional path components + // are not passed to node_feed(). + 'page arguments' => array(FALSE, array()), ); // @todo Remove this loop when we have a 'description callback' property. // Reset internal static cache of _node_types_build(), forces to rebuild the @@ -2063,25 +2171,56 @@ } /** - * Title callback for a node type. + * Title callback: Returns the unsanitized title of the node type edit form. + * + * @param $type + * The node type object. + * + * @return string + * An unsanitized string that is the title of the node type edit form. + * + * @see node_menu() */ function node_type_page_title($type) { return $type->name; } /** - * Title callback. + * Title callback: Returns the title of the node. + * + * @param $node + * The node object. + * + * @return + * An unsanitized string that is the title of the node. + * + * @see node_menu() */ function node_page_title($node) { return $node->title; } +/** + * Finds the last time a node was changed. + * + * @param $nid + * The ID of a node. + * + * @return + * A unix timestamp indicating the last time the node was changed. + */ function node_last_changed($nid) { return db_query('SELECT changed FROM {node} WHERE nid = :nid', array(':nid' => $nid))->fetch()->changed; } /** - * Return a list of all the existing revision numbers. + * Returns a list of all the existing revision numbers. + * + * @param $node + * The node object. + * + * @return + * An associative array keyed by node revision number. */ function node_revision_list($node) { $revisions = array(); @@ -2161,22 +2300,22 @@ } /** - * Find the most recent nodes that are available to the current user. + * Finds the most recently changed nodes that are available to the current user. * * @param $number * (optional) The maximum number of nodes to find. Defaults to 10. * * @return - * An array of partial node objects or an empty array if there are no recent - * nodes visible to the current user. + * An array of node entities or an empty array if there are no recent nodes + * visible to the current user. */ function node_get_recent($number = 10) { $query = db_select('node', 'n'); if (!user_access('bypass node access')) { - // If the user is able to view their own unpublished nodes, allow them - // to see these in addition to published nodes. Check that they actually - // have some unpublished nodes to view before adding the condition. + // If the user is able to view their own unpublished nodes, allow them to + // see these in addition to published nodes. Check that they actually have + // some unpublished nodes to view before adding the condition. if (user_access('view own unpublished content') && $own_unpublished = db_query('SELECT nid FROM {node} WHERE uid = :uid AND status = :status', array(':uid' => $GLOBALS['user']->uid, ':status' => NODE_NOT_PUBLISHED))->fetchCol()) { $query->condition(db_or() ->condition('n.status', NODE_PUBLISHED) @@ -2190,7 +2329,7 @@ } $nids = $query ->fields('n', array('nid')) - ->orderBy('changed', 'DESC') + ->orderBy('n.changed', 'DESC') ->range(0, $number) ->addTag('node_access') ->execute() @@ -2306,7 +2445,7 @@ } /** - * Form submit handler for block configuration form. + * Form submission handler for node_form_block_admin_configure_alter(). * * @see node_form_block_admin_configure_alter() */ @@ -2338,7 +2477,7 @@ } /** - * Form submit handler for custom block delete form. + * Form submission handler for node_form_block_custom_block_delete_alter(). * * @see node_form_block_custom_block_delete_alter() */ @@ -2363,8 +2502,8 @@ /** * Implements hook_block_list_alter(). * - * Check the content type specific visibilty settings. - * Remove the block if the visibility conditions are not met. + * Check the content type specific visibilty settings. Remove the block if the + * visibility conditions are not met. */ function node_block_list_alter(&$blocks) { global $theme_key; @@ -2417,7 +2556,10 @@ } /** - * A generic function for generating RSS feeds from a set of nodes. + * Generates and prints an RSS feed. + * + * Generates an RSS feed from an array of node IDs, and prints it with an HTTP + * header, with Content Type set to RSS/XML. * * @param $nids * An array of node IDs (nid). Defaults to FALSE so empty feeds can be @@ -2426,7 +2568,8 @@ * @param $channel * An associative array containing title, link, description and other keys, * to be parsed by format_rss_channel() and format_xml_elements(). - * A list of channel elements can be found at the @link http://cyber.law.harvard.edu/rss/rss.html RSS 2.0 Specification. @endlink + * A list of channel elements can be found at the + * @link http://cyber.law.harvard.edu/rss/rss.html RSS 2.0 Specification. @endlink * The link should be an absolute URL. */ function node_feed($nids = FALSE, $channel = array()) { @@ -2436,7 +2579,7 @@ $nids = db_select('node', 'n') ->fields('n', array('nid', 'created')) ->condition('n.promote', 1) - ->condition('status', 1) + ->condition('n.status', 1) ->orderBy('n.created', 'DESC') ->range(0, variable_get('feed_default_items', 10)) ->addTag('node_access') @@ -2446,7 +2589,6 @@ $item_length = variable_get('feed_item_length', 'fulltext'); $namespaces = array('xmlns:dc' => 'http://purl.org/dc/elements/1.1/'); - $teaser = ($item_length == 'teaser'); // Load all nodes to be rendered. $nodes = node_load_multiple($nids); @@ -2456,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')) ); @@ -2500,7 +2643,7 @@ } /** - * Construct a drupal_render() style array from an array of loaded nodes. + * Constructs a drupal_render() style array from an array of loaded nodes. * * @param $nodes * An array of nodes as returned by node_load_multiple(). @@ -2509,35 +2652,51 @@ * @param $weight * An integer representing the weight of the first node in the list. * @param $langcode - * (optional) A language code to use for rendering. Defaults to the global - * content language of the current request. + * (optional) A language code to use for rendering. Defaults to NULL which is + * the global content language of the current request. * * @return * 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); - entity_prepare_view('node', $nodes); $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; } /** - * Menu callback; Generate a listing of promoted nodes. + * Menu callback: Generates a listing of promoted nodes. + * + * @return array + * An array in the format expected by drupal_render(). + * + * @see node_menu() */ function node_page_default() { $select = db_select('node', 'n') ->fields('n', array('nid', 'sticky', 'created')) - ->condition('promote', 1) - ->condition('status', 1) - ->orderBy('sticky', 'DESC') - ->orderBy('created', 'DESC') + ->condition('n.promote', 1) + ->condition('n.status', 1) + ->orderBy('n.sticky', 'DESC') + ->orderBy('n.created', 'DESC') ->extend('PagerDefault') ->limit(variable_get('default_nodes_main', 10)) ->addTag('node_access'); @@ -2579,7 +2738,15 @@ } /** - * Menu callback; view a single node. + * Menu callback: Displays a single node. + * + * @param $node + * The node object. + * + * @return + * A page array suitable for use by drupal_render(). + * + * @see node_menu() */ function node_page_view($node) { // If there is a menu link to this node, the link becomes the last part @@ -2608,7 +2775,7 @@ } /** - * Index a single node. + * Indexes a single node. * * @param $node * The node to index. @@ -2712,7 +2879,7 @@ } /** - * Form API callback for the search form. Registered in node_form_alter(). + * Form validation handler for node_form_alter(). */ function node_search_validate($form, &$form_state) { // Initialize using any existing basic search keywords. @@ -2720,7 +2887,7 @@ // Insert extra restrictions into the search keywords string. if (isset($form_state['values']['type']) && is_array($form_state['values']['type'])) { - // Retrieve selected types - Forms API sets the value of unselected + // Retrieve selected types - Form API sets the value of unselected // checkboxes to 0. $form_state['values']['type'] = array_filter($form_state['values']['type']); if (count($form_state['values']['type'])) { @@ -2760,8 +2927,8 @@ * @{ * The node access system determines who can do what to which nodes. * - * In determining access rights for a node, node_access() first checks - * whether the user has the "bypass node access" permission. Such users have + * In determining access rights for a node, node_access() first checks whether + * the user has the "bypass node access" permission. Such users have * unrestricted access to all nodes. user 1 will always pass this check. * * Next, all implementations of hook_node_access() will be called. Each @@ -2779,12 +2946,17 @@ * that this table is a list of grants; any matching row is sufficient to * grant access to the node. * - * In node listings, the process above is followed except that - * hook_node_access() is not called on each node for performance reasons and for - * proper functioning of the pager 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" to ensure that only nodes to which the user has access - * are retrieved. + * In node listings (lists of nodes generated from a select query, such as the + * default home page at path 'node', an RSS feed, a recent content block, etc.), + * the process above is followed except that hook_node_access() is not called on + * each node for performance reasons and for proper functioning of the pager + * 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(). 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 @@ -2797,8 +2969,7 @@ */ /** - * Determine whether the current user may perform the given operation on the - * specified node. + * Determines whether the current user may perform the operation on the node. * * @param $op * The operation to be performed on the node. Possible values are: @@ -2812,12 +2983,11 @@ * @param $account * Optional, a user object representing the user for whom the operation is to * be performed. Determines access for a user other than the current user. + * * @return * TRUE if the operation may be performed, FALSE otherwise. */ function node_access($op, $node, $account = NULL) { - global $user; - $rights = &drupal_static(__FUNCTION__, array()); if (!$node || !in_array($op, array('view', 'update', 'delete', 'create'), TRUE)) { @@ -2827,7 +2997,7 @@ } // If no user object is supplied, the access check is for the current user. if (empty($account)) { - $account = $user; + $account = $GLOBALS['user']; } // $node may be either an object or a node type. Since node types cannot be @@ -2904,7 +3074,7 @@ return $result; } elseif (is_object($node) && $op == 'view' && $node->status) { - // If no modules implement hook_node_grants(), the default behaviour is to + // If no modules implement hook_node_grants(), the default behavior is to // allow all users to view published nodes, so reflect that here. $rights[$account->uid][$cid][$op] = TRUE; return TRUE; @@ -2946,12 +3116,12 @@ * * @param $type * The machine-readable name of the node type. + * * @return array * An array of permission names and descriptions. */ function node_list_permissions($type) { $info = node_type_get_type($type); - $type = check_plain($info->type); // Build standard list of node permissions for this type. $perms = array( @@ -2980,11 +3150,11 @@ * * By default, this will include all node types in the system. To exclude a * specific node from getting permissions defined for it, set the - * node_permissions_$type variable to 0. Core does not provide an interface - * for doing so, however, contrib modules may exclude their own nodes in + * node_permissions_$type variable to 0. Core does not provide an interface for + * doing so. However, contrib modules may exclude their own nodes in * hook_install(). Alternatively, contrib modules may configure all node types - * at once, or decide to apply some other hook_node_access() implementation - * to some or all node types. + * at once, or decide to apply some other hook_node_access() implementation to + * some or all node types. * * @return * An array of node types managed by this module. @@ -3003,21 +3173,22 @@ } /** - * Fetch an array of permission IDs granted to the given user ID. + * Fetches an array of permission IDs granted to the given user ID. * * The implementation here provides only the universal "all" grant. A node - * access module should implement hook_node_grants() to provide a grant - * list for the user. + * access module should implement hook_node_grants() to provide a grant list for + * the user. * - * After the default grants have been loaded, we allow modules to alter - * the grants array by reference. This hook allows for complex business - * logic to be applied when integrating multiple node access modules. + * After the default grants have been loaded, we allow modules to alter the + * grants array by reference. This hook allows for complex business logic to be + * applied when integrating multiple node access modules. * * @param $op * The operation that the user is trying to perform. * @param $account * The user object for the user performing the operation. If omitted, the * current user is used. + * * @return * An associative array in which the keys are realms, and the values are * arrays of grants for those realms. @@ -3105,14 +3276,13 @@ /** * Implements hook_query_TAG_alter(). * - * This is the hook_query_alter() for queries tagged with 'node_access'. - * It adds node access checks for the user account given by the 'account' - * meta-data (or global $user if not provided), for an operation given by - * the 'op' meta-data (or 'view' if not provided; other possible values are - * 'update' and 'delete'). + * This is the hook_query_alter() for queries tagged with 'node_access'. It adds + * node access checks for the user account given by the 'account' meta-data (or + * global $user if not provided), for an operation given by the 'op' meta-data + * (or 'view' if not provided; other possible values are 'update' and 'delete'). */ function node_query_node_access_alter(QueryAlterableInterface $query) { - _node_query_node_access_alter($query, 'node', 'node'); + _node_query_node_access_alter($query, 'node'); } /** @@ -3123,22 +3293,31 @@ * conditions are added for field values belonging to nodes only. */ function node_query_entity_field_access_alter(QueryAlterableInterface $query) { - _node_query_node_access_alter($query, $query->getMetaData('base_table'), 'entity'); + _node_query_node_access_alter($query, 'entity'); } /** * 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 $base_table - * The table holding node ids. * @param $type * Either 'node' or 'entity' depending on what sort of query it is. See * node_query_node_access_alter() and node_query_entity_field_access_alter() * for more. */ -function _node_query_node_access_alter($query, $base_table, $type) { +function _node_query_node_access_alter($query, $type) { global $user; // Read meta-data from query, if provided. @@ -3150,8 +3329,8 @@ } // If $account can bypass node access, or there are no node access modules, - // or the operation is 'view' and the $acount has a global view grant (i.e., - // a view grant for node ID 0), we don't need to alter the query. + // or the operation is 'view' and the $account has a global view grant + // (such as a view grant for node ID 0), we don't need to alter the query. if (user_access('bypass node access', $account)) { return; } @@ -3162,14 +3341,58 @@ return; } - // Prevent duplicate records. - $query->distinct(); + $tables = $query->getTables(); + $base_table = $query->getMetaData('base_table'); + // If no base table is specified explicitly, search for one. + if (!$base_table) { + $fallback = ''; + foreach ($tables as $alias => $table_info) { + if (!($table_info instanceof SelectQueryInterface)) { + $table = $table_info['table']; + // If the node table is in the query, it wins immediately. + if ($table == 'node') { + $base_table = $table; + break; + } + // Check whether the table has a foreign key to node.nid. If it does, + // do not run this check again as we found a base table and only node + // can triumph that. + if (!$base_table) { + // The schema is cached. + $schema = drupal_get_schema($table); + if (isset($schema['fields']['nid'])) { + if (isset($schema['foreign keys'])) { + foreach ($schema['foreign keys'] as $relation) { + if ($relation['table'] === 'node' && $relation['columns'] === array('nid' => 'nid')) { + $base_table = $table; + } + } + } + else { + // At least it's a nid. A table with a field called nid is very + // very likely to be a node.nid in a node access query. + $fallback = $table; + } + } + } + } + } + // If there is nothing else, use the fallback. + if (!$base_table) { + if ($fallback) { + watchdog('security', 'Your node listing query is using @fallback as a base table in a query tagged for node access. This might not be secure and might not even work. Specify foreign keys in your schema to node.nid ', array('@fallback' => $fallback), WATCHDOG_WARNING); + $base_table = $fallback; + } + else { + throw new Exception(t('Query tagged for node access but there is no nid. Add foreign keys to node.nid in schema to fix.')); + } + } + } - // Find all instances of the {node} table being joined -- could appear + // Find all instances of the base table being joined -- could appear // more than once in the query, and could be aliased. Join each one to // the node_access table. - $tables = $query->getTables(); $grants = node_access_grants($op, $account); if ($type == 'entity') { // The original query looked something like: @@ -3188,23 +3411,16 @@ // @endcode // // So instead of directly adding to the query object, we need to collect - // in a separate db_and() object and then at the end add it to the query. - $entity_conditions = db_and(); + // all of the node access conditions in a separate db_and() object and + // then add it to the query at the end. + $node_conditions = db_and(); } foreach ($tables as $nalias => $tableinfo) { $table = $tableinfo['table']; if (!($table instanceof SelectQueryInterface) && $table == $base_table) { - - // The node_access table has the access grants for any given node so JOIN - // it to the table containing the nid which can be either the node - // table or a field value table. - if ($type == 'node') { - $access_alias = $query->join('node_access', 'na', '%alias.nid = ' . $nalias . '.nid'); - } - else { - $access_alias = $query->leftJoin('node_access', 'na', '%alias.nid = ' . $nalias . '.entity_id'); - $base_alias = $nalias; - } + // Set the subquery. + $subquery = db_select('node_access', 'na') + ->fields('na', array('nid')); $grant_conditions = db_or(); // If any grant exists for the specified user, then user has access @@ -3212,40 +3428,50 @@ foreach ($grants as $realm => $gids) { foreach ($gids as $gid) { $grant_conditions->condition(db_and() - ->condition($access_alias . '.gid', $gid) - ->condition($access_alias . '.realm', $realm) + ->condition('na.gid', $gid) + ->condition('na.realm', $realm) ); } } - $count = count($grant_conditions->conditions()); - if ($type == 'node') { - if ($count) { - $query->condition($grant_conditions); - } - $query->condition($access_alias . '.grant_' . $op, 1, '>='); + // Attach conditions to the subquery for nodes. + if (count($grant_conditions->conditions())) { + $subquery->condition($grant_conditions); + } + $subquery->condition('na.grant_' . $op, 1, '>='); + $field = 'nid'; + // Now handle entities. + if ($type == 'entity') { + // Set a common alias for entities. + $base_alias = $nalias; + $field = 'entity_id'; + } + $subquery->where("$nalias.$field = na.nid"); + + // For an entity query, attach the subquery to entity conditions. + if ($type == 'entity') { + $node_conditions->exists($subquery); } + // Otherwise attach it to the node query itself. else { - if ($count) { - $entity_conditions->condition($grant_conditions); - } - $entity_conditions->condition($access_alias . '.grant_' . $op, 1, '>='); + $query->exists($subquery); } } } - if ($type == 'entity' && count($entity_conditions->conditions())) { + if ($type == 'entity' && count($subquery->conditions())) { // All the node access conditions are only for field values belonging to // nodes. - $entity_conditions->condition("$base_alias.entity_type", 'node'); + $node_conditions->condition("$base_alias.entity_type", 'node'); $or = db_or(); - $or->condition($entity_conditions); + $or->condition($node_conditions); // If the field value belongs to a non-node entity type then this function // does not do anything with it. $or->condition("$base_alias.entity_type", 'node', '<>'); // Add the compiled set of rules to the query. $query->condition($or); } + } /** @@ -3291,12 +3517,14 @@ * * If a realm is provided, it will only delete grants from that realm, but it * will always delete a grant from the 'all' realm. Modules that utilize - * node_access can use this function when doing mass updates due to widespread + * node_access() can use this function when doing mass updates due to widespread * permission changes. * + * Note: Don't call this function directly from a contributed module. Call + * node_access_acquire_grants() instead. + * * @param $node - * The $node being written to. All that is necessary is that it contains a - * nid. + * The node whose grants are being written. * @param $grants * A list of grants to write. Each grant is an array that must contain the * following keys: realm, gid, grant_view, grant_update, grant_delete. @@ -3304,10 +3532,14 @@ * is a module-defined id to define grant privileges. each grant_* field * is a boolean value. * @param $realm - * If provided, only read/write grants for that realm. + * (optional) If provided, read/write grants for that realm only. Defaults to + * NULL. * @param $delete - * If false, do not delete records. This is only for optimization purposes, - * and assumes the caller has already performed a mass delete of some form. + * (optional) If false, does not delete records. This is only for optimization + * purposes, and assumes the caller has already performed a mass delete of + * some form. Defaults to TRUE. + * + * @see node_access_acquire_grants() */ function node_access_write_grants($node, $grants, $realm = NULL, $delete = TRUE) { if ($delete) { @@ -3336,21 +3568,23 @@ } /** - * Flag / unflag the node access grants for rebuilding, or read the current - * value of the flag. + * Flags or unflags the node access grants for rebuilding. * + * If the argument isn't specified, the current value of the flag is returned. * When the flag is set, a message is displayed to users with 'access * administration pages' permission, pointing to the 'rebuild' confirm form. * This can be used as an alternative to direct node_access_rebuild calls, * allowing administrators to decide when they want to perform the actual - * (possibly time consuming) rebuild. - * When unsure the current user is an administrator, node_access_rebuild - * should be used instead. + * (possibly time consuming) rebuild. When unsure if the current user is an + * administrator, node_access_rebuild() should be used instead. * * @param $rebuild * (Optional) The boolean value to be written. - * @return - * (If no value was provided for $rebuild) The current value of the flag. + * + * @return + * The current value of the flag if no value was provided for $rebuild. + * + * @see node_access_rebuild() */ function node_access_needs_rebuild($rebuild = NULL) { if (!isset($rebuild)) { @@ -3365,15 +3599,15 @@ } /** - * Rebuild the node access database. This is occasionally needed by modules - * that make system-wide changes to access levels. + * Rebuilds the node access database. * - * When the rebuild is required by an admin-triggered action (e.g module - * settings form), calling node_access_needs_rebuild(TRUE) instead of + * This is occasionally needed by modules that make system-wide changes to + * access levels. When the rebuild is required by an admin-triggered action (e.g + * module settings form), calling node_access_needs_rebuild(TRUE) instead of * node_access_rebuild() lets the user perform his changes and actually * rebuild only once he is done. * - * Note : As of Drupal 6, node access modules are not required to (and actually + * Note: As of Drupal 6, node access modules are not required to (and actually * should not) call node_access_rebuild() in hook_enable/disable anymore. * * @see node_access_needs_rebuild() @@ -3404,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 @@ -3437,18 +3672,23 @@ } /** - * Batch operation for node_access_rebuild_batch. + * 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 + * batch processing engine interrupts processing and sends progress feedback + * after 1 second execution time. * - * This is a multistep operation : we go through all nodes by packs of 20. - * The batch processing engine interrupts processing and sends progress - * feedback after 1 second execution time. + * @param array $context + * An array of contextual key/value information for rebuild batch process. */ function _node_access_rebuild_batch_operation(&$context) { if (empty($context['sandbox'])) { // 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. @@ -3472,7 +3712,16 @@ } /** - * Post-processing for node_access_rebuild_batch. + * Implements callback_batch_finished(). + * + * Performs post-processing for node_access_rebuild(). + * + * @param bool $success + * A boolean indicating whether the re-build process has completed. + * @param array $results + * An array of results information. + * @param array $operations + * An array of function calls (not used in this function). */ function _node_access_rebuild_batch_finished($success, $results, $operations) { if ($success) { @@ -3489,7 +3738,6 @@ * @} End of "defgroup node_access". */ - /** * @defgroup node_content Hook implementations for user-created content types * @{ @@ -3525,6 +3773,7 @@ /** * Implements hook_forms(). + * * All node forms share the same form handler. */ function node_forms() { @@ -3609,6 +3858,12 @@ /** * Sets the status of a node to 1 (published). * + * @param $node + * A node object. + * @param $context + * (optional) Array of additional information about what triggered the action. + * Not used for this action. + * * @ingroup actions */ function node_publish_action($node, $context = array()) { @@ -3619,6 +3874,12 @@ /** * Sets the status of a node to 0 (unpublished). * + * @param $node + * A node object. + * @param $context + * (optional) Array of additional information about what triggered the action. + * Not used for this action. + * * @ingroup actions */ function node_unpublish_action($node, $context = array()) { @@ -3629,6 +3890,12 @@ /** * Sets the sticky-at-top-of-list property of a node to 1. * + * @param $node + * A node object. + * @param $context + * (optional) Array of additional information about what triggered the action. + * Not used for this action. + * * @ingroup actions */ function node_make_sticky_action($node, $context = array()) { @@ -3639,6 +3906,12 @@ /** * Sets the sticky-at-top-of-list property of a node to 0. * + * @param $node + * A node object. + * @param $context + * (optional) Array of additional information about what triggered the action. + * Not used for this action. + * * @ingroup actions */ function node_make_unsticky_action($node, $context = array()) { @@ -3649,6 +3922,12 @@ /** * Sets the promote property of a node to 1. * + * @param $node + * A node object. + * @param $context + * (optional) Array of additional information about what triggered the action. + * Not used for this action. + * * @ingroup actions */ function node_promote_action($node, $context = array()) { @@ -3659,6 +3938,12 @@ /** * Sets the promote property of a node to 0. * + * @param $node + * A node object. + * @param $context + * (optional) Array of additional information about what triggered the action. + * Not used for this action. + * * @ingroup actions */ function node_unpromote_action($node, $context = array()) { @@ -3669,6 +3954,9 @@ /** * Saves a node. * + * @param $node + * The node to be saved. + * * @ingroup actions */ function node_save_action($node) { @@ -3685,6 +3973,9 @@ * Array with the following elements: * - 'owner_uid': User ID to assign to the node. * + * @see node_assign_owner_action_form() + * @see node_assign_owner_action_validate() + * @see node_assign_owner_action_submit() * @ingroup actions */ function node_assign_owner_action($node, $context) { @@ -3695,6 +3986,16 @@ /** * Generates the settings form for node_assign_owner_action(). + * + * @param $context + * Array of additional information about what triggered the action. Includes + * the following elements: + * - 'owner_uid': User ID to assign to the node. + * + * @see node_assign_owner_action_submit() + * @see node_assign_owner_action_validate() + * + * @ingroup forms */ function node_assign_owner_action_form($context) { $description = t('The username of the user to which you would like to assign ownership.'); @@ -3735,6 +4036,8 @@ /** * Validates settings form for node_assign_owner_action(). + * + * @see node_assign_owner_action_submit() */ function node_assign_owner_action_validate($form, $form_state) { $exists = (bool) db_query_range('SELECT 1 FROM {users} WHERE name = :name', 0, 1, array(':name' => $form_state['values']['owner_name']))->fetchField(); @@ -3745,6 +4048,8 @@ /** * Saves settings form for node_assign_owner_action(). + * + * @see node_assign_owner_action_validate() */ function node_assign_owner_action_submit($form, $form_state) { // Username can change, so we need to store the ID, not the username. @@ -3754,6 +4059,14 @@ /** * Generates settings form for node_unpublish_by_keyword_action(). + * + * @param array $context + * Array of additional information about what triggered this action. + * + * @return array + * A form array. + * + * @see node_unpublish_by_keyword_action_submit() */ function node_unpublish_by_keyword_action_form($context) { $form['keywords'] = array( @@ -3800,24 +4113,25 @@ */ function node_requirements($phase) { $requirements = array(); - // Ensure translations don't break at install time - $t = get_t(); - // Only show rebuild button if there are either 0, or 2 or more, rows - // in the {node_access} table, or if there are modules that - // implement hook_node_grants(). - $grant_count = db_query('SELECT COUNT(*) FROM {node_access}')->fetchField(); - if ($grant_count != 1 || count(module_implements('node_grants')) > 0) { - $value = format_plural($grant_count, 'One permission in use', '@count permissions in use', array('@count' => $grant_count)); - } else { - $value = $t('Disabled'); - } - $description = $t('If the site is experiencing problems with permissions to content, you may have to rebuild the permissions cache. Rebuilding will remove all privileges to content and replace them with permissions based on the current modules and settings. Rebuilding may take some time if there is a lot of content or complex permission settings. After rebuilding has completed, content will automatically use the new permissions.'); - - $requirements['node_access'] = array( - 'title' => $t('Node Access Permissions'), - 'value' => $value, - 'description' => $description . ' ' . l(t('Rebuild permissions'), 'admin/reports/status/rebuild'), - ); + if ($phase === 'runtime') { + // Only show rebuild button if there are either 0, or 2 or more, rows + // in the {node_access} table, or if there are modules that + // implement hook_node_grants(). + $grant_count = db_query('SELECT COUNT(*) FROM {node_access}')->fetchField(); + if ($grant_count != 1 || count(module_implements('node_grants')) > 0) { + $value = format_plural($grant_count, 'One permission in use', '@count permissions in use', array('@count' => $grant_count)); + } + else { + $value = t('Disabled'); + } + $description = t('If the site is experiencing problems with permissions to content, you may have to rebuild the permissions cache. Rebuilding will remove all privileges to content and replace them with permissions based on the current modules and settings. Rebuilding may take some time if there is a lot of content or complex permission settings. After rebuilding has completed, content will automatically use the new permissions.'); + + $requirements['node_access'] = array( + 'title' => t('Node Access Permissions'), + 'value' => $value, + 'description' => $description . ' ' . l(t('Rebuild permissions'), 'admin/reports/status/rebuild'), + ); + } return $requirements; } diff -Naur drupal-7.0/modules/node/node.pages.inc drupal-7.66/modules/node/node.pages.inc --- drupal-7.0/modules/node/node.pages.inc 2010-11-26 20:23:01.000000000 +0100 +++ drupal-7.66/modules/node/node.pages.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,14 +1,12 @@ type . '_node_form', $node); } +/** + * Page callback: Displays add content links for available content types. + * + * Redirects to node/add/[type] if only one content type is available. + * + * @see node_menu() + */ function node_add_page() { $item = menu_get_item(); $content = system_admin_menu_block($item); @@ -57,6 +62,12 @@ /** * Returns a node submission form. + * + * @param $type + * The node type for the submitted node. + * + * @return + * The themed form. */ function node_add($type) { global $user; @@ -69,6 +80,12 @@ return $output; } +/** + * Form validation handler for node_form(). + * + * @see node_form() + * @see node_form_submit() + */ function node_form_validate($form, &$form_state) { // $form_state['node'] contains the actual entity being edited, but we must // not update it with form values that have not yet been validated, so we @@ -79,7 +96,13 @@ } /** - * Generate the node add/edit form array. + * Form constructor for the node add/edit form. + * + * @see node_form_validate() + * @see node_form_submit() + * @see node_form_build_preview() + * @see node_form_delete_submit() + * @ingroup forms */ function node_form($form, &$form_state, $node) { global $user; @@ -300,12 +323,17 @@ } $form += array('#submit' => array()); - field_attach_form('node', $node, $form, $form_state, $node->language); + field_attach_form('node', $node, $form, $form_state, entity_language('node', $node)); return $form; } /** - * Button submit function: handle the 'Delete' button on the node form. + * Form submission handler for node_form(). + * + * Handles the 'Delete' button on the node form. + * + * @see node_form() + * @see node_form_validate() */ function node_form_delete_submit($form, &$form_state) { $destination = array(); @@ -317,7 +345,14 @@ $form_state['redirect'] = array('node/' . $node->nid . '/delete', array('query' => $destination)); } - +/** + * Form submission handler for node_form(). + * + * Handles the 'Preview' button on the node form. + * + * @see node_form() + * @see node_form_validate() + */ function node_form_build_preview($form, &$form_state) { $node = node_form_submit_build_node($form, $form_state); $form_state['node_preview'] = node_preview($node); @@ -325,38 +360,48 @@ } /** - * Generate a node preview. + * Generates a node preview. + * + * @param $node + * The node to preview. + * + * @return + * An HTML-formatted string of a node preview. + * + * @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); @@ -371,6 +416,7 @@ * An associative array containing: * - node: The node object which is being previewed. * + * @see node_preview() * @ingroup themeable */ function theme_node_preview($variables) { @@ -401,6 +447,12 @@ return $output; } +/** + * Form submission handler for node_form(). + * + * @see node_form() + * @see node_form_validate() + */ function node_form_submit($form, &$form_state) { $node = node_form_submit_build_node($form, $form_state); $insert = empty($node->nid); @@ -420,7 +472,7 @@ if ($node->nid) { $form_state['values']['nid'] = $node->nid; $form_state['nid'] = $node->nid; - $form_state['redirect'] = 'node/' . $node->nid; + $form_state['redirect'] = node_access('view', $node) ? 'node/' . $node->nid : ''; } else { // In the unlikely case something went wrong on save, the node will be @@ -466,7 +518,9 @@ } /** - * Menu callback -- ask for confirmation of node deletion + * Form constructor for the node deletion confirmation form. + * + * @see node_delete_confirm_submit() */ function node_delete_confirm($form, &$form_state, $node) { $form['#node'] = $node; @@ -482,12 +536,15 @@ } /** - * Execute node deletion + * Executes node deletion. + * + * @see node_delete_confirm() */ function node_delete_confirm_submit($form, &$form_state) { 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))); } @@ -496,7 +553,15 @@ } /** - * Generate an overview table of older revisions of a node. + * Generates an overview table of older revisions of a node. + * + * @param $node + * A node object. + * + * @return array + * An array as expected by drupal_render(). + * + * @see node_menu() */ function node_revision_overview($node) { drupal_set_title(t('Revisions for %title', array('%title' => $node->title)), PASS_THROUGH); @@ -547,13 +612,26 @@ } /** - * Ask for confirmation of the reversion to prevent against CSRF attacks. + * Asks for confirmation of the reversion to prevent against CSRF attacks. + * + * @param int $node_revision + * The node revision ID. + * + * @return array + * An array as expected by drupal_render(). + * + * @see node_menu() + * @see node_revision_revert_confirm_submit() + * @ingroup forms */ function node_revision_revert_confirm($form, $form_state, $node_revision) { $form['#node_revision'] = $node_revision; return confirm_form($form, t('Are you sure you want to revert to the revision from %revision-date?', array('%revision-date' => format_date($node_revision->revision_timestamp))), 'node/' . $node_revision->nid . '/revisions', '', t('Revert'), t('Cancel')); } +/** + * Form submission handler for node_revision_revert_confirm(). + */ function node_revision_revert_confirm_submit($form, &$form_state) { $node_revision = $form['#node_revision']; $node_revision->revision = 1; @@ -566,11 +644,29 @@ $form_state['redirect'] = 'node/' . $node_revision->nid . '/revisions'; } +/** + * Form constructor for the revision deletion confirmation form. + * + * This form prevents against CSRF attacks. + * + * @param $node_revision + * The node revision ID. + * + * @return + * An array as expected by drupal_render(). + * + * @see node_menu() + * @see node_revision_delete_confirm_submit() + * @ingroup forms + */ function node_revision_delete_confirm($form, $form_state, $node_revision) { $form['#node_revision'] = $node_revision; return confirm_form($form, t('Are you sure you want to delete the revision from %revision-date?', array('%revision-date' => format_date($node_revision->revision_timestamp))), 'node/' . $node_revision->nid . '/revisions', t('This action cannot be undone.'), t('Delete'), t('Cancel')); } +/** + * Form submission handler for node_revision_delete_confirm(). + */ function node_revision_delete_confirm_submit($form, &$form_state) { $node_revision = $form['#node_revision']; node_revision_delete($node_revision->vid); diff -Naur drupal-7.0/modules/node/node.test drupal-7.66/modules/node/node.test --- drupal-7.0/modules/node/node.test 2010-12-17 20:28:14.000000000 +0100 +++ drupal-7.66/modules/node/node.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,10 +1,34 @@ profile != 'standard') { + $this->drupalCreateContentType(array('type' => 'page', 'name' => 'Basic page')); + $this->drupalCreateContentType(array('type' => 'article', 'name' => 'Article')); + } + } +} /** * Test the node_load_multiple() function. */ -class NodeLoadMultipleUnitTest extends DrupalWebTestCase { +class NodeLoadMultipleTestCase extends DrupalWebTestCase { public static function getInfo() { return array( @@ -31,53 +55,53 @@ // Confirm that promoted nodes appear in the default node listing. $this->drupalGet('node'); - $this->assertText($node1->title, t('Node title appears on the default listing.')); - $this->assertText($node2->title, t('Node title appears on the default listing.')); - $this->assertNoText($node3->title, t('Node title does not appear in the default listing.')); - $this->assertNoText($node4->title, t('Node title does not appear in the default listing.')); + $this->assertText($node1->title, 'Node title appears on the default listing.'); + $this->assertText($node2->title, 'Node title appears on the default listing.'); + $this->assertNoText($node3->title, 'Node title does not appear in the default listing.'); + $this->assertNoText($node4->title, 'Node title does not appear in the default listing.'); // Load nodes with only a condition. Nodes 3 and 4 will be loaded. $nodes = node_load_multiple(NULL, array('promote' => 0)); - $this->assertEqual($node3->title, $nodes[$node3->nid]->title, t('Node was loaded.')); - $this->assertEqual($node4->title, $nodes[$node4->nid]->title, t('Node was loaded.')); + $this->assertEqual($node3->title, $nodes[$node3->nid]->title, 'Node was loaded.'); + $this->assertEqual($node4->title, $nodes[$node4->nid]->title, 'Node was loaded.'); $count = count($nodes); - $this->assertTrue($count == 2, t('@count nodes loaded.', array('@count' => $count))); + $this->assertTrue($count == 2, format_string('@count nodes loaded.', array('@count' => $count))); // Load nodes by nid. Nodes 1, 2 and 4 will be loaded. $nodes = node_load_multiple(array(1, 2, 4)); $count = count($nodes); - $this->assertTrue(count($nodes) == 3, t('@count nodes loaded', array('@count' => $count))); - $this->assertTrue(isset($nodes[$node1->nid]), t('Node is correctly keyed in the array')); - $this->assertTrue(isset($nodes[$node2->nid]), t('Node is correctly keyed in the array')); - $this->assertTrue(isset($nodes[$node4->nid]), t('Node is correctly keyed in the array')); + $this->assertTrue(count($nodes) == 3, format_string('@count nodes loaded', array('@count' => $count))); + $this->assertTrue(isset($nodes[$node1->nid]), 'Node is correctly keyed in the array'); + $this->assertTrue(isset($nodes[$node2->nid]), 'Node is correctly keyed in the array'); + $this->assertTrue(isset($nodes[$node4->nid]), 'Node is correctly keyed in the array'); foreach ($nodes as $node) { - $this->assertTrue(is_object($node), t('Node is an object')); + $this->assertTrue(is_object($node), 'Node is an object'); } // Load nodes by nid, where type = article. Nodes 1, 2 and 3 will be loaded. $nodes = node_load_multiple(array(1, 2, 3, 4), array('type' => 'article')); $count = count($nodes); - $this->assertTrue($count == 3, t('@count nodes loaded', array('@count' => $count))); - $this->assertEqual($nodes[$node1->nid]->title, $node1->title, t('Node successfully loaded.')); - $this->assertEqual($nodes[$node2->nid]->title, $node2->title, t('Node successfully loaded.')); - $this->assertEqual($nodes[$node3->nid]->title, $node3->title, t('Node successfully loaded.')); + $this->assertTrue($count == 3, format_string('@count nodes loaded', array('@count' => $count))); + $this->assertEqual($nodes[$node1->nid]->title, $node1->title, 'Node successfully loaded.'); + $this->assertEqual($nodes[$node2->nid]->title, $node2->title, 'Node successfully loaded.'); + $this->assertEqual($nodes[$node3->nid]->title, $node3->title, 'Node successfully loaded.'); $this->assertFalse(isset($nodes[$node4->nid])); // Now that all nodes have been loaded into the static cache, ensure that // they are loaded correctly again when a condition is passed. $nodes = node_load_multiple(array(1, 2, 3, 4), array('type' => 'article')); $count = count($nodes); - $this->assertTrue($count == 3, t('@count nodes loaded.', array('@count' => $count))); - $this->assertEqual($nodes[$node1->nid]->title, $node1->title, t('Node successfully loaded')); - $this->assertEqual($nodes[$node2->nid]->title, $node2->title, t('Node successfully loaded')); - $this->assertEqual($nodes[$node3->nid]->title, $node3->title, t('Node successfully loaded')); - $this->assertFalse(isset($nodes[$node4->nid]), t('Node was not loaded')); + $this->assertTrue($count == 3, format_string('@count nodes loaded.', array('@count' => $count))); + $this->assertEqual($nodes[$node1->nid]->title, $node1->title, 'Node successfully loaded'); + $this->assertEqual($nodes[$node2->nid]->title, $node2->title, 'Node successfully loaded'); + $this->assertEqual($nodes[$node3->nid]->title, $node3->title, 'Node successfully loaded'); + $this->assertFalse(isset($nodes[$node4->nid]), 'Node was not loaded'); // Load nodes by nid, where type = article and promote = 0. $nodes = node_load_multiple(array(1, 2, 3, 4), array('type' => 'article', 'promote' => 0)); $count = count($nodes); - $this->assertTrue($count == 1, t('@count node loaded', array('@count' => $count))); - $this->assertEqual($nodes[$node3->nid]->title, $node3->title, t('Node successfully loaded.')); + $this->assertTrue($count == 1, format_string('@count node loaded', array('@count' => $count))); + $this->assertEqual($nodes[$node3->nid]->title, $node3->title, 'Node successfully loaded.'); } } @@ -112,21 +136,36 @@ // reflect the expected values. $nodes = node_load_multiple(array(), array('status' => NODE_PUBLISHED)); $loaded_node = end($nodes); - $this->assertEqual($loaded_node->node_test_loaded_nids, array($node1->nid, $node2->nid), t('hook_node_load() received the correct list of node IDs the first time it was called.')); - $this->assertEqual($loaded_node->node_test_loaded_types, array('article'), t('hook_node_load() received the correct list of node types the first time it was called.')); + $this->assertEqual($loaded_node->node_test_loaded_nids, array($node1->nid, $node2->nid), 'hook_node_load() received the correct list of node IDs the first time it was called.'); + $this->assertEqual($loaded_node->node_test_loaded_types, array('article'), 'hook_node_load() received the correct list of node types the first time it was called.'); // Now, as part of the same page request, load a set of nodes that contain // both articles and pages, and make sure the parameters passed to // node_test_node_load() are correctly updated. $nodes = node_load_multiple(array(), array('status' => NODE_NOT_PUBLISHED)); $loaded_node = end($nodes); - $this->assertEqual($loaded_node->node_test_loaded_nids, array($node3->nid, $node4->nid), t('hook_node_load() received the correct list of node IDs the second time it was called.')); - $this->assertEqual($loaded_node->node_test_loaded_types, array('article', 'page'), t('hook_node_load() received the correct list of node types the second time it was called.')); + $this->assertEqual($loaded_node->node_test_loaded_nids, array($node3->nid, $node4->nid), 'hook_node_load() received the correct list of node IDs the second time it was called.'); + $this->assertEqual($loaded_node->node_test_loaded_types, array('article', 'page'), 'hook_node_load() received the correct list of node types the second time it was called.'); } } +/** + * Tests the node revision functionality. + */ class NodeRevisionsTestCase extends DrupalWebTestCase { + + /** + * Nodes used by the test. + * + * @var array + */ protected $nodes; + + /** + * The revision messages for node revisions created in the test. + * + * @var array + */ protected $logs; public static function getInfo() { @@ -174,7 +213,7 @@ } /** - * Check node revision related operations. + * Checks node revision related operations. */ function testRevisions() { $nodes = $this->nodes; @@ -185,28 +224,28 @@ // Confirm the correct revision text appears on "view revisions" page. $this->drupalGet("node/$node->nid/revisions/$node->vid/view"); - $this->assertText($node->body[LANGUAGE_NONE][0]['value'], t('Correct text displays for version.')); + $this->assertText($node->body[LANGUAGE_NONE][0]['value'], 'Correct text displays for version.'); // Confirm the correct log message appears on "revisions overview" page. $this->drupalGet("node/$node->nid/revisions"); foreach ($logs as $log) { - $this->assertText($log, t('Log message found.')); + $this->assertText($log, 'Log message found.'); } // Confirm that revisions revert properly. $this->drupalPost("node/$node->nid/revisions/{$nodes[1]->vid}/revert", array(), t('Revert')); $this->assertRaw(t('@type %title has been reverted back to the revision from %revision-date.', array('@type' => 'Basic page', '%title' => $nodes[1]->title, - '%revision-date' => format_date($nodes[1]->revision_timestamp))), t('Revision reverted.')); + '%revision-date' => format_date($nodes[1]->revision_timestamp))), 'Revision reverted.'); $reverted_node = node_load($node->nid); - $this->assertTrue(($nodes[1]->body[LANGUAGE_NONE][0]['value'] == $reverted_node->body[LANGUAGE_NONE][0]['value']), t('Node reverted correctly.')); + $this->assertTrue(($nodes[1]->body[LANGUAGE_NONE][0]['value'] == $reverted_node->body[LANGUAGE_NONE][0]['value']), 'Node reverted correctly.'); // Confirm revisions delete properly. $this->drupalPost("node/$node->nid/revisions/{$nodes[1]->vid}/delete", array(), t('Delete')); $this->assertRaw(t('Revision from %revision-date of @type %title has been deleted.', array('%revision-date' => format_date($nodes[1]->revision_timestamp), - '@type' => 'Basic page', '%title' => $nodes[1]->title)), t('Revision deleted.')); - $this->assertTrue(db_query('SELECT COUNT(vid) FROM {node_revision} WHERE nid = :nid and vid = :vid', array(':nid' => $node->nid, ':vid' => $nodes[1]->vid))->fetchField() == 0, t('Revision not found.')); + '@type' => 'Basic page', '%title' => $nodes[1]->title)), 'Revision deleted.'); + $this->assertTrue(db_query('SELECT COUNT(vid) FROM {node_revision} WHERE nid = :nid and vid = :vid', array(':nid' => $node->nid, ':vid' => $nodes[1]->vid))->fetchField() == 0, 'Revision not found.'); } /** @@ -232,9 +271,9 @@ ); node_save($updated_node); $this->drupalGet('node/' . $node->nid); - $this->assertText($new_title, t('New node title appears on the page.')); + $this->assertText($new_title, 'New node title appears on the page.'); $node_revision = node_load($node->nid, NULL, TRUE); - $this->assertEqual($node_revision->log, $log, t('After an existing node revision is re-saved without a log message, the original log message is preserved.')); + $this->assertEqual($node_revision->log, $log, 'After an existing node revision is re-saved without a log message, the original log message is preserved.'); // Create another node with an initial log message. $node = $this->drupalCreateNode(array('log' => $log)); @@ -258,8 +297,23 @@ } } +/** + * Tests the node edit functionality. + */ class PageEditTestCase extends DrupalWebTestCase { + + /** + * A user with permission to create and edit own page content. + * + * @var object + */ protected $web_user; + + /** + * A user with permission to bypass node access and administer nodes. + * + * @var object + */ protected $admin_user; public static function getInfo() { @@ -278,7 +332,7 @@ } /** - * Check node edit functionality. + * Checks node edit functionality. */ function testPageEdit() { $this->drupalLogin($this->web_user); @@ -294,20 +348,20 @@ // Check that the node exists in the database. $node = $this->drupalGetNodeByTitle($edit[$title_key]); - $this->assertTrue($node, t('Node found in database.')); + $this->assertTrue($node, 'Node found in database.'); // Check that "edit" link points to correct page. $this->clickLink(t('Edit')); $edit_url = url("node/$node->nid/edit", array('absolute' => TRUE)); $actual_url = $this->getURL(); - $this->assertEqual($edit_url, $actual_url, t('On edit page.')); + $this->assertEqual($edit_url, $actual_url, 'On edit page.'); // Check that the title and body fields are displayed with the correct values. $active = '' . t('(active tab)') . ''; $link_text = t('!local-task-title!active', array('!local-task-title' => t('Edit'), '!active' => $active)); - $this->assertText(strip_tags($link_text), 0, t('Edit tab found and marked active.')); - $this->assertFieldByName($title_key, $edit[$title_key], t('Title field displayed.')); - $this->assertFieldByName($body_key, $edit[$body_key], t('Body field displayed.')); + $this->assertText(strip_tags($link_text), 0, 'Edit tab found and marked active.'); + $this->assertFieldByName($title_key, $edit[$title_key], 'Title field displayed.'); + $this->assertFieldByName($body_key, $edit[$body_key], 'Body field displayed.'); // Edit the content of the node. $edit = array(); @@ -317,8 +371,8 @@ $this->drupalPost(NULL, $edit, t('Save')); // Check that the title and body fields are displayed with the updated values. - $this->assertText($edit[$title_key], t('Title displayed.')); - $this->assertText($edit[$body_key], t('Body displayed.')); + $this->assertText($edit[$title_key], 'Title displayed.'); + $this->assertText($edit[$body_key], 'Body displayed.'); // Login as a second administrator user. $second_web_user = $this->drupalCreateUser(array('administer nodes', 'edit any page content')); @@ -345,7 +399,7 @@ } /** - * Check changing node authored by fields. + * Tests changing a node's "authored by" field. */ function testPageAuthoredBy() { $this->drupalLogin($this->admin_user); @@ -390,6 +444,9 @@ } } +/** + * Tests the node entity preview functionality. + */ class PagePreviewTestCase extends DrupalWebTestCase { public static function getInfo() { return array( @@ -400,43 +457,109 @@ } 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); } /** - * Check the node preview functionality. + * Checks the node preview functionality. */ function testPagePreview() { $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. - $this->assertTitle(t('Preview | Drupal'), t('Basic page title is preview.')); - $this->assertText($edit[$title_key], t('Title displayed.')); - $this->assertText($edit[$body_key], t('Body displayed.')); + // 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. - $this->assertFieldByName($title_key, $edit[$title_key], t('Title field displayed.')); - $this->assertFieldByName($body_key, $edit[$body_key], t('Body field displayed.')); + // 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 the node preview functionality, when using revisions. + * Checks the node preview functionality, when using revisions. */ function testPagePreviewWithRevisions() { $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')); @@ -444,23 +567,30 @@ $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. - $this->assertTitle(t('Preview | Drupal'), t('Basic page title is preview.')); - $this->assertText($edit[$title_key], t('Title displayed.')); - $this->assertText($edit[$body_key], t('Body displayed.')); - - // Check that the title and body fields are displayed with the correct values. - $this->assertFieldByName($title_key, $edit[$title_key], t('Title field displayed.')); - $this->assertFieldByName($body_key, $edit[$body_key], t('Body field displayed.')); + // 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, 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'], t('Log field displayed.')); + $this->assertFieldByName('log', $edit['log'], 'Log field displayed.'); } } +/** + * Tests creating and saving a node. + */ class NodeCreationTestCase extends DrupalWebTestCase { public static function getInfo() { return array( @@ -479,7 +609,7 @@ } /** - * Create a "Basic page" node and verify its consistency in the database. + * Creates a "Basic page" node and verifies its consistency in the database. */ function testNodeCreation() { // Create a node. @@ -490,15 +620,15 @@ $this->drupalPost('node/add/page', $edit, t('Save')); // Check that the Basic page has been created. - $this->assertRaw(t('!post %title has been created.', array('!post' => 'Basic page', '%title' => $edit["title"])), t('Basic page created.')); + $this->assertRaw(t('!post %title has been created.', array('!post' => 'Basic page', '%title' => $edit["title"])), 'Basic page created.'); // Check that the node exists in the database. $node = $this->drupalGetNodeByTitle($edit["title"]); - $this->assertTrue($node, t('Node found in database.')); + $this->assertTrue($node, 'Node found in database.'); } /** - * Create a page node and verify that a transaction rolls back the failed creation + * Verifies that a transaction rolls back the failed creation. */ function testFailedPageCreation() { // Create a node. @@ -511,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.')); } @@ -521,24 +653,46 @@ if (Database::getConnection()->supportsTransactions()) { // Check that the node does not exist in the database. $node = $this->drupalGetNodeByTitle($edit['title']); - $this->assertFalse($node, t('Transactions supported, and node not found in database.')); + $this->assertFalse($node, 'Transactions supported, and node not found in database.'); } else { // Check that the node exists in the database. $node = $this->drupalGetNodeByTitle($edit['title']); - $this->assertTrue($node, t('Transactions not supported, and node found in database.')); + $this->assertTrue($node, 'Transactions not supported, and node found in database.'); // Check that the failed rollback was logged. $records = db_query("SELECT wid FROM {watchdog} WHERE message LIKE 'Explicit rollback failed%'")->fetchAll(); - $this->assertTrue(count($records) > 0, t('Transactions not supported, and rollback error logged to watchdog.')); + $this->assertTrue(count($records) > 0, 'Transactions not supported, and rollback error logged to watchdog.'); } // Check that the rollback error was logged. $records = db_query("SELECT wid FROM {watchdog} WHERE variables LIKE '%Test exception for rollback.%'")->fetchAll(); - $this->assertTrue(count($records) > 0, t('Rollback explanatory error logged to watchdog.')); + $this->assertTrue(count($records) > 0, 'Rollback explanatory error logged to watchdog.'); + } + + /** + * Create an unpublished node and confirm correct redirect behavior. + */ + function testUnpublishedNodeCreation() { + // Set "Basic page" content type to be unpublished by default. + variable_set('node_options_page', array()); + // Set the front page to the default "node" page. + variable_set('site_frontpage', 'node'); + + // Create a node. + $edit = array(); + $edit["title"] = $this->randomName(8); + $edit["body[" . LANGUAGE_NONE . "][0][value]"] = $this->randomName(16); + $this->drupalPost('node/add/page', $edit, t('Save')); + + // Check that the user was redirected to the home page. + $this->assertText(t('Welcome to Drupal'), t('The user is redirected to the home page.')); } } +/** + * Tests the functionality of node entity edit permissions. + */ class PageViewTestCase extends DrupalWebTestCase { public static function getInfo() { return array( @@ -549,12 +703,12 @@ } /** - * Creates a node and then an anonymous and unpermissioned user attempt to edit the node. + * Tests an anonymous and unpermissioned user attempting to edit the node. */ function testPageView() { // Create a node to view. $node = $this->drupalCreateNode(); - $this->assertTrue(node_load($node->nid), t('Node created.')); + $this->assertTrue(node_load($node->nid), 'Node created.'); // Try to edit with anonymous user. $html = $this->drupalGet("node/$node->nid/edit"); @@ -578,6 +732,9 @@ } } +/** + * Tests the summary length functionality. + */ class SummaryLengthTestCase extends DrupalWebTestCase { public static function getInfo() { return array( @@ -588,7 +745,7 @@ } /** - * Creates a node and then an anonymous and unpermissioned user attempt to edit the node. + * Tests the node summary length functionality. */ function testSummaryLength() { // Create a node to view. @@ -597,7 +754,7 @@ 'promote' => 1, ); $node = $this->drupalCreateNode($settings); - $this->assertTrue(node_load($node->nid), t('Node created.')); + $this->assertTrue(node_load($node->nid), 'Node created.'); // Create user with permission to view the node. $web_user = $this->drupalCreateUser(array('access content', 'administer content types')); @@ -607,7 +764,7 @@ $this->drupalGet("node"); // The node teaser when it has 600 characters in length $expected = 'What is a Drupalism?'; - $this->assertRaw($expected, t('Check that the summary is 600 characters in length'), 'Node'); + $this->assertRaw($expected, 'Check that the summary is 600 characters in length', 'Node'); // Change the teaser length for "Basic page" content type. $instance = field_info_instance('node', 'body', $node->type); @@ -616,10 +773,13 @@ // Attempt to access the front page again and check if the summary is now only 200 characters in length. $this->drupalGet("node"); - $this->assertNoRaw($expected, t('Check that the summary is not longer than 200 characters'), 'Node'); + $this->assertNoRaw($expected, 'Check that the summary is not longer than 200 characters', 'Node'); } } +/** + * Tests XSS functionality with a node entity. + */ class NodeTitleXSSTestCase extends DrupalWebTestCase { public static function getInfo() { return array( @@ -629,6 +789,9 @@ ); } + /** + * Tests XSS functionality with a node entity. + */ function testNodeTitleXSS() { // Prepare a user to do the stuff. $web_user = $this->drupalCreateUser(array('create page content', 'edit any page content')); @@ -639,21 +802,24 @@ $edit = array("title" => $title); $this->drupalPost('node/add/page', $edit, t('Preview')); - $this->assertNoRaw($xss, t('Harmful tags are escaped when previewing a node.')); + $this->assertNoRaw($xss, 'Harmful tags are escaped when previewing a node.'); $settings = array('title' => $title); $node = $this->drupalCreateNode($settings); $this->drupalGet('node/' . $node->nid); // assertTitle() decodes HTML-entities inside the element. - $this->assertTitle($edit["title"] . ' | Drupal', t('Title is diplayed when viewing a node.')); - $this->assertNoRaw($xss, t('Harmful tags are escaped when viewing a node.')); + $this->assertTitle($edit["title"] . ' | Drupal', 'Title is diplayed when viewing a node.'); + $this->assertNoRaw($xss, 'Harmful tags are escaped when viewing a node.'); $this->drupalGet('node/' . $node->nid . '/edit'); - $this->assertNoRaw($xss, t('Harmful tags are escaped when editing a node.')); + $this->assertNoRaw($xss, 'Harmful tags are escaped when editing a node.'); } } +/** + * Tests the availability of the syndicate block. + */ class NodeBlockTestCase extends DrupalWebTestCase { public static function getInfo() { return array( @@ -671,21 +837,24 @@ $this->drupalLogin($admin_user); } - function testSearchFormBlock() { + /** + * Tests that the "Syndicate" block is shown when enabled. + */ + function testSyndicateBlock() { // Set block title to confirm that the interface is available. $this->drupalPost('admin/structure/block/manage/node/syndicate/configure', array('title' => $this->randomName(8)), 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.'); // Set the block to a region to confirm block is available. $edit = array(); $edit['blocks[node_syndicate][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.'); } } /** - * Check that the post information displays when enabled for a content type. + * Checks that the post information displays when enabled for a content type. */ class NodePostSettingsTestCase extends DrupalWebTestCase { public static function getInfo() { @@ -704,7 +873,7 @@ } /** - * Set "Basic page" content type to display post information and confirm its presence on a new node. + * Confirms "Basic page" content type and post information is on a new node. */ function testPagePostInfo() { @@ -723,11 +892,11 @@ // Check that the post information is displayed. $node = $this->drupalGetNodeByTitle($edit["title"]); $elements = $this->xpath('//div[contains(@class,:class)]', array(':class' => 'submitted')); - $this->assertEqual(count($elements), 1, t('Post information is displayed.')); + $this->assertEqual(count($elements), 1, 'Post information is displayed.'); } /** - * Set "Basic page" content type to not display post information and confirm its absence on a new node. + * Confirms absence of post information on a new node. */ function testPageNotPostInfo() { @@ -745,12 +914,12 @@ // Check that the post information is displayed. $node = $this->drupalGetNodeByTitle($edit["title"]); - $this->assertNoRaw('<span class="submitted">', t('Post information is not displayed.')); + $this->assertNoRaw('<span class="submitted">', 'Post information is not displayed.'); } } /** - * Ensure that data added to nodes by other modules appears in RSS feeds. + * Ensures that data added to nodes by other modules appears in RSS feeds. * * Create a node, enable the node_test module to ensure that extra data is * added to the node->content array, then verify that the data appears on the @@ -777,8 +946,7 @@ } /** - * Create a new node and ensure that it includes the custom data when added - * to an RSS feed. + * Ensures that a new node includes the custom data when added to an RSS feed. */ function testNodeRSSContent() { // Create a node. @@ -788,12 +956,12 @@ // Check that content added in 'rss' view mode appear in RSS feed. $rss_only_content = t('Extra data that should appear only in the RSS feed for node !nid.', array('!nid' => $node->nid)); - $this->assertText($rss_only_content, t('Node content designated for RSS appear in RSS feed.')); + $this->assertText($rss_only_content, 'Node content designated for RSS appear in RSS feed.'); // Check that content added in view modes other than 'rss' doesn't // appear in RSS feed. $non_rss_content = t('Extra data that should appear everywhere except the RSS feed for node !nid.', array('!nid' => $node->nid)); - $this->assertNoText($non_rss_content, t('Node content not designed for RSS doesn\'t appear in RSS feed.')); + $this->assertNoText($non_rss_content, 'Node content not designed for RSS doesn\'t appear in RSS feed.'); // Check that extra RSS elements and namespaces are added to RSS feed. $test_element = array( @@ -801,23 +969,29 @@ 'value' => t('Value of testElement RSS element for node !nid.', array('!nid' => $node->nid)), ); $test_ns = 'xmlns:drupaltest="http://example.com/test-namespace"'; - $this->assertRaw(format_xml_elements(array($test_element)), t('Extra RSS elements appear in RSS feed.')); - $this->assertRaw($test_ns, t('Extra namespaces appear in RSS feed.')); + $this->assertRaw(format_xml_elements(array($test_element)), 'Extra RSS elements appear in RSS feed.'); + $this->assertRaw($test_ns, 'Extra namespaces appear in RSS feed.'); // Check that content added in 'rss' view mode doesn't appear when // viewing node. $this->drupalGet("node/$node->nid"); - $this->assertNoText($rss_only_content, t('Node content designed for RSS doesn\'t appear when viewing node.')); + $this->assertNoText($rss_only_content, 'Node content designed for RSS doesn\'t appear when viewing node.'); + // Check that the node feed page does not try to interpret additional path + // components as arguments for node_feed() and returns default content. + $this->drupalGet('rss.xml/' . $this->randomName() . '/' . $this->randomName()); + $this->assertText($rss_only_content, 'Ignore page arguments when delivering rss.xml.'); } } /** - * Test case to verify basic node_access functionality. + * Tests basic node_access functionality. + * + * Note that hook_node_access_records() is covered in another test class. + * * @todo Cover hook_node_access in a separate test class. - * hook_node_access_records is covered in another test class. */ -class NodeAccessUnitTest extends DrupalWebTestCase { +class NodeAccessTestCase extends DrupalWebTestCase { public static function getInfo() { return array( 'name' => 'Node access', @@ -827,11 +1001,11 @@ } /** - * Asserts node_access correctly grants or denies access. + * Asserts node_access() correctly grants or denies access. */ function assertNodeAccess($ops, $node, $account) { foreach ($ops as $op => $result) { - $msg = t("node_access returns @result with operation '@op'.", array('@result' => $result ? 'true' : 'false', '@op' => $op)); + $msg = format_string("node_access returns @result with operation '@op'.", array('@result' => $result ? 'true' : 'false', '@op' => $op)); $this->assertEqual($result, node_access($op, $node, $account), $msg); } } @@ -882,9 +1056,9 @@ } /** - * Test case to verify hook_node_access_records functionality. + * Tests hook_node_access_records() functionality. */ -class NodeAccessRecordsUnitTest extends DrupalWebTestCase { +class NodeAccessRecordsTestCase extends DrupalWebTestCase { public static function getInfo() { return array( 'name' => 'Node access records', @@ -901,49 +1075,49 @@ } /** - * Create a node and test the creation of node access rules. + * Creates a node and tests the creation of node access rules. */ function testNodeAccessRecords() { // Create an article node. $node1 = $this->drupalCreateNode(array('type' => 'article')); - $this->assertTrue(node_load($node1->nid), t('Article node created.')); + $this->assertTrue(node_load($node1->nid), 'Article node created.'); // Check to see if grants added by node_test_node_access_records made it in. $records = db_query('SELECT realm, gid FROM {node_access} WHERE nid = :nid', array(':nid' => $node1->nid))->fetchAll(); - $this->assertEqual(count($records), 1, t('Returned the correct number of rows.')); - $this->assertEqual($records[0]->realm, 'test_article_realm', t('Grant with article_realm acquired for node without alteration.')); - $this->assertEqual($records[0]->gid, 1, t('Grant with gid = 1 acquired for node without alteration.')); + $this->assertEqual(count($records), 1, 'Returned the correct number of rows.'); + $this->assertEqual($records[0]->realm, 'test_article_realm', 'Grant with article_realm acquired for node without alteration.'); + $this->assertEqual($records[0]->gid, 1, 'Grant with gid = 1 acquired for node without alteration.'); // Create an unpromoted "Basic page" node. $node2 = $this->drupalCreateNode(array('type' => 'page', 'promote' => 0)); - $this->assertTrue(node_load($node2->nid), t('Unpromoted basic page node created.')); + $this->assertTrue(node_load($node2->nid), 'Unpromoted basic page node created.'); // Check to see if grants added by node_test_node_access_records made it in. $records = db_query('SELECT realm, gid FROM {node_access} WHERE nid = :nid', array(':nid' => $node2->nid))->fetchAll(); - $this->assertEqual(count($records), 1, t('Returned the correct number of rows.')); - $this->assertEqual($records[0]->realm, 'test_page_realm', t('Grant with page_realm acquired for node without alteration.')); - $this->assertEqual($records[0]->gid, 1, t('Grant with gid = 1 acquired for node without alteration.')); + $this->assertEqual(count($records), 1, 'Returned the correct number of rows.'); + $this->assertEqual($records[0]->realm, 'test_page_realm', 'Grant with page_realm acquired for node without alteration.'); + $this->assertEqual($records[0]->gid, 1, 'Grant with gid = 1 acquired for node without alteration.'); // Create an unpromoted, unpublished "Basic page" node. $node3 = $this->drupalCreateNode(array('type' => 'page', 'promote' => 0, 'status' => 0)); - $this->assertTrue(node_load($node3->nid), t('Unpromoted, unpublished basic page node created.')); + $this->assertTrue(node_load($node3->nid), 'Unpromoted, unpublished basic page node created.'); // Check to see if grants added by node_test_node_access_records made it in. $records = db_query('SELECT realm, gid FROM {node_access} WHERE nid = :nid', array(':nid' => $node3->nid))->fetchAll(); - $this->assertEqual(count($records), 1, t('Returned the correct number of rows.')); - $this->assertEqual($records[0]->realm, 'test_page_realm', t('Grant with page_realm acquired for node without alteration.')); - $this->assertEqual($records[0]->gid, 1, t('Grant with gid = 1 acquired for node without alteration.')); + $this->assertEqual(count($records), 1, 'Returned the correct number of rows.'); + $this->assertEqual($records[0]->realm, 'test_page_realm', 'Grant with page_realm acquired for node without alteration.'); + $this->assertEqual($records[0]->gid, 1, 'Grant with gid = 1 acquired for node without alteration.'); // Create a promoted "Basic page" node. $node4 = $this->drupalCreateNode(array('type' => 'page', 'promote' => 1)); - $this->assertTrue(node_load($node4->nid), t('Promoted basic page node created.')); + $this->assertTrue(node_load($node4->nid), 'Promoted basic page node created.'); // Check to see if grant added by node_test_node_access_records was altered // by node_test_node_access_records_alter. $records = db_query('SELECT realm, gid FROM {node_access} WHERE nid = :nid', array(':nid' => $node4->nid))->fetchAll(); - $this->assertEqual(count($records), 1, t('Returned the correct number of rows.')); - $this->assertEqual($records[0]->realm, 'test_alter_realm', t('Altered grant with alter_realm acquired for node.')); - $this->assertEqual($records[0]->gid, 2, t('Altered grant with gid = 2 acquired for node.')); + $this->assertEqual(count($records), 1, 'Returned the correct number of rows.'); + $this->assertEqual($records[0]->realm, 'test_alter_realm', 'Altered grant with alter_realm acquired for node.'); + $this->assertEqual($records[0]->gid, 2, 'Altered grant with gid = 2 acquired for node.'); // Check to see if we can alter grants with hook_node_grants_alter(). $operations = array('view', 'update', 'delete'); @@ -953,19 +1127,175 @@ $grants = node_test_node_grants($op, $web_user); $altered_grants = $grants; drupal_alter('node_grants', $altered_grants, $web_user, $op); - $this->assertNotEqual($grants, $altered_grants, t('Altered the %op grant for a user.', array('%op' => $op))); + $this->assertNotEqual($grants, $altered_grants, format_string('Altered the %op grant for a user.', array('%op' => $op))); } // Check that core does not grant access to an unpublished node when an // empty $grants array is returned. $node6 = $this->drupalCreateNode(array('status' => 0, 'disable_node_access' => TRUE)); $records = db_query('SELECT realm, gid FROM {node_access} WHERE nid = :nid', array(':nid' => $node6->nid))->fetchAll(); - $this->assertEqual(count($records), 0, t('Returned no records for unpublished node.')); + $this->assertEqual(count($records), 0, 'Returned no records for unpublished node.'); } } /** - * Test case to check node save related functionality, including import-save + * Tests for Node Access with a non-node base table. + */ +class NodeAccessBaseTableTestCase extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Node Access on any table', + 'description' => 'Checks behavior of the node access subsystem if the base table is not node.', + 'group' => 'Node', + ); + } + + public function setUp() { + parent::setUp('node_access_test'); + node_access_rebuild(); + variable_set('node_access_test_private', TRUE); + } + + /** + * Tests the "private" node access functionality. + * + * - Create 2 users with "access content" and "create article" permissions. + * - Each user creates one private and one not private article. + + * - Test that each user can view the other user's non-private article. + * - Test that each user cannot view the other user's private article. + * - Test that each user finds only appropriate (non-private + own private) + * in taxonomy listing. + * - Create another user with 'view any private content'. + * - Test that user 4 can view all content created above. + * - Test that user 4 can view all content on taxonomy listing. + */ + function testNodeAccessBasic() { + $num_simple_users = 2; + $simple_users = array(); + + // nodes keyed by uid and nid: $nodes[$uid][$nid] = $is_private; + $this->nodesByUser = array(); + $titles = array(); // Titles keyed by nid + $private_nodes = array(); // Array of nids marked private. + for ($i = 0; $i < $num_simple_users; $i++) { + $simple_users[$i] = $this->drupalCreateUser(array('access content', 'create article content')); + } + foreach ($simple_users as $this->webUser) { + $this->drupalLogin($this->webUser); + foreach (array(0 => 'Public', 1 => 'Private') as $is_private => $type) { + $edit = array( + 'title' => t('@private_public Article created by @user', array('@private_public' => $type, '@user' => $this->webUser->name)), + ); + if ($is_private) { + $edit['private'] = TRUE; + $edit['body[und][0][value]'] = 'private node'; + $edit['field_tags[und]'] = 'private'; + } + else { + $edit['body[und][0][value]'] = 'public node'; + $edit['field_tags[und]'] = 'public'; + } + + $this->drupalPost('node/add/article', $edit, t('Save')); + $nid = db_query('SELECT nid FROM {node} WHERE title = :title', array(':title' => $edit['title']))->fetchField(); + $private_status = db_query('SELECT private FROM {node_access_test} where nid = :nid', array(':nid' => $nid))->fetchField(); + $this->assertTrue($is_private == $private_status, 'The private status of the node was properly set in the node_access_test table.'); + if ($is_private) { + $private_nodes[] = $nid; + } + $titles[$nid] = $edit['title']; + $this->nodesByUser[$this->webUser->uid][$nid] = $is_private; + } + } + $this->publicTid = db_query('SELECT tid FROM {taxonomy_term_data} WHERE name = :name', array(':name' => 'public'))->fetchField(); + $this->privateTid = db_query('SELECT tid FROM {taxonomy_term_data} WHERE name = :name', array(':name' => 'private'))->fetchField(); + $this->assertTrue($this->publicTid, 'Public tid was found'); + $this->assertTrue($this->privateTid, 'Private tid was found'); + foreach ($simple_users as $this->webUser) { + $this->drupalLogin($this->webUser); + // Check own nodes to see that all are readable. + foreach ($this->nodesByUser as $uid => $data) { + foreach ($data as $nid => $is_private) { + $this->drupalGet('node/' . $nid); + if ($is_private) { + $should_be_visible = $uid == $this->webUser->uid; + } + else { + $should_be_visible = TRUE; + } + $this->assertResponse($should_be_visible ? 200 : 403, strtr('A %private node by user %uid is %visible for user %current_uid.', array( + '%private' => $is_private ? 'private' : 'public', + '%uid' => $uid, + '%visible' => $should_be_visible ? 'visible' : 'not visible', + '%current_uid' => $this->webUser->uid, + ))); + } + } + + // Check to see that the correct nodes are shown on taxonomy/private + // and taxonomy/public. + $this->assertTaxonomyPage(FALSE); + } + + // Now test that a user with 'access any private content' can view content. + $access_user = $this->drupalCreateUser(array('access content', 'create article content', 'node test view', 'search content')); + $this->drupalLogin($access_user); + + foreach ($this->nodesByUser as $uid => $private_status) { + foreach ($private_status as $nid => $is_private) { + $this->drupalGet('node/' . $nid); + $this->assertResponse(200); + } + } + + // This user should be able to see all of the nodes on the relevant + // taxonomy pages. + $this->assertTaxonomyPage(TRUE); + } + + /** + * Checks taxonomy/term listings to ensure only accessible nodes are listed. + * + * @param $is_admin + * A boolean indicating whether the current user is an administrator. If + * TRUE, all nodes should be listed. If FALSE, only public nodes and the + * user's own private nodes should be listed. + */ + protected function assertTaxonomyPage($is_admin) { + foreach (array($this->publicTid, $this->privateTid) as $tid_is_private => $tid) { + $this->drupalGet("taxonomy/term/$tid"); + $this->nids_visible = array(); + foreach ($this->xpath("//a[text()='Read more']") as $link) { + $this->assertTrue(preg_match('|node/(\d+)$|', (string) $link['href'], $matches), 'Read more points to a node'); + $this->nids_visible[$matches[1]] = TRUE; + } + foreach ($this->nodesByUser as $uid => $data) { + foreach ($data as $nid => $is_private) { + // Private nodes should be visible on the private term page, + // public nodes should be visible on the public term page. + $should_be_visible = $tid_is_private == $is_private; + // Non-administrators can only see their own nodes on the private + // term page. + if (!$is_admin && $tid_is_private) { + $should_be_visible = $should_be_visible && $uid == $this->webUser->uid; + } + $this->assertIdentical(isset($this->nids_visible[$nid]), $should_be_visible, strtr('A %private node by user %uid is %visible for user %current_uid on the %tid_is_private page.', array( + '%private' => $is_private ? 'private' : 'public', + '%uid' => $uid, + '%visible' => isset($this->nids_visible[$nid]) ? 'visible' : 'not visible', + '%current_uid' => $this->webUser->uid, + '%tid_is_private' => $tid_is_private ? 'private' : 'public', + ))); + } + } + } + } +} + +/** + * Tests node save related functionality, including import-save. */ class NodeSaveTestCase extends DrupalWebTestCase { @@ -986,7 +1316,8 @@ } /** - * Import test, to check if custom node ids are saved properly. + * Checks whether custom node IDs are saved properly during an import operation. + * * Workflow: * - first create a piece of content * - save the content @@ -1008,20 +1339,19 @@ $node = node_submit((object) $node); // Verify that node_submit did not overwrite the user ID. - $this->assertEqual($node->uid, $this->web_user->uid, t('Function node_submit() preserves user ID')); + $this->assertEqual($node->uid, $this->web_user->uid, 'Function node_submit() preserves user ID'); node_save($node); // Test the import. $node_by_nid = node_load($test_nid); - $this->assertTrue($node_by_nid, t('Node load by node ID.')); + $this->assertTrue($node_by_nid, 'Node load by node ID.'); $node_by_title = $this->drupalGetNodeByTitle($title); - $this->assertTrue($node_by_title, t('Node load by node title.')); + $this->assertTrue($node_by_title, 'Node load by node title.'); } /** - * Check that the "created" and "changed" timestamps are set correctly when - * saving a new node or updating an existing node. + * Verifies accuracy of the "created" and "changed" timestamp functionality. */ function testTimestamps() { // Use the default timestamps. @@ -1033,8 +1363,8 @@ node_save((object) $edit); $node = $this->drupalGetNodeByTitle($edit['title']); - $this->assertEqual($node->created, REQUEST_TIME, t('Creating a node sets default "created" timestamp.')); - $this->assertEqual($node->changed, REQUEST_TIME, t('Creating a node sets default "changed" timestamp.')); + $this->assertEqual($node->created, REQUEST_TIME, 'Creating a node sets default "created" timestamp.'); + $this->assertEqual($node->changed, REQUEST_TIME, 'Creating a node sets default "changed" timestamp.'); // Store the timestamps. $created = $node->created; @@ -1042,15 +1372,15 @@ node_save($node); $node = $this->drupalGetNodeByTitle($edit['title'], TRUE); - $this->assertEqual($node->created, $created, t('Updating a node preserves "created" timestamp.')); + $this->assertEqual($node->created, $created, 'Updating a node preserves "created" timestamp.'); // Programmatically set the timestamps using hook_node_presave. $node->title = 'testing_node_presave'; node_save($node); $node = $this->drupalGetNodeByTitle('testing_node_presave', TRUE); - $this->assertEqual($node->created, 280299600, t('Saving a node uses "created" timestamp set in presave hook.')); - $this->assertEqual($node->changed, 979534800, t('Saving a node uses "changed" timestamp set in presave hook.')); + $this->assertEqual($node->created, 280299600, 'Saving a node uses "created" timestamp set in presave hook.'); + $this->assertEqual($node->changed, 979534800, 'Saving a node uses "changed" timestamp set in presave hook.'); // Programmatically set the timestamps on the node. $edit = array( @@ -1063,8 +1393,8 @@ node_save((object) $edit); $node = $this->drupalGetNodeByTitle($edit['title']); - $this->assertEqual($node->created, 280299600, t('Creating a node uses user-set "created" timestamp.')); - $this->assertNotEqual($node->changed, 979534800, t('Creating a node doesn\'t use user-set "changed" timestamp.')); + $this->assertEqual($node->created, 280299600, 'Creating a node uses user-set "created" timestamp.'); + $this->assertNotEqual($node->changed, 979534800, 'Creating a node doesn\'t use user-set "changed" timestamp.'); // Update the timestamps. $node->created = 979534800; @@ -1072,8 +1402,8 @@ node_save($node); $node = $this->drupalGetNodeByTitle($edit['title'], TRUE); - $this->assertEqual($node->created, 979534800, t('Updating a node uses user-set "created" timestamp.')); - $this->assertNotEqual($node->changed, 280299600, t('Updating a node doesn\'t use user-set "changed" timestamp.')); + $this->assertEqual($node->created, 979534800, 'Updating a node uses user-set "created" timestamp.'); + $this->assertNotEqual($node->changed, 280299600, 'Updating a node doesn\'t use user-set "changed" timestamp.'); } /** @@ -1105,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.'); + } } /** @@ -1120,7 +1466,7 @@ } /** - * Ensure that node type functions (node_type_get_*) work correctly. + * Ensures that node type functions (node_type_get_*) work correctly. * * Load available node types and validate the returned data. */ @@ -1128,18 +1474,18 @@ $node_types = node_type_get_types(); $node_names = node_type_get_names(); - $this->assertTrue(isset($node_types['article']), t('Node type article is available.')); - $this->assertTrue(isset($node_types['page']), t('Node type basic page is available.')); + $this->assertTrue(isset($node_types['article']), 'Node type article is available.'); + $this->assertTrue(isset($node_types['page']), 'Node type basic page is available.'); - $this->assertEqual($node_types['article']->name, $node_names['article'], t('Correct node type base has been returned.')); + $this->assertEqual($node_types['article']->name, $node_names['article'], 'Correct node type base has been returned.'); - $this->assertEqual($node_types['article'], node_type_get_type('article'), t('Correct node type has been returned.')); - $this->assertEqual($node_types['article']->name, node_type_get_name('article'), t('Correct node type name has been returned.')); - $this->assertEqual($node_types['page']->base, node_type_get_base('page'), t('Correct node type base has been returned.')); + $this->assertEqual($node_types['article'], node_type_get_type('article'), 'Correct node type has been returned.'); + $this->assertEqual($node_types['article']->name, node_type_get_name('article'), 'Correct node type name has been returned.'); + $this->assertEqual($node_types['page']->base, node_type_get_base('page'), 'Correct node type base has been returned.'); } /** - * Test creating a content type programmatically and via a form. + * Tests creating a content type programmatically and via a form. */ function testNodeTypeCreation() { // Create a content type programmaticaly. @@ -1169,19 +1515,19 @@ } /** - * Test editing a node type using the UI. + * 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'); - $this->assertEqual($instance['label'], 'Body', t('Body field was found.')); + $this->assertEqual($instance['label'], 'Body', 'Body field was found.'); // Verify that title and body fields are displayed. $this->drupalGet('node/add/page'); - $this->assertRaw('Title', t('Title field was found.')); - $this->assertRaw('Body', t('Body field was found.')); + $this->assertRaw('Title', 'Title field was found.'); + $this->assertRaw('Body', 'Body field was found.'); // Rename the title field. $edit = array( @@ -1192,8 +1538,8 @@ field_info_cache_clear(); $this->drupalGet('node/add/page'); - $this->assertRaw('Foo', t('New title label was displayed.')); - $this->assertNoRaw('Title', t('Old title label was not displayed.')); + $this->assertRaw('Foo', 'New title label was displayed.'); + $this->assertNoRaw('Title', 'Old title label was not displayed.'); // Change the name, machine name and description. $edit = array( @@ -1205,12 +1551,12 @@ field_info_cache_clear(); $this->drupalGet('node/add'); - $this->assertRaw('Bar', t('New name was displayed.')); - $this->assertRaw('Lorem ipsum', t('New description was displayed.')); + $this->assertRaw('Bar', 'New name was displayed.'); + $this->assertRaw('Lorem ipsum', 'New description was displayed.'); $this->clickLink('Bar'); - $this->assertEqual(url('node/add/bar', array('absolute' => TRUE)), $this->getUrl(), t('New machine name was used in URL.')); - $this->assertRaw('Foo', t('Title field was found.')); - $this->assertRaw('Body', t('Body field was found.')); + $this->assertEqual(url('node/add/bar', array('absolute' => TRUE)), $this->getUrl(), 'New machine name was used in URL.'); + $this->assertRaw('Foo', 'Title field was found.'); + $this->assertRaw('Body', 'Body field was found.'); // Remove the body field. $this->drupalPost('admin/structure/types/manage/bar/fields/body/delete', NULL, t('Delete')); @@ -1218,11 +1564,11 @@ $this->drupalPost('admin/structure/types/manage/bar', array(), t('Save content type')); // Check that the body field doesn't exist. $this->drupalGet('node/add/bar'); - $this->assertNoRaw('Body', t('Body field was not found.')); + $this->assertNoRaw('Body', 'Body field was not found.'); } /** - * Test that node_types_rebuild() correctly handles the 'disabled' flag. + * Tests that node_types_rebuild() correctly handles the 'disabled' flag. */ function testNodeTypeStatus() { // Enable all core node modules, and all types should be active. @@ -1230,42 +1576,42 @@ node_types_rebuild(); $types = node_type_get_types(); foreach (array('blog', 'book', 'poll', 'article', 'page') as $type) { - $this->assertTrue(isset($types[$type]), t('%type is found in node types.', array('%type' => $type))); - $this->assertTrue(isset($types[$type]->disabled) && empty($types[$type]->disabled), t('%type type is enabled.', array('%type' => $type))); + $this->assertTrue(isset($types[$type]), format_string('%type is found in node types.', array('%type' => $type))); + $this->assertTrue(isset($types[$type]->disabled) && empty($types[$type]->disabled), format_string('%type type is enabled.', array('%type' => $type))); } // Disable poll module and the respective type should be marked as disabled. module_disable(array('poll'), FALSE); node_types_rebuild(); $types = node_type_get_types(); - $this->assertTrue(!empty($types['poll']->disabled), t("Poll module's node type disabled.")); - $this->assertTrue(isset($types['blog']) && empty($types['blog']->disabled), t("Blog module's node type still active.")); + $this->assertTrue(!empty($types['poll']->disabled), "Poll module's node type disabled."); + $this->assertTrue(isset($types['blog']) && empty($types['blog']->disabled), "Blog module's node type still active."); // Disable blog module and the respective type should be marked as disabled. module_disable(array('blog'), FALSE); node_types_rebuild(); $types = node_type_get_types(); - $this->assertTrue(!empty($types['blog']->disabled), t("Blog module's node type disabled.")); - $this->assertTrue(!empty($types['poll']->disabled), t("Poll module's node type still disabled.")); + $this->assertTrue(!empty($types['blog']->disabled), "Blog module's node type disabled."); + $this->assertTrue(!empty($types['poll']->disabled), "Poll module's node type still disabled."); // Disable book module and the respective type should still be active, since // it is not provided by hook_node_info(). module_disable(array('book'), FALSE); node_types_rebuild(); $types = node_type_get_types(); - $this->assertTrue(isset($types['book']) && empty($types['book']->disabled), t("Book module's node type still active.")); - $this->assertTrue(!empty($types['blog']->disabled), t("Blog module's node type still disabled.")); - $this->assertTrue(!empty($types['poll']->disabled), t("Poll module's node type still disabled.")); - $this->assertTrue(isset($types['article']) && empty($types['article']->disabled), t("Article node type still active.")); - $this->assertTrue(isset($types['page']) && empty($types['page']->disabled), t("Basic page node type still active.")); + $this->assertTrue(isset($types['book']) && empty($types['book']->disabled), "Book module's node type still active."); + $this->assertTrue(!empty($types['blog']->disabled), "Blog module's node type still disabled."); + $this->assertTrue(!empty($types['poll']->disabled), "Poll module's node type still disabled."); + $this->assertTrue(isset($types['article']) && empty($types['article']->disabled), "Article node type still active."); + $this->assertTrue(isset($types['page']) && empty($types['page']->disabled), "Basic page node type still active."); // Re-enable the modules and verify that the types are active again. module_enable(array('blog', 'book', 'poll'), FALSE); node_types_rebuild(); $types = node_type_get_types(); foreach (array('blog', 'book', 'poll', 'article', 'page') as $type) { - $this->assertTrue(isset($types[$type]), t('%type is found in node types.', array('%type' => $type))); - $this->assertTrue(isset($types[$type]->disabled) && empty($types[$type]->disabled), t('%type type is enabled.', array('%type' => $type))); + $this->assertTrue(isset($types[$type]), format_string('%type is found in node types.', array('%type' => $type))); + $this->assertTrue(isset($types[$type]->disabled) && empty($types[$type]->disabled), format_string('%type type is enabled.', array('%type' => $type))); } } } @@ -1283,7 +1629,7 @@ } /** - * Test node type customizations persist through disable and uninstall. + * Tests that node type customizations persist through disable and uninstall. */ function testNodeTypeCustomizationPersistence() { $web_user = $this->drupalCreateUser(array('bypass node access', 'administer content types', 'administer modules')); @@ -1296,12 +1642,12 @@ // disabled. $this->drupalPost('admin/modules', $poll_enable, t('Save configuration')); $disabled = db_query('SELECT disabled FROM {node_type} WHERE type = :type', array(':type' => 'poll'))->fetchField(); - $this->assertNotIdentical($disabled, FALSE, t('Poll node type found in the database')); - $this->assertEqual($disabled, 0, t('Poll node type is not disabled')); + $this->assertNotIdentical($disabled, FALSE, 'Poll node type found in the database'); + $this->assertEqual($disabled, 0, 'Poll node type is not disabled'); // Check that poll node type (uncustomized) shows up. $this->drupalGet('node/add'); - $this->assertText('poll', t('poll type is found on node/add')); + $this->assertText('poll', 'poll type is found on node/add'); // Customize poll description. $description = $this->randomName(); @@ -1310,23 +1656,23 @@ // Check that poll node type customization shows up. $this->drupalGet('node/add'); - $this->assertText($description, t('Customized description found')); + $this->assertText($description, 'Customized description found'); // Disable poll and check that the node type gets disabled. $this->drupalPost('admin/modules', $poll_disable, t('Save configuration')); $disabled = db_query('SELECT disabled FROM {node_type} WHERE type = :type', array(':type' => 'poll'))->fetchField(); - $this->assertEqual($disabled, 1, t('Poll node type is disabled')); + $this->assertEqual($disabled, 1, 'Poll node type is disabled'); $this->drupalGet('node/add'); - $this->assertNoText('poll', t('poll type is not found on node/add')); + $this->assertNoText('poll', 'poll type is not found on node/add'); // Reenable poll and check that the customization survived the module // disable. $this->drupalPost('admin/modules', $poll_enable, t('Save configuration')); $disabled = db_query('SELECT disabled FROM {node_type} WHERE type = :type', array(':type' => 'poll'))->fetchField(); - $this->assertNotIdentical($disabled, FALSE, t('Poll node type found in the database')); - $this->assertEqual($disabled, 0, t('Poll node type is not disabled')); + $this->assertNotIdentical($disabled, FALSE, 'Poll node type found in the database'); + $this->assertEqual($disabled, 0, 'Poll node type is not disabled'); $this->drupalGet('node/add'); - $this->assertText($description, t('Customized description found')); + $this->assertText($description, 'Customized description found'); // Disable and uninstall poll. $this->drupalPost('admin/modules', $poll_disable, t('Save configuration')); @@ -1334,20 +1680,20 @@ $this->drupalPost('admin/modules/uninstall', $edit, t('Uninstall')); $this->drupalPost(NULL, array(), t('Uninstall')); $disabled = db_query('SELECT disabled FROM {node_type} WHERE type = :type', array(':type' => 'poll'))->fetchField(); - $this->assertTrue($disabled, t('Poll node type is in the database and is disabled')); + $this->assertTrue($disabled, 'Poll node type is in the database and is disabled'); $this->drupalGet('node/add'); - $this->assertNoText('poll', t('poll type is no longer found on node/add')); + $this->assertNoText('poll', 'poll type is no longer found on node/add'); // Reenable poll and check that the customization survived the module // uninstall. $this->drupalPost('admin/modules', $poll_enable, t('Save configuration')); $this->drupalGet('node/add'); - $this->assertText($description, t('Customized description is found even after uninstall and reenable.')); + $this->assertText($description, 'Customized description is found even after uninstall and reenable.'); } } /** - * Rebuild the node_access table. + * Verifies the rebuild functionality for the node_access table. */ class NodeAccessRebuildTestCase extends DrupalWebTestCase { public static function getInfo() { @@ -1366,6 +1712,9 @@ $this->web_user = $web_user; } + /** + * Tests rebuilding the node access permissions table. + */ function testNodeAccessRebuild() { $this->drupalGet('admin/reports/status'); $this->clickLink(t('Rebuild permissions')); @@ -1375,7 +1724,7 @@ } /** - * Test node administration page functionality. + * Tests node administration page functionality. */ class NodeAdminTestCase extends DrupalWebTestCase { public static function getInfo() { @@ -1443,6 +1792,7 @@ * Tests content overview with different user permissions. * * Taxonomy filters are tested separately. + * * @see TaxonomyNodeFilterTestCase */ function testContentAdminPages() { @@ -1461,7 +1811,7 @@ $this->assertLinkByHref('node/' . $node->nid . '/edit'); $this->assertLinkByHref('node/' . $node->nid . '/delete'); // Verify tableselect. - $this->assertFieldByName('nodes[' . $node->nid . ']', '', t('Tableselect found.')); + $this->assertFieldByName('nodes[' . $node->nid . ']', '', 'Tableselect found.'); } // Verify filtering by publishing status. @@ -1470,7 +1820,7 @@ ); $this->drupalPost(NULL, $edit, t('Filter')); - $this->assertRaw(t('where %property is %value', array('%property' => t('status'), '%value' => 'published')), t('Content list is filtered by status.')); + $this->assertRaw(t('where %property is %value', array('%property' => t('status'), '%value' => 'published')), 'Content list is filtered by status.'); $this->assertLinkByHref('node/' . $nodes['published_page']->nid . '/edit'); $this->assertLinkByHref('node/' . $nodes['published_article']->nid . '/edit'); @@ -1482,8 +1832,8 @@ ); $this->drupalPost(NULL, $edit, t('Refine')); - $this->assertRaw(t('where %property is %value', array('%property' => t('status'), '%value' => 'published')), t('Content list is filtered by status.')); - $this->assertRaw(t('and where %property is %value', array('%property' => t('type'), '%value' => 'Basic page')), t('Content list is filtered by content type.')); + $this->assertRaw(t('where %property is %value', array('%property' => t('status'), '%value' => 'published')), 'Content list is filtered by status.'); + $this->assertRaw(t('and where %property is %value', array('%property' => t('type'), '%value' => 'Basic page')), 'Content list is filtered by content type.'); $this->assertLinkByHref('node/' . $nodes['published_page']->nid . '/edit'); $this->assertNoLinkByHref('node/' . $nodes['published_article']->nid . '/edit'); @@ -1506,7 +1856,7 @@ $this->assertNoLinkByHref('node/' . $nodes['unpublished_page_1']->nid . '/delete'); // Verify no tableselect. - $this->assertNoFieldByName('nodes[' . $nodes['published_page']->nid . ']', '', t('No tableselect found.')); + $this->assertNoFieldByName('nodes[' . $nodes['published_page']->nid . ']', '', 'No tableselect found.'); // Verify unpublished content is displayed with permission. $this->drupalLogout(); @@ -1524,7 +1874,7 @@ $this->assertNoLinkByHref('node/' . $nodes['unpublished_page_1']->nid . '/delete'); // Verify no tableselect. - $this->assertNoFieldByName('nodes[' . $nodes['unpublished_page_2']->nid . ']', '', t('No tableselect found.')); + $this->assertNoFieldByName('nodes[' . $nodes['unpublished_page_2']->nid . ']', '', 'No tableselect found.'); // Verify node access can be bypassed. $this->drupalLogout(); @@ -1540,9 +1890,15 @@ } /** - * Test node title. + * Tests node title functionality. */ class NodeTitleTestCase extends DrupalWebTestCase { + + /** + * A user with permission to create and edit content and to administer nodes. + * + * @var object + */ protected $admin_user; public static function getInfo() { @@ -1560,7 +1916,7 @@ } /** - * Create one node and test if the node title has the correct value. + * Creates one node and tests if the node title has the correct value. */ function testNodeTitle() { // Create "Basic page" content with title. @@ -1603,7 +1959,7 @@ } /** - * Ensure that node_feed accepts and prints extra channel elements. + * Ensures that node_feed() accepts and prints extra channel elements. */ function testNodeFeedExtraChannelElements() { ob_start(); @@ -1635,7 +1991,7 @@ } /** - * Test the recent comments block. + * Tests the recent comments block. */ function testRecentNodeBlock() { $this->drupalLogin($this->admin_user); @@ -1650,7 +2006,7 @@ 'blocks[node_recent][region]' => 'sidebar_first', ); $this->drupalPost('admin/structure/block', $edit, t('Save blocks')); - $this->assertText(t('The block settings have been updated.'), t('Block saved to first sidebar region.')); + $this->assertText(t('The block settings have been updated.'), 'Block saved to first sidebar region.'); // Set block title and variables. $block = array( @@ -1658,11 +2014,11 @@ 'node_recent_block_count' => 2, ); $this->drupalPost('admin/structure/block/manage/node/recent/configure', $block, t('Save block')); - $this->assertText(t('The block configuration has been saved.'), t('Block saved.')); + $this->assertText(t('The block configuration has been saved.'), 'Block saved.'); // Test that block is not visible without nodes $this->drupalGet(''); - $this->assertText(t('No content available.'), t('Block with "No content available." found.')); + $this->assertText(t('No content available.'), 'Block with "No content available." found.'); // Add some test nodes. $default_settings = array('uid' => $this->web_user->uid, 'type' => 'article'); @@ -1688,16 +2044,16 @@ // see the block. $this->drupalLogout(); $this->drupalGet(''); - $this->assertNoText($block['title'], t('Block was not found.')); + $this->assertNoText($block['title'], 'Block was not found.'); // Test that only the 2 latest nodes are shown. $this->drupalLogin($this->web_user); - $this->assertNoText($node1->title, t('Node not found in block.')); - $this->assertText($node2->title, t('Node found in block.')); - $this->assertText($node3->title, t('Node found in block.')); + $this->assertNoText($node1->title, 'Node not found in block.'); + $this->assertText($node2->title, 'Node found in block.'); + $this->assertText($node3->title, 'Node found in block.'); // Check to make sure nodes are in the right order. - $this->assertTrue($this->xpath('//div[@id="block-node-recent"]/div/table/tbody/tr[position() = 1]/td/div/a[text() = "' . $node3->title . '"]'), t('Nodes were ordered correctly in block.')); + $this->assertTrue($this->xpath('//div[@id="block-node-recent"]/div/table/tbody/tr[position() = 1]/td/div/a[text() = "' . $node3->title . '"]'), 'Nodes were ordered correctly in block.'); // Set the number of recent nodes to show to 10. $this->drupalLogout(); @@ -1706,17 +2062,17 @@ 'node_recent_block_count' => 10, ); $this->drupalPost('admin/structure/block/manage/node/recent/configure', $block, t('Save block')); - $this->assertText(t('The block configuration has been saved.'), t('Block saved.')); + $this->assertText(t('The block configuration has been saved.'), 'Block saved.'); // Post an additional node. $node4 = $this->drupalCreateNode($default_settings); // Test that all four nodes are shown. $this->drupalGet(''); - $this->assertText($node1->title, t('Node found in block.')); - $this->assertText($node2->title, t('Node found in block.')); - $this->assertText($node3->title, t('Node found in block.')); - $this->assertText($node4->title, t('Node found in block.')); + $this->assertText($node1->title, 'Node found in block.'); + $this->assertText($node2->title, 'Node found in block.'); + $this->assertText($node3->title, 'Node found in block.'); + $this->assertText($node4->title, 'Node found in block.'); // Create the custom block. $custom_block = array(); @@ -1731,24 +2087,24 @@ $this->drupalPost('admin/structure/block/add', $custom_block, t('Save block')); $bid = db_query("SELECT bid FROM {block_custom} WHERE info = :info", array(':info' => $custom_block['info']))->fetchField(); - $this->assertTrue($bid, t('Custom block with visibility rule was created.')); + $this->assertTrue($bid, 'Custom block with visibility rule was created.'); // Verify visibility rules. $this->drupalGet(''); - $this->assertNoText($custom_block['title'], t('Block was displayed on the front page.')); + $this->assertNoText($custom_block['title'], 'Block was displayed on the front page.'); $this->drupalGet('node/add/article'); - $this->assertText($custom_block['title'], t('Block was displayed on the node/add/article page.')); + $this->assertText($custom_block['title'], 'Block was displayed on the node/add/article page.'); $this->drupalGet('node/' . $node1->nid); - $this->assertText($custom_block['title'], t('Block was displayed on the node/N.')); + $this->assertText($custom_block['title'], 'Block was displayed on the node/N.'); // Delete the created custom block & verify that it's been deleted. $this->drupalPost('admin/structure/block/manage/block/' . $bid . '/delete', array(), t('Delete')); $bid = db_query("SELECT 1 FROM {block_node_type} WHERE module = 'block' AND delta = :delta", array(':delta' => $bid))->fetchField(); - $this->assertFalse($bid, t('Custom block was deleted.')); + $this->assertFalse($bid, 'Custom block was deleted.'); } } /** - * Test multistep node forms basic options. + * Tests basic options of multi-step node forms. */ class MultiStepNodeFormBasicOptionsTest extends DrupalWebTestCase { public static function getInfo() { @@ -1766,7 +2122,7 @@ } /** - * Change the default values of basic options to ensure they persist. + * Tests changing the default values of basic options to ensure they persist. */ function testMultiStepNodeFormBasicOptions() { $edit = array( @@ -1798,17 +2154,17 @@ } /** - * Test to ensure that a node's content array is rebuilt on every call to node_build_content(). + * Ensures that content array is rebuilt on every call to node_build_content(). */ function testNodeRebuildContent() { $node = $this->drupalCreateNode(); - // Set a property in the content array so we can test for its existance later on. + // Set a property in the content array so we can test for its existence later on. $node->content['test_content_property'] = array('#value' => $this->randomString()); $content = node_build_content($node); // If the property doesn't exist it means the node->content was rebuilt. - $this->assertFalse(isset($content['test_content_property']), t('Node content was emptied prior to being built.')); + $this->assertFalse(isset($content['test_content_property']), 'Node content was emptied prior to being built.'); } } @@ -1827,11 +2183,15 @@ /** * User with permission to view content. + * + * @var object */ protected $accessUser; /** * User without permission to view content. + * + * @var object */ protected $noAccessUser; @@ -1847,9 +2207,9 @@ // Create user with simple node access permission. The 'node test view' // permission is implemented and granted by the node_access_test module. - $this->accessUser = $this->drupalCreateUser(array('access content', 'node test view')); - $this->noAccessUser = $this->drupalCreateUser(array('access content')); - $this->noAccessUser2 = $this->drupalCreateUser(array('access content')); + $this->accessUser = $this->drupalCreateUser(array('access content overview', 'access content', 'node test view')); + $this->noAccessUser = $this->drupalCreateUser(array('access content overview', 'access content')); + $this->noAccessUser2 = $this->drupalCreateUser(array('access content overview', 'access content')); } /** @@ -1862,18 +2222,26 @@ $this->assertText('Yes, 4 nodes', "4 nodes were found for access user"); $this->assertNoText('Exception', "No database exception"); + // Test the content overview page. + $this->drupalGet('admin/content'); + $table_rows = $this->xpath('//tbody/tr'); + $this->assertEqual(4, count($table_rows), "4 nodes were found for access user"); + // Verify that a user with no access permission cannot see nodes. $this->drupalLogin($this->noAccessUser); $this->drupalGet('node_access_test_page'); $this->assertText('No nodes', "No nodes were found for no access user"); $this->assertNoText('Exception', "No database exception"); + + $this->drupalGet('admin/content'); + $this->assertText(t('No content available.')); } /** - * Lower-level test of 'node_access' query alter, for user with access. + * Tests 'node_access' query alter, for user with access. * - * Verifies that a non-standard table alias can be used, and that a - * user with node access can view the nodes. + * Verifies that a non-standard table alias can be used, and that a user with + * node access can view the nodes. */ function testNodeQueryAlterLowLevelWithAccess() { // User with access should be able to view 4 nodes. @@ -1885,7 +2253,7 @@ $query->addMetaData('account', $this->accessUser); $result = $query->execute()->fetchAll(); - $this->assertEqual(count($result), 4, t('User with access can see correct nodes')); + $this->assertEqual(count($result), 4, 'User with access can see correct nodes'); } catch (Exception $e) { $this->fail(t('Altered query is malformed')); @@ -1893,10 +2261,10 @@ } /** - * Lower-level test of 'node_access' query alter, for user without access. + * Tests 'node_access' query alter, for user without access. * - * Verifies that a non-standard table alias can be used, and that a - * user without node access cannot view the nodes. + * Verifies that a non-standard table alias can be used, and that a user + * without node access cannot view the nodes. */ function testNodeQueryAlterLowLevelNoAccess() { // User without access should be able to view 0 nodes. @@ -1908,7 +2276,7 @@ $query->addMetaData('account', $this->noAccessUser); $result = $query->execute()->fetchAll(); - $this->assertEqual(count($result), 0, t('User with no access cannot see nodes')); + $this->assertEqual(count($result), 0, 'User with no access cannot see nodes'); } catch (Exception $e) { $this->fail(t('Altered query is malformed')); @@ -1916,10 +2284,10 @@ } /** - * Lower-level test of 'node_access' query alter, for edit access. + * Tests 'node_access' query alter, for edit access. * - * Verifies that a non-standard table alias can be used, and that a - * user with view-only node access cannot edit the nodes. + * Verifies that a non-standard table alias can be used, and that a user with + * view-only node access cannot edit the nodes. */ function testNodeQueryAlterLowLevelEditAccess() { // User with view-only access should not be able to edit nodes. @@ -1931,7 +2299,7 @@ $query->addMetaData('account', $this->accessUser); $result = $query->execute()->fetchAll(); - $this->assertEqual(count($result), 0, t('User with view-only access cannot edit nodes')); + $this->assertEqual(count($result), 0, 'User with view-only access cannot edit nodes'); } catch (Exception $e) { $this->fail($e->getMessage()); @@ -1941,13 +2309,13 @@ } /** - * Lower-level test of 'node_access' query alter override. + * Tests 'node_access' query alter override. * * Verifies that node_access_view_all_nodes() is called from - * node_query_node_access_alter(). We do this by checking that - * a user which normally would not have view privileges is able - * to view the nodes when we add a record to {node_access} paired - * with a corresponding privilege in hook_node_grants(). + * node_query_node_access_alter(). We do this by checking that a user who + * normally would not have view privileges is able to view the nodes when we + * add a record to {node_access} paired with a corresponding privilege in + * hook_node_grants(). */ function testNodeQueryAlterOverride() { $record = array( @@ -1971,7 +2339,7 @@ $query->addMetaData('account', $this->noAccessUser); $result = $query->execute()->fetchAll(); - $this->assertEqual(count($result), 0, t('User view privileges are not overridden')); + $this->assertEqual(count($result), 0, 'User view privileges are not overridden'); } catch (Exception $e) { $this->fail(t('Altered query is malformed')); @@ -1993,7 +2361,7 @@ $query->addMetaData('account', $this->noAccessUser); $result = $query->execute()->fetchAll(); - $this->assertEqual(count($result), 4, t('User view privileges are overridden')); + $this->assertEqual(count($result), 4, 'User view privileges are overridden'); } catch (Exception $e) { $this->fail(t('Altered query is malformed')); @@ -2018,11 +2386,15 @@ /** * User with permission to view content. + * + * @var object */ protected $accessUser; /** * User without permission to view content. + * + * @var object */ protected $noAccessUser; @@ -2105,40 +2477,545 @@ // Generate and test sanitized tokens. $tests = array(); + $langcode = entity_language('node', $node); $tests['[node:nid]'] = $node->nid; $tests['[node:vid]'] = $node->vid; $tests['[node:tnid]'] = $node->tnid; $tests['[node:type]'] = 'article'; $tests['[node:type-name]'] = 'Article'; $tests['[node:title]'] = check_plain($node->title); - $tests['[node:body]'] = _text_sanitize($instance, $node->language, $node->body[$node->language][0], 'value'); - $tests['[node:summary]'] = _text_sanitize($instance, $node->language, $node->body[$node->language][0], 'summary'); - $tests['[node:language]'] = check_plain($node->language); + $tests['[node:body]'] = _text_sanitize($instance, $langcode, $node->body[$langcode][0], 'value'); + $tests['[node:summary]'] = _text_sanitize($instance, $langcode, $node->body[$langcode][0], 'summary'); + $tests['[node:language]'] = check_plain($langcode); $tests['[node:url]'] = url('node/' . $node->nid, $url_options); $tests['[node:edit-url]'] = url('node/' . $node->nid . '/edit', $url_options); + $tests['[node:author]'] = check_plain(format_username($account)); $tests['[node:author:uid]'] = $node->uid; $tests['[node:author:name]'] = check_plain(format_username($account)); $tests['[node:created:since]'] = format_interval(REQUEST_TIME - $node->created, 2, $language->language); $tests['[node:changed:since]'] = format_interval(REQUEST_TIME - $node->changed, 2, $language->language); // Test to make sure that we generated something for each token. - $this->assertFalse(in_array(0, array_map('strlen', $tests)), t('No empty tokens generated.')); + $this->assertFalse(in_array(0, array_map('strlen', $tests)), 'No empty tokens generated.'); foreach ($tests as $input => $expected) { $output = token_replace($input, array('node' => $node), array('language' => $language)); - $this->assertFalse(strcmp($output, $expected), t('Sanitized node token %token replaced.', array('%token' => $input))); + $this->assertEqual($output, $expected, format_string('Sanitized node token %token replaced.', array('%token' => $input))); } // Generate and test unsanitized tokens. $tests['[node:title]'] = $node->title; - $tests['[node:body]'] = $node->body[$node->language][0]['value']; - $tests['[node:summary]'] = $node->body[$node->language][0]['summary']; - $tests['[node:language]'] = $node->language; + $tests['[node:body]'] = $node->body[$langcode][0]['value']; + $tests['[node:summary]'] = $node->body[$langcode][0]['summary']; + $tests['[node:language]'] = $langcode; $tests['[node:author:name]'] = format_username($account); foreach ($tests as $input => $expected) { $output = token_replace($input, array('node' => $node), array('language' => $language, 'sanitize' => FALSE)); - $this->assertFalse(strcmp($output, $expected), t('Unsanitized node token %token replaced.', array('%token' => $input))); + $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))); } } } + +/** + * Tests user permissions for node revisions. + */ +class NodeRevisionPermissionsTestCase extends DrupalWebTestCase { + + /** + * Nodes used by the test. + * + * @var array + */ + protected $node_revisions = array(); + + /** + * Users with different revision permission used by the test. + * + * @var array + */ + protected $accounts = array(); + + /** + * Map revision permission names to node revision access ops. + * + * @var array + */ + protected $map = array( + 'view' => 'view revisions', + 'update' => 'revert revisions', + 'delete' => 'delete revisions', + ); + + public static function getInfo() { + return array( + 'name' => 'Node revision permissions', + 'description' => 'Tests user permissions for node revision operations.', + 'group' => 'Node', + ); + } + + function setUp() { + parent::setUp(); + + // Create a node with several revisions. + $node = $this->drupalCreateNode(); + $this->node_revisions[] = $node; + + for ($i = 0; $i < 3; $i++) { + // Create a revision for the same nid and settings with a random log. + $revision = clone $node; + $revision->revision = 1; + $revision->log = $this->randomName(32); + node_save($revision); + $this->node_revisions[] = $revision; + } + + // Create three users, one with each revision permission. + foreach ($this->map as $op => $permission) { + // Create the user. + $account = $this->drupalCreateUser( + array( + 'access content', + 'edit any page content', + 'delete any page content', + $permission, + ) + ); + $account->op = $op; + $this->accounts[] = $account; + } + + // Create an admin account (returns TRUE for all revision permissions). + $admin_account = $this->drupalCreateUser(array('access content', 'administer nodes')); + $admin_account->is_admin = TRUE; + $this->accounts['admin'] = $admin_account; + + // Create a normal account (returns FALSE for all revision permissions). + $normal_account = $this->drupalCreateUser(); + $normal_account->op = FALSE; + $this->accounts[] = $normal_account; + } + + /** + * Tests the _node_revision_access() function. + */ + function testNodeRevisionAccess() { + $revision = $this->node_revisions[1]; + + $parameters = array( + 'op' => array_keys($this->map), + 'account' => $this->accounts, + ); + + $permutations = $this->generatePermutations($parameters); + foreach ($permutations as $case) { + if (!empty($case['account']->is_admin) || $case['op'] == $case['account']->op) { + $this->assertTrue(_node_revision_access($revision, $case['op'], $case['account']), "{$this->map[$case['op']]} granted."); + } + else { + $this->assertFalse(_node_revision_access($revision, $case['op'], $case['account']), "{$this->map[$case['op']]} not granted."); + } + } + + // Test that access is FALSE for a node administrator with an invalid $node + // or $op parameters. + $admin_account = $this->accounts['admin']; + $this->assertFalse(_node_revision_access(FALSE, 'view', $admin_account), '_node_revision_access() returns FALSE with an invalid node.'); + $this->assertFalse(_node_revision_access($revision, 'invalid-op', $admin_account), '_node_revision_access() returns FALSE with an invalid op.'); + + // Test that the $account parameter defaults to the "logged in" user. + $original_user = $GLOBALS['user']; + $GLOBALS['user'] = $admin_account; + $this->assertTrue(_node_revision_access($revision, 'view'), '_node_revision_access() returns TRUE when used with global user.'); + $GLOBALS['user'] = $original_user; + } +} + +/** + * Tests pagination with a node access module enabled. + */ +class NodeAccessPagerTestCase extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Node access pagination', + 'description' => 'Test access controlled node views have the right amount of comment pages.', + 'group' => 'Node', + ); + } + + public function setUp() { + parent::setUp('node_access_test', 'comment', 'forum'); + node_access_rebuild(); + $this->web_user = $this->drupalCreateUser(array('access content', 'access comments', 'node test view')); + } + + /** + * Tests the comment pager for nodes with multiple grants per realm. + */ + public function testCommentPager() { + // Create a node. + $node = $this->drupalCreateNode(); + + // Create 60 comments. + for ($i = 0; $i < 60; $i++) { + $comment = new stdClass(); + $comment->cid = 0; + $comment->pid = 0; + $comment->uid = $this->web_user->uid; + $comment->nid = $node->nid; + $comment->subject = $this->randomName(); + $comment->comment_body = array( + LANGUAGE_NONE => array( + array('value' => $this->randomName()), + ), + ); + comment_save($comment); + } + + $this->drupalLogin($this->web_user); + + // View the node page. With the default 50 comments per page there should + // be two pages (0, 1) but no third (2) page. + $this->drupalGet('node/' . $node->nid); + $this->assertText($node->title); + $this->assertText(t('Comments')); + $this->assertRaw('page=1'); + $this->assertNoRaw('page=2'); + } + + /** + * Tests the forum node pager for nodes with multiple grants per realm. + */ + public function testForumPager() { + // Look up the forums vocabulary ID. + $vid = variable_get('forum_nav_vocabulary', 0); + $this->assertTrue($vid, 'Forum navigation vocabulary ID is set.'); + + // Look up the general discussion term. + $tree = taxonomy_get_tree($vid, 0, 1); + $tid = reset($tree)->tid; + $this->assertTrue($tid, 'General discussion term is found in the forum vocabulary.'); + + // Create 30 nodes. + for ($i = 0; $i < 30; $i++) { + $this->drupalCreateNode(array( + 'nid' => NULL, + 'type' => 'forum', + 'taxonomy_forums' => array( + LANGUAGE_NONE => array( + array('tid' => $tid, 'vid' => $vid, 'vocabulary_machine_name' => 'forums'), + ), + ), + )); + } + + // View the general discussion forum page. With the default 25 nodes per + // page there should be two pages for 30 nodes, no more. + $this->drupalLogin($this->web_user); + $this->drupalGet('forum/' . $tid); + $this->assertRaw('page=1'); + $this->assertNoRaw('page=2'); + } +} + + +/** + * Tests the interaction of the node access system with fields. + */ +class NodeAccessFieldTestCase extends NodeWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Node access and fields', + 'description' => 'Tests the interaction of the node access system with fields.', + 'group' => 'Node', + ); + } + + public function setUp() { + parent::setUp('node_access_test', 'field_ui'); + node_access_rebuild(); + + // Create some users. + $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'); + $this->field = field_create_field(array('field_name' => $this->field_name, 'type' => 'text')); + $this->instance = field_create_instance(array( + 'field_name' => $this->field_name, + 'entity_type' => 'node', + 'bundle' => 'page', + )); + } + + /** + * Tests administering fields when node access is restricted. + */ + function testNodeAccessAdministerField() { + // Create a page node. + $langcode = LANGUAGE_NONE; + $field_data = array(); + $value = $field_data[$langcode][0]['value'] = $this->randomName(); + $node = $this->drupalCreateNode(array($this->field_name => $field_data)); + + // Log in as the administrator and confirm that the field value is present. + $this->drupalLogin($this->admin_user); + $this->drupalGet("node/{$node->nid}"); + $this->assertText($value, 'The saved field value is visible to an administrator.'); + + // Log in as the content admin and try to view the node. + $this->drupalLogin($this->content_admin_user); + $this->drupalGet("node/{$node->nid}"); + $this->assertText('Access denied', 'Access is denied for the content admin.'); + + // Modify the field default as the content admin. + $edit = array(); + $default = 'Sometimes words have two meanings'; + $edit["{$this->field_name}[$langcode][0][value]"] = $default; + $this->drupalPost( + "admin/structure/types/manage/page/fields/{$this->field_name}", + $edit, + t('Save settings') + ); + + // Log in as the administrator. + $this->drupalLogin($this->admin_user); + + // Confirm that the existing node still has the correct field value. + $this->drupalGet("node/{$node->nid}"); + $this->assertText($value, 'The original field value is visible to an administrator.'); + + // Confirm that the new default value appears when creating a new node. + $this->drupalGet('node/add/page'); + $this->assertRaw($default, 'The updated default value is displayed when creating a new node.'); + } +} + +/** + * Tests changing view modes for nodes. + */ +class NodeEntityViewModeAlterTest extends NodeWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Node entity view mode', + 'description' => 'Test changing view mode.', + 'group' => 'Node' + ); + } + + function setUp() { + parent::setUp(array('node_test')); + } + + /** + * Create a "Basic page" node and verify its consistency in the database. + */ + function testNodeViewModeChange() { + $web_user = $this->drupalCreateUser(array('create page content', 'edit own page 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.'; + $this->drupalPost('node/add/page', $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'); + + // 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 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.0/modules/node/node.tokens.inc drupal-7.66/modules/node/node.tokens.inc --- drupal-7.0/modules/node/node.tokens.inc 2010-06-29 02:57:19.000000000 +0200 +++ drupal-7.66/modules/node/node.tokens.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: node.tokens.inc,v 1.15 2010/06/29 00:57:19 dries Exp $ /** * @file @@ -136,16 +135,36 @@ case 'body': case 'summary': - if (!empty($node->body)) { - $item = $node->body[$node->language][0]; - $column = ($name == 'body') ? 'value' : 'summary'; + if ($items = field_get_items('node', $node, 'body', $language_code)) { $instance = field_info_instance('node', 'body', $node->type); - $replacements[$original] = $sanitize ? _text_sanitize($instance, $node->language, $item, $column) : $item[$column]; + $field_langcode = field_language('node', $node, 'body', $language_code); + // 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; case 'language': - $replacements[$original] = $sanitize ? check_plain($node->language) : $node->language; + $langcode = entity_language('node', $node); + $replacements[$original] = $sanitize ? check_plain($langcode) : $langcode; break; case 'url': @@ -158,8 +177,9 @@ // Default values for the chained tokens handled below. case 'author': - $name = ($node->uid == 0) ? variable_get('anonymous', t('Anonymous')) : $node->name; - $replacements[$original] = $sanitize ? filter_xss($name) : $name; + $account = user_load($node->uid); + $name = format_username($account); + $replacements[$original] = $sanitize ? check_plain($name) : $name; break; case 'created': diff -Naur drupal-7.0/modules/node/node.tpl.php drupal-7.66/modules/node/node.tpl.php --- drupal-7.0/modules/node/node.tpl.php 2010-12-01 01:18:15.000000000 +0100 +++ drupal-7.66/modules/node/node.tpl.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: node.tpl.php,v 1.34 2010/12/01 00:18:15 webchick Exp $ /** * @file @@ -15,7 +14,7 @@ * - $date: Formatted creation date. Preprocess functions can reformat it by * calling format_date() with the desired parameters on the $created variable. * - $name: Themed username of node author output from theme_username(). - * - $node_url: Direct url of the current node. + * - $node_url: Direct URL of the current node. * - $display_submitted: Whether submission information should be displayed. * - $submitted: Submission information created from $name and $date during * template_preprocess_node(). @@ -23,7 +22,7 @@ * CSS. It can be manipulated through the variable $classes_array from * preprocess functions. The default values can be one or more of the * following: - * - node: The current template type, i.e., "theming hook". + * - node: The current template type; for example, "theming hook". * - node-[type]: The current node type. For example, if the node is a * "Blog entry" it would result in "node-blog". Note that the machine * name will often be in a short form of the human readable label. @@ -43,7 +42,7 @@ * * Other variables: * - $node: Full node object. Contains data that may not be safe. - * - $type: Node type, i.e. story, page, blog, etc. + * - $type: Node type; for example, story, page, blog, etc. * - $comment_count: Number of comments attached to the node. * - $uid: User ID of the node author. * - $created: Time the node was published formatted in Unix timestamp. @@ -54,7 +53,7 @@ * - $id: Position of the node. Increments each time it's output. * * Node status variables: - * - $view_mode: View mode, e.g. 'full', 'teaser'... + * - $view_mode: View mode; for example, "full", "teaser". * - $teaser: Flag for the teaser state (shortcut for $view_mode == 'teaser'). * - $page: Flag for the full page state. * - $promote: Flag for front page promotion state. @@ -68,15 +67,17 @@ * - $is_admin: Flags true when the current user is an administrator. * * Field variables: for each field instance attached to the node a corresponding - * variable is defined, e.g. $node->body becomes $body. When needing to access - * a field's raw values, developers/themers are strongly encouraged to use these - * variables. Otherwise they will have to explicitly specify the desired field - * language, e.g. $node->body['en'], thus overriding any language negotiation - * rule that was previously applied. + * variable is defined; for example, $node->body becomes $body. When needing to + * access a field's raw values, developers/themers are strongly encouraged to + * use these variables. Otherwise they will have to explicitly specify the + * desired field language; for example, $node->body['en'], thus overriding any + * language negotiation rule that was previously applied. * * @see template_preprocess() * @see template_preprocess_node() * @see template_process() + * + * @ingroup themeable */ ?> <div id="node-<?php print $node->nid; ?>" class="<?php print $classes; ?> clearfix"<?php print $attributes; ?>> diff -Naur drupal-7.0/modules/node/tests/node_access_test.info drupal-7.66/modules/node/tests/node_access_test.info --- drupal-7.0/modules/node/tests/node_access_test.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/node/tests/node_access_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: node_access_test.info,v 1.2 2010/12/20 19:59:42 webchick Exp $ name = "Node module access tests" description = "Support module for node permission testing." package = Testing @@ -6,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/node/tests/node_access_test.install drupal-7.66/modules/node/tests/node_access_test.install --- drupal-7.0/modules/node/tests/node_access_test.install 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/node/tests/node_access_test.install 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,42 @@ +<?php + +/** + * @file + * Install, update and uninstall functions for the node_access_test module. + */ + +/** + * Implements hook_schema(). + */ +function node_access_test_schema() { + $schema['node_access_test'] = array( + 'description' => 'The base table for node_access_test.', + 'fields' => array( + 'nid' => array( + 'description' => 'The {node}.nid this record affects.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'private' => array( + 'description' => 'Boolean indicating whether the node is private (visible to administrator) or not (visible to non-administrators).', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + ), + 'indexes' => array( + 'nid' => array('nid'), + ), + 'primary key' => array('nid'), + 'foreign keys' => array( + 'versioned_node' => array( + 'table' => 'node', + 'columns' => array('nid' => 'nid'), + ), + ), + ); + + return $schema; +} diff -Naur drupal-7.0/modules/node/tests/node_access_test.module drupal-7.66/modules/node/tests/node_access_test.module --- drupal-7.0/modules/node/tests/node_access_test.module 2010-11-20 05:33:56.000000000 +0100 +++ drupal-7.66/modules/node/tests/node_access_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,9 +1,10 @@ <?php -// $Id: node_access_test.module,v 1.4 2010/11/20 04:33:56 webchick Exp $ /** * @file - * Dummy module implementing node access related hooks to test API interaction + * A dummy module implementing node access related hooks for testing purposes. + * + * A dummy module implementing node access related hooks to test API interaction * with the Node module. This module restricts view permission to those with * a special 'node test view' permission. */ @@ -13,8 +14,10 @@ */ function node_access_test_node_grants($account, $op) { $grants = array(); + // First grant a grant to the author for own content. + $grants['node_access_test_author'] = array($account->uid); if ($op == 'view' && user_access('node test view', $account)) { - $grants['node_access_test'] = array(888); + $grants['node_access_test'] = array(8888, 8889); } if ($op == 'view' && $account->uid == variable_get('node_test_node_access_all_uid', 0)) { $grants['node_access_all'] = array(0); @@ -27,14 +30,35 @@ */ function node_access_test_node_access_records($node) { $grants = array(); - $grants[] = array( - 'realm' => 'node_access_test', - 'gid' => 888, - 'grant_view' => 1, - 'grant_update' => 0, - 'grant_delete' => 0, - 'priority' => 999, + // For NodeAccessBaseTableTestCase, only set records for private nodes. + if (!variable_get('node_access_test_private') || $node->private) { + $grants[] = array( + 'realm' => 'node_access_test', + 'gid' => 8888, + 'grant_view' => 1, + 'grant_update' => 0, + 'grant_delete' => 0, + 'priority' => 0, + ); + $grants[] = array( + 'realm' => 'node_access_test', + 'gid' => 8889, + 'grant_view' => 1, + 'grant_update' => 0, + 'grant_delete' => 0, + 'priority' => 0, + ); + // For the author realm, the GID is equivalent to a UID, which + // means there are many many groups of just 1 user. + $grants[] = array( + 'realm' => 'node_access_test_author', + 'gid' => $node->uid, + 'grant_view' => 1, + 'grant_update' => 1, + 'grant_delete' => 1, + 'priority' => 0, ); + } return $grants; } @@ -118,6 +142,8 @@ * database query is shown, and a list of the node IDs, for debugging purposes. * And if there is a query exception, the page says "Exception" and gives the * error. + * + * @see node_access_test_menu() */ function node_access_entity_test_page() { $output = ''; @@ -143,3 +169,62 @@ return $output; } + +/** + * Implements hook_form_BASE_FORM_ID_alter(). + */ +function node_access_test_form_node_form_alter(&$form, $form_state) { + // Only show this checkbox for NodeAccessBaseTableTestCase. + if (variable_get('node_access_test_private')) { + $form['private'] = array( + '#type' => 'checkbox', + '#title' => t('Private'), + '#description' => t('Check here if this content should be set private and only shown to privileged users.'), + '#default_value' => isset($form['#node']->private) ? $form['#node']->private : FALSE, + ); + } +} + +/** + * Implements hook_node_load(). + */ +function node_access_test_node_load($nodes, $types) { + $result = db_query('SELECT nid, private FROM {node_access_test} WHERE nid IN(:nids)', array(':nids' => array_keys($nodes))); + foreach ($result as $record) { + $nodes[$record->nid]->private = $record->private; + } +} + +/** + * Implements hook_node_delete(). + */ + +function node_access_test_node_delete($node) { + db_delete('node_access_test')->condition('nid', $node->nid)->execute(); +} + +/** + * Implements hook_node_insert(). + */ +function node_access_test_node_insert($node) { + _node_access_test_node_write($node); +} + +/** + * Implements hook_node_update(). + */ +function node_access_test_node_update($node) { + _node_access_test_node_write($node); +} + +/** + * Helper for node insert/update. + */ +function _node_access_test_node_write($node) { + if (isset($node->private)) { + db_merge('node_access_test') + ->key(array('nid' => $node->nid)) + ->fields(array('private' => (int) $node->private)) + ->execute(); + } +} diff -Naur drupal-7.0/modules/node/tests/node_test.info drupal-7.66/modules/node/tests/node_test.info --- drupal-7.0/modules/node/tests/node_test.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/node/tests/node_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: node_test.info,v 1.2 2010/12/20 19:59:42 webchick Exp $ name = "Node module tests" description = "Support module for node related testing." package = Testing @@ -6,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/node/tests/node_test.module drupal-7.66/modules/node/tests/node_test.module --- drupal-7.0/modules/node/tests/node_test.module 2010-11-30 20:31:46.000000000 +0100 +++ drupal-7.66/modules/node/tests/node_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,10 +1,11 @@ <?php -// $Id: node_test.module,v 1.14 2010/11/30 19:31:46 dries Exp $ /** * @file - * Dummy module implementing node related hooks to test API interaction with - * the Node module. + * A dummy module for testing node related hooks. + * + * This is a dummy module that implements node related hooks to test API + * interaction with the Node module. */ /** @@ -150,3 +151,31 @@ } } } + +/** + * Implements hook_entity_view_mode_alter(). + */ +function node_test_entity_view_mode_alter(&$view_mode, $context) { + // Only alter the view mode if we are on the test callback. + if ($change_view_mode = variable_get('node_test_change_view_mode', '')) { + $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.0/modules/node/tests/node_test_exception.info drupal-7.66/modules/node/tests/node_test_exception.info --- drupal-7.0/modules/node/tests/node_test_exception.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/node/tests/node_test_exception.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: node_test_exception.info,v 1.2 2010/12/20 19:59:42 webchick Exp $ name = "Node module exception tests" description = "Support module for node related exception testing." package = Testing @@ -6,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/node/tests/node_test_exception.module drupal-7.66/modules/node/tests/node_test_exception.module --- drupal-7.0/modules/node/tests/node_test_exception.module 2010-01-09 22:54:01.000000000 +0100 +++ drupal-7.66/modules/node/tests/node_test_exception.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,10 +1,8 @@ <?php -// $Id: node_test_exception.module,v 1.4 2010/01/09 21:54:01 webchick Exp $ /** * @file - * Dummy module implementing node related hooks to test API interaction with - * the Node module. + * A module implementing node related hooks to test API interaction. */ /** diff -Naur drupal-7.0/modules/openid/openid-rtl.css drupal-7.66/modules/openid/openid-rtl.css --- drupal-7.0/modules/openid/openid-rtl.css 2010-06-04 22:39:44.000000000 +0200 +++ drupal-7.66/modules/openid/openid-rtl.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -/* $Id: openid-rtl.css,v 1.1 2010/06/04 20:39:44 dries Exp $ */ #edit-openid-identifier { background-position: right 50%; diff -Naur drupal-7.0/modules/openid/openid.api.php drupal-7.66/modules/openid/openid.api.php --- drupal-7.0/modules/openid/openid.api.php 2010-05-13 19:37:24.000000000 +0200 +++ drupal-7.66/modules/openid/openid.api.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: openid.api.php,v 1.5 2010/05/13 17:37:24 dries Exp $ /** * @file @@ -50,8 +49,13 @@ * Allow modules to declare OpenID discovery methods. * * The discovery function callbacks will be called in turn with an unique - * parameter, the claimed identifier. They have to return an array of services, - * in the same form returned by openid_discover(). + * parameter, the claimed identifier. They have to return an associative array + * with array of services and claimed identifier in the same form as returned by + * openid_discover(). The resulting array must contain following keys: + * - 'services' (required) an array of discovered services (including OpenID + * version, endpoint URI, etc). + * - 'claimed_id' (optional) new claimed identifer, found by following HTTP + * redirects during the services discovery. * * The first discovery method that succeed (return at least one services) will * stop the discovery process. @@ -59,6 +63,7 @@ * @return * An associative array which keys are the name of the discovery methods and * values are function callbacks. + * * @see hook_openid_discovery_method_info_alter() */ function hook_openid_discovery_method_info() { diff -Naur drupal-7.0/modules/openid/openid.css drupal-7.66/modules/openid/openid.css --- drupal-7.0/modules/openid/openid.css 2010-05-05 18:28:06.000000000 +0200 +++ drupal-7.66/modules/openid/openid.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -/* $Id: openid.css,v 1.11 2010/05/05 16:28:06 dries Exp $ */ #edit-openid-identifier { background-image: url("login-bg.png"); diff -Naur drupal-7.0/modules/openid/openid.inc drupal-7.66/modules/openid/openid.inc --- drupal-7.0/modules/openid/openid.inc 2010-10-28 04:27:09.000000000 +0200 +++ drupal-7.66/modules/openid/openid.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: openid.inc,v 1.37 2010/10/28 02:27:09 dries Exp $ /** * @file @@ -90,7 +89,7 @@ */ function openid_redirect($url, $message) { global $language; - + $output = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">' . "\n"; $output .= '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="' . $language->language . '" lang="' . $language->language . '">' . "\n"; $output .= "<head>\n"; @@ -139,8 +138,33 @@ */ function _openid_xrds_parse($raw_xml) { $services = array(); - try { - $xml = @new SimpleXMLElement($raw_xml); + + // For PHP version >= 5.2.11, we can use this function to protect against + // malicious doctype declarations and other unexpected entity loading. + // However, we will not rely on it, and reject any XML with a DOCTYPE. + $disable_entity_loader = function_exists('libxml_disable_entity_loader'); + if ($disable_entity_loader) { + $load_entities = libxml_disable_entity_loader(TRUE); + } + + // Load the XML into a DOM document. + $dom = new DOMDocument(); + @$dom->loadXML($raw_xml); + + // Since DOCTYPE declarations from an untrusted source could be malicious, we + // stop parsing here and treat the XML as invalid since XRDS documents do not + // require, and are not expected to have, a DOCTYPE. + if (isset($dom->doctype)) { + 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) { foreach ($xrd->children(OPENID_NS_XRD)->Service as $service_element) { $service = array( @@ -166,9 +190,12 @@ } } } - catch (Exception $e) { - // Invalid XML. + + // Return the LIBXML options to the previous state before returning. + if ($disable_entity_loader) { + libxml_disable_entity_loader($load_entities); } + return $services; } @@ -189,32 +216,33 @@ // Extensible Resource Identifier (XRI) Resolution Version 2.0, section 4.3.3: // Find the service with the highest priority (lowest integer value). If there // is a tie, select a random one, not just the first in the XML document. - $selected_service = NULL; shuffle($services); + $selected_service = NULL; + $selected_type_priority = FALSE; // Search for an OP Identifier Element. foreach ($services as $service) { if (!empty($service['uri'])) { + $type_priority = FALSE; if (in_array('http://specs.openid.net/auth/2.0/server', $service['types'])) { $service['version'] = 2; + $type_priority = 1; + } + elseif (in_array('http://specs.openid.net/auth/2.0/signon', $service['types'])) { + $service['version'] = 2; + $type_priority = 2; } elseif (in_array(OPENID_NS_1_0, $service['types']) || in_array(OPENID_NS_1_1, $service['types'])) { $service['version'] = 1; + $type_priority = 3; } - if (isset($service['version']) && (!$selected_service || $service['priority'] < $selected_service['priority'])) { - $selected_service = $service; - } - } - } - if (!$selected_service) { - // Search for Claimed Identifier Element. - foreach ($services as $service) { - if (!empty($service['uri']) && in_array('http://specs.openid.net/auth/2.0/signon', $service['types'])) { - $service['version'] = 2; - if (!$selected_service || $service['priority'] < $selected_service['priority']) { - $selected_service = $service; - } + if ($type_priority + && (!$selected_service + || $type_priority < $selected_type_priority + || ($type_priority == $selected_type_priority && $service['priority'] < $selected_service['priority']))) { + $selected_service = $service; + $selected_type_priority = $type_priority; } } } @@ -357,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. @@ -448,15 +479,15 @@ $n = 0; foreach ($bytes as $byte) { - $n = bcmul($n, pow(2, 8)); - $n = bcadd($n, $byte); + $n = _openid_math_mul($n, pow(2, 8)); + $n = _openid_math_add($n, $byte); } return $n; } function _openid_dh_long_to_binary($long) { - $cmp = bccomp($long, 0); + $cmp = _openid_math_cmp($long, 0); if ($cmp < 0) { return FALSE; } @@ -467,9 +498,9 @@ $bytes = array(); - while (bccomp($long, 0) > 0) { - array_unshift($bytes, bcmod($long, 256)); - $long = bcdiv($long, pow(2, 8)); + while (_openid_math_cmp($long, 0) > 0) { + array_unshift($bytes, _openid_math_mod($long, 256)); + $long = _openid_math_div($long, pow(2, 8)); } if ($bytes && ($bytes[0] > 127)) { @@ -512,11 +543,11 @@ $nbytes = strlen($rbytes); } - $mxrand = bcpow(256, $nbytes); + $mxrand = _openid_math_pow(256, $nbytes); // If we get a number less than this, then it is in the // duplicated range. - $duplicate = bcmod($mxrand, $stop); + $duplicate = _openid_math_mod($mxrand, $stop); if (count($duplicate_cache) > 10) { $duplicate_cache = array(); @@ -526,32 +557,16 @@ } 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 (bccomp($n, $duplicate) < 0); + } while (_openid_math_cmp($n, $duplicate) < 0); - return bcmod($n, $stop); + return _openid_math_mod($n, $stop); } 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) { @@ -617,18 +632,31 @@ * @param $fallback_prefix * An optional prefix that will be used in case no prefix is found for the * target extension namespace. + * @param $only_signed + * Return only keys that are included in the message signature in openid.sig. + * Unsigned fields may have been modified or added by other parties than the + * OpenID Provider. + * * @return * An associative array containing all the parameters in the response message * that belong to the extension. The keys are stripped from their namespace * prefix. + * * @see http://openid.net/specs/openid-authentication-2_0.html#extensions */ -function openid_extract_namespace($response, $extension_namespace, $fallback_prefix = NULL) { +function openid_extract_namespace($response, $extension_namespace, $fallback_prefix = NULL, $only_signed = FALSE) { + $signed_keys = explode(',', $response['openid.signed']); + // Find the namespace prefix. $prefix = $fallback_prefix; foreach ($response as $key => $value) { if ($value == $extension_namespace && preg_match('/^openid\.ns\.([^.]+)$/', $key, $matches)) { $prefix = $matches[1]; + if ($only_signed && !in_array('ns.' . $matches[1], $signed_keys)) { + // The namespace was defined but was not signed as required. In this + // case we do not fall back to $fallback_prefix. + $prefix = NULL; + } break; } } @@ -641,7 +669,9 @@ foreach ($response as $key => $value) { if (preg_match('/^openid\.' . $prefix . '\.(.+)$/', $key, $matches)) { $local_key = $matches[1]; - $output[$local_key] = $value; + if (!$only_signed || in_array($prefix . '.' . $local_key, $signed_keys)) { + $output[$local_key] = $value; + } } } @@ -683,3 +713,187 @@ return $output; } +/** + * Determine the available math library GMP vs. BCMath, favouring GMP for performance. + */ +function _openid_get_math_library() { + // Not drupal_static(), because a function is not going to disappear and + // change the output of this under any circumstances. + static $library; + + if (empty($library)) { + if (function_exists('gmp_add')) { + $library = 'gmp'; + } + elseif (function_exists('bcadd')) { + $library = 'bcmath'; + } + } + + return $library; +} + +/** + * Calls the add function from the available math library for OpenID. + */ +function _openid_math_add($x, $y) { + $library = _openid_get_math_library(); + switch ($library) { + case 'gmp': + return gmp_strval(gmp_add($x, $y)); + case 'bcmath': + return bcadd($x, $y); + } +} + +/** + * Calls the mul function from the available math library for OpenID. + */ +function _openid_math_mul($x, $y) { + $library = _openid_get_math_library(); + switch ($library) { + case 'gmp': + return gmp_mul($x, $y); + case 'bcmath': + return bcmul($x, $y); + } +} + +/** + * Calls the div function from the available math library for OpenID. + */ +function _openid_math_div($x, $y) { + $library = _openid_get_math_library(); + switch ($library) { + case 'gmp': + return gmp_div($x, $y); + case 'bcmath': + return bcdiv($x, $y); + } +} + +/** + * Calls the cmp function from the available math library for OpenID. + */ +function _openid_math_cmp($x, $y) { + $library = _openid_get_math_library(); + switch ($library) { + case 'gmp': + return gmp_cmp($x, $y); + case 'bcmath': + return bccomp($x, $y); + } +} + +/** + * Calls the mod function from the available math library for OpenID. + */ +function _openid_math_mod($x, $y) { + $library = _openid_get_math_library(); + switch ($library) { + case 'gmp': + return gmp_mod($x, $y); + case 'bcmath': + return bcmod($x, $y); + } +} + +/** + * Calls the pow function from the available math library for OpenID. + */ +function _openid_math_pow($x, $y) { + $library = _openid_get_math_library(); + switch ($library) { + case 'gmp': + return gmp_pow($x, $y); + case 'bcmath': + return bcpow($x, $y); + } +} + +/** + * Calls the mul function from the available math library for OpenID. + */ +function _openid_math_powmod($x, $y, $z) { + $library = _openid_get_math_library(); + switch ($library) { + case 'gmp': + return gmp_powm($x, $y, $z); + case 'bcmath': + return bcpowmod($x, $y, $z); + } +} + +/** + * Provides transition for accounts with possibly invalid OpenID identifiers in authmap. + * + * This function provides a less safe but more unobtrusive procedure for users + * who cannot login with their OpenID identifiers. OpenID identifiers in the + * authmap could be incomplete due to invalid OpenID implementation in previous + * versions of Drupal (e.g. fragment part of the identifier could be missing). + * For more information see http://drupal.org/node/1120290. + * + * @param string $identity + * The user's claimed OpenID identifier. + * + * @return + * A fully-loaded user object if the user is found or FALSE if not found. + */ +function _openid_invalid_openid_transition($identity, $response) { + $account = FALSE; + $fallback_account = NULL; + $fallback_identity = $identity; + + // Try to strip the fragment if it is present. + if (strpos($fallback_identity, '#') !== FALSE) { + $fallback_identity = preg_replace('/#.*/', '', $fallback_identity); + $fallback_account = user_external_load($fallback_identity); + } + + // Try to replace HTTPS with HTTP. OpenID providers often redirect + // from http to https, but Drupal didn't follow the redirect. + if (!$fallback_account && strpos($fallback_identity, 'https://') !== FALSE) { + $fallback_identity = str_replace('https://', 'http://', $fallback_identity); + $fallback_account = user_external_load($fallback_identity); + } + + // Try to use original identifier. + if (!$fallback_account && isset($_SESSION['openid']['user_login_values']['openid_identifier'])) { + $fallback_identity = openid_normalize($_SESSION['openid']['user_login_values']['openid_identifier']); + $fallback_account = user_external_load($fallback_identity); + } + + if ($fallback_account) { + // Try to extract e-mail address from Simple Registration (SREG) or + // Attribute Exchanges (AX) keys. + $email = ''; + $sreg_values = openid_extract_namespace($response, OPENID_NS_SREG, 'sreg', TRUE); + $ax_values = openid_extract_namespace($response, OPENID_NS_AX, 'ax', TRUE); + if (!empty($sreg_values['email']) && valid_email_address($sreg_values['email'])) { + $email = $sreg_values['email']; + } + elseif ($ax_mail_values = openid_extract_ax_values($ax_values, array('http://axschema.org/contact/email', 'http://schema.openid.net/contact/email'))) { + $email = current($ax_mail_values); + } + + // If this e-mail address is the same as the e-mail address found in user + // account, login the user and update the claimed identifier. + if ($email && ($email == $fallback_account->mail || $email == $fallback_account->init)) { + $query = db_insert('authmap') + ->fields(array( + 'authname' => $identity, + 'uid' => $fallback_account->uid, + 'module' => 'openid', + )) + ->execute(); + drupal_set_message(t('New OpenID identifier %identity was added as a replacement for invalid identifier %invalid_identity. To finish the invalid OpenID transition process, please go to your <a href="@openid_url">OpenID identities page</a> and remove the old identifier %invalid_identity.', array('%invalid_identity' => $fallback_identity, '%identity' => $identity, '@openid_url' => 'user/' . $fallback_account->uid . '/openid'))); + // Set the account to the found one. + $account = $fallback_account; + } + else { + drupal_set_message(t('There is already an existing account associated with the OpenID identifier that you have provided. However, due to a bug in the previous version of the authentication system, we can\'t be sure that this account belongs to you. If you are new on this site, please continue registering the new user account. If you already have a registered account on this site associated with the provided OpenID identifier, please try to <a href="@url_password">reset the password</a> or contact the site administrator.', array('@url_password' => 'user/password')), 'warning'); + } + } + + return $account; +} diff -Naur drupal-7.0/modules/openid/openid.info drupal-7.66/modules/openid/openid.info --- drupal-7.0/modules/openid/openid.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/openid/openid.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: openid.info,v 1.9 2010/12/20 19:59:42 webchick Exp $ name = OpenID description = "Allows users to log into your site using OpenID." version = VERSION @@ -6,8 +5,7 @@ core = 7.x files[] = openid.test -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/openid/openid.install drupal-7.66/modules/openid/openid.install --- drupal-7.0/modules/openid/openid.install 2011-01-02 18:26:39.000000000 +0100 +++ drupal-7.66/modules/openid/openid.install 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: openid.install,v 1.13 2011/01/02 17:26:39 webchick Exp $ /** * @file @@ -16,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', @@ -52,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( @@ -92,20 +95,27 @@ if ($phase == 'runtime') { // Check for the PHP BC Math library. - if (!function_exists('bcadd')) { - $requirements['bcmath'] = array( + if (!function_exists('bcadd') && !function_exists('gmp_add')) { + $requirements['openid_math'] = array( 'value' => t('Not installed'), 'severity' => REQUIREMENT_ERROR, - 'description' => t('OpenID requires the BC Math library for PHP which is missing or outdated. Check the <a href="@url">PHP BC Math Library documentation</a> for information on how to correct this.', array('@url' => 'http://www.php.net/manual/en/book.bc.php')), + 'description' => t('OpenID suggests the use of either the <a href="@gmp">GMP Math</a> (recommended for performance) or <a href="@bc">BC Math</a> libraries to enable OpenID associations.', array('@gmp' => 'http://php.net/manual/en/book.gmp.php', '@bc' => 'http://www.php.net/manual/en/book.bc.php')), + ); + } + elseif (!function_exists('gmp_add')) { + $requirements['openid_math'] = array( + 'value' => t('Not optimized'), + 'severity' => REQUIREMENT_WARNING, + 'description' => t('OpenID suggests the use of the GMP Math library for PHP for optimal performance. Check the <a href="@url">GMP Math Library documentation</a> for installation instructions.', array('@url' => 'http://www.php.net/manual/en/book.gmp.php')), ); } else { - $requirements['bcmath'] = array( + $requirements['openid_math'] = array( 'value' => t('Installed'), 'severity' => REQUIREMENT_OK, ); } - $requirements['bcmath']['title'] = t('BC Math library'); + $requirements['openid_math']['title'] = t('OpenID Math library'); } return $requirements; @@ -150,5 +160,71 @@ } /** - * @} End of "addtogroup updates-6.x-to-7.x" + * @} 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.0/modules/openid/openid.js drupal-7.66/modules/openid/openid.js --- drupal-7.0/modules/openid/openid.js 2010-03-22 19:55:45.000000000 +0100 +++ drupal-7.66/modules/openid/openid.js 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -// $Id: openid.js,v 1.13 2010/03/22 18:55:45 dries Exp $ (function ($) { Drupal.behaviors.openid = { @@ -8,14 +7,14 @@ var cookie = $.cookie('Drupal.visitor.openid_identifier'); // This behavior attaches by ID, so is only valid once on a page. - if (!$('#edit-openid-identifier.openid-processed').size()) { + if (!$('#edit-openid-identifier.openid-processed').length) { if (cookie) { $('#edit-openid-identifier').val(cookie); } - if ($('#edit-openid-identifier').val()) { + if ($('#edit-openid-identifier').val() || location.hash == '#openid-login') { $('#edit-openid-identifier').addClass('openid-processed'); loginElements.hide(); - // Use .css('display', 'block') instead of .show() to Konqueror friendly. + // Use .css('display', 'block') instead of .show() to be Konqueror friendly. openidElements.css('display', 'block'); } } diff -Naur drupal-7.0/modules/openid/openid.module drupal-7.66/modules/openid/openid.module --- drupal-7.0/modules/openid/openid.module 2010-11-30 18:16:37.000000000 +0100 +++ drupal-7.66/modules/openid/openid.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: openid.module,v 1.98 2010/11/30 17:16:37 dries Exp $ /** * @file @@ -71,7 +70,7 @@ case 'admin/help#openid': $output = ''; $output .= '<h3>' . t('About') . '</h3>'; - $output .= '<p>' . t('The OpenID module allows users to log in using the OpenID single sign on service. <a href="@openid-net">OpenID</a> is a secure method for logging into many websites with a single username and password. It does not require special software, and it does not share passwords with any site to which it is associated, including the site being logged into. The main benefit to users is that they can have a single password that they can use on many websites. This means they can easily update their single password from a centralized location, rather than having to change dozens of passwords individually. For more information, see the online handbook entry for <a href="@handbook">OpenID module</a>.', array('@openid-net' => 'http://openid.net', '@handbook' => 'http://drupal.org/handbook/modules/openid')) . '</p>'; + $output .= '<p>' . t('The OpenID module allows users to log in using the OpenID single sign on service. <a href="@openid-net">OpenID</a> is a secure method for logging into many websites with a single username and password. It does not require special software, and it does not share passwords with any site to which it is associated, including the site being logged into. The main benefit to users is that they can have a single password that they can use on many websites. This means they can easily update their single password from a centralized location, rather than having to change dozens of passwords individually. For more information, see the online handbook entry for <a href="@handbook">OpenID module</a>.', array('@openid-net' => 'http://openid.net', '@handbook' => 'http://drupal.org/documentation/modules/openid')) . '</p>'; $output .= '<h3>' . t('Uses') . '</h3>'; $output .= '<dl>'; $output .= '<dt>' . t('Logging in with OpenID') . '</dt>'; @@ -147,11 +146,11 @@ $items = array(); $items[] = array( - 'data' => l(t('Log in using OpenID'), '#'), + 'data' => l(t('Log in using OpenID'), '#openid-login', array('external' => TRUE)), 'class' => array('openid-link'), ); $items[] = array( - 'data' => l(t('Cancel OpenID login'), '#'), + 'data' => l(t('Cancel OpenID login'), '#', array('external' => TRUE)), 'class' => array('user-link'), ); @@ -186,10 +185,15 @@ $response = $_SESSION['openid']['response']; - // Extract Simple Registration keys from the response. - $sreg_values = openid_extract_namespace($response, OPENID_NS_SREG, 'sreg'); - // Extract Attribute Exchanges keys from the response. - $ax_values = openid_extract_namespace($response, OPENID_NS_AX, 'ax'); + // Extract Simple Registration keys from the response. We only include + // signed keys as required by OpenID Simple Registration Extension 1.0, + // section 4. + $sreg_values = openid_extract_namespace($response, OPENID_NS_SREG, 'sreg', TRUE); + // Extract Attribute Exchanges keys from the response. We only include + // signed keys. This is not required by the specification, but it is + // recommended by Google, see + // http://googlecode.blogspot.com/2011/05/security-advisory-to-websites-using.html + $ax_values = openid_extract_namespace($response, OPENID_NS_AX, 'ax', TRUE); if (!empty($sreg_values['nickname'])) { // Use the nickname returned by Simple Registration if available. @@ -257,16 +261,25 @@ function openid_begin($claimed_id, $return_to = '', $form_values = array()) { module_load_include('inc', 'openid'); + $service = NULL; $claimed_id = openid_normalize($claimed_id); + $discovery = openid_discovery($claimed_id); - $services = openid_discovery($claimed_id); - $service = _openid_select_service($services); + if (!empty($discovery['services'])) { + $service = _openid_select_service($discovery['services']); + } - if (!$service) { + // Quit if the discovery result was empty or if we can't select any service. + if (!$discovery || !$service) { form_set_error('openid_identifier', t('Sorry, that is not a valid OpenID. Ensure you have spelled your ID correctly.')); return; } + // Set claimed id from discovery. + if (!empty($discovery['claimed_id'])) { + $claimed_id = $discovery['claimed_id']; + } + // Store discovered information in the users' session so we don't have to rediscover. $_SESSION['openid']['service'] = $service; // Store the claimed id @@ -275,9 +288,9 @@ // user_exteral_login later. $_SESSION['openid']['user_login_values'] = $form_values; - // If bcmath is present, then create an association + // If a supported math library is present, then create an association. $assoc_handle = ''; - if (function_exists('bcadd')) { + if (_openid_get_math_library()) { $assoc_handle = openid_association($service['uri']); } @@ -342,17 +355,29 @@ $response['openid.claimed_id'] = $service['claimed_id']; } elseif ($service['version'] == 2) { - $response['openid.claimed_id'] = openid_normalize($response['openid.claimed_id']); + // Returned Claimed Identifier could contain unique fragment + // identifier to allow identifier recycling so we need to preserve + // it in the response. + $response_claimed_id = openid_normalize($response['openid.claimed_id']); + // OpenID Authentication, section 11.2: // If the returned Claimed Identifier is different from the one sent // 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['openid.claimed_id'] != $claimed_id) { - $services = openid_discovery($response['openid.claimed_id']); + if ($response_claimed_id != $claimed_id || $response_claimed_id != $response['openid.identity']) { + $discovery = openid_discovery($response['openid.claimed_id']); $uris = array(); - foreach ($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'])) { + if ($discovery && !empty($discovery['services'])) { + 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'])) { + 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']; } } @@ -375,10 +400,21 @@ /** * Perform discovery on a claimed ID to determine the OpenID provider endpoint. * - * @param $claimed_id The OpenID URL to perform discovery on. + * Discovery methods are provided by the hook_openid_discovery_method_info and + * could be further altered using the hook_openid_discovery_method_info_alter. * - * @return Array of services discovered (including OpenID version, endpoint - * URI, etc). + * @param $claimed_id + * The OpenID URL to perform discovery on. + * + * @return + * The resulting discovery array from the first successful discovery method, + * which must contain following keys: + * - 'services' (required) an array of discovered services (including OpenID + * version, endpoint URI, etc). + * - 'claimed_id' (optional) new claimed identifer, found by following HTTP + * redirects during the services discovery. + * If all the discovery method fails or if no appropriate discovery method is + * found, FALSE is returned. */ function openid_discovery($claimed_id) { module_load_include('inc', 'openid'); @@ -386,19 +422,19 @@ $methods = module_invoke_all('openid_discovery_method_info'); drupal_alter('openid_discovery_method_info', $methods); - // Execute each method in turn. + // Execute each method in turn and return first successful discovery. foreach ($methods as $method) { - $discovered_services = $method($claimed_id); - if (!empty($discovered_services)) { - return $discovered_services; + $discovery = $method($claimed_id); + if (!empty($discovery)) { + return $discovery; } } - return array(); + return FALSE; } /** - * Implementation of hook_openid_discovery_method_info(). + * Implements hook_openid_discovery_method_info(). * * Define standard discovery methods. */ @@ -418,24 +454,33 @@ * * @see http://openid.net/specs/openid-authentication-2_0.html#discovery * @see hook_openid_discovery_method_info() + * @see openid_discovery() + * + * @return + * An array of discovered services and claimed identifier or NULL. See + * openid_discovery() for more specific information. */ function _openid_xri_discovery($claimed_id) { if (_openid_is_xri($claimed_id)) { // Resolve XRI using a proxy resolver (Extensible Resource Identifier (XRI) // Resolution Version 2.0, section 11.2 and 14.3). $xrds_url = variable_get('xri_proxy_resolver', 'http://xri.net/') . rawurlencode($claimed_id) . '?_xrd_r=application/xrds+xml'; - $services = _openid_xrds_discovery($xrds_url); - foreach ($services as $i => &$service) { - $status = $service['xrd']->children(OPENID_NS_XRD)->Status; - if ($status && $status->attributes()->cid == 'verified') { - $service['claimed_id'] = openid_normalize((string)$service['xrd']->children(OPENID_NS_XRD)->CanonicalID); + $discovery = _openid_xrds_discovery($xrds_url); + if (!empty($discovery['services']) && is_array($discovery['services'])) { + foreach ($discovery['services'] as $i => &$service) { + $status = $service['xrd']->children(OPENID_NS_XRD)->Status; + if ($status && $status->attributes()->cid == 'verified') { + $service['claimed_id'] = openid_normalize((string)$service['xrd']->children(OPENID_NS_XRD)->CanonicalID); + } + else { + // Ignore service if the Canonical ID could not be verified. + unset($discovery['services'][$i]); + } } - else { - // Ignore service if CanonicalID could not be verified. - unset($services[$i]); + if (!empty($discovery['services'])) { + return $discovery; } } - return $services; } } @@ -444,6 +489,11 @@ * * @see http://openid.net/specs/openid-authentication-2_0.html#discovery * @see hook_openid_discovery_method_info() + * @see openid_discovery() + * + * @return + * An array of discovered services and claimed identifier or NULL. See + * openid_discovery() for more specific information. */ function _openid_xrds_discovery($claimed_id) { $services = array(); @@ -455,7 +505,18 @@ $headers = array('Accept' => 'application/xrds+xml'); $result = drupal_http_request($xrds_url, array('headers' => $headers)); - if (!isset($result->error)) { + // Check for HTTP error and make sure, that we reach the target. If the + // maximum allowed redirects are exhausted, final destination URL isn't + // reached, but drupal_http_request() doesn't return any error. + // @todo Remove the check for 200 HTTP result code after the following issue + // will be fixed: http://drupal.org/node/1096890. + if (!isset($result->error) && $result->code == 200) { + + // Replace the user-entered claimed_id if we received a redirect. + if (!empty($result->redirect_url)) { + $claimed_id = openid_normalize($result->redirect_url); + } + if (isset($result->headers['content-type']) && preg_match("/application\/xrds\+xml/", $result->headers['content-type'])) { // Parse XML document to find URL $services = _openid_xrds_parse($result->data); @@ -501,11 +562,17 @@ } } } - return $services; + + if (!empty($services)) { + return array( + 'services' => $services, + 'claimed_id' => $claimed_id, + ); + } } /** - * Implementation of hook_openid_normalization_method_info(). + * Implements hook_openid_normalization_method_info(). * * Define standard normalization methods. */ @@ -540,8 +607,8 @@ $mod = OPENID_DH_DEFAULT_MOD; $gen = OPENID_DH_DEFAULT_GEN; $r = _openid_dh_rand($mod); - $private = bcadd($r, 1); - $public = bcpowmod($gen, $private, $mod); + $private = _openid_math_add($r, 1); + $public = _openid_math_powmod($gen, $private, $mod); // If there is no existing association, then request one $assoc_request = openid_association_request($public); @@ -564,7 +631,7 @@ if ($assoc_response['session_type'] == 'DH-SHA1') { $spub = _openid_dh_base64_to_long($assoc_response['dh_server_public']); $enc_mac_key = base64_decode($assoc_response['enc_mac_key']); - $shared = bcpowmod($spub, $private, $mod); + $shared = _openid_math_powmod($spub, $private, $mod); $assoc_response['mac_key'] = base64_encode(_openid_dh_xorsecret($shared, $enc_mac_key)); } db_insert('openid_association') @@ -590,8 +657,15 @@ */ function openid_authentication($response) { $identity = $response['openid.claimed_id']; - $account = user_external_load($identity); + + // Tries to load user account if user_external_load fails due to possibly + // incompletely stored OpenID identifier in the authmap. + if (!isset($account->uid) && variable_get('openid_less_obtrusive_transition', FALSE)) { + module_load_include('inc', 'openid'); + $account = _openid_invalid_openid_transition($identity, $response); + } + if (isset($account->uid)) { if (!variable_get('user_email_verification', TRUE) || $account->login) { // Check if user is blocked. @@ -635,7 +709,7 @@ drupal_set_message(t('Account registration using the information provided by your OpenID provider failed due to the reasons listed below. Complete the registration by filling out the form below. If you already have an account, you can <a href="@login">log in</a> now and add your OpenID under "My account".', array('@login' => url('user/login'))), 'warning'); // Append form validation errors below the above warning. foreach ($messages['error'] as $message) { - drupal_set_message( $message, 'error'); + drupal_set_message($message, 'error'); } } @@ -719,7 +793,21 @@ $request = array_merge($request, module_invoke_all('openid', 'request', $request)); - return $request; + // module_invoke_all() uses array_merge_recursive() which might return nested + // arrays if two or more modules alter a given parameter, resulting in an + // invalid request format. To ensure this doesn't happen, we flatten the returned + // value by taking the last entry in the array if an array is returned. + $flattened_request = array(); + foreach ($request as $key => $value) { + if (is_array($value)) { + $flattened_request[$key] = end($value); + } + else { + $flattened_request[$key] = $value; + } + } + + return $flattened_request; } /** @@ -757,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)) { @@ -789,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(); } @@ -942,7 +1031,7 @@ /** * Remove expired nonces from the database. * - * Implementation of hook_cron(). + * Implements hook_cron(). */ function openid_cron() { db_delete('openid_nonce') diff -Naur drupal-7.0/modules/openid/openid.pages.inc drupal-7.66/modules/openid/openid.pages.inc --- drupal-7.0/modules/openid/openid.pages.inc 2010-04-24 16:49:14.000000000 +0200 +++ drupal-7.66/modules/openid/openid.pages.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: openid.pages.inc,v 1.28 2010/04/24 14:49:14 dries Exp $ /** * @file @@ -57,6 +56,7 @@ '#theme' => 'table', '#header' => $header, '#rows' => $rows, + '#empty' => t('No OpenID identities available for this account.'), ); $build['openid_user_add'] = drupal_get_form('openid_user_add'); return $build; diff -Naur drupal-7.0/modules/openid/openid.test drupal-7.66/modules/openid/openid.test --- drupal-7.0/modules/openid/openid.test 2010-10-08 07:28:30.000000000 +0200 +++ drupal-7.66/modules/openid/openid.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,9 @@ <?php -// $Id: openid.test,v 1.32 2010/10/08 05:28:30 webchick Exp $ + +/** + * @file + * Tests for openid.module. + */ /** * Base class for OpenID tests. @@ -15,7 +19,7 @@ $this->drupalPost('', $edit, t('Log in')); // Check we are on the OpenID redirect form. - $this->assertTitle(t('OpenID redirect'), t('OpenID redirect page was displayed.')); + $this->assertTitle(t('OpenID redirect'), 'OpenID redirect page was displayed.'); // Submit form to the OpenID Provider Endpoint. $this->drupalPost(NULL, array(), t('Send')); @@ -74,23 +78,23 @@ // the URL of the OpenID Provider Endpoint. // Identifier is the URL of an XRDS document. - // The URL scheme is stripped in order to test that the supplied identifier - // is normalized in openid_begin(). + // On HTTP test environments, the URL scheme is stripped in order to test + // that the supplied identifier is normalized in openid_begin(). $identity = url('openid-test/yadis/xrds', array('absolute' => TRUE)); - $this->addIdentity(preg_replace('@^https?://@', '', $identity), 2, 'http://example.com/xrds', $identity); + $this->addIdentity(preg_replace('@^http://@', '', $identity), 2, 'http://example.com/xrds', $identity); $identity = url('openid-test/yadis/xrds/delegate', array('absolute' => TRUE)); - $this->addIdentity(preg_replace('@^https?://@', '', $identity), 2, 'http://example.com/xrds-delegate', $identity); + $this->addIdentity(preg_replace('@^http://@', '', $identity), 2, 'http://example.com/xrds-delegate', $identity); // Identifier is the URL of an XRDS document containing an OP Identifier // Element. The Relying Party sends the special value // "http://specs.openid.net/auth/2.0/identifier_select" as Claimed - // Identifier. The OpenID Provider responds with the actual identifier. - $identity = url('openid-test/yadis/xrds/dummy-user', array('absolute' => TRUE)); - // Tell openid_test.module to respond with this identifier. The URL scheme - // is stripped in order to test that the returned identifier is normalized in - // openid_complete(). - variable_set('openid_test_response', array('openid.claimed_id' => preg_replace('@^https?://@', '', $identity))); + // Identifier. The OpenID Provider responds with the actual identifier + // including the fragment. + $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, '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()); @@ -120,6 +124,28 @@ // OpenID Authentication 2.0, section 7.3.3: $this->addIdentity(url('openid-test/html/openid2', array('absolute' => TRUE)), 2, 'http://example.com/html-openid2'); + + // OpenID Authentication 2.0, section 7.2.4: + // URL Identifiers MUST then be further normalized by both (1) following + // redirects when retrieving their content and finally (2) applying the + // rules in Section 6 of RFC3986 to the final destination URL. This final + // URL MUST be noted by the Relying Party as the Claimed Identifier and be + // used when requesting authentication. + + // Single redirect. + $identity = $expected_claimed_id = url('openid-test/redirected/yadis/xrds/1', array('absolute' => TRUE)); + $this->addRedirectedIdentity($identity, 2, 'http://example.com/xrds', $expected_claimed_id, 0); + + // Exact 3 redirects (default value for the 'max_redirects' option in + // drupal_http_request()). + $identity = $expected_claimed_id = url('openid-test/redirected/yadis/xrds/2', array('absolute' => TRUE)); + $this->addRedirectedIdentity($identity, 2, 'http://example.com/xrds', $expected_claimed_id, 2); + + // Fails because there are more than 3 redirects (default value for the + // 'max_redirects' option in drupal_http_request()). + $identity = url('openid-test/redirected/yadis/xrds/3', array('absolute' => TRUE)); + $expected_claimed_id = FALSE; + $this->addRedirectedIdentity($identity, 2, 'http://example.com/xrds', $expected_claimed_id, 3); } /** @@ -136,7 +162,7 @@ // Test logging in via the login block on the front page. $this->submitLoginForm($identity); - $this->assertLink(t('Log out'), 0, t('User was logged in.')); + $this->assertLink(t('Log out'), 0, 'User was logged in.'); $this->drupalLogout(); @@ -145,15 +171,24 @@ $this->drupalPost('user/login', $edit, t('Log in')); // Check we are on the OpenID redirect form. - $this->assertTitle(t('OpenID redirect'), t('OpenID redirect page was displayed.')); + $this->assertTitle(t('OpenID redirect'), 'OpenID redirect page was displayed.'); // Submit form to the OpenID Provider Endpoint. $this->drupalPost(NULL, array(), t('Send')); - $this->assertLink(t('Log out'), 0, t('User was logged in.')); + $this->assertLink(t('Log out'), 0, 'User was logged in.'); // Verify user was redirected away from user/login to an accessible page. $this->assertResponse(200); + + $this->drupalLogout(); + // Use a User-supplied Identity that is the URL of an XRDS document. + // Tell the test module to add a doctype. This should fail. + $identity = url('openid-test/yadis/xrds', array('absolute' => TRUE, 'query' => array('doctype' => 1))); + // Test logging in via the login block on the front page. + $edit = array('openid_identifier' => $identity); + $this->drupalPost('', $edit, t('Log in')); + $this->assertRaw(t('Sorry, that is not a valid OpenID. Ensure you have spelled your ID correctly.'), 'XML with DOCTYPE was rejected.'); } /** @@ -176,12 +211,12 @@ $this->drupalPost('user/login', $edit, t('Log in')); // Check we are on the OpenID redirect form. - $this->assertTitle(t('OpenID redirect'), t('OpenID redirect page was displayed.')); + $this->assertTitle(t('OpenID redirect'), 'OpenID redirect page was displayed.'); // Submit form to the OpenID Provider Endpoint. $this->drupalPost(NULL, array(), t('Send')); - $this->assertLink(t('Log out'), 0, t('User was logged in.')); + $this->assertLink(t('Log out'), 0, 'User was logged in.'); // Verify user was redirected away from user/login to an accessible page. $this->assertText(t('Operating in maintenance mode.')); @@ -197,14 +232,14 @@ // Add identity to user's profile. $identity = url('openid-test/yadis/xrds', array('absolute' => TRUE)); $this->addIdentity($identity); - $this->assertText($identity, t('Identity appears in list.')); + $this->assertText($identity, 'Identity appears in list.'); // Delete the newly added identity. $this->clickLink(t('Delete')); $this->drupalPost(NULL, array(), t('Confirm')); - $this->assertText(t('OpenID deleted.'), t('Identity deleted')); - $this->assertNoText($identity, t('Identity no longer appears in list.')); + $this->assertText(t('OpenID deleted.'), 'Identity deleted'); + $this->assertNoText($identity, 'Identity no longer appears in list.'); } /** @@ -228,11 +263,11 @@ 'accounts[' . $this->web_user->uid . ']' => TRUE, ); $this->drupalPost('admin/people', $edit, t('Update')); - $this->assertRaw('The update has been performed.', t('Account was blocked.')); + $this->assertRaw('The update has been performed.', 'Account was blocked.'); $this->drupalLogout(); $this->submitLoginForm($identity); - $this->assertRaw(t('The username %name has not been activated or is blocked.', array('%name' => $this->web_user->name)), t('User login was blocked.')); + $this->assertRaw(t('The username %name has not been activated or is blocked.', array('%name' => $this->web_user->name)), 'User login was blocked.'); } /** @@ -256,14 +291,14 @@ $this->drupalPost('user/' . $this->web_user->uid . '/openid', $edit, t('Add an OpenID')); if ($claimed_id === FALSE) { - $this->assertRaw(t('Sorry, that is not a valid OpenID. Ensure you have spelled your ID correctly.'), t('Invalid identity was rejected.')); + $this->assertRaw(t('Sorry, that is not a valid OpenID. Ensure you have spelled your ID correctly.'), 'Invalid identity was rejected.'); return; } // OpenID 1 used a HTTP redirect, OpenID 2 uses a HTML form that is submitted automatically using JavaScript. if ($version == 2) { // Check we are on the OpenID redirect form. - $this->assertTitle(t('OpenID redirect'), t('OpenID redirect page was displayed.')); + $this->assertTitle(t('OpenID redirect'), 'OpenID redirect page was displayed.'); // Submit form to the OpenID Provider Endpoint. $this->drupalPost(NULL, array(), t('Send')); @@ -272,7 +307,42 @@ if (!isset($claimed_id)) { $claimed_id = $identity; } - $this->assertRaw(t('Successfully added %identity', array('%identity' => $claimed_id)), t('Identity %identity was added.', array('%identity' => $identity))); + $this->assertRaw(t('Successfully added %identity', array('%identity' => $claimed_id)), format_string('Identity %identity was added.', array('%identity' => $identity))); + } + + /** + * Add OpenID identity, changed by the following redirects, to user's profile. + * + * According to OpenID Authentication 2.0, section 7.2.4, URL Identifiers MUST + * be further normalized by following redirects when retrieving their content + * and this final URL MUST be noted by the Relying Party as the Claimed + * Identifier and be used when requesting authentication. + * + * @param $identity + * The User-supplied Identifier. + * @param $version + * The protocol version used by the service. + * @param $local_id + * The expected OP-Local Identifier found during discovery. + * @param $claimed_id + * The expected Claimed Identifier returned by the OpenID Provider, or FALSE + * if the discovery is expected to fail. + * @param $redirects + * The number of redirects. + */ + function addRedirectedIdentity($identity, $version = 2, $local_id = 'http://example.com/xrds', $claimed_id = NULL, $redirects = 0) { + // Set the final destination URL which is the same as the Claimed + // Identifier, we insert the same identifier also to the provider response, + // but provider could further change the Claimed ID actually (e.g. it could + // add unique fragment). + variable_set('openid_test_redirect_url', $identity); + variable_set('openid_test_response', array('openid.claimed_id' => $identity)); + + $this->addIdentity(url('openid-test/redirect/' . $redirects, array('absolute' => TRUE)), $version, $local_id, $claimed_id); + + // Clean up. + variable_del('openid_test_redirect_url'); + variable_del('openid_test_response'); } /** @@ -282,17 +352,49 @@ // Use a User-supplied Identity that is the URL of an XRDS document. $identity = url('openid-test/yadis/xrds', array('absolute' => TRUE)); - // Do not sign all mandatory fields (e.g. assoc_handle). + // Respond with an invalid signature. + variable_set('openid_test_response', array('openid.sig' => 'this-is-an-invalid-signature')); + $this->submitLoginForm($identity); + $this->assertRaw('OpenID login failed.'); + + // Do not sign the mandatory field openid.assoc_handle. variable_set('openid_test_response', array('openid.signed' => 'op_endpoint,claimed_id,identity,return_to,response_nonce')); $this->submitLoginForm($identity); $this->assertRaw('OpenID login failed.'); - // Sign all mandatory fields and some custom fields. - variable_set('openid_test_response', array('openid.foo' => 'bar', 'openid.signed' => 'op_endpoint,claimed_id,identity,return_to,response_nonce,assoc_handle,foo')); + // Sign all mandatory fields and a custom field. + $keys_to_sign = array('op_endpoint', 'claimed_id', 'identity', 'return_to', 'response_nonce', 'assoc_handle', 'foo'); + $association = new stdClass(); + $association->mac_key = variable_get('mac_key'); + $response = array( + 'openid.op_endpoint' => url('openid-test/endpoint', array('absolute' => TRUE)), + 'openid.claimed_id' => $identity, + 'openid.identity' => $identity, + 'openid.return_to' => url('openid/authenticate', array('absolute' => TRUE)), + 'openid.response_nonce' => _openid_nonce(), + 'openid.assoc_handle' => 'openid-test', + 'openid.foo' => 123, + 'openid.signed' => implode(',', $keys_to_sign), + ); + $response['openid.sig'] = _openid_signature($association, $response, $keys_to_sign); + variable_set('openid_test_response', $response); $this->submitLoginForm($identity); $this->assertNoRaw('OpenID login failed.'); - } + $this->assertFieldByName('name', '', 'No username was supplied by provider.'); + $this->assertFieldByName('mail', '', 'No e-mail address was supplied by provider.'); + // Check that unsigned SREG fields are ignored. + $response = array( + 'openid.signed' => 'op_endpoint,claimed_id,identity,return_to,response_nonce,assoc_handle,sreg.nickname', + 'openid.sreg.nickname' => 'john', + 'openid.sreg.email' => 'john@example.com', + ); + variable_set('openid_test_response', $response); + $this->submitLoginForm($identity); + $this->assertNoRaw('OpenID login failed.'); + $this->assertFieldByName('name', 'john', 'Username was supplied by provider.'); + $this->assertFieldByName('mail', '', 'E-mail address supplied by provider was ignored.'); + } } /** @@ -324,14 +426,14 @@ // Use a User-supplied Identity that is the URL of an XRDS document. $identity = url('openid-test/yadis/xrds', array('absolute' => TRUE)); $this->submitLoginForm($identity); - $this->assertRaw(t('Once you have verified your e-mail address, you may log in via OpenID.'), t('User was asked to verify e-mail address.')); - $this->assertRaw(t('A welcome message with further instructions has been sent to your e-mail address.'), t('A welcome message was sent to the user.')); + $this->assertRaw(t('Once you have verified your e-mail address, you may log in via OpenID.'), 'User was asked to verify e-mail address.'); + $this->assertRaw(t('A welcome message with further instructions has been sent to your e-mail address.'), 'A welcome message was sent to the user.'); $reset_url = $this->getPasswordResetURLFromMail(); $user = user_load_by_name('john'); - $this->assertTrue($user, t('User was registered with right username.')); - $this->assertEqual($user->mail, 'john@example.com', t('User was registered with right email address.')); - $this->assertFalse($user->data, t('No additional user info was saved.')); + $this->assertTrue($user, 'User was registered with right username.'); + $this->assertEqual($user->mail, 'john@example.com', 'User was registered with right email address.'); + $this->assertFalse($user->data, 'No additional user info was saved.'); $this->submitLoginForm($identity); $this->assertRaw(t('You must validate your email address for this account before logging in via OpenID.')); @@ -344,7 +446,7 @@ // Verify that the account was activated. $this->submitLoginForm($identity); - $this->assertLink(t('Log out'), 0, t('User was logged in.')); + $this->assertLink(t('Log out'), 0, 'User was logged in.'); } /** @@ -359,17 +461,17 @@ // Use a User-supplied Identity that is the URL of an XRDS document. $identity = url('openid-test/yadis/xrds', array('absolute' => TRUE)); $this->submitLoginForm($identity); - $this->assertLink(t('Log out'), 0, t('User was logged in.')); + $this->assertLink(t('Log out'), 0, 'User was logged in.'); $user = user_load_by_name('john'); - $this->assertTrue($user, t('User was registered with right username.')); - $this->assertEqual($user->mail, 'john@example.com', t('User was registered with right email address.')); - $this->assertFalse($user->data, t('No additional user info was saved.')); + $this->assertTrue($user, 'User was registered with right username.'); + $this->assertEqual($user->mail, 'john@example.com', 'User was registered with right email address.'); + $this->assertFalse($user->data, 'No additional user info was saved.'); $this->drupalLogout(); $this->submitLoginForm($identity); - $this->assertLink(t('Log out'), 0, t('User was logged in.')); + $this->assertLink(t('Log out'), 0, 'User was logged in.'); } /** @@ -385,29 +487,29 @@ $identity = url('openid-test/yadis/xrds', array('absolute' => TRUE)); $this->submitLoginForm($identity); - $this->assertRaw(t('Account registration using the information provided by your OpenID provider failed due to the reasons listed below. Complete the registration by filling out the form below. If you already have an account, you can <a href="@login">log in</a> now and add your OpenID under "My account".', array('@login' => url('user/login'))), t('User was asked to complete the registration process manually.')); - $this->assertRaw(t('The name %name is already taken.', array('%name' => $web_user->name)), t('Form validation error for username was displayed.')); - $this->assertRaw(t('The e-mail address %mail is not valid.', array('%mail' => 'mail@invalid#')), t('Form validation error for e-mail address was displayed.')); + $this->assertRaw(t('Account registration using the information provided by your OpenID provider failed due to the reasons listed below. Complete the registration by filling out the form below. If you already have an account, you can <a href="@login">log in</a> now and add your OpenID under "My account".', array('@login' => url('user/login'))), 'User was asked to complete the registration process manually.'); + $this->assertRaw(t('The name %name is already taken.', array('%name' => $web_user->name)), 'Form validation error for username was displayed.'); + $this->assertRaw(t('The e-mail address %mail is not valid.', array('%mail' => 'mail@invalid#')), 'Form validation error for e-mail address was displayed.'); // Enter username and e-mail address manually. $edit = array('name' => 'john', 'mail' => 'john@example.com'); $this->drupalPost(NULL, $edit, t('Create new account')); - $this->assertRaw(t('Once you have verified your e-mail address, you may log in via OpenID.'), t('User was asked to verify e-mail address.')); + $this->assertRaw(t('Once you have verified your e-mail address, you may log in via OpenID.'), 'User was asked to verify e-mail address.'); $reset_url = $this->getPasswordResetURLFromMail(); $user = user_load_by_name('john'); - $this->assertTrue($user, t('User was registered with right username.')); - $this->assertFalse($user->data, t('No additional user info was saved.')); + $this->assertTrue($user, 'User was registered with right username.'); + $this->assertFalse($user->data, 'No additional user info was saved.'); // Follow the one-time login that was sent in the welcome e-mail. $this->drupalGet($reset_url); $this->drupalPost(NULL, array(), t('Log in')); // The user is taken to user/%uid/edit. - $this->assertFieldByName('mail', 'john@example.com', t('User was registered with right e-mail address.')); + $this->assertFieldByName('mail', 'john@example.com', 'User was registered with right e-mail address.'); $this->clickLink(t('OpenID identities')); - $this->assertRaw($identity, t('OpenID identity was registered.')); + $this->assertRaw($identity, 'OpenID identity was registered.'); } /** @@ -421,29 +523,29 @@ // Use a User-supplied Identity that is the URL of an XRDS document. $identity = url('openid-test/yadis/xrds', array('absolute' => TRUE)); $this->submitLoginForm($identity); - $this->assertRaw(t('Complete the registration by filling out the form below. If you already have an account, you can <a href="@login">log in</a> now and add your OpenID under "My account".', array('@login' => url('user/login'))), t('User was asked to complete the registration process manually.')); - $this->assertNoRaw(t('You must enter a username.'), t('Form validation error for username was not displayed.')); - $this->assertNoRaw(t('You must enter an e-mail address.'), t('Form validation error for e-mail address was not displayed.')); + $this->assertRaw(t('Complete the registration by filling out the form below. If you already have an account, you can <a href="@login">log in</a> now and add your OpenID under "My account".', array('@login' => url('user/login'))), 'User was asked to complete the registration process manually.'); + $this->assertNoRaw(t('You must enter a username.'), 'Form validation error for username was not displayed.'); + $this->assertNoRaw(t('You must enter an e-mail address.'), 'Form validation error for e-mail address was not displayed.'); // Enter username and e-mail address manually. $edit = array('name' => 'john', 'mail' => 'john@example.com'); $this->drupalPost(NULL, $edit, t('Create new account')); - $this->assertRaw(t('Once you have verified your e-mail address, you may log in via OpenID.'), t('User was asked to verify e-mail address.')); + $this->assertRaw(t('Once you have verified your e-mail address, you may log in via OpenID.'), 'User was asked to verify e-mail address.'); $reset_url = $this->getPasswordResetURLFromMail(); $user = user_load_by_name('john'); - $this->assertTrue($user, t('User was registered with right username.')); - $this->assertFalse($user->data, t('No additional user info was saved.')); + $this->assertTrue($user, 'User was registered with right username.'); + $this->assertFalse($user->data, 'No additional user info was saved.'); // Follow the one-time login that was sent in the welcome e-mail. $this->drupalGet($reset_url); $this->drupalPost(NULL, array(), t('Log in')); // The user is taken to user/%uid/edit. - $this->assertFieldByName('mail', 'john@example.com', t('User was registered with right e-mail address.')); + $this->assertFieldByName('mail', 'john@example.com', 'User was registered with right e-mail address.'); $this->clickLink(t('OpenID identities')); - $this->assertRaw($identity, t('OpenID identity was registered.')); + $this->assertRaw($identity, 'OpenID identity was registered.'); } /** @@ -466,18 +568,101 @@ // Use a User-supplied Identity that is the URL of an XRDS document. $identity = url('openid-test/yadis/xrds', array('absolute' => TRUE)); $this->submitLoginForm($identity); - $this->assertLink(t('Log out'), 0, t('User was logged in.')); + $this->assertLink(t('Log out'), 0, 'User was logged in.'); $user = user_load_by_name('john'); - $this->assertTrue($user, t('User was registered with right username.')); - $this->assertEqual($user->mail, 'john@example.com', t('User was registered with right email address.')); + $this->assertTrue($user, 'User was registered with right username.'); + $this->assertEqual($user->mail, 'john@example.com', 'User was registered with right email address.'); + } +} + +/** + * Test account registration using Simple Registration and Attribute Exchange. + */ +class OpenIDInvalidIdentifierTransitionTestCase extends OpenIDFunctionalTestCase { + + public static function getInfo() { + return array( + 'name' => 'OpenID account update', + 'description' => 'Tries to correct OpenID identifiers attached to accounts if their identifiers were stripped.', + 'group' => 'OpenID', + ); + } + + function setUp() { + parent::setUp('openid', 'openid_test'); + variable_set('user_register', USER_REGISTER_VISITORS); + variable_set('openid_less_obtrusive_transition', TRUE); + } + + /** + * Test OpenID transition with e-mail mismatch. + */ + function testStrippedFragmentAccountEmailMismatch() { + $this->drupalLogin($this->web_user); + + // Use a User-supplied Identity that is the URL of an XRDS document. + $identity = url('openid-test/yadis/xrds', array('absolute' => TRUE, 'fragment' => $this->randomName())); + $identity_stripped = preg_replace('/#.*/', '', $identity); + + // Add invalid identifier to the authmap (identifier has stripped fragment). + $this->addIdentity($identity_stripped); + $this->drupalLogout(); + + // Test logging in via the login form, provider will respond with full + // identifier (including fragment) but with different email, so we can't + // provide auto-update. + variable_set('openid_test_response', array( + 'openid.claimed_id' => $identity, + 'openid.sreg.nickname' => $this->web_user->name, + 'openid.sreg.email' => 'invalid-' . $this->web_user->mail)); + + $edit = array('openid_identifier' => $identity_stripped); + $this->submitLoginForm($identity_stripped); + + // Verify user was redirected away from user login to an accessible page. + $this->assertResponse(200); + + // Verify the message. + $this->assertRaw(t('There is already an existing account associated with the OpenID identifier that you have provided.'), 'Message that OpenID identifier must be updated manually was displayed.'); + } + + /** + * Test OpenID auto transition with e-mail. + */ + function testStrippedFragmentAccountAutoUpdateSreg() { + $this->drupalLogin($this->web_user); + + // Use a User-supplied Identity that is the URL of an XRDS document. + $identity = url('openid-test/yadis/xrds', array('absolute' => TRUE, 'fragment' => $this->randomName())); + $identity_stripped = preg_replace('/#.*/', '', $identity); + + // Add invalid identifier to the authmap (identifier has stripped fragment). + $this->addIdentity($identity_stripped); + $this->drupalLogout(); + + // Test logging in via the login form, provider will respond with full + // identifier (including fragment) but with different email, so we can't + // provide auto-update. + variable_set('openid_test_response', array( + 'openid.claimed_id' => $identity, + 'openid.sreg.nickname' => $this->web_user->name, + 'openid.sreg.email' => $this->web_user->mail)); + + $this->submitLoginForm($identity_stripped); + + // Verify user was redirected away from user login to an accessible page. + $this->assertResponse(200); + + // Verify the message. + $this->assertRaw(t('New OpenID identifier %identity was added as a replacement for invalid identifier %invalid_identity.', array('%invalid_identity' => $identity_stripped, '%identity' => $identity)), 'Message that OpenID identifier was added automatically was displayed.'); } } /** * Test internal helper functions. */ -class OpenIDUnitTest extends DrupalWebTestCase { +class OpenIDTestCase extends DrupalWebTestCase { public static function getInfo() { return array( 'name' => 'OpenID helper functions', @@ -495,25 +680,18 @@ * Test _openid_dh_XXX_to_XXX() functions. */ function testConversion() { - $this->assertEqual(_openid_dh_long_to_base64('12345678901234567890123456789012345678901234567890'), 'CHJ/Y2mq+DyhUCZ0evjH8ZbOPwrS', t('_openid_dh_long_to_base64() returned expected result.')); - $this->assertEqual(_openid_dh_base64_to_long('BsH/g8Nrpn2dtBSdu/sr1y8hxwyx'), '09876543210987654321098765432109876543210987654321', t('_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", t('_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', t('_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.'); } /** * Test _openid_dh_xorsecret(). */ function testOpenidDhXorsecret() { - $this->assertEqual(_openid_dh_xorsecret('123456790123456790123456790', "abc123ABC\x00\xFF"), "\xa4'\x06\xbe\xf1.\x00y\xff\xc2\xc1", t('_openid_dh_xorsecret() returned expected result.')); - } - - /** - * Test _openid_get_bytes(). - */ - function testOpenidGetBytes() { - $this->assertEqual(strlen(_openid_get_bytes(20)), 20, t('_openid_get_bytes() returned expected result.')); + $this->assertEqual(_openid_dh_xorsecret('123456790123456790123456790', "abc123ABC\x00\xFF"), "\xa4'\x06\xbe\xf1.\x00y\xff\xc2\xc1", '_openid_dh_xorsecret() returned expected result.'); } /** @@ -533,7 +711,7 @@ ); $association = new stdClass(); $association->mac_key = "1234567890abcdefghij\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\xF0\xF1\xF2\xF3\xF4\xF5\xF6\xF7\xF8\xF9"; - $this->assertEqual(_openid_signature($association, $response, array('foo', 'bar')), 'QnKZQzSFstT+GNiJDFOptdcZjrc=', t('Expected signature calculated.')); + $this->assertEqual(_openid_signature($association, $response, array('foo', 'bar')), 'QnKZQzSFstT+GNiJDFOptdcZjrc=', 'Expected signature calculated.'); } /** @@ -544,26 +722,26 @@ // section 7.2. If the user-supplied string starts with xri:// it should be // stripped and the resulting string should be treated as an XRI when it // starts with "=", "@", "+", "$", "!" or "(". - $this->assertTrue(_openid_is_xri('xri://=foo'), t('_openid_is_xri() returned expected result for an xri identifier with xri scheme.')); - $this->assertTrue(_openid_is_xri('xri://@foo'), t('_openid_is_xri() returned expected result for an xri identifier with xri scheme.')); - $this->assertTrue(_openid_is_xri('xri://+foo'), t('_openid_is_xri() returned expected result for an xri identifier with xri scheme.')); - $this->assertTrue(_openid_is_xri('xri://$foo'), t('_openid_is_xri() returned expected result for an xri identifier with xri scheme.')); - $this->assertTrue(_openid_is_xri('xri://!foo'), t('_openid_is_xri() returned expected result for an xri identifier with xri scheme..')); - $this->assertTrue(_openid_is_xri('xri://(foo'), t('_openid_is_xri() returned expected result for an xri identifier with xri scheme..')); - - $this->assertTrue(_openid_is_xri('=foo'), t('_openid_is_xri() returned expected result for an xri identifier.')); - $this->assertTrue(_openid_is_xri('@foo'), t('_openid_is_xri() returned expected result for an xri identifier.')); - $this->assertTrue(_openid_is_xri('+foo'), t('_openid_is_xri() returned expected result for an xri identifier.')); - $this->assertTrue(_openid_is_xri('$foo'), t('_openid_is_xri() returned expected result for an xri identifier.')); - $this->assertTrue(_openid_is_xri('!foo'), t('_openid_is_xri() returned expected result for an xri identifier.')); - $this->assertTrue(_openid_is_xri('(foo'), t('_openid_is_xri() returned expected result for an xri identifier.')); - - $this->assertFalse(_openid_is_xri('foo'), t('_openid_is_xri() returned expected result for an http URL.')); - $this->assertFalse(_openid_is_xri('xri://foo'), t('_openid_is_xri() returned expected result for an http URL.')); - $this->assertFalse(_openid_is_xri('http://foo/'), t('_openid_is_xri() returned expected result for an http URL.')); - $this->assertFalse(_openid_is_xri('http://example.com/'), t('_openid_is_xri() returned expected result for an http URL.')); - $this->assertFalse(_openid_is_xri('user@example.com/'), t('_openid_is_xri() returned expected result for an http URL.')); - $this->assertFalse(_openid_is_xri('http://user@example.com/'), t('_openid_is_xri() returned expected result for an http URL.')); + $this->assertTrue(_openid_is_xri('xri://=foo'), '_openid_is_xri() returned expected result for an xri identifier with xri scheme.'); + $this->assertTrue(_openid_is_xri('xri://@foo'), '_openid_is_xri() returned expected result for an xri identifier with xri scheme.'); + $this->assertTrue(_openid_is_xri('xri://+foo'), '_openid_is_xri() returned expected result for an xri identifier with xri scheme.'); + $this->assertTrue(_openid_is_xri('xri://$foo'), '_openid_is_xri() returned expected result for an xri identifier with xri scheme.'); + $this->assertTrue(_openid_is_xri('xri://!foo'), '_openid_is_xri() returned expected result for an xri identifier with xri scheme..'); + $this->assertTrue(_openid_is_xri('xri://(foo'), '_openid_is_xri() returned expected result for an xri identifier with xri scheme..'); + + $this->assertTrue(_openid_is_xri('=foo'), '_openid_is_xri() returned expected result for an xri identifier.'); + $this->assertTrue(_openid_is_xri('@foo'), '_openid_is_xri() returned expected result for an xri identifier.'); + $this->assertTrue(_openid_is_xri('+foo'), '_openid_is_xri() returned expected result for an xri identifier.'); + $this->assertTrue(_openid_is_xri('$foo'), '_openid_is_xri() returned expected result for an xri identifier.'); + $this->assertTrue(_openid_is_xri('!foo'), '_openid_is_xri() returned expected result for an xri identifier.'); + $this->assertTrue(_openid_is_xri('(foo'), '_openid_is_xri() returned expected result for an xri identifier.'); + + $this->assertFalse(_openid_is_xri('foo'), '_openid_is_xri() returned expected result for an http URL.'); + $this->assertFalse(_openid_is_xri('xri://foo'), '_openid_is_xri() returned expected result for an http URL.'); + $this->assertFalse(_openid_is_xri('http://foo/'), '_openid_is_xri() returned expected result for an http URL.'); + $this->assertFalse(_openid_is_xri('http://example.com/'), '_openid_is_xri() returned expected result for an http URL.'); + $this->assertFalse(_openid_is_xri('user@example.com/'), '_openid_is_xri() returned expected result for an http URL.'); + $this->assertFalse(_openid_is_xri('http://user@example.com/'), '_openid_is_xri() returned expected result for an http URL.'); } /** @@ -573,15 +751,52 @@ // Test that the normalization is according to OpenID Authentication 2.0, // section 7.2 and 11.5.2. - $this->assertEqual(openid_normalize('$foo'), '$foo', t('openid_normalize() correctly normalized an XRI.')); - $this->assertEqual(openid_normalize('xri://$foo'), '$foo', t('openid_normalize() correctly normalized an XRI with an xri:// scheme.')); + $this->assertEqual(openid_normalize('$foo'), '$foo', 'openid_normalize() correctly normalized an XRI.'); + $this->assertEqual(openid_normalize('xri://$foo'), '$foo', 'openid_normalize() correctly normalized an XRI with an xri:// scheme.'); + + $this->assertEqual(openid_normalize('example.com/'), 'http://example.com/', 'openid_normalize() correctly normalized a URL with a missing scheme.'); + $this->assertEqual(openid_normalize('example.com'), 'http://example.com/', 'openid_normalize() correctly normalized a URL with a missing scheme and empty path.'); + $this->assertEqual(openid_normalize('http://example.com'), 'http://example.com/', 'openid_normalize() correctly normalized a URL with an empty path.'); + + $this->assertEqual(openid_normalize('http://example.com/path'), 'http://example.com/path', 'openid_normalize() correctly normalized a URL with a path.'); + + $this->assertEqual(openid_normalize('http://example.com/path#fragment'), 'http://example.com/path', 'openid_normalize() correctly normalized a URL with a fragment.'); + } + + /** + * Test openid_extract_namespace(). + */ + function testOpenidExtractNamespace() { + $response = array( + 'openid.sreg.nickname' => 'john', + 'openid.ns.ext1' => OPENID_NS_SREG, + 'openid.ext1.nickname' => 'george', + 'openid.ext1.email' => 'george@example.com', + 'openid.ns.ext2' => 'http://example.com/ns/ext2', + 'openid.ext2.foo' => '123', + 'openid.ext2.bar' => '456', + 'openid.signed' => 'sreg.nickname,ns.ext1,ext1.email,ext2.foo', + ); + + $values = openid_extract_namespace($response, 'http://example.com/ns/dummy', NULL, FALSE); + $this->assertEqual($values, array(), 'Nothing found for unused namespace.'); - $this->assertEqual(openid_normalize('example.com/'), 'http://example.com/', t('openid_normalize() correctly normalized a URL with a missing scheme.')); - $this->assertEqual(openid_normalize('example.com'), 'http://example.com/', t('openid_normalize() correctly normalized a URL with a missing scheme and empty path.')); - $this->assertEqual(openid_normalize('http://example.com'), 'http://example.com/', t('openid_normalize() correctly normalized a URL with an empty path.')); + $values = openid_extract_namespace($response, 'http://example.com/ns/dummy', 'sreg', FALSE); + $this->assertEqual($values, array('nickname' => 'john'), 'Value found for fallback prefix.'); - $this->assertEqual(openid_normalize('http://example.com/path'), 'http://example.com/path', t('openid_normalize() correctly normalized a URL with a path.')); + $values = openid_extract_namespace($response, OPENID_NS_SREG, 'sreg', FALSE); + $this->assertEqual($values, array('nickname' => 'george', 'email' => 'george@example.com'), 'Namespace takes precedence over fallback prefix.'); - $this->assertEqual(openid_normalize('http://example.com/path#fragment'), 'http://example.com/path', t('openid_normalize() correctly normalized a URL with a fragment.')); + // ext1.email is signed, but ext1.nickname is not. + $values = openid_extract_namespace($response, OPENID_NS_SREG, 'sreg', TRUE); + $this->assertEqual($values, array('email' => 'george@example.com'), 'Unsigned namespaced fields ignored.'); + + $values = openid_extract_namespace($response, 'http://example.com/ns/ext2', 'sreg', FALSE); + $this->assertEqual($values, array('foo' => '123', 'bar' => '456'), 'Unsigned fields found.'); + + // ext2.foo and ext2.bar are ignored, because ns.ext2 is not signed. The + // fallback prefix is not used, because the namespace is specified. + $values = openid_extract_namespace($response, 'http://example.com/ns/ext2', 'sreg', TRUE); + $this->assertEqual($values, array(), 'Unsigned fields ignored.'); } } diff -Naur drupal-7.0/modules/openid/tests/openid_test.info drupal-7.66/modules/openid/tests/openid_test.info --- drupal-7.0/modules/openid/tests/openid_test.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/openid/tests/openid_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: openid_test.info,v 1.2 2010/12/20 19:59:42 webchick Exp $ name = OpenID dummy provider description = "OpenID provider used for testing." package = Testing @@ -7,8 +6,7 @@ dependencies[] = openid hidden = TRUE -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/openid/tests/openid_test.install drupal-7.66/modules/openid/tests/openid_test.install --- drupal-7.0/modules/openid/tests/openid_test.install 2009-12-04 17:49:47.000000000 +0100 +++ drupal-7.66/modules/openid/tests/openid_test.install 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: openid_test.install,v 1.4 2009/12/04 16:49:47 dries Exp $ /** * @file @@ -14,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.0/modules/openid/tests/openid_test.module drupal-7.66/modules/openid/tests/openid_test.module --- drupal-7.0/modules/openid/tests/openid_test.module 2010-07-07 10:05:01.000000000 +0200 +++ drupal-7.66/modules/openid/tests/openid_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: openid_test.module,v 1.17 2010/07/07 08:05:01 webchick Exp $ /** * @file @@ -61,6 +60,19 @@ 'access callback' => TRUE, 'type' => MENU_CALLBACK, ); + $items['openid-test/redirect'] = array( + 'title' => 'OpenID Provider Redirection Point', + 'page callback' => 'openid_test_redirect', + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); + $items['openid-test/redirected/%/%'] = array( + 'title' => 'OpenID Provider Final URL', + 'page callback' => 'openid_test_redirected_method', + 'page arguments' => array(2, 3), + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); return $items; } @@ -97,7 +109,11 @@ } } drupal_add_http_header('Content-Type', 'application/xrds+xml'); - print '<?xml version="1.0" encoding="UTF-8"?> + print '<?xml version="1.0" encoding="UTF-8"?>'; + if (!empty($_GET['doctype'])) { + print "\n<!DOCTYPE dct [ <!ELEMENT blue (#PCDATA)> ]>\n"; + } + print ' <xrds:XRDS xmlns:xrds="xri://$xrds" xmlns="xri://$xrd*($v*2.0)" xmlns:openid="http://openid.net/xmlns/1.0"> <XRD> <Status cid="' . check_plain(variable_get('openid_test_canonical_id_status', 'verified')) . '"/> @@ -106,6 +122,10 @@ <Service> <Type>http://example.com/this-is-ignored</Type> </Service> + <Service priority="5"> + <Type>http://openid.net/signon/1.0</Type> + <URI>http://example.com/this-is-only-openid-1.0</URI> + </Service> <Service priority="10"> <Type>http://specs.openid.net/auth/2.0/signon</Type> <Type>http://openid.net/srv/ax/1.0</Type> @@ -130,11 +150,12 @@ <Service priority="20"> <Type>http://specs.openid.net/auth/2.0/server</Type> <URI>' . url('openid-test/endpoint', array('absolute' => TRUE)) . '</URI> + <LocalID>' . url('openid-test/yadis/xrds/server', array('absolute' => TRUE)) . '</LocalID> </Service>'; } elseif (arg(3) == 'delegate') { print ' - <Service priority="5"> + <Service priority="0"> <Type>http://specs.openid.net/auth/2.0/signon</Type> <Type>http://openid.net/srv/ax/1.0</Type> <URI>' . url('openid-test/endpoint', array('absolute' => TRUE)) . '</URI> @@ -210,6 +231,28 @@ } /** + * Menu callback; redirect during Normalization/Discovery. + */ +function openid_test_redirect($count = 0) { + if ($count == 0) { + $url = variable_get('openid_test_redirect_url', ''); + } + else { + $url = url('openid-test/redirect/' . --$count, array('absolute' => TRUE)); + } + $http_response_code = variable_get('openid_test_redirect_http_reponse_code', 301); + header('Location: ' . $url, TRUE, $http_response_code); + exit(); +} + +/** + * Menu callback; respond with appropriate callback. + */ +function openid_test_redirected_method($method1, $method2) { + return call_user_func('openid_test_' . $method1 . '_' . $method2); +} + +/** * OpenID endpoint; handle "associate" requests (see OpenID Authentication 2.0, * section 8). * @@ -228,14 +271,14 @@ // Generate private Diffie-Helmann key. $r = _openid_dh_rand($mod); - $private = bcadd($r, 1); + $private = _openid_math_add($r, 1); // Calculate public Diffie-Helmann key. - $public = bcpowmod($gen, $private, $mod); + $public = _openid_math_powmod($gen, $private, $mod); // Calculate shared secret based on Relying Party's public key. $cpub = _openid_dh_base64_to_long($_REQUEST['openid_dh_consumer_public']); - $shared = bcpowmod($cpub, $private, $mod); + $shared = _openid_math_powmod($cpub, $private, $mod); // Encrypt the MAC key using the shared secret. $enc_mac_key = base64_encode(_openid_dh_xorsecret($shared, base64_decode(variable_get('mac_key')))); @@ -286,9 +329,7 @@ // Generate unique identifier for this authentication. $nonce = _openid_nonce(); - // Generate response containing the user's identity. The openid.sreg.xxx - // entries contain profile data stored by the OpenID Provider (see OpenID - // Simple Registration Extension 1.0). + // Generate response containing the user's identity. $response = variable_get('openid_test_response', array()) + array( 'openid.ns' => OPENID_NS_2_0, 'openid.mode' => 'id_res', @@ -298,14 +339,27 @@ 'openid.return_to' => $_REQUEST['openid_return_to'], 'openid.response_nonce' => $nonce, 'openid.assoc_handle' => 'openid-test', - 'openid.signed' => 'op_endpoint,claimed_id,identity,return_to,response_nonce,assoc_handle', ); + if (isset($response['openid.signed'])) { + $keys_to_sign = explode(',', $response['openid.signed']); + } + else { + // Unless openid.signed is explicitly defined, all keys are signed. + $keys_to_sign = array(); + foreach ($response as $key => $value) { + // Strip off the "openid." prefix. + $keys_to_sign[] = substr($key, 7); + } + $response['openid.signed'] = implode(',', $keys_to_sign); + } + // Sign the message using the MAC key that was exchanged during association. $association = new stdClass(); $association->mac_key = variable_get('mac_key'); - $keys_to_sign = explode(',', $response['openid.signed']); - $response['openid.sig'] = _openid_signature($association, $response, $keys_to_sign); + if (!isset($response['openid.sig'])) { + $response['openid.sig'] = _openid_signature($association, $response, $keys_to_sign); + } // Put the signed message into the query string of a URL supplied by the // Relying Party, and redirect the user. diff -Naur drupal-7.0/modules/overlay/images/close-rtl.png drupal-7.66/modules/overlay/images/close-rtl.png --- drupal-7.0/modules/overlay/images/close-rtl.png 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/overlay/images/close-rtl.png 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,5 @@ +PNG + +��� IHDR������������gAMA��|Q���QPLTE���ooo```ppp000���"���tRNS�0/@?���IDAT(υ DIAЂMVS܁Q*{>J<)/ߨQųI;�x!I9V` `j',>?~|ѧFGg'/A9aW<㺏eE16ŚcoT" +Y +)շ� :59]����IENDB` \ No newline at end of file diff -Naur drupal-7.0/modules/overlay/overlay-child-rtl.css drupal-7.66/modules/overlay/overlay-child-rtl.css --- drupal-7.0/modules/overlay/overlay-child-rtl.css 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/overlay/overlay-child-rtl.css 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,40 @@ + +/** + * @file + * RTL styling for Overlay child pages. + */ + +html { + direction: rtl; +} + +#overlay-title { + float: right; + left: auto; +} +#overlay { + padding: 0.2em; + padding-left: 26px; +} +#overlay-close-wrapper { + left: 0; + right: auto; +} +#overlay-close, +#overlay-close:hover { + background: transparent url(images/close-rtl.png) no-repeat; + -moz-border-radius-topright: 0; + -webkit-border-top-right-radius: 0; + border-top-right-radius: 0; +} + +/** + * Tabs on the overlay. + */ +#overlay-tabs { + left: 20px; + right: auto; +} +#overlay-tabs li { + margin: 0 -3px 0 0; +} diff -Naur drupal-7.0/modules/overlay/overlay-child.css drupal-7.66/modules/overlay/overlay-child.css --- drupal-7.0/modules/overlay/overlay-child.css 2011-01-03 07:53:35.000000000 +0100 +++ drupal-7.66/modules/overlay/overlay-child.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,8 @@ -/* $Id: overlay-child.css,v 1.8 2011/01/03 06:53:35 webchick Exp $ */ + +/** + * @file + * Basic styling for the Overlay child pages. + */ html.js { background: transparent !important; @@ -18,7 +22,8 @@ min-width: 700px; position: relative; padding: .2em; - padding-right: 26px; + padding-bottom: 2em; + padding-right: 26px; /* LTR */ width: 88%; } #overlay-titlebar { @@ -40,7 +45,7 @@ } #overlay-title { color: #fff; - float: left; + float: left; /* LTR */ font-size: 20px; margin: 0; padding: 0.3em 0; @@ -50,16 +55,23 @@ outline: 0; } +.overlay #skip-link { + margin-top: -20px; +} +.overlay #skip-link a { + color: #fff; /* This is white to contrast with the dark background behind it. */ +} + #overlay-close-wrapper { position: absolute; - right: 0; + right: 0; /* LTR */ } #overlay-close, #overlay-close:hover { - background: transparent url(images/close.png) no-repeat; - -moz-border-radius-topleft: 0; - -webkit-border-top-left-radius: 0; - border-top-left-radius: 0; + background: transparent url(images/close.png) no-repeat; /* LTR */ + -moz-border-radius-topleft: 0; /* LTR */ + -webkit-border-top-left-radius: 0; /* LTR */ + border-top-left-radius: 0; /* LTR */ display: block; height: 26px; margin: 0; @@ -76,13 +88,13 @@ line-height: 27px; margin: -28px 0 0 0; position: absolute; - right: 20px; + right: 20px; /* LTR */ text-transform: uppercase; } #overlay-tabs li { display: inline; list-style: none; - margin: 0 0 0 -3px; + margin: 0 0 0 -3px; /* LTR */ padding: 0; } #overlay-tabs li a, diff -Naur drupal-7.0/modules/overlay/overlay-child.js drupal-7.66/modules/overlay/overlay-child.js --- drupal-7.0/modules/overlay/overlay-child.js 2010-11-06 01:18:24.000000000 +0100 +++ drupal-7.66/modules/overlay/overlay-child.js 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,7 @@ -// $Id: overlay-child.js,v 1.11 2010/11/06 00:18:24 dries Exp $ +/** + * @file + * Attaches the behaviors for the Overlay child pages. + */ (function ($) { diff -Naur drupal-7.0/modules/overlay/overlay-parent.css drupal-7.66/modules/overlay/overlay-parent.css --- drupal-7.0/modules/overlay/overlay-parent.css 2010-11-06 01:18:24.000000000 +0100 +++ drupal-7.66/modules/overlay/overlay-parent.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,8 @@ -/* $Id: overlay-parent.css,v 1.17 2010/11/06 00:18:24 dries Exp $ */ + +/** + * @file + * Basic styling for the Overlay module. + */ html.overlay-open, html.overlay-open body { diff -Naur drupal-7.0/modules/overlay/overlay-parent.js drupal-7.66/modules/overlay/overlay-parent.js --- drupal-7.0/modules/overlay/overlay-parent.js 2010-11-06 01:18:24.000000000 +0100 +++ drupal-7.66/modules/overlay/overlay-parent.js 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,7 @@ -// $Id: overlay-parent.js,v 1.57 2010/11/06 00:18:24 dries Exp $ +/** + * @file + * Attaches the behaviors for the Overlay parent pages. + */ (function ($) { @@ -335,24 +338,34 @@ $placeholder.one('blur', function () { $(this).remove(); }); -} +}; /** * Check if the given link is in the administrative section of the site. * * @param url - * The url to be tested. + * The URL to be tested. * * @return boolean * TRUE if the URL represents an administrative link, FALSE otherwise. */ Drupal.overlay.isAdminLink = function (url) { + if (!Drupal.urlIsLocal(url)) { + return false; + } + var path = this.getPath(url); // Turn the list of administrative paths into a regular expression. if (!this.adminPathRegExp) { - var adminPaths = '^(' + Drupal.settings.overlay.paths.admin.replace(/\s+/g, ')$|^(') + ')$'; - var nonAdminPaths = '^(' + Drupal.settings.overlay.paths.non_admin.replace(/\s+/g, ')$|^(') + ')$'; + var prefix = ''; + if (Drupal.settings.overlay.pathPrefixes.length) { + // Allow path prefixes used for language negatiation followed by slash, + // and the empty string. + prefix = '(' + Drupal.settings.overlay.pathPrefixes.join('/|') + '/|)'; + } + var adminPaths = '^' + prefix + '(' + Drupal.settings.overlay.paths.admin.replace(/\s+/g, '|') + ')$'; + var nonAdminPaths = '^' + prefix + '(' + Drupal.settings.overlay.paths.non_admin.replace(/\s+/g, '|') + ')$'; adminPaths = adminPaths.replace(/\*/g, '.*'); nonAdminPaths = nonAdminPaths.replace(/\*/g, '.*'); this.adminPathRegExp = new RegExp(adminPaths); @@ -363,6 +376,42 @@ }; /** + * Determine whether a link is external to the site. + * + * Deprecated. Use Drupal.urlIsLocal() instead. + * + * @param url + * The URL to be tested. + * + * @return boolean + * TRUE if the URL is external to the site, FALSE otherwise. + */ +Drupal.overlay.isExternalLink = function (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; + } +}; + +/** * Event handler: resizes overlay according to the size of the parent window. * * @param event @@ -413,6 +462,27 @@ // IE6 doesn't support maxWidth, use width instead. var maxWidthName = (typeof document.body.style.maxWidth == 'string') ? 'maxWidth' : 'width'; + if (Drupal.overlay.leftSidedScrollbarOffset === undefined && $(document.documentElement).attr('dir') === 'rtl') { + // We can't use element.clientLeft to detect whether scrollbars are placed + // on the left side of the element when direction is set to "rtl" as most + // browsers dont't support it correctly. + // http://www.gtalbot.org/BugzillaSection/DocumentAllDHTMLproperties.html + // There seems to be absolutely no way to detect whether the scrollbar + // is on the left side in Opera; always expect scrollbar to be on the left. + if ($.browser.opera) { + Drupal.overlay.leftSidedScrollbarOffset = document.documentElement.clientWidth - this.iframeWindow.document.documentElement.clientWidth + this.iframeWindow.document.documentElement.clientLeft; + } + else if (this.iframeWindow.document.documentElement.clientLeft) { + Drupal.overlay.leftSidedScrollbarOffset = this.iframeWindow.document.documentElement.clientLeft; + } + else { + var el1 = $('<div style="direction: rtl; overflow: scroll;"></div>').appendTo(document.body); + var el2 = $('<div></div>').appendTo(el1); + Drupal.overlay.leftSidedScrollbarOffset = parseInt(el2[0].offsetLeft - el1[0].offsetLeft); + el1.remove(); + } + } + // Consider any element that should be visible above the overlay (such as // a toolbar). $('.overlay-displace-top, .overlay-displace-bottom').each(function () { @@ -423,6 +493,10 @@ maxWidth -= 1; } + if (Drupal.overlay.leftSidedScrollbarOffset) { + $(this).css('left', Drupal.overlay.leftSidedScrollbarOffset); + } + // Prevent displaced elements overlapping window's scrollbar. var currentMaxWidth = parseInt($(this).css(maxWidthName)); if ((data.drupalOverlay && data.drupalOverlay.maxWidth) || isNaN(currentMaxWidth) || currentMaxWidth > maxWidth || currentMaxWidth <= 0) { @@ -435,7 +509,12 @@ var offset = $(this).offset(); var offsetRight = offset.left + $(this).outerWidth(); if ((data.drupalOverlay && data.drupalOverlay.clip) || offsetRight > maxWidth) { - $(this).css('clip', 'rect(auto, ' + (maxWidth - offset.left) + 'px, ' + (documentHeight - offset.top) + 'px, auto)'); + if (Drupal.overlay.leftSidedScrollbarOffset) { + $(this).css('clip', 'rect(auto, auto, ' + (documentHeight - offset.top) + 'px, ' + (Drupal.overlay.leftSidedScrollbarOffset + 2) + 'px)'); + } + else { + $(this).css('clip', 'rect(auto, ' + (maxWidth - offset.left) + 'px, ' + (documentHeight - offset.top) + 'px, auto)'); + } (data.drupalOverlay = data.drupalOverlay || {}).clip = true; } }); @@ -453,7 +532,7 @@ Drupal.overlay.eventhandlerRestoreDisplacedElements = function (event) { var $displacedElements = $('.overlay-displace-top, .overlay-displace-bottom'); try { - $displacedElements.css({ maxWidth: null, clip: null }); + $displacedElements.css({ maxWidth: '', clip: '' }); } // IE bug that doesn't allow unsetting style.clip (http://dev.jquery.com/ticket/6512). catch (err) { @@ -508,7 +587,7 @@ var target = $target[0]; var href = target.href; - // Only handle links that have an href attribute and use the http(s) protocol. + // Only handle links that have an href attribute and use the HTTP(S) protocol. if (href != undefined && href != '' && target.protocol.match(/^https?\:/)) { var anchor = href.replace(target.ownerDocument.location.href, ''); // Skip anchor links. @@ -520,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 @@ -555,7 +634,14 @@ else { // Add the overlay-context state to the link, so "overlay-restore" links // can restore the context. - $target.attr('href', $.param.fragment(href, { 'overlay-context': this.getPath(window.location) + window.location.search })); + if ($target[0].hash) { + // Leave links with an existing fragment alone. Adding an extra + // parameter to a link like "node/1#section-1" breaks the link. + } + else { + // For links with no existing fragment, add the overlay context. + $target.attr('href', $.param.fragment(href, { 'overlay-context': this.getPath(window.location) + window.location.search })); + } // When the link has a destination query parameter and that destination // is an admin link we need to fragmentize it. This will make it reopen @@ -566,8 +652,11 @@ $target.attr('href', $.param.querystring(href, { destination: fragmentizedDestination })); } - // Make the link open in the immediate parent of the frame. - $target.attr('target', '_parent'); + // Make the link open in the immediate parent of the frame, unless the + // link already has a different target. + if (!$target.attr('target')) { + $target.attr('target', '_parent'); + } } } } @@ -590,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)); @@ -678,7 +771,7 @@ * Make a regular admin link into a URL that will trigger the overlay to open. * * @param link - * A Javascript Link object (i.e. an <a> element). + * A JavaScript Link object (i.e. an <a> element). * @param parentLocation * (optional) URL to override the parent window's location with. * @@ -808,8 +901,13 @@ if (lastDisplaced.length) { displacement = lastDisplaced.offset().top + lastDisplaced.outerHeight(); - // Remove height added by IE Shadow filter. - if (lastDisplaced[0].filters && lastDisplaced[0].filters.length && lastDisplaced[0].filters.item('DXImageTransform.Microsoft.Shadow')) { + // In modern browsers (including IE9), when box-shadow is defined, use the + // normal height. + var cssBoxShadowValue = lastDisplaced.css('box-shadow'); + var boxShadow = (typeof cssBoxShadowValue !== 'undefined' && cssBoxShadowValue !== 'none'); + // In IE8 and below, we use the shadow filter to apply box-shadow styles to + // the toolbar. It adds some extra height that we need to remove. + if (!boxShadow && /DXImageTransform\.Microsoft\.Shadow/.test(lastDisplaced.css('filter'))) { displacement -= lastDisplaced[0].filters.item('DXImageTransform.Microsoft.Shadow').strength; displacement = Math.max(0, displacement); } @@ -913,7 +1011,7 @@ var $element = $(this); var tabindex = $(this).attr('tabindex'); $element.data('drupalOverlayOriginalTabIndex', tabindex); -} +}; /** * Restore an element's original tabindex. diff -Naur drupal-7.0/modules/overlay/overlay.api.php drupal-7.66/modules/overlay/overlay.api.php --- drupal-7.0/modules/overlay/overlay.api.php 2010-07-17 04:12:36.000000000 +0200 +++ drupal-7.66/modules/overlay/overlay.api.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: overlay.api.php,v 1.2 2010/07/17 02:12:36 dries Exp $ /** * @file @@ -16,7 +15,7 @@ * * The parent window is initialized when a page is displayed in which the * overlay might be required to be displayed, so modules can act here if they - * need to take action to accomodate the possibility of the overlay appearing + * need to take action to accommodate the possibility of the overlay appearing * within a Drupal page. */ function hook_overlay_parent_initialize() { diff -Naur drupal-7.0/modules/overlay/overlay.info drupal-7.66/modules/overlay/overlay.info --- drupal-7.0/modules/overlay/overlay.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/overlay/overlay.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,12 +1,10 @@ -; $Id: overlay.info,v 1.3 2010/12/20 19:59:42 webchick Exp $ name = Overlay description = Displays the Drupal administration interface in an overlay. package = Core version = VERSION core = 7.x -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/overlay/overlay.install drupal-7.66/modules/overlay/overlay.install --- drupal-7.0/modules/overlay/overlay.install 2010-01-18 18:12:04.000000000 +0100 +++ drupal-7.66/modules/overlay/overlay.install 2019-04-17 22:20:46.000000000 +0200 @@ -1,16 +1,15 @@ <?php -// $Id: overlay.install,v 1.3 2010/01/18 17:12:04 dries Exp $ /** * @file - * Install, update and uninstall functions for the overlay module. + * Install, update, and uninstall functions for the Overlay module. */ /** * Implements hook_enable(). * * If the module is being enabled through the admin UI, and not from an - * install profile, reopen the modules page in an overlay. + * installation profile, reopen the modules page in an overlay. */ function overlay_enable() { if (strpos(current_path(), 'admin/modules') === 0) { diff -Naur drupal-7.0/modules/overlay/overlay.module drupal-7.66/modules/overlay/overlay.module --- drupal-7.0/modules/overlay/overlay.module 2010-11-30 18:16:37.000000000 +0100 +++ drupal-7.66/modules/overlay/overlay.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: overlay.module,v 1.34 2010/11/30 17:16:37 dries Exp $ /** * @file @@ -14,7 +13,7 @@ case 'admin/help#overlay': $output = ''; $output .= '<h3>' . t('About') . '</h3>'; - $output .= '<p>' . t('The Overlay module makes the administration pages on your site display in a JavaScript overlay of the page you were viewing when you clicked the administrative link, instead of replacing the page in your browser window. Use the close link on the overlay to return to the page you were viewing when you clicked the link. For more information, see the online handbook entry for <a href="@overlay">Overlay module</a>.', array('@overlay' => 'http://drupal.org/handbook/modules/overlay')) . '</p>'; + $output .= '<p>' . t('The Overlay module makes the administration pages on your site display in a JavaScript overlay of the page you were viewing when you clicked the administrative link, instead of replacing the page in your browser window. Use the close link on the overlay to return to the page you were viewing when you clicked the link. For more information, see the online handbook entry for <a href="@overlay">Overlay module</a>.', array('@overlay' => 'http://drupal.org/documentation/modules/overlay')) . '</p>'; return $output; } } @@ -80,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) { @@ -138,9 +151,20 @@ } if (isset($_GET['render']) && $_GET['render'] == 'overlay') { - // If this page shouldn't be rendered here, redirect to the parent. - if (!path_is_admin($current_path)) { - overlay_close_dialog($current_path); + // If a previous page requested that we close the overlay, close it and + // redirect to the final destination. + if (isset($_SESSION['overlay_close_dialog'])) { + call_user_func_array('overlay_close_dialog', $_SESSION['overlay_close_dialog']); + unset($_SESSION['overlay_close_dialog']); + } + // 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 = '<front>'; + } + overlay_close_dialog($current_path, array('query' => drupal_get_query_parameters(NULL, array('q', 'render')))); } // Indicate that we are viewing an overlay child page. @@ -196,7 +220,7 @@ // Overlay parent. $libraries['parent'] = array( 'title' => 'Overlay: Parent', - 'website' => 'http://drupal.org/handbook/modules/overlay', + 'website' => 'http://drupal.org/documentation/modules/overlay', 'version' => '1.0', 'js' => array( $module_path . '/overlay-parent.js' => array(), @@ -212,7 +236,7 @@ // Overlay child. $libraries['child'] = array( 'title' => 'Overlay: Child', - 'website' => 'http://drupal.org/handbook/modules/overlay', + 'website' => 'http://drupal.org/documentation/modules/overlay', 'version' => '1.0', 'js' => array( $module_path . '/overlay-child.js' => array(), @@ -227,14 +251,23 @@ /** * Implements hook_drupal_goto_alter(). - * - * If the current page request is inside the overlay, add ?render=overlay to - * the new path, so that it appears correctly inside the overlay. - * - * @see overlay_get_mode() */ function overlay_drupal_goto_alter(&$path, &$options, &$http_response_code) { if (overlay_get_mode() == 'child') { + // The authorize.php script bootstraps Drupal to a very low level, where + // the PHP code that is necessary to close the overlay properly will not be + // loaded. Therefore, if we are redirecting to authorize.php inside the + // overlay, instead redirect back to the current page with instructions to + // close the overlay there before redirecting to the final destination; see + // overlay_init(). + if ($path == system_authorized_get_url() || $path == system_authorized_batch_processing_url()) { + $_SESSION['overlay_close_dialog'] = array($path, $options); + $path = current_path(); + $options = drupal_get_query_parameters(); + } + + // If the current page request is inside the overlay, add ?render=overlay + // to the new path, so that it appears correctly inside the overlay. if (isset($options['query'])) { $options['query'] += array('render' => 'overlay'); } @@ -287,7 +320,10 @@ } /** - * Menu callback; dismisses the overlay accessibility message for this user. + * Page callback: Dismisses the overlay accessibility message for this user. + * + * @return + * A render array for a page containing a list of content. */ function overlay_user_dismiss_message() { global $user; @@ -312,10 +348,12 @@ * If the current user can access the overlay and has not previously indicated * that this message should be dismissed, this function returns a message * containing a link to disable the overlay. Nothing is returned for anonymous - * users, because the links control per-user settings. Therefore, because some - * screen readers are unable to properly read overlay contents, site builders - * are discouraged from granting the "access overlay" permission to the - * anonymous role. See http://drupal.org/node/890284. + * users, because the links control per-user settings. Because some screen + * readers are unable to properly read overlay contents, site builders are + * discouraged from granting the "access overlay" permission to the anonymous + * role. + * + * @see http://drupal.org/node/890284 */ function overlay_disable_message() { global $user; @@ -370,7 +408,13 @@ /** * Returns the HTML for the message about how to disable the overlay. * - * @see overlay_disable_message() + * @param $variables + * An associative array with an 'element' element, which itself is an + * associative array containing: + * - profile_link: The link to this user's account. + * - dismiss_message_link: The link to dismiss the overlay. + * + * @ingroup themeable */ function theme_overlay_disable_message($variables) { $element = $variables['element']; @@ -458,8 +502,12 @@ } /** - * Preprocesses template variables for overlay.tpl.php + * Implements template_preprocess_HOOK() for overlay.tpl.php * + * If the current page request is inside the overlay, add appropriate classes + * to the <body> element, and simplify the page title. + * + * @see template_process_overlay() * @see overlay.tpl.php */ function template_preprocess_overlay(&$variables) { @@ -470,20 +518,21 @@ } /** - * Processes variables for overlay.tpl.php + * Implements template_process_HOOK() for overlay.tpl.php + * + * Places the rendered HTML for the page body into a top level variable. * * @see template_preprocess_overlay() * @see overlay.tpl.php */ function template_process_overlay(&$variables) { - // Place the rendered HTML for the page body into a top level variable. $variables['page'] = $variables['page']['#children']; } /** * Implements hook_preprocess_page(). * - * Hide tabs inside the overlay. + * If the current page request is inside the overlay, hide the tabs. * * @see overlay_get_mode() */ @@ -494,7 +543,7 @@ } /** - * Callback to request that the overlay display an empty page. + * Stores and returns whether an empty page override is needed. * * This is used to prevent a page request which closes the overlay (for * example, a form submission) from being fully re-rendered before the overlay @@ -535,7 +584,7 @@ } /** - * Delivery callback to display an empty page. + * Prints an empty page. * * This function is used to print out a bare minimum empty page which still has * the scripts and styles necessary in order to trigger the overlay to close. @@ -547,7 +596,7 @@ } /** - * Get the current overlay mode. + * Gets the current overlay mode. * * @see overlay_set_mode() */ @@ -626,7 +675,21 @@ $type = str_replace('<front>', variable_get('site_frontpage', 'node'), $type); } drupal_add_js(array('overlay' => array('paths' => $paths)), 'setting'); - // Pass along the AJAX callback for rerendering sections of the parent window. + $path_prefixes = array(); + if (module_exists('locale') && variable_get('locale_language_negotiation_url_part', LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX) == LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX) { + // Get languages grouped by status and select only the enabled ones. + $languages = language_list('enabled'); + $languages = $languages[1]; + + $path_prefixes = array(); + foreach ($languages as $language) { + if ($language->prefix) { + $path_prefixes[] = $language->prefix; + } + } + } + drupal_add_js(array('overlay' => array('pathPrefixes' => $path_prefixes)), 'setting'); + // Pass along the Ajax callback for rerendering sections of the parent window. drupal_add_js(array('overlay' => array('ajaxCallback' => 'overlay-ajax')), 'setting'); } @@ -643,7 +706,7 @@ // the initial rendered content of those regions here, so that we can compare // it to the same content rendered in overlay_exit(), at the end of the page // request. This allows us to check if anything actually did change, and, if - // so, trigger an immediate AJAX refresh of the parent window. + // so, trigger an immediate Ajax refresh of the parent window. if (!empty($_POST) || isset($_GET['token'])) { foreach (overlay_supplemental_regions() as $region) { overlay_store_rendered_content($region, overlay_render_region($region)); @@ -659,12 +722,13 @@ } /** - * Callback to request that the overlay close as soon as the page is displayed. + * Requests that the overlay closes when the page is displayed. * * @param $redirect * (optional) The path that should open in the parent window after the * overlay closes. If not set, no redirect will be performed on the parent * window. + * * @param $redirect_options * (optional) An associative array of options to use when generating the * redirect URL. @@ -728,7 +792,7 @@ } /** - * Helper function for returning a list of page regions related to the overlay. + * Returns a list of page regions related to the overlay. * * @param $type * The type of regions to return. This can either be 'overlay_regions' or @@ -833,11 +897,14 @@ // on the final rendered page. $original_js = drupal_add_js(); $original_css = drupal_add_css(); + $original_libraries = drupal_static('drupal_add_library'); $js = &drupal_static('drupal_add_js'); $css = &drupal_static('drupal_add_css'); + $libraries = &drupal_static('drupal_add_library'); $markup = drupal_render_page($page); $js = $original_js; $css = $original_css; + $libraries = $original_libraries; // Indicate that the main page content has not, in fact, been displayed, so // that future calls to drupal_render_page() will be able to render it // correctly. @@ -891,7 +958,7 @@ } /** - * Request that the parent window refresh a particular page region. + * Requests that the parent window refreshes a particular page region. * * @param $region * The name of the page region to refresh. The parent window will trigger a @@ -906,7 +973,7 @@ } /** - * Request that the entire parent window be reloaded when the overlay closes. + * Requests that the entire parent window is reloaded when the overlay closes. * * @see overlay_trigger_refresh() */ @@ -915,7 +982,7 @@ } /** - * Check if the parent window needs to be refreshed on this page load. + * Checks if the parent window needs to be refreshed on this page load. * * If the previous page load requested that any page regions be refreshed, or * if it requested that the entire page be refreshed when the overlay closes, @@ -945,7 +1012,7 @@ /** * Prints the markup obtained by rendering a single region of the page. * - * This function is intended to be called via AJAX. + * This function is intended to be called via Ajax. * * @param $region * The name of the page region to render. diff -Naur drupal-7.0/modules/overlay/overlay.tpl.php drupal-7.66/modules/overlay/overlay.tpl.php --- drupal-7.0/modules/overlay/overlay.tpl.php 2010-11-06 01:18:24.000000000 +0100 +++ drupal-7.66/modules/overlay/overlay.tpl.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: overlay.tpl.php,v 1.5 2010/11/06 00:18:24 dries Exp $ /** * @file @@ -18,6 +17,8 @@ * @see template_preprocess() * @see template_preprocess_overlay() * @see template_process() + * + * @ingroup themeable */ ?> diff -Naur drupal-7.0/modules/path/path.admin.inc drupal-7.66/modules/path/path.admin.inc --- drupal-7.0/modules/path/path.admin.inc 2010-09-11 05:04:43.000000000 +0200 +++ drupal-7.66/modules/path/path.admin.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: path.admin.inc,v 1.45 2010/09/11 03:04:43 dries Exp $ /** * @file @@ -7,7 +6,7 @@ */ /** - * Return a listing of all defined URL aliases. + * Returns a listing of all defined URL aliases. * * When filter key passed, perform a standard search on the given key, * and return the list of matching URL aliases. @@ -88,7 +87,15 @@ } /** - * Menu callback; handles pages for creating and editing URL aliases. + * Page callback: Returns a form creating or editing a path alias. + * + * @param $path + * An array containing the path ID, source, alias, and language code. + * + * @return + * A form for adding or editing a URL alias. + * + * @see path_menu() */ function path_admin_edit($path = array()) { if ($path) { @@ -103,11 +110,15 @@ } /** - * Return a form for editing or creating an individual URL alias. + * Form constructor for the path administration form. + * + * @param $path + * An array containing the path ID, source, alias, and language code. * * @ingroup forms * @see path_admin_form_validate() * @see path_admin_form_submit() + * @see path_admin_form_delete_submit() */ function path_admin_form($form, &$form_state, $path = array('source' => '', 'alias' => '', 'language' => LANGUAGE_NONE, 'pid' => NULL)) { $form['source'] = array( @@ -158,7 +169,10 @@ } /** - * Submit function for the 'Delete' button on the URL alias editing form. + * Form submission handler for the 'Delete' button on path_admin_form(). + * + * @see path_admin_form_validate() + * @see path_admin_form_submit() */ function path_admin_form_delete_submit($form, &$form_state) { $destination = array(); @@ -170,7 +184,10 @@ } /** - * Verify that a URL alias is valid + * Form validation handler for path_admin_form(). + * + * @see path_admin_form_submit() + * @see path_admin_form_delete_submit() */ function path_admin_form_validate($form, &$form_state) { $source = &$form_state['values']['source']; @@ -196,7 +213,10 @@ } /** - * Save a URL alias to the database. + * Form submission handler for path_admin_form(). + * + * @see path_admin_form_validate() + * @see path_admin_form_delete_submit() */ function path_admin_form_submit($form, &$form_state) { // Remove unnecessary values. @@ -209,7 +229,12 @@ } /** - * Menu callback; confirms deleting an URL alias + * Form constructor for the path deletion form. + * + * @param $path + * The path alias that will be deleted. + * + * @see path_admin_delete_confirm_submit() */ function path_admin_delete_confirm($form, &$form_state, $path) { if (user_access('administer url aliases')) { @@ -225,7 +250,7 @@ } /** - * Execute URL alias deletion + * Form submission handler for path_admin_delete_confirm(). */ function path_admin_delete_confirm_submit($form, &$form_state) { if ($form_state['values']['confirm']) { @@ -235,10 +260,11 @@ } /** - * Return a form to filter URL aliases. + * Form constructor for the path admin overview filter form. * * @ingroup forms - * @see path_admin_filter_form_submit() + * @see path_admin_filter_form_submit_filter() + * @see path_admin_filter_form_submit_reset() */ function path_admin_filter_form($form, &$form_state, $keys = '') { $form['#attributes'] = array('class' => array('search-form')); @@ -270,14 +296,18 @@ } /** - * Process filter form submission when the Filter button is pressed. + * Form submission handler for the path_admin_filter_form() Filter button. + * + * @see path_admin_filter_form_submit_reset() */ function path_admin_filter_form_submit_filter($form, &$form_state) { $form_state['redirect'] = 'admin/config/search/path/list/' . trim($form_state['values']['filter']); } /** - * Process filter form submission when the Reset button is pressed. + * Form submission handler for the path_admin_filter_form() Reset button. + * + * @see path_admin_filter_form_submit_filter() */ function path_admin_filter_form_submit_reset($form, &$form_state) { $form_state['redirect'] = 'admin/config/search/path/list'; diff -Naur drupal-7.0/modules/path/path.api.php drupal-7.66/modules/path/path.api.php --- drupal-7.0/modules/path/path.api.php 2010-12-02 01:22:20.000000000 +0100 +++ drupal-7.66/modules/path/path.api.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: path.api.php,v 1.2 2010/12/02 00:22:20 webchick Exp $ /** * @file @@ -11,9 +10,8 @@ * @{ */ - /** - * Allow modules to respond to a path being inserted. + * Respond to a path being inserted. * * @param $path * An associative array containing the following keys: @@ -34,7 +32,7 @@ } /** - * Allow modules to respond to a path being updated. + * Respond to a path being updated. * * @param $path * An associative array containing the following keys: @@ -53,7 +51,7 @@ } /** - * Allow modules to respond to a path being deleted. + * Respond to a path being deleted. * * @param $path * An associative array containing the following keys: diff -Naur drupal-7.0/modules/path/path.info drupal-7.66/modules/path/path.info --- drupal-7.0/modules/path/path.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/path/path.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: path.info,v 1.10 2010/12/20 19:59:42 webchick Exp $ name = Path description = Allows users to rename URLs. package = Core @@ -7,8 +6,7 @@ files[] = path.test configure = admin/config/search/path -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/path/path.js drupal-7.66/modules/path/path.js --- drupal-7.0/modules/path/path.js 2010-11-05 20:47:20.000000000 +0100 +++ drupal-7.66/modules/path/path.js 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,8 @@ -// $Id: path.js,v 1.5 2010/11/05 19:47:20 dries Exp $ + +/** + * @file + * Attaches behaviors for the Path module. + */ (function ($) { diff -Naur drupal-7.0/modules/path/path.module drupal-7.66/modules/path/path.module --- drupal-7.0/modules/path/path.module 2010-12-01 01:31:38.000000000 +0100 +++ drupal-7.66/modules/path/path.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: path.module,v 1.187 2010/12/01 00:31:38 webchick Exp $ /** * @file @@ -14,7 +13,7 @@ case 'admin/help#path': $output = ''; $output .= '<h3>' . t('About') . '</h3>'; - $output .= '<p>' . t('The Path module allows you to specify an alias, or custom URL, for any existing internal system path. Aliases should not be confused with URL redirects, which allow you to forward a changed or inactive URL to a new URL. In addition to making URLs more readable, aliases also help search engines index content more effectively. Multiple aliases may be used for a single internal system path. To automate the aliasing of paths, you can install the contributed module <a href="@pathauto">Pathauto</a>. For more information, see the online handbook entry for the <a href="@path">Path module</a>.', array('@path' => 'http://drupal.org/handbook/modules/path', '@pathauto' => 'http://drupal.org/project/pathauto')) . '</p>'; + $output .= '<p>' . t('The Path module allows you to specify an alias, or custom URL, for any existing internal system path. Aliases should not be confused with URL redirects, which allow you to forward a changed or inactive URL to a new URL. In addition to making URLs more readable, aliases also help search engines index content more effectively. Multiple aliases may be used for a single internal system path. To automate the aliasing of paths, you can install the contributed module <a href="@pathauto">Pathauto</a>. For more information, see the online handbook entry for the <a href="@path">Path module</a>.', array('@path' => 'http://drupal.org/documentation/modules/path', '@pathauto' => 'http://drupal.org/project/pathauto')) . '</p>'; $output .= '<h3>' . t('Uses') . '</h3>'; $output .= '<dl>'; $output .= '<dt>' . t('Creating aliases') . '</dt>'; @@ -58,7 +57,7 @@ 'description' => "Change your site's URL paths by aliasing them.", 'page callback' => 'path_admin_overview', 'access arguments' => array('administer url aliases'), - 'weight' => -10, + 'weight' => -5, 'file' => 'path.admin.inc', ); $items['admin/config/search/path/edit/%path'] = array( @@ -92,14 +91,17 @@ } /** - * Implements hook_form_BASE_FORM_ID_alter(). + * Implements hook_form_BASE_FORM_ID_alter() for node_form(). + * + * @see path_form_element_validate() */ function path_form_node_form_alter(&$form, $form_state) { $path = array(); if (!empty($form['#node']->nid)) { $conditions = array('source' => 'node/' . $form['#node']->nid); - if ($form['#node']->language != LANGUAGE_NONE) { - $conditions['language'] = $form['#node']->language; + $langcode = entity_language('node', $form['#node']); + if ($langcode != LANGUAGE_NONE) { + $conditions['language'] = $langcode; } $path = path_load($conditions); if ($path === FALSE) { @@ -110,7 +112,7 @@ 'pid' => NULL, 'source' => isset($form['#node']->nid) ? 'node/' . $form['#node']->nid : NULL, 'alias' => '', - 'language' => isset($form['#node']->language) ? $form['#node']->language : LANGUAGE_NONE, + 'language' => isset($langcode) ? $langcode : LANGUAGE_NONE, ); $form['path'] = array( @@ -135,8 +137,6 @@ '#title' => t('URL alias'), '#default_value' => $path['alias'], '#maxlength' => 255, - '#collapsible' => TRUE, - '#collapsed' => TRUE, '#description' => t('Optionally specify an alternative URL by which this content can be accessed. For example, type "about" when writing an about page. Use a relative path and don\'t add a trailing slash or the URL alias won\'t work.'), ); $form['path']['pid'] = array('#type' => 'value', '#value' => $path['pid']); @@ -146,11 +146,13 @@ /** * Form element validation handler for URL alias form element. + * + * @see path_form_node_form_alter() */ function path_form_element_validate($element, &$form_state, $complete_form) { - if (!empty($form_state['values']['path']['alias'])) { - // Trim the submitted value. - $alias = trim($form_state['values']['path']['alias']); + // Trim the submitted value. + $alias = trim($form_state['values']['path']['alias']); + if (!empty($alias)) { form_set_value($element['alias'], $alias, $form_state); // Node language (Locale module) needs special care. Since the language of // the URL alias depends on the node language, and the node language can be @@ -174,7 +176,7 @@ $query->addExpression('1'); $query->range(0, 1); if ($query->execute()->fetchField()) { - form_set_error('alias', t('The alias is already in use.')); + form_error($element, t('The alias is already in use.')); } } } @@ -183,14 +185,15 @@ * 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. if (!empty($path['alias'])) { // Ensure fields for programmatic executions. + $langcode = entity_language('node', $node); $path['source'] = 'node/' . $node->nid; - $path['language'] = isset($node->language) ? $node->language : LANGUAGE_NONE; + $path['language'] = isset($langcode) ? $langcode : LANGUAGE_NONE; path_save($path); } } @@ -202,18 +205,12 @@ 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']); } - // Only save a non-empty alias. - if (!empty($path['alias'])) { - // Ensure fields for programmatic executions. - $path['source'] = 'node/' . $node->nid; - $path['language'] = isset($node->language) ? $node->language : LANGUAGE_NONE; - path_save($path); - } + path_node_insert($node); } } @@ -226,12 +223,15 @@ } /** - * Implements hook_form_FORM_ID_alter(). + * Implements hook_form_FORM_ID_alter() for taxonomy_form_term(). */ function path_form_taxonomy_form_term_alter(&$form, $form_state) { // Make sure this does not show up on the delete confirmation form. if (empty($form_state['confirm_delete'])) { - $path = (isset($form['#term']['tid']) ? path_load('taxonomy/term/' . $form['#term']['tid']) : array()); + $langcode = entity_language('taxonomy_term', (object) $form['#term']); + $langcode = !empty($langcode) ? $langcode : LANGUAGE_NONE; + $conditions = array('source' => 'taxonomy/term/' . $form['#term']['tid'], 'language' => $langcode); + $path = (isset($form['#term']['tid']) ? path_load($conditions) : array()); if ($path === FALSE) { $path = array(); } @@ -239,7 +239,7 @@ 'pid' => NULL, 'source' => isset($form['#term']['tid']) ? 'taxonomy/term/' . $form['#term']['tid'] : NULL, 'alias' => '', - 'language' => LANGUAGE_NONE, + 'language' => $langcode, ); $form['path'] = array( '#access' => user_access('create url aliases') || user_access('administer url aliases'), @@ -271,7 +271,10 @@ if (!empty($path['alias'])) { // Ensure fields for programmatic executions. $path['source'] = 'taxonomy/term/' . $term->tid; - $path['language'] = LANGUAGE_NONE; + // Core does not provide a way to store the term language but contrib + // modules can do it so we need to take this into account. + $langcode = entity_language('taxonomy_term', $term); + $path['language'] = !empty($langcode) ? $langcode : LANGUAGE_NONE; path_save($path); } } @@ -292,7 +295,10 @@ if (!empty($path['alias'])) { // Ensure fields for programmatic executions. $path['source'] = 'taxonomy/term/' . $term->tid; - $path['language'] = LANGUAGE_NONE; + // Core does not provide a way to store the term language but contrib + // modules can do it so we need to take this into account. + $langcode = entity_language('taxonomy_term', $term); + $path['language'] = !empty($langcode) ? $langcode : LANGUAGE_NONE; path_save($path); } } diff -Naur drupal-7.0/modules/path/path.test drupal-7.66/modules/path/path.test --- drupal-7.0/modules/path/path.test 2010-10-09 19:38:41.000000000 +0200 +++ drupal-7.66/modules/path/path.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,11 +1,13 @@ <?php -// $Id: path.test,v 1.41 2010/10/09 17:38:41 webchick Exp $ /** * @file - * Tests for the path module + * Tests for the Path module. */ +/** + * Provides a base class for testing the Path module. + */ class PathTestCase extends DrupalWebTestCase { public static function getInfo() { return array( @@ -19,12 +21,12 @@ 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); } /** - * Test the path cache. + * Tests the path cache. */ function testPathCache() { // Create test node. @@ -40,16 +42,16 @@ // created. cache_clear_all('*', 'cache_path', TRUE); $this->drupalGet($edit['source']); - $this->assertTrue(cache_get($edit['source'], 'cache_path'), t('Cache entry was created.')); + $this->assertTrue(cache_get($edit['source'], 'cache_path'), 'Cache entry was created.'); // Visit the alias for the node and confirm a cache entry is created. cache_clear_all('*', 'cache_path', TRUE); $this->drupalGet($edit['alias']); - $this->assertTrue(cache_get($edit['source'], 'cache_path'), t('Cache entry was created.')); + $this->assertTrue(cache_get($edit['source'], 'cache_path'), 'Cache entry was created.'); } /** - * Test alias functionality through the admin interfaces. + * Tests alias functionality through the admin interfaces. */ function testAdminAlias() { // Create test node. @@ -108,7 +110,7 @@ } /** - * Test alias functionality through the node interfaces. + * Tests alias functionality through the node interfaces. */ function testNodeAlias() { // Create test node. @@ -158,15 +160,69 @@ $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); } + /** + * Returns the path ID. + * + * @param $alias + * A string containing an aliased path. + * + * @return int + * Integer representing the path ID. + */ function getPID($alias) { return db_query("SELECT pid FROM {url_alias} WHERE alias = :alias", array(':alias' => $alias))->fetchField(); } + + /** + * Tests that duplicate aliases fail validation. + */ + function testDuplicateNodeAlias() { + // Create one node with a random alias. + $node_one = $this->drupalCreateNode(); + $edit = array(); + $edit['path[alias]'] = $this->randomName(); + $this->drupalPost('node/' . $node_one->nid . '/edit', $edit, t('Save')); + + // Now create another node and try to set the same alias. + $node_two = $this->drupalCreateNode(); + $this->drupalPost('node/' . $node_two->nid . '/edit', $edit, t('Save')); + $this->assertText(t('The alias is already in use.')); + $this->assertFieldByXPath("//input[@name='path[alias]' and contains(@class, 'error')]", $edit['path[alias]'], 'Textfield exists and has the error class.'); + } } /** - * Test URL aliases for taxonomy terms. + * Tests URL aliases for taxonomy terms. */ class PathTaxonomyTermTestCase extends DrupalWebTestCase { public static function getInfo() { @@ -186,7 +242,7 @@ } /** - * Test alias functionality through the admin interfaces. + * Tests alias functionality through the admin interfaces. */ function testTermAlias() { // Create a term in the default 'Tags' vocabulary with URL alias. @@ -229,6 +285,9 @@ } } +/** + * Tests URL aliases for translated nodes. + */ class PathLanguageTestCase extends DrupalWebTestCase { public static function getInfo() { return array( @@ -280,8 +339,9 @@ $this->drupalGet('node/' . $english_node->nid . '/translate'); $this->clickLink(t('add translation')); $edit = array(); + $langcode = LANGUAGE_NONE; $edit["title"] = $this->randomName(); - $edit["body[fr][0][value]"] = $this->randomName(); + $edit["body[$langcode][0][value]"] = $this->randomName(); $french_alias = $this->randomName(); $edit['path[alias]'] = $french_alias; $this->drupalPost(NULL, $edit, t('Save')); @@ -302,7 +362,7 @@ drupal_static_reset('locale_url_outbound_alter'); $languages = language_list(); $url = url('node/' . $french_node->nid, array('language' => $languages[$french_node->language])); - $this->assertTrue(strpos($url, $edit['path[alias]']), t('URL contains the path alias.')); + $this->assertTrue(strpos($url, $edit['path[alias]']), 'URL contains the path alias.'); // Confirm that the alias works even when changing language negotiation // options. Enable User language detection and selection over URL one. @@ -346,23 +406,23 @@ // situation only aliases in the default language and language neutral ones // should keep working. $this->drupalGet($french_alias); - $this->assertResponse(404, t('Alias for French translation is unavailable when URL language negotiation is disabled.')); + $this->assertResponse(404, 'Alias for French translation is unavailable when URL language negotiation is disabled.'); // drupal_lookup_path() has an internal static cache. Check to see that // it has the appropriate contents at this point. drupal_lookup_path('wipe'); $french_node_path = drupal_lookup_path('source', $french_alias, $french_node->language); - $this->assertEqual($french_node_path, 'node/' . $french_node->nid, t('Normal path works.')); + $this->assertEqual($french_node_path, 'node/' . $french_node->nid, 'Normal path works.'); // Second call should return the same path. $french_node_path = drupal_lookup_path('source', $french_alias, $french_node->language); - $this->assertEqual($french_node_path, 'node/' . $french_node->nid, t('Normal path is the same.')); + $this->assertEqual($french_node_path, 'node/' . $french_node->nid, 'Normal path is the same.'); // Confirm that the alias works. $french_node_alias = drupal_lookup_path('alias', 'node/' . $french_node->nid, $french_node->language); - $this->assertEqual($french_node_alias, $french_alias, t('Alias works.')); + $this->assertEqual($french_node_alias, $french_alias, 'Alias works.'); // Second call should return the same alias. $french_node_alias = drupal_lookup_path('alias', 'node/' . $french_node->nid, $french_node->language); - $this->assertEqual($french_node_alias, $french_alias, t('Alias is the same.')); + $this->assertEqual($french_node_alias, $french_alias, 'Alias is the same.'); } } @@ -476,8 +536,8 @@ $this->drupalPost('admin/config/regional/language', $edit, t('Save configuration')); // Verify that French is the only language. - $this->assertFalse(drupal_multilingual(), t('Site is mono-lingual')); - $this->assertEqual(language_default('language'), 'fr', t('French is the default language')); + $this->assertFalse(drupal_multilingual(), 'Site is mono-lingual'); + $this->assertEqual(language_default('language'), 'fr', 'French is the default language'); // Set language detection to URL. $edit = array('language[enabled][locale-url]' => TRUE); diff -Naur drupal-7.0/modules/php/php.info drupal-7.66/modules/php/php.info --- drupal-7.0/modules/php/php.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/php/php.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: php.info,v 1.8 2010/12/20 19:59:42 webchick Exp $ name = PHP filter description = Allows embedded PHP code/snippets to be evaluated. package = Core @@ -6,8 +5,7 @@ core = 7.x files[] = php.test -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/php/php.install drupal-7.66/modules/php/php.install --- drupal-7.0/modules/php/php.install 2010-10-20 03:15:58.000000000 +0200 +++ drupal-7.66/modules/php/php.install 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: php.install,v 1.19 2010/10/20 01:15:58 dries Exp $ /** * @file diff -Naur drupal-7.0/modules/php/php.module drupal-7.66/modules/php/php.module --- drupal-7.0/modules/php/php.module 2010-12-01 01:31:38.000000000 +0100 +++ drupal-7.66/modules/php/php.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: php.module,v 1.29 2010/12/01 00:31:38 webchick Exp $ /** * @file @@ -14,11 +13,11 @@ case 'admin/help#php': $output = ''; $output .= '<h3>' . t('About') . '</h3>'; - $output .= '<p>' . t('The PHP filter module adds a PHP filter to your site, for use with <a href="@filter">text formats</a>. This filter adds the ability to execute PHP code in any text field that uses a text format (such as the body of a content item or the text of a comment). <a href="@php-net">PHP</a> is a general-purpose scripting language widely-used for web development, and is the language with which Drupal has been developed. For more information, see the online handbook entry for the <a href="@php">PHP filter module</a>.', array('@filter' => url('admin/help/filter'), '@php-net' => 'http://www.php.net', '@php' => 'http://drupal.org/handbook/modules/php/')) . '</p>'; + $output .= '<p>' . t('The PHP filter module adds a PHP filter to your site, for use with <a href="@filter">text formats</a>. This filter adds the ability to execute PHP code in any text field that uses a text format (such as the body of a content item or the text of a comment). <a href="@php-net">PHP</a> is a general-purpose scripting language widely-used for web development, and is the language with which Drupal has been developed. For more information, see the online handbook entry for the <a href="@php">PHP filter module</a>.', array('@filter' => url('admin/help/filter'), '@php-net' => 'http://www.php.net', '@php' => 'http://drupal.org/documentation/modules/php/')) . '</p>'; $output .= '<h3>' . t('Uses') . '</h3>'; $output .= '<dl>'; $output .= '<dt>' . t('Enabling execution of PHP in text fields') . '</dt>'; - $output .= '<dd>' . 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. <a href="@php-snippets">Example PHP snippets</a> can be found on Drupal.org.', array('@php-snippets' => url('http://drupal.org/handbook/customization/php-snippets'))) . '</dd>'; + $output .= '<dd>' . 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. <a href="@php-snippets">Example PHP snippets</a> can be found on Drupal.org.', array('@php-snippets' => url('http://drupal.org/documentation/customization/php-snippets'))) . '</dd>'; $output .= '</dl>'; return $output; } @@ -37,23 +36,29 @@ } /** - * Evaluate a string of PHP code. + * Evaluates a string of PHP code. * - * This is a wrapper around PHP's eval(). It uses output buffering to capture both - * returned and printed text. Unlike eval(), we require code to be surrounded by - * <?php ?> tags; in other words, we evaluate the code as if it were a stand-alone - * PHP file. + * This is a wrapper around PHP's eval(). It uses output buffering to capture + * both returned and printed text. Unlike eval(), we require code to be + * surrounded by <?php ?> tags; in other words, we evaluate the code as if it + * were a stand-alone PHP file. * * Using this wrapper also ensures that the PHP code which is evaluated can not * overwrite any variables in the calling code, unlike a regular eval() call. * + * This function is also used as an implementation of + * callback_filter_process(). + * * @param $code * The code to evaluate. + * * @return - * A string containing the printed output of the code, followed by the returned - * output of the code. + * A string containing the printed output of the code, followed by the + * returned output of the code. * * @ingroup php_wrappers + * + * @see php_filter_info() */ function php_eval($code) { global $theme_path, $theme_info, $conf; @@ -83,7 +88,9 @@ } /** - * Tips callback for php filter. + * Implements callback_filter_tips(). + * + * @see php_filter_info() */ function _php_filter_tips($filter, $format, $long = FALSE) { global $base_url; @@ -115,7 +122,7 @@ print t(\'Welcome visitor! Thank you for visiting.\'); } </pre>') . '</li></ul>'; - $output .= '<p>' . t('<a href="@drupal">Drupal.org</a> offers <a href="@php-snippets">some example PHP snippets</a>, or you can create your own with some PHP experience and knowledge of the Drupal system.', array('@drupal' => url('http://drupal.org'), '@php-snippets' => url('http://drupal.org/handbook/customization/php-snippets'))) . '</p>'; + $output .= '<p>' . t('<a href="@drupal">Drupal.org</a> offers <a href="@php-snippets">some example PHP snippets</a>, or you can create your own with some PHP experience and knowledge of the Drupal system.', array('@drupal' => url('http://drupal.org'), '@php-snippets' => url('http://drupal.org/documentation/customization/php-snippets'))) . '</p>'; return $output; } else { @@ -138,4 +145,3 @@ ); return $filters; } - diff -Naur drupal-7.0/modules/php/php.test drupal-7.66/modules/php/php.test --- drupal-7.0/modules/php/php.test 2010-11-29 07:38:51.000000000 +0100 +++ drupal-7.66/modules/php/php.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,8 +1,12 @@ <?php -// $Id: php.test,v 1.27 2010/11/29 06:38:51 webchick Exp $ /** - * Base PHP test case class. + * @file + * Tests for php.module. + */ + +/** + * Defines a base PHP test case class. */ class PHPTestCase extends DrupalWebTestCase { protected $php_code_format; @@ -17,24 +21,24 @@ // Verify that the PHP code text format was inserted. $php_format_id = 'php_code'; $this->php_code_format = filter_format_load($php_format_id); - $this->assertEqual($this->php_code_format->name, 'PHP code', t('PHP code text format was created.')); + $this->assertEqual($this->php_code_format->name, 'PHP code', 'PHP code text format was created.'); // Verify that the format has the PHP code filter enabled. $filters = filter_list_format($php_format_id); - $this->assertTrue($filters['php_code']->status, t('PHP code filter is enabled.')); + $this->assertTrue($filters['php_code']->status, 'PHP code filter is enabled.'); // Verify that the format exists on the administration page. $this->drupalGet('admin/config/content/formats'); - $this->assertText('PHP code', t('PHP code text format was created.')); + $this->assertText('PHP code', 'PHP code text format was created.'); // Verify that anonymous and authenticated user roles do not have access. $this->drupalGet('admin/config/content/formats/' . $php_format_id); - $this->assertFieldByName('roles[1]', FALSE, t('Anonymous users do not have access to PHP code format.')); - $this->assertFieldByName('roles[2]', FALSE, t('Authenticated users do not have access to PHP code format.')); + $this->assertFieldByName('roles[' . DRUPAL_ANONYMOUS_RID . ']', FALSE, 'Anonymous users do not have access to PHP code format.'); + $this->assertFieldByName('roles[' . DRUPAL_AUTHENTICATED_RID . ']', FALSE, 'Authenticated users do not have access to PHP code format.'); } /** - * Create a test node with PHP code in the body. + * Creates a test node with PHP code in the body. * * @return stdObject Node object. */ @@ -56,7 +60,7 @@ } /** - * Make sure that the PHP filter evaluates PHP code when used. + * Makes sure that the PHP filter evaluates PHP code when used. */ function testPHPFilter() { // Log in as a user with permission to use the PHP code text format. @@ -69,18 +73,18 @@ // Make sure that the PHP code shows up as text. $this->drupalGet('node/' . $node->nid); - $this->assertText('print "SimpleTest PHP was executed!"', t('PHP code is displayed.')); + $this->assertText('print "SimpleTest PHP was executed!"', 'PHP code is displayed.'); // Change filter to PHP filter and see that PHP code is evaluated. $edit = array(); $langcode = LANGUAGE_NONE; $edit["body[$langcode][0][format]"] = $this->php_code_format->format; $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save')); - $this->assertRaw(t('Basic page %title has been updated.', array('%title' => $node->title)), t('PHP code filter turned on.')); + $this->assertRaw(t('Basic page %title has been updated.', array('%title' => $node->title)), 'PHP code filter turned on.'); // Make sure that the PHP code shows up as text. - $this->assertNoText('print "SimpleTest PHP was executed!"', t("PHP code isn't displayed.")); - $this->assertText('SimpleTest PHP was executed!', t('PHP code has been evaluated.')); + $this->assertNoText('print "SimpleTest PHP was executed!"', "PHP code isn't displayed."); + $this->assertText('SimpleTest PHP was executed!', 'PHP code has been evaluated.'); } } @@ -97,7 +101,7 @@ } /** - * Make sure that user can't use the PHP filter when not given access. + * Makes sure that the user can't use the PHP filter when not given access. */ function testNoPrivileges() { // Create node with PHP filter enabled. @@ -107,10 +111,10 @@ // Make sure that the PHP code shows up as text. $this->drupalGet('node/' . $node->nid); - $this->assertText('print', t('PHP code was not evaluated.')); + $this->assertText('print', 'PHP code was not evaluated.'); // Make sure that user doesn't have access to filter. $this->drupalGet('node/' . $node->nid . '/edit'); - $this->assertNoRaw('<option value="' . $this->php_code_format->format . '">', t('PHP code format not available.')); + $this->assertNoRaw('<option value="' . $this->php_code_format->format . '">', 'PHP code format not available.'); } } diff -Naur drupal-7.0/modules/poll/poll-bar--block.tpl.php drupal-7.66/modules/poll/poll-bar--block.tpl.php --- drupal-7.0/modules/poll/poll-bar--block.tpl.php 2010-01-13 07:07:27.000000000 +0100 +++ drupal-7.66/modules/poll/poll-bar--block.tpl.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: poll-bar--block.tpl.php,v 1.1 2010/01/13 06:07:27 webchick Exp $ /** * @file diff -Naur drupal-7.0/modules/poll/poll-bar.tpl.php drupal-7.66/modules/poll/poll-bar.tpl.php --- drupal-7.0/modules/poll/poll-bar.tpl.php 2008-10-13 14:31:42.000000000 +0200 +++ drupal-7.66/modules/poll/poll-bar.tpl.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: poll-bar.tpl.php,v 1.3 2008/10/13 12:31:42 dries Exp $ /** * @file diff -Naur drupal-7.0/modules/poll/poll-results--block.tpl.php drupal-7.66/modules/poll/poll-results--block.tpl.php --- drupal-7.0/modules/poll/poll-results--block.tpl.php 2010-01-13 07:07:27.000000000 +0100 +++ drupal-7.66/modules/poll/poll-results--block.tpl.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: poll-results--block.tpl.php,v 1.1 2010/01/13 06:07:27 webchick Exp $ /** * @file * Default theme implementation to display the poll results in a block. diff -Naur drupal-7.0/modules/poll/poll-results.tpl.php drupal-7.66/modules/poll/poll-results.tpl.php --- drupal-7.0/modules/poll/poll-results.tpl.php 2008-10-13 14:31:42.000000000 +0200 +++ drupal-7.66/modules/poll/poll-results.tpl.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: poll-results.tpl.php,v 1.3 2008/10/13 12:31:42 dries Exp $ /** * @file @@ -16,6 +15,8 @@ * - $vote: The choice number of the current user's vote. * * @see template_preprocess_poll_results() + * + * @ingroup themeable */ ?> <div class="poll"> diff -Naur drupal-7.0/modules/poll/poll-rtl.css drupal-7.66/modules/poll/poll-rtl.css --- drupal-7.0/modules/poll/poll-rtl.css 2007-05-30 20:28:13.000000000 +0200 +++ drupal-7.66/modules/poll/poll-rtl.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -/* $Id: poll-rtl.css,v 1.2 2007/05/30 18:28:13 goba Exp $ */ .poll .bar .foreground { float: right; diff -Naur drupal-7.0/modules/poll/poll-vote.tpl.php drupal-7.66/modules/poll/poll-vote.tpl.php --- drupal-7.0/modules/poll/poll-vote.tpl.php 2009-06-13 21:38:42.000000000 +0200 +++ drupal-7.66/modules/poll/poll-vote.tpl.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: poll-vote.tpl.php,v 1.4 2009/06/13 19:38:42 dries Exp $ /** * @file @@ -13,6 +12,8 @@ * form_alter hooks. * * @see template_preprocess_poll_vote() + * + * @ingroup themeable */ ?> <div class="poll"> diff -Naur drupal-7.0/modules/poll/poll.css drupal-7.66/modules/poll/poll.css --- drupal-7.0/modules/poll/poll.css 2010-04-06 17:25:51.000000000 +0200 +++ drupal-7.66/modules/poll/poll.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -/* $Id: poll.css,v 1.8 2010/04/06 15:25:51 dries Exp $ */ .poll { overflow: hidden; diff -Naur drupal-7.0/modules/poll/poll.info drupal-7.66/modules/poll/poll.info --- drupal-7.0/modules/poll/poll.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/poll/poll.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: poll.info,v 1.12 2010/12/20 19:59:42 webchick Exp $ name = Poll description = Allows your site to capture votes on different topics in the form of multiple choice questions. package = Core @@ -7,8 +6,7 @@ files[] = poll.test stylesheets[all][] = poll.css -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/poll/poll.install drupal-7.66/modules/poll/poll.install --- drupal-7.0/modules/poll/poll.install 2010-08-22 15:55:53.000000000 +0200 +++ drupal-7.66/modules/poll/poll.install 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: poll.install,v 1.33 2010/08/22 13:55:53 dries Exp $ /** * @file @@ -197,3 +196,20 @@ 'description' => 'The sort order of this choice among all choices for the same node.', )); } + +/** + * @addtogroup updates-7.x-extra + * @{ + */ + +/** + * Update the database to match the schema. + */ +function poll_update_7004() { + // Remove field default. + db_change_field('poll_vote', 'chid', 'chid', array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE)); +} + +/** + * @} End of "addtogroup updates-7.x-extra". + */ diff -Naur drupal-7.0/modules/poll/poll.module drupal-7.66/modules/poll/poll.module --- drupal-7.0/modules/poll/poll.module 2010-11-20 10:25:30.000000000 +0100 +++ drupal-7.66/modules/poll/poll.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: poll.module,v 1.361 2010/11/20 09:25:30 webchick Exp $ /** * @file @@ -15,7 +14,7 @@ case 'admin/help#poll': $output = ''; $output .= '<h3>' . t('About') . '</h3>'; - $output .= '<p>' . t('The Poll module can be used to create simple surveys or questionnaires that display cumulative results. A poll is a good way to receive feedback from site users and community members. For more information, see the online handbook entry for the <a href="@poll">Poll module</a>.', array('@poll' => 'http://drupal.org/handbook/modules/poll/')) . '</p>'; + $output .= '<p>' . t('The Poll module can be used to create simple surveys or questionnaires that display cumulative results. A poll is a good way to receive feedback from site users and community members. For more information, see the online handbook entry for the <a href="@poll">Poll module</a>.', array('@poll' => 'http://drupal.org/documentation/modules/poll/')) . '</p>'; $output .= '<h3>' . t('Uses') . '</h3>'; $output .= '<dl>'; $output .= '<dt>' . t('Creating a poll') . '</dt>'; @@ -77,7 +76,7 @@ 'title' => t('Cancel and change own votes'), ), 'inspect all votes' => array( - 'title' => t('View voting results'), + 'title' => t('View details for all votes'), ), ); @@ -192,7 +191,6 @@ 'base' => 'poll', 'description' => t('A <em>poll</em> is a question with a set of possible responses. A <em>poll</em>, once created, automatically provides a simple running count of the number of votes received for each response.'), 'title_label' => t('Question'), - 'has_body' => FALSE, ) ); } @@ -242,7 +240,7 @@ $type = node_type_get_type($node); // The submit handlers to add more poll choices require that this form is - // cached, regardless of whether AJAX is used. + // cached, regardless of whether Ajax is used. $form_state['cache'] = TRUE; $form['title'] = array( @@ -250,6 +248,7 @@ '#title' => check_plain($type->title_label), '#required' => TRUE, '#default_value' => $node->title, + '#maxlength' => 255, '#weight' => -5, ); @@ -289,18 +288,21 @@ // Add initial or additional choices. $existing_delta = $delta; - $weight++; for ($delta; $delta < $choice_count; $delta++) { $key = 'new:' . ($delta - $existing_delta); + // Increase the weight of each new choice. + $weight++; $form['choice_wrapper']['choice'][$key] = _poll_choice_form($key, NULL, '', 0, $weight, $choice_count); } // We name our button 'poll_more' to avoid conflicts with other modules using - // AJAX-enabled buttons with the id 'more'. + // Ajax-enabled buttons with the id 'more'. $form['choice_wrapper']['poll_more'] = array( '#type' => 'submit', '#value' => t('More choices'), - '#description' => t("If the amount of boxes above isn't enough, click here to add more choices."), + '#attributes' => array( + 'title' => t("If the amount of boxes above isn't enough, click here to add more choices."), + ), '#weight' => 1, '#limit_validation_errors' => array(array('choice')), '#submit' => array('poll_more_choices_submit'), @@ -362,7 +364,7 @@ * return just the changed part of the form. */ function poll_more_choices_submit($form, &$form_state) { - // If this is a AJAX POST, add 1, otherwise add 5 more choices to the form. + // If this is a Ajax POST, add 1, otherwise add 5 more choices to the form. if ($form_state['values']['poll_more']) { $n = $_GET['q'] == 'system/ajax' ? 1 : 5; $form_state['choice_count'] = count($form_state['values']['choice']) + $n; @@ -407,6 +409,7 @@ '#maxlength' => 7, '#parents' => array('choice', $key, 'chvotes'), '#access' => user_access('administer nodes'), + '#element_validate' => array('element_validate_integer'), ); $form['weight'] = array( @@ -450,10 +453,8 @@ */ function poll_validate($node, $form) { if (isset($node->title)) { - // Check for at least two options and validate amount of votes: + // Check for at least two options and validate amount of votes. $realchoices = 0; - // Renumber fields - $node->choice = array_values($node->choice); foreach ($node->choice as $i => $choice) { if ($choice['chtext'] != '') { $realchoices++; @@ -475,6 +476,9 @@ function poll_field_attach_prepare_translation_alter(&$entity, $context) { if ($context['entity_type'] == 'node' && $entity->type == 'poll') { $entity->choice = $context['source_entity']->choice; + foreach ($entity->choice as $i => $options) { + $entity->choice[$i]['chvotes'] = 0; + } } } @@ -486,6 +490,10 @@ foreach ($nodes as $node) { $poll = db_query("SELECT runtime, active FROM {poll} WHERE nid = :nid", array(':nid' => $node->nid))->fetchObject(); + if (empty($poll)) { + $poll = new stdClass(); + } + // Load the appropriate choices into the $poll object. $poll->choice = db_select('poll_choice', 'c') ->addTag('translatable') @@ -580,7 +588,12 @@ 'chvotes' => (int) $choice['chvotes'], 'weight' => $choice['weight'], )) - ->insertFields(array('nid' => $node->nid)) + ->insertFields(array( + 'nid' => $node->nid, + 'chtext' => $choice['chtext'], + 'chvotes' => (int) $choice['chvotes'], + 'weight' => $choice['weight'], + )) ->execute(); } else { @@ -588,6 +601,10 @@ ->condition('nid', $node->nid) ->condition('chid', $key) ->execute(); + db_delete('poll_choice') + ->condition('nid', $node->nid) + ->condition('chid', $choice['chid']) + ->execute(); } } } @@ -614,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 @@ -657,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); } @@ -677,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"; } @@ -703,7 +714,6 @@ '#type' => 'radios', '#title' => t('Choices'), '#title_display' => 'invisible', - '#default_value' => -1, '#options' => $list, ); } @@ -731,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.')); } } @@ -800,7 +810,7 @@ // Make sure that choices are ordered by their weight. uasort($node->choice, 'drupal_sort_weight'); - // Count the votes and find the maximum + // Count the votes and find the maximum. $total_votes = 0; $max_votes = 0; foreach ($node->choice as $choice) { @@ -908,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.0/modules/poll/poll.pages.inc drupal-7.66/modules/poll/poll.pages.inc --- drupal-7.0/modules/poll/poll.pages.inc 2010-10-06 15:38:40.000000000 +0200 +++ drupal-7.66/modules/poll/poll.pages.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: poll.pages.inc,v 1.28 2010/10/06 13:38:40 dries Exp $ /** * @file diff -Naur drupal-7.0/modules/poll/poll.test drupal-7.66/modules/poll/poll.test --- drupal-7.0/modules/poll/poll.test 2010-10-25 17:51:21.000000000 +0200 +++ drupal-7.66/modules/poll/poll.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,9 +1,8 @@ <?php -// $Id: poll.test,v 1.39 2010/10/25 15:51:21 webchick Exp $ /** * @file - * Tests for the poll module. + * Tests for poll.module. */ class PollTestCase extends DrupalWebTestCase { @@ -25,8 +24,9 @@ function pollCreate($title, $choices, $preview = TRUE) { $this->assertTrue(TRUE, 'Create a poll'); + $admin_user = $this->drupalCreateUser(array('create poll content', 'administer nodes')); $web_user = $this->drupalCreateUser(array('create poll content', 'access content', 'edit own poll content')); - $this->drupalLogin($web_user); + $this->drupalLogin($admin_user); // Get the form first to initialize the state of the internal browser. $this->drupalGet('node/add/poll'); @@ -34,6 +34,18 @@ // Prepare a form with two choices. list($edit, $index) = $this->_pollGenerateEdit($title, $choices); + // Verify that the vote count element only allows non-negative integers. + $edit['choice[new:1][chvotes]'] = -1; + $edit['choice[new:0][chvotes]'] = $this->randomString(7); + $this->drupalPost(NULL, $edit, t('Save')); + $this->assertText(t('Negative values are not allowed.')); + $this->assertText(t('Vote count for new choice must be an integer.')); + + // Repeat steps for initializing the state of the internal browser. + $this->drupalLogin($web_user); + $this->drupalGet('node/add/poll'); + list($edit, $index) = $this->_pollGenerateEdit($title, $choices); + // Re-submit the form until all choices are filled in. if (count($choices) > 2) { while ($index < count($choices)) { @@ -52,7 +64,7 @@ $this->drupalPost(NULL, $edit, t('Save')); $node = $this->drupalGetNodeByTitle($title); $this->assertText(t('@type @title has been created.', array('@type' => node_type_get_name('poll'), '@title' => $title)), 'Poll has been created.'); - $this->assertTrue($node->nid, t('Poll has been found in the database.')); + $this->assertTrue($node->nid, 'Poll has been found in the database.'); return isset($node->nid) ? $node->nid : FALSE; } @@ -89,10 +101,6 @@ } foreach ($new_choices as $k => $text) { $edit['choice[new:' . $k . '][chtext]'] = $text; - // To test poll choice weights, every new choice is sorted in front of - // existing choices. Existing/already submitted choices should keep their - // weight. - $edit['choice[new:' . $k . '][weight]'] = (- $index - $k); } return array($edit, count($already_submitted_choices) + count($new_choices)); } @@ -123,13 +131,13 @@ */ function assertPollChoiceOrder(array $choices, $index = 0, $preview = FALSE) { $expected = array(); + $weight = 0; foreach ($choices as $id => $label) { if ($id < $index) { - // The expected weight of each choice is exactly the negated id. - // @see PollTestCase::_pollGenerateEdit() - $weight = -$id; + // The expected weight of each choice is higher than the previous one. + $weight++; // Directly assert the weight form element value for this choice. - $this->assertFieldByName('choice[chid:' . $id . '][weight]', $weight, t('Found choice @id with weight @weight.', array( + $this->assertFieldByName('choice[chid:' . $id . '][weight]', $weight, format_string('Found choice @id with weight @weight.', array( '@id' => $id, '@weight' => $weight, ))); @@ -159,7 +167,7 @@ $expected_order = $expected; foreach ($elements as $element) { $next_label = array_shift($expected_order); - $this->assertEqual((string) $element, $next_label, t('Found choice @label in preview.', array( + $this->assertEqual((string) $element, $next_label, format_string('Found choice @label in preview.', array( '@label' => $next_label, ))); } @@ -189,7 +197,7 @@ function testPollCreate() { $title = $this->randomName(); $choices = $this->_generateChoices(7); - $this->pollCreate($title, $choices, TRUE); + $poll_nid = $this->pollCreate($title, $choices, TRUE); // Verify poll appears on 'poll' page. $this->drupalGet('poll'); @@ -199,6 +207,32 @@ // Click on the poll title to go to node page. $this->clickLink($title); $this->assertText('Total votes: 0', 'Link to poll correct.'); + + // Now add a new option to make sure that when we update the node the + // option is displayed. + $node = node_load($poll_nid); + + $new_option = $this->randomName(); + + $vote_count = '2000'; + $node->choice[] = array( + 'chid' => '', + 'chtext' => $new_option, + 'chvotes' => (int) $vote_count, + 'weight' => 1000, + ); + + node_save($node); + + $this->drupalGet('poll'); + $this->clickLink($title); + $this->assertText($new_option, 'New option found.'); + + $option = $this->xpath('//div[@id="node-1"]//div[@class="poll"]//div[@class="text"]'); + $this->assertEqual(end($option), $new_option, 'Last item is equal to new option.'); + + $votes = $this->xpath('//div[@id="node-1"]//div[@class="poll"]//div[@class="percent"]'); + $this->assertTrue(strpos(end($votes), $vote_count) > 0, "Votes saved."); } function testPollClose() { @@ -220,7 +254,7 @@ // Verify 'Vote' button no longer appears. $this->drupalGet('node/' . $poll_nid); $elements = $this->xpath('//input[@id="edit-vote"]'); - $this->assertTrue(empty($elements), t("Vote button doesn't appear.")); + $this->assertTrue(empty($elements), "Vote button doesn't appear."); // Verify status on 'poll' page is 'closed'. $this->drupalGet('poll'); @@ -238,7 +272,7 @@ $this->drupalPost('node/' . $poll_nid, $vote_edit, t('Vote')); $this->assertText('Your vote was recorded.', 'Your vote was recorded.'); $elements = $this->xpath('//input[@value="Cancel your vote"]'); - $this->assertTrue(isset($elements[0]), t("'Cancel your vote' button appears.")); + $this->assertTrue(isset($elements[0]), "'Cancel your vote' button appears."); // Edit the poll node and close the poll. $this->drupalLogout(); @@ -249,7 +283,7 @@ // Verify 'Cancel your vote' button no longer appears. $this->drupalGet('node/' . $poll_nid); $elements = $this->xpath('//input[@value="Cancel your vote"]'); - $this->assertTrue(empty($elements), t("'Cancel your vote' button no longer appears.")); + $this->assertTrue(empty($elements), "'Cancel your vote' button no longer appears."); } } @@ -281,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', @@ -289,7 +328,7 @@ $this->assertText('Your vote was recorded.', 'Your vote was recorded.'); $this->assertText('Total votes: 1', 'Vote count updated correctly.'); $elements = $this->xpath('//input[@value="Cancel your vote"]'); - $this->assertTrue(isset($elements[0]), t("'Cancel your vote' button appears.")); + $this->assertTrue(isset($elements[0]), "'Cancel your vote' button appears."); $this->drupalGet("node/$poll_nid/votes"); $this->assertText(t('This table lists all the recorded votes for this poll. If anonymous users are allowed to vote, they will be identified by the IP address of the computer they used when they voted.'), 'Vote table text.'); @@ -325,7 +364,7 @@ $this->assertText('Your vote was recorded.', 'Your vote was recorded.'); $this->assertText('Total votes: 1', 'Vote count updated correctly.'); $elements = $this->xpath('//input[@value="Cancel your vote"]'); - $this->assertTrue(empty($elements), t("'Cancel your vote' button does not appear.")); + $this->assertTrue(empty($elements), "'Cancel your vote' button does not appear."); } } @@ -349,13 +388,13 @@ function testRecentBlock() { // Set block title to confirm that the interface is available. $this->drupalPost('admin/structure/block/manage/poll/recent/configure', array('title' => $this->randomName(8)), 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.'); // Set the block to a region to confirm block is available. $edit = array(); $edit['blocks[poll_recent][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.'); // Create a poll which should appear in recent polls block. $title = $this->randomName(); @@ -426,14 +465,14 @@ 'choice[new:1][chtext]' => $this->randomName(), ); - // Press 'add choice' button through AJAX, and place the expected HTML result + // Press 'add choice' button through Ajax, and place the expected HTML result // as the tested content. $commands = $this->drupalPostAJAX(NULL, $edit, array('op' => t('More choices'))); $this->content = $commands[1]['data']; - $this->assertFieldByName('choice[chid:0][chtext]', $edit['choice[new:0][chtext]'], t('Field !i found', array('!i' => 0))); - $this->assertFieldByName('choice[chid:1][chtext]', $edit['choice[new:1][chtext]'], t('Field !i found', array('!i' => 1))); - $this->assertFieldByName('choice[new:0][chtext]', '', t('Field !i found', array('!i' => 2))); + $this->assertFieldByName('choice[chid:0][chtext]', $edit['choice[new:0][chtext]'], format_string('Field !i found', array('!i' => 0))); + $this->assertFieldByName('choice[chid:1][chtext]', $edit['choice[new:1][chtext]'], format_string('Field !i found', array('!i' => 1))); + $this->assertFieldByName('choice[new:0][chtext]', '', format_string('Field !i found', array('!i' => 2))); } } @@ -490,49 +529,49 @@ // User1 vote on Poll. $this->drupalPost('node/' . $this->poll_nid, $edit, t('Vote')); - $this->assertText(t('Your vote was recorded.'), t('%user vote was recorded.', array('%user' => $this->web_user1->name))); - $this->assertText(t('Total votes: @votes', array('@votes' => 1)), t('Vote count updated correctly.')); + $this->assertText(t('Your vote was recorded.'), format_string('%user vote was recorded.', array('%user' => $this->web_user1->name))); + $this->assertText(t('Total votes: @votes', array('@votes' => 1)), 'Vote count updated correctly.'); // Check to make sure User1 cannot vote again. $this->drupalGet('node/' . $this->poll_nid); $elements = $this->xpath('//input[@value="Vote"]'); - $this->assertTrue(empty($elements), t("%user is not able to vote again.", array('%user' => $this->web_user1->name))); + $this->assertTrue(empty($elements), format_string("%user is not able to vote again.", array('%user' => $this->web_user1->name))); $elements = $this->xpath('//input[@value="Cancel your vote"]'); - $this->assertTrue(!empty($elements), t("'Cancel your vote' button appears.")); + $this->assertTrue(!empty($elements), "'Cancel your vote' button appears."); // Logout User1. $this->drupalLogout(); // Fill the page cache by requesting the poll. $this->drupalGet('node/' . $this->poll_nid); - $this->assertEqual($this->drupalGetHeader('x-drupal-cache'), 'MISS', t('Page was cacheable but was not in the cache.')); + $this->assertEqual($this->drupalGetHeader('x-drupal-cache'), 'MISS', 'Page was cacheable but was not in the cache.'); $this->drupalGet('node/' . $this->poll_nid); - $this->assertEqual($this->drupalGetHeader('x-drupal-cache'), 'HIT', t('Page was cached.')); + $this->assertEqual($this->drupalGetHeader('x-drupal-cache'), 'HIT', 'Page was cached.'); // Anonymous user vote on Poll. $this->drupalPost(NULL, $edit, t('Vote')); - $this->assertText(t('Your vote was recorded.'), t('Anonymous vote was recorded.')); - $this->assertText(t('Total votes: @votes', array('@votes' => 2)), t('Vote count updated correctly.')); + $this->assertText(t('Your vote was recorded.'), 'Anonymous vote was recorded.'); + $this->assertText(t('Total votes: @votes', array('@votes' => 2)), 'Vote count updated correctly.'); $elements = $this->xpath('//input[@value="Cancel your vote"]'); - $this->assertTrue(!empty($elements), t("'Cancel your vote' button appears.")); + $this->assertTrue(!empty($elements), "'Cancel your vote' button appears."); // Check to make sure Anonymous user cannot vote again. $this->drupalGet('node/' . $this->poll_nid); - $this->assertFalse($this->drupalGetHeader('x-drupal-cache'), t('Page was not cacheable.')); + $this->assertFalse($this->drupalGetHeader('x-drupal-cache'), 'Page was not cacheable.'); $elements = $this->xpath('//input[@value="Vote"]'); - $this->assertTrue(empty($elements), t("Anonymous is not able to vote again.")); + $this->assertTrue(empty($elements), "Anonymous is not able to vote again."); $elements = $this->xpath('//input[@value="Cancel your vote"]'); - $this->assertTrue(!empty($elements), t("'Cancel your vote' button appears.")); + $this->assertTrue(!empty($elements), "'Cancel your vote' button appears."); // Login User2. $this->drupalLogin($this->web_user2); // User2 vote on poll. $this->drupalPost('node/' . $this->poll_nid, $edit, t('Vote')); - $this->assertText(t('Your vote was recorded.'), t('%user vote was recorded.', array('%user' => $this->web_user2->name))); + $this->assertText(t('Your vote was recorded.'), format_string('%user vote was recorded.', array('%user' => $this->web_user2->name))); $this->assertText(t('Total votes: @votes', array('@votes' => 3)), 'Vote count updated correctly.'); $elements = $this->xpath('//input[@value="Cancel your vote"]'); - $this->assertTrue(empty($elements), t("'Cancel your vote' button does not appear.")); + $this->assertTrue(empty($elements), "'Cancel your vote' button does not appear."); // Logout User2. $this->drupalLogout(); @@ -548,22 +587,22 @@ // Check to make sure Anonymous user can vote again with a new session after // a hostname change. $this->drupalGet('node/' . $this->poll_nid); - $this->assertEqual($this->drupalGetHeader('x-drupal-cache'), 'MISS', t('Page was cacheable but was not in the cache.')); + $this->assertEqual($this->drupalGetHeader('x-drupal-cache'), 'MISS', 'Page was cacheable but was not in the cache.'); $this->drupalPost(NULL, $edit, t('Vote')); - $this->assertText(t('Your vote was recorded.'), t('%user vote was recorded.', array('%user' => $this->web_user2->name))); + $this->assertText(t('Your vote was recorded.'), format_string('%user vote was recorded.', array('%user' => $this->web_user2->name))); $this->assertText(t('Total votes: @votes', array('@votes' => 4)), 'Vote count updated correctly.'); $elements = $this->xpath('//input[@value="Cancel your vote"]'); - $this->assertTrue(!empty($elements), t("'Cancel your vote' button appears.")); + $this->assertTrue(!empty($elements), "'Cancel your vote' button appears."); // Check to make sure Anonymous user cannot vote again with a new session, // and that the vote from the previous session cannot be cancelledd. $this->curlClose(); $this->drupalGet('node/' . $this->poll_nid); - $this->assertEqual($this->drupalGetHeader('x-drupal-cache'), 'MISS', t('Page was cacheable but was not in the cache.')); + $this->assertEqual($this->drupalGetHeader('x-drupal-cache'), 'MISS', 'Page was cacheable but was not in the cache.'); $elements = $this->xpath('//input[@value="Vote"]'); - $this->assertTrue(empty($elements), t('Anonymous is not able to vote again.')); + $this->assertTrue(empty($elements), 'Anonymous is not able to vote again.'); $elements = $this->xpath('//input[@value="Cancel your vote"]'); - $this->assertTrue(empty($elements), t("'Cancel your vote' button does not appear.")); + $this->assertTrue(empty($elements), "'Cancel your vote' button does not appear."); // Login User1. $this->drupalLogin($this->web_user1); @@ -571,9 +610,9 @@ // Check to make sure User1 still cannot vote even after hostname changed. $this->drupalGet('node/' . $this->poll_nid); $elements = $this->xpath('//input[@value="Vote"]'); - $this->assertTrue(empty($elements), t("%user is not able to vote again.", array('%user' => $this->web_user1->name))); + $this->assertTrue(empty($elements), format_string("%user is not able to vote again.", array('%user' => $this->web_user1->name))); $elements = $this->xpath('//input[@value="Cancel your vote"]'); - $this->assertTrue(!empty($elements), t("'Cancel your vote' button appears.")); + $this->assertTrue(!empty($elements), "'Cancel your vote' button appears."); } } @@ -649,11 +688,11 @@ $tests['[node:poll-duration]'] = format_interval($poll->runtime, 1, $language->language); // Test to make sure that we generated something for each token. - $this->assertFalse(in_array(0, array_map('strlen', $tests)), t('No empty tokens generated.')); + $this->assertFalse(in_array(0, array_map('strlen', $tests)), 'No empty tokens generated.'); foreach ($tests as $input => $expected) { $output = token_replace($input, array('node' => $poll), array('language' => $language)); - $this->assertFalse(strcmp($output, $expected), t('Sanitized poll token %token replaced.', array('%token' => $input))); + $this->assertEqual($output, $expected, format_string('Sanitized poll token %token replaced.', array('%token' => $input))); } // Generate and test unsanitized tokens. @@ -661,7 +700,7 @@ foreach ($tests as $input => $expected) { $output = token_replace($input, array('node' => $poll), array('language' => $language, 'sanitize' => FALSE)); - $this->assertFalse(strcmp($output, $expected), t('Unsanitized poll token %token replaced.', array('%token' => $input))); + $this->assertEqual($output, $expected, format_string('Unsanitized poll token %token replaced.', array('%token' => $input))); } } } @@ -684,33 +723,33 @@ $title = $this->randomName(); $choices = $this->_generateChoices(2); $poll_nid = $this->pollCreate($title, $choices, FALSE); - $this->assertTrue($poll_nid, t('Poll for auto-expire test created.')); + $this->assertTrue($poll_nid, 'Poll for auto-expire test created.'); // Visit the poll edit page and verify that by default, expiration // is set to unlimited. $this->drupalGet("node/$poll_nid/edit"); - $this->assertField('runtime', t('Poll expiration setting found.')); + $this->assertField('runtime', 'Poll expiration setting found.'); $elements = $this->xpath('//select[@id="edit-runtime"]/option[@selected="selected"]'); - $this->assertTrue(isset($elements[0]['value']) && $elements[0]['value'] == 0, t('Poll expiration set to unlimited.')); + $this->assertTrue(isset($elements[0]['value']) && $elements[0]['value'] == 0, 'Poll expiration set to unlimited.'); // Set the expiration to one week. $edit = array(); $poll_expiration = 604800; // One week. $edit['runtime'] = $poll_expiration; $this->drupalPost(NULL, $edit, t('Save')); - $this->assertRaw(t('Poll %title has been updated.', array('%title' => $title)), t('Poll expiration settings saved.')); + $this->assertRaw(t('Poll %title has been updated.', array('%title' => $title)), 'Poll expiration settings saved.'); // Make sure that the changed expiration settings is kept. $this->drupalGet("node/$poll_nid/edit"); $elements = $this->xpath('//select[@id="edit-runtime"]/option[@selected="selected"]'); - $this->assertTrue(isset($elements[0]['value']) && $elements[0]['value'] == $poll_expiration, t('Poll expiration set to unlimited.')); + $this->assertTrue(isset($elements[0]['value']) && $elements[0]['value'] == $poll_expiration, 'Poll expiration set to unlimited.'); // Force a cron run. Since the expiration date has not yet been reached, // the poll should remain active. drupal_cron_run(); $this->drupalGet("node/$poll_nid/edit"); $elements = $this->xpath('//input[@id="edit-active-1"]'); - $this->assertTrue(isset($elements[0]) && !empty($elements[0]['checked']), t('Poll is still active.')); + $this->assertTrue(isset($elements[0]) && !empty($elements[0]['checked']), 'Poll is still active.'); // Test expiration. Since REQUEST_TIME is a constant and we don't // want to keep SimpleTest waiting until the moment of expiration arrives, @@ -725,6 +764,114 @@ drupal_cron_run(); $this->drupalGet("node/$poll_nid/edit"); $elements = $this->xpath('//input[@id="edit-active-0"]'); - $this->assertTrue(isset($elements[0]) && !empty($elements[0]['checked']), t('Poll has expired.')); + $this->assertTrue(isset($elements[0]) && !empty($elements[0]['checked']), 'Poll has expired.'); + } +} + +class PollDeleteChoiceTestCase extends PollTestCase { + public static function getInfo() { + return array( + 'name' => 'Poll choice deletion', + 'description' => 'Test the poll choice deletion logic.', + 'group' => 'Poll', + ); + } + + function setUp() { + parent::setUp('poll'); + } + + function testChoiceRemoval() { + // Set up a poll with three choices. + $title = $this->randomName(); + $choices = array('First choice', 'Second choice', 'Third choice'); + $poll_nid = $this->pollCreate($title, $choices, FALSE); + $this->assertTrue($poll_nid, 'Poll for choice deletion logic test created.'); + + // Edit the poll, and try to delete first poll choice. + $this->drupalGet("node/$poll_nid/edit"); + $edit['choice[chid:1][chtext]'] = ''; + $this->drupalPost(NULL, $edit, t('Save')); + + // Click on the poll title to go to node page. + $this->drupalGet('poll'); + $this->clickLink($title); + + // Check the first poll choice is deleted, while the others remain. + $this->assertNoText('First choice', 'First choice removed.'); + $this->assertText('Second choice', 'Second choice remains.'); + $this->assertText('Third choice', 'Third choice remains.'); + } +} + +/** + * Tests poll translation logic. + */ +class PollTranslateTestCase extends PollTestCase { + public static function getInfo() { + return array( + 'name' => 'Poll translation', + 'description' => 'Test the poll translation logic.', + 'group' => 'Poll', + ); + } + + function setUp() { + parent::setUp('poll', 'translation'); + } + + /** + * Tests poll creation and translation. + * + * Checks that the choice names get copied from the original poll and that + * the vote count values are set to 0. + */ + function testPollTranslate() { + $admin_user = $this->drupalCreateUser(array('administer content types', 'administer languages', 'edit any poll content', 'create poll content', 'administer nodes', 'translate content')); + + // Set up a poll with two choices. + $title = $this->randomName(); + $choices = array($this->randomName(), $this->randomName()); + $poll_nid = $this->pollCreate($title, $choices, FALSE); + $this->assertTrue($poll_nid, 'Poll for translation logic test created.'); + + $this->drupalLogout(); + $this->drupalLogin($admin_user); + + // Enable a second language. + $this->drupalGet('admin/config/regional/language'); + $edit = array(); + $edit['langcode'] = 'nl'; + $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language')); + $this->assertRaw(t('The language %language has been created and can now be used.', array('%language' => 'Dutch')), 'Language Dutch has been created.'); + + // Set "Poll" content type to use multilingual support with translation. + $this->drupalGet('admin/structure/types/manage/poll'); + $edit = array(); + $edit['language_content_type'] = 2; + $this->drupalPost('admin/structure/types/manage/poll', $edit, t('Save content type')); + $this->assertRaw(t('The content type %type has been updated.', array('%type' => 'Poll')), 'Poll content type has been updated.'); + + // Edit poll. + $this->drupalGet("node/$poll_nid/edit"); + $edit = array(); + // Set the poll's first choice count to 200. + $edit['choice[chid:1][chvotes]'] = 200; + // Set the language to Dutch. + $edit['language'] = 'nl'; + $this->drupalPost(NULL, $edit, t('Save')); + + // Translate the Dutch poll. + $this->drupalGet('node/add/poll', array('query' => array('translation' => $poll_nid, 'target' => 'en'))); + + $dutch_poll = node_load($poll_nid); + + // Check that the vote count values didn't get copied from the Dutch poll + // and are set to 0. + $this->assertFieldByName('choice[chid:1][chvotes]', '0', ('Found choice with vote count 0')); + $this->assertFieldByName('choice[chid:2][chvotes]', '0', ('Found choice with vote count 0')); + // Check that the choice names got copied from the Dutch poll. + $this->assertFieldByName('choice[chid:1][chtext]', $dutch_poll->choice[1]['chtext'], format_string('Found choice with text @text', array('@text' => $dutch_poll->choice[1]['chtext']))); + $this->assertFieldByName('choice[chid:2][chtext]', $dutch_poll->choice[2]['chtext'], format_string('Found choice with text @text', array('@text' => $dutch_poll->choice[2]['chtext']))); } } diff -Naur drupal-7.0/modules/poll/poll.tokens.inc drupal-7.66/modules/poll/poll.tokens.inc --- drupal-7.0/modules/poll/poll.tokens.inc 2010-12-01 01:31:38.000000000 +0100 +++ drupal-7.66/modules/poll/poll.tokens.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: poll.tokens.inc,v 1.6 2010/12/01 00:31:38 webchick Exp $ /** * @file diff -Naur drupal-7.0/modules/profile/profile-block.tpl.php drupal-7.66/modules/profile/profile-block.tpl.php --- drupal-7.0/modules/profile/profile-block.tpl.php 2009-08-06 07:05:59.000000000 +0200 +++ drupal-7.66/modules/profile/profile-block.tpl.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: profile-block.tpl.php,v 1.4 2009/08/06 05:05:59 webchick Exp $ /** * @file @@ -32,9 +31,9 @@ ?> <?php print $user_picture; ?> -<?php foreach ($profile as $field) : ?> +<?php foreach ($profile as $field): ?> <p> - <?php if ($field->type != 'checkbox') : ?> + <?php if ($field->type != 'checkbox'): ?> <strong><?php print $field->title; ?></strong><br /> <?php endif; ?> <?php print $field->value; ?> diff -Naur drupal-7.0/modules/profile/profile-listing.tpl.php drupal-7.66/modules/profile/profile-listing.tpl.php --- drupal-7.0/modules/profile/profile-listing.tpl.php 2009-08-06 07:05:59.000000000 +0200 +++ drupal-7.66/modules/profile/profile-listing.tpl.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: profile-listing.tpl.php,v 1.7 2009/08/06 05:05:59 webchick Exp $ /** * @file @@ -44,7 +43,7 @@ <?php print $name; ?> </div> - <?php foreach ($profile as $field) : ?> + <?php foreach ($profile as $field): ?> <div class="field"> <?php print $field->value; ?> </div> diff -Naur drupal-7.0/modules/profile/profile-wrapper.tpl.php drupal-7.66/modules/profile/profile-wrapper.tpl.php --- drupal-7.0/modules/profile/profile-wrapper.tpl.php 2008-10-13 14:31:42.000000000 +0200 +++ drupal-7.66/modules/profile/profile-wrapper.tpl.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: profile-wrapper.tpl.php,v 1.3 2008/10/13 12:31:42 dries Exp $ /** * @file @@ -7,7 +6,7 @@ * profiles. * * This template is used when viewing a list of users. It can be a general - * list for viewing all users with the url of "example.com/profile" or when + * list for viewing all users with the URL of "example.com/profile" or when * viewing a set of users who share a specific value for a profile such * as "example.com/profile/country/belgium". * diff -Naur drupal-7.0/modules/profile/profile.admin.inc drupal-7.66/modules/profile/profile.admin.inc --- drupal-7.0/modules/profile/profile.admin.inc 2010-10-28 04:27:09.000000000 +0200 +++ drupal-7.66/modules/profile/profile.admin.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: profile.admin.inc,v 1.44 2010/10/28 02:27:09 dries Exp $ /** * @file diff -Naur drupal-7.0/modules/profile/profile.css drupal-7.66/modules/profile/profile.css --- drupal-7.0/modules/profile/profile.css 2007-11-30 10:02:51.000000000 +0100 +++ drupal-7.66/modules/profile/profile.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -/* $Id: profile.css,v 1.3 2007/11/30 09:02:51 goba Exp $ */ #profile-fields td.category { font-weight: bold; diff -Naur drupal-7.0/modules/profile/profile.info drupal-7.66/modules/profile/profile.info --- drupal-7.0/modules/profile/profile.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/profile/profile.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: profile.info,v 1.12 2010/12/20 19:59:42 webchick Exp $ name = Profile description = Supports configurable user profiles. package = Core @@ -12,8 +11,7 @@ ; See user_system_info_alter(). hidden = TRUE -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/profile/profile.install drupal-7.66/modules/profile/profile.install --- drupal-7.0/modules/profile/profile.install 2010-08-22 15:55:53.000000000 +0200 +++ drupal-7.66/modules/profile/profile.install 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: profile.install,v 1.26 2010/08/22 13:55:53 dries Exp $ /** * @file diff -Naur drupal-7.0/modules/profile/profile.js drupal-7.66/modules/profile/profile.js --- drupal-7.0/modules/profile/profile.js 2010-01-30 08:59:25.000000000 +0100 +++ drupal-7.66/modules/profile/profile.js 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -// $Id: profile.js,v 1.8 2010/01/30 07:59:25 dries Exp $ (function ($) { /** @@ -25,7 +24,7 @@ } } // This category has become empty - if ($(this).next('tr').is(':not(.draggable)') || $(this).next('tr').size() == 0) { + if ($(this).next('tr').is(':not(.draggable)') || $(this).next('tr').length == 0) { $(this).removeClass('category-populated').addClass('category-empty'); } // This category has become populated. diff -Naur drupal-7.0/modules/profile/profile.module drupal-7.66/modules/profile/profile.module --- drupal-7.0/modules/profile/profile.module 2010-10-01 17:24:18.000000000 +0200 +++ drupal-7.66/modules/profile/profile.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: profile.module,v 1.294 2010/10/01 15:24:18 webchick Exp $ /** * @file @@ -34,7 +33,7 @@ case 'admin/help#profile': $output = ''; $output .= '<h3>' . t('About') . '</h3>'; - $output .= '<p>' . t('The Profile module allows site administrators to define custom fields (such as country, full name, or age) for user profiles, which are then displayed in the <a href="@user">My Account</a> section. This permits users of a site to share more information about themselves, and can help community-based sites organize users around specific information. For more information, see the online handbook entry for <a href="@profile">Profile module</a>.', array('@user' => url('user'), '@profile' => 'http://drupal.org/handbook/modules/profile/')) . '</p>'; + $output .= '<p>' . t('The Profile module allows site administrators to define custom fields (such as country, full name, or age) for user profiles, which are then displayed in the <a href="@user">My Account</a> section. This permits users of a site to share more information about themselves, and can help community-based sites organize users around specific information. For more information, see the online handbook entry for <a href="@profile">Profile module</a>.', array('@user' => url('user'), '@profile' => 'http://drupal.org/documentation/modules/profile/')) . '</p>'; $output .= '<h3>' . t('Uses') . '</h3>'; $output .= '<dl>'; $output .= '<dt>' . t('Adding fields to the default profile') . '</dt>'; @@ -215,7 +214,7 @@ * Implements hook_user_presave(). */ function profile_user_presave(&$edit, $account, $category) { - if ($account->uid) { + if (!empty($account->uid)) { profile_save_profile($edit, $account, $category); } } @@ -230,7 +229,7 @@ /** * Implements hook_user_cancel(). */ -function profile_user_cancel(&$edit, $account, $method) { +function profile_user_cancel($edit, $account, $method) { switch ($method) { case 'user_cancel_reassign': db_delete('profile_value') @@ -436,7 +435,7 @@ break; case 'selection': - $options = $field->required ? array() : array('--'); + $options = array(); $lines = preg_split("/[\n\r]/", $field->options); foreach ($lines as $line) { if ($line = trim($line)) { @@ -450,6 +449,7 @@ '#options' => $options, '#description' => _profile_form_explanation($field), '#required' => $field->required, + '#empty_value' => 0, ); break; @@ -545,6 +545,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 = check_plain($field->title); $variables['profile'][$field->name]->value = $field->value; $variables['profile'][$field->name]->type = $field->type; @@ -570,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.0/modules/profile/profile.pages.inc drupal-7.66/modules/profile/profile.pages.inc --- drupal-7.0/modules/profile/profile.pages.inc 2010-11-12 03:57:15.000000000 +0100 +++ drupal-7.66/modules/profile/profile.pages.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: profile.pages.inc,v 1.28 2010/11/12 02:57:15 dries Exp $ /** * @file @@ -18,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(); @@ -55,8 +52,7 @@ $query->condition('v.value', '%' . db_like($value) . '%', 'LIKE'); break; default: - drupal_not_found(); - return; + return MENU_NOT_FOUND; } $uids = $query @@ -86,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.0/modules/profile/profile.test drupal-7.66/modules/profile/profile.test --- drupal-7.0/modules/profile/profile.test 2010-08-30 02:22:03.000000000 +0200 +++ drupal-7.66/modules/profile/profile.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,9 @@ <?php -// $Id: profile.test,v 1.30 2010/08/30 00:22:03 webchick Exp $ + +/** + * @file + * Tests for profile.module. + */ /** * A class for common methods for testing profile fields. @@ -38,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, @@ -92,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, @@ -137,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; @@ -156,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']))); } } @@ -266,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'], @@ -301,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.'); } } @@ -335,20 +339,30 @@ $this->setProfileField($field, $field['value']); // Set some html for what we want to see in the page output later. - $autocomplete_html = '<input type="hidden" id="' . drupal_html_id('edit-' . $field['form_name'] . '-autocomplete') . '" value="' . url('profile/autocomplete/' . $field['fid'], array('absolute' => TRUE)) . '" disabled="disabled" class="autocomplete" />'; - $field_html = '<input type="text" maxlength="255" name="' . $field['form_name'] . '" id="' . drupal_html_id('edit-' . $field['form_name']) . '" size="60" value="' . $field['value'] . '" class="form-text form-autocomplete required" />'; + // 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 = '<input type="hidden" id="' . $autocomplete_id . '" value="' . $autocomplete_url . '" disabled="disabled" class="autocomplete" />'; // 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(); @@ -356,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.'); } } @@ -395,52 +409,52 @@ } function testAuthorInformationBlock() { - // Set the block to a region to confirm the block is availble. + // Set the block to a region to confirm the block is available. $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); @@ -477,6 +491,46 @@ } } +/** + * Test profile integration with user CRUD operations. + */ +class ProfileCrudTestCase extends ProfileTestCase { + public static function getInfo() { + return array( + 'name' => 'Profile CRUD tests', + 'description' => 'Test profile integration with user create, read, update, delete.', + 'group' => 'Profile', + ); + } + + /** + * Test profile integration with user CRUD operations. + */ + public function testUserCRUD() { + // @todo Add profile fields in addition to base user properties. + $edit = array( + 'name' => 'Test user', + 'mail' => 'test@example.com', + ); + + // Create. + // @todo Add assertions. + $account = user_save(NULL, $edit); + + // Read. + // @todo Add assertions. + $account = user_load($account->uid); + + // Update. + // @todo Add assertions. + $account = user_save($account, $edit); + + // Delete. + // @todo Add assertions. + user_delete($account->uid); + } +} + /** * TODO: * - Test field visibility diff -Naur drupal-7.0/modules/rdf/rdf.api.php drupal-7.66/modules/rdf/rdf.api.php --- drupal-7.0/modules/rdf/rdf.api.php 2010-10-01 03:44:39.000000000 +0200 +++ drupal-7.66/modules/rdf/rdf.api.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: rdf.api.php,v 1.6 2010/10/01 01:44:39 webchick Exp $ /** * @file diff -Naur drupal-7.0/modules/rdf/rdf.info drupal-7.66/modules/rdf/rdf.info --- drupal-7.0/modules/rdf/rdf.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/rdf/rdf.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: rdf.info,v 1.4 2010/12/20 19:59:42 webchick Exp $ name = RDF description = Enriches your content with metadata to let other applications (e.g. search engines, aggregators) better understand its relationships and attributes. package = Core @@ -6,8 +5,7 @@ core = 7.x files[] = rdf.test -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/rdf/rdf.install drupal-7.66/modules/rdf/rdf.install --- drupal-7.0/modules/rdf/rdf.install 2010-10-01 03:44:39.000000000 +0200 +++ drupal-7.66/modules/rdf/rdf.install 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: rdf.install,v 1.5 2010/10/01 01:44:39 webchick Exp $ /** * @file diff -Naur drupal-7.0/modules/rdf/rdf.module drupal-7.66/modules/rdf/rdf.module --- drupal-7.0/modules/rdf/rdf.module 2010-12-11 03:38:24.000000000 +0100 +++ drupal-7.66/modules/rdf/rdf.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: rdf.module,v 1.49 2010/12/11 02:38:24 dries Exp $ /** * @file @@ -14,7 +13,7 @@ case 'admin/help#rdf': $output = ''; $output .= '<h3>' . t('About') . '</h3>'; - $output .= '<p>' . t('The RDF module enriches your content with metadata to let other applications (e.g., search engines, aggregators, and so on) better understand its relationships and attributes. This semantically enriched, machine-readable output for Drupal sites uses the <a href="@rdfa">RDFa specification</a> which allows RDF data to be embedded in HTML markup. Other modules can define mappings of their data to RDF terms, and the RDF module makes this RDF data available to the theme. The core Drupal modules define RDF mappings for their data model, and the core Drupal themes output this RDF metadata information along with the human-readable visual information. For more information, see the online handbook entry for <a href="@rdf">RDF module</a>.', array('@rdfa' => 'http://www.w3.org/TR/xhtml-rdfa-primer/', '@rdf' => 'http://drupal.org/handbook/modules/rdf')) . '</p>'; + $output .= '<p>' . t('The RDF module enriches your content with metadata to let other applications (e.g., search engines, aggregators, and so on) better understand its relationships and attributes. This semantically enriched, machine-readable output for Drupal sites uses the <a href="@rdfa">RDFa specification</a> which allows RDF data to be embedded in HTML markup. Other modules can define mappings of their data to RDF terms, and the RDF module makes this RDF data available to the theme. The core Drupal modules define RDF mappings for their data model, and the core Drupal themes output this RDF metadata information along with the human-readable visual information. For more information, see the online handbook entry for <a href="@rdf">RDF module</a>.', array('@rdfa' => 'http://www.w3.org/TR/xhtml-rdfa-primer/', '@rdf' => 'http://drupal.org/documentation/modules/rdf')) . '</p>'; return $output; } } @@ -191,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; } /** @@ -369,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 @@ -472,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 <h2> 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 <head> 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. @@ -512,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 <head> 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'); - } + ), + ); } } @@ -647,10 +640,12 @@ if (!empty($rdf_mapping['rdftype'])) { $attributes['typeof'] = $rdf_mapping['rdftype']; } - // Annotate the user name in RDFa. The property attribute is used here - // because the user name is a literal. + // Annotate the username in RDFa. A property attribute is used with an empty + // datatype attribute to ensure the username is parsed as a plain literal + // in RDFa 1.0 and 1.1. if (!empty($rdf_mapping['name'])) { $attributes['property'] = $rdf_mapping['name']['predicates']; + $attributes['datatype'] = ''; } // Add the homepage RDFa markup if present. if (!empty($variables['homepage']) && !empty($rdf_mapping['homepage'])) { @@ -758,7 +753,10 @@ $element[$delta]['#options']['attributes']['typeof'] = $term->rdf_mapping['rdftype']; } if (!empty($term->rdf_mapping['name']['predicates'])) { + // A property attribute is used with an empty datatype attribute so + // the term name is parsed as a plain literal in RDFa 1.0 and 1.1. $element[$delta]['#options']['attributes']['property'] = $term->rdf_mapping['name']['predicates']; + $element[$delta]['#options']['attributes']['datatype'] = ''; } } } @@ -770,6 +768,9 @@ * Implements MODULE_preprocess_HOOK(). */ function rdf_preprocess_image(&$variables) { + // Adds the RDF type for image. We cannot use the usual entity-based mapping + // to get 'foaf:Image' because image does not have its own entity type or + // bundle. $variables['attributes']['typeof'] = array('foaf:Image'); } @@ -858,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 <span> 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 <span> tags in the document. $attributes['class'][] = 'rdf-meta'; + $attributes['class'][] = 'element-hidden'; // The XHTML+RDFa doctype allows either <span></span> or <span /> 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.0/modules/rdf/rdf.test drupal-7.66/modules/rdf/rdf.test --- drupal-7.0/modules/rdf/rdf.test 2010-12-14 02:04:27.000000000 +0100 +++ drupal-7.66/modules/rdf/rdf.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,9 +1,8 @@ <?php -// $Id: rdf.test,v 1.30 2010/12/14 01:04:27 dries Exp $ /** * @file - * Tests for RDF functionality. + * Tests for rdf.module. */ class RdfMappingHookTestCase extends DrupalWebTestCase { @@ -25,17 +24,17 @@ function testMapping() { // Test that the mapping is returned correctly by the hook. $mapping = rdf_mapping_load('test_entity', 'test_bundle'); - $this->assertIdentical($mapping['rdftype'], array('sioc:Post'), t('Mapping for rdftype is sioc:Post.')); - $this->assertIdentical($mapping['title'], array('predicates' => array('dc:title')), t('Mapping for title is dc:title.')); + $this->assertIdentical($mapping['rdftype'], array('sioc:Post'), 'Mapping for rdftype is sioc:Post.'); + $this->assertIdentical($mapping['title'], array('predicates' => array('dc:title')), 'Mapping for title is dc:title.'); $this->assertIdentical($mapping['created'], array( 'predicates' => array('dc:created'), 'datatype' => 'xsd:dateTime', 'callback' => 'date_iso8601', ), t('Mapping for created is dc:created with datatype xsd:dateTime and callback date_iso8601.')); - $this->assertIdentical($mapping['uid'], array('predicates' => array('sioc:has_creator', 'dc:creator'), 'type' => 'rel'), t('Mapping for uid is sioc:has_creator and dc:creator, and type is rel.')); + $this->assertIdentical($mapping['uid'], array('predicates' => array('sioc:has_creator', 'dc:creator'), 'type' => 'rel'), 'Mapping for uid is sioc:has_creator and dc:creator, and type is rel.'); $mapping = rdf_mapping_load('test_entity', 'test_bundle_no_mapping'); - $this->assertEqual($mapping, array(), t('Empty array returned when an entity type, bundle pair has no mapping.')); + $this->assertEqual($mapping, array(), 'Empty array returned when an entity type, bundle pair has no mapping.'); } } @@ -160,7 +159,7 @@ $image = current($this->drupalGetTestFiles('image')); // Create an array for drupalPost with the field names as the keys and - // the uris for the test files as the values. + // the URIs for the test files as the values. $edit = array("files[" . $field_name . "_" . $langcode . "_0]" => drupal_realpath($file->uri), "files[" . $image_field . "_" . $langcode . "_0]" => drupal_realpath($image->uri)); @@ -180,13 +179,13 @@ $file_rel = $this->xpath('//div[contains(@about, :node-uri)]//div[contains(@rel, "rdfs:seeAlso") and contains(@resource, ".txt")]', array( ':node-uri' => 'node/' . $nid, )); - $this->assertTrue(!empty($file_rel), t('Attribute \'rel\' set on file field. Attribute \'resource\' is also set.')); + $this->assertTrue(!empty($file_rel), "Attribute 'rel' set on file field. Attribute 'resource' is also set."); $image_rel = $this->xpath('//div[contains(@about, :node-uri)]//div[contains(@rel, "rdfs:seeAlso") and contains(@resource, :image)]//img[contains(@typeof, "foaf:Image")]', array( ':node-uri' => 'node/' . $nid, ':image' => $image_filename, )); - $this->assertTrue(!empty($image_rel), t('Attribute \'rel\' set on image field. Attribute \'resource\' is also set.')); + $this->assertTrue(!empty($image_rel), "Attribute 'rel' set on image field. Attribute 'resource' is also set."); // Edits the node to add tags. $tag1 = $this->randomName(8); @@ -196,16 +195,16 @@ $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save')); // Ensures the RDFa markup for the relationship between the node and its // tags is correct. - $term_rdfa_meta = $this->xpath('//div[@about=:node-url and contains(@typeof, "sioc:Item") and contains(@typeof, "foaf:Document")]//ul[@class="links"]/li[@rel="dc:subject"]/a[@typeof="skos:Concept" and text()=:term-name]', array( + $term_rdfa_meta = $this->xpath('//div[@about=:node-url and contains(@typeof, "sioc:Item") and contains(@typeof, "foaf:Document")]//ul[@class="links"]/li[@rel="dc:subject"]/a[@typeof="skos:Concept" and @datatype="" and text()=:term-name]', array( ':node-url' => url('node/' . $node->nid), ':term-name' => $tag1, )); - $this->assertTrue(!empty($term_rdfa_meta), t('Property dc:subject is present for the tag1 field item.')); - $term_rdfa_meta = $this->xpath('//div[@about=:node-url and contains(@typeof, "sioc:Item") and contains(@typeof, "foaf:Document")]//ul[@class="links"]/li[@rel="dc:subject"]/a[@typeof="skos:Concept" and text()=:term-name]', array( + $this->assertTrue(!empty($term_rdfa_meta), 'Property dc:subject is present for the tag1 field item.'); + $term_rdfa_meta = $this->xpath('//div[@about=:node-url and contains(@typeof, "sioc:Item") and contains(@typeof, "foaf:Document")]//ul[@class="links"]/li[@rel="dc:subject"]/a[@typeof="skos:Concept" and @datatype="" and text()=:term-name]', array( ':node-url' => url('node/' . $node->nid), ':term-name' => $tag2, )); - $this->assertTrue(!empty($term_rdfa_meta), t('Property dc:subject is present for the tag2 field item.')); + $this->assertTrue(!empty($term_rdfa_meta), 'Property dc:subject is present for the tag2 field item.'); } } @@ -228,7 +227,7 @@ function testCRUD() { // Verify loading of a default mapping. $mapping = _rdf_mapping_load('test_entity', 'test_bundle'); - $this->assertTrue(count($mapping), t('Default mapping was found.')); + $this->assertTrue(count($mapping), 'Default mapping was found.'); // Verify saving a mapping. $mapping = array( @@ -245,35 +244,35 @@ ), ), ); - $this->assertTrue(rdf_mapping_save($mapping) === SAVED_NEW, t('Mapping was saved.')); + $this->assertTrue(rdf_mapping_save($mapping) === SAVED_NEW, 'Mapping was saved.'); // Read the raw record from the {rdf_mapping} table. $result = db_query('SELECT * FROM {rdf_mapping} WHERE type = :type AND bundle = :bundle', array(':type' => $mapping['type'], ':bundle' => $mapping['bundle'])); $stored_mapping = $result->fetchAssoc(); $stored_mapping['mapping'] = unserialize($stored_mapping['mapping']); - $this->assertEqual($mapping, $stored_mapping, t('Mapping was stored properly in the {rdf_mapping} table.')); + $this->assertEqual($mapping, $stored_mapping, 'Mapping was stored properly in the {rdf_mapping} table.'); // Verify loading of saved mapping. - $this->assertEqual($mapping['mapping'], _rdf_mapping_load($mapping['type'], $mapping['bundle']), t('Saved mapping loaded successfully.')); + $this->assertEqual($mapping['mapping'], _rdf_mapping_load($mapping['type'], $mapping['bundle']), 'Saved mapping loaded successfully.'); // Verify updating of mapping. $mapping['mapping']['title'] = array( 'predicates' => array('dc2:bar2'), ); - $this->assertTrue(rdf_mapping_save($mapping) === SAVED_UPDATED, t('Mapping was updated.')); + $this->assertTrue(rdf_mapping_save($mapping) === SAVED_UPDATED, 'Mapping was updated.'); // Read the raw record from the {rdf_mapping} table. $result = db_query('SELECT * FROM {rdf_mapping} WHERE type = :type AND bundle = :bundle', array(':type' => $mapping['type'], ':bundle' => $mapping['bundle'])); $stored_mapping = $result->fetchAssoc(); $stored_mapping['mapping'] = unserialize($stored_mapping['mapping']); - $this->assertEqual($mapping, $stored_mapping, t('Updated mapping was stored properly in the {rdf_mapping} table.')); + $this->assertEqual($mapping, $stored_mapping, 'Updated mapping was stored properly in the {rdf_mapping} table.'); // Verify loading of saved mapping. - $this->assertEqual($mapping['mapping'], _rdf_mapping_load($mapping['type'], $mapping['bundle']), t('Saved mapping loaded successfully.')); + $this->assertEqual($mapping['mapping'], _rdf_mapping_load($mapping['type'], $mapping['bundle']), 'Saved mapping loaded successfully.'); // Verify deleting of mapping. - $this->assertTrue(rdf_mapping_delete($mapping['type'], $mapping['bundle']), t('Mapping was deleted.')); - $this->assertFalse(_rdf_mapping_load($mapping['type'], $mapping['bundle']), t('Deleted mapping is no longer found in the database.')); + $this->assertTrue(rdf_mapping_delete($mapping['type'], $mapping['bundle']), 'Mapping was deleted.'); + $this->assertFalse(_rdf_mapping_load($mapping['type'], $mapping['bundle']), 'Deleted mapping is no longer found in the database.'); } } @@ -302,10 +301,10 @@ // 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), t('Property dc:title is present in meta tag.')); - $this->assertTrue(!empty($blog_meta), t('RDF type is present on post. Properties dc:date and dc:created are present on post date.')); + $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.'); } /** @@ -314,16 +313,21 @@ */ 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), t('Property dc:title is present in meta tag.')); - $this->assertTrue(!empty($test_bundle_meta), t('RDF type is present on post. Properties dc:date and dc:created are present on post date.')); + $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.'); } /** @@ -339,10 +343,10 @@ // 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), t('Property dc:title is present in meta tag.')); - $this->assertTrue(!empty($random_bundle_meta), t('RDF type is present on post. Properties dc:date and dc:created are present on post date.')); + $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.'); } /** @@ -364,19 +368,19 @@ $user2_profile_about = $this->xpath('//div[@class="profile" and @typeof="sioc:UserAccount" and @about=:account-uri]', array( ':account-uri' => $account_uri, )); - $this->assertTrue(!empty($user2_profile_about), t('RDFa markup found on user profile page')); + $this->assertTrue(!empty($user2_profile_about), 'RDFa markup found on user profile page'); $user_account_holder = $this->xpath('//meta[contains(@typeof, "foaf:Person") and @about=:person-uri and @resource=:account-uri and contains(@rel, "foaf:account")]', array( ':person-uri' => $person_uri, ':account-uri' => $account_uri, )); - $this->assertTrue(!empty($user_account_holder), t('URI created for account holder and username set on sioc:UserAccount.')); + $this->assertTrue(!empty($user_account_holder), 'URI created for account holder and username set on sioc:UserAccount.'); $user_username = $this->xpath('//meta[@about=:account-uri and contains(@property, "foaf:name") and @content=:username]', array( ':account-uri' => $account_uri, ':username' => $username, )); - $this->assertTrue(!empty($user_username), t('foaf:name set on username.')); + $this->assertTrue(!empty($user_username), 'foaf:name set on username.'); // User 2 creates node. $this->drupalLogin($user2); @@ -385,10 +389,10 @@ $this->drupalGet('node/' . $node->nid); // Ensures the default bundle mapping for user is used on the Authored By // information on the node. - $author_about = $this->xpath('//a[@typeof="sioc:UserAccount" and @about=:account-uri and @property="foaf:name" and contains(@xml:lang, "")]', array( + $author_about = $this->xpath('//a[@typeof="sioc:UserAccount" and @about=:account-uri and @property="foaf:name" and @datatype="" and contains(@xml:lang, "")]', array( ':account-uri' => $account_uri, )); - $this->assertTrue(!empty($author_about), t('RDFa markup found on author information on post. xml:lang on username is set to empty string.')); + $this->assertTrue(!empty($author_about), 'RDFa markup found on author information on post. xml:lang on username is set to empty string.'); } /** @@ -406,7 +410,7 @@ ':term-url' => $term_url, ':term-name' => $term_name, )); - $this->assertTrue(!empty($term_rdfa_meta), t('RDFa markup found on term page.')); + $this->assertTrue(!empty($term_rdfa_meta), 'RDFa markup found on term page.'); } } @@ -437,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); @@ -457,16 +461,14 @@ // 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"]'); - $this->assertTrue(!empty($comment_count_teaser), t('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), t('Empty rel attribute found in comment count link.')); + $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.'); // 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)); - $this->assertTrue(!empty($comment_count_teaser), t('RDFa markup for the number of comments found on full node view.')); + $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.'); } /** @@ -503,22 +505,22 @@ // Tests comment #2 as anonymous user. $this->_testBasicCommentRdfaMarkup($comment2, $anonymous_user); // Tests the RDFa markup for the homepage (specific to anonymous comments). - $comment_homepage = $this->xpath('//div[contains(@class, "comment") and contains(@typeof, "sioct:Comment")]//span[@rel="sioc:has_creator"]/a[contains(@class, "username") and @typeof="sioc:UserAccount" and @property="foaf:name" and @href="http://example.org/" and contains(@rel, "foaf:page")]'); - $this->assertTrue(!empty($comment_homepage), t('RDFa markup for the homepage of anonymous user found.')); + $comment_homepage = $this->xpath('//div[contains(@class, "comment") and contains(@typeof, "sioct:Comment")]//span[@rel="sioc:has_creator"]/a[contains(@class, "username") and @typeof="sioc:UserAccount" and @property="foaf:name" and @datatype="" and @href="http://example.org/" and contains(@rel, "foaf:page")]'); + $this->assertTrue(!empty($comment_homepage), 'RDFa markup for the homepage of anonymous user found.'); // There should be no about attribute on anonymous comments. $comment_homepage = $this->xpath('//div[contains(@class, "comment") and contains(@typeof, "sioct:Comment")]//span[@rel="sioc:has_creator"]/a[@about]'); - $this->assertTrue(empty($comment_homepage), t('No about attribute is present on anonymous user comment.')); + $this->assertTrue(empty($comment_homepage), 'No about attribute is present on anonymous user comment.'); // Tests comment #2 as logged in user. $this->drupalLogin($this->web_user); $this->drupalGet('node/' . $this->node2->nid); $this->_testBasicCommentRdfaMarkup($comment2, $anonymous_user); // Tests the RDFa markup for the homepage (specific to anonymous comments). - $comment_homepage = $this->xpath('//div[contains(@class, "comment") and contains(@typeof, "sioct:Comment")]//span[@rel="sioc:has_creator"]/a[contains(@class, "username") and @typeof="sioc:UserAccount" and @property="foaf:name" and @href="http://example.org/" and contains(@rel, "foaf:page")]'); - $this->assertTrue(!empty($comment_homepage), t("RDFa markup for the homepage of anonymous user found.")); + $comment_homepage = $this->xpath('//div[contains(@class, "comment") and contains(@typeof, "sioct:Comment")]//span[@rel="sioc:has_creator"]/a[contains(@class, "username") and @typeof="sioc:UserAccount" and @property="foaf:name" and @datatype="" and @href="http://example.org/" and contains(@rel, "foaf:page")]'); + $this->assertTrue(!empty($comment_homepage), "RDFa markup for the homepage of anonymous user found."); // There should be no about attribute on anonymous comments. $comment_homepage = $this->xpath('//div[contains(@class, "comment") and contains(@typeof, "sioct:Comment")]//span[@rel="sioc:has_creator"]/a[@about]'); - $this->assertTrue(empty($comment_homepage), t("No about attribute is present on anonymous user comment.")); + $this->assertTrue(empty($comment_homepage), "No about attribute is present on anonymous user comment."); } /** @@ -531,9 +533,9 @@ // Tests the reply_of relationship of a first level comment. $result = $this->xpath("(id('comments')//div[contains(@class,'comment ')])[position()=1]//span[@rel='sioc:reply_of' and @resource=:node]", array(':node' => url("node/{$this->node1->nid}"))); - $this->assertEqual(1, count($result), t('RDFa markup referring to the node is present.')); + $this->assertEqual(1, count($result), 'RDFa markup referring to the node is present.'); $result = $this->xpath("(id('comments')//div[contains(@class,'comment ')])[position()=1]//span[@rel='sioc:reply_of' and @resource=:comment]", array(':comment' => url('comment/1#comment-1'))); - $this->assertFalse($result, t('No RDFa markup referring to the comment itself is present.')); + $this->assertFalse($result, 'No RDFa markup referring to the comment itself is present.'); // Posts a reply to the first comment. $this->drupalGet('comment/reply/' . $this->node1->nid . '/' . $comments[0]->id); @@ -541,9 +543,9 @@ // Tests the reply_of relationship of a second level comment. $result = $this->xpath("(id('comments')//div[contains(@class,'comment ')])[position()=2]//span[@rel='sioc:reply_of' and @resource=:node]", array(':node' => url("node/{$this->node1->nid}"))); - $this->assertEqual(1, count($result), t('RDFa markup referring to the node is present.')); + $this->assertEqual(1, count($result), 'RDFa markup referring to the node is present.'); $result = $this->xpath("(id('comments')//div[contains(@class,'comment ')])[position()=2]//span[@rel='sioc:reply_of' and @resource=:comment]", array(':comment' => url('comment/1', array('fragment' => 'comment-1')))); - $this->assertEqual(1, count($result), t('RDFa markup referring to the parent comment is present.')); + $this->assertEqual(1, count($result), 'RDFa markup referring to the parent comment is present.'); $comments = $this->xpath("(id('comments')//div[contains(@class,'comment ')])[position()=2]"); } @@ -559,17 +561,17 @@ */ function _testBasicCommentRdfaMarkup($comment, $account = array()) { $comment_container = $this->xpath('//div[contains(@class, "comment") and contains(@typeof, "sioct:Comment")]'); - $this->assertTrue(!empty($comment_container), t("Comment RDF type for comment found.")); + $this->assertTrue(!empty($comment_container), "Comment RDF type for comment found."); $comment_title = $this->xpath('//div[contains(@class, "comment") and contains(@typeof, "sioct:Comment")]//h3[@property="dc:title"]'); - $this->assertEqual((string)$comment_title[0]->a, $comment->subject, t("RDFa markup for the comment title found.")); + $this->assertEqual((string)$comment_title[0]->a, $comment->subject, "RDFa markup for the comment title found."); $comment_date = $this->xpath('//div[contains(@class, "comment") and contains(@typeof, "sioct:Comment")]//*[contains(@property, "dc:date") and contains(@property, "dc:created")]'); - $this->assertTrue(!empty($comment_date), t("RDFa markup for the date of the comment found.")); + $this->assertTrue(!empty($comment_date), "RDFa markup for the date of the comment found."); // The author tag can be either a or span - $comment_author = $this->xpath('//div[contains(@class, "comment") and contains(@typeof, "sioct:Comment")]//span[@rel="sioc:has_creator"]/*[contains(@class, "username") and @typeof="sioc:UserAccount" and @property="foaf:name"]'); + $comment_author = $this->xpath('//div[contains(@class, "comment") and contains(@typeof, "sioct:Comment")]//span[@rel="sioc:has_creator"]/*[contains(@class, "username") and @typeof="sioc:UserAccount" and @property="foaf:name" and @datatype=""]'); $name = empty($account["name"]) ? $this->web_user->name : $account["name"] . " (not verified)"; - $this->assertEqual((string)$comment_author[0], $name, t("RDFa markup for the comment author found.")); + $this->assertEqual((string)$comment_author[0], $name, "RDFa markup for the comment author found."); $comment_body = $this->xpath('//div[contains(@class, "comment") and contains(@typeof, "sioct:Comment")]//div[@class="content"]//div[contains(@class, "comment-body")]//div[@property="content:encoded"]'); - $this->assertEqual((string)$comment_body[0]->p, $comment->comment, t("RDFa markup for the comment body found.")); + $this->assertEqual((string)$comment_body[0]->p, $comment->comment, "RDFa markup for the comment body found."); } } @@ -630,35 +632,35 @@ // success of the following tests, but making it explicit will make // debugging easier in case of failure. $tracker_about = $this->xpath('//tr[@about=:url]', array(':url' => $url)); - $this->assertTrue(!empty($tracker_about), t('About attribute found on table row for @user content.', array('@user'=> $user))); + $this->assertTrue(!empty($tracker_about), format_string('About attribute found on table row for @user content.', array('@user'=> $user))); // Tests whether the title has the correct property attribute. $tracker_title = $this->xpath('//tr[@about=:url]/td[@property="dc:title" and @datatype=""]', array(':url' => $url)); - $this->assertTrue(!empty($tracker_title), t('Title property attribute found on @user content.', array('@user'=> $user))); + $this->assertTrue(!empty($tracker_title), format_string('Title property attribute found on @user content.', array('@user'=> $user))); // Tests whether the relationship between the content and user has been set. $tracker_user = $this->xpath('//tr[@about=:url]//td[contains(@rel, "sioc:has_creator")]//*[contains(@typeof, "sioc:UserAccount") and contains(@property, "foaf:name")]', array(':url' => $url)); - $this->assertTrue(!empty($tracker_user), t('Typeof and name property attributes found on @user.', array('@user'=> $user))); + $this->assertTrue(!empty($tracker_user), format_string('Typeof and name property attributes found on @user.', array('@user'=> $user))); // There should be an about attribute on logged in users and no about // attribute for anonymous users. $tracker_user = $this->xpath('//tr[@about=:url]//td[@rel="sioc:has_creator"]/*[@about]', array(':url' => $url)); if ($node->uid == 0) { - $this->assertTrue(empty($tracker_user), t('No about attribute is present on @user.', array('@user'=> $user))); + $this->assertTrue(empty($tracker_user), format_string('No about attribute is present on @user.', array('@user'=> $user))); } elseif ($node->uid > 0) { - $this->assertTrue(!empty($tracker_user), t('About attribute is present on @user.', array('@user'=> $user))); + $this->assertTrue(!empty($tracker_user), format_string('About attribute is present on @user.', array('@user'=> $user))); } // Tests whether the property has been set for number of comments. $tracker_replies = $this->xpath('//tr[@about=:url]//td[contains(@property, "sioc:num_replies") and contains(@content, "0") and @datatype="xsd:integer"]', array(':url' => $url)); - $this->assertTrue($tracker_replies, t('Num replies property and content attributes found on @user content.', array('@user'=> $user))); + $this->assertTrue($tracker_replies, format_string('Num replies property and content attributes found on @user content.', array('@user'=> $user))); // Tests that the appropriate RDFa markup to annotate the latest activity // date has been added to the tracker output before comments have been // posted, meaning the latest activity reflects changes to the node itself. $isoDate = date('c', $node->changed); $tracker_activity = $this->xpath('//tr[@about=:url]//td[contains(@property, "dc:modified") and contains(@property, "sioc:last_activity_date") and contains(@datatype, "xsd:dateTime") and @content=:date]', array(':url' => $url, ':date' => $isoDate)); - $this->assertTrue(!empty($tracker_activity), t('Latest activity date and changed properties found when there are no comments on @user content. Latest activity date content is correct.', array('@user'=> $user))); + $this->assertTrue(!empty($tracker_activity), format_string('Latest activity date and changed properties found when there are no comments on @user content. Latest activity date content is correct.', array('@user'=> $user))); // Tests that the appropriate RDFa markup to annotate the latest activity // date has been added to the tracker output after a comment is posted. @@ -671,7 +673,7 @@ // Tests whether the property has been set for number of comments. $tracker_replies = $this->xpath('//tr[@about=:url]//td[contains(@property, "sioc:num_replies") and contains(@content, "1") and @datatype="xsd:integer"]', array(':url' => $url)); - $this->assertTrue($tracker_replies, t('Num replies property and content attributes found on @user content.', array('@user'=> $user))); + $this->assertTrue($tracker_replies, format_string('Num replies property and content attributes found on @user content.', array('@user'=> $user))); // Need to query database directly to obtain last_activity_date because // it cannot be accessed via node_load(). @@ -681,7 +683,7 @@ } $isoDate = date('c', $expected_last_activity_date); $tracker_activity = $this->xpath('//tr[@about=:url]//td[@property="sioc:last_activity_date" and @datatype="xsd:dateTime" and @content=:date]', array(':url' => $url, ':date' => $isoDate)); - $this->assertTrue(!empty($tracker_activity), t('Latest activity date found when there are comments on @user content. Latest activity date content is correct.', array('@user'=> $user))); + $this->assertTrue(!empty($tracker_activity), format_string('Latest activity date found when there are comments on @user content. Latest activity date content is correct.', array('@user'=> $user))); } } @@ -708,9 +710,9 @@ // Get all RDF namespaces. $ns = rdf_get_namespaces(); - $this->assertEqual($ns['rdfs'], 'http://www.w3.org/2000/01/rdf-schema#', t('A prefix declared once is included.')); - $this->assertEqual($ns['foaf'], 'http://xmlns.com/foaf/0.1/', t('The same prefix declared in several implementations of hook_rdf_namespaces() is valid as long as all the namespaces are the same.')); - $this->assertEqual($ns['foaf1'], 'http://xmlns.com/foaf/0.1/', t('Two prefixes can be assigned the same namespace.')); - $this->assertTrue(!isset($ns['dc']), t('A prefix with conflicting namespaces is discarded.')); + $this->assertEqual($ns['rdfs'], 'http://www.w3.org/2000/01/rdf-schema#', 'A prefix declared once is included.'); + $this->assertEqual($ns['foaf'], 'http://xmlns.com/foaf/0.1/', 'The same prefix declared in several implementations of hook_rdf_namespaces() is valid as long as all the namespaces are the same.'); + $this->assertEqual($ns['foaf1'], 'http://xmlns.com/foaf/0.1/', 'Two prefixes can be assigned the same namespace.'); + $this->assertTrue(!isset($ns['dc']), 'A prefix with conflicting namespaces is discarded.'); } } diff -Naur drupal-7.0/modules/rdf/tests/rdf_test.info drupal-7.66/modules/rdf/tests/rdf_test.info --- drupal-7.0/modules/rdf/tests/rdf_test.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/rdf/tests/rdf_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,13 +1,12 @@ -; $Id: rdf_test.info,v 1.2 2010/12/20 19:59:42 webchick Exp $ name = "RDF module tests" description = "Support module for RDF module testing." package = Testing version = VERSION core = 7.x hidden = TRUE +dependencies[] = blog -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/rdf/tests/rdf_test.install drupal-7.66/modules/rdf/tests/rdf_test.install --- drupal-7.0/modules/rdf/tests/rdf_test.install 2009-12-04 17:49:47.000000000 +0100 +++ drupal-7.66/modules/rdf/tests/rdf_test.install 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: rdf_test.install,v 1.3 2009/12/04 16:49:47 dries Exp $ /** * @file diff -Naur drupal-7.0/modules/rdf/tests/rdf_test.module drupal-7.66/modules/rdf/tests/rdf_test.module --- drupal-7.0/modules/rdf/tests/rdf_test.module 2010-04-22 23:41:09.000000000 +0200 +++ drupal-7.66/modules/rdf/tests/rdf_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: rdf_test.module,v 1.4 2010/04/22 21:41:09 webchick Exp $ /** * @file diff -Naur drupal-7.0/modules/search/search-block-form.tpl.php drupal-7.66/modules/search/search-block-form.tpl.php --- drupal-7.0/modules/search/search-block-form.tpl.php 2011-01-03 01:17:55.000000000 +0100 +++ drupal-7.66/modules/search/search-block-form.tpl.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: search-block-form.tpl.php,v 1.6 2011/01/03 00:17:55 webchick Exp $ /** * @file @@ -31,7 +30,7 @@ */ ?> <div class="container-inline"> - <?php if (empty($variables['form']['#block']->subject)) : ?> + <?php if (empty($variables['form']['#block']->subject)): ?> <h2 class="element-invisible"><?php print t('Search form'); ?></h2> <?php endif; ?> <?php print $search_form; ?> diff -Naur drupal-7.0/modules/search/search-result.tpl.php drupal-7.66/modules/search/search-result.tpl.php --- drupal-7.0/modules/search/search-result.tpl.php 2010-11-21 21:36:36.000000000 +0100 +++ drupal-7.66/modules/search/search-result.tpl.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: search-result.tpl.php,v 1.9 2010/11/21 20:36:36 dries Exp $ /** * @file @@ -26,14 +25,12 @@ * 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. * - $info_split['comment']: Number of comments output as "% comments", % * being the count. Depends on comment.module. - * - $info_split['upload']: Number of attachments output as "% attachments", % - * being the count. Depends on upload.module. * * Other variables: * - $classes_array: Array of HTML class attribute values. It is flattened @@ -48,7 +45,7 @@ * for its existence before printing. The default keys of 'type', 'user' and * 'date' always exist for node searches. Modules may provide other data. * @code - * <?php if (isset($info_split['comment'])) : ?> + * <?php if (isset($info_split['comment'])): ?> * <span class="info-comment"> * <?php print $info_split['comment']; ?> * </span> @@ -63,6 +60,8 @@ * @see template_preprocess() * @see template_preprocess_search_result() * @see template_process() + * + * @ingroup themeable */ ?> <li class="<?php print $classes; ?>"<?php print $attributes; ?>> @@ -72,10 +71,10 @@ </h3> <?php print render($title_suffix); ?> <div class="search-snippet-info"> - <?php if ($snippet) : ?> + <?php if ($snippet): ?> <p class="search-snippet"<?php print $content_attributes; ?>><?php print $snippet; ?></p> <?php endif; ?> - <?php if ($info) : ?> + <?php if ($info): ?> <p class="search-info"><?php print $info; ?></p> <?php endif; ?> </div> diff -Naur drupal-7.0/modules/search/search-results.tpl.php drupal-7.66/modules/search/search-results.tpl.php --- drupal-7.0/modules/search/search-results.tpl.php 2010-08-18 20:40:50.000000000 +0200 +++ drupal-7.66/modules/search/search-results.tpl.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: search-results.tpl.php,v 1.7 2010/08/18 18:40:50 dries Exp $ /** * @file @@ -20,9 +19,11 @@ * * * @see template_preprocess_search_results() + * + * @ingroup themeable */ ?> -<?php if ($search_results) : ?> +<?php if ($search_results): ?> <h2><?php print t('Search results');?></h2> <ol class="search-results <?php print $module; ?>-results"> <?php print $search_results; ?> diff -Naur drupal-7.0/modules/search/search-rtl.css drupal-7.66/modules/search/search-rtl.css --- drupal-7.0/modules/search/search-rtl.css 2010-05-29 09:50:33.000000000 +0200 +++ drupal-7.66/modules/search/search-rtl.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -/* $Id: search-rtl.css,v 1.5 2010/05/29 07:50:33 dries Exp $ */ .search-advanced .criterion { float: right; diff -Naur drupal-7.0/modules/search/search.admin.inc drupal-7.66/modules/search/search.admin.inc --- drupal-7.0/modules/search/search.admin.inc 2010-10-20 03:31:07.000000000 +0200 +++ drupal-7.66/modules/search/search.admin.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: search.admin.inc,v 1.19 2010/10/20 01:31:07 dries Exp $ /** * @file @@ -11,7 +10,7 @@ */ function search_reindex_confirm() { return confirm_form(array(), t('Are you sure you want to re-index the site?'), - 'admin/config/search/settings', t(' The search index is not cleared but systematically updated to reflect the new settings. Searching will continue to work but new content won\'t be indexed until all existing content has been re-indexed. This action cannot be undone.'), t('Re-index site'), t('Cancel')); + 'admin/config/search/settings', t('The search index is not cleared but systematically updated to reflect the new settings. Searching will continue to work but new content won\'t be indexed until all existing content has been re-indexed. This action cannot be undone.'), t('Re-index site'), t('Cancel')); } /** @@ -32,18 +31,12 @@ function _search_get_module_names() { $search_info = search_get_info(TRUE); - $modules = db_select('system', 's') - ->fields('s', array('name', 'info')) - ->condition('s.status', 1) - ->condition('s.type', 'module') - ->condition('s.name', array_keys($search_info), 'IN') - ->orderBy('s.name') - ->execute(); + $system_info = system_get_info('module'); $names = array(); - foreach ($modules as $item) { - $info = unserialize($item->info); - $names[$item->name] = $info['name']; + foreach ($search_info as $module => $info) { + $names[$module] = $system_info[$module]['name']; } + asort($names, SORT_STRING); return $names; } @@ -102,7 +95,8 @@ '#default_value' => variable_get('minimum_word_size', 3), '#size' => 5, '#maxlength' => 3, - '#description' => t('The number of characters a word has to be to be indexed. A lower setting means better search result ranking, but also a larger database. Each search query must contain at least one keyword that is this size (or longer).') + '#description' => t('The number of characters a word has to be to be indexed. A lower setting means better search result ranking, but also a larger database. Each search query must contain at least one keyword that is this size (or longer).'), + '#element_validate' => array('element_validate_integer_positive'), ); $form['indexing_settings']['overlap_cjk'] = array( '#type' => 'checkbox', @@ -113,23 +107,34 @@ $form['active'] = array( '#type' => 'fieldset', - '#title' => t('Active search modules ') + '#title' => t('Active search modules') ); + $module_options = _search_get_module_names(); $form['active']['search_active_modules'] = array( '#type' => 'checkboxes', '#title' => t('Active modules'), '#title_display' => 'invisible', '#default_value' => variable_get('search_active_modules', array('node', 'user')), - '#options' => _search_get_module_names(), + '#options' => $module_options, '#description' => t('Choose which search modules are active from the available modules.') ); $form['active']['search_default_module'] = array( '#title' => t('Default search module'), '#type' => 'radios', '#default_value' => variable_get('search_default_module', 'node'), - '#options' => _search_get_module_names(), + '#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'; @@ -149,7 +154,7 @@ */ function search_admin_settings_validate($form, &$form_state) { // Check whether we selected a valid default. - if ($form_state['clicked_button']['#value'] != t('Reset to defaults')) { + if ($form_state['triggering_element']['#value'] != t('Reset to defaults')) { $new_modules = array_filter($form_state['values']['search_active_modules']); $default = $form_state['values']['search_default_module']; if (!in_array($default, $new_modules, TRUE)) { @@ -170,7 +175,7 @@ } $current_modules = variable_get('search_active_modules', array('node', 'user')); // Check whether we are resetting the values. - if ($form_state['clicked_button']['#value'] == t('Reset to defaults')) { + if ($form_state['triggering_element']['#value'] == t('Reset to defaults')) { $new_modules = array('node', 'user'); } else { diff -Naur drupal-7.0/modules/search/search.api.php drupal-7.66/modules/search/search.api.php --- drupal-7.0/modules/search/search.api.php 2010-11-21 21:36:36.000000000 +0100 +++ drupal-7.66/modules/search/search.api.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: search.api.php,v 1.33 2010/11/21 20:36:36 dries Exp $ /** * @file @@ -23,62 +22,35 @@ * on your module's type of content. If you want to have your content * indexed in the standard search index, your module should also implement * hook_update_index(). If your search type has settings, you can implement - * hook_search_admin() to add them to the search settings page. You can also - * alter the display of your module's search results by implementing - * hook_search_page(). You can use hook_form_FORM_ID_alter(), with - * FORM_ID set to 'search', to add fields to the search form (see - * node_form_search_form_alter() for an example). You can use - * hook_search_access() to limit access to searching, and hook_search_page() to - * override how search results are displayed. + * hook_search_admin() to add them to the search settings page. You can use + * hook_form_FORM_ID_alter(), with FORM_ID set to 'search_form', to add fields + * to the search form (see node_form_search_form_alter() for an example). + * You can use hook_search_access() to limit access to searching, + * and hook_search_page() to override how search results are displayed. * * @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. - * - 'path': Path component after 'search/' for searching with this module. + * - 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': Name of a callback function that is invoked by - * search_view() to get an array of additional search conditions to pass to - * search_data(). For example, a search module may get additional keywords, - * filters, or modifiers for the search from the query string. Sample - * callback function: sample_search_conditions_callback(). + * - conditions_callback: An implementation of callback_search_conditions(). * * @ingroup search */ function hook_search_info() { + // Make the title translatable. + t('Content'); + return array( 'title' => 'Content', 'path' => 'node', - 'conditions_callback' => 'sample_search_conditions_callback', + 'conditions_callback' => 'callback_search_conditions', ); } /** - * An example conditions callback function for search. - * - * This example pulls additional search keywords out of the $_REQUEST variable, - * (i.e. from the query string of the request). The conditions may also be - * generated internally - for example based on a module's settings. - * - * @see hook_search_info() - * @ingroup search - */ -function sample_search_conditions_callback($keys) { - $conditions = array(); - - if (!empty($_REQUEST['keys'])) { - $conditions['keys'] = $_REQUEST['keys']; - } - if (!empty($_REQUEST['sample_search_keys'])) { - $conditions['sample_search_keys'] = $_REQUEST['sample_search_keys']; - } - if ($force_keys = variable_get('sample_search_force_keywords', '')) { - $conditions['sample_search_force_keywords'] = $force_keys; - } - return $conditions; -} - -/** * Define access to a custom search routine. * * This hook allows a module to define permissions for a search tab. @@ -108,6 +80,10 @@ /** * Report the status of indexing. * + * The core search module only invokes this hook on active modules. + * Implementing modules do not need to check whether they are active when + * calculating their return values. + * * @return * An associative array with the key-value pairs: * - 'remaining': The number of items left to index. @@ -169,7 +145,7 @@ * parameters to the search expression. * * See node_search_execute() for an example of a module that uses the search - * index, and user_search_execute() for an example that doesn't ues the search + * index, and user_search_execute() for an example that doesn't use the search * index. * * @param $keys @@ -251,22 +227,23 @@ /** * Override the rendering of search results. * - * A module that implements hook_search_info() to define a type of search - * may implement this hook in order to override the default theming of - * its search results, which is otherwise themed using theme('search_results'). + * A module that implements hook_search_info() to define a type of search may + * implement this hook in order to override the default theming of its search + * results, which is otherwise themed using theme('search_results'). * * Note that by default, theme('search_results') and theme('search_result') * work together to create an ordered list (OL). So your hook_search_page() * implementation should probably do this as well. * - * @see search-result.tpl.php, search-results.tpl.php - * * @param $results * An array of search results. * * @return - * A renderable array, which will render the formatted search results with - * a pager included. + * A renderable array, which will render the formatted search results with a + * pager included. + * + * @see search-result.tpl.php + * @see search-results.tpl.php */ function hook_search_page($results) { $output['prefix']['#markup'] = '<ol class="search-results">'; @@ -363,3 +340,41 @@ /** * @} End of "addtogroup hooks". */ + +/** + * Provide search query conditions. + * + * Callback for hook_search_info(). + * + * This callback is invoked by search_view() to get an array of additional + * search conditions to pass to search_data(). For example, a search module + * may get additional keywords, filters, or modifiers for the search from + * the query string. + * + * This example pulls additional search keywords out of the $_REQUEST variable, + * (i.e. from the query string of the request). The conditions may also be + * generated internally - for example based on a module's settings. + * + * @param $keys + * The search keywords string. + * + * @return + * An array of additional conditions, such as filters. + * + * @ingroup callbacks + * @ingroup search + */ +function callback_search_conditions($keys) { + $conditions = array(); + + if (!empty($_REQUEST['keys'])) { + $conditions['keys'] = $_REQUEST['keys']; + } + if (!empty($_REQUEST['sample_search_keys'])) { + $conditions['sample_search_keys'] = $_REQUEST['sample_search_keys']; + } + if ($force_keys = config('sample_search.settings')->get('force_keywords')) { + $conditions['sample_search_force_keywords'] = $force_keys; + } + return $conditions; +} diff -Naur drupal-7.0/modules/search/search.css drupal-7.66/modules/search/search.css --- drupal-7.0/modules/search/search.css 2010-05-29 09:50:33.000000000 +0200 +++ drupal-7.66/modules/search/search.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -/* $Id: search.css,v 1.5 2010/05/29 07:50:33 dries Exp $ */ .search-form { margin-bottom: 1em; diff -Naur drupal-7.0/modules/search/search.extender.inc drupal-7.66/modules/search/search.extender.inc --- drupal-7.0/modules/search/search.extender.inc 2010-11-22 08:32:12.000000000 +0100 +++ drupal-7.66/modules/search/search.extender.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: search.extender.inc,v 1.10 2010/11/22 07:32:12 webchick Exp $ /** * @file @@ -106,6 +105,8 @@ * Stores score expressions. * * @var array + * + * @see addScore() */ protected $scores = array(); @@ -117,13 +118,23 @@ protected $scoresArguments = array(); /** - * Total value of all the multipliers. + * Stores multipliers for score expressions. * * @var array */ protected $multiply = array(); /** + * Whether or not search expressions were ignored. + * + * The maximum number of AND/OR combinations exceeded can be configured to + * avoid Denial-of-Service attacks. Expressions beyond the limit are ignored. + * + * @var boolean + */ + protected $expressionsIgnored = FALSE; + + /** * Sets up the search query expression. * * @param $query @@ -138,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; } @@ -184,7 +206,17 @@ // Classify tokens. $or = FALSE; $warning = ''; + $limit_combinations = variable_get('search_and_or_limit', 7); + // The first search expression does not count as AND. + $and_count = -1; + $or_count = 0; foreach ($keywords as $match) { + if ($or_count && $and_count + $or_count >= $limit_combinations) { + // Ignore all further search expressions to prevent Denial-of-Service + // attacks using a high number of AND/OR combinations. + $this->expressionsIgnored = TRUE; + break; + } $phrase = FALSE; // Strip off phrase quotes. if ($match[2]{0} == '"') { @@ -213,6 +245,7 @@ } $this->keys['positive'][] = $last; $or = TRUE; + $or_count++; continue; } // AND operator: implied, so just ignore it. @@ -232,6 +265,7 @@ } else { $this->keys['positive'] = array_merge($this->keys['positive'], $words); + $and_count++; } } $or = FALSE; @@ -324,6 +358,9 @@ form_set_error('keys', format_plural(variable_get('minimum_word_size', 3), 'You must include at least one positive keyword with 1 character or more.', 'You must include at least one positive keyword with @count characters or more.')); return FALSE; } + if ($this->expressionsIgnored) { + drupal_set_message(t('Your search used too many AND/OR expressions. Only the first @count terms were included in this search.', array('@count' => variable_get('search_and_or_limit', 7))), 'warning'); + } $this->executedFirstPass = TRUE; if (!empty($this->words)) { @@ -367,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; } @@ -422,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) { @@ -432,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.0/modules/search/search.info drupal-7.66/modules/search/search.info --- drupal-7.0/modules/search/search.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/search/search.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: search.info,v 1.13 2010/12/20 19:59:43 webchick Exp $ name = Search description = Enables site-wide keyword searching. package = Core @@ -9,8 +8,7 @@ configure = admin/config/search/settings stylesheets[all][] = search.css -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/search/search.install drupal-7.66/modules/search/search.install --- drupal-7.0/modules/search/search.install 2010-08-22 15:55:53.000000000 +0200 +++ drupal-7.66/modules/search/search.install 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: search.install,v 1.29 2010/08/22 13:55:53 dries Exp $ /** * @file @@ -13,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.0/modules/search/search.module drupal-7.66/modules/search/search.module --- drupal-7.0/modules/search/search.module 2011-01-04 06:31:09.000000000 +0100 +++ drupal-7.66/modules/search/search.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: search.module,v 1.371 2011/01/04 05:31:09 webchick Exp $ /** * @file @@ -69,7 +68,7 @@ case 'admin/help#search': $output = ''; $output .= '<h3>' . t('About') . '</h3>'; - $output .= '<p>' . t('The Search module provides the ability to index and search for content by exact keywords, and for users by username or e-mail. For more information, see the online handbook entry for <a href="@search-module">Search module</a>.', array('@search-module' => 'http://drupal.org/handbook/modules/search/', '@search' => url('search'))) . '</p>'; + $output .= '<p>' . t('The Search module provides the ability to index and search for content by exact keywords, and for users by username or e-mail. For more information, see the online handbook entry for <a href="@search-module">Search module</a>.', array('@search-module' => 'http://drupal.org/documentation/modules/search/', '@search' => url('search'))) . '</p>'; $output .= '<h3>' . t('Uses') . '</h3>'; $output .= '<dl>'; $output .= '<dt>' . t('Searching content and users') . '</dt>'; @@ -1005,7 +1004,6 @@ * * @ingroup forms * @see search_box_form_submit() - * @see search-theme-form.tpl.php * @see search-block-form.tpl.php */ function search_box($form, &$form_state, $form_id) { @@ -1069,7 +1067,7 @@ $hidden = array(); // Provide variables named after form keys so themers can print each element independently. foreach (element_children($variables['form']) as $key) { - $type = $variables['form'][$key]['#type']; + $type = isset($variables['form'][$key]['#type']) ? $variables['form'][$key]['#type'] : ''; if ($type == 'hidden' || $type == 'token') { $hidden[] = drupal_render($variables['form'][$key]); } @@ -1190,7 +1188,7 @@ if (($s = strrpos($end, ' ')) !== FALSE) { // Account for the added spaces. $q = max($q - 1, 0); - $s = min($s, drupal_strlen($end) - 1); + $s = min($s, strlen($end) - 1); $ranges[$q] = $p + $s; $length += $p + $s - $q; $included[$key] = $p + 1; @@ -1295,6 +1293,11 @@ $simplified_key = search_simplify($key); $simplified_text = search_simplify($text); + // Return immediately if simplified key or text are empty. + if (!$simplified_key || !$simplified_text) { + return FALSE; + } + // Check if we have a match after simplification in the text. if (!preg_match('/' . $boundary . $simplified_key . $boundary . '/iu', $simplified_text, $match, PREG_OFFSET_CAPTURE, $offset)) { return FALSE; diff -Naur drupal-7.0/modules/search/search.pages.inc drupal-7.66/modules/search/search.pages.inc --- drupal-7.0/modules/search/search.pages.inc 2010-11-21 21:36:36.000000000 +0100 +++ drupal-7.66/modules/search/search.pages.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: search.pages.inc,v 1.25 2010/11/21 20:36:36 dries Exp $ /** * @file @@ -16,7 +15,6 @@ */ function search_view($module = NULL, $keys = '') { $info = FALSE; - $redirect = FALSE; $keys = trim($keys); // Also try to pull search keywords out of the $_REQUEST variable to // support old GET format of searches for existing links. @@ -51,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. @@ -59,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); } @@ -157,5 +156,4 @@ } $form_state['redirect'] = $form_state['action'] . '/' . $keys; - return; } diff -Naur drupal-7.0/modules/search/search.test drupal-7.66/modules/search/search.test --- drupal-7.0/modules/search/search.test 2011-01-04 06:31:09.000000000 +0100 +++ drupal-7.66/modules/search/search.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,9 @@ <?php -// $Id: search.test,v 1.86 2011/01/04 05:31:09 webchick Exp $ + +/** + * @file + * Tests for search.module. + */ // The search index can contain different types of content. Typically the type is 'node'. // Here we test with _test_ and _test2_ as the type. @@ -7,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( @@ -271,7 +278,7 @@ $edit = array(); $edit['keys'] = 'bike shed ' . $this->randomName(); $this->drupalPost('search/node', $edit, t('Search')); - $this->assertText(t('Consider loosening your query with OR. bike OR shed will often show more results than bike shed.'), t('Help text is displayed when search returns no results.')); + $this->assertText(t('Consider loosening your query with OR. bike OR shed will often show more results than bike shed.'), 'Help text is displayed when search returns no results.'); $this->assertText(t('Search')); $this->assertTitle($title, 'Search page title is correct'); @@ -286,9 +293,26 @@ $this->drupalGet('search/node/' . $arg); $input = $this->xpath("//input[@id='edit-keys' and @value='{$arg}']"); $this->assertFalse(empty($input), 'Search keys with a / are correctly set as the default value in the search box.'); + + // Test a search input exceeding the limit of AND/OR combinations to test + // the Denial-of-Service protection. + $limit = variable_get('search_and_or_limit', 7); + $keys = array(); + for ($i = 0; $i < $limit + 1; $i++) { + $keys[] = $this->randomName(3); + if ($i % 2 == 0) { + $keys[] = 'OR'; + } + } + $edit['keys'] = implode(' ', $keys); + $this->drupalPost('search/node', $edit, t('Search')); + $this->assertRaw(t('Your search used too many AND/OR expressions. Only the first @count terms were included in this search.', array('@count' => $limit))); } } +/** + * Indexes content and tests the advanced search form. + */ class SearchAdvancedSearchForm extends DrupalWebTestCase { protected $node; @@ -324,34 +348,37 @@ * Test using the advanced search form to limit search to nodes of type "Basic page". */ function testNodeType() { - $this->assertTrue($this->node->type == 'page', t('Node type is Basic page.')); + $this->assertTrue($this->node->type == 'page', 'Node type is Basic page.'); // Assert that the dummy title doesn't equal the real title. $dummy_title = 'Lorem ipsum'; - $this->assertNotEqual($dummy_title, $this->node->title, t("Dummy title doens't equal node title")); + $this->assertNotEqual($dummy_title, $this->node->title, "Dummy title doesn't equal node title"); // Search for the dummy title with a GET query. $this->drupalGet('search/node/' . $dummy_title); - $this->assertNoText($this->node->title, t('Basic page node is not found with dummy title.')); + $this->assertNoText($this->node->title, 'Basic page node is not found with dummy title.'); // Search for the title of the node with a GET query. $this->drupalGet('search/node/' . $this->node->title); - $this->assertText($this->node->title, t('Basic page node is found with GET query.')); + $this->assertText($this->node->title, 'Basic page node is found with GET query.'); // Search for the title of the node with a POST query. $edit = array('or' => $this->node->title); $this->drupalPost('search/node', $edit, t('Advanced search')); - $this->assertText($this->node->title, t('Basic page node is found with POST query.')); + $this->assertText($this->node->title, 'Basic page node is found with POST query.'); // Advanced search type option. $this->drupalPost('search/node', array_merge($edit, array('type[page]' => 'page')), t('Advanced search')); - $this->assertText($this->node->title, t('Basic page node is found with POST query and type:page.')); + $this->assertText($this->node->title, 'Basic page node is found with POST query and type:page.'); $this->drupalPost('search/node', array_merge($edit, array('type[article]' => 'article')), t('Advanced search')); - $this->assertText('bike shed', t('Article node is not found with POST query and type:article.')); + $this->assertText('bike shed', 'Article node is not found with POST query and type:article.'); } } +/** + * Indexes content and tests ranking factors. + */ class SearchRankingTestCase extends DrupalWebTestCase { public static function getInfo() { return array( @@ -446,7 +473,7 @@ function testHTMLRankings() { // Login with sufficient privileges. $this->drupalLogin($this->drupalCreateUser(array('create page content'))); - + // Test HTML tags with different weights. $sorted_tags = array('h1', 'h2', 'h3', 'h4', 'a', 'h5', 'h6', 'notag'); $shuffled_tags = $sorted_tags; @@ -478,7 +505,7 @@ // Refresh variables after the treatment. $this->refreshVariables(); - + // Disable all other rankings. $node_ranks = array('sticky', 'promote', 'recent', 'comments', 'views'); foreach ($node_ranks as $node_rank) { @@ -516,7 +543,7 @@ // Assert the results. $this->assertEqual($set[0]['node']->nid, $node->nid, 'Search tag ranking for "<' . $tag . '>" order.'); - + // Delete node so it doesn't show up in subsequent search results. node_delete($node->nid); } @@ -562,6 +589,9 @@ } } +/** + * Tests the rendering of the search block. + */ class SearchBlockTestCase extends DrupalWebTestCase { public static function getInfo() { return array( @@ -580,15 +610,15 @@ } function testSearchFormBlock() { - // Set block title to confirm that the interface is availble. + // Set block title to confirm that the interface is available. $this->drupalPost('admin/structure/block/manage/search/form/configure', array('title' => $this->randomName(8)), 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.'); - // Set the block to a region to confirm block is availble. + // Set the block to a region to confirm block is available. $edit = array(); $edit['blocks[search_form][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.'); } /** @@ -622,7 +652,7 @@ $this->assertEqual( $this->getUrl(), url('search/node/' . $terms['search_block_form'], array('absolute' => TRUE)), - t('Redirected to correct url.') + 'Redirected to correct url.' ); // Test an empty search via the block form, from the front page. @@ -634,8 +664,26 @@ $this->assertEqual( $this->getUrl(), url('search/node/', array('absolute' => TRUE)), - t('Redirected to correct url.') + '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'); } } @@ -709,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', ); } @@ -772,20 +820,20 @@ 'search_block_form' => "'" . $edit_comment['subject'] . "'", ); $this->drupalPost('', $edit, t('Search')); - $this->assertText($node->title, t('Node found in search results.')); - $this->assertText($edit_comment['subject'], t('Comment subject found in search results.')); + $this->assertText($node->title, 'Node found in search results.'); + $this->assertText($edit_comment['subject'], 'Comment subject found in search results.'); // Search for the comment body. $edit = array( 'search_block_form' => "'" . $comment_body . "'", ); $this->drupalPost('', $edit, t('Search')); - $this->assertText($node->title, t('Node found in search results.')); + $this->assertText($node->title, 'Node found in search results.'); // Verify that comment is rendered using proper format. - $this->assertText($comment_body, t('Comment body text found in search results.')); - $this->assertNoRaw(t('n/a'), t('HTML in comment body is not hidden.')); - $this->assertNoRaw(check_plain($edit_comment['comment_body[' . LANGUAGE_NONE . '][0][value]']), t('HTML in comment body is not escaped.')); + $this->assertText($comment_body, 'Comment body text found in search results.'); + $this->assertNoRaw(t('n/a'), 'HTML in comment body is not hidden.'); + $this->assertNoRaw(check_plain($edit_comment['comment_body[' . LANGUAGE_NONE . '][0][value]']), 'HTML in comment body is not escaped.'); // Hide comments. $this->drupalLogin($this->admin_user); @@ -798,7 +846,7 @@ // Search for $title. $this->drupalPost('', $edit, t('Search')); - $this->assertNoText($comment_body, t('Comment body text not found in search results.')); + $this->assertNoText($comment_body, 'Comment body text not found in search results.'); } /** @@ -857,7 +905,7 @@ $this->setRolePermissions(DRUPAL_AUTHENTICATED_RID, TRUE, TRUE); $this->setRolePermissions($this->admin_role, TRUE, FALSE); $this->checkCommentAccess('Admin user has access comments permission and no search permission, but comments should be indexed because admin user inherits authenticated user\'s permission to search', TRUE); - + } /** @@ -908,7 +956,7 @@ // Verify that if you view the node on its own page, 'add new comment' // is there. $this->drupalGet('node/' . $node->nid); - $this->assertText(t('Add new comment'), t('Add new comment appears on node page')); + $this->assertText(t('Add new comment'), 'Add new comment appears on node page'); // Run cron to index this page. $this->drupalLogout(); @@ -917,13 +965,13 @@ // Search for 'comment'. Should be no results. $this->drupalLogin($user); $this->drupalPost('search/node', array('keys' => 'comment'), t('Search')); - $this->assertText(t('Your search yielded no results'), t('No results searching for the word comment')); + $this->assertText(t('Your search yielded no results'), 'No results searching for the word comment'); // Search for the node title. Should be found, and 'Add new comment' should // not be part of the search snippet. $this->drupalPost('search/node', array('keys' => 'short'), t('Search')); - $this->assertText($node->title, t('Search for keyword worked')); - $this->assertNoText(t('Add new comment'), t('Add new comment does not appear on search results page')); + $this->assertText($node->title, 'Search for keyword worked'); + $this->assertNoText(t('Add new comment'), 'Add new comment does not appear on search results page'); } } @@ -1056,8 +1104,8 @@ // Test comment count display for nodes with comment status set to Open $this->drupalPost('', $edit, t('Search')); - $this->assertText(t('0 comments'), t('Empty comment count displays for nodes with comment status set to Open')); - $this->assertText(t('1 comment'), t('Non-empty comment count displays for nodes with comment status set to Open')); + $this->assertText(t('0 comments'), 'Empty comment count displays for nodes with comment status set to Open'); + $this->assertText(t('1 comment'), 'Non-empty comment count displays for nodes with comment status set to Open'); // Test comment count display for nodes with comment status set to Closed $this->searchable_nodes['0 comments']->comment = COMMENT_NODE_CLOSED; @@ -1066,8 +1114,8 @@ node_save($this->searchable_nodes['1 comment']); $this->drupalPost('', $edit, t('Search')); - $this->assertNoText(t('0 comments'), t('Empty comment count does not display for nodes with comment status set to Closed')); - $this->assertText(t('1 comment'), t('Non-empty comment count displays for nodes with comment status set to Closed')); + $this->assertNoText(t('0 comments'), 'Empty comment count does not display for nodes with comment status set to Closed'); + $this->assertText(t('1 comment'), 'Non-empty comment count displays for nodes with comment status set to Closed'); // Test comment count display for nodes with comment status set to Hidden $this->searchable_nodes['0 comments']->comment = COMMENT_NODE_HIDDEN; @@ -1076,8 +1124,8 @@ node_save($this->searchable_nodes['1 comment']); $this->drupalPost('', $edit, t('Search')); - $this->assertNoText(t('0 comments'), t('Empty comment count does not display for nodes with comment status set to Hidden')); - $this->assertNoText(t('1 comment'), t('Non-empty comment count does not display for nodes with comment status set to Hidden')); + $this->assertNoText(t('0 comments'), 'Empty comment count does not display for nodes with comment status set to Hidden'); + $this->assertNoText(t('1 comment'), 'Non-empty comment count does not display for nodes with comment status set to Hidden'); } } @@ -1142,7 +1190,7 @@ for ($i = 0; $i < 32; $i++) { $string .= chr($i); } - $this->assertIdentical(' ', search_simplify($string), t('Search simplify works for ASCII control characters.')); + $this->assertIdentical(' ', search_simplify($string), 'Search simplify works for ASCII control characters.'); } /** @@ -1298,7 +1346,7 @@ $this->drupalPost('search/node', array('keys' => $number), t('Search')); - $this->assertText($node->title, $type . ': node title shown (search found the node) in search for number ' . $number); + $this->assertText($node->title, format_string('%type: node title shown (search found the node) in search for number %number.', array('%type' => $type, '%number' => $number))); } } } @@ -1366,7 +1414,7 @@ $this->drupalPost('search/node', array('keys' => 'foo'), t('Search')); - $this->assertNoText($node->title, $i . ': node title not shown in dummy search'); + $this->assertNoText($node->title, format_string('%number: node title not shown in dummy search', array('%number' => $i))); // Now verify that we can find node i by searching for any of the // numbers. @@ -1379,7 +1427,7 @@ $this->drupalPost('search/node', array('keys' => $number), t('Search')); - $this->assertText($node->title, $i . ': node title shown (search found the node) in search for number ' . $number); + $this->assertText($node->title, format_string('%i: node title shown (search found the node) in search for number %number', array('%i' => $i, '%number' => $number))); } } @@ -1405,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. @@ -1443,6 +1491,30 @@ $this->assertText(t('The index will be rebuilt')); $this->drupalGet('admin/config/search/settings'); $this->assertText(t('There is 1 item left to index.')); + + // Test that the form saves with the default values. + $this->drupalPost('admin/config/search/settings', array(), t('Save configuration')); + $this->assertText(t('The configuration options have been saved.'), 'Form saves with the default values.'); + + // Test that the form does not save with an invalid word length. + $edit = array( + 'minimum_word_size' => $this->randomName(3), + ); + $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'); } /** @@ -1529,7 +1601,7 @@ $this->drupalGet($path); foreach ($modules as $module) { $title = $module_info[$module]['title']; - $this->assertText($title, $title . ' search tab is shown'); + $this->assertText($title, format_string('%title search tab is shown', array('%title' => $title))); } } } @@ -1538,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', @@ -1548,8 +1620,7 @@ } function setUp() { - drupal_load('module', 'search'); - parent::setUp(); + parent::setUp('search'); } /** @@ -1574,13 +1645,19 @@ $this->assertEqual($result, 'The quick brown <strong>fox</strong> & 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); $result = preg_replace('| +|', ' ', search_excerpt('nothing', $entities)); $this->assertFalse(strpos($result, '&'), 'Entities are not present in excerpt'); $this->assertTrue(strpos($result, 'í') > 0, 'Entities are converted in excerpt'); + + // The node body that will produce this rendered $text is: + // 123456789 HTMLTest +123456789+‘ +‘ +‘ +‘ +12345678    +‘ +‘ +‘ ‘ + $text = "<div class=\"field field-name-body field-type-text-with-summary field-label-hidden\"><div class=\"field-items\"><div class=\"field-item even\" property=\"content:encoded\"><p>123456789 HTMLTest +123456789+‘ +‘ +‘ +‘ +12345678    +‘ +‘ +‘ ‘</p>\n</div></div></div> "; + $result = search_excerpt('HTMLTest', $text); + $this->assertFalse(empty($result), 'Rendered Multi-byte HTML encodings are not corrupted in search excerpts'); } /** @@ -1615,6 +1692,21 @@ $result = preg_replace('| +|', ' ', search_excerpt('"abc def"', $text)); $this->assertTrue(strpos($result, '<strong>abc,def</strong>') !== FALSE, 'Phrase with keyword simplified into two separate words is highlighted with simplified match'); + + // Test phrases with characters which are being truncated. + $result = preg_replace('| +|', ' ', search_excerpt('"ipsum _"', $text)); + $this->assertTrue(strpos($result, '<strong>ipsum </strong>') !== FALSE, 'Only valid part of the phrase is highlighted and invalid part containing "_" is ignored.'); + + $result = preg_replace('| +|', ' ', search_excerpt('"ipsum 0000"', $text)); + $this->assertTrue(strpos($result, '<strong>ipsum </strong>') !== FALSE, 'Only valid part of the phrase is highlighted and invalid part "0000" is ignored.'); + + // Test combination of the valid keyword and keyword containing only + // characters which are being truncated during simplification. + $result = preg_replace('| +|', ' ', search_excerpt('ipsum _', $text)); + $this->assertTrue(strpos($result, '<strong>ipsum</strong>') !== FALSE, 'Only valid keyword is highlighted and invalid keyword "_" is ignored.'); + + $result = preg_replace('| +|', ' ', search_excerpt('ipsum 0000', $text)); + $this->assertTrue(strpos($result, '<strong>ipsum</strong>') !== FALSE, 'Only valid keyword is highlighted and invalid keyword "0000" is ignored.'); } } @@ -1905,41 +1997,179 @@ function testLanguages() { // Check that there are initially no languages displayed. $this->drupalGet('search/node'); - $this->assertNoText(t('Languages'), t('No languages to choose from.')); + $this->assertNoText(t('Languages'), 'No languages to choose from.'); // Add predefined language. $edit = array('langcode' => 'fr'); $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language')); - $this->assertText('fr', t('Language added successfully.')); + $this->assertText('fr', 'Language added successfully.'); // Now we should have languages displayed. $this->drupalGet('search/node'); - $this->assertText(t('Languages'), t('Languages displayed to choose from.')); - $this->assertText(t('English'), t('English is a possible choice.')); - $this->assertText(t('French'), t('French is a possible choice.')); + $this->assertText(t('Languages'), 'Languages displayed to choose from.'); + $this->assertText(t('English'), 'English is a possible choice.'); + $this->assertText(t('French'), 'French is a possible choice.'); // Ensure selecting no language does not make the query different. $this->drupalPost('search/node', array(), t('Advanced search')); - $this->assertEqual($this->getUrl(), url('search/node/', array('absolute' => TRUE)), t('Correct page redirection, no language filtering.')); + $this->assertEqual($this->getUrl(), url('search/node/', array('absolute' => TRUE)), 'Correct page redirection, no language filtering.'); // Pick French and ensure it is selected. $edit = array('language[fr]' => TRUE); $this->drupalPost('search/node', $edit, t('Advanced search')); - $this->assertFieldByXPath('//input[@name="keys"]', 'language:fr', t('Language filter added to query.')); + $this->assertFieldByXPath('//input[@name="keys"]', 'language:fr', 'Language filter added to query.'); // Change the default language and disable English. $path = 'admin/config/regional/language'; $this->drupalGet($path); - $this->assertFieldChecked('edit-site-default-en', t('English is the default language.')); + $this->assertFieldChecked('edit-site-default-en', 'English is the default language.'); $edit = array('site_default' => 'fr'); $this->drupalPost(NULL, $edit, t('Save configuration')); - $this->assertNoFieldChecked('edit-site-default-en', t('Default language updated.')); + $this->assertNoFieldChecked('edit-site-default-en', 'Default language updated.'); $edit = array('enabled[en]' => FALSE); $this->drupalPost('admin/config/regional/language', $edit, t('Save configuration')); - $this->assertNoFieldChecked('edit-enabled-en', t('Language disabled.')); + $this->assertNoFieldChecked('edit-enabled-en', 'Language disabled.'); // Check that there are again no languages displayed. $this->drupalGet('search/node'); - $this->assertNoText(t('Languages'), t('No languages to choose from.')); + $this->assertNoText(t('Languages'), 'No languages to choose from.'); + } +} + +/** + * Tests node search with node access control. + */ +class SearchNodeAccessTest extends DrupalWebTestCase { + public $test_user; + + public static function getInfo() { + return array( + 'name' => 'Search and node access', + 'description' => 'Tests search functionality with node access control.', + 'group' => 'Search', + ); + } + + function setUp() { + parent::setUp('search', 'node_access_test'); + node_access_rebuild(); + + // Create a test user and log in. + $this->test_user = $this->drupalCreateUser(array('access content', 'search content', 'use advanced search')); + $this->drupalLogin($this->test_user); + } + + /** + * 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'); + search_update_totals(); + + // Refresh variables after the treatment. + $this->refreshVariables(); + + // Submit a phrase wrapped in double quotes to include the punctuation. + $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('<strong>&</strong>amp;'); + $this->assertText('You must include at least one positive keyword'); + + $edit = array('keys' => '&'); + $this->drupalPost('search/node', $edit, t('Search')); + $this->assertNoRaw('<strong>&</strong>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.0/modules/search/tests/search_embedded_form.info drupal-7.66/modules/search/tests/search_embedded_form.info --- drupal-7.0/modules/search/tests/search_embedded_form.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/search/tests/search_embedded_form.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: search_embedded_form.info,v 1.2 2010/12/20 19:59:43 webchick Exp $ name = "Search embedded form" description = "Support module for search module testing of embedded forms." package = Testing @@ -6,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/search/tests/search_embedded_form.module drupal-7.66/modules/search/tests/search_embedded_form.module --- drupal-7.0/modules/search/tests/search_embedded_form.module 2010-08-11 16:21:39.000000000 +0200 +++ drupal-7.66/modules/search/tests/search_embedded_form.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: search_embedded_form.module,v 1.1 2010/08/11 14:21:39 dries Exp $ /** * @file @@ -66,5 +65,6 @@ * Adds the test form to search results. */ function search_embedded_form_preprocess_search_result(&$variables) { - $variables['snippet'] .= drupal_render(drupal_get_form('search_embedded_form_form')); + $form = drupal_get_form('search_embedded_form_form'); + $variables['snippet'] .= drupal_render($form); } diff -Naur drupal-7.0/modules/search/tests/search_extra_type.info drupal-7.66/modules/search/tests/search_extra_type.info --- drupal-7.0/modules/search/tests/search_extra_type.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/search/tests/search_extra_type.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: search_extra_type.info,v 1.2 2010/12/20 19:59:43 webchick Exp $ name = "Test search type" description = "Support module for search module testing." package = Testing @@ -6,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/search/tests/search_extra_type.module drupal-7.66/modules/search/tests/search_extra_type.module --- drupal-7.0/modules/search/tests/search_extra_type.module 2010-08-18 20:40:50.000000000 +0200 +++ drupal-7.66/modules/search/tests/search_extra_type.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: search_extra_type.module,v 1.3 2010/08/18 18:40:50 dries Exp $ /** * @file diff -Naur drupal-7.0/modules/search/tests/search_node_tags.info drupal-7.66/modules/search/tests/search_node_tags.info --- drupal-7.0/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.0/modules/search/tests/search_node_tags.module drupal-7.66/modules/search/tests/search_node_tags.module --- drupal-7.0/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 @@ +<?php + +/** + * @file + * Dummy module implementing a node search hooks for search module testing. + */ + + +/** + * Implements hook_query_alter(). + */ +function search_node_tags_query_alter(QueryAlterableInterface $query) { + if ($query->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.0/modules/shortcut/shortcut-rtl.css drupal-7.66/modules/shortcut/shortcut-rtl.css --- drupal-7.0/modules/shortcut/shortcut-rtl.css 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/shortcut/shortcut-rtl.css 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,48 @@ + +div#toolbar a#edit-shortcuts { + position: absolute; + left: 0; + top: 0; + padding: 5px 5px 5px 10px; +} +div#toolbar div.toolbar-shortcuts ul { + float: none; + margin-right: 5px; + margin-left: 10em; +} +div#toolbar div.toolbar-shortcuts ul li a { + margin-left: 5px; + margin-right: 0; + padding: 0 5px; +} +div#toolbar div.toolbar-shortcuts span.icon { + float: right; +} +div.add-or-remove-shortcuts a span.icon { + float: right; + margin-right: 8px; + margin-left: 0; +} +div.add-or-remove-shortcuts a span.text { + float: right; + padding-right: 10px; + padding-left: 0; +} +div.add-or-remove-shortcuts a:focus span.text, +div.add-or-remove-shortcuts a:hover span.text { + -moz-border-radius: 5px 0 0 5px; + -webkit-border-top-left-radius: 5px; + -webkit-border-bottom-left-radius: 5px; + border-radius: 5px 0 0 5px; + padding-left: 6px; +} +#shortcut-set-switch .form-item-new { + padding-right: 17px; + padding-left: 0; +} +div.add-shortcut a:hover span.icon { + background-position: 0 -24px; +} +div.remove-shortcut a:hover span.icon { + background-position: -12px -24px; +} diff -Naur drupal-7.0/modules/shortcut/shortcut.admin.css drupal-7.66/modules/shortcut/shortcut.admin.css --- drupal-7.0/modules/shortcut/shortcut.admin.css 2010-02-25 16:58:54.000000000 +0100 +++ drupal-7.66/modules/shortcut/shortcut.admin.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -/* $Id: shortcut.admin.css,v 1.2 2010/02/25 15:58:54 dries Exp $ */ .shortcut-slot-hidden { display: none; diff -Naur drupal-7.0/modules/shortcut/shortcut.admin.inc drupal-7.66/modules/shortcut/shortcut.admin.inc --- drupal-7.0/modules/shortcut/shortcut.admin.inc 2010-10-20 03:31:07.000000000 +0200 +++ drupal-7.66/modules/shortcut/shortcut.admin.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: shortcut.admin.inc,v 1.16 2010/10/20 01:31:07 dries Exp $ /** * @file @@ -265,6 +264,7 @@ * @see shortcut_set_customize_submit() */ function shortcut_set_customize($form, &$form_state, $shortcut_set) { + $form['#shortcut_set_name'] = $shortcut_set->set_name; $form['shortcuts'] = array( '#tree' => TRUE, '#weight' => -20, @@ -300,7 +300,10 @@ 'js' => array(drupal_get_path('module', 'shortcut') . '/shortcut.admin.js'), ); - $form['actions'] = array('#type' => 'actions'); + $form['actions'] = array( + '#type' => 'actions', + '#access' => !empty($shortcut_set->links), + ); $form['actions']['submit'] = array( '#type' => 'submit', '#value' => t('Save changes'), @@ -337,9 +340,15 @@ function theme_shortcut_set_customize($variables) { $form = $variables['form']; $map = array('disabled' => t('Disabled'), 'enabled' => t('Enabled')); + $shortcuts_by_status = array( + 'enabled' => element_children($form['shortcuts']['enabled']), + 'disabled' => element_children($form['shortcuts']['disabled']), + ); + // Do not add any rows to the table if there are no shortcuts to display. + $statuses = empty($shortcuts_by_status['enabled']) && empty($shortcuts_by_status['disabled']) ? array() : array_keys($shortcuts_by_status); $rows = array(); - foreach (array('enabled', 'disabled') as $status) { + foreach ($statuses as $status) { drupal_add_tabledrag('shortcuts', 'match', 'sibling', 'shortcut-status-select'); drupal_add_tabledrag('shortcuts', 'order', 'sibling', 'shortcut-weight'); $rows[] = array( @@ -350,7 +359,7 @@ 'class' => array('shortcut-status', 'shortcut-status-' . $status), ); - foreach (element_children($form['shortcuts'][$status]) as $key) { + foreach ($shortcuts_by_status[$status] as $key) { $shortcut = &$form['shortcuts'][$status][$key]; $row = array(); $row[] = drupal_render($shortcut['name']); @@ -374,7 +383,7 @@ 'class' => array('shortcut-slot-empty'), ); } - $count_shortcuts = count(element_children($form['shortcuts'][$status])); + $count_shortcuts = count($shortcuts_by_status[$status]); if (!empty($count_shortcuts)) { for ($i = 0; $i < min($count_shortcuts, shortcut_max_slots()); $i++) { $rows['empty-' . $i]['class'][] = 'shortcut-slot-hidden'; @@ -384,7 +393,7 @@ } $header = array(t('Name'), t('Weight'), t('Status'), array('data' => t('Operations'), 'colspan' => 2)); - $output = theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('id' => 'shortcuts'))); + $output = theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('id' => 'shortcuts'), 'empty' => t('No shortcuts available. <a href="@link">Add a shortcut</a>.', array('@link' => url('admin/config/user-interface/shortcut/' . $form['#shortcut_set_name'] . '/add-link'))))); $output .= drupal_render($form['actions']); $output = drupal_render_children($form) . $output; return $output; @@ -462,7 +471,7 @@ ); } else { - $shortcut_link['link_path'] = drupal_get_path_alias($shortcut_link['link_path']); + $shortcut_link['link_path'] = ($shortcut_link['link_path'] == '<front>') ? '' : drupal_get_path_alias($shortcut_link['link_path']); } $form['shortcut_link']['#tree'] = TRUE; @@ -473,6 +482,7 @@ '#size' => 40, '#maxlength' => 255, '#default_value' => $shortcut_link['link_title'], + '#required' => TRUE, ); $form['shortcut_link']['link_path'] = array( @@ -510,7 +520,11 @@ */ function shortcut_link_edit_submit($form, &$form_state) { // Normalize the path in case it is an alias. - $form_state['values']['shortcut_link']['link_path'] = drupal_get_normal_path($form_state['values']['shortcut_link']['link_path']); + $shortcut_path = drupal_get_normal_path($form_state['values']['shortcut_link']['link_path']); + if (empty($shortcut_path)) { + $shortcut_path = '<front>'; + } + $form_state['values']['shortcut_link']['link_path'] = $shortcut_path; $shortcut_link = array_merge($form_state['values']['original_shortcut_link'], $form_state['values']['shortcut_link']); @@ -567,6 +581,9 @@ // Normalize the path in case it is an alias. $shortcut_link['link_path'] = drupal_get_normal_path($shortcut_link['link_path']); + if (empty($shortcut_link['link_path'])) { + $shortcut_link['link_path'] = '<front>'; + } // Add the link to the end of the list. $shortcut_set->links[] = $shortcut_link; @@ -767,5 +784,5 @@ drupal_goto(); } - return drupal_access_denied(); + return MENU_ACCESS_DENIED; } diff -Naur drupal-7.0/modules/shortcut/shortcut.admin.js drupal-7.66/modules/shortcut/shortcut.admin.js --- drupal-7.0/modules/shortcut/shortcut.admin.js 2009-10-17 02:51:52.000000000 +0200 +++ drupal-7.66/modules/shortcut/shortcut.admin.js 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -// $Id: shortcut.admin.js,v 1.1 2009/10/17 00:51:52 dries Exp $ (function ($) { /** @@ -34,7 +33,7 @@ tableDrag.row.prototype.onSwap = function (swappedRow) { var disabledIndex = $(table).find('tr').index($(table).find('tr.shortcut-status-disabled')) - slots - 2, count = 0; - $(table).find('tr.shortcut-status-enabled').nextAll().filter(':not(.shortcut-slot-empty)').each(function(index) { + $(table).find('tr.shortcut-status-enabled').nextAll(':not(.shortcut-slot-empty)').each(function(index) { if (index < disabledIndex) { count++; } @@ -42,7 +41,19 @@ var total = slots - count; if (total == -1) { var disabled = $(table).find('tr.shortcut-status-disabled'); - disabled.after(disabled.prevAll().filter(':not(.shortcut-slot-empty)').get(0)); + // To maintain the shortcut links limit, we need to move the last + // element from the enabled section to the disabled section. + var changedRow = disabled.prevAll(':not(.shortcut-slot-empty)').not($(this.element)).get(0); + disabled.after(changedRow); + if ($(changedRow).hasClass('draggable')) { + // The dropped element will automatically be marked as changed by + // the tableDrag system. However, the row that swapped with it + // has moved to the "disabled" section, so we need to force its + // status to be disabled and mark it also as changed. + var changedRowObject = new tableDrag.row(changedRow, 'mouse', false, 0, true); + changedRowObject.markChanged(); + tableDrag.rowStatusChange(changedRowObject); + } } else if (total != visibleLength) { if (total > visibleLength) { @@ -60,13 +71,17 @@ // Add a handler so when a row is dropped, update fields dropped into new regions. tableDrag.onDrop = function () { + tableDrag.rowStatusChange(this.rowObject); + return true; + }; + + tableDrag.rowStatusChange = function (rowObject) { // Use "status-message" row instead of "status" row because // "status-{status_name}-message" is less prone to regexp match errors. - var statusRow = $(this.rowObject.element).prevAll('tr.shortcut-status').get(0); + var statusRow = $(rowObject.element).prevAll('tr.shortcut-status').get(0); var statusName = statusRow.className.replace(/([^ ]+[ ]+)*shortcut-status-([^ ]+)([ ]+[^ ]+)*/, '$2'); - var statusField = $('select.shortcut-status-select', this.rowObject.element); + var statusField = $('select.shortcut-status-select', rowObject.element); statusField.val(statusName); - return true; }; tableDrag.restripeTable = function () { @@ -91,7 +106,7 @@ Drupal.behaviors.newSet = { attach: function (context, settings) { var selectDefault = function() { - $($(this).parents('div.form-item').get(1)).find('> label > input').attr('checked', 'checked'); + $(this).closest('form').find('.form-item-set .form-type-radio:last input').attr('checked', 'checked'); }; $('div.form-item-new input').focus(selectDefault).keyup(selectDefault); } diff -Naur drupal-7.0/modules/shortcut/shortcut.api.php drupal-7.66/modules/shortcut/shortcut.api.php --- drupal-7.0/modules/shortcut/shortcut.api.php 2009-10-17 02:51:52.000000000 +0200 +++ drupal-7.66/modules/shortcut/shortcut.api.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: shortcut.api.php,v 1.1 2009/10/17 00:51:52 dries Exp $ /** * @file diff -Naur drupal-7.0/modules/shortcut/shortcut.css drupal-7.66/modules/shortcut/shortcut.css --- drupal-7.0/modules/shortcut/shortcut.css 2011-01-03 08:04:48.000000000 +0100 +++ drupal-7.66/modules/shortcut/shortcut.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -/* $Id: shortcut.css,v 1.10 2011/01/03 07:04:48 webchick Exp $ */ div#toolbar a#edit-shortcuts { float: right; padding: 5px 10px 5px 5px; @@ -16,13 +15,13 @@ padding: 5px 0 2px 0; height: 28px; line-height: 24px; - float: left; - margin-left:5px; + float: left; /* LTR */ + margin-left:5px; /* LTR */ } div#toolbar div.toolbar-shortcuts ul li a { padding: 0 5px 0 5px; - margin-right: 5px; + margin-right: 5px; /* LTR */ -moz-border-radius: 5px; -webkit-border-radius: 5px; border-radius: 5px; @@ -40,11 +39,11 @@ } div#toolbar div.toolbar-shortcuts span.icon { - float: left; + float: left; /* LTR */ background: #444; width: 30px; height: 30px; - margin-right: 5px; + margin-right: 5px; /* LTR */ -moz-border-radius: 5px; -webkit-border-radius: 5px; border-radius: 5px; @@ -88,14 +87,12 @@ color: #fff; background-color: #5f605b; display: block; - padding-right: 6px; + padding-right: 6px; /* LTR */ cursor: pointer; - -moz-border-radius-bottomright: 5px; - -moz-border-radius-topright: 5px; - -webkit-border-bottom-right-radius: 5px; - -webkit-border-top-right-radius: 5px; - border-bottom-right-radius: 5px; - border-top-right-radius: 5px; + -moz-border-radius: 0 5px 5px 0; /* LTR */ + -webkit-border-top-right-radius: 5px; /* LTR */ + -webkit-border-bottom-right-radius: 5px; /* LTR */ + border-radius: 0 5px 5px 0; /* LTR */ } #shortcut-set-switch .form-type-radios { @@ -105,5 +102,5 @@ #shortcut-set-switch .form-item-new { padding-top: 0; - padding-left: 17px; + padding-left: 17px; /* LTR */ } diff -Naur drupal-7.0/modules/shortcut/shortcut.info drupal-7.66/modules/shortcut/shortcut.info --- drupal-7.0/modules/shortcut/shortcut.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/shortcut/shortcut.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: shortcut.info,v 1.5 2010/12/20 19:59:43 webchick Exp $ name = Shortcut description = Allows users to manage customizable lists of shortcut links. package = Core @@ -7,8 +6,7 @@ files[] = shortcut.test configure = admin/config/user-interface/shortcut -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/shortcut/shortcut.install drupal-7.66/modules/shortcut/shortcut.install --- drupal-7.0/modules/shortcut/shortcut.install 2010-08-22 15:55:53.000000000 +0200 +++ drupal-7.66/modules/shortcut/shortcut.install 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: shortcut.install,v 1.6 2010/08/22 13:55:53 dries Exp $ /** * @file @@ -26,6 +25,13 @@ 'weight' => -19, ), ); + // If Drupal is being installed, rebuild the menu before saving the shortcut + // set, to make sure the links defined above can be correctly saved. (During + // installation, the menu might not have been built at all yet, or it might + // have been built but without the node module's links in it.) + if (drupal_installation_attempted()) { + menu_rebuild(); + } shortcut_set_save($shortcut_set); } @@ -33,6 +39,7 @@ * Implements hook_uninstall(). */ function shortcut_uninstall() { + drupal_load('module', 'shortcut'); // Delete the menu links associated with each shortcut set. foreach (shortcut_sets() as $shortcut_set) { menu_delete_links($shortcut_set->set_name); diff -Naur drupal-7.0/modules/shortcut/shortcut.module drupal-7.66/modules/shortcut/shortcut.module --- drupal-7.0/modules/shortcut/shortcut.module 2010-09-24 02:37:44.000000000 +0200 +++ drupal-7.66/modules/shortcut/shortcut.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: shortcut.module,v 1.27 2010/09/24 00:37:44 dries Exp $ /** * @file @@ -23,7 +22,7 @@ switch ($path) { case 'admin/help#shortcut': $output = '<h3>' . t('About') . '</h3>'; - $output .= '<p>' . t('The Shortcut module allows users to create sets of <em>shortcut</em> links to commonly-visited pages of the site. Shortcuts are contained within <em>sets</em>. Each user with <em>Select any shortcut set</em> permission can select a shortcut set created by anyone at the site. For more information, see the online handbook entry for <a href="@shortcut">Shortcut module</a>.', array('@shortcut' => 'http://drupal.org/handbook/modules/shortcut/')) . '</p>'; + $output .= '<p>' . t('The Shortcut module allows users to create sets of <em>shortcut</em> links to commonly-visited pages of the site. Shortcuts are contained within <em>sets</em>. Each user with <em>Select any shortcut set</em> permission can select a shortcut set created by anyone at the site. For more information, see the online handbook entry for <a href="@shortcut">Shortcut module</a>.', array('@shortcut' => 'http://drupal.org/documentation/modules/shortcut/')) . '</p>'; $output .= '<h3>' . t('Uses') . '</h3>'; $output .= '<dl><dt>' . t('Administering shortcuts') . '</dt>'; $output .= '<dd>' . t('Users with the <em>Administer shortcuts</em> permission can manage shortcut sets and edit the shortcuts within sets from the <a href="@shortcuts">Shortcuts administration page</a>.', array('@shortcuts' => url('admin/config/user-interface/shortcut'))) . '</dd>'; @@ -87,7 +86,7 @@ 'title' => 'Edit shortcuts', 'page callback' => 'drupal_get_form', 'page arguments' => array('shortcut_set_customize', 4), - 'title callback' => 'shortcut_set_title', + 'title callback' => 'shortcut_set_title_callback', 'title arguments' => array(4), 'access callback' => 'shortcut_set_edit_access', 'access arguments' => array(4), @@ -461,7 +460,7 @@ * to any set. */ function shortcut_set_unassign_user($account) { - $deleted = db_delete('shortcut_set') + $deleted = db_delete('shortcut_set_users') ->condition('uid', $account->uid) ->execute(); return (bool) $deleted; @@ -618,8 +617,8 @@ if ($path != $normal_path) { $path = $normal_path; } - // Only accept links that correspond to valid paths on the site itself. - return !url_is_external($path) && menu_get_item($path); + // An empty path is valid too and will be converted to <front>. + return (!url_is_external($path) && menu_get_item($path)) || empty($path) || $path == '<front>'; } /** @@ -644,7 +643,11 @@ * Implements hook_preprocess_page(). */ function shortcut_preprocess_page(&$variables) { - if (shortcut_set_edit_access()) { + // Only display the shortcut link if the user has the ability to edit + // shortcuts and if the page's actual content is being shown (for example, + // we do not want to display it on "access denied" or "page not found" + // pages). + if (shortcut_set_edit_access() && ($item = menu_get_item()) && $item['access']) { $link = $_GET['q']; $query_parameters = drupal_get_query_parameters(); if (!empty($query_parameters)) { @@ -732,9 +735,15 @@ } /** - * Returns the title of a shortcut set. + * Returns the sanitized title of a shortcut set. * - * Title callback for the editing pages for shortcut sets. + * Deprecated. This function was previously used as a menu item title callback + * but has been replaced by shortcut_set_title_callback() (which does not + * sanitize the title, since the menu system does that automatically). In + * Drupal 7, use that function for title callbacks, and call check_plain() + * directly if you need a sanitized title. In Drupal 8, this function will be + * restored as a title callback and therefore will no longer sanitize its + * output. * * @param $shortcut_set * An object representing the shortcut set, as returned by @@ -744,3 +753,15 @@ return check_plain($shortcut_set->title); } +/** + * Returns the title of a shortcut set. + * + * Title callback for the editing pages for shortcut sets. + * + * @param $shortcut_set + * An object representing the shortcut set, as returned by + * shortcut_set_load(). + */ +function shortcut_set_title_callback($shortcut_set) { + return $shortcut_set->title; +} diff -Naur drupal-7.0/modules/shortcut/shortcut.png drupal-7.66/modules/shortcut/shortcut.png --- drupal-7.0/modules/shortcut/shortcut.png 2010-06-29 17:39:51.000000000 +0200 +++ drupal-7.66/modules/shortcut/shortcut.png 2019-04-17 22:20:46.000000000 +0200 @@ -1,3 +1,5 @@ PNG  -��� IHDR���������ש���PLTE���_`[cd_⣣pql���طEEExxx~~~KKKHHHִlll---NNNɥ)i��� tRNS�loj9���IDATx^]G0DfPl*GOg1J pH(?|%蟽n(7o#tقM&Uc2@0CS4|9[I_a-$Yݥ@y?^МAƄZ3Is1zs*b44ʡUZhbjhUc-B,A}"F}_T('et����IENDB` \ No newline at end of file +��� IHDR������$��� +���gAMA��|Q���PLTE���إ~~~NNNlllHHHEEExxxɥpql---cd_KKK_`[���|��� tRNS�lo6e��-IDAT(]Y0DB&vO7vg,>ЃZOdIAs8~/ͱ>`lL0 KfN.)~A3sژFli&8AĭJ*!ub朔{e춦c}T>U4 )EV{T52UfjJJd,U +RtWkpe:v~*s q٢,_B5/2[ddr[̜BҬwyT6VC7B% SV%[d7̹)]w1_M];<<=6a8mڴ[����IENDB` \ No newline at end of file diff -Naur drupal-7.0/modules/shortcut/shortcut.test drupal-7.66/modules/shortcut/shortcut.test --- drupal-7.0/modules/shortcut/shortcut.test 2010-11-27 21:25:44.000000000 +0100 +++ drupal-7.66/modules/shortcut/shortcut.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,9 +1,8 @@ <?php -// $Id: shortcut.test,v 1.7 2010/11/27 20:25:44 dries Exp $ /** * @file - * Tests for the shortcut module. + * Tests for shortcut.module. */ /** @@ -125,6 +124,7 @@ // Create some paths to test. $test_cases = array( + array('path' => ''), array('path' => 'admin'), array('path' => 'admin/config/system/site-information'), array('path' => "node/{$this->node->nid}/edit"), @@ -142,7 +142,8 @@ $this->assertResponse(200); $saved_set = shortcut_set_load($set->set_name); $paths = $this->getShortcutInformation($saved_set, 'link_path'); - $this->assertTrue(in_array(drupal_get_normal_path($test['path']), $paths), 'Shortcut created: '. $test['path']); + $test_path = empty($test['path']) ? '<front>' : $test['path']; + $this->assertTrue(in_array(drupal_get_normal_path($test_path), $paths), 'Shortcut created: '. $test['path']); $this->assertLink($title, 0, 'Shortcut link found on the page.'); } } @@ -198,6 +199,29 @@ $mlids = $this->getShortcutInformation($saved_set, 'mlid'); $this->assertFalse(in_array($set->links[0]['mlid'], $mlids), 'Successfully deleted a shortcut.'); } + + /** + * Tests that the add shortcut link is not displayed for 404/403 errors. + * + * Tests that the "Add to shortcuts" link is not displayed on a page not + * found or a page the user does not have access to. + */ + function testNoShortcutLink() { + // Change to a theme that displays shortcuts. + variable_set('theme_default', 'seven'); + + $this->drupalGet('page-that-does-not-exist'); + $this->assertNoRaw('add-shortcut', 'Add to shortcuts link was not shown on a page not found.'); + + // The user does not have access to this path. + $this->drupalGet('admin/modules'); + $this->assertNoRaw('add-shortcut', 'Add to shortcuts link was not shown on a page the user does not have access to.'); + + // Verify that the testing mechanism works by verifying the shortcut + // link appears on admin/content/node. + $this->drupalGet('admin/content/node'); + $this->assertRaw('add-shortcut', 'Add to shortcuts link was shown on a page the user does have access to.'); + } } /** @@ -310,7 +334,20 @@ $this->drupalPost('admin/config/user-interface/shortcut/' . $set->set_name . '/edit', array('title' => $existing_title), t('Save')); $this->assertRaw(t('The shortcut set %name already exists. Choose another name.', array('%name' => $existing_title))); $set = shortcut_set_load($set->set_name); - $this->assertNotEqual($set->title, $existing_title, t('The shortcut set %title cannot be renamed to %new-title because a shortcut set with that title already exists.', array('%title' => $set->title, '%new-title' => $existing_title))); + $this->assertNotEqual($set->title, $existing_title, format_string('The shortcut set %title cannot be renamed to %new-title because a shortcut set with that title already exists.', array('%title' => $set->title, '%new-title' => $existing_title))); + } + + /** + * Tests unassigning a shortcut set. + */ + function testShortcutSetUnassign() { + $new_set = $this->generateShortcutSet($this->randomName(10)); + + shortcut_set_assign_user($new_set, $this->shortcut_user); + shortcut_set_unassign_user($this->shortcut_user); + $current_set = shortcut_current_displayed_set($this->shortcut_user); + $default_set = shortcut_default_set($this->shortcut_user); + $this->assertTrue($current_set->set_name == $default_set->set_name, "Successfully unassigned another user's shortcut set."); } /** diff -Naur drupal-7.0/modules/simpletest/drupal_web_test_case.php drupal-7.66/modules/simpletest/drupal_web_test_case.php --- drupal-7.0/modules/simpletest/drupal_web_test_case.php 2011-01-03 00:54:05.000000000 +0100 +++ drupal-7.66/modules/simpletest/drupal_web_test_case.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: drupal_web_test_case.php,v 1.257 2011/01/02 23:54:05 webchick Exp $ /** * Global variable that holds information about the tests being run. @@ -41,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; @@ -75,6 +81,22 @@ protected $skipClasses = array(__CLASS__ => TRUE); /** + * Flag to indicate whether the test has been set up. + * + * The setUp() method isolates the test from the parent Drupal site by + * creating a random prefix for the database and setting up a clean file + * storage directory. The tearDown() method then cleans up this test + * environment. We must ensure that setUp() has been run. Otherwise, + * tearDown() will act on the parent Drupal site rather than the test + * environment, destroying live data. + */ + protected $setup = FALSE; + + protected $setupDatabasePrefix = FALSE; + + protected $setupEnvironment = FALSE; + + /** * Constructor for DrupalTestCase. * * @param $test_id @@ -128,7 +150,7 @@ ); // Store assertion for display after the test has completed. - Database::getConnection('default', 'simpletest_original_default') + self::getDatabaseConnection() ->insert('simpletest') ->fields($assertion) ->execute(); @@ -144,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 @@ -182,7 +223,8 @@ 'file' => $caller['file'], ); - return db_insert('simpletest') + return self::getDatabaseConnection() + ->insert('simpletest') ->fields($assertion) ->execute(); } @@ -198,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(); } @@ -412,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. @@ -424,8 +467,12 @@ */ protected function verbose($message) { if ($id = simpletest_verbose($message)) { - $url = file_create_url($this->originalFileDirectory . '/simpletest/verbose/' . get_class($this) . '-' . $id . '.html'); - $this->error(l(t('Verbose message'), $url, array('attributes' => array('target' => '_blank'))), 'User notice'); + $class_safe = str_replace('\\', '_', get_class($this)); + $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 = '<a href="' . $url . '" target="_blank">' . t('Verbose message') . '</a>'; + $this->error($link, 'User notice'); } } @@ -443,7 +490,8 @@ */ public function run(array $methods = array()) { // Initialize verbose debugging. - simpletest_verbose(NULL, variable_get('file_public_path', conf_path() . '/files'), get_class($this)); + $class = get_class($this); + simpletest_verbose(NULL, variable_get('file_public_path', conf_path() . '/files'), str_replace('\\', '_', $class)); // HTTP auth settings (<username>:<password>) for the simpletest browser // when sending requests to the test site. @@ -455,7 +503,6 @@ } set_error_handler(array($this, 'errorHandler')); - $class = get_class($this); // Iterate through all the methods in this class, unless a specific list of // methods to run was passed. $class_methods = get_class_methods($class); @@ -475,14 +522,19 @@ ); $completion_check_id = DrupalTestCase::insertAssert($this->testId, $class, FALSE, t('The test did not complete due to a fatal error.'), 'Completion check', $caller); $this->setUp(); - try { - $this->$method(); - // Finish up. + if ($this->setup) { + try { + $this->$method(); + // Finish up. + } + catch (Exception $e) { + $this->exceptionHandler($e); + } + $this->tearDown(); } - catch (Exception $e) { - $this->exceptionHandler($e); + else { + $this->fail(t("The test cannot be executed because it has not been set up properly.")); } - $this->tearDown(); // Remove the completion check record. DrupalTestCase::deleteAssert($completion_check_id); } @@ -512,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)); } @@ -538,14 +599,21 @@ /** * Generates a random string of ASCII characters of codes 32 to 126. * - * The generated string includes alpha-numeric characters and common misc - * characters. Use this method when testing general input where the content - * is not restricted. + * The generated string includes alpha-numeric characters and common + * miscellaneous characters. Use this method when testing general input + * where the content is not restricted. + * + * Do not use this method when special characters are not possible (e.g., in + * machine or file names that have already been validated); instead, + * use DrupalWebTestCase::randomName(). * * @param $length * Length of random string to generate. + * * @return * Randomly generated string. + * + * @see DrupalWebTestCase::randomName() */ public static function randomString($length = 8) { $str = ''; @@ -564,10 +632,16 @@ * require machine readable values (i.e. without spaces and non-standard * characters) this method is best. * + * Do not use this method when testing unvalidated user input. Instead, use + * DrupalWebTestCase::randomString(). + * * @param $length * Length of random string to generate. + * * @return * Randomly generated string. + * + * @see DrupalWebTestCase::randomString() */ public static function randomName($length = 8) { $values = array_merge(range(65, 90), range(97, 122), range(48, 57)); @@ -591,7 +665,7 @@ * 'one' => array(0, 1), * 'two' => array(2, 3), * ); - * $permutations = $this->permute($parameters); + * $permutations = DrupalTestCase::generatePermutations($parameters) * // Result: * $permutations == array( * array('one' => 0, 'two' => 2), @@ -655,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(); @@ -688,14 +769,19 @@ // 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); } + $this->setup = TRUE; } protected function tearDown() { - global $conf; + global $conf, $language; // Get back to the original connection. Database::removeConnection('default'); @@ -706,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; + } } } @@ -785,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. @@ -846,7 +945,7 @@ /** * Get a node from the database based on its title. * - * @param title + * @param $title * A node title, usually generated by $this->randomName(). * @param $reset * (optional) Whether to reset the internal node_load() cache. @@ -873,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, @@ -888,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'); @@ -946,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( @@ -998,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. @@ -1046,28 +1148,35 @@ } /** - * Create a user with a given set of permissions. The permissions correspond to the - * names given on the privileges page. + * Create a user with a given set of permissions. * - * @param $permissions - * Array of permission names to assign to user. - * @return + * @param array $permissions + * Array of permission names to assign to user. Note that the user always + * has the default permissions derived from the "authenticated users" role. + * + * @return object|false * A fully loaded user object with pass_raw property, or FALSE if account * creation fails. */ - protected function drupalCreateUser($permissions = array('access comments', 'access content', 'post comments', 'skip comment approval')) { - // Create a role with the given permission set. - if (!($rid = $this->drupalCreateRole($permissions))) { - return FALSE; + protected function drupalCreateUser(array $permissions = array()) { + // Create a role with the given permission set, if any. + $rid = FALSE; + if ($permissions) { + $rid = $this->drupalCreateRole($permissions); + if (!$rid) { + return FALSE; + } } // Create a user assigned to that role. $edit = array(); $edit['name'] = $this->randomName(); $edit['mail'] = $edit['name'] . '@example.com'; - $edit['roles'] = array($rid => $rid); $edit['pass'] = user_password(); $edit['status'] = 1; + if ($rid) { + $edit['roles'] = array($rid => $rid); + } $account = user_save(drupal_anonymous_user(), $edit); @@ -1082,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. @@ -1152,7 +1261,7 @@ * If a user is already logged in, then the current user is logged out before * logging in the specified user. * - * Please note that neither the global $user nor the passed in user object is + * Please note that neither the global $user nor the passed-in user object is * populated with data of the logged in user. If you need full access to the * user object after logging in, it must be updated manually. If you also need * access to the plain-text password of the user (set by drupalCreateUser()), @@ -1168,28 +1277,28 @@ * $account->pass_raw = $pass_raw; * @endcode * - * @param $user + * @param $account * User object representing the user to log in. * * @see drupalCreateUser() */ - protected function drupalLogin(stdClass $user) { + protected function drupalLogin(stdClass $account) { if ($this->loggedInUser) { $this->drupalLogout(); } $edit = array( - 'name' => $user->name, - 'pass' => $user->pass_raw + 'name' => $account->name, + 'pass' => $account->pass_raw ); $this->drupalPost('user', $edit, t('Log in')); // If a "log out" link appears on the page, it is almost certainly because // the login was successful. - $pass = $this->assertLink(t('Log out'), 0, t('User %name successfully logged in.', array('%name' => $user->name)), t('User login')); + $pass = $this->assertLink(t('Log out'), 0, t('User %name successfully logged in.', array('%name' => $account->name)), t('User login')); if ($pass) { - $this->loggedInUser = $user; + $this->loggedInUser = $account; } } @@ -1219,25 +1328,46 @@ } /** - * Generates a random database prefix, runs the install scripts on the - * prefixed database and enable the specified modules. After installation - * many caches are flushed and the internal browser is setup so that the - * page requests will run on the new prefix. A temporary files directory - * is created with the same name as the database prefix. + * Generates a database prefix for running tests. * - * @param ... - * List of modules to enable for the duration of the test. This can be - * either a single array or a variable number of string arguments. + * The generated database table prefix is used for the Drupal installation + * being performed for the test. It is also used as user agent HTTP header + * value by the cURL-based browser of DrupalWebTestCase, which is sent + * to the Drupal installation of the test. During early Drupal bootstrap, the + * user agent HTTP header is parsed, and if it matches, all database queries + * use the database table prefix that has been generated here. + * + * @see DrupalWebTestCase::curlInitialize() + * @see drupal_valid_test_ua() + * @see DrupalWebTestCase::setUp() */ - protected function setUp() { - global $user, $language, $conf; - - // Generate a temporary prefixed database to ensure that tests have a clean starting point. + protected function prepareDatabasePrefix() { $this->databasePrefix = 'simpletest' . mt_rand(1000, 1000000); + + // As soon as the database prefix is set, the test might start to execute. + // All assertions as well as the SimpleTest batch operations are associated + // with the testId, so the database prefix has to be associated with it. db_update('simpletest_test_id') ->fields(array('last_prefix' => $this->databasePrefix)) ->condition('test_id', $this->testId) ->execute(); + } + + /** + * Changes the database connection to the prefixed one. + * + * @see DrupalWebTestCase::setUp() + */ + protected function changeDatabasePrefix() { + if (empty($this->databasePrefix)) { + $this->prepareDatabasePrefix(); + // If $this->prepareDatabasePrefix() failed to work, return without + // setting $this->setupDatabasePrefix to TRUE, so setUp() methods will + // know to bail out. + if (empty($this->databasePrefix)) { + return; + } + } // Clone the current connection and replace the current prefix. $connection_info = Database::getConnectionInfo('default'); @@ -1249,17 +1379,45 @@ } Database::addConnectionInfo('default', 'default', $connection_info['default']); + // Indicate the database prefix was set up correctly. + $this->setupDatabasePrefix = TRUE; + } + + /** + * Prepares the current environment for running the test. + * + * Backups various current environment variables and resets them, so they do + * not interfere with the Drupal site installation in which tests are executed + * and can be restored in tearDown(). + * + * Also sets up new resources for the testing environment, such as the public + * filesystem and configuration directories. + * + * @see DrupalWebTestCase::setUp() + * @see DrupalWebTestCase::tearDown() + */ + protected function prepareEnvironment() { + 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(); - $clean_url_original = variable_get('clean_url', 0); + $this->originalCleanUrl = variable_get('clean_url', 0); + $this->originalUser = $user; - // Save and clean shutdown callbacks array because it static cached and - // will be changed by the test run. If we don't, then it will contain - // callbacks from both environments. So testing environment will try - // to call handlers from original environment. + // 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_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 + // from both environments and the testing environment will try to call the + // handlers defined by the original one. $callbacks = &drupal_register_shutdown_function(); $this->originalShutdownCallbacks = $callbacks; $callbacks = array(); @@ -1267,38 +1425,98 @@ // Create test directory ahead of installation so fatal errors and debug // information can be logged during installation process. // Use temporary files directory with the same prefix as the database. - $public_files_directory = $this->originalFileDirectory . '/simpletest/' . substr($this->databasePrefix, 10); - $private_files_directory = $public_files_directory . '/private'; - $temp_files_directory = $private_files_directory . '/temp'; + $this->public_files_directory = $this->originalFileDirectory . '/simpletest/' . substr($this->databasePrefix, 10); + $this->private_files_directory = $this->public_files_directory . '/private'; + $this->temp_files_directory = $this->private_files_directory . '/temp'; // Create the directories - file_prepare_directory($public_files_directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS); - file_prepare_directory($private_files_directory, FILE_CREATE_DIRECTORY); - file_prepare_directory($temp_files_directory, FILE_CREATE_DIRECTORY); + file_prepare_directory($this->public_files_directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS); + file_prepare_directory($this->private_files_directory, FILE_CREATE_DIRECTORY); + file_prepare_directory($this->temp_files_directory, FILE_CREATE_DIRECTORY); $this->generatedTestFiles = FALSE; // Log fatal errors. ini_set('log_errors', 1); - ini_set('error_log', $public_files_directory . '/error.log'); - - // Reset all statics and variables to perform tests in a clean environment. - $conf = array(); - drupal_static_reset(); + ini_set('error_log', $this->public_files_directory . '/error.log'); // Set the test information for use in other parts of Drupal. $test_info = &$GLOBALS['drupal_test_info']; $test_info['test_run_id'] = $this->databasePrefix; $test_info['in_child_site'] = FALSE; + // Indicate the environment was set up correctly. + $this->setupEnvironment = TRUE; + } + + /** + * Sets up a Drupal site for running functional and integration tests. + * + * Generates a random database prefix and installs Drupal with the specified + * installation profile in DrupalWebTestCase::$profile into the + * prefixed database. Afterwards, installs any additional modules specified by + * the test. + * + * After installation all caches are flushed and several configuration values + * are reset to the values of the parent site executing the test, since the + * default values may be incompatible with the environment in which tests are + * being executed. + * + * @param ... + * List of modules to enable for the duration of the test. This can be + * either a single array or a variable number of string arguments. + * + * @see DrupalWebTestCase::prepareDatabasePrefix() + * @see DrupalWebTestCase::changeDatabasePrefix() + * @see DrupalWebTestCase::prepareEnvironment() + */ + protected function setUp() { + global $user, $language, $language_url, $conf; + + // Create the database prefix for this test. + $this->prepareDatabasePrefix(); + + // Prepare the environment for running tests. + $this->prepareEnvironment(); + if (!$this->setupEnvironment) { + return FALSE; + } + + // Reset all statics and variables to perform tests in a clean environment. + $conf = array(); + drupal_static_reset(); + + // Change the database prefix. + // All static variables need to be reset before the database prefix is + // changed, since DrupalCacheArray implementations attempt to + // write back to persistent caches when they are destructed. + $this->changeDatabasePrefix(); + if (!$this->setupDatabasePrefix) { + return FALSE; + } + + // Preset the 'install_profile' system variable, so the first call into + // system_rebuild_module_data() (in drupal_install_system()) will register + // the test's profile as a module. Without this, the installation profile of + // the parent site (executing the test) is registered, and the test + // profile's hook_install() and other hook implementations are never invoked. + $conf['install_profile'] = $this->profile; + + // Perform the actual Drupal installation. include_once DRUPAL_ROOT . '/includes/install.inc'; drupal_install_system(); $this->preloadRegistry(); // Set path variables. - variable_set('file_public_path', $public_files_directory); - variable_set('file_private_path', $private_files_directory); - variable_set('file_temporary_path', $temp_files_directory); + variable_set('file_public_path', $this->public_files_directory); + variable_set('file_private_path', $this->private_files_directory); + variable_set('file_temporary_path', $this->temp_files_directory); + + // Set the 'simpletest_parent_profile' variable to add the parent profile's + // search path to the child site's search paths. + // @see drupal_system_listing() + // @todo This may need to be primed like 'install_profile' above. + variable_set('simpletest_parent_profile', $this->originalProfile); // Include the testing profile. variable_set('install_profile', $this->profile); @@ -1316,7 +1534,8 @@ $modules = $modules[0]; } if ($modules) { - module_enable($modules, TRUE); + $success = module_enable($modules, TRUE); + $this->assertTrue($success, t('Enabled modules: %modules', array('%modules' => implode(', ', $modules)))); } // Run the profile tasks. @@ -1334,24 +1553,27 @@ // the installation process. drupal_cron_run(); - // Log in with a clean $user. - $this->originalUser = $user; + // Ensure that the session is not written to the new environment and replace + // the global $user session with uid 1 from the new test site. drupal_save_session(FALSE); + // Login as uid 1. $user = user_load(1); // Restore necessary variables. variable_set('install_task', 'done'); - variable_set('clean_url', $clean_url_original); + variable_set('clean_url', $this->originalCleanUrl); variable_set('site_mail', 'simpletest@example.com'); variable_set('date_default_timezone', date_default_timezone_get()); + // Set up English language. - unset($GLOBALS['conf']['language_default']); - $language = language_default(); + unset($conf['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')); drupal_set_time_limit($this->timeLimit); + $this->setup = TRUE; } /** @@ -1439,9 +1661,9 @@ * and reset the database prefix. */ protected function tearDown() { - global $user, $language; + global $user, $language, $language_url; - // In case a fatal error occured that was not in the test process read the + // In case a fatal error occurred that was not in the test process read the // log to pick up any fatal errors. simpletest_log_read($this->testId, $this->databasePrefix, get_class($this), TRUE); @@ -1454,10 +1676,21 @@ // Delete temporary files directory. file_unmanaged_delete_recursive($this->originalFileDirectory . '/simpletest/' . substr($this->databasePrefix, 10)); - // Remove all prefixed tables (all the tables in the schema). - $schema = drupal_get_schema(NULL, TRUE); - foreach ($schema as $name => $table) { - db_drop_table($name); + // Remove all prefixed tables. + $tables = db_find_tables($this->databasePrefix . '%'); + $connection_info = Database::getConnectionInfo('default'); + $tables = db_find_tables($connection_info['default']['prefix']['default'] . '%'); + if (empty($tables)) { + $this->fail('Failed to find test tables to drop.'); + } + $prefix_length = strlen($connection_info['default']['prefix']['default']); + foreach ($tables as $table) { + if (db_drop_table(substr($table, $prefix_length))) { + unset($tables[$table]); + } + } + if (!empty($tables)) { + $this->fail('Failed to drop all prefixed tables.'); } // Get back to the original connection. @@ -1488,14 +1721,20 @@ // Rebuild caches. $this->refreshVariables(); + // Reset public files directory. + $GLOBALS['conf']['file_public_path'] = $this->originalFileDirectory; + // 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(); } /** @@ -1511,13 +1750,20 @@ if (!isset($this->curlHandle)) { $this->curlHandle = curl_init(); + + // Some versions/configurations of cURL break on a NULL cookie jar, so + // supply a real file. + if (empty($this->cookieFile)) { + $this->cookieFile = $this->public_files_directory . '/cookie.jar'; + } + $curl_options = array( CURLOPT_COOKIEJAR => $this->cookieFile, CURLOPT_URL => $base_url, CURLOPT_FOLLOWLOCATION => FALSE, CURLOPT_RETURNTRANSFER => TRUE, - CURLOPT_SSL_VERIFYPEER => FALSE, // Required to make the tests run on https. - CURLOPT_SSL_VERIFYHOST => FALSE, // Required to make the tests run on https. + CURLOPT_SSL_VERIFYPEER => FALSE, // Required to make the tests run on HTTPS. + CURLOPT_SSL_VERIFYHOST => FALSE, // Required to make the tests run on HTTPS. CURLOPT_HEADERFUNCTION => array(&$this, 'curlHeaderCallback'), CURLOPT_USERAGENT => $this->databasePrefix, ); @@ -1525,7 +1771,12 @@ $curl_options[CURLOPT_HTTPAUTH] = $this->httpauth_method; $curl_options[CURLOPT_USERPWD] = $this->httpauth_credentials; } - curl_setopt_array($this->curlHandle, $this->additionalCurlOptions + $curl_options); + // curl_setopt_array() returns FALSE if any of the specified options + // cannot be set, and stops processing any further options. + $result = curl_setopt_array($this->curlHandle, $this->additionalCurlOptions + $curl_options); + if (!$result) { + throw new Exception('One or more cURL options could not be set.'); + } // By default, the child session name should be the same as the parent. $this->session_name = session_name(); @@ -1556,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]; @@ -1626,7 +1887,16 @@ * An header. */ protected function curlHeaderCallback($curlHandler, $header) { - $this->headers[] = $header; + // Header fields can be extended over multiple lines by preceding each + // extra line with at least one SP or HT. They should be joined on receive. + // Details are in RFC2616 section 4. + if ($header[0] == ' ' || $header[0] == "\t") { + // Normalize whitespace between chucks. + $this->headers[] = array_pop($this->headers) . ' ' . trim($header); + } + else { + $this->headers[] = $header; + } // Errors are being sent via X-Drupal-Assertion-* headers, // generated by _drupal_log_error() in the exact form required @@ -1769,17 +2039,17 @@ * button with the value t('Delete'), and execute different code depending * on which one is clicked. * - * This function can also be called to emulate an AJAX submission. In this + * This function can also be called to emulate an Ajax submission. In this * case, this value needs to be an array with the following keys: - * - path: A path to submit the form values to for AJAX-specific processing, + * - path: A path to submit the form values to for Ajax-specific processing, * which is likely different than the $path parameter used for retrieving * the initial form. Defaults to 'system/ajax'. * - triggering_element: If the value for the 'path' key is 'system/ajax' or - * another generic AJAX processing path, this needs to be set to the name + * another generic Ajax processing path, this needs to be set to the name * of the element. If the name doesn't identify the element uniquely, then * this should instead be an array with a single key/value pair, * corresponding to the element name and value. The callback for the - * generic AJAX processing path uses this to find the #ajax information + * generic Ajax processing path uses this to find the #ajax information * for the element, including which specific callback to use for * processing the request. * @@ -1829,7 +2099,7 @@ $action = isset($form['action']) ? $this->getAbsoluteUrl((string) $form['action']) : $this->getUrl(); if ($ajax) { $action = $this->getAbsoluteUrl(!empty($submit['path']) ? $submit['path'] : 'system/ajax'); - // AJAX callbacks verify the triggering element if necessary, so while + // Ajax callbacks verify the triggering element if necessary, so while // we may eventually want extra code that verifies it in the // handleForm() function, it's not currently a requirement. $submit_matches = TRUE; @@ -1846,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; + } } } } @@ -1886,12 +2163,45 @@ } /** - * Execute an AJAX submission. + * Execute an Ajax submission. * * This executes a POST as ajax.js does. It uses the returned JSON data, an * array of commands, to update $this->content using equivalent DOM * manipulation as is used by ajax.js. It also returns the array of commands. * + * @param $path + * Location of the form containing the Ajax enabled element to test. Can be + * either a Drupal path or an absolute path or NULL to use the current page. + * @param $edit + * Field data in an associative array. Changes the current input fields + * (where possible) to the values indicated. + * @param $triggering_element + * The name of the form element that is responsible for triggering the Ajax + * functionality to test. May be a string or, if the triggering element is + * a button, an associative array where the key is the name of the button + * and the value is the button label. i.e.) array('op' => t('Refresh')). + * @param $ajax_path + * (optional) Override the path set by the Ajax settings of the triggering + * element. In the absence of both the triggering element's Ajax path and + * $ajax_path 'system/ajax' will be used. + * @param $options + * (optional) Options to be forwarded to url(). + * @param $headers + * (optional) An array containing additional HTTP request headers, each + * formatted as "name: value". Forwarded to drupalPost(). + * @param $form_html_id + * (optional) HTML ID of the form to be submitted, use when there is more + * than one identical form on the same page and the value of the triggering + * element is not enough to identify the form. Note this is not the Drupal + * ID of the form but rather the HTML ID of the form. + * @param $ajax_settings + * (optional) An array of Ajax settings which if specified will be used in + * place of the Ajax settings of the triggering element. + * + * @return + * An array of Ajax commands. + * + * @see drupalPost() * @see ajax.js */ protected function drupalPostAJAX($path, $edit, $triggering_element, $ajax_path = NULL, array $options = array(), array $headers = array(), $form_html_id = NULL, $ajax_settings = NULL) { @@ -1903,7 +2213,7 @@ $content = $this->content; $drupal_settings = $this->drupalSettings; - // Get the AJAX settings bound to the triggering element. + // Get the Ajax settings bound to the triggering element. if (!isset($ajax_settings)) { if (is_array($triggering_element)) { $xpath = '//*[@name="' . key($triggering_element) . '" and @value="' . current($triggering_element) . '"]'; @@ -1930,15 +2240,26 @@ $id = (string) $element['id']; $extra_post .= '&' . urlencode('ajax_html_ids[]') . '=' . urlencode($id); } + if (isset($drupal_settings['ajaxPageState'])) { + $extra_post .= '&' . urlencode('ajax_page_state[theme]') . '=' . urlencode($drupal_settings['ajaxPageState']['theme']); + $extra_post .= '&' . urlencode('ajax_page_state[theme_token]') . '=' . urlencode($drupal_settings['ajaxPageState']['theme_token']); + foreach ($drupal_settings['ajaxPageState']['css'] as $key => $value) { + $extra_post .= '&' . urlencode("ajax_page_state[css][$key]") . '=1'; + } + foreach ($drupal_settings['ajaxPageState']['js'] as $key => $value) { + $extra_post .= '&' . urlencode("ajax_page_state[js][$key]") . '=1'; + } + } // Unless a particular path is specified, use the one specified by the - // AJAX settings, or else 'system/ajax'. + // Ajax settings, or else 'system/ajax'. if (!isset($ajax_path)) { $ajax_path = isset($ajax_settings['url']) ? $ajax_settings['url'] : 'system/ajax'; } // 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)) { @@ -1951,63 +2272,77 @@ // them. $dom = new DOMDocument(); @$dom->loadHTML($content); + // XPath allows for finding wrapper nodes better than DOM does. + $xpath = new DOMXPath($dom); foreach ($return as $command) { switch ($command['command']) { case 'settings': - $drupal_settings = array_merge_recursive($drupal_settings, $command['settings']); + $drupal_settings = drupal_array_merge_deep($drupal_settings, $command['settings']); break; case 'insert': - // @todo ajax.js can process commands that include a 'selector', but - // these are hard to emulate with DOMDocument. For now, we only - // implement 'insert' commands that use $ajax_settings['wrapper']. + $wrapperNode = NULL; + // When a command doesn't specify a selector, use the + // #ajax['wrapper'] which is always an HTML ID. if (!isset($command['selector'])) { - // $dom->getElementById() doesn't work when drupalPostAJAX() is - // invoked multiple times for a page, so use XPath instead. This - // also sets us up for adding support for $command['selector'] in - // the future, once we figure out how to transform a jQuery - // selector to XPath. - $xpath = new DOMXPath($dom); $wrapperNode = $xpath->query('//*[@id="' . $ajax_settings['wrapper'] . '"]')->item(0); - if ($wrapperNode) { - // ajax.js adds an enclosing DIV to work around a Safari bug. - $newDom = new DOMDocument(); - $newDom->loadHTML('<div>' . $command['data'] . '</div>'); - $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. - switch ($method) { - case 'replaceWith': - $wrapperNode->parentNode->replaceChild($newNode, $wrapperNode); - break; - case 'append': - $wrapperNode->appendChild($newNode); - break; - case 'prepend': - // If no firstChild, insertBefore() falls back to - // appendChild(). - $wrapperNode->insertBefore($newNode, $wrapperNode->firstChild); - break; - case 'before': - $wrapperNode->parentNode->insertBefore($newNode, $wrapperNode); - break; - case 'after': - // If no nextSibling, insertBefore() falls back to - // appendChild(). - $wrapperNode->parentNode->insertBefore($newNode, $wrapperNode->nextSibling); - break; - case 'html': - foreach ($wrapperNode->childNodes as $childNode) { - $wrapperNode->removeChild($childNode); - } - $wrapperNode->appendChild($newNode); - break; - } + } + // @todo Ajax commands can target any jQuery selector, but these are + // hard to fully emulate with XPath. For now, just handle 'head' + // and 'body', since these are used by ajax_render(). + elseif (in_array($command['selector'], array('head', 'body'))) { + $wrapperNode = $xpath->query('//' . $command['selector'])->item(0); + } + 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('<div>' . $command['data'] . '</div>'); + // 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. + switch ($method) { + case 'replaceWith': + $wrapperNode->parentNode->replaceChild($newNode, $wrapperNode); + break; + case 'append': + $wrapperNode->appendChild($newNode); + break; + case 'prepend': + // If no firstChild, insertBefore() falls back to + // appendChild(). + $wrapperNode->insertBefore($newNode, $wrapperNode->firstChild); + break; + case 'before': + $wrapperNode->parentNode->insertBefore($newNode, $wrapperNode); + break; + case 'after': + // If no nextSibling, insertBefore() falls back to + // appendChild(). + $wrapperNode->parentNode->insertBefore($newNode, $wrapperNode->nextSibling); + break; + case 'html': + foreach ($wrapperNode->childNodes as $childNode) { + $wrapperNode->removeChild($childNode); + } + $wrapperNode->appendChild($newNode); + break; } } 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': @@ -2020,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 .= '<br />AJAX callback path: ' . $ajax_path; + $verbose .= '<hr />Ending URL: ' . $this->getUrl(); + $verbose .= '<hr />' . $this->content; + + $this->verbose($verbose); + return $return; } @@ -2109,9 +2454,16 @@ if (isset($edit[$name])) { switch ($type) { case 'text': + case 'tel': case 'textarea': + case 'url': + case 'number': + case 'range': + case 'color': case 'hidden': case 'password': + case 'email': + case 'search': $post[$name] = $edit[$name]; unset($edit[$name]); break; @@ -2272,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, @@ -2341,8 +2698,6 @@ * * @param $label * Text between the anchor tags. - * @param $index - * Link position counting from zero. * @param $message * Message to display. * @param $group @@ -2399,31 +2754,28 @@ /** * Follows a link by name. * - * Will click the first link found with this link text by default, or a - * later one if an index is given. Match is case insensitive with - * normalized space. The label is translated label. There is an assert - * for successful click. + * 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. + * + * 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; } @@ -2448,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. @@ -2458,10 +2810,10 @@ } /** - * Get the current url from the cURL handler. + * Get the current URL from the cURL handler. * * @return - * The current url. + * The current URL. */ protected function getUrl() { return $this->url; @@ -2660,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); } /** @@ -2680,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); } /** @@ -2902,12 +3254,50 @@ } /** + * Asserts themed output. + * + * @param $callback + * The name of the theme function to invoke; e.g. 'links' for theme_links(). + * @param $variables + * (optional) An array of variables to pass to the theme function. + * @param $expected + * The expected themed output string. + * @param $message + * (optional) A message to display with the assertion. Do not translate + * messages: use format_string() to embed variables in the message text, not + * t(). If left blank, a default message will be displayed. + * @param $group + * (optional) The group this message is in, which is displayed in a column + * in test output. Use 'Debug' to indicate this is debugging output. Do not + * translate this string. Defaults to 'Other'; most tests do not override + * this default. + * + * @return + * TRUE on pass, FALSE on fail. + */ + protected function assertThemeOutput($callback, array $variables = array(), $expected, $message = '', $group = 'Other') { + $output = theme($callback, $variables); + $this->verbose('Variables:' . '<pre>' . check_plain(var_export($variables, TRUE)) . '</pre>' + . '<hr />' . 'Result:' . '<pre>' . check_plain(var_export($output, TRUE)) . '</pre>' + . '<hr />' . 'Expected:' . '<pre>' . check_plain(var_export($expected, TRUE)) . '</pre>' + . '<hr />' . $output + ); + if (!$message) { + $message = '%callback rendered correctly.'; + } + $message = format_string($message, array('%callback' => 'theme_' . $callback . '()')); + return $this->assertIdentical($output, $expected, $message, $group); + } + + /** * Asserts that a field exists in the current page by the given XPath. * * @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 @@ -2975,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 @@ -3013,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 @@ -3021,8 +3415,21 @@ * @return * TRUE on pass, FALSE on fail. */ - protected function assertFieldByName($name, $value = '', $message = '') { - return $this->assertFieldByXPath($this->constructFieldXpath('name', $name), $value, $message ? $message : t('Found field by name @name', array('@name' => $name)), t('Browser')); + protected function assertFieldByName($name, $value = NULL, $message = NULL) { + if (!isset($message)) { + if (!isset($value)) { + $message = t('Found field with name @name', array( + '@name' => var_export($name, TRUE), + )); + } + else { + $message = t('Found field with name @name and value @value', array( + '@name' => var_export($name, TRUE), + '@value' => var_export($value, TRUE), + )); + } + } + return $this->assertFieldByXPath($this->constructFieldXpath('name', $name), $value, $message, t('Browser')); } /** @@ -3031,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 @@ -3044,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 @@ -3062,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 @@ -3083,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. */ @@ -3098,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. */ @@ -3113,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. * @@ -3132,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. */ @@ -3146,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 @@ -3162,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.0/modules/simpletest/files/README.txt drupal-7.66/modules/simpletest/files/README.txt --- drupal-7.0/modules/simpletest/files/README.txt 2010-04-28 22:25:21.000000000 +0200 +++ drupal-7.66/modules/simpletest/files/README.txt 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ -$Id: README.txt,v 1.2 2010/04/28 20:25:21 dries Exp $ - -These files are use in some tests that upload files or other operations were -a file is useful. These files are copied to the files directory as specified -in the site settings. Other tests files are generated in order to save space. +These files are useful in tests that upload files or otherwise need to +manipulate files, in which case they are copied to the files directory as +specified in the site settings. Dummy files can also be generated by tests in +order to save space. diff -Naur drupal-7.0/modules/simpletest/files/css_test_files/comment_hacks.css.unoptimized.css drupal-7.66/modules/simpletest/files/css_test_files/comment_hacks.css.unoptimized.css --- drupal-7.0/modules/simpletest/files/css_test_files/comment_hacks.css.unoptimized.css 2010-06-21 03:32:21.000000000 +0200 +++ drupal-7.66/modules/simpletest/files/css_test_files/comment_hacks.css.unoptimized.css 2019-04-17 22:20:46.000000000 +0200 @@ -4,7 +4,7 @@ * */ /* -A large comment block to test for segfaults and speed. This is 60K a's. Extreme but usefull to demonstrate flaws in comment striping regexp. aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa*/ +A large comment block to test for segfaults and speed. This is 60K a's. Extreme but useful to demonstrate flaws in comment striping regexp. aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa*/ .test1 { display:block;} /* A multiline IE-mac hack (v.2) taken fron Zen theme*/ diff -Naur drupal-7.0/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.0/modules/simpletest/files/css_test_files/css_input_with_import.css 2010-01-07 08:45:03.000000000 +0100 +++ 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.0/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.0/modules/simpletest/files/css_test_files/css_input_with_import.css.optimized.css 2010-11-17 05:15:37.000000000 +0100 +++ 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.0/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.0/modules/simpletest/files/css_test_files/css_input_with_import.css.unoptimized.css 2010-01-07 08:45:03.000000000 +0100 +++ 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.0/modules/simpletest/files/css_test_files/css_input_without_import.css drupal-7.66/modules/simpletest/files/css_test_files/css_input_without_import.css --- drupal-7.0/modules/simpletest/files/css_test_files/css_input_without_import.css 2010-10-08 17:36:12.000000000 +0200 +++ drupal-7.66/modules/simpletest/files/css_test_files/css_input_without_import.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -/* $Id: css_input_without_import.css,v 1.3 2010/10/08 15:36:12 dries Exp $ */ /** * @file Basic css that does not use import diff -Naur drupal-7.0/modules/simpletest/files/css_test_files/css_input_without_import.css.unoptimized.css drupal-7.66/modules/simpletest/files/css_test_files/css_input_without_import.css.unoptimized.css --- drupal-7.0/modules/simpletest/files/css_test_files/css_input_without_import.css.unoptimized.css 2010-10-08 17:36:12.000000000 +0200 +++ drupal-7.66/modules/simpletest/files/css_test_files/css_input_without_import.css.unoptimized.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -/* $Id: css_input_without_import.css.unoptimized.css,v 1.3 2010/10/08 15:36:12 dries Exp $ */ /** * @file Basic css that does not use import diff -Naur drupal-7.0/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.0/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.0/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.0/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.0/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.0/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.0/modules/simpletest/files/css_test_files/import1.css drupal-7.66/modules/simpletest/files/css_test_files/import1.css --- drupal-7.0/modules/simpletest/files/css_test_files/import1.css 2010-01-07 08:45:03.000000000 +0100 +++ 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.0/modules/simpletest/files/image-test-no-transparency.gif drupal-7.66/modules/simpletest/files/image-test-no-transparency.gif --- drupal-7.0/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̤<:$@*V5�Q�]fulح_{U&[iϲ-nBsݚ=7l^,+wHx 8bƅ?6 �; \ No newline at end of file diff -Naur drupal-7.0/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.0/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.0/modules/simpletest/files/phar-1.phar drupal-7.66/modules/simpletest/files/phar-1.phar --- drupal-7.0/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 @@ +<?php + +$web = 'index.php'; + +if (in_array('phar', stream_get_wrappers()) && class_exists('Phar', 0)) { +Phar::interceptFileFuncs(); +set_include_path('phar://' . __FILE__ . PATH_SEPARATOR . get_include_path()); +Phar::webPhar(null, $web); +include 'phar://' . __FILE__ . '/' . Extract_Phar::START; +return; +} + +if (@(isset($_SERVER['REQUEST_URI']) && isset($_SERVER['REQUEST_METHOD']) && ($_SERVER['REQUEST_METHOD'] == 'GET' || $_SERVER['REQUEST_METHOD'] == 'POST'))) { +Extract_Phar::go(true); +$mimes = array( +'phps' => 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 "<html>\n <head>\n <title>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.0/modules/simpletest/lib/Drupal/simpletest/Tests/PSR0WebTest.php drupal-7.66/modules/simpletest/lib/Drupal/simpletest/Tests/PSR0WebTest.php --- drupal-7.0/modules/simpletest/lib/Drupal/simpletest/Tests/PSR0WebTest.php 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/lib/Drupal/simpletest/Tests/PSR0WebTest.php 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,18 @@ +<?php + +namespace Drupal\simpletest\Tests; + +class PSR0WebTest extends \DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'PSR0 web test', + 'description' => 'We want to assert that this PSR-0 test case is being discovered.', + 'group' => 'SimpleTest', + ); + } + + function testArithmetics() { + $this->assert(1 + 1 == 2, '1 + 1 == 2'); + } +} diff -Naur drupal-7.0/modules/simpletest/simpletest.api.php drupal-7.66/modules/simpletest/simpletest.api.php --- drupal-7.0/modules/simpletest/simpletest.api.php 2009-12-20 22:12:54.000000000 +0100 +++ drupal-7.66/modules/simpletest/simpletest.api.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: simpletest.api.php,v 1.4 2009/12/20 21:12:54 dries Exp $ /** * @file @@ -21,7 +20,7 @@ */ function hook_simpletest_alter(&$groups) { // An alternative session handler module would not want to run the original - // Session https handling test because it checks the sessions table in the + // Session HTTPS handling test because it checks the sessions table in the // database. unset($groups['Session']['testHttpsSession']); } diff -Naur drupal-7.0/modules/simpletest/simpletest.css drupal-7.66/modules/simpletest/simpletest.css --- drupal-7.0/modules/simpletest/simpletest.css 2010-10-09 20:09:48.000000000 +0200 +++ drupal-7.66/modules/simpletest/simpletest.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -/* $Id: simpletest.css,v 1.10 2010/10/09 18:09:48 webchick Exp $ */ /* Test Table */ #simpletest-form-table th.select-all { diff -Naur drupal-7.0/modules/simpletest/simpletest.info drupal-7.66/modules/simpletest/simpletest.info --- drupal-7.0/modules/simpletest/simpletest.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/simpletest/simpletest.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: simpletest.info,v 1.28 2010/12/20 19:59:43 webchick Exp $ name = Testing description = Provides a framework for unit and functional testing. package = Core @@ -12,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 @@ -28,11 +29,13 @@ files[] = tests/mail.test files[] = tests/menu.test files[] = tests/module.test +files[] = tests/pager.test files[] = tests/password.test files[] = tests/path.test files[] = tests/registry.test files[] = tests/schema.test files[] = tests/session.test +files[] = tests/tablesort.test files[] = tests/theme.test files[] = tests/unicode.test files[] = tests/update.test @@ -40,13 +43,21 @@ files[] = tests/upgrade/upgrade.test files[] = tests/upgrade/upgrade.comment.test files[] = tests/upgrade/upgrade.filter.test +files[] = tests/upgrade/upgrade.forum.test +files[] = tests/upgrade/upgrade.locale.test +files[] = tests/upgrade/upgrade.menu.test files[] = tests/upgrade/upgrade.node.test files[] = tests/upgrade/upgrade.taxonomy.test +files[] = tests/upgrade/upgrade.trigger.test +files[] = tests/upgrade/upgrade.translatable.test files[] = tests/upgrade/upgrade.upload.test -files[] = tests/upgrade/upgrade.locale.test +files[] = tests/upgrade/upgrade.user.test +files[] = tests/upgrade/update.aggregator.test +files[] = tests/upgrade/update.trigger.test +files[] = tests/upgrade/update.field.test +files[] = tests/upgrade/update.user.test -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/simpletest/simpletest.install drupal-7.66/modules/simpletest/simpletest.install --- drupal-7.0/modules/simpletest/simpletest.install 2011-01-03 07:40:49.000000000 +0100 +++ drupal-7.66/modules/simpletest/simpletest.install 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: simpletest.install,v 1.41 2011/01/03 06:40:49 webchick Exp $ /** * @file @@ -46,7 +45,7 @@ ); if (!$has_domdocument) { $requirements['php_domdocument']['severity'] = REQUIREMENT_ERROR; - $requirements['php_domdocument']['description'] =t('The testing framework requires the DOMDocument class to be available. Check the configure command at the <a href="@link-phpinfo">PHP info page</a>.', array('@link-phpinfo' => url('admin/reports/status/php'))); + $requirements['php_domdocument']['description'] = $t('The testing framework requires the DOMDocument class to be available. Check the configure command at the <a href="@link-phpinfo">PHP info page</a>.', array('@link-phpinfo' => url('admin/reports/status/php'))); } // SimpleTest currently needs 2 cURL options which are incompatible with @@ -58,15 +57,15 @@ ); if ($open_basedir) { $requirements['php_open_basedir']['severity'] = REQUIREMENT_ERROR; - $requirements['php_open_basedir']['description'] = t('The testing framework requires the PHP <a href="@open_basedir-url">open_basedir</a> restriction to be disabled. Check your webserver configuration or contact your web host.', array('@open_basedir-url' => 'http://php.net/manual/en/ini.core.php#ini.open-basedir')); + $requirements['php_open_basedir']['description'] = $t('The testing framework requires the PHP <a href="@open_basedir-url">open_basedir</a> restriction to be disabled. Check your webserver configuration or contact your web host.', array('@open_basedir-url' => 'http://php.net/manual/en/ini.core.php#ini.open-basedir')); } // Check the current memory limit. If it is set too low, SimpleTest will fail // to load all tests and throw a fatal error. $memory_limit = ini_get('memory_limit'); - if ($memory_limit && $memory_limit != -1 && parse_size($memory_limit) < parse_size(SIMPLETEST_MINIMUM_PHP_MEMORY_LIMIT)) { + if (!drupal_check_memory_limit(SIMPLETEST_MINIMUM_PHP_MEMORY_LIMIT, $memory_limit)) { $requirements['php_memory_limit']['severity'] = REQUIREMENT_ERROR; - $requirements['php_memory_limit']['description'] = t('The testing framework requires the PHP memory limit to be at least %memory_minimum_limit. The current value is %memory_limit. <a href="@url">Follow these steps to continue</a>.', array('%memory_limit' => $memory_limit, '%memory_minimum_limit' => SIMPLETEST_MINIMUM_PHP_MEMORY_LIMIT, '@url' => 'http://drupal.org/node/207036')); + $requirements['php_memory_limit']['description'] = $t('The testing framework requires the PHP memory limit to be at least %memory_minimum_limit. The current value is %memory_limit. <a href="@url">Follow these steps to continue</a>.', array('%memory_limit' => $memory_limit, '%memory_minimum_limit' => SIMPLETEST_MINIMUM_PHP_MEMORY_LIMIT, '@url' => 'http://drupal.org/node/207036')); } return $requirements; @@ -168,7 +167,8 @@ * Implements hook_uninstall(). */ function simpletest_uninstall() { - simpletest_clean_environment(); + drupal_load('module', 'simpletest'); + simpletest_clean_database(); // Remove settings variables. variable_del('simpletest_httpauth_method'); diff -Naur drupal-7.0/modules/simpletest/simpletest.js drupal-7.66/modules/simpletest/simpletest.js --- drupal-7.0/modules/simpletest/simpletest.js 2009-04-27 22:19:37.000000000 +0200 +++ drupal-7.66/modules/simpletest/simpletest.js 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -// $Id: simpletest.js,v 1.11 2009/04/27 20:19:37 webchick Exp $ (function ($) { /** @@ -8,49 +7,50 @@ attach: function (context, settings) { var timeout = null; // Adds expand-collapse functionality. - $('div.simpletest-image').each(function () { - direction = settings.simpleTest[$(this).attr('id')].imageDirection; - $(this).html(settings.simpleTest.images[direction]); - }); - - // Adds group toggling functionality to arrow images. - $('div.simpletest-image').click(function () { - var trs = $(this).parents('tbody').children('.' + settings.simpleTest[this.id].testClass); + $('div.simpletest-image').once('simpletest-image', function () { + var $this = $(this); var direction = settings.simpleTest[this.id].imageDirection; - var row = direction ? trs.size() - 1 : 0; + $this.html(settings.simpleTest.images[direction]); - // If clicked in the middle of expanding a group, stop so we can switch directions. - if (timeout) { - clearTimeout(timeout); - } + // Adds group toggling functionality to arrow images. + $this.click(function () { + var trs = $this.closest('tbody').children('.' + settings.simpleTest[this.id].testClass); + var direction = settings.simpleTest[this.id].imageDirection; + var row = direction ? trs.length - 1 : 0; + + // If clicked in the middle of expanding a group, stop so we can switch directions. + if (timeout) { + clearTimeout(timeout); + } - // Function to toggle an individual row according to the current direction. - // We set a timeout of 20 ms until the next row will be shown/hidden to - // create a sliding effect. - function rowToggle() { - if (direction) { - if (row >= 0) { - $(trs[row]).hide(); - row--; - timeout = setTimeout(rowToggle, 20); + // Function to toggle an individual row according to the current direction. + // We set a timeout of 20 ms until the next row will be shown/hidden to + // create a sliding effect. + function rowToggle() { + if (direction) { + if (row >= 0) { + $(trs[row]).hide(); + row--; + timeout = setTimeout(rowToggle, 20); + } } - } - else { - if (row < trs.size()) { - $(trs[row]).removeClass('js-hide').show(); - row++; - timeout = setTimeout(rowToggle, 20); + else { + if (row < trs.length) { + $(trs[row]).removeClass('js-hide').show(); + row++; + timeout = setTimeout(rowToggle, 20); + } } } - } - // Kick-off the toggling upon a new click. - rowToggle(); + // Kick-off the toggling upon a new click. + rowToggle(); - // Toggle the arrow image next to the test group title. - $(this).html(settings.simpleTest.images[(direction ? 0 : 1)]); - settings.simpleTest[this.id].imageDirection = !direction; + // Toggle the arrow image next to the test group title. + $this.html(settings.simpleTest.images[(direction ? 0 : 1)]); + settings.simpleTest[this.id].imageDirection = !direction; + }); }); } }; @@ -61,7 +61,7 @@ */ Drupal.behaviors.simpleTestSelectAll = { attach: function (context, settings) { - $('td.simpletest-select-all').each(function () { + $('td.simpletest-select-all').once('simpletest-select-all', function () { var testCheckboxes = settings.simpleTest['simpletest-test-group-' + $(this).attr('id')].testNames; var groupCheckbox = $('<input type="checkbox" class="form-checkbox" id="' + $(this).attr('id') + '-select-all" />'); diff -Naur drupal-7.0/modules/simpletest/simpletest.module drupal-7.66/modules/simpletest/simpletest.module --- drupal-7.0/modules/simpletest/simpletest.module 2010-11-12 04:06:52.000000000 +0100 +++ drupal-7.66/modules/simpletest/simpletest.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: simpletest.module,v 1.97 2010/11/12 03:06:52 dries Exp $ /** * @file @@ -14,7 +13,7 @@ case 'admin/help#simpletest': $output = ''; $output .= '<h3>' . t('About') . '</h3>'; - $output .= '<p>' . t('The Testing module provides a framework for running automated unit tests. It can be used to verify a working state of Drupal before and after any code changes, or as a means for developers to write and execute tests for their modules. For more information, see the online handbook entry for <a href="@simpletest">Testing module</a>.', array('@simpletest' => 'http://drupal.org/handbook/modules/simpletest', '@blocks' => url('admin/structure/block'))) . '</p>'; + $output .= '<p>' . t('The Testing module provides a framework for running automated unit tests. It can be used to verify a working state of Drupal before and after any code changes, or as a means for developers to write and execute tests for their modules. For more information, see the online handbook entry for <a href="@simpletest">Testing module</a>.', array('@simpletest' => 'http://drupal.org/documentation/modules/simpletest', '@blocks' => url('admin/structure/block'))) . '</p>'; $output .= '<h3>' . t('Uses') . '</h3>'; $output .= '<dl>'; $output .= '<dt>' . t('Running tests') . '</dt>'; @@ -155,9 +154,10 @@ } /** - * Batch operation callback. + * Implements callback_batch_operation(). */ function _simpletest_batch_operation($test_list_init, $test_id, &$context) { + simpletest_classloader_register(); // Get working values. if (!isset($context['sandbox']['max'])) { // First iteration: initialize working values. @@ -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))); @@ -273,7 +276,7 @@ DrupalTestCase::insertAssert($test_id, $test_class, FALSE, $match[2], $match[1], $caller); } else { - // Unkown format, place the entire message in the log. + // Unknown format, place the entire message in the log. DrupalTestCase::insertAssert($test_id, $test_class, FALSE, $line, 'Fatal error'); } $found = TRUE; @@ -290,6 +293,9 @@ * a static variable. In order to list tests provided by disabled modules * hook_registry_files_alter() is used to forcefully add them to the registry. * + * PSR-0 classes are found by searching the designated directory for each module + * for files matching the PSR-0 standard. + * * @return * An array of tests keyed with the groups specified in each of the tests * getInfo() method and then keyed by the test class. An example of the array @@ -310,6 +316,9 @@ $groups = &drupal_static(__FUNCTION__); if (!$groups) { + // Register a simple class loader for PSR-0 test classes. + simpletest_classloader_register(); + // Load test information from cache if available, otherwise retrieve the // information from each tests getInfo() method. if ($cache = cache_get('simpletest', 'cache')) { @@ -319,6 +328,41 @@ // Select all clases in files ending with .test. $classes = db_query("SELECT name FROM {registry} WHERE type = :type AND filename LIKE :name", array(':type' => 'class', ':name' => '%.test'))->fetchCol(); + // Also discover PSR-0 test classes, if the PHP version allows it. + if (version_compare(PHP_VERSION, '5.3') > 0) { + + // 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) { + $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); + } + } + } + } + } + } + // Check that each class has a getInfo() method and store the information // in an array keyed with the group specified in the test information. $groups = array(); @@ -330,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; } } @@ -354,6 +401,93 @@ return $groups; } +/* + * Register a simple class loader that can find D8-style PSR-0 test classes. + * + * Other PSR-0 class loading can happen in contrib, but those contrib class + * loader modules will not be enabled when testbot runs. So we need to do this + * one in core. + */ +function simpletest_classloader_register() { + + // Prevent duplicate classloader registration. + static $first_run = TRUE; + if (!$first_run) { + return; + } + $first_run = FALSE; + + // 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_psr4_psr0'); + } +} + +/** + * 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_psr4_psr0($class) { + + // Static cache for extension paths. + // This cache is lazily filled as soon as it is needed. + static $extensions; + + // Check that the first namespace fragment is "Drupal\" + if (substr($class, 0, 7) === 'Drupal\\') { + // Find the position of the second namespace separator. + $pos = strpos($class, '\\', 7); + // Check that the third namespace fragment is "\Tests\". + if (substr($class, $pos, 7) === '\\Tests\\') { + + // Extract the second namespace fragment, which we expect to be the + // extension name. + $extension = substr($class, 7, $pos - 7); + + // Lazy-load the extension paths, both enabled and disabled. + if (!isset($extensions)) { + $extensions = db_query("SELECT name, filename FROM {system}")->fetchAllKeyed(); + } + + // Check if the second namespace fragment is a known extension name. + if (isset($extensions[$extension])) { + + // Split the class into namespace and classname. + $nspos = strrpos($class, '\\'); + $namespace = substr($class, 0, $nspos); + $classname = substr($class, $nspos + 1); + + // 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($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; + } + } + } + } + } +} + /** * Implements hook_registry_files_alter(). * @@ -381,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 symetrical file. // Create filename. file_put_contents('public://' . $filename . '.txt', $text); @@ -453,13 +587,15 @@ * Find all leftover temporary directories and remove them. */ function simpletest_clean_temporary_directories() { - $files = scandir('public://simpletest'); $count = 0; - foreach ($files as $file) { - $path = 'public://simpletest/' . $file; - if (is_dir($path) && is_numeric($file)) { - file_unmanaged_delete_recursive($path); - $count++; + if (is_dir('public://simpletest')) { + $files = scandir('public://simpletest'); + foreach ($files as $file) { + $path = 'public://simpletest/' . $file; + if (is_dir($path) && is_numeric($file)) { + file_unmanaged_delete_recursive($path); + $count++; + } } } @@ -503,3 +639,16 @@ } return 0; } + +/** + * Implements hook_mail_alter(). + * + * Aborts sending of messages with ID 'simpletest_cancel_test'. + * + * @see MailTestCase::testCancelMessage() + */ +function simpletest_mail_alter(&$message) { + if ($message['id'] == 'simpletest_cancel_test') { + $message['send'] = FALSE; + } +} diff -Naur drupal-7.0/modules/simpletest/simpletest.pages.inc drupal-7.66/modules/simpletest/simpletest.pages.inc --- drupal-7.0/modules/simpletest/simpletest.pages.inc 2010-10-20 03:31:07.000000000 +0200 +++ drupal-7.66/modules/simpletest/simpletest.pages.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: simpletest.pages.inc,v 1.33 2010/10/20 01:31:07 dries Exp $ /** * @file @@ -83,8 +82,8 @@ // Define the images used to expand/collapse the test groups. $js = array( 'images' => array( - theme('image', array('path' => 'misc/menu-collapsed.png', 'alt' => t('Expand'), 'title' => t('Expand'))) . ' <a href="#" class="simpletest-collapse">(' . t('Expand') . ')</a>', - theme('image', array('path' => 'misc/menu-expanded.png', 'alt' => t('Collapse'), 'title' => t('Collapse'))) . ' <a href="#" class="simpletest-collapse">(' . t('Collapse') . ')</a>', + theme('image', array('path' => 'misc/menu-collapsed.png', 'width' => 7, 'height' => 7, 'alt' => t('Expand'), 'title' => t('Expand'))) . ' <a href="#" class="simpletest-collapse">(' . t('Expand') . ')</a>', + theme('image', array('path' => 'misc/menu-expanded.png', 'width' => 7, 'height' => 7, 'alt' => t('Collapse'), 'title' => t('Collapse'))) . ' <a href="#" class="simpletest-collapse">(' . t('Collapse') . ')</a>', ), ); @@ -129,7 +128,7 @@ ); // Sorting $element by children's #title attribute instead of by class name. - uasort($element, '_simpletest_sort_by_title'); + uasort($element, 'element_sort_by_title'); // Cycle through each test within the current group. foreach (element_children($element) as $test_name) { @@ -179,21 +178,10 @@ } /** - * Sort element by title instead of by class name. - */ -function _simpletest_sort_by_title($a, $b) { - // This is for parts of $element that are not an array. - if (!isset($a['#title']) || !isset($b['#title'])) { - return 1; - } - - return strcasecmp($a['#title'], $b['#title']); -} - -/** * Run selected tests. */ function simpletest_test_form_submit($form, &$form_state) { + simpletest_classloader_register(); // Get list of tests. $tests_list = array(); foreach ($form_state['values'] as $class_name => $value) { @@ -246,6 +234,8 @@ '#debug' => 0, ); + simpletest_classloader_register(); + // Cycle through each test group. $header = array(t('Message'), t('Group'), t('Filename'), t('Line'), t('Function'), array('colspan' => 2, 'data' => t('Status'))); $form['result']['results'] = array(); @@ -267,7 +257,7 @@ $row = array(); $row[] = $assertion->message; $row[] = $assertion->message_group; - $row[] = basename($assertion->file); + $row[] = drupal_basename($assertion->file); $row[] = $assertion->line; $row[] = $assertion->function; $row[] = simpletest_result_status_image($assertion->status); @@ -318,7 +308,7 @@ ); $form['action']['filter']['#default_value'] = ($filter['fail'] ? 'fail' : 'all'); - // Catagorized test classes for to be used with selected filter value. + // Categorized test classes for to be used with selected filter value. $form['action']['filter_pass'] = array( '#type' => 'hidden', '#default_value' => implode(',', $filter['pass']), @@ -427,10 +417,10 @@ if (!isset($map)) { $map = array( - 'pass' => theme('image', array('path' => 'misc/watchdog-ok.png', 'alt' => t('Pass'))), - 'fail' => theme('image', array('path' => 'misc/watchdog-error.png', 'alt' => t('Fail'))), - 'exception' => theme('image', array('path' => 'misc/watchdog-warning.png', 'alt' => t('Exception'))), - 'debug' => theme('image', array('path' => 'misc/watchdog-warning.png', 'alt' => t('Debug'))), + 'pass' => theme('image', array('path' => 'misc/watchdog-ok.png', 'width' => 18, 'height' => 18, 'alt' => t('Pass'))), + 'fail' => theme('image', array('path' => 'misc/watchdog-error.png', 'width' => 18, 'height' => 18, 'alt' => t('Fail'))), + 'exception' => theme('image', array('path' => 'misc/watchdog-warning.png', 'width' => 18, 'height' => 18, 'alt' => t('Exception'))), + 'debug' => theme('image', array('path' => 'misc/watchdog-warning.png', 'width' => 18, 'height' => 18, 'alt' => t('Debug'))), ); } if (isset($map[$status])) { @@ -441,6 +431,9 @@ /** * Provides settings form for SimpleTest variables. + * + * @ingroup forms + * @see simpletest_settings_form_validate() */ function simpletest_settings_form($form, &$form_state) { $form['general'] = array( @@ -480,16 +473,41 @@ ), '#default_value' => variable_get('simpletest_httpauth_method', CURLAUTH_BASIC), ); + $username = variable_get('simpletest_httpauth_username'); + $password = variable_get('simpletest_httpauth_password'); $form['httpauth']['simpletest_httpauth_username'] = array( '#type' => 'textfield', '#title' => t('Username'), - '#default_value' => variable_get('simpletest_httpauth_username', ''), + '#default_value' => $username, ); + if ($username && $password) { + $form['httpauth']['simpletest_httpauth_username']['#description'] = t('Leave this blank to delete both the existing username and password.'); + } $form['httpauth']['simpletest_httpauth_password'] = array( - '#type' => 'textfield', + '#type' => 'password', '#title' => t('Password'), - '#default_value' => variable_get('simpletest_httpauth_password', ''), ); + if ($password) { + $form['httpauth']['simpletest_httpauth_password']['#description'] = t('To change the password, enter the new password here.'); + } return system_settings_form($form); } + +/** + * Validation handler for simpletest_settings_form(). + */ +function simpletest_settings_form_validate($form, &$form_state) { + // If a username was provided but a password wasn't, preserve the existing + // password. + if (!empty($form_state['values']['simpletest_httpauth_username']) && empty($form_state['values']['simpletest_httpauth_password'])) { + $form_state['values']['simpletest_httpauth_password'] = variable_get('simpletest_httpauth_password', ''); + } + + // If a password was provided but a username wasn't, the credentials are + // incorrect, so throw an error. + if (empty($form_state['values']['simpletest_httpauth_username']) && !empty($form_state['values']['simpletest_httpauth_password'])) { + form_set_error('simpletest_httpauth_username', t('HTTP authentication credentials must include a username in addition to a password.')); + } +} + diff -Naur drupal-7.0/modules/simpletest/simpletest.test drupal-7.66/modules/simpletest/simpletest.test --- drupal-7.0/modules/simpletest/simpletest.test 2010-11-12 04:06:52.000000000 +0100 +++ drupal-7.66/modules/simpletest/simpletest.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,9 @@ <?php -// $Id: simpletest.test,v 1.48 2010/11/12 03:06:52 dries Exp $ + +/** + * @file + * Tests for simpletest.module. + */ class SimpleTestFunctionalTest extends DrupalWebTestCase { /** @@ -16,10 +20,7 @@ public static function getInfo() { return array( 'name' => 'SimpleTest functionality', - 'description' => 'Test SimpleTest\'s web interface: check that the intended tests were - run and ensure that test reports display the intended results. Also - test SimpleTest\'s internal browser and API\'s both explicitly and - implicitly.', + 'description' => "Test SimpleTest's web interface: check that the intended tests were run and ensure that test reports display the intended results. Also test SimpleTest's internal browser and API's both explicitly and implicitly.", 'group' => 'SimpleTest' ); } @@ -33,7 +34,7 @@ $this->drupalLogin($admin_user); } else { - parent::setUp(); + parent::setUp('non_existent_module'); } } @@ -44,9 +45,9 @@ global $conf; if (!$this->inCURL()) { $this->drupalGet('node'); - $this->assertTrue($this->drupalGetHeader('Date'), t('An HTTP header was received.')); - $this->assertTitle(t('Welcome to @site-name | @site-name', array('@site-name' => variable_get('site_name', 'Drupal'))), t('Site title matches.')); - $this->assertNoTitle('Foo', t('Site title does not match.')); + $this->assertTrue($this->drupalGetHeader('Date'), 'An HTTP header was received.'); + $this->assertTitle(t('Welcome to @site-name | @site-name', array('@site-name' => variable_get('site_name', 'Drupal'))), 'Site title matches.'); + $this->assertNoTitle('Foo', 'Site title does not match.'); // Make sure that we are locked out of the installer when prefixing // using the user-agent header. This is an important security check. global $base_url; @@ -57,12 +58,12 @@ $user = $this->drupalCreateUser(); $this->drupalLogin($user); $headers = $this->drupalGetHeaders(TRUE); - $this->assertEqual(count($headers), 2, t('There was one intermediate request.')); - $this->assertTrue(strpos($headers[0][':status'], '302') !== FALSE, t('Intermediate response code was 302.')); - $this->assertFalse(empty($headers[0]['location']), t('Intermediate request contained a Location header.')); - $this->assertEqual($this->getUrl(), $headers[0]['location'], t('HTTP redirect was followed')); - $this->assertFalse($this->drupalGetHeader('Location'), t('Headers from intermediate request were reset.')); - $this->assertResponse(200, t('Response code from intermediate request was reset.')); + $this->assertEqual(count($headers), 2, 'There was one intermediate request.'); + $this->assertTrue(strpos($headers[0][':status'], '302') !== FALSE, 'Intermediate response code was 302.'); + $this->assertFalse(empty($headers[0]['location']), 'Intermediate request contained a Location header.'); + $this->assertEqual($this->getUrl(), $headers[0]['location'], 'HTTP redirect was followed'); + $this->assertFalse($this->drupalGetHeader('Location'), 'Headers from intermediate request were reset.'); + $this->assertResponse(200, 'Response code from intermediate request was reset.'); // Test the maximum redirection option. $this->drupalLogout(); @@ -73,7 +74,7 @@ variable_set('simpletest_maximum_redirects', 1); $this->drupalPost('user?destination=user/logout', $edit, t('Log in')); $headers = $this->drupalGetHeaders(TRUE); - $this->assertEqual(count($headers), 2, t('Simpletest stopped following redirects after the first one.')); + $this->assertEqual(count($headers), 2, 'Simpletest stopped following redirects after the first one.'); } } @@ -87,30 +88,30 @@ $HTTP_path = $simpletest_path .'/tests/http.php?q=node'; $https_path = $simpletest_path .'/tests/https.php?q=node'; // Generate a valid simpletest User-Agent to pass validation. - $this->assertTrue(preg_match('/simpletest\d+/', $this->databasePrefix, $matches), t('Database prefix contains simpletest prefix.')); + $this->assertTrue(preg_match('/simpletest\d+/', $this->databasePrefix, $matches), 'Database prefix contains simpletest prefix.'); $test_ua = drupal_generate_test_ua($matches[0]); $this->additionalCurlOptions = array(CURLOPT_USERAGENT => $test_ua); // Test pages only available for testing. $this->drupalGet($HTTP_path); - $this->assertResponse(200, t('Requesting http.php with a legitimate simpletest User-Agent returns OK.')); + $this->assertResponse(200, 'Requesting http.php with a legitimate simpletest User-Agent returns OK.'); $this->drupalGet($https_path); - $this->assertResponse(200, t('Requesting https.php with a legitimate simpletest User-Agent returns OK.')); + $this->assertResponse(200, 'Requesting https.php with a legitimate simpletest User-Agent returns OK.'); // Now slightly modify the HMAC on the header, which should not validate. $this->additionalCurlOptions = array(CURLOPT_USERAGENT => $test_ua . 'X'); $this->drupalGet($HTTP_path); - $this->assertResponse(403, t('Requesting http.php with a bad simpletest User-Agent fails.')); + $this->assertResponse(403, 'Requesting http.php with a bad simpletest User-Agent fails.'); $this->drupalGet($https_path); - $this->assertResponse(403, t('Requesting https.php with a bad simpletest User-Agent fails.')); + $this->assertResponse(403, 'Requesting https.php with a bad simpletest User-Agent fails.'); // Use a real User-Agent and verify that the special files http.php and // https.php can't be accessed. $this->additionalCurlOptions = array(CURLOPT_USERAGENT => 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'); $this->drupalGet($HTTP_path); - $this->assertResponse(403, t('Requesting http.php with a normal User-Agent fails.')); + $this->assertResponse(403, 'Requesting http.php with a normal User-Agent fails.'); $this->drupalGet($https_path); - $this->assertResponse(403, t('Requesting https.php with a normal User-Agent fails.')); + $this->assertResponse(403, 'Requesting https.php with a normal User-Agent fails.'); } } @@ -146,7 +147,7 @@ // Regression test for #290316. // Check that test_id is incrementing. - $this->assertTrue($this->test_ids[0] != $this->test_ids[1], t('Test ID is incrementing.')); + $this->assertTrue($this->test_ids[0] != $this->test_ids[1], 'Test ID is incrementing.'); } } @@ -185,6 +186,8 @@ * Confirm that the stub test produced the desired results. */ function confirmStubTestResults() { + $this->assertAssertion(t('Enabled modules: %modules', array('%modules' => 'non_existent_module')), 'Other', 'Fail', 'simpletest.test', 'SimpleTestFunctionalTest->setUp()'); + $this->assertAssertion($this->pass, 'Other', 'Pass', 'simpletest.test', 'SimpleTestFunctionalTest->stubTest()'); $this->assertAssertion($this->fail, 'Other', 'Fail', 'simpletest.test', 'SimpleTestFunctionalTest->stubTest()'); @@ -204,10 +207,10 @@ $this->assertAssertion("Debug: 'Foo'", 'Debug', 'Fail', 'simpletest.test', 'SimpleTestFunctionalTest->stubTest()'); - $this->assertEqual('6 passes, 2 fails, 2 exceptions, and 1 debug message', $this->childTestResults['summary'], 'Stub test summary is correct'); + $this->assertEqual('6 passes, 5 fails, 2 exceptions, and 1 debug message', $this->childTestResults['summary'], 'Stub test summary is correct'); $this->test_ids[] = $test_id = $this->getTestIdFromResults(); - $this->assertTrue($test_id, t('Found test ID in results.')); + $this->assertTrue($test_id, 'Found test ID in results.'); } /** @@ -246,7 +249,7 @@ break; } } - return $this->assertTrue($found, t('Found assertion {"@message", "@type", "@status", "@file", "@function"}.', array('@message' => $message, '@type' => $type, '@status' => $status, "@file" => $file, "@function" => $function))); + return $this->assertTrue($found, format_string('Found assertion {"@message", "@type", "@status", "@file", "@function"}.', array('@message' => $message, '@type' => $type, '@status' => $status, "@file" => $file, "@function" => $function))); } /** @@ -319,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', @@ -342,18 +353,18 @@ $this->drupalGet($url); $absolute = url($url, array('absolute' => TRUE)); - $this->assertEqual($absolute, $this->url, t('Passed and requested URL are equal.')); - $this->assertEqual($this->url, $this->getAbsoluteUrl($this->url), t('Requested and returned absolute URL are equal.')); + $this->assertEqual($absolute, $this->url, 'Passed and requested URL are equal.'); + $this->assertEqual($this->url, $this->getAbsoluteUrl($this->url), 'Requested and returned absolute URL are equal.'); $this->drupalPost(NULL, array(), t('Log in')); - $this->assertEqual($absolute, $this->url, t('Passed and requested URL are equal.')); - $this->assertEqual($this->url, $this->getAbsoluteUrl($this->url), t('Requested and returned absolute URL are equal.')); + $this->assertEqual($absolute, $this->url, 'Passed and requested URL are equal.'); + $this->assertEqual($this->url, $this->getAbsoluteUrl($this->url), 'Requested and returned absolute URL are equal.'); $this->clickLink('Create new account'); $url = 'user/register'; $absolute = url($url, array('absolute' => TRUE)); - $this->assertEqual($absolute, $this->url, t('Passed and requested URL are equal.')); - $this->assertEqual($this->url, $this->getAbsoluteUrl($this->url), t('Requested and returned absolute URL are equal.')); + $this->assertEqual($absolute, $this->url, 'Passed and requested URL are equal.'); + $this->assertEqual($this->url, $this->getAbsoluteUrl($this->url), 'Requested and returned absolute URL are equal.'); } /** @@ -377,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 { @@ -408,19 +459,19 @@ // Before we send the e-mail, drupalGetMails should return an empty array. $captured_emails = $this->drupalGetMails(); - $this->assertEqual(count($captured_emails), 0, t('The captured e-mails queue is empty.'), t('E-mail')); + $this->assertEqual(count($captured_emails), 0, 'The captured e-mails queue is empty.', 'E-mail'); // Send the e-mail. $response = drupal_mail_system('simpletest', 'drupal_mail_test')->mail($message); // Ensure that there is one e-mail in the captured e-mails array. $captured_emails = $this->drupalGetMails(); - $this->assertEqual(count($captured_emails), 1, t('One e-mail was captured.'), t('E-mail')); + $this->assertEqual(count($captured_emails), 1, 'One e-mail was captured.', 'E-mail'); // Assert that the e-mail was sent by iterating over the message properties // and ensuring that they are captured intact. foreach ($message as $field => $value) { - $this->assertMail($field, $value, t('The e-mail was sent and the value for property @field is intact.', array('@field' => $field)), t('E-mail')); + $this->assertMail($field, $value, format_string('The e-mail was sent and the value for property @field is intact.', array('@field' => $field)), 'E-mail'); } // Send additional e-mails so more than one e-mail is captured. @@ -437,21 +488,21 @@ // There should now be 6 e-mails captured. $captured_emails = $this->drupalGetMails(); - $this->assertEqual(count($captured_emails), 6, t('All e-mails were captured.'), t('E-mail')); + $this->assertEqual(count($captured_emails), 6, 'All e-mails were captured.', 'E-mail'); // Test different ways of getting filtered e-mails via drupalGetMails(). $captured_emails = $this->drupalGetMails(array('id' => 'drupal_mail_test')); - $this->assertEqual(count($captured_emails), 1, t('Only one e-mail is returned when filtering by id.'), t('E-mail')); + $this->assertEqual(count($captured_emails), 1, 'Only one e-mail is returned when filtering by id.', 'E-mail'); $captured_emails = $this->drupalGetMails(array('id' => 'drupal_mail_test', 'subject' => $subject)); - $this->assertEqual(count($captured_emails), 1, t('Only one e-mail is returned when filtering by id and subject.'), t('E-mail')); + $this->assertEqual(count($captured_emails), 1, 'Only one e-mail is returned when filtering by id and subject.', 'E-mail'); $captured_emails = $this->drupalGetMails(array('id' => 'drupal_mail_test', 'subject' => $subject, 'from' => 'this_was_not_used@example.com')); - $this->assertEqual(count($captured_emails), 0, t('No e-mails are returned when querying with an unused from address.'), t('E-mail')); + $this->assertEqual(count($captured_emails), 0, 'No e-mails are returned when querying with an unused from address.', 'E-mail'); // Send the last e-mail again, so we can confirm that the drupalGetMails-filter // correctly returns all e-mails with a given property/value. drupal_mail_system('drupal_mail_test', $index)->mail($message); $captured_emails = $this->drupalGetMails(array('id' => 'drupal_mail_test_4')); - $this->assertEqual(count($captured_emails), 2, t('All e-mails with the same id are returned when filtering by id.'), t('E-mail')); + $this->assertEqual(count($captured_emails), 2, 'All e-mails with the same id are returned when filtering by id.', 'E-mail'); } } @@ -473,7 +524,7 @@ function testFolderSetup() { $directory = file_default_scheme() . '://styles'; - $this->assertTrue(file_prepare_directory($directory, FALSE), "Directory created."); + $this->assertTrue(file_prepare_directory($directory, FALSE), 'Directory created.'); } } @@ -497,3 +548,254 @@ $this->fail(t('Running test with missing required module.')); } } + +/** + * Tests a test case that does not run parent::setUp() in its setUp() method. + * + * If a test case does not call parent::setUp(), running + * DrupalTestCase::tearDown() would destroy the main site's database tables. + * Therefore, we ensure that tests which are not set up properly are skipped. + * + * @see DrupalTestCase + */ +class SimpleTestBrokenSetUp extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'Broken SimpleTest method', + 'description' => 'Tests a test case that does not call parent::setUp().', + 'group' => 'SimpleTest' + ); + } + + function setUp() { + // If the test is being run from the main site, set up normally. + if (!drupal_valid_test_ua()) { + parent::setUp('simpletest'); + // Create and log in user. + $admin_user = $this->drupalCreateUser(array('administer unit tests')); + $this->drupalLogin($admin_user); + } + // If the test is being run from within simpletest, set up the broken test. + else { + $this->pass(t('The test setUp() method has been run.')); + // Don't call parent::setUp(). This should trigger an error message. + } + } + + function tearDown() { + // If the test is being run from the main site, tear down normally. + if (!drupal_valid_test_ua()) { + parent::tearDown(); + } + else { + // If the test is being run from within simpletest, output a message. + $this->pass(t('The tearDown() method has run.')); + } + } + + /** + * Runs this test case from within the simpletest child site. + */ + function testBreakSetUp() { + // If the test is being run from the main site, run it again from the web + // interface within the simpletest child site. + if (!drupal_valid_test_ua()) { + $edit['SimpleTestBrokenSetUp'] = TRUE; + $this->drupalPost('admin/config/development/testing', $edit, t('Run tests')); + + // Verify that the broken test and its tearDown() method are skipped. + $this->assertRaw(t('The test setUp() method has been run.')); + $this->assertRaw(t('The test cannot be executed because it has not been set up properly.')); + $this->assertNoRaw(t('The test method has run.')); + $this->assertNoRaw(t('The tearDown() method has run.')); + } + // If the test is being run from within simpletest, output a message. + else { + $this->pass(t('The test method has run.')); + } + } +} + +/** + * Verifies that tests bundled with installation profile modules are found. + */ +class SimpleTestInstallationProfileModuleTestsTestCase extends DrupalWebTestCase { + /** + * Use the Testing profile. + * + * The Testing profile contains drupal_system_listing_compatible_test.test, + * which attempts to: + * - run tests using the Minimal profile (which does not contain the + * drupal_system_listing_compatible_test.module) + * - but still install the drupal_system_listing_compatible_test.module + * contained in the Testing profile. + * + * @see DrupalSystemListingCompatibleTestCase + */ + protected $profile = 'testing'; + + public static function getInfo() { + return array( + 'name' => 'Installation profile module tests', + 'description' => 'Verifies that tests bundled with installation profile modules are found.', + 'group' => 'SimpleTest', + ); + } + + function setUp() { + parent::setUp(array('simpletest')); + + $this->admin_user = $this->drupalCreateUser(array('administer unit tests')); + $this->drupalLogin($this->admin_user); + } + + /** + * Tests existence of test case located in an installation profile module. + */ + function testInstallationProfileTests() { + $this->drupalGet('admin/config/development/testing'); + $this->assertText('Installation profile module tests helper'); + $edit = array( + 'DrupalSystemListingCompatibleTestCase' => TRUE, + ); + $this->drupalPost(NULL, $edit, t('Run tests')); + $this->assertText('DrupalSystemListingCompatibleTestCase test executed.'); + } +} + +/** + * Verifies that tests in other installation profiles are not found. + * + * @see SimpleTestInstallationProfileModuleTestsTestCase + */ +class SimpleTestOtherInstallationProfileModuleTestsTestCase extends DrupalWebTestCase { + /** + * Use the Minimal profile. + * + * The Testing profile contains drupal_system_listing_compatible_test.test, + * which should not be found. + * + * @see SimpleTestInstallationProfileModuleTestsTestCase + * @see DrupalSystemListingCompatibleTestCase + */ + protected $profile = 'minimal'; + + public static function getInfo() { + return array( + 'name' => 'Other Installation profiles', + 'description' => 'Verifies that tests in other installation profiles are not found.', + 'group' => 'SimpleTest', + ); + } + + function setUp() { + parent::setUp(array('simpletest')); + + $this->admin_user = $this->drupalCreateUser(array('administer unit tests')); + $this->drupalLogin($this->admin_user); + } + + /** + * Tests that tests located in another installation profile do not appear. + */ + function testOtherInstallationProfile() { + $this->drupalGet('admin/config/development/testing'); + $this->assertNoText('Installation profile module tests helper'); + } +} + +/** + * Verifies that tests in other installation profiles are not found. + * + * @see SimpleTestInstallationProfileModuleTestsTestCase + */ +class SimpleTestDiscoveryTestCase extends DrupalWebTestCase { + /** + * Use the Testing profile. + * + * The Testing profile contains drupal_system_listing_compatible_test.test, + * which attempts to: + * - run tests using the Minimal profile (which does not contain the + * drupal_system_listing_compatible_test.module) + * - but still install the drupal_system_listing_compatible_test.module + * contained in the Testing profile. + * + * @see DrupalSystemListingCompatibleTestCase + */ + protected $profile = 'testing'; + + public static function getInfo() { + return array( + 'name' => 'Discovery of test classes', + 'description' => 'Verifies that tests classes are discovered and can be autoloaded (class_exists).', + 'group' => 'SimpleTest', + ); + } + + function setUp() { + parent::setUp(array('simpletest')); + + $this->admin_user = $this->drupalCreateUser(array('administer unit tests')); + $this->drupalLogin($this->admin_user); + } + + /** + * Test discovery of PSR-0 test classes. + */ + function testDiscoveryFunctions() { + if (version_compare(PHP_VERSION, '5.3') < 0) { + // Don't expect PSR-0 tests to be discovered on older PHP versions. + return; + } + // TODO: What if we have cached values? Do we need to force a cache refresh? + $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))); + } + } + + /** + * Tests existence of test cases. + */ + function testDiscovery() { + $this->drupalGet('admin/config/development/testing'); + // Tests within enabled modules. + // (without these, this test wouldn't happen in the first place, so this is + // a bit pointless. We still run it for proof-of-concept.) + // This one is defined in system module. + $this->assertText('Drupal error handlers'); + // This one is defined in simpletest module. + $this->assertText('Discovery of test classes'); + // Tests within disabled modules. + if (version_compare(PHP_VERSION, '5.3') < 0) { + // Don't expect PSR-0 tests to be discovered on older PHP versions. + return; + } + // 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); + $this->drupalPost(NULL, $edit, t('Run tests')); + $this->assertText('The test run finished', t('Test @class must finish.', array('@class' => $class))); + $this->assertText('1 pass, 0 fails, and 0 exceptions', t('Test @class must pass.', array('@class' => $class))); + } + } +} diff -Naur drupal-7.0/modules/simpletest/src/Tests/PSR4WebTest.php drupal-7.66/modules/simpletest/src/Tests/PSR4WebTest.php --- drupal-7.0/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.0/modules/simpletest/tests/actions.test drupal-7.66/modules/simpletest/tests/actions.test --- drupal-7.0/modules/simpletest/tests/actions.test 2010-08-06 01:53:38.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/actions.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: actions.test,v 1.18 2010/08/05 23:53:38 webchick Exp $ class ActionsConfigurationTestCase extends DrupalWebTestCase { public static function getInfo() { @@ -98,7 +97,7 @@ // recursion level should be kept low enough to prevent the xdebug // infinite recursion protection mechanism from aborting the request. // See http://drupal.org/node/587634. - variable_set('actions_max_stack', mt_rand(3, 12)); + variable_set('actions_max_stack', 7); $this->triggerActions(); } diff -Naur drupal-7.0/modules/simpletest/tests/actions_loop_test.info drupal-7.66/modules/simpletest/tests/actions_loop_test.info --- drupal-7.0/modules/simpletest/tests/actions_loop_test.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/actions_loop_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: actions_loop_test.info,v 1.2 2010/12/20 19:59:43 webchick Exp $ name = Actions loop test description = Support module for action loop testing. package = Testing @@ -6,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/simpletest/tests/actions_loop_test.install drupal-7.66/modules/simpletest/tests/actions_loop_test.install --- drupal-7.0/modules/simpletest/tests/actions_loop_test.install 2009-12-04 17:49:47.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/actions_loop_test.install 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: actions_loop_test.install,v 1.2 2009/12/04 16:49:47 dries Exp $ /** * Implements hook_install(). diff -Naur drupal-7.0/modules/simpletest/tests/actions_loop_test.module drupal-7.66/modules/simpletest/tests/actions_loop_test.module --- drupal-7.0/modules/simpletest/tests/actions_loop_test.module 2009-12-04 17:49:47.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/actions_loop_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: actions_loop_test.module,v 1.3 2009/12/04 16:49:47 dries Exp $ /** * Implements hook_trigger_info(). @@ -82,6 +81,7 @@ 'severity' => $severity, 'link' => $link, 'user' => $user, + 'uid' => isset($user->uid) ? $user->uid : 0, 'request_uri' => $base_root . request_uri(), 'referer' => $_SERVER['HTTP_REFERER'], 'ip' => ip_address(), diff -Naur drupal-7.0/modules/simpletest/tests/ajax.test drupal-7.66/modules/simpletest/tests/ajax.test --- drupal-7.0/modules/simpletest/tests/ajax.test 2010-11-29 04:00:50.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/ajax.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: ajax.test,v 1.21 2010/11/29 03:00:50 webchick Exp $ class AJAXTestCase extends DrupalWebTestCase { function setUp() { @@ -11,9 +10,9 @@ } /** - * Assert that a command with the required properties exists within the array of AJAX commands returned by the server. + * Assert that a command with the required properties exists within the array of Ajax commands returned by the server. * - * The AJAX framework, via the ajax_deliver() and ajax_render() functions, + * The Ajax framework, via the ajax_deliver() and ajax_render() functions, * returns an array of commands. This array sometimes includes commands * automatically provided by the framework in addition to commands returned by * a particular page callback. During testing, we're usually interested that a @@ -28,7 +27,7 @@ * additional settings that aren't part of $needle. * * @param $haystack - * An array of AJAX commands returned by the server. + * An array of Ajax commands returned by the server. * @param $needle * Array of info we're expecting in one of those commands. * @param $message @@ -56,7 +55,7 @@ } /** - * Tests primary AJAX framework functions. + * Tests primary Ajax framework functions. */ class AJAXFrameworkTestCase extends AJAXTestCase { protected $profile = 'testing'; @@ -87,7 +86,7 @@ ); $this->assertCommand($commands, $expected, t('ajax_render() loads settings added with drupal_add_js().')); - // Verify that AJAX settings are loaded for #type 'link'. + // Verify that Ajax settings are loaded for #type 'link'. $this->drupalGet('ajax-test/link'); $settings = $this->drupalGetSettings(); $this->assertEqual($settings['ajax']['ajax-link']['url'], url('filter/tips')); @@ -117,10 +116,110 @@ ); $this->assertCommand($commands, $expected, t('Custom error message is output.')); } + + /** + * Test that new JavaScript and CSS files added during an AJAX request are returned. + */ + function testLazyLoad() { + $expected = array( + 'setting_name' => 'ajax_forms_test_lazy_load_form_submit', + 'setting_value' => 'executed', + 'css' => drupal_get_path('module', 'system') . '/system.admin.css', + 'js' => drupal_get_path('module', 'system') . '/system.js', + ); + // @todo D8: Add a drupal_css_defaults() helper function. + $expected_css_html = drupal_get_css(array($expected['css'] => array( + 'type' => 'file', + 'group' => CSS_DEFAULT, + 'weight' => 0, + 'every_page' => FALSE, + 'media' => 'all', + 'preprocess' => TRUE, + 'data' => $expected['css'], + 'browsers' => array('IE' => TRUE, '!IE' => TRUE), + )), TRUE); + $expected_js_html = drupal_get_js('header', array($expected['js'] => drupal_js_defaults($expected['js'])), TRUE); + + // Get the base page. + $this->drupalGet('ajax_forms_test_lazy_load_form'); + $original_settings = $this->drupalGetSettings(); + $original_css = $original_settings['ajaxPageState']['css']; + $original_js = $original_settings['ajaxPageState']['js']; + + // Verify that the base page doesn't have the settings and files that are to + // be lazy loaded as part of the next requests. + $this->assertTrue(!isset($original_settings[$expected['setting_name']]), t('Page originally lacks the %setting, as expected.', array('%setting' => $expected['setting_name']))); + $this->assertTrue(!isset($original_settings[$expected['css']]), t('Page originally lacks the %css file, as expected.', array('%css' => $expected['css']))); + $this->assertTrue(!isset($original_settings[$expected['js']]), t('Page originally lacks the %js file, as expected.', array('%js' => $expected['js']))); + + // Submit the AJAX request without triggering files getting added. + $commands = $this->drupalPostAJAX(NULL, array('add_files' => FALSE), array('op' => t('Submit'))); + $new_settings = $this->drupalGetSettings(); + + // Verify the setting was not added when not expected. + $this->assertTrue(!isset($new_settings['setting_name']), t('Page still lacks the %setting, as expected.', array('%setting' => $expected['setting_name']))); + // Verify a settings command does not add CSS or scripts to Drupal.settings + // and no command inserts the corresponding tags on the page. + $found_settings_command = FALSE; + $found_markup_command = FALSE; + foreach ($commands as $command) { + if ($command['command'] == 'settings' && (array_key_exists('css', $command['settings']['ajaxPageState']) || array_key_exists('js', $command['settings']['ajaxPageState']))) { + $found_settings_command = TRUE; + } + if (isset($command['data']) && ($command['data'] == $expected_js_html || $command['data'] == $expected_css_html)) { + $found_markup_command = TRUE; + } + } + $this->assertFalse($found_settings_command, t('Page state still lacks the %css and %js files, as expected.', array('%css' => $expected['css'], '%js' => $expected['js']))); + $this->assertFalse($found_markup_command, t('Page still lacks the %css and %js files, as expected.', array('%css' => $expected['css'], '%js' => $expected['js']))); + + // Submit the AJAX request and trigger adding files. + $commands = $this->drupalPostAJAX(NULL, array('add_files' => TRUE), array('op' => t('Submit'))); + $new_settings = $this->drupalGetSettings(); + $new_css = $new_settings['ajaxPageState']['css']; + $new_js = $new_settings['ajaxPageState']['js']; + + // Verify the expected setting was added. + $this->assertIdentical($new_settings[$expected['setting_name']], $expected['setting_value'], t('Page now has the %setting.', array('%setting' => $expected['setting_name']))); + + // Verify the expected CSS file was added, both to Drupal.settings, and as + // an AJAX command for inclusion into the HTML. + $this->assertEqual($new_css, $original_css + array($expected['css'] => 1), t('Page state now has the %css file.', array('%css' => $expected['css']))); + $this->assertCommand($commands, array('data' => $expected_css_html), t('Page now has the %css file.', array('%css' => $expected['css']))); + + // Verify the expected JS file was added, both to Drupal.settings, and as + // an AJAX command for inclusion into the HTML. By testing for an exact HTML + // string containing the SCRIPT tag, we also ensure that unexpected + // JavaScript code, such as a jQuery.extend() that would potentially clobber + // rather than properly merge settings, didn't accidentally get added. + $this->assertEqual($new_js, $original_js + array($expected['js'] => 1), t('Page state now has the %js file.', array('%js' => $expected['js']))); + $this->assertCommand($commands, array('data' => $expected_js_html), t('Page now has the %js file.', array('%js' => $expected['js']))); + } + + /** + * Tests that overridden CSS files are not added during lazy load. + */ + function testLazyLoadOverriddenCSS() { + // The test theme overrides system.base.css without an implementation, + // thereby removing it. + theme_enable(array('test_theme')); + variable_set('theme_default', 'test_theme'); + + // This gets the form, and emulates an Ajax submission on it, including + // adding markup to the HEAD and BODY for any lazy loaded JS/CSS files. + $this->drupalPostAJAX('ajax_forms_test_lazy_load_form', array('add_files' => TRUE), array('op' => t('Submit'))); + + // Verify that the resulting HTML does not load the overridden CSS file. + // We add a "?" to the assertion, because Drupal.settings may include + // information about the file; we only really care about whether it appears + // in a LINK or STYLE tag, for which Drupal always adds a query string for + // cache control. + $this->assertNoText('system.base.css?', 'Ajax lazy loading does not add overridden CSS files.'); + } } /** - * Tests AJAX framework commands. + * Tests Ajax framework commands. */ class AJAXCommandsTestCase extends AJAXTestCase { public static function getInfo() { @@ -132,7 +231,7 @@ } /** - * Test the various AJAX Commands. + * Test the various Ajax Commands. */ function testAJAXCommands() { $form_path = 'ajax_forms_test_ajax_commands_form'; @@ -194,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', @@ -269,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"); } } @@ -324,7 +431,7 @@ } /** - * Tests that AJAX-enabled forms work when multiple instances of the same form are on a page. + * Tests that Ajax-enabled forms work when multiple instances of the same form are on a page. */ class AJAXMultiFormTestCase extends AJAXTestCase { public static function getInfo() { @@ -338,7 +445,7 @@ function setUp() { parent::setUp(array('form_test')); - // Create a multi-valued field for 'page' nodes to use for AJAX testing. + // Create a multi-valued field for 'page' nodes to use for Ajax testing. $field_name = 'field_ajax_test'; $field = array( 'field_name' => $field_name, @@ -363,7 +470,7 @@ */ function testMultiForm() { // HTML IDs for elements within the field are potentially modified with - // each AJAX submission, but these variables are stable and help target the + // each Ajax submission, but these variables are stable and help target the // desired elements. $field_name = 'field_ajax_test'; $field_xpaths = array( @@ -399,7 +506,86 @@ } /** - * Miscellaneous AJAX tests using ajax_test module. + * 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 { public static function getInfo() { @@ -411,12 +597,12 @@ } /** - * Try to post an AJAX change to a form that has a validated element. + * Try to post an Ajax change to a form that has a validated element. * - * The drivertext field is AJAX-enabled. An additional field is not, but + * The drivertext field is Ajax-enabled. An additional field is not, but * is set to be a required field. In this test the required field is not * filled in, and we want to see if the activation of the "drivertext" - * AJAX-enabled field fails due to the required field being empty. + * Ajax-enabled field fails due to the required field being empty. */ function testAJAXElementValidation() { $web_user = $this->drupalCreateUser(); @@ -425,7 +611,7 @@ // Post with 'drivertext' as the triggering element. $post_result = $this->drupalPostAJAX('ajax_validation_test', $edit, 'drivertext'); // Look for a validation failure in the resultant JSON. - $this->assertNoText(t('Error message'), t("No error message in resultant JSON")); - $this->assertText('ajax_forms_test_validation_form_callback invoked', t('The correct callback was invoked')); + $this->assertNoText(t('Error message'), "No error message in resultant JSON"); + $this->assertText('ajax_forms_test_validation_form_callback invoked', 'The correct callback was invoked'); } } diff -Naur drupal-7.0/modules/simpletest/tests/ajax_forms_test.info drupal-7.66/modules/simpletest/tests/ajax_forms_test.info --- drupal-7.0/modules/simpletest/tests/ajax_forms_test.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/ajax_forms_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: ajax_forms_test.info,v 1.2 2010/12/20 19:59:43 webchick Exp $ name = "AJAX form test mock module" description = "Test for AJAX form calls." core = 7.x @@ -6,8 +5,7 @@ version = VERSION hidden = TRUE -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/simpletest/tests/ajax_forms_test.module drupal-7.66/modules/simpletest/tests/ajax_forms_test.module --- drupal-7.0/modules/simpletest/tests/ajax_forms_test.module 2010-11-29 04:00:50.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/ajax_forms_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,14 +1,12 @@ <?php -// $Id: ajax_forms_test.module,v 1.9 2010/11/29 03:00:50 webchick Exp $ /** * @file - * Simpletest mock module for AJAX forms testing. + * Simpletest mock module for Ajax forms testing. */ /** * Implements hook_menu(). - * @return unknown_type */ function ajax_forms_test_menu() { $items = array(); @@ -30,6 +28,12 @@ 'page arguments' => array('ajax_forms_test_validation_form'), 'access callback' => TRUE, ); + $items['ajax_forms_test_lazy_load_form'] = array( + 'title' => 'AJAX forms lazy load test', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('ajax_forms_test_lazy_load_form'), + 'access callback' => TRUE, + ); return $items; } @@ -67,7 +71,7 @@ } /** - * AJAX callback triggered by select. + * Ajax callback triggered by select. */ function ajax_forms_test_simple_form_select_callback($form, $form_state) { $commands = array(); @@ -77,7 +81,7 @@ } /** - * AJAX callback triggered by checkbox. + * Ajax callback triggered by checkbox. */ function ajax_forms_test_simple_form_checkbox_callback($form, $form_state) { $commands = array(); @@ -88,10 +92,7 @@ /** - * Form to display the AJAX Commands. - * @param $form - * @param $form_state - * @return unknown_type + * Form to display the Ajax Commands. */ function ajax_forms_test_ajax_commands_form($form, &$form_state) { $form = array(); @@ -154,9 +155,9 @@ ), ); - // Shows the AJAX 'css' command. + // 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', @@ -165,7 +166,7 @@ ); - // Shows the AJAX 'data' command. But there is no use of this information, + // Shows the Ajax 'data' command. But there is no use of this information, // as this would require a javascript client to use the data. $form['data_command_example'] = array( '#value' => t("AJAX data command: Issue command."), @@ -176,7 +177,7 @@ '#suffix' => '<div id="data_div">Data attached to this div.</div>', ); - // Shows the AJAX 'invoke' command. + // Shows the Ajax 'invoke' command. $form['invoke_command_example'] = array( '#value' => t("AJAX invoke command: Invoke addClass() method."), '#type' => 'submit', @@ -186,7 +187,7 @@ '#suffix' => '<div id="invoke_div">Original contents</div>', ); - // Shows the AJAX 'html' command. + // Shows the Ajax 'html' command. $form['html_command_example'] = array( '#value' => t("AJAX html: Replace the HTML in a selector."), '#type' => 'submit', @@ -196,7 +197,7 @@ '#suffix' => '<div id="html_div">Original contents</div>', ); - // Shows the AJAX 'insert' command. + // Shows the Ajax 'insert' command. $form['insert_command_example'] = array( '#value' => t("AJAX insert: Let client insert based on #ajax['method']."), '#type' => 'submit', @@ -207,7 +208,7 @@ '#suffix' => '<div id="insert_div">Original contents</div>', ); - // Shows the AJAX 'prepend' command. + // Shows the Ajax 'prepend' command. $form['prepend_command_example'] = array( '#value' => t("AJAX 'prepend': Click to prepend something"), '#type' => 'submit', @@ -217,7 +218,7 @@ '#suffix' => '<div id="prepend_div">Something will be prepended to this div. </div>', ); - // Shows the AJAX 'remove' command. + // Shows the Ajax 'remove' command. $form['remove_command_example'] = array( '#value' => t("AJAX 'remove': Click to remove text"), '#type' => 'submit', @@ -227,7 +228,7 @@ '#suffix' => '<div id="remove_div"><div id="remove_text">text to be removed</div></div>', ); - // Shows the AJAX 'restripe' command. + // Shows the Ajax 'restripe' command. $form['restripe_command_example'] = array( '#type' => 'submit', '#value' => t("AJAX 'restripe' command"), @@ -242,7 +243,7 @@ </div>', ); - // Demonstrates the AJAX 'settings' command. The 'settings' command has + // Demonstrates the Ajax 'settings' command. The 'settings' command has // nothing visual to "show", but it can be tested via SimpleTest and via // Firebug. $form['settings_command_example'] = array( @@ -253,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'), @@ -262,7 +272,7 @@ } /** - * AJAX callback for 'after'. + * Ajax callback for 'after'. */ function ajax_forms_test_advanced_commands_after_callback($form, $form_state) { $selector = '#after_div'; @@ -273,7 +283,7 @@ } /** - * AJAX callback for 'alert'. + * Ajax callback for 'alert'. */ function ajax_forms_test_advanced_commands_alert_callback($form, $form_state) { $commands = array(); @@ -282,7 +292,7 @@ } /** - * AJAX callback for 'append'. + * Ajax callback for 'append'. */ function ajax_forms_test_advanced_commands_append_callback($form, $form_state) { $selector = '#append_div'; @@ -292,7 +302,7 @@ } /** - * AJAX callback for 'before'. + * Ajax callback for 'before'. */ function ajax_forms_test_advanced_commands_before_callback($form, $form_state) { $selector = '#before_div'; @@ -303,14 +313,14 @@ } /** - * AJAX callback for 'changed'. + * Ajax callback for 'changed'. */ function ajax_forms_test_advanced_commands_changed_callback($form, $form_state) { $commands[] = ajax_command_changed('#changed_div'); return array('#type' => 'ajax', '#commands' => $commands); } /** - * AJAX callback for 'changed' with asterisk marking inner div. + * Ajax callback for 'changed' with asterisk marking inner div. */ function ajax_forms_test_advanced_commands_changed_asterisk_callback($form, $form_state) { $commands = array(); @@ -319,7 +329,7 @@ } /** - * AJAX callback for 'css'. + * Ajax callback for 'css'. */ function ajax_forms_test_advanced_commands_css_callback($form, $form_state) { $selector = '#css_div'; @@ -331,7 +341,7 @@ } /** - * AJAX callback for 'data'. + * Ajax callback for 'data'. */ function ajax_forms_test_advanced_commands_data_callback($form, $form_state) { $selector = '#data_div'; @@ -342,7 +352,7 @@ } /** - * AJAX callback for 'invoke'. + * Ajax callback for 'invoke'. */ function ajax_forms_test_advanced_commands_invoke_callback($form, $form_state) { $commands = array(); @@ -351,7 +361,7 @@ } /** - * AJAX callback for 'html'. + * Ajax callback for 'html'. */ function ajax_forms_test_advanced_commands_html_callback($form, $form_state) { $commands = array(); @@ -360,7 +370,7 @@ } /** - * AJAX callback for 'insert'. + * Ajax callback for 'insert'. */ function ajax_forms_test_advanced_commands_insert_callback($form, $form_state) { $commands = array(); @@ -369,7 +379,7 @@ } /** - * AJAX callback for 'prepend'. + * Ajax callback for 'prepend'. */ function ajax_forms_test_advanced_commands_prepend_callback($form, $form_state) { $commands = array(); @@ -378,7 +388,7 @@ } /** - * AJAX callback for 'remove'. + * Ajax callback for 'remove'. */ function ajax_forms_test_advanced_commands_remove_callback($form, $form_state) { $commands = array(); @@ -387,7 +397,7 @@ } /** - * AJAX callback for 'restripe'. + * Ajax callback for 'restripe'. */ function ajax_forms_test_advanced_commands_restripe_callback($form, $form_state) { $commands = array(); @@ -396,7 +406,7 @@ } /** - * AJAX callback for 'settings'. + * Ajax callback for 'settings'. */ function ajax_forms_test_advanced_commands_settings_callback($form, $form_state) { $commands = array(); @@ -406,13 +416,22 @@ } /** + * 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. + * not validating another form element when a single Ajax element is triggered. * - * The "drivertext" element is an AJAX-enabled textfield, free-form. + * The "drivertext" element is an Ajax-enabled textfield, free-form. * The "required_field" element is a textfield marked required. * - * The correct behavior is that the AJAX-enabled drivertext element should + * The correct behavior is that the Ajax-enabled drivertext element should * be able to trigger without causing validation of the "required_field". */ function ajax_forms_test_validation_form($form, &$form_state) { @@ -451,10 +470,51 @@ } /** - * AJAX callback for the 'drivertext' element of the validation form. + * Ajax callback for the 'drivertext' element of the validation form. */ function ajax_forms_test_validation_form_callback($form, $form_state) { drupal_set_message("ajax_forms_test_validation_form_callback invoked"); drupal_set_message(t("Callback: drivertext=%drivertext, spare_required_field=%spare_required_field", array('%drivertext' => $form_state['values']['drivertext'], '%spare_required_field' => $form_state['values']['spare_required_field']))); return '<div id="message_area">ajax_forms_test_validation_form_callback at ' . date('c') . '</div>'; } + +/** + * Form builder: Builds a form that triggers a simple AJAX callback. + */ +function ajax_forms_test_lazy_load_form($form, &$form_state) { + $form['add_files'] = array( + '#type' => 'checkbox', + '#default_value' => FALSE, + ); + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Submit'), + '#ajax' => array( + 'callback' => 'ajax_forms_test_lazy_load_form_ajax', + ), + ); + return $form; +} + +/** + * Form submit handler: Adds JavaScript and CSS that wasn't on the original form. + */ +function ajax_forms_test_lazy_load_form_submit($form, &$form_state) { + if ($form_state['values']['add_files']) { + drupal_add_js(array('ajax_forms_test_lazy_load_form_submit' => 'executed'), 'setting'); + drupal_add_css(drupal_get_path('module', 'system') . '/system.admin.css'); + drupal_add_js(drupal_get_path('module', 'system') . '/system.js'); + } + $form_state['rebuild'] = TRUE; +} + +/** + * AJAX callback for the ajax_forms_test_lazy_load_form() form. + * + * This function returns nothing, because all we're interested in testing is + * ajax_render() adding commands for JavaScript and CSS added during the page + * request, such as the ones added in ajax_forms_test_lazy_load_form_submit(). + */ +function ajax_forms_test_lazy_load_form_ajax($form, &$form_state) { + return NULL; +} diff -Naur drupal-7.0/modules/simpletest/tests/ajax_test.info drupal-7.66/modules/simpletest/tests/ajax_test.info --- drupal-7.0/modules/simpletest/tests/ajax_test.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/ajax_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: ajax_test.info,v 1.2 2010/12/20 19:59:43 webchick Exp $ name = AJAX Test description = Support module for AJAX framework tests. package = Testing @@ -6,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/simpletest/tests/ajax_test.module drupal-7.66/modules/simpletest/tests/ajax_test.module --- drupal-7.0/modules/simpletest/tests/ajax_test.module 2010-10-21 21:31:39.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/ajax_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,9 +1,8 @@ <?php -// $Id: ajax_test.module,v 1.5 2010/10/21 19:31:39 dries Exp $ /** * @file - * Helper module for AJAX framework tests. + * Helper module for Ajax framework tests. */ /** @@ -33,6 +32,14 @@ } /** + * Implements hook_system_theme_info(). + */ +function ajax_test_system_theme_info() { + $themes['test_theme'] = drupal_get_path('module', 'ajax_test') . '/themes/test_theme/test_theme.info'; + return $themes; +} + +/** * Menu callback; Return an element suitable for use by ajax_deliver(). * * Additionally ensures that ajax_render() incorporates JavaScript settings @@ -45,7 +52,7 @@ } /** - * Menu callback; Returns AJAX element with #error property set. + * Menu callback; Returns Ajax element with #error property set. */ function ajax_test_error() { $message = ''; diff -Naur drupal-7.0/modules/simpletest/tests/batch.test drupal-7.66/modules/simpletest/tests/batch.test --- drupal-7.0/modules/simpletest/tests/batch.test 2010-11-27 21:25:44.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/batch.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: batch.test,v 1.16 2010/11/27 20:25:44 dries Exp $ /** * @file @@ -342,8 +341,6 @@ '33' => array('total' => 3, 'current' => 1), // 2/3 is closer to 67% than to 66%. '67' => array('total' => 3, 'current' => 2), - // A full 3/3 should equal 100%. - '100' => array('total' => 3, 'current' => 3), // 1/199 should round up to 1%. '1' => array('total' => 199, 'current' => 1), // 198/199 should round down to 99%. @@ -366,6 +363,19 @@ '99.95' => array('total' => 2000, 'current' => 1999), // 19999/20000 should add yet another digit and go to 99.995%. '99.995' => array('total' => 20000, 'current' => 19999), + // The next five test cases simulate a batch with a single operation + // ('total' equals 1) that takes several steps to complete. Within the + // operation, we imagine that there are 501 items to process, and 100 are + // completed during each step. The percentages we get back should be + // rounded the usual way for the first few passes (i.e., 20%, 40%, etc.), + // but for the last pass through, when 500 out of 501 items have been + // processed, we do not want to round up to 100%, since that would + // erroneously indicate that the processing is complete. + '20' => array('total' => 1, 'current' => 100/501), + '40' => array('total' => 1, 'current' => 200/501), + '60' => array('total' => 1, 'current' => 300/501), + '80' => array('total' => 1, 'current' => 400/501), + '99.8' => array('total' => 1, 'current' => 500/501), ); require_once DRUPAL_ROOT . '/includes/batch.inc'; parent::setUp(); diff -Naur drupal-7.0/modules/simpletest/tests/batch_test.callbacks.inc drupal-7.66/modules/simpletest/tests/batch_test.callbacks.inc --- drupal-7.0/modules/simpletest/tests/batch_test.callbacks.inc 2010-10-03 04:42:25.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/batch_test.callbacks.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,6 +1,5 @@ <?php -// $Id: batch_test.callbacks.inc,v 1.2 2010/10/03 02:42:25 dries Exp $ /** * @file @@ -8,6 +7,8 @@ */ /** + * Implements callback_batch_operation(). + * * Simple batch operation. */ function _batch_test_callback_1($id, $sleep, &$context) { @@ -21,6 +22,8 @@ } /** + * Implements callback_batch_operation(). + * * Multistep batch operation. */ function _batch_test_callback_2($start, $total, $sleep, &$context) { @@ -54,6 +57,8 @@ } /** + * Implements callback_batch_operation(). + * * Simple batch operation. */ function _batch_test_callback_5($id, $sleep, &$context) { @@ -69,6 +74,8 @@ } /** + * Implements callback_batch_operation(). + * * Batch operation setting up its own batch. */ function _batch_test_nested_batch_callback() { @@ -77,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) { @@ -100,6 +109,8 @@ } /** + * Implements callback_batch_finished(). + * * 'finished' callback for batch 0. */ function _batch_test_finished_0($success, $results, $operations) { @@ -107,6 +118,8 @@ } /** + * Implements callback_batch_finished(). + * * 'finished' callback for batch 1. */ function _batch_test_finished_1($success, $results, $operations) { @@ -114,6 +127,8 @@ } /** + * Implements callback_batch_finished(). + * * 'finished' callback for batch 2. */ function _batch_test_finished_2($success, $results, $operations) { @@ -121,6 +136,8 @@ } /** + * Implements callback_batch_finished(). + * * 'finished' callback for batch 3. */ function _batch_test_finished_3($success, $results, $operations) { @@ -128,6 +145,8 @@ } /** + * Implements callback_batch_finished(). + * * 'finished' callback for batch 4. */ function _batch_test_finished_4($success, $results, $operations) { @@ -135,6 +154,8 @@ } /** + * Implements callback_batch_finished(). + * * 'finished' callback for batch 5. */ function _batch_test_finished_5($success, $results, $operations) { diff -Naur drupal-7.0/modules/simpletest/tests/batch_test.info drupal-7.66/modules/simpletest/tests/batch_test.info --- drupal-7.0/modules/simpletest/tests/batch_test.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/batch_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: batch_test.info,v 1.2 2010/12/20 19:59:43 webchick Exp $ name = "Batch API test" description = "Support module for Batch API tests." package = Testing @@ -6,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/simpletest/tests/batch_test.module drupal-7.66/modules/simpletest/tests/batch_test.module --- drupal-7.0/modules/simpletest/tests/batch_test.module 2010-10-03 04:42:25.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/batch_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: batch_test.module,v 1.3 2010/10/03 02:42:25 dries Exp $ /** * @file diff -Naur drupal-7.0/modules/simpletest/tests/boot.test drupal-7.66/modules/simpletest/tests/boot.test --- drupal-7.0/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.0/modules/simpletest/tests/boot_test_1.info drupal-7.66/modules/simpletest/tests/boot_test_1.info --- drupal-7.0/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.0/modules/simpletest/tests/boot_test_1.module drupal-7.66/modules/simpletest/tests/boot_test_1.module --- drupal-7.0/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.0/modules/simpletest/tests/boot_test_2.info drupal-7.66/modules/simpletest/tests/boot_test_2.info --- drupal-7.0/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.0/modules/simpletest/tests/boot_test_2.module drupal-7.66/modules/simpletest/tests/boot_test_2.module --- drupal-7.0/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.0/modules/simpletest/tests/bootstrap.test drupal-7.66/modules/simpletest/tests/bootstrap.test --- drupal-7.0/modules/simpletest/tests/bootstrap.test 2010-11-23 04:08:34.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/bootstrap.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: bootstrap.test,v 1.35 2010/11/23 03:08:34 dries Exp $ class BootstrapIPAddressTestCase extends DrupalWebTestCase { @@ -43,14 +42,14 @@ // Test the normal IP address. $this->assertTrue( ip_address() == $this->remote_ip, - t('Got remote IP address.') + 'Got remote IP address.' ); // Proxy forwarding on but no proxy addresses defined. variable_set('reverse_proxy', 1); $this->assertTrue( ip_address() == $this->remote_ip, - t('Proxy forwarding without trusted proxies got remote IP address.') + 'Proxy forwarding without trusted proxies got remote IP address.' ); // Proxy forwarding on and proxy address not trusted. @@ -59,7 +58,7 @@ $_SERVER['REMOTE_ADDR'] = $this->untrusted_ip; $this->assertTrue( ip_address() == $this->untrusted_ip, - t('Proxy forwarding with untrusted proxy got remote IP address.') + 'Proxy forwarding with untrusted proxy got remote IP address.' ); // Proxy forwarding on and proxy address trusted. @@ -68,7 +67,16 @@ drupal_static_reset('ip_address'); $this->assertTrue( ip_address() == $this->forwarded_ip, - t('Proxy forwarding with trusted proxy got forwarded IP address.') + '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. @@ -77,7 +85,7 @@ drupal_static_reset('ip_address'); $this->assertTrue( ip_address() == $this->forwarded_ip, - t('Proxy forwarding with trusted 2-tier proxy got forwarded IP address.') + 'Proxy forwarding with trusted 2-tier proxy got forwarded IP address.' ); // Custom client-IP header. @@ -86,16 +94,21 @@ drupal_static_reset('ip_address'); $this->assertTrue( ip_address() == $this->cluster_ip, - t('Cluster environment got cluster client IP.') + 'Cluster environment got cluster client IP.' ); // Verifies that drupal_valid_http_host() prevents invalid characters. - $this->assertFalse(drupal_valid_http_host('security/.drupal.org:80'), t('HTTP_HOST with / is invalid')); - $this->assertFalse(drupal_valid_http_host('security\\.drupal.org:80'), t('HTTP_HOST with \\ is invalid')); - $this->assertFalse(drupal_valid_http_host('security<.drupal.org:80'), t('HTTP_HOST with < is invalid')); - $this->assertFalse(drupal_valid_http_host('security..drupal.org:80'), t('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'); + $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'), t('HTTP_HOST containing IPv6 loopback is valid')); + $this->assertTrue(drupal_valid_http_host('[::1]:80'), 'HTTP_HOST containing IPv6 loopback is valid'); } } @@ -123,32 +136,34 @@ $this->drupalGet(''); $this->drupalHead(''); - $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', t('Page was cached.')); + $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', 'Page was cached.'); $etag = $this->drupalGetHeader('ETag'); $last_modified = $this->drupalGetHeader('Last-Modified'); $this->drupalGet('', array(), array('If-Modified-Since: ' . $last_modified, 'If-None-Match: ' . $etag)); - $this->assertResponse(304, t('Conditional request returned 304 Not Modified.')); + $this->assertResponse(304, 'Conditional request returned 304 Not Modified.'); $this->drupalGet('', array(), array('If-Modified-Since: ' . gmdate(DATE_RFC822, strtotime($last_modified)), 'If-None-Match: ' . $etag)); - $this->assertResponse(304, t('Conditional request with obsolete If-Modified-Since date returned 304 Not Modified.')); + $this->assertResponse(304, 'Conditional request with obsolete If-Modified-Since date returned 304 Not Modified.'); $this->drupalGet('', array(), array('If-Modified-Since: ' . gmdate(DATE_RFC850, strtotime($last_modified)), 'If-None-Match: ' . $etag)); - $this->assertResponse(304, t('Conditional request with obsolete If-Modified-Since date returned 304 Not Modified.')); + $this->assertResponse(304, 'Conditional request with obsolete If-Modified-Since date returned 304 Not Modified.'); $this->drupalGet('', array(), array('If-Modified-Since: ' . $last_modified)); - $this->assertResponse(200, t('Conditional request without If-None-Match returned 200 OK.')); - $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', t('Page was cached.')); + $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->assertResponse(200, t('Conditional request with new a If-Modified-Since date newer than Last-Modified returned 200 OK.')); - $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', t('Page was cached.')); + $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.'); $user = $this->drupalCreateUser(); $this->drupalLogin($user); $this->drupalGet('', array(), array('If-Modified-Since: ' . $last_modified, 'If-None-Match: ' . $etag)); - $this->assertResponse(200, t('Conditional request returned 200 OK for authenticated user.')); - $this->assertFalse($this->drupalGetHeader('X-Drupal-Cache'), t('Absense of Page was not cached.')); + $this->assertResponse(200, 'Conditional request returned 200 OK for authenticated user.'); + $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.'); } /** @@ -159,35 +174,35 @@ // Fill the cache. $this->drupalGet('system-test/set-header', array('query' => array('name' => 'Foo', 'value' => 'bar'))); - $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS', t('Page was not cached.')); - $this->assertEqual($this->drupalGetHeader('Vary'), 'Cookie,Accept-Encoding', t('Vary header was sent.')); - $this->assertEqual($this->drupalGetHeader('Cache-Control'), 'public, max-age=0', t('Cache-Control header was sent.')); - $this->assertEqual($this->drupalGetHeader('Expires'), 'Sun, 19 Nov 1978 05:00:00 GMT', t('Expires header was sent.')); - $this->assertEqual($this->drupalGetHeader('Foo'), 'bar', t('Custom header was sent.')); + $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS', 'Page was not cached.'); + $this->assertEqual($this->drupalGetHeader('Vary'), 'Cookie,Accept-Encoding', 'Vary header was sent.'); + $this->assertEqual($this->drupalGetHeader('Cache-Control'), 'public, max-age=0', '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.'); // Check cache. $this->drupalGet('system-test/set-header', array('query' => array('name' => 'Foo', 'value' => 'bar'))); - $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', t('Page was cached.')); - $this->assertEqual($this->drupalGetHeader('Vary'), 'Cookie,Accept-Encoding', t('Vary: Cookie header was sent.')); - $this->assertEqual($this->drupalGetHeader('Cache-Control'), 'public, max-age=0', t('Cache-Control header was sent.')); - $this->assertEqual($this->drupalGetHeader('Expires'), 'Sun, 19 Nov 1978 05:00:00 GMT', t('Expires header was sent.')); - $this->assertEqual($this->drupalGetHeader('Foo'), 'bar', t('Custom header was sent.')); + $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', 'Page was cached.'); + $this->assertEqual($this->drupalGetHeader('Vary'), 'Cookie,Accept-Encoding', 'Vary: Cookie header was sent.'); + $this->assertEqual($this->drupalGetHeader('Cache-Control'), 'public, max-age=0', '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.'); // Check replacing default headers. $this->drupalGet('system-test/set-header', array('query' => array('name' => 'Expires', 'value' => 'Fri, 19 Nov 2008 05:00:00 GMT'))); - $this->assertEqual($this->drupalGetHeader('Expires'), 'Fri, 19 Nov 2008 05:00:00 GMT', t('Default header was replaced.')); + $this->assertEqual($this->drupalGetHeader('Expires'), 'Fri, 19 Nov 2008 05:00:00 GMT', 'Default header was replaced.'); $this->drupalGet('system-test/set-header', array('query' => array('name' => 'Vary', 'value' => 'User-Agent'))); - $this->assertEqual($this->drupalGetHeader('Vary'), 'User-Agent,Accept-Encoding', t('Default header was replaced.')); + $this->assertEqual($this->drupalGetHeader('Vary'), 'User-Agent,Accept-Encoding', 'Default header was replaced.'); // Check that authenticated users bypass the cache. $user = $this->drupalCreateUser(); $this->drupalLogin($user); $this->drupalGet('system-test/set-header', array('query' => array('name' => 'Foo', 'value' => 'bar'))); - $this->assertFalse($this->drupalGetHeader('X-Drupal-Cache'), t('Caching was bypassed.')); - $this->assertTrue(strpos($this->drupalGetHeader('Vary'), 'Cookie') === FALSE, t('Vary: Cookie header was not sent.')); - $this->assertEqual($this->drupalGetHeader('Cache-Control'), 'no-cache, must-revalidate, post-check=0, pre-check=0', t('Cache-Control header was sent.')); - $this->assertEqual($this->drupalGetHeader('Expires'), 'Sun, 19 Nov 1978 05:00:00 GMT', t('Expires header was sent.')); - $this->assertEqual($this->drupalGetHeader('Foo'), 'bar', t('Custom header was sent.')); + $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', '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.'); } @@ -203,23 +218,35 @@ // Fill the cache and verify that output is compressed. $this->drupalGet('', array(), array('Accept-Encoding: gzip,deflate')); - $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS', t('Page was not cached.')); + $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS', 'Page was not cached.'); $this->drupalSetContent(gzinflate(substr($this->drupalGetContent(), 10, -8))); - $this->assertRaw('</html>', t('Page was gzip compressed.')); + $this->assertRaw('</html>', 'Page was gzip compressed.'); // Verify that cached output is compressed. $this->drupalGet('', array(), array('Accept-Encoding: gzip,deflate')); - $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', t('Page was cached.')); - $this->assertEqual($this->drupalGetHeader('Content-Encoding'), 'gzip', t('A Content-Encoding header was sent.')); + $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', 'Page was cached.'); + $this->assertEqual($this->drupalGetHeader('Content-Encoding'), 'gzip', 'A Content-Encoding header was sent.'); $this->drupalSetContent(gzinflate(substr($this->drupalGetContent(), 10, -8))); - $this->assertRaw('</html>', t('Page was gzip compressed.')); + $this->assertRaw('</html>', 'Page was gzip compressed.'); // Verify that a client without compression support gets an uncompressed page. $this->drupalGet(''); - $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', t('Page was cached.')); - $this->assertFalse($this->drupalGetHeader('Content-Encoding'), t('A Content-Encoding header was not sent.')); - $this->assertTitle(t('Welcome to @site-name | @site-name', array('@site-name' => variable_get('site_name', 'Drupal'))), t('Site title matches.')); - $this->assertRaw('</html>', t('Page was not compressed.')); + $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', 'Page was cached.'); + $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).'); } } @@ -244,17 +271,17 @@ // Setting and retrieving values. $variable = $this->randomName(); variable_set('simpletest_bootstrap_variable_test', $variable); - $this->assertIdentical($variable, variable_get('simpletest_bootstrap_variable_test'), t('Setting and retrieving values')); + $this->assertIdentical($variable, variable_get('simpletest_bootstrap_variable_test'), 'Setting and retrieving values'); // Make sure the variable persists across multiple requests. $this->drupalGet('system-test/variable-get'); - $this->assertText($variable, t('Variable persists across multiple requests')); + $this->assertText($variable, 'Variable persists across multiple requests'); // Deleting variables. $default_value = $this->randomName(); variable_del('simpletest_bootstrap_variable_test'); $variable = variable_get('simpletest_bootstrap_variable_test', $default_value); - $this->assertIdentical($variable, $default_value, t('Deleting variables')); + $this->assertIdentical($variable, $default_value, 'Deleting variables'); } /** @@ -262,10 +289,43 @@ */ function testVariableDefaults() { // Tests passing nothing through to the default. - $this->assertIdentical(NULL, variable_get('simpletest_bootstrap_variable_test'), t('Variables are correctly defaulting to NULL.')); + $this->assertIdentical(NULL, variable_get('simpletest_bootstrap_variable_test'), 'Variables are correctly defaulting to NULL.'); // Tests passing 5 to the default parameter. - $this->assertIdentical(5, variable_get('simpletest_bootstrap_variable_test', 5), t('The default variable parameter is passed through correctly.')); + $this->assertIdentical(5, variable_get('simpletest_bootstrap_variable_test', 5), 'The default variable parameter is passed through correctly.'); + } + +} + +/** + * 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.'); + } } } @@ -309,15 +369,15 @@ variable_set('page_cache_invoke_hooks', FALSE); $this->assertTrue(cache_get(url('', array('absolute' => TRUE)), 'cache_page'), t('Page has been cached.')); $this->drupalGet(''); - $this->assertEqual(db_query('SELECT COUNT(*) FROM {watchdog} WHERE type = :type AND message = :message', array(':type' => 'system_test', ':message' => 'hook_boot'))->fetchField(), $calls, t('hook_boot not called with agressive cache and a cached page.')); - $this->assertEqual(db_query('SELECT COUNT(*) FROM {watchdog} WHERE type = :type AND message = :message', array(':type' => 'system_test', ':message' => 'hook_exit'))->fetchField(), $calls, t('hook_exit not called with agressive cache and a cached page.')); + $this->assertEqual(db_query('SELECT COUNT(*) FROM {watchdog} WHERE type = :type AND message = :message', array(':type' => 'system_test', ':message' => 'hook_boot'))->fetchField(), $calls, t('hook_boot not called with aggressive cache and a cached page.')); + $this->assertEqual(db_query('SELECT COUNT(*) FROM {watchdog} WHERE type = :type AND message = :message', array(':type' => 'system_test', ':message' => 'hook_exit'))->fetchField(), $calls, t('hook_exit not called with aggressive cache and a cached page.')); // Test with page cache cleared, boot and exit should be called. $this->assertTrue(db_delete('cache_page')->execute(), t('Page cache cleared.')); $this->drupalGet(''); $calls++; - $this->assertEqual(db_query('SELECT COUNT(*) FROM {watchdog} WHERE type = :type AND message = :message', array(':type' => 'system_test', ':message' => 'hook_boot'))->fetchField(), $calls, t('hook_boot called with agressive cache and no cached page.')); - $this->assertEqual(db_query('SELECT COUNT(*) FROM {watchdog} WHERE type = :type AND message = :message', array(':type' => 'system_test', ':message' => 'hook_exit'))->fetchField(), $calls, t('hook_exit called with agressive cache and no cached page.')); + $this->assertEqual(db_query('SELECT COUNT(*) FROM {watchdog} WHERE type = :type AND message = :message', array(':type' => 'system_test', ':message' => 'hook_boot'))->fetchField(), $calls, t('hook_boot called with aggressive cache and no cached page.')); + $this->assertEqual(db_query('SELECT COUNT(*) FROM {watchdog} WHERE type = :type AND message = :message', array(':type' => 'system_test', ':message' => 'hook_exit'))->fetchField(), $calls, t('hook_exit called with aggressive cache and no cached page.')); } } @@ -328,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() { @@ -351,8 +418,215 @@ // Retrieving the location of a theme engine. $this->assertIdentical(drupal_get_filename('theme_engine', 'phptemplate'), 'themes/engines/phptemplate/phptemplate.engine', t('Retrieve theme engine location.')); - // Retrieving a file that is definitely not stored in the database. + // Retrieving the location of a profile. Profiles are a special case with + // a fixed location and naming. $this->assertIdentical(drupal_get_filename('profile', 'standard'), 'profiles/standard/standard.profile', t('Retrieve install profile location.')); + + // When a file is not found in the database cache, drupal_get_filename() + // searches several locations on the filesystem, including the DRUPAL_ROOT + // directory. We use the '.script' extension below because this is a + // non-existent filetype that will definitely not exist in the database. + // Since there is already a scripts directory, drupal_get_filename() will + // 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); } } @@ -374,17 +648,17 @@ function testTimer() { timer_start('test'); sleep(1); - $this->assertTrue(timer_read('test') >= 1000, t('Timer measured 1 second of sleeping while running.')); + $this->assertTrue(timer_read('test') >= 1000, 'Timer measured 1 second of sleeping while running.'); sleep(1); timer_stop('test'); - $this->assertTrue(timer_read('test') >= 2000, t('Timer measured 2 seconds of sleeping after being stopped.')); + $this->assertTrue(timer_read('test') >= 2000, 'Timer measured 2 seconds of sleeping after being stopped.'); timer_start('test'); sleep(1); - $this->assertTrue(timer_read('test') >= 3000, t('Timer measured 3 seconds of sleeping after being restarted.')); + $this->assertTrue(timer_read('test') >= 3000, 'Timer measured 3 seconds of sleeping after being restarted.'); sleep(1); $timer = timer_stop('test'); - $this->assertTrue(timer_read('test') >= 4000, t('Timer measured 4 seconds of sleeping after being stopped for a second time.')); - $this->assertEqual($timer['count'], 2, t('Timer counted 2 instances of being started.')); + $this->assertTrue(timer_read('test') >= 4000, 'Timer measured 4 seconds of sleeping after being stopped for a second time.'); + $this->assertEqual($timer['count'], 2, 'Timer counted 2 instances of being started.'); } } @@ -408,22 +682,22 @@ function testDrupalStatic() { $name = __CLASS__ . '_' . __METHOD__; $var = &drupal_static($name, 'foo'); - $this->assertEqual($var, 'foo', t('Variable returned by drupal_static() was set to its default.')); + $this->assertEqual($var, 'foo', 'Variable returned by drupal_static() was set to its default.'); // Call the specific reset and the global reset each twice to ensure that // multiple resets can be issued without odd side effects. $var = 'bar'; drupal_static_reset($name); - $this->assertEqual($var, 'foo', t('Variable was reset after first invocation of name-specific reset.')); + $this->assertEqual($var, 'foo', 'Variable was reset after first invocation of name-specific reset.'); $var = 'bar'; drupal_static_reset($name); - $this->assertEqual($var, 'foo', t('Variable was reset after second invocation of name-specific reset.')); + $this->assertEqual($var, 'foo', 'Variable was reset after second invocation of name-specific reset.'); $var = 'bar'; drupal_static_reset(); - $this->assertEqual($var, 'foo', t('Variable was reset after first invocation of global reset.')); + $this->assertEqual($var, 'foo', 'Variable was reset after first invocation of global reset.'); $var = 'bar'; drupal_static_reset(); - $this->assertEqual($var, 'foo', t('Variable was reset after second invocation of global reset.')); + $this->assertEqual($var, 'foo', 'Variable was reset after second invocation of global reset.'); } } @@ -448,7 +722,26 @@ $link_options_1 = array('fragment' => 'x', 'attributes' => array('title' => 'X', 'class' => array('a', 'b')), 'language' => 'en'); $link_options_2 = array('fragment' => 'y', 'attributes' => array('title' => 'Y', 'class' => array('c', 'd')), 'html' => TRUE); $expected = array('fragment' => 'y', 'attributes' => array('title' => 'Y', 'class' => array('a', 'b', 'c', 'd')), 'language' => 'en', 'html' => TRUE); - $this->assertIdentical(drupal_array_merge_deep($link_options_1, $link_options_2), $expected, t('drupal_array_merge_deep() returned a properly merged array.')); + $this->assertIdentical(drupal_array_merge_deep($link_options_1, $link_options_2), $expected, 'drupal_array_merge_deep() returned a properly merged array.'); + } + + /** + * Tests that the drupal_check_memory_limit() function works as expected. + */ + function testCheckMemoryLimit() { + // Test that a very reasonable amount of memory is available. + $this->assertTrue(drupal_check_memory_limit('30MB'), '30MB of memory tested available.'); + + // 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('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. + $this->assertFalse(drupal_check_memory_limit('30MB', '16MB'), 'drupal_check_memory_limit() returns FALSE with a 16MB upper limit on a 30MB requirement.'); + + // Test that an equal amount of memory to the amount requested returns TRUE. + $this->assertTrue(drupal_check_memory_limit('30MB', '30MB'), 'drupal_check_memory_limit() returns TRUE when requesting 30MB on a 30MB requirement.'); } } @@ -498,3 +791,84 @@ } } +/** + * 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.0/modules/simpletest/tests/cache.test drupal-7.66/modules/simpletest/tests/cache.test --- drupal-7.0/modules/simpletest/tests/cache.test 2010-08-06 01:53:38.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/cache.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: cache.test,v 1.12 2010/08/05 23:53:38 webchick Exp $ class CacheTestCase extends DrupalWebTestCase { protected $default_bin = 'cache'; @@ -149,16 +148,24 @@ cache_set('test_object', $test_object, 'cache'); $cache = cache_get('test_object', 'cache'); - $this->assertTrue(isset($cache->data) && $cache->data == $test_object, t('Object is saved and restored properly.')); + $this->assertTrue(isset($cache->data) && $cache->data == $test_object, 'Object is saved and restored properly.'); } - /* + /** * Check or a variable is stored and restored properly. - **/ + */ function checkVariable($var) { cache_set('test_var', $var, 'cache'); $cache = cache_get('test_var', 'cache'); - $this->assertTrue(isset($cache->data) && $cache->data === $var, t('@type is saved and restored properly.', array('@type' => ucfirst(gettype($var))))); + $this->assertTrue(isset($cache->data) && $cache->data === $var, format_string('@type is saved and restored properly.', array('@type' => ucfirst(gettype($var))))); + } + + /** + * Test no empty cids are written in cache table. + */ + function testNoEmptyCids() { + $this->drupalGet('user/register'); + $this->assertFalse(cache_get(''), 'No cache entry is written with an empty cid.'); } } @@ -188,14 +195,14 @@ $item2 = $this->randomName(10); cache_set('item1', $item1, $this->default_bin); cache_set('item2', $item2, $this->default_bin); - $this->assertTrue($this->checkCacheExists('item1', $item1), t('Item 1 is cached.')); - $this->assertTrue($this->checkCacheExists('item2', $item2), t('Item 2 is cached.')); + $this->assertTrue($this->checkCacheExists('item1', $item1), 'Item 1 is cached.'); + $this->assertTrue($this->checkCacheExists('item2', $item2), 'Item 2 is cached.'); // Fetch both records from the database with cache_get_multiple(). $item_ids = array('item1', 'item2'); $items = cache_get_multiple($item_ids, $this->default_bin); - $this->assertEqual($items['item1']->data, $item1, t('Item was returned from cache successfully.')); - $this->assertEqual($items['item2']->data, $item2, t('Item was returned from cache successfully.')); + $this->assertEqual($items['item1']->data, $item1, 'Item was returned from cache successfully.'); + $this->assertEqual($items['item2']->data, $item2, 'Item was returned from cache successfully.'); // Remove one item from the cache. cache_clear_all('item2', $this->default_bin); @@ -203,9 +210,9 @@ // Confirm that only one item is returned by cache_get_multiple(). $item_ids = array('item1', 'item2'); $items = cache_get_multiple($item_ids, $this->default_bin); - $this->assertEqual($items['item1']->data, $item1, t('Item was returned from cache successfully.')); - $this->assertFalse(isset($items['item2']), t('Item was not returned from the cache.')); - $this->assertTrue(count($items) == 1, t('Only valid cache entries returned.')); + $this->assertEqual($items['item1']->data, $item1, 'Item was returned from cache successfully.'); + $this->assertFalse(isset($items['item2']), 'Item was not returned from the cache.'); + $this->assertTrue(count($items) == 1, 'Only valid cache entries returned.'); } } @@ -243,11 +250,11 @@ cache_set('test_cid_clear2', $this->default_value, $this->default_bin); $this->assertTrue($this->checkCacheExists('test_cid_clear1', $this->default_value) && $this->checkCacheExists('test_cid_clear2', $this->default_value), - t('Two caches were created for checking cid "*" with wildcard false.')); + 'Two caches were created for checking cid "*" with wildcard false.'); cache_clear_all('*', $this->default_bin); $this->assertTrue($this->checkCacheExists('test_cid_clear1', $this->default_value) && $this->checkCacheExists('test_cid_clear2', $this->default_value), - t('Two caches still exists after clearing cid "*" with wildcard false.')); + 'Two caches still exists after clearing cid "*" with wildcard false.'); } /** @@ -258,21 +265,21 @@ cache_set('test_cid_clear2', $this->default_value, $this->default_bin); $this->assertTrue($this->checkCacheExists('test_cid_clear1', $this->default_value) && $this->checkCacheExists('test_cid_clear2', $this->default_value), - t('Two caches were created for checking cid "*" with wildcard true.')); + 'Two caches were created for checking cid "*" with wildcard true.'); cache_clear_all('*', $this->default_bin, TRUE); $this->assertFalse($this->checkCacheExists('test_cid_clear1', $this->default_value) || $this->checkCacheExists('test_cid_clear2', $this->default_value), - t('Two caches removed after clearing cid "*" with wildcard true.')); + 'Two caches removed after clearing cid "*" with wildcard true.'); cache_set('test_cid_clear1', $this->default_value, $this->default_bin); cache_set('test_cid_clear2', $this->default_value, $this->default_bin); $this->assertTrue($this->checkCacheExists('test_cid_clear1', $this->default_value) && $this->checkCacheExists('test_cid_clear2', $this->default_value), - t('Two caches were created for checking cid substring with wildcard true.')); + 'Two caches were created for checking cid substring with wildcard true.'); cache_clear_all('test_', $this->default_bin, TRUE); $this->assertFalse($this->checkCacheExists('test_cid_clear1', $this->default_value) || $this->checkCacheExists('test_cid_clear2', $this->default_value), - t('Two caches removed after clearing cid substring with wildcard true.')); + 'Two caches removed after clearing cid substring with wildcard true.'); } /** @@ -286,16 +293,16 @@ $this->assertTrue($this->checkCacheExists('test_cid_clear1', $this->default_value) && $this->checkCacheExists('test_cid_clear2', $this->default_value) && $this->checkCacheExists('test_cid_clear3', $this->default_value), - t('Three cache entries were created.')); + 'Three cache entries were created.'); // Clear two entries using an array. cache_clear_all(array('test_cid_clear1', 'test_cid_clear2'), $this->default_bin); $this->assertFalse($this->checkCacheExists('test_cid_clear1', $this->default_value) || $this->checkCacheExists('test_cid_clear2', $this->default_value), - t('Two cache entries removed after clearing with an array.')); + 'Two cache entries removed after clearing with an array.'); $this->assertTrue($this->checkCacheExists('test_cid_clear3', $this->default_value), - t('Entry was not cleared from the cache')); + 'Entry was not cleared from the cache'); // Set the cache clear threshold to 2 to confirm that the full bin is cleared // when the threshold is exceeded. @@ -304,12 +311,91 @@ cache_set('test_cid_clear2', $this->default_value, $this->default_bin); $this->assertTrue($this->checkCacheExists('test_cid_clear1', $this->default_value) && $this->checkCacheExists('test_cid_clear2', $this->default_value), - t('Two cache entries were created.')); + 'Two cache entries were created.'); cache_clear_all(array('test_cid_clear1', 'test_cid_clear2', 'test_cid_clear3'), $this->default_bin); $this->assertFalse($this->checkCacheExists('test_cid_clear1', $this->default_value) || $this->checkCacheExists('test_cid_clear2', $this->default_value) || $this->checkCacheExists('test_cid_clear3', $this->default_value), - t('All cache entries removed when the array exceeded the cache clear threshold.')); + 'All cache entries removed when the array exceeded the cache clear threshold.'); + } + + /** + * Test drupal_flush_all_caches(). + */ + function testFlushAllCaches() { + // Create cache entries for each flushed cache bin. + $bins = array('cache', 'cache_filter', 'cache_page', 'cache_boostrap', 'cache_path'); + $bins = array_merge(module_invoke_all('flush_caches'), $bins); + foreach ($bins as $id => $bin) { + $id = 'test_cid_clear' . $id; + cache_set($id, $this->default_value, $bin); + } + + // Remove all caches then make sure that they are cleared. + drupal_flush_all_caches(); + + foreach ($bins as $id => $bin) { + $id = 'test_cid_clear' . $id; + $this->assertFalse($this->checkCacheExists($id, $this->default_value, $bin), format_string('All cache entries removed from @bin.', array('@bin' => $bin))); + } + } + + /** + * Test DrupalDatabaseCache::isValidBin(). + */ + function testIsValidBin() { + // Retrieve existing cache bins. + $valid_bins = array('cache', 'cache_filter', 'cache_page', 'cache_boostrap', 'cache_path'); + $valid_bins = array_merge(module_invoke_all('flush_caches'), $valid_bins); + foreach ($valid_bins as $id => $bin) { + $cache = _cache_get_object($bin); + if ($cache instanceof DrupalDatabaseCache) { + $this->assertTrue($cache->isValidBin(), format_string('Cache bin @bin is valid.', array('@bin' => $bin))); + } + } + + // Check for non-cache tables and invalid bins. + $invalid_bins = array('block', 'filter', 'missing_table', $this->randomName()); + foreach ($invalid_bins as $id => $bin) { + $cache = _cache_get_object($bin); + if ($cache instanceof DrupalDatabaseCache) { + $this->assertFalse($cache->isValidBin(), format_string('Cache bin @bin is not valid.', array('@bin' => $bin))); + } + } + } + + /** + * Test minimum cache lifetime. + */ + function testMinimumCacheLifetime() { + // Set a minimum/maximum cache lifetime. + $this->setupLifetime(300); + // Login as a newly-created user. + $account = $this->drupalCreateUser(array()); + $this->drupalLogin($account); + + // Set two cache objects in different bins. + $data = $this->randomName(100); + cache_set($data, $data, 'cache', CACHE_TEMPORARY); + $cached = cache_get($data); + $this->assertTrue(isset($cached->data) && $cached->data === $data, 'Cached item retrieved.'); + cache_set($data, $data, 'cache_page', CACHE_TEMPORARY); + + // Expire temporary items in the 'page' bin. + cache_clear_all(NULL, 'cache_page'); + + // Since the database cache uses REQUEST_TIME, set the $_SESSION variable + // manually to force it to the current time. + $_SESSION['cache_expiration']['cache_page'] = time(); + + // Items in the default cache bin should not be expired. + $cached = cache_get($data); + $this->assertTrue(isset($cached->data) && $cached->data == $data, 'Cached item retrieved'); + + // Despite the minimum cache lifetime, the item in the 'page' bin should + // be invalidated for the current user. + $cached = cache_get($data, 'cache_page'); + $this->assertFalse($cached, 'Cached item was invalidated'); } } @@ -338,14 +424,14 @@ function testIsEmpty() { // Clear the cache bin. cache_clear_all('*', $this->default_bin); - $this->assertTrue(cache_is_empty($this->default_bin), t('The cache bin is empty')); + $this->assertTrue(cache_is_empty($this->default_bin), 'The cache bin is empty'); // Add some data to the cache bin. cache_set($this->default_cid, $this->default_value, $this->default_bin); $this->assertCacheExists(t('Cache was set.'), $this->default_value, $this->default_cid); - $this->assertFalse(cache_is_empty($this->default_bin), t('The cache bin is not empty')); + $this->assertFalse(cache_is_empty($this->default_bin), 'The cache bin is not empty'); // Remove the cached data. cache_clear_all($this->default_cid, $this->default_bin); $this->assertCacheRemoved(t('Cache was removed.'), $this->default_cid); - $this->assertTrue(cache_is_empty($this->default_bin), t('The cache bin is empty')); + $this->assertTrue(cache_is_empty($this->default_bin), 'The cache bin is empty'); } } diff -Naur drupal-7.0/modules/simpletest/tests/common.test drupal-7.66/modules/simpletest/tests/common.test --- drupal-7.0/modules/simpletest/tests/common.test 2010-12-15 05:21:39.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/common.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: common.test,v 1.138 2010/12/15 04:21:39 webchick Exp $ /** * @file @@ -37,13 +36,13 @@ $array_copy = $array; $array_expected = array('foo' => 'Drupal theme'); drupal_alter('drupal_alter', $array_copy); - $this->assertEqual($array_copy, $array_expected, t('Single array was altered.')); + $this->assertEqual($array_copy, $array_expected, 'Single array was altered.'); $entity_copy = clone $entity; $entity_expected = clone $entity; $entity_expected->foo = 'Drupal theme'; drupal_alter('drupal_alter', $entity_copy); - $this->assertEqual($entity_copy, $entity_expected, t('Single object was altered.')); + $this->assertEqual($entity_copy, $entity_expected, 'Single object was altered.'); // Verify alteration of multiple arguments. $array_copy = $array; @@ -54,9 +53,17 @@ $array2_copy = $array; $array2_expected = array('foo' => 'Drupal theme'); drupal_alter('drupal_alter', $array_copy, $entity_copy, $array2_copy); - $this->assertEqual($array_copy, $array_expected, t('First argument to drupal_alter() was altered.')); - $this->assertEqual($entity_copy, $entity_expected, t('Second argument to drupal_alter() was altered.')); - $this->assertEqual($array2_copy, $array2_expected, t('Third argument to drupal_alter() was altered.')); + $this->assertEqual($array_copy, $array_expected, 'First argument to drupal_alter() was altered.'); + $this->assertEqual($entity_copy, $entity_expected, 'Second argument to drupal_alter() was altered.'); + $this->assertEqual($array2_copy, $array2_expected, 'Third argument to drupal_alter() was altered.'); + + // Verify alteration order when passing an array of types to drupal_alter(). + // common_test_module_implements_alter() places 'block' implementation after + // other modules. + $array_copy = $array; + $array_expected = array('foo' => 'Drupal block theme'); + drupal_alter(array('drupal_alter', 'drupal_alter_foo'), $array_copy); + $this->assertEqual($array_copy, $array_expected, 'hook_TYPE_alter() implementations ran in correct order.'); } } @@ -69,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', ); @@ -83,7 +90,7 @@ $path = "<SCRIPT>alert('XSS')</SCRIPT>"; $link = l($text, $path); $sanitized_path = check_url(url($path)); - $this->assertTrue(strpos($link, $sanitized_path) !== FALSE, t('XSS attack @path was filtered', array('@path' => $path))); + $this->assertTrue(strpos($link, $sanitized_path) !== FALSE, format_string('XSS attack @path was filtered', array('@path' => $path))); } /* @@ -91,7 +98,7 @@ */ function testLActiveClass() { $link = l($this->randomName(), $_GET['q']); - $this->assertTrue($this->hasClass($link, 'active'), t('Class @class is present on link to the current page', array('@class' => 'active'))); + $this->assertTrue($this->hasClass($link, 'active'), format_string('Class @class is present on link to the current page', array('@class' => 'active'))); } /** @@ -100,8 +107,8 @@ function testLCustomClass() { $class = $this->randomName(); $link = l($this->randomName(), $_GET['q'], array('attributes' => array('class' => array($class)))); - $this->assertTrue($this->hasClass($link, $class), t('Custom class @class is present on link when requested', array('@class' => $class))); - $this->assertTrue($this->hasClass($link, 'active'), t('Class @class is present on link to the current page', array('@class' => 'active'))); + $this->assertTrue($this->hasClass($link, $class), format_string('Custom class @class is present on link when requested', array('@class' => $class))); + $this->assertTrue($this->hasClass($link, 'active'), format_string('Class @class is present on link to the current page', array('@class' => 'active'))); } private function hasClass($link, $class) { @@ -127,42 +134,42 @@ // Default arguments. $result = $_GET; unset($result['q']); - $this->assertEqual(drupal_get_query_parameters(), $result, t("\$_GET['q'] was removed.")); + $this->assertEqual(drupal_get_query_parameters(), $result, "\$_GET['q'] was removed."); // Default exclusion. $result = $original; unset($result['q']); - $this->assertEqual(drupal_get_query_parameters($original), $result, t("'q' was removed.")); + $this->assertEqual(drupal_get_query_parameters($original), $result, "'q' was removed."); // First-level exclusion. $result = $original; unset($result['b']); - $this->assertEqual(drupal_get_query_parameters($original, array('b')), $result, t("'b' was removed.")); + $this->assertEqual(drupal_get_query_parameters($original, array('b')), $result, "'b' was removed."); // Second-level exclusion. $result = $original; unset($result['b']['d']); - $this->assertEqual(drupal_get_query_parameters($original, array('b[d]')), $result, t("'b[d]' was removed.")); + $this->assertEqual(drupal_get_query_parameters($original, array('b[d]')), $result, "'b[d]' was removed."); // Third-level exclusion. $result = $original; unset($result['b']['e']['f']); - $this->assertEqual(drupal_get_query_parameters($original, array('b[e][f]')), $result, t("'b[e][f]' was removed.")); + $this->assertEqual(drupal_get_query_parameters($original, array('b[e][f]')), $result, "'b[e][f]' was removed."); // Multiple exclusions. $result = $original; unset($result['a'], $result['b']['e'], $result['c']); - $this->assertEqual(drupal_get_query_parameters($original, array('a', 'b[e]', 'c')), $result, t("'a', 'b[e]', 'c' were removed.")); + $this->assertEqual(drupal_get_query_parameters($original, array('a', 'b[e]', 'c')), $result, "'a', 'b[e]', 'c' were removed."); } /** * Test drupal_http_build_query(). */ function testDrupalHttpBuildQuery() { - $this->assertEqual(drupal_http_build_query(array('a' => ' &#//+%20@۞')), 'a=%20%26%23//%2B%2520%40%DB%9E', t('Value was properly encoded.')); - $this->assertEqual(drupal_http_build_query(array(' &#//+%20@۞' => 'a')), '%20%26%23%2F%2F%2B%2520%40%DB%9E=a', t('Key was properly encoded.')); - $this->assertEqual(drupal_http_build_query(array('a' => '1', 'b' => '2', 'c' => '3')), 'a=1&b=2&c=3', t('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', t('Nested array was properly encoded.')); + $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%5Bb%5D=2&a%5Bc%5D=3&d=foo', 'Nested array was properly encoded.'); } /** @@ -176,7 +183,7 @@ 'query' => array('foo' => 'bar', 'bar' => 'baz', 'baz' => ''), 'fragment' => 'foo', ); - $this->assertEqual(drupal_parse_url($url), $result, t('Relative URL parsed correctly.')); + $this->assertEqual(drupal_parse_url($url), $result, 'Relative URL parsed correctly.'); // Relative URL that is known to confuse parse_url(). $url = 'foo/bar:1'; @@ -185,7 +192,7 @@ 'query' => array(), 'fragment' => '', ); - $this->assertEqual(drupal_parse_url($url), $result, t('Relative URL parsed correctly.')); + $this->assertEqual(drupal_parse_url($url), $result, 'Relative URL parsed correctly.'); // Absolute URL. $url = '/foo/bar?foo=bar&bar=baz&baz#foo'; @@ -194,21 +201,30 @@ 'query' => array('foo' => 'bar', 'bar' => 'baz', 'baz' => ''), 'fragment' => 'foo', ); - $this->assertEqual(drupal_parse_url($url), $result, t('Absolute URL parsed correctly.')); + $this->assertEqual(drupal_parse_url($url), $result, 'Absolute URL parsed correctly.'); // External URL testing. $url = 'http://drupal.org/foo/bar?foo=bar&bar=baz&baz#foo'; // Test that drupal can recognize an absolute URL. Used to prevent attack vectors. - $this->assertTrue(url_is_external($url), t('Correctly identified an external URL.')); + $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' => ''), 'fragment' => 'foo', ); - $this->assertEqual(drupal_parse_url($url), $result, t('External URL parsed correctly.')); + $this->assertEqual(drupal_parse_url($url), $result, 'External URL parsed correctly.'); // Verify proper parsing of URLs when clean URLs are disabled. $result = array( @@ -218,19 +234,19 @@ ); // Non-clean URLs #1: Absolute URL generated by url(). $url = $GLOBALS['base_url'] . '/?q=foo/bar&bar=baz#foo'; - $this->assertEqual(drupal_parse_url($url), $result, t('Absolute URL with clean URLs disabled parsed correctly.')); + $this->assertEqual(drupal_parse_url($url), $result, 'Absolute URL with clean URLs disabled parsed correctly.'); // Non-clean URLs #2: Relative URL generated by url(). $url = '?q=foo/bar&bar=baz#foo'; - $this->assertEqual(drupal_parse_url($url), $result, t('Relative URL with clean URLs disabled parsed correctly.')); + $this->assertEqual(drupal_parse_url($url), $result, 'Relative URL with clean URLs disabled parsed correctly.'); // Non-clean URLs #3: URL generated by url() on non-Apache webserver. $url = 'index.php?q=foo/bar&bar=baz#foo'; - $this->assertEqual(drupal_parse_url($url), $result, t('Relative URL on non-Apache webserver with clean URLs disabled parsed correctly.')); + $this->assertEqual(drupal_parse_url($url), $result, 'Relative URL on non-Apache webserver with clean URLs disabled parsed correctly.'); // Test that drupal_parse_url() does not allow spoofing a URL to force a malicious redirect. $parts = drupal_parse_url('forged:http://cwe.mitre.org/data/definitions/601.html'); - $this->assertFalse(valid_url($parts['path'], TRUE), t('drupal_parse_url() correctly parsed a forged URL.')); + $this->assertFalse(valid_url($parts['path'], TRUE), 'drupal_parse_url() correctly parsed a forged URL.'); } /** @@ -318,42 +334,144 @@ // Verify external URL can contain a fragment. $url = $test_url . '#drupal'; $result = url($url); - $this->assertEqual($url, $result, t('External URL with fragment works without a fragment in $options.')); + $this->assertEqual($url, $result, 'External URL with fragment works without a fragment in $options.'); // Verify fragment can be overidden in an external URL. $url = $test_url . '#drupal'; $fragment = $this->randomName(10); $result = url($url, array('fragment' => $fragment)); - $this->assertEqual($test_url . '#' . $fragment, $result, t('External URL fragment is overidden with a custom fragment in $options.')); + $this->assertEqual($test_url . '#' . $fragment, $result, 'External URL fragment is overidden with a custom fragment in $options.'); // Verify external URL can contain a query string. $url = $test_url . '?drupal=awesome'; $result = url($url); - $this->assertEqual($url, $result, t('External URL with query string works without a query string in $options.')); + $this->assertEqual($url, $result, 'External URL with query string works without a query string in $options.'); // Verify external URL can be extended with a query string. $url = $test_url; $query = array($this->randomName(5) => $this->randomName(5)); $result = url($url, array('query' => $query)); - $this->assertEqual($url . '?' . http_build_query($query, '', '&'), $result, t('External URL can be extended with a query string in $options.')); + $this->assertEqual($url . '?' . http_build_query($query, '', '&'), $result, 'External URL can be extended with a query string in $options.'); // Verify query string can be extended in an external URL. $url = $test_url . '?drupal=awesome'; $query = array($this->randomName(5) => $this->randomName(5)); $result = url($url, array('query' => $query)); - $this->assertEqual($url . '&' . http_build_query($query, '', '&'), $result, t('External URL query string can be extended with a custom query string in $options.')); + $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, + ); } } /** - * Tests for the check_plain() and filter_xss() functions. + * Tests for check_plain(), filter_xss(), format_string(), and check_url(). */ class CommonXssUnitTest extends DrupalUnitTestCase { public static function getInfo() { return array( 'name' => 'String filtering tests', - 'description' => 'Confirm that check_plain(), filter_xss(), and check_url() work correctly, including invalid multi-byte sequences.', + 'description' => 'Confirm that check_plain(), filter_xss(), format_string() and check_url() work correctly, including invalid multi-byte sequences.', 'group' => 'System', ); } @@ -387,6 +505,22 @@ } /** + * Test t() and format_string() replacement functionality. + */ + function testFormatStringAndT() { + foreach (array('format_string', 't') as $function) { + $text = $function('Simple text'); + $this->assertEqual($text, 'Simple text', $function . ' leaves simple text alone.'); + $text = $function('Escaped text: @value', array('@value' => '<script>')); + $this->assertEqual($text, 'Escaped text: <script>', $function . ' replaces and escapes string.'); + $text = $function('Placeholder text: %value', array('%value' => '<script>')); + $this->assertEqual($text, 'Placeholder text: <em class="placeholder"><script></em>', $function . ' replaces, escapes and themes string.'); + $text = $function('Verbatim text: !value', array('!value' => '<script>')); + $this->assertEqual($text, 'Verbatim text: <script>', $function . ' replaces verbatim string as-is.'); + } + } + + /** * Check that harmful protocols are stripped. */ function testBadProtocolStripping() { @@ -396,11 +530,14 @@ $url = 'javascript:http://www.example.com/?x=1&y=2'; $expected_plain = 'http://www.example.com/?x=1&y=2'; $expected_html = 'http://www.example.com/?x=1&y=2'; - $this->assertIdentical(check_url($url), $expected_html, t('check_url() filters a URL and encodes it for HTML.')); - $this->assertIdentical(drupal_strip_dangerous_protocols($url), $expected_plain, t('drupal_strip_dangerous_protocols() filters a URL and returns plain text.')); + $this->assertIdentical(check_url($url), $expected_html, 'check_url() filters a URL and encodes it for HTML.'); + $this->assertIdentical(drupal_strip_dangerous_protocols($url), $expected_plain, 'drupal_strip_dangerous_protocols() filters a URL and returns plain text.'); } } +/** + * Tests file size parsing and formatting functions. + */ class CommonSizeTestCase extends DrupalUnitTestCase { protected $exact_test_cases; protected $rounded_test_cases; @@ -501,7 +638,7 @@ /** * Test drupal_explode_tags() and drupal_implode_tags(). */ -class DrupalTagsHandlingTestCase extends DrupalWebTestCase { +class DrupalTagsHandlingTestCase extends DrupalUnitTestCase { var $validTags = array( 'Drupal' => 'Drupal', 'Drupal with some spaces' => 'Drupal with some spaces', @@ -546,11 +683,11 @@ $original = $this->validTags; foreach ($tags as $tag) { $key = array_search($tag, $original); - $this->assertTrue($key, t('Make sure tag %tag shows up in the final tags array (originally %original)', array('%tag' => $tag, '%original' => $key))); + $this->assertTrue($key, format_string('Make sure tag %tag shows up in the final tags array (originally %original)', array('%tag' => $tag, '%original' => $key))); unset($original[$key]); } foreach ($original as $leftover) { - $this->fail(t('Leftover tag %leftover was left over.', array('%leftover' => $leftover))); + $this->fail(format_string('Leftover tag %leftover was left over.', array('%leftover' => $leftover))); } } } @@ -577,7 +714,7 @@ * Check default stylesheets as empty. */ function testDefault() { - $this->assertEqual(array(), drupal_add_css(), t('Default CSS is empty.')); + $this->assertEqual(array(), drupal_add_css(), 'Default CSS is empty.'); } /** @@ -607,7 +744,7 @@ function testAddFile() { $path = drupal_get_path('module', 'simpletest') . '/simpletest.css'; $css = drupal_add_css($path); - $this->assertEqual($css[$path]['data'], $path, t('Adding a CSS file caches it properly.')); + $this->assertEqual($css[$path]['data'], $path, 'Adding a CSS file caches it properly.'); } /** @@ -616,7 +753,7 @@ function testAddExternal() { $path = 'http://example.com/style.css'; $css = drupal_add_css($path, 'external'); - $this->assertEqual($css[$path]['type'], 'external', t('Adding an external CSS file caches it properly.')); + $this->assertEqual($css[$path]['type'], 'external', 'Adding an external CSS file caches it properly.'); } /** @@ -624,7 +761,7 @@ */ function testReset() { drupal_static_reset('drupal_add_css'); - $this->assertEqual(array(), drupal_add_css(), t('Resetting the CSS empties the cache.')); + $this->assertEqual(array(), drupal_add_css(), 'Resetting the CSS empties the cache.'); } /** @@ -634,7 +771,11 @@ $css = drupal_get_path('module', 'simpletest') . '/simpletest.css'; drupal_add_css($css); $styles = drupal_get_css(); - $this->assertTrue(strpos($styles, $css) > 0, t('Rendered CSS includes the added stylesheet.')); + $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.'); } /** @@ -646,7 +787,7 @@ $styles = drupal_get_css(); // Stylesheet URL may be the href of a LINK tag or in an @import statement // of a STYLE tag. - $this->assertTrue(strpos($styles, 'href="' . $css) > 0 || strpos($styles, '@import url("' . $css . '")') > 0, t('Rendering an external CSS file.')); + $this->assertTrue(strpos($styles, 'href="' . $css) > 0 || strpos($styles, '@import url("' . $css . '")') > 0, 'Rendering an external CSS file.'); } /** @@ -657,7 +798,32 @@ $css_preprocessed = '<style type="text/css" media="all">' . "\n<!--/*--><![CDATA[/*><!--*/\n" . drupal_load_stylesheet_content($css, TRUE) . "\n/*]]>*/-->\n" . '</style>'; drupal_add_css($css, array('type' => 'inline')); $styles = drupal_get_css(); - $this->assertEqual(trim($styles), $css_preprocessed, t('Rendering preprocessed inline CSS adds it to the page.')); + $this->assertEqual(trim($styles), $css_preprocessed, 'Rendering preprocessed inline CSS adds it to the page.'); + } + + /** + * 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.' + ); + } } /** @@ -667,7 +833,7 @@ $css = 'body { padding: 0px; }'; drupal_add_css($css, array('type' => 'inline', 'preprocess' => FALSE)); $styles = drupal_get_css(); - $this->assertTrue(strpos($styles, $css) > 0, t('Rendering non-preprocessed inline CSS adds it to the page.')); + $this->assertTrue(strpos($styles, $css) > 0, 'Rendering non-preprocessed inline CSS adds it to the page.'); } /** @@ -697,7 +863,7 @@ // Fetch the page. $this->drupalGet('node/' . $node->nid); - $this->assertRaw($expected, t('Inline stylesheets appear in the full page rendering.')); + $this->assertRaw($expected, 'Inline stylesheets appear in the full page rendering.'); } /** @@ -730,7 +896,7 @@ $result = array(); } - $this->assertIdentical($result, $expected, t('The CSS files are in the expected order.')); + $this->assertIdentical($result, $expected, 'The CSS files are in the expected order.'); } /** @@ -745,16 +911,16 @@ // The dummy stylesheet should be the only one included. $styles = drupal_get_css(); - $this->assert(strpos($styles, $simpletest . '/tests/system.base.css') !== FALSE, t('The overriding CSS file is output.')); - $this->assert(strpos($styles, $system . '/system.base.css') === FALSE, t('The overridden CSS file is not output.')); + $this->assert(strpos($styles, $simpletest . '/tests/system.base.css') !== FALSE, 'The overriding CSS file is output.'); + $this->assert(strpos($styles, $system . '/system.base.css') === FALSE, 'The overridden CSS file is not output.'); drupal_add_css($simpletest . '/tests/system.base.css'); drupal_add_css($system . '/system.base.css'); // The standard stylesheet should be the only one included. $styles = drupal_get_css(); - $this->assert(strpos($styles, $system . '/system.base.css') !== FALSE, t('The overriding CSS file is output.')); - $this->assert(strpos($styles, $simpletest . '/tests/system.base.css') === FALSE, t('The overridden CSS file is not output.')); + $this->assert(strpos($styles, $system . '/system.base.css') !== FALSE, 'The overriding CSS file is output.'); + $this->assert(strpos($styles, $simpletest . '/tests/system.base.css') === FALSE, 'The overridden CSS file is not output.'); } /** @@ -769,7 +935,7 @@ // Check to see if system.base-rtl.css was also added. $styles = drupal_get_css(); - $this->assert(strpos($styles, $path . '/system.base-rtl.css') !== FALSE, t('CSS is alterable as right to left overrides are added.')); + $this->assert(strpos($styles, $path . '/system.base-rtl.css') !== FALSE, 'CSS is alterable as right to left overrides are added.'); // Change the language back to left to right. $language->direction = LANGUAGE_LTR; @@ -782,8 +948,8 @@ function testAddCssFileWithQueryString() { $this->drupalGet('common-test/query-string'); $query_string = variable_get('css_js_query_string', '0'); - $this->assertRaw(drupal_get_path('module', 'node') . '/node.css?' . $query_string, t('Query string was appended correctly to css.')); - $this->assertRaw(drupal_get_path('module', 'node') . '/node-fake.css?arg1=value1&arg2=value2', t('Query string not escaped on a URI.')); + $this->assertRaw(drupal_get_path('module', 'node') . '/node.css?' . $query_string, 'Query string was appended correctly to css.'); + $this->assertRaw(drupal_get_path('module', 'node') . '/node-fake.css?arg1=value1&arg2=value2', 'Query string not escaped on a URI.'); } } @@ -805,14 +971,39 @@ function testDrupalCleanCSSIdentifier() { // Verify that no valid ASCII characters are stripped from the identifier. $identifier = 'abcdefghijklmnopqrstuvwxyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ-0123456789'; - $this->assertIdentical(drupal_clean_css_identifier($identifier, array()), $identifier, t('Verify valid ASCII characters pass through.')); + $this->assertIdentical(drupal_clean_css_identifier($identifier, array()), $identifier, 'Verify valid ASCII characters pass through.'); // Verify that valid UTF-8 characters are not stripped from the identifier. $identifier = '¡¢£¤¥'; - $this->assertIdentical(drupal_clean_css_identifier($identifier, array()), $identifier, t('Verify valid UTF-8 characters pass through.')); + $this->assertIdentical(drupal_clean_css_identifier($identifier, array()), $identifier, 'Verify valid UTF-8 characters pass through.'); // Verify that invalid characters (including non-breaking space) are stripped from the identifier. - $this->assertIdentical(drupal_clean_css_identifier('invalid !"#$%&\'()*+,./:;<=>?@[\\]^`{|}~ identifier', array()), 'invalididentifier', t('Strip invalid characters.')); + $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'); } /** @@ -820,7 +1011,7 @@ */ function testDrupalHTMLClass() { // Verify Drupal coding standards are enforced. - $this->assertIdentical(drupal_html_class('CLASS NAME_[Ü]'), 'class-name--ü', t('Enforce Drupal coding standards.')); + $this->assertIdentical(drupal_html_class('CLASS NAME_[Ü]'), 'class-name--ü', 'Enforce Drupal coding standards.'); } /** @@ -829,21 +1020,21 @@ function testDrupalHTMLId() { // Verify that letters, digits, and hyphens are not stripped from the ID. $id = 'abcdefghijklmnopqrstuvwxyz-0123456789'; - $this->assertIdentical(drupal_html_id($id), $id, t('Verify valid characters pass through.')); + $this->assertIdentical(drupal_html_id($id), $id, 'Verify valid characters pass through.'); // Verify that invalid characters are stripped from the ID. - $this->assertIdentical(drupal_html_id('invalid,./:@\\^`{Üidentifier'), 'invalididentifier', t('Strip invalid characters.')); + $this->assertIdentical(drupal_html_id('invalid,./:@\\^`{Üidentifier'), 'invalididentifier', 'Strip invalid characters.'); // Verify Drupal coding standards are enforced. - $this->assertIdentical(drupal_html_id('ID NAME_[1]'), 'id-name-1', t('Enforce Drupal coding standards.')); + $this->assertIdentical(drupal_html_id('ID NAME_[1]'), 'id-name-1', 'Enforce Drupal coding standards.'); // Reset the static cache so we can ensure the unique id count is at zero. drupal_static_reset('drupal_html_id'); // Clean up IDs with invalid starting characters. - $this->assertIdentical(drupal_html_id('test-unique-id'), 'test-unique-id', t('Test the uniqueness of IDs #1.')); - $this->assertIdentical(drupal_html_id('test-unique-id'), 'test-unique-id--2', t('Test the uniqueness of IDs #2.')); - $this->assertIdentical(drupal_html_id('test-unique-id'), 'test-unique-id--3', t('Test the uniqueness of IDs #3.')); + $this->assertIdentical(drupal_html_id('test-unique-id'), 'test-unique-id', 'Test the uniqueness of IDs #1.'); + $this->assertIdentical(drupal_html_id('test-unique-id'), 'test-unique-id--2', 'Test the uniqueness of IDs #2.'); + $this->assertIdentical(drupal_html_id('test-unique-id'), 'test-unique-id--3', 'Test the uniqueness of IDs #3.'); } } @@ -863,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/'. @@ -875,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); - $this->assertEqual($unoptimized_output, $expected, t('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); - $this->assertEqual($optimized_output, $expected, t('Optimized CSS file has expected contents (@file)', array('@file' => $file))); + $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($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, t('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, t('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))); } } } @@ -912,7 +1109,7 @@ } function setUp() { - parent::setUp('system_test'); + parent::setUp('system_test', 'locale'); } function testDrupalHTTPRequest() { @@ -920,24 +1117,24 @@ // Parse URL schema. $missing_scheme = drupal_http_request('example.com/path'); - $this->assertEqual($missing_scheme->code, -1002, t('Returned with "-1002" error code.')); - $this->assertEqual($missing_scheme->error, 'missing schema', t('Returned with "missing schema" error message.')); + $this->assertEqual($missing_scheme->code, -1002, 'Returned with "-1002" error code.'); + $this->assertEqual($missing_scheme->error, 'missing schema', 'Returned with "missing schema" error message.'); $unable_to_parse = drupal_http_request('http:///path'); - $this->assertEqual($unable_to_parse->code, -1001, t('Returned with "-1001" error code.')); - $this->assertEqual($unable_to_parse->error, 'unable to parse URL', t('Returned with "unable to parse URL" error message.')); + $this->assertEqual($unable_to_parse->code, -1001, 'Returned with "-1001" error code.'); + $this->assertEqual($unable_to_parse->error, 'unable to parse URL', 'Returned with "unable to parse URL" error message.'); // Fetch page. $result = drupal_http_request(url('node', array('absolute' => TRUE))); - $this->assertEqual($result->code, 200, t('Fetched page successfully.')); + $this->assertEqual($result->code, 200, 'Fetched page successfully.'); $this->drupalSetContent($result->data); - $this->assertTitle(t('Welcome to @site-name | @site-name', array('@site-name' => variable_get('site_name', 'Drupal'))), t('Site title matches.')); + $this->assertTitle(t('Welcome to @site-name | @site-name', array('@site-name' => variable_get('site_name', 'Drupal'))), 'Site title matches.'); // Test that code and status message is returned. $result = drupal_http_request(url('pagedoesnotexist', array('absolute' => TRUE))); - $this->assertTrue(!empty($result->protocol), t('Result protocol is returned.')); - $this->assertEqual($result->code, '404', t('Result code is 404')); - $this->assertEqual($result->status_message, 'Not Found', t('Result status message is "Not Found"')); + $this->assertTrue(!empty($result->protocol), 'Result protocol is returned.'); + $this->assertEqual($result->code, '404', 'Result code is 404'); + $this->assertEqual($result->status_message, 'Not Found', 'Result status message is "Not Found"'); // Skip the timeout tests when the testing environment is HTTPS because // stream_set_timeout() does not work for SSL connections. @@ -952,9 +1149,9 @@ timer_start(__METHOD__); $result = drupal_http_request(url('system-test/sleep/10', array('absolute' => TRUE)), array('timeout' => 2)); $time = timer_read(__METHOD__) / 1000; - $this->assertTrue(1.8 < $time && $time < 5, t('Request timed out (%time seconds).', array('%time' => $time))); - $this->assertTrue($result->error, t('An error message was returned.')); - $this->assertEqual($result->code, HTTP_REQUEST_TIMEOUT, t('Proper error code was returned.')); + $this->assertTrue(1.8 < $time && $time < 5, format_string('Request timed out (%time seconds).', array('%time' => $time))); + $this->assertTrue($result->error, 'An error message was returned.'); + $this->assertEqual($result->code, HTTP_REQUEST_TIMEOUT, 'Proper error code was returned.'); } } @@ -967,40 +1164,131 @@ $result = drupal_http_request($auth); $this->drupalSetContent($result->data); - $this->assertRaw($username, t('$_SERVER["PHP_AUTH_USER"] is passed correctly.')); - $this->assertRaw($password, t('$_SERVER["PHP_AUTH_PW"] is passed correctly.')); + $this->assertRaw($username, 'Username is passed correctly.'); + $this->assertRaw($password, 'Password is passed correctly.'); } function testDrupalHTTPRequestRedirect() { $redirect_301 = drupal_http_request(url('system-test/redirect/301', array('absolute' => TRUE)), array('max_redirects' => 1)); - $this->assertEqual($redirect_301->redirect_code, 301, t('drupal_http_request follows the 301 redirect.')); + $this->assertEqual($redirect_301->redirect_code, 301, 'drupal_http_request follows the 301 redirect.'); $redirect_301 = drupal_http_request(url('system-test/redirect/301', array('absolute' => TRUE)), array('max_redirects' => 0)); - $this->assertFalse(isset($redirect_301->redirect_code), t('drupal_http_request does not follow 301 redirect if max_redirects = 0.')); + $this->assertFalse(isset($redirect_301->redirect_code), 'drupal_http_request does not follow 301 redirect if max_redirects = 0.'); $redirect_invalid = drupal_http_request(url('system-test/redirect-noscheme', array('absolute' => TRUE)), array('max_redirects' => 1)); - $this->assertEqual($redirect_invalid->code, -1002, t('301 redirect to invalid URL returned with error code !error.', array('!error' => $redirect_invalid->error))); - $this->assertEqual($redirect_invalid->error, 'missing schema', t('301 redirect to invalid URL returned with error message "!error".', array('!error' => $redirect_invalid->error))); + $this->assertEqual($redirect_invalid->code, -1002, format_string('301 redirect to invalid URL returned with error code !error.', array('!error' => $redirect_invalid->error))); + $this->assertEqual($redirect_invalid->error, 'missing schema', format_string('301 redirect to invalid URL returned with error message "!error".', array('!error' => $redirect_invalid->error))); $redirect_invalid = drupal_http_request(url('system-test/redirect-noparse', array('absolute' => TRUE)), array('max_redirects' => 1)); - $this->assertEqual($redirect_invalid->code, -1001, t('301 redirect to invalid URL returned with error message code "!error".', array('!error' => $redirect_invalid->error))); - $this->assertEqual($redirect_invalid->error, 'unable to parse URL', t('301 redirect to invalid URL returned with error message "!error".', array('!error' => $redirect_invalid->error))); + $this->assertEqual($redirect_invalid->code, -1001, format_string('301 redirect to invalid URL returned with error message code "!error".', array('!error' => $redirect_invalid->error))); + $this->assertEqual($redirect_invalid->error, 'unable to parse URL', format_string('301 redirect to invalid URL returned with error message "!error".', array('!error' => $redirect_invalid->error))); $redirect_invalid = drupal_http_request(url('system-test/redirect-invalid-scheme', array('absolute' => TRUE)), array('max_redirects' => 1)); - $this->assertEqual($redirect_invalid->code, -1003, t('301 redirect to invalid URL returned with error code !error.', array('!error' => $redirect_invalid->error))); - $this->assertEqual($redirect_invalid->error, 'invalid schema ftp', t('301 redirect to invalid URL returned with error message "!error".', array('!error' => $redirect_invalid->error))); + $this->assertEqual($redirect_invalid->code, -1003, format_string('301 redirect to invalid URL returned with error code !error.', array('!error' => $redirect_invalid->error))); + $this->assertEqual($redirect_invalid->error, 'invalid schema ftp', format_string('301 redirect to invalid URL returned with error message "!error".', array('!error' => $redirect_invalid->error))); $redirect_302 = drupal_http_request(url('system-test/redirect/302', array('absolute' => TRUE)), array('max_redirects' => 1)); - $this->assertEqual($redirect_302->redirect_code, 302, t('drupal_http_request follows the 302 redirect.')); + $this->assertEqual($redirect_302->redirect_code, 302, 'drupal_http_request follows the 302 redirect.'); $redirect_302 = drupal_http_request(url('system-test/redirect/302', array('absolute' => TRUE)), array('max_redirects' => 0)); - $this->assertFalse(isset($redirect_302->redirect_code), t('drupal_http_request does not follow 302 redirect if $retry = 0.')); + $this->assertFalse(isset($redirect_302->redirect_code), 'drupal_http_request does not follow 302 redirect if $retry = 0.'); $redirect_307 = drupal_http_request(url('system-test/redirect/307', array('absolute' => TRUE)), array('max_redirects' => 1)); - $this->assertEqual($redirect_307->redirect_code, 307, t('drupal_http_request follows the 307 redirect.')); + $this->assertEqual($redirect_307->redirect_code, 307, 'drupal_http_request follows the 307 redirect.'); $redirect_307 = drupal_http_request(url('system-test/redirect/307', array('absolute' => TRUE)), array('max_redirects' => 0)); - $this->assertFalse(isset($redirect_307->redirect_code), t('drupal_http_request does not follow 307 redirect if max_redirects = 0.')); + $this->assertFalse(isset($redirect_307->redirect_code), 'drupal_http_request does not follow 307 redirect if max_redirects = 0.'); + + $multiple_redirect_final_url = url('system-test/multiple-redirects/0', array('absolute' => TRUE)); + $multiple_redirect_1 = drupal_http_request(url('system-test/multiple-redirects/1', array('absolute' => TRUE)), array('max_redirects' => 1)); + $this->assertEqual($multiple_redirect_1->redirect_url, $multiple_redirect_final_url, 'redirect_url contains the final redirection location after 1 redirect.'); + + $multiple_redirect_3 = drupal_http_request(url('system-test/multiple-redirects/3', array('absolute' => TRUE)), array('max_redirects' => 3)); + $this->assertEqual($multiple_redirect_3->redirect_url, $multiple_redirect_final_url, 'redirect_url contains the final redirection location after 3 redirects.'); + } + + /** + * Tests Content-language headers generated by Drupal. + */ + function testDrupalHTTPRequestHeaders() { + // Check the default header. + $request = drupal_http_request(url('<front>', array('absolute' => TRUE))); + $this->assertEqual($request->headers['content-language'], 'en', 'Content-Language HTTP header is English.'); + + // Add German language and set as default. + locale_add_language('de', 'German', 'Deutsch', LANGUAGE_LTR, '', '', TRUE, TRUE); + + // Request front page and check for matching Content-Language. + $request = drupal_http_request(url('<front>', array('absolute' => TRUE))); + $this->assertEqual($request->headers['content-language'], 'de', 'Content-Language HTTP header is German.'); + } +} + +/** + * 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', + ), + ), + ); } } @@ -1023,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. @@ -1039,13 +1327,13 @@ // Ensure drupal_get_region_content returns expected results when fetching all regions. $content = drupal_get_region_content(NULL, $delimiter); foreach ($content as $region => $region_content) { - $this->assertEqual($region_content, $values[$region], t('@region region text verified when fetching all regions', array('@region' => $region))); + $this->assertEqual($region_content, $values[$region], format_string('@region region text verified when fetching all regions', array('@region' => $region))); } // Ensure drupal_get_region_content returns expected results when fetching a single region. foreach ($block_regions as $region) { $region_content = drupal_get_region_content($region, $delimiter); - $this->assertEqual($region_content, $values[$region], t('@region region text verified when fetching single region.', array('@region' => $region))); + $this->assertEqual($region_content, $values[$region], format_string('@region region text verified when fetching single region.', array('@region' => $region))); } } } @@ -1073,23 +1361,32 @@ $this->drupalGet('common-test/drupal_goto/redirect'); $headers = $this->drupalGetHeaders(TRUE); list(, $status) = explode(' ', $headers[0][':status'], 3); - $this->assertEqual($status, 302, t('Expected response code was sent.')); - $this->assertText('drupal_goto', t('Drupal goto redirect succeeded.')); - $this->assertEqual($this->getUrl(), url('common-test/drupal_goto', array('absolute' => TRUE)), t('Drupal goto redirected to expected URL.')); + $this->assertEqual($status, 302, 'Expected response code was sent.'); + $this->assertText('drupal_goto', 'Drupal goto redirect succeeded.'); + $this->assertEqual($this->getUrl(), url('common-test/drupal_goto', array('absolute' => TRUE)), 'Drupal goto redirected to expected URL.'); $this->drupalGet('common-test/drupal_goto/redirect_advanced'); $headers = $this->drupalGetHeaders(TRUE); list(, $status) = explode(' ', $headers[0][':status'], 3); - $this->assertEqual($status, 301, t('Expected response code was sent.')); - $this->assertText('drupal_goto', t('Drupal goto redirect succeeded.')); - $this->assertEqual($this->getUrl(), url('common-test/drupal_goto', array('query' => array('foo' => '123'), 'absolute' => TRUE)), t('Drupal goto redirected to expected URL.')); - + $this->assertEqual($status, 301, 'Expected response code was sent.'); + $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'; $this->drupalGet('common-test/drupal_goto/redirect', array('query' => array('destination' => $destination))); - $this->assertText('drupal_goto', t('Drupal goto redirect with destination succeeded.')); - $this->assertEqual($this->getUrl(), url('common-test/drupal_goto/destination', array('query' => array('foo' => '%25', 'bar' => '123'), 'absolute' => TRUE)), t('Drupal goto redirected to given query string destination. ')); + $this->assertText('drupal_goto', 'Drupal goto redirect with destination succeeded.'); + $this->assertEqual($this->getUrl(), url('common-test/drupal_goto/destination', array('query' => array('foo' => '%25', 'bar' => '123'), 'absolute' => TRUE)), 'Drupal goto redirected to given query string destination.'); } /** @@ -1098,8 +1395,8 @@ function testDrupalGotoAlter() { $this->drupalGet('common-test/drupal_goto/redirect_fail'); - $this->assertNoText(t("Drupal goto failed to stop program"), t("Drupal goto stopped program.")); - $this->assertNoText('drupal_goto_fail', t("Drupal goto redirect failed.")); + $this->assertNoText(t("Drupal goto failed to stop program"), "Drupal goto stopped program."); + $this->assertNoText('drupal_goto_fail', "Drupal goto redirect failed."); } /** @@ -1110,12 +1407,12 @@ // Verify that a 'destination' query string is used as destination. $this->drupalGet('common-test/destination', array('query' => array('destination' => $query))); - $this->assertText('The destination: ' . $query, t('The given query string destination is determined as destination.')); + $this->assertText('The destination: ' . $query, 'The given query string destination is determined as destination.'); // Verify that the current path is used as destination. $this->drupalGet('common-test/destination', array('query' => array($query => NULL))); $url = 'common-test/destination?' . $query; - $this->assertText('The destination: ' . $url, t('The current path is determined as destination.')); + $this->assertText('The destination: ' . $url, 'The current path is determined as destination.'); } } @@ -1159,7 +1456,7 @@ * Test default JavaScript is empty. */ function testDefault() { - $this->assertEqual(array(), drupal_add_js(), t('Default JavaScript is empty.')); + $this->assertEqual(array(), drupal_add_js(), 'Default JavaScript is empty.'); } /** @@ -1167,10 +1464,12 @@ */ function testAddFile() { $javascript = drupal_add_js('misc/collapse.js'); - $this->assertTrue(array_key_exists('misc/jquery.js', $javascript), t('jQuery is added when a file is added.')); - $this->assertTrue(array_key_exists('misc/drupal.js', $javascript), t('Drupal.js is added when file is added.')); - $this->assertTrue(array_key_exists('misc/collapse.js', $javascript), t('JavaScript files are correctly added.')); - $this->assertEqual(base_path(), $javascript['settings']['data'][0]['basePath'], t('Base path JavaScript setting is correctly set.')); + $this->assertTrue(array_key_exists('misc/jquery.js', $javascript), 'jQuery is added when a file is added.'); + $this->assertTrue(array_key_exists('misc/drupal.js', $javascript), 'Drupal.js is added when file is added.'); + $this->assertTrue(array_key_exists('misc/collapse.js', $javascript), 'JavaScript files are correctly added.'); + $this->assertEqual(base_path(), $javascript['settings']['data'][0]['basePath'], 'Base path JavaScript setting is correctly set.'); + url('', array('prefix' => &$prefix)); + $this->assertEqual(empty($prefix) ? '' : $prefix, $javascript['settings']['data'][1]['pathPrefix'], 'Path prefix JavaScript setting is correctly set.'); } /** @@ -1178,8 +1477,8 @@ */ function testAddSetting() { $javascript = drupal_add_js(array('drupal' => 'rocks', 'dries' => 280342800), 'setting'); - $this->assertEqual(280342800, $javascript['settings']['data'][1]['dries'], t('JavaScript setting is set correctly.')); - $this->assertEqual('rocks', $javascript['settings']['data'][1]['drupal'], t('The other JavaScript setting is set correctly.')); + $this->assertEqual(280342800, $javascript['settings']['data'][2]['dries'], 'JavaScript setting is set correctly.'); + $this->assertEqual('rocks', $javascript['settings']['data'][2]['drupal'], 'The other JavaScript setting is set correctly.'); } /** @@ -1188,7 +1487,7 @@ function testAddExternal() { $path = 'http://example.com/script.js'; $javascript = drupal_add_js($path, 'external'); - $this->assertTrue(array_key_exists('http://example.com/script.js', $javascript), t('Added an external JavaScript file.')); + $this->assertTrue(array_key_exists('http://example.com/script.js', $javascript), 'Added an external JavaScript file.'); } /** @@ -1207,22 +1506,23 @@ drupal_add_js(array('commonTestArray' => array('key' => 'commonTestNewValue')), 'setting'); $javascript = drupal_get_js('header'); - $this->assertTrue(strpos($javascript, 'basePath') > 0, t('Rendered JavaScript header returns basePath setting.')); - $this->assertTrue(strpos($javascript, 'misc/jquery.js') > 0, t('Rendered JavaScript header includes jQuery.')); + $this->assertTrue(strpos($javascript, 'basePath') > 0, 'Rendered JavaScript header returns basePath setting.'); + $this->assertTrue(strpos($javascript, 'misc/jquery.js') > 0, 'Rendered JavaScript header includes jQuery.'); + $this->assertTrue(strpos($javascript, 'pathPrefix') > 0, 'Rendered JavaScript header returns pathPrefix setting.'); // Test whether drupal_add_js can be used to override a previous setting. - $this->assertTrue(strpos($javascript, 'commonTestShouldAppear') > 0, t('Rendered JavaScript header returns custom setting.')); - $this->assertTrue(strpos($javascript, 'commonTestShouldNotAppear') === FALSE, t('drupal_add_js() correctly overrides a custom setting.')); + $this->assertTrue(strpos($javascript, 'commonTestShouldAppear') > 0, 'Rendered JavaScript header returns custom setting.'); + $this->assertTrue(strpos($javascript, 'commonTestShouldNotAppear') === FALSE, 'drupal_add_js() correctly overrides a custom setting.'); // Test whether drupal_add_js can be used to add numerically indexed values // to an array. $array_values_appear = strpos($javascript, 'commonTestValue0') > 0 && strpos($javascript, 'commonTestValue1') > 0 && strpos($javascript, 'commonTestValue2') > 0; - $this->assertTrue($array_values_appear, t('drupal_add_js() correctly adds settings to the end of an indexed array.')); + $this->assertTrue($array_values_appear, 'drupal_add_js() correctly adds settings to the end of an indexed array.'); // Test whether drupal_add_js can be used to override the entry for an // existing key in an associative array. $associative_array_override = strpos($javascript, 'commonTestNewValue') > 0 && strpos($javascript, 'commonTestOldValue') === FALSE; - $this->assertTrue($associative_array_override, t('drupal_add_js() correctly overrides settings within an associative array.')); + $this->assertTrue($associative_array_override, 'drupal_add_js() correctly overrides settings within an associative array.'); } /** @@ -1231,7 +1531,7 @@ function testReset() { drupal_add_js('misc/collapse.js'); drupal_static_reset('drupal_add_js'); - $this->assertEqual(array(), drupal_add_js(), t('Resetting the JavaScript correctly empties the cache.')); + $this->assertEqual(array(), drupal_add_js(), 'Resetting the JavaScript correctly empties the cache.'); } /** @@ -1240,9 +1540,9 @@ function testAddInline() { $inline = 'jQuery(function () { });'; $javascript = drupal_add_js($inline, array('type' => 'inline', 'scope' => 'footer')); - $this->assertTrue(array_key_exists('misc/jquery.js', $javascript), t('jQuery is added when inline scripts are added.')); + $this->assertTrue(array_key_exists('misc/jquery.js', $javascript), 'jQuery is added when inline scripts are added.'); $data = end($javascript); - $this->assertEqual($inline, $data['data'], t('Inline JavaScript is correctly added to the footer.')); + $this->assertEqual($inline, $data['data'], 'Inline JavaScript is correctly added to the footer.'); } /** @@ -1253,7 +1553,7 @@ drupal_add_js($external, 'external'); $javascript = drupal_get_js(); // Local files have a base_path() prefix, external files should not. - $this->assertTrue(strpos($javascript, 'src="' . $external) > 0, t('Rendering an external JavaScript file.')); + $this->assertTrue(strpos($javascript, 'src="' . $external) > 0, 'Rendering an external JavaScript file.'); } /** @@ -1263,7 +1563,128 @@ $inline = 'jQuery(function () { });'; drupal_add_js($inline, array('type' => 'inline', 'scope' => 'footer')); $javascript = drupal_get_js('footer'); - $this->assertTrue(strpos($javascript, $inline) > 0, t('Rendered JavaScript footer returns the inline code.')); + $this->assertTrue(strpos($javascript, $inline) > 0, 'Rendered JavaScript footer returns the inline code.'); + } + + /** + * 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'); } /** @@ -1271,7 +1692,7 @@ */ function testNoCache() { $javascript = drupal_add_js('misc/collapse.js', array('cache' => FALSE)); - $this->assertFalse($javascript['misc/collapse.js']['preprocess'], t('Setting cache to FALSE sets proprocess to FALSE when adding JavaScript.')); + $this->assertFalse($javascript['misc/collapse.js']['preprocess'], 'Setting cache to FALSE sets proprocess to FALSE when adding JavaScript.'); } /** @@ -1279,7 +1700,7 @@ */ function testDifferentGroup() { $javascript = drupal_add_js('misc/collapse.js', array('group' => JS_THEME)); - $this->assertEqual($javascript['misc/collapse.js']['group'], JS_THEME, t('Adding a JavaScript file with a different group caches the given group.')); + $this->assertEqual($javascript['misc/collapse.js']['group'], JS_THEME, 'Adding a JavaScript file with a different group caches the given group.'); } /** @@ -1287,7 +1708,49 @@ */ function testDifferentWeight() { $javascript = drupal_add_js('misc/collapse.js', array('weight' => 2)); - $this->assertEqual($javascript['misc/collapse.js']['weight'], 2, t('Adding a JavaScript file with a different weight caches the given weight.')); + $this->assertEqual($javascript['misc/collapse.js']['weight'], 2, 'Adding a JavaScript file with a different weight caches the given weight.'); + } + + /** + * Tests JavaScript aggregation when files are added to a different scope. + */ + function testAggregationOrder() { + // Enable JavaScript aggregation. + variable_set('preprocess_js', 1); + drupal_static_reset('drupal_add_js'); + + // Add two JavaScript files to the current request and build the cache. + drupal_add_js('misc/ajax.js'); + drupal_add_js('misc/autocomplete.js'); + + $js_items = drupal_add_js(); + drupal_build_js_cache(array( + 'misc/ajax.js' => $js_items['misc/ajax.js'], + 'misc/autocomplete.js' => $js_items['misc/autocomplete.js'] + )); + + // Store the expected key for the first item in the cache. + $cache = array_keys(variable_get('drupal_js_cache_files', array())); + $expected_key = $cache[0]; + + // Reset variables and add a file in a different scope first. + variable_del('drupal_js_cache_files'); + drupal_static_reset('drupal_add_js'); + drupal_add_js('some/custom/javascript_file.js', array('scope' => 'footer')); + drupal_add_js('misc/ajax.js'); + drupal_add_js('misc/autocomplete.js'); + + // Rebuild the cache. + $js_items = drupal_add_js(); + drupal_build_js_cache(array( + 'misc/ajax.js' => $js_items['misc/ajax.js'], + 'misc/autocomplete.js' => $js_items['misc/autocomplete.js'] + )); + + // Compare the expected key for the first file to the current one. + $cache = array_keys(variable_get('drupal_js_cache_files', array())); + $key = $cache[0]; + $this->assertEqual($key, $expected_key, 'JavaScript aggregation is not affected by ordering in different scopes.'); } /** @@ -1329,7 +1792,7 @@ else { $result = array(); } - $this->assertIdentical($result, $expected, t('JavaScript is added in the expected weight order.')); + $this->assertIdentical($result, $expected, 'JavaScript is added in the expected weight order.'); } /** @@ -1341,7 +1804,7 @@ // weight, we need the other two options to be the same. drupal_add_js('misc/collapse.js', array('group' => JS_LIBRARY, 'every_page' => TRUE, 'weight' => -21)); $javascript = drupal_get_js(); - $this->assertTrue(strpos($javascript, 'misc/collapse.js') < strpos($javascript, 'misc/jquery.js'), t('Rendering a JavaScript file above jQuery.')); + $this->assertTrue(strpos($javascript, 'misc/collapse.js') < strpos($javascript, 'misc/jquery.js'), 'Rendering a JavaScript file above jQuery.'); } /** @@ -1358,7 +1821,7 @@ // tableselect.js. See simpletest_js_alter() to see where this alteration // takes place. $javascript = drupal_get_js(); - $this->assertTrue(strpos($javascript, 'simpletest.js') < strpos($javascript, 'misc/tableselect.js'), t('Altering JavaScript weight through the alter hook.')); + $this->assertTrue(strpos($javascript, 'simpletest.js') < strpos($javascript, 'misc/tableselect.js'), 'Altering JavaScript weight through the alter hook.'); } /** @@ -1366,11 +1829,11 @@ */ function testLibraryRender() { $result = drupal_add_library('system', 'farbtastic'); - $this->assertTrue($result !== FALSE, t('Library was added without errors.')); + $this->assertTrue($result !== FALSE, 'Library was added without errors.'); $scripts = drupal_get_js(); $styles = drupal_get_css(); - $this->assertTrue(strpos($scripts, 'misc/farbtastic/farbtastic.js'), t('JavaScript of library was added to the page.')); - $this->assertTrue(strpos($styles, 'misc/farbtastic/farbtastic.css'), t('Stylesheet of library was added to the page.')); + $this->assertTrue(strpos($scripts, 'misc/farbtastic/farbtastic.js'), 'JavaScript of library was added to the page.'); + $this->assertTrue(strpos($styles, 'misc/farbtastic/farbtastic.css'), 'Stylesheet of library was added to the page.'); } /** @@ -1381,12 +1844,12 @@ function testLibraryAlter() { // Verify that common_test altered the title of Farbtastic. $library = drupal_get_library('system', 'farbtastic'); - $this->assertEqual($library['title'], 'Farbtastic: Altered Library', t('Registered libraries were altered.')); + $this->assertEqual($library['title'], 'Farbtastic: Altered Library', 'Registered libraries were altered.'); // common_test_library_alter() also added a dependency on jQuery Form. drupal_add_library('system', 'farbtastic'); $scripts = drupal_get_js(); - $this->assertTrue(strpos($scripts, 'misc/jquery.form.js'), t('Altered library dependencies are added to the page.')); + $this->assertTrue(strpos($scripts, 'misc/jquery.form.js'), 'Altered library dependencies are added to the page.'); } /** @@ -1396,7 +1859,7 @@ */ function testLibraryNameConflicts() { $farbtastic = drupal_get_library('common_test', 'farbtastic'); - $this->assertEqual($farbtastic['title'], 'Custom Farbtastic Library', t('Alternative libraries can be added to the page.')); + $this->assertEqual($farbtastic['title'], 'Custom Farbtastic Library', 'Alternative libraries can be added to the page.'); } /** @@ -1404,13 +1867,13 @@ */ function testLibraryUnknown() { $result = drupal_get_library('unknown', 'unknown'); - $this->assertFalse($result, t('Unknown library returned FALSE.')); + $this->assertFalse($result, 'Unknown library returned FALSE.'); drupal_static_reset('drupal_get_library'); $result = drupal_add_library('unknown', 'unknown'); - $this->assertFalse($result, t('Unknown library returned FALSE.')); + $this->assertFalse($result, 'Unknown library returned FALSE.'); $scripts = drupal_get_js(); - $this->assertTrue(strpos($scripts, 'unknown') === FALSE, t('Unknown library was not added to the page.')); + $this->assertTrue(strpos($scripts, 'unknown') === FALSE, 'Unknown library was not added to the page.'); } /** @@ -1420,7 +1883,7 @@ $element['#attached']['library'][] = array('system', 'farbtastic'); drupal_render($element); $scripts = drupal_get_js(); - $this->assertTrue(strpos($scripts, 'misc/farbtastic/farbtastic.js'), t('The attached_library property adds the additional libraries.')); + $this->assertTrue(strpos($scripts, 'misc/farbtastic/farbtastic.js'), 'The attached_library property adds the additional libraries.'); } /** @@ -1429,18 +1892,18 @@ function testGetLibrary() { // Retrieve all libraries registered by a module. $libraries = drupal_get_library('common_test'); - $this->assertTrue(isset($libraries['farbtastic']), t('Retrieved all module libraries.')); + $this->assertTrue(isset($libraries['farbtastic']), 'Retrieved all module libraries.'); // Retrieve all libraries for a module not implementing hook_library(). // Note: This test installs Locale module. $libraries = drupal_get_library('locale'); - $this->assertEqual($libraries, array(), t('Retrieving libraries from a module not implementing hook_library() returns an emtpy array.')); + $this->assertEqual($libraries, array(), 'Retrieving libraries from a module not implementing hook_library() returns an emtpy array.'); // Retrieve a specific library by module and name. $farbtastic = drupal_get_library('common_test', 'farbtastic'); - $this->assertEqual($farbtastic['version'], '5.3', t('Retrieved a single library.')); + $this->assertEqual($farbtastic['version'], '5.3', 'Retrieved a single library.'); // Retrieve a non-existing library by module and name. $farbtastic = drupal_get_library('common_test', 'foo'); - $this->assertIdentical($farbtastic, FALSE, t('Retrieving a non-existing library returns FALSE.')); + $this->assertIdentical($farbtastic, FALSE, 'Retrieving a non-existing library returns FALSE.'); } /** @@ -1450,7 +1913,16 @@ function testAddJsFileWithQueryString() { $this->drupalGet('common-test/query-string'); $query_string = variable_get('css_js_query_string', '0'); - $this->assertRaw(drupal_get_path('module', 'node') . '/node.js?' . $query_string, t('Query string was appended correctly to js.')); + $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'); } } @@ -1471,6 +1943,60 @@ } /** + * Tests the output drupal_render() for some elementary input values. + */ + function testDrupalRenderBasics() { + $types = array( + array( + 'name' => 'null', + 'value' => NULL, + 'expected' => '', + ), + array( + 'name' => 'no value', + 'expected' => '', + ), + array( + 'name' => 'empty string', + 'value' => '', + 'expected' => '', + ), + array( + 'name' => 'no access', + 'value' => array( + '#markup' => 'foo', + '#access' => FALSE, + ), + 'expected' => '', + ), + array( + 'name' => 'previously printed', + 'value' => array( + '#markup' => 'foo', + '#printed' => TRUE, + ), + 'expected' => '', + ), + array( + 'name' => 'printed in prerender', + 'value' => array( + '#markup' => 'foo', + '#pre_render' => array('common_test_drupal_render_printing_pre_render'), + ), + 'expected' => '', + ), + array( + 'name' => 'basic renderable array', + 'value' => array('#markup' => 'foo'), + 'expected' => 'foo', + ), + ); + foreach($types as $type) { + $this->assertIdentical(drupal_render($type['value']), $type['expected'], '"' . $type['name'] . '" input rendered correctly by drupal_render().'); + } + } + + /** * Test sorting by weight. */ function testDrupalRenderSorting() { @@ -1490,17 +2016,17 @@ $output = drupal_render($elements); // The lowest weight element should appear last in $output. - $this->assertTrue(strpos($output, $second) > strpos($output, $first), t('Elements were sorted correctly by weight.')); + $this->assertTrue(strpos($output, $second) > strpos($output, $first), 'Elements were sorted correctly by weight.'); // Confirm that the $elements array has '#sorted' set to TRUE. - $this->assertTrue($elements['#sorted'], t("'#sorted' => TRUE was added to the array")); + $this->assertTrue($elements['#sorted'], "'#sorted' => TRUE was added to the array"); // Pass $elements through element_children() and ensure it remains // sorted in the correct order. drupal_render() will return an empty string // if used on the same array in the same request. $children = element_children($elements); - $this->assertTrue(array_shift($children) == 'first', t('Child found in the correct order.')); - $this->assertTrue(array_shift($children) == 'second', t('Child found in the correct order.')); + $this->assertTrue(array_shift($children) == 'first', 'Child found in the correct order.'); + $this->assertTrue(array_shift($children) == 'second', 'Child found in the correct order.'); // The same array structure again, but with #sorted set to TRUE. @@ -1518,7 +2044,57 @@ $output = drupal_render($elements); // The elements should appear in output in the same order as the array. - $this->assertTrue(strpos($output, $second) < strpos($output, $first), t('Elements were not sorted.')); + $this->assertTrue(strpos($output, $second) < strpos($output, $first), 'Elements were not sorted.'); + } + + /** + * Test #attached functionality in children elements. + */ + function testDrupalRenderChildrenAttached() { + // The cache system is turned off for POST requests. + $request_method = $_SERVER['REQUEST_METHOD']; + $_SERVER['REQUEST_METHOD'] = 'GET'; + + // Create an element with a child and subchild. Each element loads a + // different JavaScript file using #attached. + $parent_js = drupal_get_path('module', 'user') . '/user.js'; + $child_js = drupal_get_path('module', 'forum') . '/forum.js'; + $subchild_js = drupal_get_path('module', 'book') . '/book.js'; + $element = array( + '#type' => 'fieldset', + '#cache' => array( + 'keys' => array('simpletest', 'drupal_render', 'children_attached'), + ), + '#attached' => array('js' => array($parent_js)), + '#title' => 'Parent', + ); + $element['child'] = array( + '#type' => 'fieldset', + '#attached' => array('js' => array($child_js)), + '#title' => 'Child', + ); + $element['child']['subchild'] = array( + '#attached' => array('js' => array($subchild_js)), + '#markup' => 'Subchild', + ); + + // Render the element and verify the presence of #attached JavaScript. + drupal_render($element); + $scripts = drupal_get_js(); + $this->assertTrue(strpos($scripts, $parent_js), 'The element #attached JavaScript was included.'); + $this->assertTrue(strpos($scripts, $child_js), 'The child #attached JavaScript was included.'); + $this->assertTrue(strpos($scripts, $subchild_js), 'The subchild #attached JavaScript was included.'); + + // Load the element from cache and verify the presence of the #attached + // JavaScript. + drupal_static_reset('drupal_add_js'); + $this->assertTrue(drupal_render_cache_get($element), 'The element was retrieved from cache.'); + $scripts = drupal_get_js(); + $this->assertTrue(strpos($scripts, $parent_js), 'The element #attached JavaScript was included when loading from cache.'); + $this->assertTrue(strpos($scripts, $child_js), 'The child #attached JavaScript was included when loading from cache.'); + $this->assertTrue(strpos($scripts, $subchild_js), 'The subchild #attached JavaScript was included when loading from cache.'); + + $_SERVER['REQUEST_METHOD'] = $request_method; } /** @@ -1660,10 +2236,94 @@ // @see DrupalWebTestCase::xpath() $xpath = $this->buildXPathQuery($xpath, $xpath_args); $element += array('#value' => NULL); - $this->assertFieldByXPath($xpath, $element['#value'], t('#type @type was properly rendered.', array( + $this->assertFieldByXPath($xpath, $element['#value'], format_string('#type @type was properly rendered.', array( '@type' => var_export($element['#type'], TRUE), ))); } + + /** + * Tests caching of render items. + */ + function testDrupalRenderCache() { + // Force a request via GET. + $request_method = $_SERVER['REQUEST_METHOD']; + $_SERVER['REQUEST_METHOD'] = 'GET'; + // Create an empty element. + $test_element = array( + '#cache' => array( + 'cid' => 'render_cache_test', + ), + '#markup' => '', + ); + + // Render the element and confirm that it goes through the rendering + // process (which will set $element['#printed']). + $element = $test_element; + drupal_render($element); + $this->assertTrue(isset($element['#printed']), 'No cache hit'); + + // Render the element again and confirm that it is retrieved from the cache + // instead (so $element['#printed'] will not be set). + $element = $test_element; + 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; + } } /** @@ -1672,14 +2332,14 @@ class ValidUrlTestCase extends DrupalUnitTestCase { public static function getInfo() { return array( - 'name' => 'Valid Url', - 'description' => "Performs tests on Drupal's valid url function.", + 'name' => 'Valid URL', + 'description' => "Performs tests on Drupal's valid URL function.", 'group' => 'System' ); } /** - * Test valid absolute urls. + * Test valid absolute URLs. */ function testValidAbsolute() { $url_schemes = array('http', 'https', 'ftp'); @@ -1708,13 +2368,13 @@ foreach ($valid_absolute_urls as $url) { $test_url = $scheme . '://' . $url; $valid_url = valid_url($test_url, TRUE); - $this->assertTrue($valid_url, t('@url is a valid url.', array('@url' => $test_url))); + $this->assertTrue($valid_url, format_string('@url is a valid url.', array('@url' => $test_url))); } } } /** - * Test invalid absolute urls. + * Test invalid absolute URLs. */ function testInvalidAbsolute() { $url_schemes = array('http', 'https', 'ftp'); @@ -1728,13 +2388,13 @@ foreach ($invalid_ablosule_urls as $url) { $test_url = $scheme . '://' . $url; $valid_url = valid_url($test_url, TRUE); - $this->assertFalse($valid_url, t('@url is NOT a valid url.', array('@url' => $test_url))); + $this->assertFalse($valid_url, format_string('@url is NOT a valid url.', array('@url' => $test_url))); } } } /** - * Test valid relative urls. + * Test valid relative URLs. */ function testValidRelative() { $valid_relative_urls = array( @@ -1749,13 +2409,13 @@ foreach ($valid_relative_urls as $url) { $test_url = $front . $url; $valid_url = valid_url($test_url); - $this->assertTrue($valid_url, t('@url is a valid url.', array('@url' => $test_url))); + $this->assertTrue($valid_url, format_string('@url is a valid url.', array('@url' => $test_url))); } } } /** - * Test invalid relative urls. + * Test invalid relative URLs. */ function testInvalidRelative() { $invalid_relative_urls = array( @@ -1768,7 +2428,7 @@ foreach ($invalid_relative_urls as $url) { $test_url = $front . $url; $valid_url = valid_url($test_url); - $this->assertFALSE($valid_url, t('@url is NOT a valid url.', array('@url' => $test_url))); + $this->assertFALSE($valid_url, format_string('@url is NOT a valid url.', array('@url' => $test_url))); } } } @@ -1799,30 +2459,30 @@ $person->name = 'John'; $person->unknown_column = 123; $insert_result = drupal_write_record('test', $person); - $this->assertTrue($insert_result == SAVED_NEW, t('Correct value returned when a record is inserted with drupal_write_record() for a table with a single-field primary key.')); - $this->assertTrue(isset($person->id), t('Primary key is set on record created with drupal_write_record().')); - $this->assertIdentical($person->age, 0, t('Age field set to default value.')); - $this->assertIdentical($person->job, 'Undefined', t('Job field set to default value.')); + $this->assertTrue($insert_result == SAVED_NEW, 'Correct value returned when a record is inserted with drupal_write_record() for a table with a single-field primary key.'); + $this->assertTrue(isset($person->id), 'Primary key is set on record created with drupal_write_record().'); + $this->assertIdentical($person->age, 0, 'Age field set to default value.'); + $this->assertIdentical($person->job, 'Undefined', 'Job field set to default value.'); // Verify that the record was inserted. $result = db_query("SELECT * FROM {test} WHERE id = :id", array(':id' => $person->id))->fetchObject(); - $this->assertIdentical($result->name, 'John', t('Name field set.')); - $this->assertIdentical($result->age, '0', t('Age field set to default value.')); - $this->assertIdentical($result->job, 'Undefined', t('Job field set to default value.')); - $this->assertFalse(isset($result->unknown_column), t('Unknown column was ignored.')); + $this->assertIdentical($result->name, 'John', 'Name field set.'); + $this->assertIdentical($result->age, '0', 'Age field set to default value.'); + $this->assertIdentical($result->job, 'Undefined', 'Job field set to default value.'); + $this->assertFalse(isset($result->unknown_column), 'Unknown column was ignored.'); // Update the newly created record. $person->name = 'Peter'; $person->age = 27; $person->job = NULL; $update_result = drupal_write_record('test', $person, array('id')); - $this->assertTrue($update_result == SAVED_UPDATED, t('Correct value returned when a record updated with drupal_write_record() for table with single-field primary key.')); + $this->assertTrue($update_result == SAVED_UPDATED, 'Correct value returned when a record updated with drupal_write_record() for table with single-field primary key.'); // Verify that the record was updated. $result = db_query("SELECT * FROM {test} WHERE id = :id", array(':id' => $person->id))->fetchObject(); - $this->assertIdentical($result->name, 'Peter', t('Name field set.')); - $this->assertIdentical($result->age, '27', t('Age field set.')); - $this->assertIdentical($result->job, '', t('Job field set and cast to string.')); + $this->assertIdentical($result->name, 'Peter', 'Name field set.'); + $this->assertIdentical($result->age, '27', 'Age field set.'); + $this->assertIdentical($result->job, '', 'Job field set and cast to string.'); // Try to insert NULL in columns that does not allow this. $person = new stdClass(); @@ -1830,65 +2490,65 @@ $person->age = NULL; $person->job = NULL; $insert_result = drupal_write_record('test', $person); - $this->assertTrue(isset($person->id), t('Primary key is set on record created with drupal_write_record().')); + $this->assertTrue(isset($person->id), 'Primary key is set on record created with drupal_write_record().'); $result = db_query("SELECT * FROM {test} WHERE id = :id", array(':id' => $person->id))->fetchObject(); - $this->assertIdentical($result->name, 'Ringo', t('Name field set.')); - $this->assertIdentical($result->age, '0', t('Age field set.')); - $this->assertIdentical($result->job, '', t('Job field set.')); + $this->assertIdentical($result->name, 'Ringo', 'Name field set.'); + $this->assertIdentical($result->age, '0', 'Age field set.'); + $this->assertIdentical($result->job, '', 'Job field set.'); // Insert a record - the "age" column allows NULL. $person = new stdClass(); $person->name = 'Paul'; $person->age = NULL; $insert_result = drupal_write_record('test_null', $person); - $this->assertTrue(isset($person->id), t('Primary key is set on record created with drupal_write_record().')); + $this->assertTrue(isset($person->id), 'Primary key is set on record created with drupal_write_record().'); $result = db_query("SELECT * FROM {test_null} WHERE id = :id", array(':id' => $person->id))->fetchObject(); - $this->assertIdentical($result->name, 'Paul', t('Name field set.')); - $this->assertIdentical($result->age, NULL, t('Age field set.')); + $this->assertIdentical($result->name, 'Paul', 'Name field set.'); + $this->assertIdentical($result->age, NULL, 'Age field set.'); // Insert a record - do not specify the value of a column that allows NULL. $person = new stdClass(); $person->name = 'Meredith'; $insert_result = drupal_write_record('test_null', $person); - $this->assertTrue(isset($person->id), t('Primary key is set on record created with drupal_write_record().')); - $this->assertIdentical($person->age, 0, t('Age field set to default value.')); + $this->assertTrue(isset($person->id), 'Primary key is set on record created with drupal_write_record().'); + $this->assertIdentical($person->age, 0, 'Age field set to default value.'); $result = db_query("SELECT * FROM {test_null} WHERE id = :id", array(':id' => $person->id))->fetchObject(); - $this->assertIdentical($result->name, 'Meredith', t('Name field set.')); - $this->assertIdentical($result->age, '0', t('Age field set to default value.')); + $this->assertIdentical($result->name, 'Meredith', 'Name field set.'); + $this->assertIdentical($result->age, '0', 'Age field set to default value.'); // Update the newly created record. $person->name = 'Mary'; $person->age = NULL; $update_result = drupal_write_record('test_null', $person, array('id')); $result = db_query("SELECT * FROM {test_null} WHERE id = :id", array(':id' => $person->id))->fetchObject(); - $this->assertIdentical($result->name, 'Mary', t('Name field set.')); - $this->assertIdentical($result->age, NULL, t('Age field set.')); + $this->assertIdentical($result->name, 'Mary', 'Name field set.'); + $this->assertIdentical($result->age, NULL, 'Age field set.'); // Insert a record - the "data" column should be serialized. $person = new stdClass(); $person->name = 'Dave'; $update_result = drupal_write_record('test_serialized', $person); $result = db_query("SELECT * FROM {test_serialized} WHERE id = :id", array(':id' => $person->id))->fetchObject(); - $this->assertIdentical($result->name, 'Dave', t('Name field set.')); - $this->assertIdentical($result->info, NULL, t('Info field set.')); + $this->assertIdentical($result->name, 'Dave', 'Name field set.'); + $this->assertIdentical($result->info, NULL, 'Info field set.'); $person->info = array(); $update_result = drupal_write_record('test_serialized', $person, array('id')); $result = db_query("SELECT * FROM {test_serialized} WHERE id = :id", array(':id' => $person->id))->fetchObject(); - $this->assertIdentical(unserialize($result->info), array(), t('Info field updated.')); + $this->assertIdentical(unserialize($result->info), array(), 'Info field updated.'); // Update the serialized record. $data = array('foo' => 'bar', 1 => 2, 'empty' => '', 'null' => NULL); $person->info = $data; $update_result = drupal_write_record('test_serialized', $person, array('id')); $result = db_query("SELECT * FROM {test_serialized} WHERE id = :id", array(':id' => $person->id))->fetchObject(); - $this->assertIdentical(unserialize($result->info), $data, t('Info field updated.')); + $this->assertIdentical(unserialize($result->info), $data, 'Info field updated.'); // Run an update query where no field values are changed. The database // layer should return zero for number of affected rows, but // db_write_record() should still return SAVED_UPDATED. $update_result = drupal_write_record('test_null', $person, array('id')); - $this->assertTrue($update_result == SAVED_UPDATED, t('Correct value returned when a valid update is run without changing any values.')); + $this->assertTrue($update_result == SAVED_UPDATED, 'Correct value returned when a valid update is run without changing any values.'); // Insert an object record for a table with a multi-field primary key. $node_access = new stdClass(); @@ -1896,11 +2556,11 @@ $node_access->gid = mt_rand(); $node_access->realm = $this->randomName(); $insert_result = drupal_write_record('node_access', $node_access); - $this->assertTrue($insert_result == SAVED_NEW, t('Correct value returned when a record is inserted with drupal_write_record() for a table with a multi-field primary key.')); + $this->assertTrue($insert_result == SAVED_NEW, 'Correct value returned when a record is inserted with drupal_write_record() for a table with a multi-field primary key.'); // Update the record. $update_result = drupal_write_record('node_access', $node_access, array('nid', 'gid', 'realm')); - $this->assertTrue($update_result == SAVED_UPDATED, t('Correct value returned when a record is updated with drupal_write_record() for a table with a multi-field primary key.')); + $this->assertTrue($update_result == SAVED_UPDATED, 'Correct value returned when a record is updated with drupal_write_record() for a table with a multi-field primary key.'); } } @@ -1938,7 +2598,7 @@ function testErrorCollect() { $this->collectedErrors = array(); $this->drupalGet('error-test/generate-warnings-with-report'); - $this->assertEqual(count($this->collectedErrors), 3, t('Three errors were collected')); + $this->assertEqual(count($this->collectedErrors), 3, 'Three errors were collected'); if (count($this->collectedErrors) == 3) { $this->assertError($this->collectedErrors[0], 'Notice', 'error_test_generate_warnings()', 'error_test.module', 'Undefined variable: bananas'); @@ -1987,11 +2647,11 @@ * Assert that a collected error matches what we are expecting. */ function assertError($error, $group, $function, $file, $message = NULL) { - $this->assertEqual($error['group'], $group, t("Group was %group", array('%group' => $group))); - $this->assertEqual($error['caller']['function'], $function, t("Function was %function", array('%function' => $function))); - $this->assertEqual(basename($error['caller']['file']), $file, t("File was %file", array('%file' => $file))); + $this->assertEqual($error['group'], $group, format_string("Group was %group", array('%group' => $group))); + $this->assertEqual($error['caller']['function'], $function, format_string("Function was %function", array('%function' => $function))); + $this->assertEqual(drupal_basename($error['caller']['file']), $file, format_string("File was %file", array('%file' => $file))); if (isset($message)) { - $this->assertEqual($error['message'], $message, t("Message was %message", array('%message' => $message))); + $this->assertEqual($error['message'], $message, format_string("Message was %message", array('%message' => $message))); } } } @@ -1999,7 +2659,7 @@ /** * Test the drupal_parse_info_file() API function. */ -class ParseInfoFilesTestCase extends DrupalWebTestCase { +class ParseInfoFilesTestCase extends DrupalUnitTestCase { public static function getInfo() { return array( 'name' => 'Parsing .info files', @@ -2013,9 +2673,9 @@ */ function testParseInfoFile() { $info_values = drupal_parse_info_file(drupal_get_path('module', 'simpletest') . '/tests/common_test_info.txt'); - $this->assertEqual($info_values['simple_string'], 'A simple string', t('Simple string value was parsed correctly.'), t('System')); - $this->assertEqual($info_values['simple_constant'], WATCHDOG_INFO, t('Constant value was parsed correctly.'), t('System')); - $this->assertEqual($info_values['double_colon'], 'dummyClassName::', t('Value containing double-colon was parsed correctly.'), t('System')); + $this->assertEqual($info_values['simple_string'], 'A simple string', 'Simple string value was parsed correctly.', 'System'); + $this->assertEqual($info_values['simple_constant'], WATCHDOG_INFO, 'Constant value was parsed correctly.', 'System'); + $this->assertEqual($info_values['double_colon'], 'dummyClassName::', 'Value containing double-colon was parsed correctly.', 'System'); } } @@ -2064,7 +2724,7 @@ foreach ($expected_directories as $module => $directories) { foreach ($directories as $directory) { $filename = "$directory/$module/$module.module"; - $this->assertTrue(file_exists(DRUPAL_ROOT . '/' . $filename), t('@filename exists.', array('@filename' => $filename))); + $this->assertTrue(file_exists(DRUPAL_ROOT . '/' . $filename), format_string('@filename exists.', array('@filename' => $filename))); } } @@ -2074,7 +2734,7 @@ foreach ($expected_directories as $module => $directories) { $expected_directory = array_shift($directories); $expected_filename = "$expected_directory/$module/$module.module"; - $this->assertEqual($files[$module]->uri, $expected_filename, t('Module @module was found at @filename.', array('@module' => $module, '@filename' => $expected_filename))); + $this->assertEqual($files[$module]->uri, $expected_filename, format_string('Module @module was found at @filename.', array('@module' => $module, '@filename' => $expected_filename))); } } } @@ -2121,7 +2781,13 @@ // Add new date format. $admin_date_format = 'j M y'; $edit = array('date_format' => $admin_date_format); + $this->drupalPost('admin/config/regional/date-time/formats/add', $edit, 'Add format'); + + // Add a new date format which just differs in the case. + $admin_date_format_uppercase = 'j M Y'; + $edit = array('date_format' => $admin_date_format_uppercase); $this->drupalPost('admin/config/regional/date-time/formats/add', $edit, t('Add format')); + $this->assertText(t('Custom date format added.')); // Add new date type. $edit = array( @@ -2129,11 +2795,21 @@ 'machine_name' => 'example_style', 'date_format' => $admin_date_format, ); + $this->drupalPost('admin/config/regional/date-time/types/add', $edit, 'Add date type'); + + // Add a second date format with a different case than the first. + $edit = array( + 'machine_name' => 'example_style_uppercase', + 'date_type' => 'Example Style Uppercase', + 'date_format' => $admin_date_format_uppercase, + ); $this->drupalPost('admin/config/regional/date-time/types/add', $edit, t('Add date type')); + $this->assertText(t('New date type added successfully.')); $timestamp = strtotime('2007-03-10T00:00:00+00:00'); - $this->assertIdentical(format_date($timestamp, 'example_style', '', 'America/Los_Angeles'), '9 Mar 07', t('Test format_date() using an admin-defined date type.')); - $this->assertIdentical(format_date($timestamp, 'undefined_style'), format_date($timestamp, 'medium'), t('Test format_date() defaulting to medium when $type not found.')); + $this->assertIdentical(format_date($timestamp, 'example_style', '', 'America/Los_Angeles'), '9 Mar 07', 'Test format_date() using an admin-defined date type.'); + $this->assertIdentical(format_date($timestamp, 'example_style_uppercase', '', 'America/Los_Angeles'), '9 Mar 2007', 'Test format_date() using an admin-defined date type with different case.'); + $this->assertIdentical(format_date($timestamp, 'undefined_style'), format_date($timestamp, 'medium'), 'Test format_date() defaulting to medium when $type not found.'); } /** @@ -2143,12 +2819,12 @@ global $user, $language; $timestamp = strtotime('2007-03-26T00:00:00+00:00'); - $this->assertIdentical(format_date($timestamp, 'custom', 'l, d-M-y H:i:s T', 'America/Los_Angeles', 'en'), 'Sunday, 25-Mar-07 17:00:00 PDT', t('Test all parameters.')); - $this->assertIdentical(format_date($timestamp, 'custom', 'l, d-M-y H:i:s T', 'America/Los_Angeles', self::LANGCODE), 'domingo, 25-Mar-07 17:00:00 PDT', t('Test translated format.')); - $this->assertIdentical(format_date($timestamp, 'custom', '\\l, d-M-y H:i:s T', 'America/Los_Angeles', self::LANGCODE), 'l, 25-Mar-07 17:00:00 PDT', t('Test an escaped format string.')); - $this->assertIdentical(format_date($timestamp, 'custom', '\\\\l, d-M-y H:i:s T', 'America/Los_Angeles', self::LANGCODE), '\\domingo, 25-Mar-07 17:00:00 PDT', t('Test format containing backslash character.')); - $this->assertIdentical(format_date($timestamp, 'custom', '\\\\\\l, d-M-y H:i:s T', 'America/Los_Angeles', self::LANGCODE), '\\l, 25-Mar-07 17:00:00 PDT', t('Test format containing backslash followed by escaped format string.')); - $this->assertIdentical(format_date($timestamp, 'custom', 'l, d-M-y H:i:s T', 'Europe/London', 'en'), 'Monday, 26-Mar-07 01:00:00 BST', t('Test a different time zone.')); + $this->assertIdentical(format_date($timestamp, 'custom', 'l, d-M-y H:i:s T', 'America/Los_Angeles', 'en'), 'Sunday, 25-Mar-07 17:00:00 PDT', 'Test all parameters.'); + $this->assertIdentical(format_date($timestamp, 'custom', 'l, d-M-y H:i:s T', 'America/Los_Angeles', self::LANGCODE), 'domingo, 25-Mar-07 17:00:00 PDT', 'Test translated format.'); + $this->assertIdentical(format_date($timestamp, 'custom', '\\l, d-M-y H:i:s T', 'America/Los_Angeles', self::LANGCODE), 'l, 25-Mar-07 17:00:00 PDT', 'Test an escaped format string.'); + $this->assertIdentical(format_date($timestamp, 'custom', '\\\\l, d-M-y H:i:s T', 'America/Los_Angeles', self::LANGCODE), '\\domingo, 25-Mar-07 17:00:00 PDT', 'Test format containing backslash character.'); + $this->assertIdentical(format_date($timestamp, 'custom', '\\\\\\l, d-M-y H:i:s T', 'America/Los_Angeles', self::LANGCODE), '\\l, 25-Mar-07 17:00:00 PDT', 'Test format containing backslash followed by escaped format string.'); + $this->assertIdentical(format_date($timestamp, 'custom', 'l, d-M-y H:i:s T', 'Europe/London', 'en'), 'Monday, 26-Mar-07 01:00:00 BST', 'Test a different time zone.'); // Create an admin user and add Spanish language. $admin_user = $this->drupalCreateUser(array('administer languages')); @@ -2178,13 +2854,13 @@ // Simulate a Drupal bootstrap with the logged-in user. date_default_timezone_set(drupal_get_user_timezone()); - $this->assertIdentical(format_date($timestamp, 'custom', 'l, d-M-y H:i:s T', 'America/Los_Angeles', 'en'), 'Sunday, 25-Mar-07 17:00:00 PDT', t('Test a different language.')); - $this->assertIdentical(format_date($timestamp, 'custom', 'l, d-M-y H:i:s T', 'Europe/London'), 'Monday, 26-Mar-07 01:00:00 BST', t('Test a different time zone.')); - $this->assertIdentical(format_date($timestamp, 'custom', 'l, d-M-y H:i:s T'), 'domingo, 25-Mar-07 17:00:00 PDT', t('Test custom date format.')); - $this->assertIdentical(format_date($timestamp, 'long'), 'domingo, 25. marzo 2007 - 17:00', t('Test long date format.')); - $this->assertIdentical(format_date($timestamp, 'medium'), '25. marzo 2007 - 17:00', t('Test medium date format.')); - $this->assertIdentical(format_date($timestamp, 'short'), '2007 Mar 25 - 5:00pm', t('Test short date format.')); - $this->assertIdentical(format_date($timestamp), '25. marzo 2007 - 17:00', t('Test default date format.')); + $this->assertIdentical(format_date($timestamp, 'custom', 'l, d-M-y H:i:s T', 'America/Los_Angeles', 'en'), 'Sunday, 25-Mar-07 17:00:00 PDT', 'Test a different language.'); + $this->assertIdentical(format_date($timestamp, 'custom', 'l, d-M-y H:i:s T', 'Europe/London'), 'Monday, 26-Mar-07 01:00:00 BST', 'Test a different time zone.'); + $this->assertIdentical(format_date($timestamp, 'custom', 'l, d-M-y H:i:s T'), 'domingo, 25-Mar-07 17:00:00 PDT', 'Test custom date format.'); + $this->assertIdentical(format_date($timestamp, 'long'), 'domingo, 25. marzo 2007 - 17:00', 'Test long date format.'); + $this->assertIdentical(format_date($timestamp, 'medium'), '25. marzo 2007 - 17:00', 'Test medium date format.'); + $this->assertIdentical(format_date($timestamp, 'short'), '2007 Mar 25 - 5:00pm', 'Test short date format.'); + $this->assertIdentical(format_date($timestamp), '25. marzo 2007 - 17:00', 'Test default date format.'); // Restore the original user and language, and enable session saving. $user = $real_user; @@ -2212,15 +2888,15 @@ */ function testDrupalAttributes() { // Verify that special characters are HTML encoded. - $this->assertIdentical(drupal_attributes(array('title' => '&"\'<>')), ' title="&"'<>"', t('HTML encode attribute values.')); + $this->assertIdentical(drupal_attributes(array('title' => '&"\'<>')), ' title="&"'<>"', 'HTML encode attribute values.'); // Verify multi-value attributes are concatenated with spaces. $attributes = array('class' => array('first', 'last')); - $this->assertIdentical(drupal_attributes(array('class' => array('first', 'last'))), ' class="first last"', t('Concatenate multi-value attributes.')); + $this->assertIdentical(drupal_attributes(array('class' => array('first', 'last'))), ' class="first last"', 'Concatenate multi-value attributes.'); // Verify empty attribute values are rendered. - $this->assertIdentical(drupal_attributes(array('alt' => '')), ' alt=""', t('Empty attribute value #1.')); - $this->assertIdentical(drupal_attributes(array('alt' => NULL)), ' alt=""', t('Empty attribute value #2.')); + $this->assertIdentical(drupal_attributes(array('alt' => '')), ' alt=""', 'Empty attribute value #1.'); + $this->assertIdentical(drupal_attributes(array('alt' => NULL)), ' alt=""', 'Empty attribute value #2.'); // Verify multiple attributes are rendered. $attributes = array( @@ -2228,10 +2904,10 @@ 'class' => array('first', 'last'), 'alt' => 'Alternate', ); - $this->assertIdentical(drupal_attributes($attributes), ' id="id-test" class="first last" alt="Alternate"', t('Multiple attributes.')); + $this->assertIdentical(drupal_attributes($attributes), ' id="id-test" class="first last" alt="Alternate"', 'Multiple attributes.'); // Verify empty attributes array is rendered. - $this->assertIdentical(drupal_attributes(array()), '', t('Empty attributes array.')); + $this->assertIdentical(drupal_attributes(array()), '', 'Empty attributes array.'); } } @@ -2258,37 +2934,44 @@ $str .= chr($i); } // Characters that must be escaped. - $html_unsafe = array('<', '>', '&'); - $html_unsafe_escaped = array('\u003c', '\u003e', '\u0026'); + // We check for unescaped " separately. + $html_unsafe = array('<', '>', '\'', '&'); + // The following are the encoded forms of: < > ' & " + $html_unsafe_escaped = array('\u003C', '\u003E', '\u0027', '\u0026', '\u0022'); // Verify there aren't character encoding problems with the source string. - $this->assertIdentical(strlen($str), 128, t('A string with the full ASCII table has the correct length.')); + $this->assertIdentical(strlen($str), 128, 'A string with the full ASCII table has the correct length.'); foreach ($html_unsafe as $char) { - $this->assertTrue(strpos($str, $char) > 0, t('A string with the full ASCII table includes @s.', array('@s' => $char))); + $this->assertTrue(strpos($str, $char) > 0, format_string('A string with the full ASCII table includes @s.', array('@s' => $char))); } // Verify that JSON encoding produces a string with all of the characters. $json = drupal_json_encode($str); - $this->assertTrue(strlen($json) > strlen($str), t('A JSON encoded string is larger than the source string.')); + $this->assertTrue(strlen($json) > strlen($str), 'A JSON encoded string is larger than the source string.'); + + // The first and last characters should be ", and no others. + $this->assertTrue($json[0] == '"', 'A JSON encoded string begins with ".'); + $this->assertTrue($json[strlen($json) - 1] == '"', 'A JSON encoded string ends with ".'); + $this->assertTrue(substr_count($json, '"') == 2, 'A JSON encoded string contains exactly two ".'); // Verify that encoding/decoding is reversible. $json_decoded = drupal_json_decode($json); - $this->assertIdentical($str, $json_decoded, t('Encoding a string to JSON and decoding back results in the original string.')); + $this->assertIdentical($str, $json_decoded, 'Encoding a string to JSON and decoding back results in the original string.'); // Verify reversibility for structured data. Also verify that necessary // characters are escaped. $source = array(TRUE, FALSE, 0, 1, '0', '1', $str, array('key1' => $str, 'key2' => array('nested' => TRUE))); $json = drupal_json_encode($source); foreach ($html_unsafe as $char) { - $this->assertTrue(strpos($json, $char) === FALSE, t('A JSON encoded string does not contain @s.', array('@s' => $char))); + $this->assertTrue(strpos($json, $char) === FALSE, format_string('A JSON encoded string does not contain @s.', array('@s' => $char))); } // Verify that JSON encoding escapes the HTML unsafe characters foreach ($html_unsafe_escaped as $char) { - $this->assertTrue(strpos($json, $char) > 0, t('A JSON encoded string contains @s.', array('@s' => $char))); + $this->assertTrue(strpos($json, $char) > 0, format_string('A JSON encoded string contains @s.', array('@s' => $char))); } $json_decoded = drupal_json_decode($json); - $this->assertNotIdentical($source, $json, t('An array encoded in JSON is not identical to the source.')); - $this->assertIdentical($source, $json_decoded, t('Encoding structured data to JSON and decoding back results in the original data.')); + $this->assertNotIdentical($source, $json, 'An array encoded in JSON is not identical to the source.'); + $this->assertIdentical($source, $json_decoded, 'Encoding structured data to JSON and decoding back results in the original data.'); } } @@ -2317,10 +3000,10 @@ $xml = new SimpleXMLElement($this->content); $ns = $xml->getDocNamespaces(); - $this->assertEqual($ns['rdfs'], 'http://www.w3.org/2000/01/rdf-schema#', t('A prefix declared once is displayed.')); - $this->assertEqual($ns['foaf'], 'http://xmlns.com/foaf/0.1/', t('The same prefix declared in several implementations of hook_rdf_namespaces() is valid as long as all the namespaces are the same.')); - $this->assertEqual($ns['foaf1'], 'http://xmlns.com/foaf/0.1/', t('Two prefixes can be assigned the same namespace.')); - $this->assertTrue(!isset($ns['dc']), t('A prefix with conflicting namespaces is discarded.')); + $this->assertEqual($ns['rdfs'], 'http://www.w3.org/2000/01/rdf-schema#', 'A prefix declared once is displayed.'); + $this->assertEqual($ns['foaf'], 'http://xmlns.com/foaf/0.1/', 'The same prefix declared in several implementations of hook_rdf_namespaces() is valid as long as all the namespaces are the same.'); + $this->assertEqual($ns['foaf1'], 'http://xmlns.com/foaf/0.1/', 'Two prefixes can be assigned the same namespace.'); + $this->assertTrue(!isset($ns['dc']), 'A prefix with conflicting namespaces is discarded.'); } } @@ -2358,12 +3041,12 @@ 'output_url' => url($path, array('absolute' => TRUE)), 'title' => '', ), - 'external url without title' => array( + 'external URL without title' => array( 'input_url' => $external_url, 'output_url' => $external_url, 'title' => '', ), - 'local url without title' => array( + 'local URL without title' => array( 'input_url' => $fully_qualified_local_url, 'output_url' => $fully_qualified_local_url, 'title' => '', @@ -2373,12 +3056,12 @@ 'output_url' => url($path_for_title, array('absolute' => TRUE)), 'title' => $this->randomName(12), ), - 'external url with title' => array( + 'external URL with title' => array( 'input_url' => $external_for_title, 'output_url' => $external_for_title, 'title' => $this->randomName(12), ), - 'local url with title' => array( + 'local URL with title' => array( 'input_url' => $fully_qualified_for_title, 'output_url' => $fully_qualified_for_title, 'title' => $this->randomName(12), @@ -2391,7 +3074,7 @@ $this->drupalSetContent(drupal_get_html_head()); foreach ($urls as $description => $feed_info) { - $this->assertPattern($this->urlToRSSLinkPattern($feed_info['output_url'], $feed_info['title']), t('Found correct feed header for %description', array('%description' => $description))); + $this->assertPattern($this->urlToRSSLinkPattern($feed_info['output_url'], $feed_info['title']), format_string('Found correct feed header for %description', array('%description' => $description))); } } @@ -2399,9 +3082,133 @@ * Create a pattern representing the RSS feed in the page. */ function urlToRSSLinkPattern($url, $title = '') { - // Escape any regular expression characters in the url ('?' is the worst). + // Escape any regular expression characters in the URL ('?' is the worst). $url = preg_replace('/([+?.*])/', '[$0]', $url); $generated_pattern = '%<link +rel="alternate" +type="application/rss.xml" +title="' . $title . '" +href="' . $url . '" */>%'; return $generated_pattern; } } + +/** + * Test for theme_feed_icon(). + */ +class FeedIconTest extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Feed icon', + 'description' => 'Check escaping of theme_feed_icon()', + 'group' => 'System', + ); + } + + /** + * Check that special characters are correctly escaped. Test for issue #1211668. + */ + function testFeedIconEscaping() { + $variables = array(); + $variables['url'] = 'node'; + $variables['title'] = '<>&"\''; + $text = theme_feed_icon($variables); + preg_match('/title="(.*?)"/', $text, $matches); + $this->assertEqual($matches[1], 'Subscribe to &"'', 'theme_feed_icon() escapes reserved HTML characters.'); + } + +} + +/** + * Test array diff functions. + */ +class ArrayDiffUnitTest extends DrupalUnitTestCase { + + /** + * Array to use for testing. + * + * @var array + */ + protected $array1; + + /** + * Array to use for testing. + * + * @var array + */ + protected $array2; + + public static function getInfo() { + return array( + 'name' => 'Array differences', + 'description' => 'Performs tests on drupal_array_diff_assoc_recursive().', + 'group' => 'System', + ); + } + + function setUp() { + parent::setUp(); + + $this->array1 = array( + 'same' => 'yes', + 'different' => 'no', + 'array_empty_diff' => array(), + 'null' => NULL, + 'int_diff' => 1, + 'array_diff' => array('same' => 'same', 'array' => array('same' => 'same')), + 'array_compared_to_string' => array('value'), + 'string_compared_to_array' => 'value', + 'new' => 'new', + ); + $this->array2 = array( + 'same' => 'yes', + 'different' => 'yes', + 'array_empty_diff' => array(), + 'null' => NULL, + 'int_diff' => '1', + 'array_diff' => array('same' => 'different', 'array' => array('same' => 'same')), + 'array_compared_to_string' => 'value', + 'string_compared_to_array' => array('value'), + ); + } + + + /** + * Tests drupal_array_diff_assoc_recursive(). + */ + public function testArrayDiffAssocRecursive() { + $expected = array( + 'different' => 'no', + 'int_diff' => 1, + // The 'array' key should not be returned, as it's the same. + 'array_diff' => array('same' => 'same'), + 'array_compared_to_string' => array('value'), + 'string_compared_to_array' => 'value', + 'new' => 'new', + ); + + $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.0/modules/simpletest/tests/common_test.css drupal-7.66/modules/simpletest/tests/common_test.css --- drupal-7.0/modules/simpletest/tests/common_test.css 2010-10-03 07:11:16.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/common_test.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,3 +1,2 @@ -/* $Id: common_test.css,v 1.1 2010/10/03 05:11:16 webchick Exp $ */ /* This file is for testing CSS file inclusion, no contents are necessary. */ diff -Naur drupal-7.0/modules/simpletest/tests/common_test.info drupal-7.66/modules/simpletest/tests/common_test.info --- drupal-7.0/modules/simpletest/tests/common_test.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/common_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: common_test.info,v 1.3 2010/12/20 19:59:43 webchick Exp $ name = "Common Test" description = "Support module for Common tests." package = Testing @@ -8,8 +7,7 @@ stylesheets[print][] = common_test.print.css hidden = TRUE -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/simpletest/tests/common_test.module drupal-7.66/modules/simpletest/tests/common_test.module --- drupal-7.0/modules/simpletest/tests/common_test.module 2010-08-17 23:31:13.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/common_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: common_test.module,v 1.13 2010/08/17 21:31:13 dries Exp $ /** * @file @@ -94,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() { @@ -102,6 +113,14 @@ } /** + * Applies #printed to an element to help test #pre_render. + */ +function common_test_drupal_render_printing_pre_render($elements) { + $elements['#printed'] = TRUE; + return $elements; +} + +/** * Implements hook_TYPE_alter(). */ function common_test_drupal_alter_alter(&$data, &$arg2 = NULL, &$arg3 = NULL) { @@ -167,6 +186,34 @@ } /** + * Implements hook_TYPE_alter() on behalf of block module. + * + * This is for verifying that drupal_alter(array(TYPE1, TYPE2), ...) allows + * hook_module_implements_alter() to affect the order in which module + * implementations are executed. + */ +function block_drupal_alter_foo_alter(&$data, &$arg2 = NULL, &$arg3 = NULL) { + $data['foo'] .= ' block'; +} + +/** + * Implements hook_module_implements_alter(). + * + * @see block_drupal_alter_foo_alter() + */ +function common_test_module_implements_alter(&$implementations, $hook) { + // For drupal_alter(array('drupal_alter', 'drupal_alter_foo'), ...), make the + // block module implementations run after all the other modules. Note that + // when drupal_alter() is called with an array of types, the first type is + // considered primary and controls the module order. + if ($hook == 'drupal_alter_alter' && isset($implementations['block'])) { + $group = $implementations['block']; + unset($implementations['block']); + $implementations['block'] = $group; + } +} + +/** * Implements hook_theme(). */ function common_test_theme() { @@ -226,3 +273,16 @@ drupal_add_css('/' . drupal_get_path('module', 'node') . '/node-fake.css?arg1=value1&arg2=value2'); return ''; } + +/** + * Implements hook_cron(). + * + * System module should handle if a module does not catch an exception and keep + * cron going. + * + * @see common_test_cron_helper() + * + */ +function common_test_cron() { + throw new Exception(t('Uncaught exception')); +} diff -Naur drupal-7.0/modules/simpletest/tests/common_test.print.css drupal-7.66/modules/simpletest/tests/common_test.print.css --- drupal-7.0/modules/simpletest/tests/common_test.print.css 2010-10-03 07:11:16.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/common_test.print.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,3 +1,2 @@ -/* $Id: common_test.print.css,v 1.1 2010/10/03 05:11:16 webchick Exp $ */ /* This file is for testing CSS file inclusion, no contents are necessary. */ diff -Naur drupal-7.0/modules/simpletest/tests/common_test_cron_helper.info drupal-7.66/modules/simpletest/tests/common_test_cron_helper.info --- drupal-7.0/modules/simpletest/tests/common_test_cron_helper.info 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/common_test_cron_helper.info 2019-04-17 22:39:36.000000000 +0200 @@ -0,0 +1,11 @@ +name = "Common Test Cron Helper" +description = "Helper module for CronRunTestCase::testCronExceptions()." +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.0/modules/simpletest/tests/common_test_cron_helper.module drupal-7.66/modules/simpletest/tests/common_test_cron_helper.module --- drupal-7.0/modules/simpletest/tests/common_test_cron_helper.module 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/common_test_cron_helper.module 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,17 @@ +<?php +/** + * @file + * Helper module for the testCronExceptions in addition to common_test module. + */ + +/** + * Implements hook_cron(). + * + * common_test_cron() throws an exception, but the execution should reach this + * function as well. + * + * @see common_test_cron() + */ +function common_test_cron_helper_cron() { + variable_set('common_test_cron', 'success'); +} diff -Naur drupal-7.0/modules/simpletest/tests/common_test_info.txt drupal-7.66/modules/simpletest/tests/common_test_info.txt --- drupal-7.0/modules/simpletest/tests/common_test_info.txt 2010-01-30 08:59:25.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/common_test_info.txt 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: common_test_info.txt,v 1.2 2010/01/30 07:59:25 dries Exp $ ; Test parsing with a simple string. simple_string = A simple string diff -Naur drupal-7.0/modules/simpletest/tests/database_test.info drupal-7.66/modules/simpletest/tests/database_test.info --- drupal-7.0/modules/simpletest/tests/database_test.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/database_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: database_test.info,v 1.3 2010/12/20 19:59:43 webchick Exp $ name = "Database Test" description = "Support module for Database layer tests." core = 7.x @@ -6,8 +5,7 @@ version = VERSION hidden = TRUE -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/simpletest/tests/database_test.install drupal-7.66/modules/simpletest/tests/database_test.install --- drupal-7.0/modules/simpletest/tests/database_test.install 2010-07-28 12:52:12.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/database_test.install 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: database_test.install,v 1.13 2010/07/28 10:52:12 dries Exp $ /** * @file @@ -29,6 +28,7 @@ 'length' => 255, 'not null' => TRUE, 'default' => '', + 'binary' => TRUE, ), 'age' => array( 'description' => "The person's age", @@ -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.0/modules/simpletest/tests/database_test.module drupal-7.66/modules/simpletest/tests/database_test.module --- drupal-7.0/modules/simpletest/tests/database_test.module 2010-10-15 06:34:15.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/database_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: database_test.module,v 1.15 2010/10/15 04:34:15 webchick Exp $ /** * Implements hook_query_alter(). diff -Naur drupal-7.0/modules/simpletest/tests/database_test.test drupal-7.66/modules/simpletest/tests/database_test.test --- drupal-7.0/modules/simpletest/tests/database_test.test 2010-12-31 21:43:43.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/database_test.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: database_test.test,v 1.110 2010/12/31 20:43:43 webchick Exp $ /** * Dummy class for fetching into a class. @@ -24,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'); @@ -49,7 +49,7 @@ } foreach ($schema as $name => $data) { - $this->assertTrue(db_table_exists($name), t('Table @name created successfully.', array('@name' => $name))); + $this->assertTrue(db_table_exists($name), format_string('Table @name created successfully.', array('@name' => $name))); } } @@ -192,25 +192,25 @@ $db1 = Database::getConnection('default', 'default'); $db2 = Database::getConnection('slave', 'default'); - $this->assertNotNull($db1, t('default connection is a real connection object.')); - $this->assertNotNull($db2, t('slave connection is a real connection object.')); - $this->assertNotIdentical($db1, $db2, t('Each target refers to a different connection.')); + $this->assertNotNull($db1, 'default connection is a real connection object.'); + $this->assertNotNull($db2, 'slave connection is a real connection object.'); + $this->assertNotIdentical($db1, $db2, 'Each target refers to a different connection.'); // Try to open those targets another time, that should return the same objects. $db1b = Database::getConnection('default', 'default'); $db2b = Database::getConnection('slave', 'default'); - $this->assertIdentical($db1, $db1b, t('A second call to getConnection() returns the same object.')); - $this->assertIdentical($db2, $db2b, t('A second call to getConnection() returns the same object.')); + $this->assertIdentical($db1, $db1b, 'A second call to getConnection() returns the same object.'); + $this->assertIdentical($db2, $db2b, 'A second call to getConnection() returns the same object.'); // Try to open an unknown target. $unknown_target = $this->randomName(); $db3 = Database::getConnection($unknown_target, 'default'); - $this->assertNotNull($db3, t('Opening an unknown target returns a real connection object.')); - $this->assertIdentical($db1, $db3, t('An unknown target opens the default connection.')); + $this->assertNotNull($db3, 'Opening an unknown target returns a real connection object.'); + $this->assertIdentical($db1, $db3, 'An unknown target opens the default connection.'); // Try to open that unknown target another time, that should return the same object. $db3b = Database::getConnection($unknown_target, 'default'); - $this->assertIdentical($db3, $db3b, t('A second call to getConnection() returns the same object.')); + $this->assertIdentical($db3, $db3b, 'A second call to getConnection() returns the same object.'); } /** @@ -228,7 +228,7 @@ $db1 = Database::getConnection('default', 'default'); $db2 = Database::getConnection('slave', 'default'); - $this->assertIdentical($db1, $db2, t('Both targets refer to the same connection.')); + $this->assertIdentical($db1, $db2, 'Both targets refer to the same connection.'); } /** @@ -238,12 +238,12 @@ // 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'); // Opening a connection after closing it should yield an object different than the original. - $this->assertNotIdentical($db1, $db2, t('Opening the default connection after it is closed returns a new object.')); + $this->assertNotIdentical($db1, $db2, 'Opening the default connection after it is closed returns a new object.'); } /** @@ -258,8 +258,8 @@ // In the MySQL driver, the port can be different, so check individual // options. - $this->assertEqual($connection_info['default']['driver'], $connectionOptions['driver'], t('The default connection info driver matches the current connection options driver.')); - $this->assertEqual($connection_info['default']['database'], $connectionOptions['database'], t('The default connection info database matches the current connection options database.')); + $this->assertEqual($connection_info['default']['driver'], $connectionOptions['driver'], 'The default connection info driver matches the current connection options driver.'); + $this->assertEqual($connection_info['default']['database'], $connectionOptions['database'], 'The default connection info database matches the current connection options database.'); // Set up identical slave and confirm connection options are identical. Database::addConnectionInfo('default', 'slave', $connection_info['default']); @@ -268,7 +268,7 @@ // Get a fresh copy of the default connection options. $connectionOptions = $db->getConnectionOptions(); - $this->assertIdentical($connectionOptions, $connectionOptions2, t('The default and slave connection options are identical.')); + $this->assertIdentical($connectionOptions, $connectionOptions2, 'The default and slave connection options are identical.'); // Set up a new connection with different connection info. $test = $connection_info['default']; @@ -278,7 +278,46 @@ // Get a fresh copy of the default connection options. $connectionOptions = $db->getConnectionOptions(); - $this->assertNotEqual($connection_info['default']['database'], $connectionOptions['database'], t('The test connection info database does not match the current connection options database.')); + $this->assertNotEqual($connection_info['default']['database'], $connectionOptions['database'], 'The test connection info database does not match the current connection options database.'); + } +} + +/** + * Test cloning Select queries. + */ +class DatabaseSelectCloneTest extends DatabaseTestCase { + + public static function getInfo() { + return array( + 'name' => 'Select tests, cloning', + 'description' => 'Test cloning Select queries.', + 'group' => 'Database', + ); + } + + /** + * Test that subqueries as value within conditions are cloned properly. + */ + function testSelectConditionSubQueryCloning() { + $subquery = db_select('test', 't'); + $subquery->addField('t', 'id', 'id'); + $subquery->condition('age', 28, '<'); + + $query = db_select('test', 't'); + $query->addField('t', 'name', 'name'); + $query->condition('id', $subquery, 'IN'); + + $clone = clone $query; + // Cloned query should not be altered by the following modification + // happening on original query. + $subquery->condition('age', 25, '>'); + + $clone_result = $clone->countQuery()->execute()->fetchField(); + $query_result = $query->countQuery()->execute()->fetchField(); + + // Make sure the cloned query has not been modified + $this->assertEqual(3, $clone_result, 'The cloned query returns the expected number of rows'); + $this->assertEqual(2, $query_result, 'The query returns the expected number of rows'); } } @@ -303,14 +342,14 @@ function testQueryFetchDefault() { $records = array(); $result = db_query('SELECT name FROM {test} WHERE age = :age', array(':age' => 25)); - $this->assertTrue($result instanceof DatabaseStatementInterface, t('Result set is a Drupal statement object.')); + $this->assertTrue($result instanceof DatabaseStatementInterface, 'Result set is a Drupal statement object.'); foreach ($result as $record) { $records[] = $record; - $this->assertTrue(is_object($record), t('Record is an object.')); - $this->assertIdentical($record->name, 'John', t('25 year old is John.')); + $this->assertTrue(is_object($record), 'Record is an object.'); + $this->assertIdentical($record->name, 'John', '25 year old is John.'); } - $this->assertIdentical(count($records), 1, t('There is only one record.')); + $this->assertIdentical(count($records), 1, 'There is only one record.'); } /** @@ -321,11 +360,11 @@ $result = db_query('SELECT name FROM {test} WHERE age = :age', array(':age' => 25), array('fetch' => PDO::FETCH_OBJ)); foreach ($result as $record) { $records[] = $record; - $this->assertTrue(is_object($record), t('Record is an object.')); - $this->assertIdentical($record->name, 'John', t('25 year old is John.')); + $this->assertTrue(is_object($record), 'Record is an object.'); + $this->assertIdentical($record->name, 'John', '25 year old is John.'); } - $this->assertIdentical(count($records), 1, t('There is only one record.')); + $this->assertIdentical(count($records), 1, 'There is only one record.'); } /** @@ -336,12 +375,12 @@ $result = db_query('SELECT name FROM {test} WHERE age = :age', array(':age' => 25), array('fetch' => PDO::FETCH_ASSOC)); foreach ($result as $record) { $records[] = $record; - if ($this->assertTrue(is_array($record), t('Record is an array.'))) { - $this->assertIdentical($record['name'], 'John', t('Record can be accessed associatively.')); + if ($this->assertTrue(is_array($record), 'Record is an array.')) { + $this->assertIdentical($record['name'], 'John', 'Record can be accessed associatively.'); } } - $this->assertIdentical(count($records), 1, t('There is only one record.')); + $this->assertIdentical(count($records), 1, 'There is only one record.'); } /** @@ -354,12 +393,12 @@ $result = db_query('SELECT name FROM {test} WHERE age = :age', array(':age' => 25), array('fetch' => 'FakeRecord')); foreach ($result as $record) { $records[] = $record; - if ($this->assertTrue($record instanceof FakeRecord, t('Record is an object of class FakeRecord.'))) { - $this->assertIdentical($record->name, 'John', t('25 year old is John.')); + if ($this->assertTrue($record instanceof FakeRecord, 'Record is an object of class FakeRecord.')) { + $this->assertIdentical($record->name, 'John', '25 year old is John.'); } } - $this->assertIdentical(count($records), 1, t('There is only one record.')); + $this->assertIdentical(count($records), 1, 'There is only one record.'); } } @@ -388,8 +427,8 @@ $result = db_query('SELECT name FROM {test} WHERE age = :age', array(':age' => 25), array('fetch' => PDO::FETCH_NUM)); foreach ($result as $record) { $records[] = $record; - if ($this->assertTrue(is_array($record), t('Record is an array.'))) { - $this->assertIdentical($record[0], 'John', t('Record can be accessed numerically.')); + if ($this->assertTrue(is_array($record), 'Record is an array.')) { + $this->assertIdentical($record[0], 'John', 'Record can be accessed numerically.'); } } @@ -404,13 +443,13 @@ $result = db_query('SELECT name FROM {test} WHERE age = :age', array(':age' => 25), array('fetch' => PDO::FETCH_BOTH)); foreach ($result as $record) { $records[] = $record; - if ($this->assertTrue(is_array($record), t('Record is an array.'))) { - $this->assertIdentical($record[0], 'John', t('Record can be accessed numerically.')); - $this->assertIdentical($record['name'], 'John', t('Record can be accessed associatively.')); + if ($this->assertTrue(is_array($record), 'Record is an array.')) { + $this->assertIdentical($record[0], 'John', 'Record can be accessed numerically.'); + $this->assertIdentical($record['name'], 'John', 'Record can be accessed associatively.'); } } - $this->assertIdentical(count($records), 1, t('There is only one record.')); + $this->assertIdentical(count($records), 1, 'There is only one record.'); } /** @@ -420,12 +459,12 @@ $records = array(); $result = db_query('SELECT name FROM {test} WHERE age > :age', array(':age' => 25)); $column = $result->fetchCol(); - $this->assertIdentical(count($column), 3, t('fetchCol() returns the right number of records.')); + $this->assertIdentical(count($column), 3, 'fetchCol() returns the right number of records.'); $result = db_query('SELECT name FROM {test} WHERE age > :age', array(':age' => 25)); $i = 0; foreach ($result as $record) { - $this->assertIdentical($record->name, $column[$i++], t('Column matches direct accesss.')); + $this->assertIdentical($record->name, $column[$i++], 'Column matches direct accesss.'); } } } @@ -457,9 +496,9 @@ $query->execute(); $num_records_after = db_query('SELECT COUNT(*) FROM {test}')->fetchField(); - $this->assertIdentical($num_records_before + 1, (int) $num_records_after, t('Record inserts correctly.')); + $this->assertIdentical($num_records_before + 1, (int) $num_records_after, 'Record inserts correctly.'); $saved_age = db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'Yoko'))->fetchField(); - $this->assertIdentical($saved_age, '29', t('Can retrieve after inserting.')); + $this->assertIdentical($saved_age, '29', 'Can retrieve after inserting.'); } /** @@ -486,13 +525,13 @@ $query->execute(); $num_records_after = (int) db_query('SELECT COUNT(*) FROM {test}')->fetchField(); - $this->assertIdentical($num_records_before + 3, $num_records_after, t('Record inserts correctly.')); + $this->assertIdentical($num_records_before + 3, $num_records_after, 'Record inserts correctly.'); $saved_age = db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'Larry'))->fetchField(); - $this->assertIdentical($saved_age, '30', t('Can retrieve after inserting.')); + $this->assertIdentical($saved_age, '30', 'Can retrieve after inserting.'); $saved_age = db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'Curly'))->fetchField(); - $this->assertIdentical($saved_age, '31', t('Can retrieve after inserting.')); + $this->assertIdentical($saved_age, '31', 'Can retrieve after inserting.'); $saved_age = db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'Moe'))->fetchField(); - $this->assertIdentical($saved_age, '32', t('Can retrieve after inserting.')); + $this->assertIdentical($saved_age, '32', 'Can retrieve after inserting.'); } /** @@ -521,13 +560,13 @@ $query->execute(); $num_records_after = db_query('SELECT COUNT(*) FROM {test}')->fetchField(); - $this->assertIdentical((int) $num_records_before + 3, (int) $num_records_after, t('Record inserts correctly.')); + $this->assertIdentical((int) $num_records_before + 3, (int) $num_records_after, 'Record inserts correctly.'); $saved_age = db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'Larry'))->fetchField(); - $this->assertIdentical($saved_age, '30', t('Can retrieve after inserting.')); + $this->assertIdentical($saved_age, '30', 'Can retrieve after inserting.'); $saved_age = db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'Curly'))->fetchField(); - $this->assertIdentical($saved_age, '31', t('Can retrieve after inserting.')); + $this->assertIdentical($saved_age, '31', 'Can retrieve after inserting.'); $saved_age = db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'Moe'))->fetchField(); - $this->assertIdentical($saved_age, '32', t('Can retrieve after inserting.')); + $this->assertIdentical($saved_age, '32', 'Can retrieve after inserting.'); } /** @@ -543,11 +582,11 @@ ->values(array('Moe', '32')) ->execute(); $saved_age = db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'Larry'))->fetchField(); - $this->assertIdentical($saved_age, '30', t('Can retrieve after inserting.')); + $this->assertIdentical($saved_age, '30', 'Can retrieve after inserting.'); $saved_age = db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'Curly'))->fetchField(); - $this->assertIdentical($saved_age, '31', t('Can retrieve after inserting.')); + $this->assertIdentical($saved_age, '31', 'Can retrieve after inserting.'); $saved_age = db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'Moe'))->fetchField(); - $this->assertIdentical($saved_age, '32', t('Can retrieve after inserting.')); + $this->assertIdentical($saved_age, '32', 'Can retrieve after inserting.'); } /** @@ -561,13 +600,13 @@ )) ->execute(); - $this->assertIdentical($id, '5', t('Auto-increment ID returned successfully.')); + $this->assertIdentical($id, '5', 'Auto-increment ID returned successfully.'); } /** - * 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 @@ -587,7 +626,28 @@ ->execute(); $saved_age = db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'Meredith'))->fetchField(); - $this->assertIdentical($saved_age, '30', t('Can retrieve after inserting.')); + $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.'); } } @@ -609,12 +669,12 @@ */ function testInsertOneBlob() { $data = "This is\000a test."; - $this->assertTrue(strlen($data) === 15, t('Test data contains a NULL.')); + $this->assertTrue(strlen($data) === 15, 'Test data contains a NULL.'); $id = db_insert('test_one_blob') ->fields(array('blob1' => $data)) ->execute(); $r = db_query('SELECT * FROM {test_one_blob} WHERE id = :id', array(':id' => $id))->fetchAssoc(); - $this->assertTrue($r['blob1'] === $data, t('Can insert a blob: id @id, @data.', array('@id' => $id, '@data' => serialize($r)))); + $this->assertTrue($r['blob1'] === $data, format_string('Can insert a blob: id @id, @data.', array('@id' => $id, '@data' => serialize($r)))); } /** @@ -628,7 +688,7 @@ )) ->execute(); $r = db_query('SELECT * FROM {test_two_blobs} WHERE id = :id', array(':id' => $id))->fetchAssoc(); - $this->assertTrue($r['blob1'] === 'This is' && $r['blob2'] === 'a test', t('Can insert multiple blobs per row.')); + $this->assertTrue($r['blob1'] === 'This is' && $r['blob2'] === 'a test', 'Can insert multiple blobs per row.'); } } @@ -655,7 +715,7 @@ $schema = drupal_get_schema('test'); $job = db_query('SELECT job FROM {test} WHERE id = :id', array(':id' => $id))->fetchField(); - $this->assertEqual($job, $schema['fields']['job']['default'], t('Default field value is set.')); + $this->assertEqual($job, $schema['fields']['job']['default'], 'Default field value is set.'); } /** @@ -667,13 +727,13 @@ try { $result = db_insert('test')->execute(); // This is only executed if no exception has been thrown. - $this->fail(t('Expected exception NoFieldsException has not been thrown.')); + $this->fail('Expected exception NoFieldsException has not been thrown.'); } catch (NoFieldsException $e) { - $this->pass(t('Expected exception NoFieldsException has been thrown.')); + $this->pass('Expected exception NoFieldsException has been thrown.'); } $num_records_after = (int) db_query('SELECT COUNT(*) FROM {test}')->fetchField(); - $this->assertIdentical($num_records_before, $num_records_after, t('Do nothing as no fields are specified.')); + $this->assertIdentical($num_records_before, $num_records_after, 'Do nothing as no fields are specified.'); } /** @@ -688,7 +748,7 @@ $schema = drupal_get_schema('test'); $job = db_query('SELECT job FROM {test} WHERE id = :id', array(':id' => $id))->fetchField(); - $this->assertEqual($job, $schema['fields']['job']['default'], t('Default field value is set.')); + $this->assertEqual($job, $schema['fields']['job']['default'], 'Default field value is set.'); } } @@ -713,10 +773,25 @@ ->fields(array('name' => 'Tiffany')) ->condition('id', 1) ->execute(); - $this->assertIdentical($num_updated, 1, t('Updated 1 record.')); + $this->assertIdentical($num_updated, 1, 'Updated 1 record.'); $saved_name = db_query('SELECT name FROM {test} WHERE id = :id', array(':id' => 1))->fetchField(); - $this->assertIdentical($saved_name, 'Tiffany', t('Updated name successfully.')); + $this->assertIdentical($saved_name, 'Tiffany', 'Updated name successfully.'); + } + + /** + * Confirm updating to NULL. + */ + function testSimpleNullUpdate() { + $this->ensureSampleDataNull(); + $num_updated = db_update('test_null') + ->fields(array('age' => NULL)) + ->condition('name', 'Kermit') + ->execute(); + $this->assertIdentical($num_updated, 1, 'Updated 1 record.'); + + $saved_age = db_query('SELECT age FROM {test_null} WHERE name = :name', array(':name' => 'Kermit'))->fetchField(); + $this->assertNull($saved_age, 'Updated name successfully.'); } /** @@ -727,10 +802,10 @@ ->fields(array('job' => 'Musician')) ->condition('job', 'Singer') ->execute(); - $this->assertIdentical($num_updated, 2, t('Updated 2 records.')); + $this->assertIdentical($num_updated, 2, 'Updated 2 records.'); $num_matches = db_query('SELECT COUNT(*) FROM {test} WHERE job = :job', array(':job' => 'Musician'))->fetchField(); - $this->assertIdentical($num_matches, '2', t('Updated fields successfully.')); + $this->assertIdentical($num_matches, '2', 'Updated fields successfully.'); } /** @@ -741,10 +816,10 @@ ->fields(array('job' => 'Musician')) ->condition('age', 26, '>') ->execute(); - $this->assertIdentical($num_updated, 2, t('Updated 2 records.')); + $this->assertIdentical($num_updated, 2, 'Updated 2 records.'); $num_matches = db_query('SELECT COUNT(*) FROM {test} WHERE job = :job', array(':job' => 'Musician'))->fetchField(); - $this->assertIdentical($num_matches, '2', t('Updated fields successfully.')); + $this->assertIdentical($num_matches, '2', 'Updated fields successfully.'); } /** @@ -755,10 +830,10 @@ ->fields(array('job' => 'Musician')) ->where('age > :age', array(':age' => 26)) ->execute(); - $this->assertIdentical($num_updated, 2, t('Updated 2 records.')); + $this->assertIdentical($num_updated, 2, 'Updated 2 records.'); $num_matches = db_query('SELECT COUNT(*) FROM {test} WHERE job = :job', array(':job' => 'Musician'))->fetchField(); - $this->assertIdentical($num_matches, '2', t('Updated fields successfully.')); + $this->assertIdentical($num_matches, '2', 'Updated fields successfully.'); } /** @@ -770,10 +845,10 @@ ->where('age > :age', array(':age' => 26)) ->condition('name', 'Ringo'); $num_updated = $update->execute(); - $this->assertIdentical($num_updated, 1, t('Updated 1 record.')); + $this->assertIdentical($num_updated, 1, 'Updated 1 record.'); $num_matches = db_query('SELECT COUNT(*) FROM {test} WHERE job = :job', array(':job' => 'Musician'))->fetchField(); - $this->assertIdentical($num_matches, '1', t('Updated fields successfully.')); + $this->assertIdentical($num_matches, '1', 'Updated fields successfully.'); } /** @@ -793,7 +868,21 @@ $num_rows = db_update('test') ->expression('age', 'age * age') ->execute(); - $this->assertIdentical($num_rows, 3, t('Number of affected rows are returned.')); + $this->assertIdentical($num_rows, 3, 'Number of affected rows are returned.'); + } + + /** + * Confirm that we can update the primary key of a record successfully. + */ + function testPrimaryKeyUpdate() { + $num_updated = db_update('test') + ->fields(array('id' => 42, 'name' => 'John')) + ->condition('id', 1) + ->execute(); + $this->assertIdentical($num_updated, 1, 'Updated 1 record.'); + + $saved_name= db_query('SELECT name FROM {test} WHERE id = :id', array(':id' => 42))->fetchField(); + $this->assertIdentical($saved_name, 'John', 'Updated primary key successfully.'); } } @@ -821,10 +910,10 @@ ->condition('name', 'Paul') ); $num_updated = $update->execute(); - $this->assertIdentical($num_updated, 2, t('Updated 2 records.')); + $this->assertIdentical($num_updated, 2, 'Updated 2 records.'); $num_matches = db_query('SELECT COUNT(*) FROM {test} WHERE job = :job', array(':job' => 'Musician'))->fetchField(); - $this->assertIdentical($num_matches, '2', t('Updated fields successfully.')); + $this->assertIdentical($num_matches, '2', 'Updated fields successfully.'); } /** @@ -835,10 +924,10 @@ ->fields(array('job' => 'Musician')) ->condition('name', array('John', 'Paul'), 'IN') ->execute(); - $this->assertIdentical($num_updated, 2, t('Updated 2 records.')); + $this->assertIdentical($num_updated, 2, 'Updated 2 records.'); $num_matches = db_query('SELECT COUNT(*) FROM {test} WHERE job = :job', array(':job' => 'Musician'))->fetchField(); - $this->assertIdentical($num_matches, '2', t('Updated fields successfully.')); + $this->assertIdentical($num_matches, '2', 'Updated fields successfully.'); } /** @@ -851,10 +940,10 @@ ->fields(array('job' => 'Musician')) ->condition('name', array('John', 'Paul', 'George'), 'NoT IN') ->execute(); - $this->assertIdentical($num_updated, 1, t('Updated 1 record.')); + $this->assertIdentical($num_updated, 1, 'Updated 1 record.'); $num_matches = db_query('SELECT COUNT(*) FROM {test} WHERE job = :job', array(':job' => 'Musician'))->fetchField(); - $this->assertIdentical($num_matches, '1', t('Updated fields successfully.')); + $this->assertIdentical($num_matches, '1', 'Updated fields successfully.'); } /** @@ -865,10 +954,10 @@ ->fields(array('job' => 'Musician')) ->condition('age', array(25, 26), 'BETWEEN') ->execute(); - $this->assertIdentical($num_updated, 2, t('Updated 2 records.')); + $this->assertIdentical($num_updated, 2, 'Updated 2 records.'); $num_matches = db_query('SELECT COUNT(*) FROM {test} WHERE job = :job', array(':job' => 'Musician'))->fetchField(); - $this->assertIdentical($num_matches, '2', t('Updated fields successfully.')); + $this->assertIdentical($num_matches, '2', 'Updated fields successfully.'); } /** @@ -879,10 +968,10 @@ ->fields(array('job' => 'Musician')) ->condition('name', '%ge%', 'LIKE') ->execute(); - $this->assertIdentical($num_updated, 1, t('Updated 1 record.')); + $this->assertIdentical($num_updated, 1, 'Updated 1 record.'); $num_matches = db_query('SELECT COUNT(*) FROM {test} WHERE job = :job', array(':job' => 'Musician'))->fetchField(); - $this->assertIdentical($num_matches, '1', t('Updated fields successfully.')); + $this->assertIdentical($num_matches, '1', 'Updated fields successfully.'); } /** @@ -896,15 +985,15 @@ ->fields(array('job' => 'Musician')) ->expression('age', 'age + :age', array(':age' => 4)) ->execute(); - $this->assertIdentical($num_updated, 1, t('Updated 1 record.')); + $this->assertIdentical($num_updated, 1, 'Updated 1 record.'); $num_matches = db_query('SELECT COUNT(*) FROM {test} WHERE job = :job', array(':job' => 'Musician'))->fetchField(); - $this->assertIdentical($num_matches, '1', t('Updated fields successfully.')); + $this->assertIdentical($num_matches, '1', 'Updated fields successfully.'); $person = db_query('SELECT * FROM {test} WHERE name = :name', array(':name' => 'Ringo'))->fetch(); - $this->assertEqual($person->name, 'Ringo', t('Name set correctly.')); - $this->assertEqual($person->age, $before_age + 4, t('Age set correctly.')); - $this->assertEqual($person->job, 'Musician', t('Job set correctly.')); + $this->assertEqual($person->name, 'Ringo', 'Name set correctly.'); + $this->assertEqual($person->age, $before_age + 4, 'Age set correctly.'); + $this->assertEqual($person->job, 'Musician', 'Job set correctly.'); $GLOBALS['larry_test'] = 0; } @@ -917,10 +1006,10 @@ ->condition('name', 'Ringo') ->expression('age', 'age + :age', array(':age' => 4)) ->execute(); - $this->assertIdentical($num_updated, 1, t('Updated 1 record.')); + $this->assertIdentical($num_updated, 1, 'Updated 1 record.'); $after_age = db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'Ringo'))->fetchField(); - $this->assertEqual($before_age + 4, $after_age, t('Age updated correctly')); + $this->assertEqual($before_age + 4, $after_age, 'Age updated correctly'); } } @@ -942,7 +1031,7 @@ */ function testUpdateOneBlob() { $data = "This is\000a test."; - $this->assertTrue(strlen($data) === 15, t('Test data contains a NULL.')); + $this->assertTrue(strlen($data) === 15, 'Test data contains a NULL.'); $id = db_insert('test_one_blob') ->fields(array('blob1' => $data)) ->execute(); @@ -954,7 +1043,7 @@ ->execute(); $r = db_query('SELECT * FROM {test_one_blob} WHERE id = :id', array(':id' => $id))->fetchAssoc(); - $this->assertTrue($r['blob1'] === $data, t('Can update a blob: id @id, @data.', array('@id' => $id, '@data' => serialize($r)))); + $this->assertTrue($r['blob1'] === $data, format_string('Can update a blob: id @id, @data.', array('@id' => $id, '@data' => serialize($r)))); } /** @@ -974,7 +1063,7 @@ ->execute(); $r = db_query('SELECT * FROM {test_two_blobs} WHERE id = :id', array(':id' => $id))->fetchAssoc(); - $this->assertTrue($r['blob1'] === 'and so' && $r['blob2'] === 'is this', t('Can update multiple blobs per row.')); + $this->assertTrue($r['blob1'] === 'and so' && $r['blob2'] === 'is this', 'Can update multiple blobs per row.'); } } @@ -1014,10 +1103,10 @@ ->condition('pid', $subquery, 'IN'); $num_deleted = $delete->execute(); - $this->assertEqual($num_deleted, 1, t("Deleted 1 record.")); + $this->assertEqual($num_deleted, 1, "Deleted 1 record."); $num_records_after = db_query('SELECT COUNT(*) FROM {test_task}')->fetchField(); - $this->assertEqual($num_records_before, $num_records_after + $num_deleted, t('Deletion adds up.')); + $this->assertEqual($num_records_before, $num_records_after + $num_deleted, 'Deletion adds up.'); } /** @@ -1029,10 +1118,10 @@ $num_deleted = db_delete('test') ->condition('id', 1) ->execute(); - $this->assertIdentical($num_deleted, 1, t('Deleted 1 record.')); + $this->assertIdentical($num_deleted, 1, 'Deleted 1 record.'); $num_records_after = db_query('SELECT COUNT(*) FROM {test}')->fetchField(); - $this->assertEqual($num_records_before, $num_records_after + $num_deleted, t('Deletion adds up.')); + $this->assertEqual($num_records_before, $num_records_after + $num_deleted, 'Deletion adds up.'); } /** @@ -1044,7 +1133,7 @@ db_truncate('test')->execute(); $num_records_after = db_query("SELECT COUNT(*) FROM {test}")->fetchField(); - $this->assertEqual(0, $num_records_after, t('Truncate really deletes everything.')); + $this->assertEqual(0, $num_records_after, 'Truncate really deletes everything.'); } } @@ -1075,15 +1164,15 @@ )) ->execute(); - $this->assertEqual($result, MergeQuery::STATUS_INSERT, t('Insert status returned.')); + $this->assertEqual($result, MergeQuery::STATUS_INSERT, 'Insert status returned.'); $num_records_after = db_query('SELECT COUNT(*) FROM {test_people}')->fetchField(); - $this->assertEqual($num_records_before + 1, $num_records_after, t('Merge inserted properly.')); + $this->assertEqual($num_records_before + 1, $num_records_after, 'Merge inserted properly.'); $person = db_query('SELECT * FROM {test_people} WHERE job = :job', array(':job' => 'Presenter'))->fetch(); - $this->assertEqual($person->name, 'Tiffany', t('Name set correctly.')); - $this->assertEqual($person->age, 31, t('Age set correctly.')); - $this->assertEqual($person->job, 'Presenter', t('Job set correctly.')); + $this->assertEqual($person->name, 'Tiffany', 'Name set correctly.'); + $this->assertEqual($person->age, 31, 'Age set correctly.'); + $this->assertEqual($person->job, 'Presenter', 'Job set correctly.'); } /** @@ -1100,15 +1189,15 @@ )) ->execute(); - $this->assertEqual($result, MergeQuery::STATUS_UPDATE, t('Update status returned.')); + $this->assertEqual($result, MergeQuery::STATUS_UPDATE, 'Update status returned.'); $num_records_after = db_query('SELECT COUNT(*) FROM {test_people}')->fetchField(); - $this->assertEqual($num_records_before, $num_records_after, t('Merge updated properly.')); + $this->assertEqual($num_records_before, $num_records_after, 'Merge updated properly.'); $person = db_query('SELECT * FROM {test_people} WHERE job = :job', array(':job' => 'Speaker'))->fetch(); - $this->assertEqual($person->name, 'Tiffany', t('Name set correctly.')); - $this->assertEqual($person->age, 31, t('Age set correctly.')); - $this->assertEqual($person->job, 'Speaker', t('Job set correctly.')); + $this->assertEqual($person->name, 'Tiffany', 'Name set correctly.'); + $this->assertEqual($person->age, 31, 'Age set correctly.'); + $this->assertEqual($person->job, 'Speaker', 'Job set correctly.'); } /** @@ -1124,12 +1213,12 @@ ->execute(); $num_records_after = db_query('SELECT COUNT(*) FROM {test_people}')->fetchField(); - $this->assertEqual($num_records_before, $num_records_after, t('Merge updated properly.')); + $this->assertEqual($num_records_before, $num_records_after, 'Merge updated properly.'); $person = db_query('SELECT * FROM {test_people} WHERE job = :job', array(':job' => 'Speaker'))->fetch(); - $this->assertEqual($person->name, 'Tiffany', t('Name set correctly.')); - $this->assertEqual($person->age, 30, t('Age skipped correctly.')); - $this->assertEqual($person->job, 'Speaker', t('Job set correctly.')); + $this->assertEqual($person->name, 'Tiffany', 'Name set correctly.'); + $this->assertEqual($person->age, 30, 'Age skipped correctly.'); + $this->assertEqual($person->job, 'Speaker', 'Job set correctly.'); } /** @@ -1150,12 +1239,12 @@ ->execute(); $num_records_after = db_query('SELECT COUNT(*) FROM {test_people}')->fetchField(); - $this->assertEqual($num_records_before, $num_records_after, t('Merge updated properly.')); + $this->assertEqual($num_records_before, $num_records_after, 'Merge updated properly.'); $person = db_query('SELECT * FROM {test_people} WHERE job = :job', array(':job' => 'Speaker'))->fetch(); - $this->assertEqual($person->name, 'Joe', t('Name set correctly.')); - $this->assertEqual($person->age, 30, t('Age skipped correctly.')); - $this->assertEqual($person->job, 'Speaker', t('Job set correctly.')); + $this->assertEqual($person->name, 'Joe', 'Name set correctly.'); + $this->assertEqual($person->age, 30, 'Age skipped correctly.'); + $this->assertEqual($person->job, 'Speaker', 'Job set correctly.'); } /** @@ -1179,12 +1268,12 @@ ->execute(); $num_records_after = db_query('SELECT COUNT(*) FROM {test_people}')->fetchField(); - $this->assertEqual($num_records_before, $num_records_after, t('Merge updated properly.')); + $this->assertEqual($num_records_before, $num_records_after, 'Merge updated properly.'); $person = db_query('SELECT * FROM {test_people} WHERE job = :job', array(':job' => 'Speaker'))->fetch(); - $this->assertEqual($person->name, 'Tiffany', t('Name set correctly.')); - $this->assertEqual($person->age, $age_before + 4, t('Age updated correctly.')); - $this->assertEqual($person->job, 'Speaker', t('Job set correctly.')); + $this->assertEqual($person->name, 'Tiffany', 'Name set correctly.'); + $this->assertEqual($person->age, $age_before + 4, 'Age updated correctly.'); + $this->assertEqual($person->job, 'Speaker', 'Job set correctly.'); } /** @@ -1198,12 +1287,12 @@ ->execute(); $num_records_after = db_query('SELECT COUNT(*) FROM {test_people}')->fetchField(); - $this->assertEqual($num_records_before + 1, $num_records_after, t('Merge inserted properly.')); + $this->assertEqual($num_records_before + 1, $num_records_after, 'Merge inserted properly.'); $person = db_query('SELECT * FROM {test_people} WHERE job = :job', array(':job' => 'Presenter'))->fetch(); - $this->assertEqual($person->name, '', t('Name set correctly.')); - $this->assertEqual($person->age, 0, t('Age set correctly.')); - $this->assertEqual($person->job, 'Presenter', t('Job set correctly.')); + $this->assertEqual($person->name, '', 'Name set correctly.'); + $this->assertEqual($person->age, 0, 'Age set correctly.'); + $this->assertEqual($person->job, 'Presenter', 'Job set correctly.'); } /** @@ -1217,12 +1306,12 @@ ->execute(); $num_records_after = db_query('SELECT COUNT(*) FROM {test_people}')->fetchField(); - $this->assertEqual($num_records_before, $num_records_after, t('Merge skipped properly.')); + $this->assertEqual($num_records_before, $num_records_after, 'Merge skipped properly.'); $person = db_query('SELECT * FROM {test_people} WHERE job = :job', array(':job' => 'Speaker'))->fetch(); - $this->assertEqual($person->name, 'Meredith', t('Name skipped correctly.')); - $this->assertEqual($person->age, 30, t('Age skipped correctly.')); - $this->assertEqual($person->job, 'Speaker', t('Job skipped correctly.')); + $this->assertEqual($person->name, 'Meredith', 'Name skipped correctly.'); + $this->assertEqual($person->age, 30, 'Age skipped correctly.'); + $this->assertEqual($person->job, 'Speaker', 'Job skipped correctly.'); db_merge('test_people') ->key(array('job' => 'Speaker')) @@ -1230,12 +1319,12 @@ ->execute(); $num_records_after = db_query('SELECT COUNT(*) FROM {test_people}')->fetchField(); - $this->assertEqual($num_records_before, $num_records_after, t('Merge skipped properly.')); + $this->assertEqual($num_records_before, $num_records_after, 'Merge skipped properly.'); $person = db_query('SELECT * FROM {test_people} WHERE job = :job', array(':job' => 'Speaker'))->fetch(); - $this->assertEqual($person->name, 'Meredith', t('Name skipped correctly.')); - $this->assertEqual($person->age, 30, t('Age skipped correctly.')); - $this->assertEqual($person->job, 'Speaker', t('Job skipped correctly.')); + $this->assertEqual($person->name, 'Meredith', 'Name skipped correctly.'); + $this->assertEqual($person->age, 30, 'Age skipped correctly.'); + $this->assertEqual($person->job, 'Speaker', 'Job skipped correctly.'); } /** @@ -1252,10 +1341,10 @@ ->execute(); } catch (InvalidMergeQueryException $e) { - $this->pass(t('InvalidMergeQueryException thrown for invalid query.')); + $this->pass('InvalidMergeQueryException thrown for invalid query.'); return; } - $this->fail(t('No InvalidMergeQueryException thrown')); + $this->fail('No InvalidMergeQueryException thrown'); } } @@ -1286,7 +1375,7 @@ $num_records++; } - $this->assertEqual($num_records, 4, t('Returned the correct number of rows.')); + $this->assertEqual($num_records, 4, 'Returned the correct number of rows.'); } /** @@ -1306,8 +1395,66 @@ $query = (string)$query; $expected = "/* Testing query comments */ SELECT test.name AS name, test.age AS age\nFROM \n{test} test"; - $this->assertEqual($num_records, 4, t('Returned the correct number of rows.')); - $this->assertEqual($query, $expected, t('The flattened query contains the comment string.')); + $this->assertEqual($num_records, 4, 'Returned the correct number of rows.'); + $this->assertEqual($query, $expected, 'The flattened query contains the comment string.'); + } + + /** + * Test query COMMENT system against vulnerabilities. + */ + function testVulnerableComment() { + $query = db_select('test')->comment('Testing query comments */ SELECT nid FROM {node}; --'); + $name_field = $query->addField('test', 'name'); + $age_field = $query->addField('test', 'age', 'age'); + $result = $query->execute(); + + $num_records = 0; + foreach ($result as $record) { + $num_records++; + } + + $query = (string)$query; + $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; --'), + ), + ); } /** @@ -1321,13 +1468,13 @@ $result = $query->execute(); // Check that the aliases are being created the way we want. - $this->assertEqual($name_field, 'name', t('Name field alias is correct.')); - $this->assertEqual($age_field, 'age', t('Age field alias is correct.')); + $this->assertEqual($name_field, 'name', 'Name field alias is correct.'); + $this->assertEqual($age_field, 'age', 'Age field alias is correct.'); // Ensure that we got the right record. $record = $result->fetch(); - $this->assertEqual($record->$name_field, 'George', t('Fetched name is correct.')); - $this->assertEqual($record->$age_field, 27, t('Fetched age is correct.')); + $this->assertEqual($record->$name_field, 'George', 'Fetched name is correct.'); + $this->assertEqual($record->$age_field, 27, 'Fetched age is correct.'); } /** @@ -1341,13 +1488,13 @@ $result = $query->execute(); // Check that the aliases are being created the way we want. - $this->assertEqual($name_field, 'name', t('Name field alias is correct.')); - $this->assertEqual($age_field, 'double_age', t('Age field alias is correct.')); + $this->assertEqual($name_field, 'name', 'Name field alias is correct.'); + $this->assertEqual($age_field, 'double_age', 'Age field alias is correct.'); // Ensure that we got the right record. $record = $result->fetch(); - $this->assertEqual($record->$name_field, 'George', t('Fetched name is correct.')); - $this->assertEqual($record->$age_field, 27*2, t('Fetched age expression is correct.')); + $this->assertEqual($record->$name_field, 'George', 'Fetched name is correct.'); + $this->assertEqual($record->$age_field, 27*2, 'Fetched age expression is correct.'); } /** @@ -1362,14 +1509,14 @@ $result = $query->execute(); // Check that the aliases are being created the way we want. - $this->assertEqual($age_double_field, 'expression', t('Double age field alias is correct.')); - $this->assertEqual($age_triple_field, 'expression_2', t('Triple age field alias is correct.')); + $this->assertEqual($age_double_field, 'expression', 'Double age field alias is correct.'); + $this->assertEqual($age_triple_field, 'expression_2', 'Triple age field alias is correct.'); // Ensure that we got the right record. $record = $result->fetch(); - $this->assertEqual($record->$name_field, 'George', t('Fetched name is correct.')); - $this->assertEqual($record->$age_double_field, 27*2, t('Fetched double age expression is correct.')); - $this->assertEqual($record->$age_triple_field, 27*3, t('Fetched triple age expression is correct.')); + $this->assertEqual($record->$name_field, 'George', 'Fetched name is correct.'); + $this->assertEqual($record->$age_double_field, 27*2, 'Fetched double age expression is correct.'); + $this->assertEqual($record->$age_triple_field, 27*3, 'Fetched triple age expression is correct.'); } /** @@ -1382,17 +1529,17 @@ ->execute()->fetchObject(); // Check that all fields we asked for are present. - $this->assertNotNull($record->id, t('ID field is present.')); - $this->assertNotNull($record->name, t('Name field is present.')); - $this->assertNotNull($record->age, t('Age field is present.')); - $this->assertNotNull($record->job, t('Job field is present.')); + $this->assertNotNull($record->id, 'ID field is present.'); + $this->assertNotNull($record->name, 'Name field is present.'); + $this->assertNotNull($record->age, 'Age field is present.'); + $this->assertNotNull($record->job, 'Job field is present.'); // Ensure that we got the right record. // Check that all fields we asked for are present. - $this->assertEqual($record->id, 2, t('ID field has the correct value.')); - $this->assertEqual($record->name, 'George', t('Name field has the correct value.')); - $this->assertEqual($record->age, 27, t('Age field has the correct value.')); - $this->assertEqual($record->job, 'Singer', t('Job field has the correct value.')); + $this->assertEqual($record->id, 2, 'ID field has the correct value.'); + $this->assertEqual($record->name, 'George', 'Name field has the correct value.'); + $this->assertEqual($record->age, 27, 'Age field has the correct value.'); + $this->assertEqual($record->job, 'Singer', 'Job field has the correct value.'); } /** @@ -1405,17 +1552,17 @@ ->execute()->fetchObject(); // Check that all fields we asked for are present. - $this->assertNotNull($record->id, t('ID field is present.')); - $this->assertNotNull($record->name, t('Name field is present.')); - $this->assertNotNull($record->age, t('Age field is present.')); - $this->assertNotNull($record->job, t('Job field is present.')); + $this->assertNotNull($record->id, 'ID field is present.'); + $this->assertNotNull($record->name, 'Name field is present.'); + $this->assertNotNull($record->age, 'Age field is present.'); + $this->assertNotNull($record->job, 'Job field is present.'); // Ensure that we got the right record. // Check that all fields we asked for are present. - $this->assertEqual($record->id, 2, t('ID field has the correct value.')); - $this->assertEqual($record->name, 'George', t('Name field has the correct value.')); - $this->assertEqual($record->age, 27, t('Age field has the correct value.')); - $this->assertEqual($record->job, 'Singer', t('Job field has the correct value.')); + $this->assertEqual($record->id, 2, 'ID field has the correct value.'); + $this->assertEqual($record->name, 'George', 'Name field has the correct value.'); + $this->assertEqual($record->age, 27, 'Age field has the correct value.'); + $this->assertEqual($record->job, 'Singer', 'Job field has the correct value.'); } /** @@ -1429,8 +1576,8 @@ ->isNull('age') ->execute()->fetchCol(); - $this->assertEqual(count($names), 1, t('Correct number of records found with NULL age.')); - $this->assertEqual($names[0], 'Fozzie', t('Correct record returned for NULL age.')); + $this->assertEqual(count($names), 1, 'Correct number of records found with NULL age.'); + $this->assertEqual($names[0], 'Fozzie', 'Correct record returned for NULL age.'); } /** @@ -1445,9 +1592,9 @@ ->orderBy('name') ->execute()->fetchCol(); - $this->assertEqual(count($names), 2, t('Correct number of records found withNOT NULL age.')); - $this->assertEqual($names[0], 'Gonzo', t('Correct record returned for NOT NULL age.')); - $this->assertEqual($names[1], 'Kermit', t('Correct record returned for NOT NULL age.')); + $this->assertEqual(count($names), 2, 'Correct number of records found withNOT NULL age.'); + $this->assertEqual($names[0], 'Gonzo', 'Correct record returned for NOT NULL age.'); + $this->assertEqual($names[1], 'Kermit', 'Correct record returned for NOT NULL age.'); } /** @@ -1468,10 +1615,10 @@ $names = $query_1->execute()->fetchCol(); // Ensure we only get 2 records. - $this->assertEqual(count($names), 2, t('UNION correctly discarded duplicates.')); + $this->assertEqual(count($names), 2, 'UNION correctly discarded duplicates.'); - $this->assertEqual($names[0], 'George', t('First query returned correct name.')); - $this->assertEqual($names[1], 'Ringo', t('Second query returned correct name.')); + $this->assertEqual($names[0], 'George', 'First query returned correct name.'); + $this->assertEqual($names[1], 'Ringo', 'Second query returned correct name.'); } /** @@ -1491,11 +1638,11 @@ $names = $query_1->execute()->fetchCol(); // Ensure we get all 3 records. - $this->assertEqual(count($names), 3, t('UNION ALL correctly preserved duplicates.')); + $this->assertEqual(count($names), 3, 'UNION ALL correctly preserved duplicates.'); - $this->assertEqual($names[0], 'George', t('First query returned correct first name.')); - $this->assertEqual($names[1], 'Ringo', t('Second query returned correct second name.')); - $this->assertEqual($names[2], 'Ringo', t('Third query returned correct name.')); + $this->assertEqual($names[0], 'George', 'First query returned correct first name.'); + $this->assertEqual($names[1], 'Ringo', 'Second query returned correct second name.'); + $this->assertEqual($names[2], 'Ringo', 'Third query returned correct name.'); } /** @@ -1530,7 +1677,7 @@ ->orderBy('id') ->execute() ->fetchCol(); - $this->assertEqual($ordered_ids, $expected_ids, t('A query without random ordering returns IDs in the correct order.')); + $this->assertEqual($ordered_ids, $expected_ids, 'A query without random ordering returns IDs in the correct order.'); // Now perform the same query, but instead choose a random ordering. We // expect this to contain a differently ordered version of the original @@ -1541,10 +1688,10 @@ ->orderRandom() ->execute() ->fetchCol(); - $this->assertNotEqual($randomized_ids, $ordered_ids, t('A query with random ordering returns an unordered set of IDs.')); + $this->assertNotEqual($randomized_ids, $ordered_ids, 'A query with random ordering returns an unordered set of IDs.'); $sorted_ids = $randomized_ids; sort($sorted_ids); - $this->assertEqual($sorted_ids, $ordered_ids, t('After sorting the random list, the result matches the original query.')); + $this->assertEqual($sorted_ids, $ordered_ids, 'After sorting the random list, the result matches the original query.'); // Now perform the exact same query again, and make sure the order is // different. @@ -1554,10 +1701,10 @@ ->orderRandom() ->execute() ->fetchCol(); - $this->assertNotEqual($randomized_ids_second_set, $randomized_ids, t('Performing the query with random ordering a second time returns IDs in a different order.')); + $this->assertNotEqual($randomized_ids_second_set, $randomized_ids, 'Performing the query with random ordering a second time returns IDs in a different order.'); $sorted_ids_second_set = $randomized_ids_second_set; sort($sorted_ids_second_set); - $this->assertEqual($sorted_ids_second_set, $sorted_ids, t('After sorting the second random list, the result matches the sorted version of the first random list.')); + $this->assertEqual($sorted_ids_second_set, $sorted_ids, 'After sorting the second random list, the result matches the sorted version of the first random list.'); } /** @@ -1594,22 +1741,28 @@ $subquery->addField('tt', 'task', 'task'); $subquery->condition('priority', 1); - // Create another query that joins against the virtual table resulting - // from the subquery. - $select = db_select($subquery, 'tt2'); - $select->join('test', 't', 't.id=tt2.pid'); - $select->addField('t', 'name'); + for ($i = 0; $i < 2; $i++) { + // Create another query that joins against the virtual table resulting + // from the subquery. + $select = db_select($subquery, 'tt2'); + $select->join('test', 't', 't.id=tt2.pid'); + $select->addField('t', 'name'); + if ($i) { + // Use a different number of conditions here to confuse the subquery + // placeholder counter, testing http://drupal.org/node/1112854. + $select->condition('name', 'John'); + } + $select->condition('task', 'code'); - $select->condition('task', 'code'); + // The resulting query should be equivalent to: + // SELECT t.name + // FROM (SELECT tt.pid AS pid, tt.task AS task FROM test_task tt WHERE priority=1) tt + // INNER JOIN test t ON t.id=tt.pid + // WHERE tt.task = 'code' + $people = $select->execute()->fetchCol(); - // The resulting query should be equivalent to: - // SELECT t.name - // FROM (SELECT tt.pid AS pid, tt.task AS task FROM test_task tt WHERE priority=1) tt - // INNER JOIN test t ON t.id=tt.pid - // WHERE tt.task = 'code' - $people = $select->execute()->fetchCol(); - - $this->assertEqual(count($people), 1, t('Returned the correct number of rows.')); + $this->assertEqual(count($people), 1, 'Returned the correct number of rows.'); + } } /** @@ -1635,7 +1788,7 @@ // INNER JOIN test t ON t.id=tt.pid $people = $select->execute()->fetchCol(); - $this->assertEqual(count($people), 1, t('Returned the correct number of rows.')); + $this->assertEqual(count($people), 1, 'Returned the correct number of rows.'); } /** @@ -1658,7 +1811,7 @@ // FROM test tt2 // WHERE tt2.pid IN (SELECT tt.pid AS pid FROM test_task tt WHERE tt.priority=1) $people = $select->execute()->fetchCol(); - $this->assertEqual(count($people), 5, t('Returned the correct number of rows.')); + $this->assertEqual(count($people), 5, 'Returned the correct number of rows.'); } /** @@ -1682,11 +1835,14 @@ // INNER JOIN (SELECT tt.pid AS pid FROM test_task tt WHERE priority=1) tt ON t.id=tt.pid $people = $select->execute()->fetchCol(); - $this->assertEqual(count($people), 2, t('Returned the correct number of rows.')); + $this->assertEqual(count($people), 2, 'Returned the correct number of rows.'); } /** * Test EXISTS subquery conditionals on SELECT statements. + * + * We essentially select all rows from the {test} table that have matching + * rows in the {test_people} table based on the shared name column. */ function testExistsSubquerySelect() { // Put George into {test_people}. @@ -1703,17 +1859,20 @@ // Subquery to {test_people}. $subquery = db_select('test_people', 'tp') ->fields('tp', array('name')) - ->condition('name', 'George'); + ->where('tp.name = t.name'); $query->exists($subquery); $result = $query->execute(); // Ensure that we got the right record. $record = $result->fetch(); - $this->assertEqual($record->name, 'George', t('Fetched name is correct using EXISTS query.')); + $this->assertEqual($record->name, 'George', 'Fetched name is correct using EXISTS query.'); } /** * Test NOT EXISTS subquery conditionals on SELECT statements. + * + * We essentially select all rows from the {test} table that don't have + * matching rows in the {test_people} table based on the shared name column. */ function testNotExistsSubquerySelect() { // Put George into {test_people}. @@ -1731,13 +1890,12 @@ // Subquery to {test_people}. $subquery = db_select('test_people', 'tp') ->fields('tp', array('name')) - ->condition('name', 'George'); + ->where('tp.name = t.name'); $query->notExists($subquery); - $result = $query->execute(); - // Ensure that we got the right record. - $record = $result->fetch(); - $this->assertFalse($record, t('NOT EXISTS query returned no results.')); + // Ensure that we got the right number of records. + $people = $query->execute()->fetchCol(); + $this->assertEqual(count($people), 3, 'NOT EXISTS query returned the correct results.'); } } @@ -1768,11 +1926,11 @@ $last_age = 0; foreach ($result as $record) { $num_records++; - $this->assertTrue($record->age >= $last_age, t('Results returned in correct order.')); + $this->assertTrue($record->age >= $last_age, 'Results returned in correct order.'); $last_age = $record->age; } - $this->assertEqual($num_records, 4, t('Returned the correct number of rows.')); + $this->assertEqual($num_records, 4, 'Returned the correct number of rows.'); } /** @@ -1799,11 +1957,11 @@ $num_records++; foreach ($record as $kk => $col) { if ($expected[$k][$kk] != $results[$k][$kk]) { - $this->assertTrue(FALSE, t('Results returned in correct order.')); + $this->assertTrue(FALSE, 'Results returned in correct order.'); } } } - $this->assertEqual($num_records, 4, t('Returned the correct number of rows.')); + $this->assertEqual($num_records, 4, 'Returned the correct number of rows.'); } /** @@ -1820,11 +1978,20 @@ $last_age = 100000000; foreach ($result as $record) { $num_records++; - $this->assertTrue($record->age <= $last_age, t('Results returned in correct order.')); + $this->assertTrue($record->age <= $last_age, 'Results returned in correct order.'); $last_age = $record->age; } - $this->assertEqual($num_records, 4, t('Returned the correct number of rows.')); + $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.'); } } @@ -1858,12 +2025,12 @@ $last_priority = 0; foreach ($result as $record) { $num_records++; - $this->assertTrue($record->$priority_field >= $last_priority, t('Results returned in correct order.')); - $this->assertNotEqual($record->$name_field, 'Ringo', t('Taskless person not selected.')); + $this->assertTrue($record->$priority_field >= $last_priority, 'Results returned in correct order.'); + $this->assertNotEqual($record->$name_field, 'Ringo', 'Taskless person not selected.'); $last_priority = $record->$priority_field; } - $this->assertEqual($num_records, 7, t('Returned the correct number of rows.')); + $this->assertEqual($num_records, 7, 'Returned the correct number of rows.'); } /** @@ -1884,11 +2051,11 @@ foreach ($result as $record) { $num_records++; - $this->assertTrue(strcmp($record->$name_field, $last_name) >= 0, t('Results returned in correct order.')); + $this->assertTrue(strcmp($record->$name_field, $last_name) >= 0, 'Results returned in correct order.'); $last_priority = $record->$name_field; } - $this->assertEqual($num_records, 8, t('Returned the correct number of rows.')); + $this->assertEqual($num_records, 8, 'Returned the correct number of rows.'); } /** @@ -1907,7 +2074,7 @@ $records = array(); foreach ($result as $record) { $num_records++; - $this->assertTrue($record->$count_field >= $last_count, t('Results returned in correct order.')); + $this->assertTrue($record->$count_field >= $last_count, 'Results returned in correct order.'); $last_count = $record->$count_field; $records[$record->$task_field] = $record->$count_field; } @@ -1921,10 +2088,10 @@ ); foreach ($correct_results as $task => $count) { - $this->assertEqual($records[$task], $count, t("Correct number of '@task' records found.", array('@task' => $task))); + $this->assertEqual($records[$task], $count, format_string("Correct number of '@task' records found.", array('@task' => $task))); } - $this->assertEqual($num_records, 6, t('Returned the correct number of total rows.')); + $this->assertEqual($num_records, 6, 'Returned the correct number of total rows.'); } /** @@ -1944,8 +2111,8 @@ $records = array(); foreach ($result as $record) { $num_records++; - $this->assertTrue($record->$count_field >= 2, t('Record has the minimum count.')); - $this->assertTrue($record->$count_field >= $last_count, t('Results returned in correct order.')); + $this->assertTrue($record->$count_field >= 2, 'Record has the minimum count.'); + $this->assertTrue($record->$count_field >= $last_count, 'Results returned in correct order.'); $last_count = $record->$count_field; $records[$record->$task_field] = $record->$count_field; } @@ -1955,10 +2122,10 @@ ); foreach ($correct_results as $task => $count) { - $this->assertEqual($records[$task], $count, t("Correct number of '@task' records found.", array('@task' => $task))); + $this->assertEqual($records[$task], $count, format_string("Correct number of '@task' records found.", array('@task' => $task))); } - $this->assertEqual($num_records, 1, t('Returned the correct number of total rows.')); + $this->assertEqual($num_records, 1, 'Returned the correct number of total rows.'); } /** @@ -1976,7 +2143,7 @@ $num_records++; } - $this->assertEqual($num_records, 2, t('Returned the correct number of rows.')); + $this->assertEqual($num_records, 2, 'Returned the correct number of rows.'); } /** @@ -1993,7 +2160,7 @@ $num_records++; } - $this->assertEqual($num_records, 6, t('Returned the correct number of rows.')); + $this->assertEqual($num_records, 6, 'Returned the correct number of rows.'); } /** @@ -2007,13 +2174,24 @@ $count = $query->countQuery()->execute()->fetchField(); - $this->assertEqual($count, 4, t('Counted the correct number of records.')); + $this->assertEqual($count, 4, 'Counted the correct number of records.'); // Now make sure we didn't break the original query! We should still have // all of the fields we asked for. $record = $query->execute()->fetch(); - $this->assertEqual($record->$name_field, 'George', t('Correct data retrieved.')); - $this->assertEqual($record->$age_field, 27, t('Correct data retrieved.')); + $this->assertEqual($record->$name_field, 'George', 'Correct data retrieved.'); + $this->assertEqual($record->$age_field, 27, 'Correct data retrieved.'); + } + + function testHavingCountQuery() { + $query = db_select('test') + ->extend('PagerDefault') + ->groupBy('age') + ->having('age + 1 > 0'); + $query->addField('test', 'age'); + $query->addExpression('age + 1'); + $count = count($query->execute()->fetchCol()); + $this->assertEqual($count, 4, 'Counted the correct number of records.'); } /** @@ -2028,20 +2206,20 @@ // Check that the 'all_fields' statement is handled properly. $tables = $query->getTables(); - $this->assertEqual($tables['test']['all_fields'], 1, t('Query correctly sets \'all_fields\' statement.')); + $this->assertEqual($tables['test']['all_fields'], 1, 'Query correctly sets \'all_fields\' statement.'); $tables = $count->getTables(); - $this->assertFalse(isset($tables['test']['all_fields']), t('Count query correctly unsets \'all_fields\' statement.')); + $this->assertFalse(isset($tables['test']['all_fields']), 'Count query correctly unsets \'all_fields\' statement.'); // Check that the ordering clause is handled properly. $orderby = $query->getOrderBy(); - $this->assertEqual($orderby['name'], 'ASC', t('Query correctly sets ordering clause.')); + $this->assertEqual($orderby['name'], 'ASC', 'Query correctly sets ordering clause.'); $orderby = $count->getOrderBy(); - $this->assertFalse(isset($orderby['name']), t('Count query correctly unsets ordering caluse.')); + $this->assertFalse(isset($orderby['name']), 'Count query correctly unsets ordering caluse.'); // Make sure that the count query works. $count = $count->execute()->fetchField(); - $this->assertEqual($count, 4, t('Counted the correct number of records.')); + $this->assertEqual($count, 4, 'Counted the correct number of records.'); } @@ -2050,17 +2228,17 @@ */ function testCountQueryFieldRemovals() { // countQuery should remove all fields and expressions, so this can be - // tested by adding a non-existant field and expression: if it ends + // tested by adding a non-existent field and expression: if it ends // up in the query, an error will be thrown. If not, it will return the // number of records, which in this case happens to be 4 (there are four // records in the {test} table). $query = db_select('test'); $query->fields('test', array('fail')); - $this->assertEqual(4, $query->countQuery()->execute()->fetchField(), t('Count Query removed fields')); + $this->assertEqual(4, $query->countQuery()->execute()->fetchField(), 'Count Query removed fields'); $query = db_select('test'); $query->addExpression('fail'); - $this->assertEqual(4, $query->countQuery()->execute()->fetchField(), t('Count Query removed expressions')); + $this->assertEqual(4, $query->countQuery()->execute()->fetchField(), 'Count Query removed expressions'); } /** @@ -2073,7 +2251,7 @@ $count = $query->countQuery()->execute()->fetchField(); - $this->assertEqual($count, 6, t('Counted the correct number of records.')); + $this->assertEqual($count, 6, 'Counted the correct number of records.'); } /** @@ -2086,7 +2264,7 @@ $count = $query->countQuery()->execute()->fetchField(); - $this->assertEqual($count, 3, t('Counted the correct number of records.')); + $this->assertEqual($count, 3, 'Counted the correct number of records.'); // Use a column alias as, without one, the query can succeed for the wrong // reason. @@ -2098,7 +2276,7 @@ $count = $query->countQuery()->execute()->fetchField(); - $this->assertEqual($count, 3, t('Counted the correct number of records.')); + $this->assertEqual($count, 3, 'Counted the correct number of records.'); } /** @@ -2115,7 +2293,7 @@ $query->condition(db_or()->condition('age', 26)->condition('age', 27)); $job = $query->execute()->fetchField(); - $this->assertEqual($job, 'Songwriter', t('Correct data retrieved.')); + $this->assertEqual($job, 'Songwriter', 'Correct data retrieved.'); } /** @@ -2128,8 +2306,8 @@ $query->addField($alias, 'job', 'otherjob'); $query->where("$alias.name <> test.name"); $crowded_job = $query->execute()->fetch(); - $this->assertEqual($crowded_job->job, $crowded_job->otherjob, t('Correctly joined same table twice.')); - $this->assertNotEqual($crowded_job->name, $crowded_job->othername, t('Correctly joined same table twice.')); + $this->assertEqual($crowded_job->job, $crowded_job->otherjob, 'Correctly joined same table twice.'); + $this->assertNotEqual($crowded_job->name, $crowded_job->othername, 'Correctly joined same table twice.'); } } @@ -2194,7 +2372,7 @@ // Verify that the string only has one copy of condition placeholder 0. $pos = strpos($str, 'db_condition_placeholder_0', 0); $pos2 = strpos($str, 'db_condition_placeholder_0', $pos + 1); - $this->assertFalse($pos2, "Condition placeholder is not repeated"); + $this->assertFalse($pos2, 'Condition placeholder is not repeated.'); } } @@ -2238,7 +2416,7 @@ $correct_number = $count - ($limit * $page); } - $this->assertEqual(count($data->names), $correct_number, t('Correct number of records returned by pager: @number', array('@number' => $correct_number))); + $this->assertEqual(count($data->names), $correct_number, format_string('Correct number of records returned by pager: @number', array('@number' => $correct_number))); } } @@ -2272,7 +2450,7 @@ $correct_number = $count - ($limit * $page); } - $this->assertEqual(count($data->names), $correct_number, t('Correct number of records returned by pager: @number', array('@number' => $correct_number))); + $this->assertEqual(count($data->names), $correct_number, format_string('Correct number of records returned by pager: @number', array('@number' => $correct_number))); } } @@ -2294,7 +2472,7 @@ $ages = $outer_query ->execute() ->fetchCol(); - $this->assertEqual($ages, array(25, 26, 27, 28), t('Inner pager query returned the correct ages.')); + $this->assertEqual($ages, array(25, 26, 27, 28), 'Inner pager query returned the correct ages.'); } /** @@ -2314,12 +2492,12 @@ $ages = $query ->execute() ->fetchCol(); - $this->assertEqual($ages, array('George', 'Ringo'), t('Pager query with having expression returned the correct ages.')); + $this->assertEqual($ages, array('George', 'Ringo'), 'Pager query with having expression returned the correct ages.'); } /** - * Confirm that every pager gets a valid non-overlaping element ID. - */ + * Confirm that every pager gets a valid non-overlaping element ID. + */ function testElementNumbers() { $_GET['page'] = '3, 2, 1, 0'; @@ -2330,7 +2508,7 @@ ->limit(1) ->execute() ->fetchField(); - $this->assertEqual($name, 'Paul', t('Pager query #1 with a specified element ID returned the correct results.')); + $this->assertEqual($name, 'Paul', 'Pager query #1 with a specified element ID returned the correct results.'); // Setting an element smaller than the previous one // should not overwrite the pager $maxElement with a smaller value. @@ -2341,7 +2519,7 @@ ->limit(1) ->execute() ->fetchField(); - $this->assertEqual($name, 'George', t('Pager query #2 with a specified element ID returned the correct results.')); + $this->assertEqual($name, 'George', 'Pager query #2 with a specified element ID returned the correct results.'); $name = db_select('test', 't')->extend('PagerDefault') ->fields('t', array('name')) @@ -2349,7 +2527,7 @@ ->limit(1) ->execute() ->fetchField(); - $this->assertEqual($name, 'John', t('Pager query #3 with a generated element ID returned the correct results.')); + $this->assertEqual($name, 'John', 'Pager query #3 with a generated element ID returned the correct results.'); unset($_GET['page']); } @@ -2389,8 +2567,8 @@ $first = array_shift($data->tasks); $last = array_pop($data->tasks); - $this->assertEqual($first->task, $sort['first'], t('Items appear in the correct order.')); - $this->assertEqual($last->task, $sort['last'], t('Items appear in the correct order.')); + $this->assertEqual($first->task, $sort['first'], 'Items appear in the correct order.'); + $this->assertEqual($last->task, $sort['last'], 'Items appear in the correct order.'); } } @@ -2415,8 +2593,8 @@ $first = array_shift($data->tasks); $last = array_pop($data->tasks); - $this->assertEqual($first->task, $sort['first'], t('Items appear in the correct order sorting by @field @sort.', array('@field' => $sort['field'], '@sort' => $sort['sort']))); - $this->assertEqual($last->task, $sort['last'], t('Items appear in the correct order sorting by @field @sort.', array('@field' => $sort['field'], '@sort' => $sort['sort']))); + $this->assertEqual($first->task, $sort['first'], format_string('Items appear in the correct order sorting by @field @sort.', array('@field' => $sort['field'], '@sort' => $sort['sort']))); + $this->assertEqual($last->task, $sort['last'], format_string('Items appear in the correct order sorting by @field @sort.', array('@field' => $sort['field'], '@sort' => $sort['sort']))); } } @@ -2456,8 +2634,8 @@ $query->addTag('test'); - $this->assertTrue($query->hasTag('test'), t('hasTag() returned true.')); - $this->assertFalse($query->hasTag('other'), t('hasTag() returned false.')); + $this->assertTrue($query->hasTag('test'), 'hasTag() returned true.'); + $this->assertFalse($query->hasTag('other'), 'hasTag() returned false.'); } /** @@ -2471,8 +2649,8 @@ $query->addTag('test'); $query->addTag('other'); - $this->assertTrue($query->hasAllTags('test', 'other'), t('hasAllTags() returned true.')); - $this->assertFalse($query->hasAllTags('test', 'stuff'), t('hasAllTags() returned false.')); + $this->assertTrue($query->hasAllTags('test', 'other'), 'hasAllTags() returned true.'); + $this->assertFalse($query->hasAllTags('test', 'stuff'), 'hasAllTags() returned false.'); } /** @@ -2485,8 +2663,54 @@ $query->addTag('test'); - $this->assertTrue($query->hasAnyTag('test', 'other'), t('hasAnyTag() returned true.')); - $this->assertFalse($query->hasAnyTag('other', 'stuff'), t('hasAnyTag() returned false.')); + $this->assertTrue($query->hasAnyTag('test', 'other'), 'hasAnyTag() returned true.'); + $this->assertFalse($query->hasAnyTag('other', 'stuff'), 'hasAnyTag() returned false.'); + } + + /** + * 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.'); } /** @@ -2507,10 +2731,10 @@ $query->addMetaData('test', $data); $return = $query->getMetaData('test'); - $this->assertEqual($data, $return, t('Corect metadata returned.')); + $this->assertEqual($data, $return, 'Corect metadata returned.'); $return = $query->getMetaData('nothere'); - $this->assertNull($return, t('Non-existent key returned NULL.')); + $this->assertNull($return, 'Non-existent key returned NULL.'); } } @@ -2545,7 +2769,7 @@ $num_records++; } - $this->assertEqual($num_records, 2, t('Returned the correct number of rows.')); + $this->assertEqual($num_records, 2, 'Returned the correct number of rows.'); } /** @@ -2562,14 +2786,14 @@ $records = $result->fetchAll(); - $this->assertEqual(count($records), 2, t('Returned the correct number of rows.')); + $this->assertEqual(count($records), 2, 'Returned the correct number of rows.'); - $this->assertEqual($records[0]->name, 'George', t('Correct data retrieved.')); - $this->assertEqual($records[0]->$tid_field, 4, t('Correct data retrieved.')); - $this->assertEqual($records[0]->$task_field, 'sing', t('Correct data retrieved.')); - $this->assertEqual($records[1]->name, 'George', t('Correct data retrieved.')); - $this->assertEqual($records[1]->$tid_field, 5, t('Correct data retrieved.')); - $this->assertEqual($records[1]->$task_field, 'sleep', t('Correct data retrieved.')); + $this->assertEqual($records[0]->name, 'George', 'Correct data retrieved.'); + $this->assertEqual($records[0]->$tid_field, 4, 'Correct data retrieved.'); + $this->assertEqual($records[0]->$task_field, 'sing', 'Correct data retrieved.'); + $this->assertEqual($records[1]->name, 'George', 'Correct data retrieved.'); + $this->assertEqual($records[1]->$tid_field, 5, 'Correct data retrieved.'); + $this->assertEqual($records[1]->$task_field, 'sleep', 'Correct data retrieved.'); } /** @@ -2590,11 +2814,11 @@ $records = $result->fetchAll(); - $this->assertEqual(count($records), 1, t('Returned the correct number of rows.')); - $this->assertEqual($records[0]->$name_field, 'John', t('Correct data retrieved.')); - $this->assertEqual($records[0]->$tid_field, 2, t('Correct data retrieved.')); - $this->assertEqual($records[0]->$pid_field, 1, t('Correct data retrieved.')); - $this->assertEqual($records[0]->$task_field, 'sleep', t('Correct data retrieved.')); + $this->assertEqual(count($records), 1, 'Returned the correct number of rows.'); + $this->assertEqual($records[0]->$name_field, 'John', 'Correct data retrieved.'); + $this->assertEqual($records[0]->$tid_field, 2, 'Correct data retrieved.'); + $this->assertEqual($records[0]->$pid_field, 1, 'Correct data retrieved.'); + $this->assertEqual($records[0]->$task_field, 'sleep', 'Correct data retrieved.'); } /** @@ -2608,8 +2832,8 @@ $query->addTag('database_test_alter_change_fields'); $record = $query->execute()->fetch(); - $this->assertEqual($record->$name_field, 'George', t('Correct data retrieved.')); - $this->assertFalse(isset($record->$age_field), t('Age field not found, as intended.')); + $this->assertEqual($record->$name_field, 'George', 'Correct data retrieved.'); + $this->assertFalse(isset($record->$age_field), 'Age field not found, as intended.'); } /** @@ -2626,8 +2850,8 @@ // Ensure that we got the right record. $record = $result->fetch(); - $this->assertEqual($record->$name_field, 'George', t('Fetched name is correct.')); - $this->assertEqual($record->$age_field, 27*3, t('Fetched age expression is correct.')); + $this->assertEqual($record->$name_field, 'George', 'Fetched name is correct.'); + $this->assertEqual($record->$age_field, 27*3, 'Fetched age expression is correct.'); } /** @@ -2642,7 +2866,7 @@ $num_records = count($query->execute()->fetchAll()); - $this->assertEqual($num_records, 4, t('Returned the correct number of rows.')); + $this->assertEqual($num_records, 4, 'Returned the correct number of rows.'); } /** @@ -2666,8 +2890,8 @@ $name_field = $query->addField('pq', 'name'); $record = $query->execute()->fetch(); - $this->assertEqual($record->$name_field, 'George', t('Fetched name is correct.')); - $this->assertEqual($record->$age_field, 27*3, t('Fetched age expression is correct.')); + $this->assertEqual($record->$name_field, 'George', 'Fetched name is correct.'); + $this->assertEqual($record->$age_field, 27*3, 'Fetched age expression is correct.'); } } @@ -2701,31 +2925,31 @@ ))->execute(); $from_database = db_query('SELECT name FROM {test} WHERE name = :name', array(':name' => $name))->fetchField(); - $this->assertIdentical($name, $from_database, t("The database handles UTF-8 characters cleanly.")); + $this->assertIdentical($name, $from_database, "The database handles UTF-8 characters cleanly."); } /** * Test the db_table_exists() function. */ function testDBTableExists() { - $this->assertIdentical(TRUE, db_table_exists('node'), t('Returns true for existent table.')); - $this->assertIdentical(FALSE, db_table_exists('nosuchtable'), t('Returns false for nonexistent table.')); + $this->assertIdentical(TRUE, db_table_exists('node'), 'Returns true for existent table.'); + $this->assertIdentical(FALSE, db_table_exists('nosuchtable'), 'Returns false for nonexistent table.'); } /** * Test the db_field_exists() function. */ function testDBFieldExists() { - $this->assertIdentical(TRUE, db_field_exists('node', 'nid'), t('Returns true for existent column.')); - $this->assertIdentical(FALSE, db_field_exists('node', 'nosuchcolumn'), t('Returns false for nonexistent column.')); + $this->assertIdentical(TRUE, db_field_exists('node', 'nid'), 'Returns true for existent column.'); + $this->assertIdentical(FALSE, db_field_exists('node', 'nosuchcolumn'), 'Returns false for nonexistent column.'); } /** * Test the db_index_exists() function. */ function testDBIndexExists() { - $this->assertIdentical(TRUE, db_index_exists('node', 'node_created'), t('Returns true for existent index.')); - $this->assertIdentical(FALSE, db_index_exists('node', 'nosuchindex'), t('Returns false for nonexistent index.')); + $this->assertIdentical(TRUE, db_index_exists('node', 'node_created'), 'Returns true for existent index.'); + $this->assertIdentical(FALSE, db_index_exists('node', 'nosuchindex'), 'Returns false for nonexistent index.'); } } @@ -2746,17 +2970,20 @@ * Test that we can log the existence of a query. */ function testEnableLogging() { - Database::startLog('testing'); + $log = Database::startLog('testing'); db_query('SELECT name FROM {test} WHERE age > :age', array(':age' => 25))->fetchCol(); db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'Ringo'))->fetchCol(); + // Trigger a call that does not have file in the backtrace. + call_user_func_array('db_query', array('SELECT age FROM {test} WHERE name = :name', array(':name' => 'Ringo')))->fetchCol(); + $queries = Database::getLog('testing', 'default'); - $this->assertEqual(count($queries), 2, t('Correct number of queries recorded.')); + $this->assertEqual(count($queries), 3, 'Correct number of queries recorded.'); foreach ($queries as $query) { - $this->assertEqual($query['caller']['function'], __FUNCTION__, t('Correct function in query log.')); + $this->assertEqual($query['caller']['function'], __FUNCTION__, 'Correct function in query log.'); } } @@ -2775,8 +3002,8 @@ $queries1 = Database::getLog('testing1'); $queries2 = Database::getLog('testing2'); - $this->assertEqual(count($queries1), 2, t('Correct number of queries recorded for log 1.')); - $this->assertEqual(count($queries2), 1, t('Correct number of queries recorded for log 2.')); + $this->assertEqual(count($queries1), 2, 'Correct number of queries recorded for log 1.'); + $this->assertEqual(count($queries2), 1, 'Correct number of queries recorded for log 2.'); } /** @@ -2796,9 +3023,9 @@ $queries1 = Database::getLog('testing1'); - $this->assertEqual(count($queries1), 2, t('Recorded queries from all targets.')); - $this->assertEqual($queries1[0]['target'], 'default', t('First query used default target.')); - $this->assertEqual($queries1[1]['target'], 'slave', t('Second query used slave target.')); + $this->assertEqual(count($queries1), 2, 'Recorded queries from all targets.'); + $this->assertEqual($queries1[0]['target'], 'default', 'First query used default target.'); + $this->assertEqual($queries1[1]['target'], 'slave', 'Second query used slave target.'); } /** @@ -2822,9 +3049,9 @@ $queries1 = Database::getLog('testing1'); - $this->assertEqual(count($queries1), 2, t('Recorded queries from all targets.')); - $this->assertEqual($queries1[0]['target'], 'default', t('First query used default target.')); - $this->assertEqual($queries1[1]['target'], 'default', t('Second query used default target as fallback.')); + $this->assertEqual(count($queries1), 2, 'Recorded queries from all targets.'); + $this->assertEqual($queries1[0]['target'], 'default', 'First query used default target.'); + $this->assertEqual($queries1[1]['target'], 'default', 'Second query used default target as fallback.'); } /** @@ -2850,8 +3077,8 @@ $queries1 = Database::getLog('testing1'); $queries2 = Database::getLog('testing1', 'test2'); - $this->assertEqual(count($queries1), 1, t('Correct number of queries recorded for first connection.')); - $this->assertEqual(count($queries2), 1, t('Correct number of queries recorded for second connection.')); + $this->assertEqual(count($queries1), 1, 'Correct number of queries recorded for first connection.'); + $this->assertEqual(count($queries2), 1, 'Correct number of queries recorded for second connection.'); } } @@ -2878,7 +3105,7 @@ // assertion. $query = unserialize(serialize($query)); $results = $query->execute()->fetchCol(); - $this->assertEqual($results[0], 28, t('Query properly executed after unserialization.')); + $this->assertEqual($results[0], 28, 'Query properly executed after unserialization.'); } } @@ -2904,12 +3131,12 @@ function testRangeQuery() { // Test if return correct number of rows. $range_rows = db_query_range("SELECT name FROM {system} ORDER BY name", 2, 3)->fetchAll(); - $this->assertEqual(count($range_rows), 3, t('Range query work and return correct number of rows.')); + $this->assertEqual(count($range_rows), 3, 'Range query work and return correct number of rows.'); // Test if return target data. $raw_rows = db_query('SELECT name FROM {system} ORDER BY name')->fetchAll(); $raw_rows = array_slice($raw_rows, 2, 3); - $this->assertEqual($range_rows, $raw_rows, t('Range query work and return target data.')); + $this->assertEqual($range_rows, $raw_rows, 'Range query work and return target data.'); } } @@ -2943,19 +3170,28 @@ $this->drupalGet('database_test/db_query_temporary'); $data = json_decode($this->drupalGetContent()); if ($data) { - $this->assertEqual($this->countTableRows("system"), $data->row_count, t('The temporary table contains the correct amount of rows.')); - $this->assertFalse(db_table_exists($data->table_name), t('The temporary table is, indeed, temporary.')); + $this->assertEqual($this->countTableRows("system"), $data->row_count, 'The temporary table contains the correct amount of rows.'); + $this->assertFalse(db_table_exists($data->table_name), 'The temporary table is, indeed, temporary.'); } else { - $this->fail(t("The creation of the temporary table failed.")); + $this->fail("The creation of the temporary table failed."); } // Now try to run two db_query_temporary() in the same request. $table_name_system = db_query_temporary('SELECT status FROM {system}', array()); $table_name_users = db_query_temporary('SELECT uid FROM {users}', array()); - $this->assertEqual($this->countTableRows($table_name_system), $this->countTableRows("system"), t('A temporary table was created successfully in this request.')); - $this->assertEqual($this->countTableRows($table_name_users), $this->countTableRows("users"), t('A second temporary table was created successfully in this request.')); + $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.'); } } @@ -2990,7 +3226,7 @@ ':a4' => ' a ', ':a5' => 'test.', )); - $this->assertIdentical($result->fetchField(), 'This is a test.', t('Basic CONCAT works.')); + $this->assertIdentical($result->fetchField(), 'This is a test.', 'Basic CONCAT works.'); } /** @@ -3003,7 +3239,7 @@ ':a3' => '.', ':age' => 25, )); - $this->assertIdentical($result->fetchField(), 'The age of John is 25.', t('Field CONCAT works.')); + $this->assertIdentical($result->fetchField(), 'The age of John is 25.', 'Field CONCAT works.'); } /** @@ -3022,14 +3258,14 @@ ->countQuery() ->execute() ->fetchField(); - $this->assertIdentical($num_matches, '2', t('Found 2 records.')); + $this->assertIdentical($num_matches, '2', 'Found 2 records.'); // Match only "Ring_" using a LIKE expression with no wildcards. $num_matches = db_select('test', 't') ->condition('name', db_like('Ring_'), 'LIKE') ->countQuery() ->execute() ->fetchField(); - $this->assertIdentical($num_matches, '1', t('Found 1 record.')); + $this->assertIdentical($num_matches, '1', 'Found 1 record.'); } /** @@ -3053,14 +3289,47 @@ ->countQuery() ->execute() ->fetchField(); - $this->assertIdentical($num_matches, '2', t('Found 2 records.')); + $this->assertIdentical($num_matches, '2', 'Found 2 records.'); // Match only the former using a LIKE expression with no wildcards. $num_matches = db_select('test', 't') ->condition('name', db_like('abc%\_'), 'LIKE') ->countQuery() ->execute() ->fetchField(); - $this->assertIdentical($num_matches, '1', t('Found 1 record.')); + $this->assertIdentical($num_matches, '1', 'Found 1 record.'); + } +} + +/** + * Test case sensitivity handling. + */ +class DatabaseCaseSensitivityTestCase extends DatabaseTestCase { + public static function getInfo() { + return array( + 'name' => 'Case sensitivity', + 'description' => 'Test handling case sensitive collation.', + 'group' => 'Database', + ); + } + + /** + * Test BINARY collation in MySQL. + */ + function testCaseSensitiveInsert() { + $num_records_before = db_query('SELECT COUNT(*) FROM {test}')->fetchField(); + + $john = db_insert('test') + ->fields(array( + 'name' => 'john', // <- A record already exists with name 'John'. + 'age' => 2, + 'job' => 'Baby', + )) + ->execute(); + + $num_records_after = db_query('SELECT COUNT(*) FROM {test}')->fetchField(); + $this->assertIdentical($num_records_before + 1, (int) $num_records_after, 'Record inserts correctly.'); + $saved_age = db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'john'))->fetchField(); + $this->assertIdentical($saved_age, '2', 'Can retrieve after inserting.'); } } @@ -3103,7 +3372,7 @@ 'job' => 'Singer', )) ->execute(); - $this->fail(t('Insert succeedded when it should not have.')); + $this->fail('Insert succeedded when it should not have.'); } catch (Exception $e) { // Check if the first record was inserted. @@ -3115,14 +3384,14 @@ // Database engines that don't support transactions can leave partial // inserts in place when an error occurs. This is the case for MySQL // when running on a MyISAM table. - $this->pass(t("The whole transaction has not been rolled-back when a duplicate key insert occurs, this is expected because the database doesn't support transactions")); + $this->pass("The whole transaction has not been rolled-back when a duplicate key insert occurs, this is expected because the database doesn't support transactions"); } else { - $this->fail(t('The whole transaction is rolled back when a duplicate key insert occurs.')); + $this->fail('The whole transaction is rolled back when a duplicate key insert occurs.'); } } else { - $this->pass(t('The whole transaction is rolled back when a duplicate key insert occurs.')); + $this->pass('The whole transaction is rolled back when a duplicate key insert occurs.'); } // Ensure the other values were not inserted. @@ -3131,7 +3400,7 @@ ->condition('age', array(17, 75), 'IN') ->execute()->fetchObject(); - $this->assertFalse($record, t('The rest of the insert aborted as expected.')); + $this->assertFalse($record, 'The rest of the insert aborted as expected.'); } } @@ -3159,8 +3428,36 @@ function testArraySubstitution() { $names = db_query('SELECT name FROM {test} WHERE age IN (:ages) ORDER BY age', array(':ages' => array(25, 26, 27)))->fetchAll(); - $this->assertEqual(count($names), 3, t('Correct number of names returned')); + $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.'); + } + } /** @@ -3194,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, @@ -3211,8 +3510,10 @@ * Suffix to add to field values to differentiate tests. * @param $rollback * Whether or not to try rolling back the transaction when we're done. + * @param $ddl_statement + * Whether to execute a DDL statement during the inner transaction. */ - protected function transactionOuterLayer($suffix, $rollback = FALSE) { + protected function transactionOuterLayer($suffix, $rollback = FALSE, $ddl_statement = FALSE) { $connection = Database::getConnection(); $depth = $connection->transactionDepth(); $txn = db_transaction(); @@ -3225,19 +3526,19 @@ )) ->execute(); - $this->assertTrue($connection->inTransaction(), t('In transaction before calling nested transaction.')); + $this->assertTrue($connection->inTransaction(), 'In transaction before calling nested transaction.'); // We're already in a transaction, but we call ->transactionInnerLayer // to nest another transaction inside the current one. - $this->transactionInnerLayer($suffix, $rollback); + $this->transactionInnerLayer($suffix, $rollback, $ddl_statement); - $this->assertTrue($connection->inTransaction(), t('In transaction after calling nested transaction.')); + $this->assertTrue($connection->inTransaction(), 'In transaction after calling nested transaction.'); if ($rollback) { // Roll back the transaction, if requested. // This rollback should propagate to the last savepoint. $txn->rollback(); - $this->assertTrue(($connection->transactionDepth() == $depth), t('Transaction has rolled back to the last savepoint after calling rollback().')); + $this->assertTrue(($connection->transactionDepth() == $depth), 'Transaction has rolled back to the last savepoint after calling rollback().'); } } @@ -3249,12 +3550,12 @@ * Suffix to add to field values to differentiate tests. * @param $rollback * Whether or not to try rolling back the transaction when we're done. + * @param $ddl_statement + * Whether to execute a DDL statement during the transaction. */ - protected function transactionInnerLayer($suffix, $rollback = FALSE) { + protected function transactionInnerLayer($suffix, $rollback = FALSE, $ddl_statement = FALSE) { $connection = Database::getConnection(); - $this->assertTrue($connection->inTransaction(), t('In transaction in nested transaction.')); - $depth = $connection->transactionDepth(); // Start a transaction. If we're being called from ->transactionOuterLayer, // then we're already in a transaction. Normally, that would make starting @@ -3263,7 +3564,7 @@ $txn = db_transaction(); $depth2 = $connection->transactionDepth(); - $this->assertTrue($depth < $depth2, t('Transaction depth is has increased with new transaction.')); + $this->assertTrue($depth < $depth2, 'Transaction depth is has increased with new transaction.'); // Insert a single row into the testing table. db_insert('test') @@ -3273,13 +3574,29 @@ )) ->execute(); - $this->assertTrue($connection->inTransaction(), t('In transaction inside nested transaction.')); + $this->assertTrue($connection->inTransaction(), 'In transaction inside nested transaction.'); + + if ($ddl_statement) { + $table = array( + 'fields' => array( + 'id' => array( + 'type' => 'serial', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + ), + 'primary key' => array('id'), + ); + db_create_table('database_test_1', $table); + + $this->assertTrue($connection->inTransaction(), 'In transaction inside nested transaction.'); + } if ($rollback) { // Roll back the transaction, if requested. // This rollback should propagate to the last savepoint. $txn->rollback(); - $this->assertTrue(($connection->transactionDepth() == $depth), t('Transaction has rolled back to the last savepoint after calling rollback().')); + $this->assertTrue(($connection->transactionDepth() == $depth), 'Transaction has rolled back to the last savepoint after calling rollback().'); } } @@ -3300,9 +3617,9 @@ // Neither of the rows we inserted in the two transaction layers // should be present in the tables post-rollback. $saved_age = db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'DavidB'))->fetchField(); - $this->assertNotIdentical($saved_age, '24', t('Cannot retrieve DavidB row after commit.')); + $this->assertNotIdentical($saved_age, '24', 'Cannot retrieve DavidB row after commit.'); $saved_age = db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'DanielB'))->fetchField(); - $this->assertNotIdentical($saved_age, '19', t('Cannot retrieve DanielB row after commit.')); + $this->assertNotIdentical($saved_age, '19', 'Cannot retrieve DanielB row after commit.'); } catch (Exception $e) { $this->fail($e->getMessage()); @@ -3326,9 +3643,9 @@ // Because our current database claims to not support transactions, // the inserted rows should be present despite the attempt to roll back. $saved_age = db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'DavidB'))->fetchField(); - $this->assertIdentical($saved_age, '24', t('DavidB not rolled back, since transactions are not supported.')); + $this->assertIdentical($saved_age, '24', 'DavidB not rolled back, since transactions are not supported.'); $saved_age = db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'DanielB'))->fetchField(); - $this->assertIdentical($saved_age, '19', t('DanielB not rolled back, since transactions are not supported.')); + $this->assertIdentical($saved_age, '19', 'DanielB not rolled back, since transactions are not supported.'); } catch (Exception $e) { $this->fail($e->getMessage()); @@ -3348,14 +3665,287 @@ // Because we committed, both of the inserted rows should be present. $saved_age = db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'DavidA'))->fetchField(); - $this->assertIdentical($saved_age, '24', t('Can retrieve DavidA row after commit.')); + $this->assertIdentical($saved_age, '24', 'Can retrieve DavidA row after commit.'); $saved_age = db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'DanielA'))->fetchField(); - $this->assertIdentical($saved_age, '19', t('Can retrieve DanielA row after commit.')); + $this->assertIdentical($saved_age, '19', 'Can retrieve DanielA row after commit.'); } catch (Exception $e) { $this->fail($e->getMessage()); } } + + /** + * Test the compatibility of transactions with DDL statements. + */ + function testTransactionWithDdlStatement() { + // First, test that a commit works normally, even with DDL statements. + $transaction = db_transaction(); + $this->insertRow('row'); + $this->executeDDLStatement(); + unset($transaction); + $this->assertRowPresent('row'); + + // Even in different order. + $this->cleanUp(); + $transaction = db_transaction(); + $this->executeDDLStatement(); + $this->insertRow('row'); + unset($transaction); + $this->assertRowPresent('row'); + + // Even with stacking. + $this->cleanUp(); + $transaction = db_transaction(); + $transaction2 = db_transaction(); + $this->executeDDLStatement(); + unset($transaction2); + $transaction3 = db_transaction(); + $this->insertRow('row'); + unset($transaction3); + unset($transaction); + $this->assertRowPresent('row'); + + // A transaction after a DDL statement should still work the same. + $this->cleanUp(); + $transaction = db_transaction(); + $transaction2 = db_transaction(); + $this->executeDDLStatement(); + unset($transaction2); + $transaction3 = db_transaction(); + $this->insertRow('row'); + $transaction3->rollback(); + unset($transaction3); + unset($transaction); + $this->assertRowAbsent('row'); + + // The behavior of a rollback depends on the type of database server. + if (Database::getConnection()->supportsTransactionalDDL()) { + // For database servers that support transactional DDL, a rollback + // of a transaction including DDL statements should be possible. + $this->cleanUp(); + $transaction = db_transaction(); + $this->insertRow('row'); + $this->executeDDLStatement(); + $transaction->rollback(); + unset($transaction); + $this->assertRowAbsent('row'); + + // Including with stacking. + $this->cleanUp(); + $transaction = db_transaction(); + $transaction2 = db_transaction(); + $this->executeDDLStatement(); + unset($transaction2); + $transaction3 = db_transaction(); + $this->insertRow('row'); + unset($transaction3); + $transaction->rollback(); + unset($transaction); + $this->assertRowAbsent('row'); + } + else { + // For database servers that do not support transactional DDL, + // the DDL statement should commit the transaction stack. + $this->cleanUp(); + $transaction = db_transaction(); + $this->insertRow('row'); + $this->executeDDLStatement(); + // Rollback the outer transaction. + try { + $transaction->rollback(); + unset($transaction); + // @TODO: an exception should be triggered here, but is not, because + // "ROLLBACK" fails silently in MySQL if there is no transaction active. + // $this->fail(t('Rolling back a transaction containing DDL should fail.')); + } + catch (DatabaseTransactionNoActiveException $e) { + $this->pass('Rolling back a transaction containing DDL should fail.'); + } + $this->assertRowPresent('row'); + } + } + + /** + * Insert a single row into the testing table. + */ + protected function insertRow($name) { + db_insert('test') + ->fields(array( + 'name' => $name, + )) + ->execute(); + } + + /** + * Execute a DDL statement. + */ + protected function executeDDLStatement() { + static $count = 0; + $table = array( + 'fields' => array( + 'id' => array( + 'type' => 'serial', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + ), + 'primary key' => array('id'), + ); + db_create_table('database_test_' . ++$count, $table); + } + + /** + * Start over for a new test. + */ + protected function cleanUp() { + db_truncate('test') + ->execute(); + } + + /** + * Assert that a given row is present in the test table. + * + * @param $name + * The name of the row. + * @param $message + * The message to log for the assertion. + */ + function assertRowPresent($name, $message = NULL) { + if (!isset($message)) { + $message = format_string('Row %name is present.', array('%name' => $name)); + } + $present = (boolean) db_query('SELECT 1 FROM {test} WHERE name = :name', array(':name' => $name))->fetchField(); + return $this->assertTrue($present, $message); + } + + /** + * Assert that a given row is absent from the test table. + * + * @param $name + * The name of the row. + * @param $message + * The message to log for the assertion. + */ + function assertRowAbsent($name, $message = NULL) { + if (!isset($message)) { + $message = format_string('Row %name is absent.', array('%name' => $name)); + } + $present = (boolean) db_query('SELECT 1 FROM {test} WHERE name = :name', array(':name' => $name))->fetchField(); + return $this->assertFalse($present, $message); + } + + /** + * Test transaction stacking and commit / rollback. + */ + function testTransactionStacking() { + // This test won't work right if transactions are not supported. + if (!Database::getConnection()->supportsTransactions()) { + return; + } + + $database = Database::getConnection(); + + // Standard case: pop the inner transaction before the outer transaction. + $transaction = db_transaction(); + $this->insertRow('outer'); + $transaction2 = db_transaction(); + $this->insertRow('inner'); + // Pop the inner transaction. + unset($transaction2); + $this->assertTrue($database->inTransaction(), 'Still in a transaction after popping the inner transaction'); + // Pop the outer transaction. + unset($transaction); + $this->assertFalse($database->inTransaction(), 'Transaction closed after popping the outer transaction'); + $this->assertRowPresent('outer'); + $this->assertRowPresent('inner'); + + // Pop the transaction in a different order they have been pushed. + $this->cleanUp(); + $transaction = db_transaction(); + $this->insertRow('outer'); + $transaction2 = db_transaction(); + $this->insertRow('inner'); + // Pop the outer transaction, nothing should happen. + unset($transaction); + $this->insertRow('inner-after-outer-commit'); + $this->assertTrue($database->inTransaction(), 'Still in a transaction after popping the outer transaction'); + // Pop the inner transaction, the whole transaction should commit. + unset($transaction2); + $this->assertFalse($database->inTransaction(), 'Transaction closed after popping the inner transaction'); + $this->assertRowPresent('outer'); + $this->assertRowPresent('inner'); + $this->assertRowPresent('inner-after-outer-commit'); + + // Rollback the inner transaction. + $this->cleanUp(); + $transaction = db_transaction(); + $this->insertRow('outer'); + $transaction2 = db_transaction(); + $this->insertRow('inner'); + // Now rollback the inner transaction. + $transaction2->rollback(); + unset($transaction2); + $this->assertTrue($database->inTransaction(), 'Still in a transaction after popping the outer transaction'); + // Pop the outer transaction, it should commit. + $this->insertRow('outer-after-inner-rollback'); + unset($transaction); + $this->assertFalse($database->inTransaction(), 'Transaction closed after popping the inner transaction'); + $this->assertRowPresent('outer'); + $this->assertRowAbsent('inner'); + $this->assertRowPresent('outer-after-inner-rollback'); + + // Rollback the inner transaction after committing the outer one. + $this->cleanUp(); + $transaction = db_transaction(); + $this->insertRow('outer'); + $transaction2 = db_transaction(); + $this->insertRow('inner'); + // Pop the outer transaction, nothing should happen. + unset($transaction); + $this->assertTrue($database->inTransaction(), 'Still in a transaction after popping the outer transaction'); + // Now rollback the inner transaction, it should rollback. + $transaction2->rollback(); + unset($transaction2); + $this->assertFalse($database->inTransaction(), 'Transaction closed after popping the inner transaction'); + $this->assertRowPresent('outer'); + $this->assertRowAbsent('inner'); + + // Rollback the outer transaction while the inner transaction is active. + // In that case, an exception will be triggered because we cannot + // ensure that the final result will have any meaning. + $this->cleanUp(); + $transaction = db_transaction(); + $this->insertRow('outer'); + $transaction2 = db_transaction(); + $this->insertRow('inner'); + $transaction3 = db_transaction(); + $this->insertRow('inner2'); + // Rollback the outer transaction. + try { + $transaction->rollback(); + unset($transaction); + $this->fail('Rolling back the outer transaction while the inner transaction is active resulted in an exception.'); + } + catch (DatabaseTransactionOutOfOrderException $e) { + $this->pass('Rolling back the outer transaction while the inner transaction is active resulted in an exception.'); + } + $this->assertFalse($database->inTransaction(), 'No more in a transaction after rolling back the outer transaction'); + // Try to commit one inner transaction. + unset($transaction3); + $this->pass('Trying to commit an inner transaction resulted in an exception.'); + // Try to rollback one inner transaction. + try { + $transaction->rollback(); + unset($transaction2); + $this->fail('Trying to commit an inner transaction resulted in an exception.'); + } + catch (DatabaseTransactionNoActiveException $e) { + $this->pass('Trying to commit an inner transaction resulted in an exception.'); + } + $this->assertRowAbsent('outer'); + $this->assertRowAbsent('inner'); + $this->assertRowAbsent('inner2'); + } } @@ -3365,9 +3955,9 @@ class DatabaseNextIdCase extends DrupalWebTestCase { public static function getInfo() { return array( - 'name' => t('Sequences API'), - 'description' => t('Test the secondary sequences API.'), - 'group' => t('Database'), + 'name' => 'Sequences API', + 'description' => 'Test the secondary sequences API.', + 'group' => 'Database', ); } @@ -3380,9 +3970,9 @@ // We can test for exact increase in here because we know there is no // other process operating on these tables -- normally we could only // expect $second > $first. - $this->assertEqual($first + 1, $second, t('The second call from a sequence provides a number increased by one.')); + $this->assertEqual($first + 1, $second, 'The second call from a sequence provides a number increased by one.'); $result = db_next_id(1000); - $this->assertEqual($result, 1001, t('Sequence provides a larger number than the existing ID.')); + $this->assertEqual($result, 1001, 'Sequence provides a larger number than the existing ID.'); } } @@ -3392,9 +3982,9 @@ class DatabaseEmptyStatementTestCase extends DrupalWebTestCase { public static function getInfo() { return array( - 'name' => t('Empty statement'), - 'description' => t('Test the empty pseudo-statement class.'), - 'group' => t('Database'), + 'name' => 'Empty statement', + 'description' => 'Test the empty pseudo-statement class.', + 'group' => 'Database', ); } @@ -3404,8 +3994,8 @@ function testEmpty() { $result = new DatabaseStatementEmpty(); - $this->assertTrue($result instanceof DatabaseStatementInterface, t('Class implements expected interface')); - $this->assertNull($result->fetchObject(), t('Null result returned.')); + $this->assertTrue($result instanceof DatabaseStatementInterface, 'Class implements expected interface'); + $this->assertNull($result->fetchObject(), 'Null result returned.'); } /** @@ -3415,11 +4005,11 @@ $result = new DatabaseStatementEmpty(); foreach ($result as $record) { - $this->fail(t('Iterating empty result set should not iterate.')); + $this->fail('Iterating empty result set should not iterate.'); return; } - $this->pass(t('Iterating empty result set skipped iteration.')); + $this->pass('Iterating empty result set skipped iteration.'); } /** @@ -3428,6 +4018,225 @@ function testEmptyFetchAll() { $result = new DatabaseStatementEmpty(); - $this->assertEqual($result->fetchAll(), array(), t('Empty array returned from empty result set.')); + $this->assertEqual($result->fetchAll(), array(), 'Empty array returned from empty result set.'); + } +} + +/** + * Tests management of database connections. + */ +class ConnectionUnitTest extends DrupalUnitTestCase { + + protected $key; + protected $target; + + protected $monitor; + protected $originalCount; + + public static function getInfo() { + return array( + 'name' => 'Connection unit tests', + 'description' => 'Tests management of database connections.', + 'group' => 'Database', + ); + } + + function setUp() { + parent::setUp(); + + $this->key = 'default'; + $this->originalTarget = 'default'; + $this->target = 'DatabaseConnectionUnitTest'; + + // Determine whether the database driver is MySQL. If it is not, the test + // methods will not be executed. + // @todo Make this test driver-agnostic, or find a proper way to skip it. + // @see http://drupal.org/node/1273478 + $connection_info = Database::getConnectionInfo('default'); + $this->skipTest = (bool) $connection_info['default']['driver'] != 'mysql'; + if ($this->skipTest) { + // Insert an assertion to prevent Simpletest from interpreting the test + // as failure. + $this->pass('This test is only compatible with MySQL.'); + } + + // Create an additional connection to monitor the connections being opened + // and closed in this test. + // @see TestBase::changeDatabasePrefix() + $connection_info = Database::getConnectionInfo('default'); + Database::addConnectionInfo('default', 'monitor', $connection_info['default']); + global $databases; + $databases['default']['monitor'] = $connection_info['default']; + $this->monitor = Database::getConnection('monitor'); + } + + /** + * Adds a new database connection info to Database. + */ + protected function addConnection() { + // Add a new target to the connection, by cloning the current connection. + $connection_info = Database::getConnectionInfo($this->key); + Database::addConnectionInfo($this->key, $this->target, $connection_info[$this->originalTarget]); + + // Verify that the new target exists. + $info = Database::getConnectionInfo($this->key); + // Note: Custom assertion message to not expose database credentials. + $this->assertIdentical($info[$this->target], $connection_info[$this->key], 'New connection info found.'); + } + + /** + * Returns the connection ID of the current test connection. + * + * @return integer + */ + protected function getConnectionID() { + return (int) Database::getConnection($this->target, $this->key)->query('SELECT CONNECTION_ID()')->fetchField(); + } + + /** + * Asserts that a connection ID exists. + * + * @param integer $id + * The connection ID to verify. + */ + protected function assertConnection($id) { + $list = $this->monitor->query('SHOW PROCESSLIST')->fetchAllKeyed(0, 0); + return $this->assertTrue(isset($list[$id]), format_string('Connection ID @id found.', array('@id' => $id))); } + + /** + * Asserts that a connection ID does not exist. + * + * @param integer $id + * The connection ID to verify. + */ + protected function assertNoConnection($id) { + $list = $this->monitor->query('SHOW PROCESSLIST')->fetchAllKeyed(0, 0); + return $this->assertFalse(isset($list[$id]), format_string('Connection ID @id not found.', array('@id' => $id))); + } + + /** + * Tests Database::closeConnection() without query. + * + * @todo getConnectionID() executes a query. + */ + function testOpenClose() { + if ($this->skipTest) { + return; + } + // Add and open a new connection. + $this->addConnection(); + $id = $this->getConnectionID(); + Database::getConnection($this->target, $this->key); + + // Verify that there is a new connection. + $this->assertConnection($id); + + // Close the connection. + Database::closeConnection($this->target, $this->key); + // Wait 20ms to give the database engine sufficient time to react. + usleep(20000); + + // Verify that we are back to the original connection count. + $this->assertNoConnection($id); + } + + /** + * Tests Database::closeConnection() with a query. + */ + function testOpenQueryClose() { + if ($this->skipTest) { + return; + } + // Add and open a new connection. + $this->addConnection(); + $id = $this->getConnectionID(); + Database::getConnection($this->target, $this->key); + + // Verify that there is a new connection. + $this->assertConnection($id); + + // Execute a query. + Database::getConnection($this->target, $this->key)->query('SHOW TABLES'); + + // Close the connection. + Database::closeConnection($this->target, $this->key); + // Wait 20ms to give the database engine sufficient time to react. + usleep(20000); + + // Verify that we are back to the original connection count. + $this->assertNoConnection($id); + } + + /** + * Tests Database::closeConnection() with a query and custom prefetch method. + */ + function testOpenQueryPrefetchClose() { + if ($this->skipTest) { + return; + } + // Add and open a new connection. + $this->addConnection(); + $id = $this->getConnectionID(); + Database::getConnection($this->target, $this->key); + + // Verify that there is a new connection. + $this->assertConnection($id); + + // Execute a query. + Database::getConnection($this->target, $this->key)->query('SHOW TABLES')->fetchCol(); + + // Close the connection. + Database::closeConnection($this->target, $this->key); + // Wait 20ms to give the database engine sufficient time to react. + usleep(20000); + + // Verify that we are back to the original connection count. + $this->assertNoConnection($id); + } + + /** + * Tests Database::closeConnection() with a select query. + */ + function testOpenSelectQueryClose() { + if ($this->skipTest) { + return; + } + // Add and open a new connection. + $this->addConnection(); + $id = $this->getConnectionID(); + Database::getConnection($this->target, $this->key); + + // Verify that there is a new connection. + $this->assertConnection($id); + + // Create a table. + $name = 'foo'; + Database::getConnection($this->target, $this->key)->schema()->createTable($name, array( + 'fields' => array( + 'name' => array( + 'type' => 'varchar', + 'length' => 255, + ), + ), + )); + + // Execute a query. + Database::getConnection($this->target, $this->key)->select('foo', 'f') + ->fields('f', array('name')) + ->execute() + ->fetchAll(); + + // Drop the table. + Database::getConnection($this->target, $this->key)->schema()->dropTable($name); + + // Close the connection. + Database::closeConnection($this->target, $this->key); + // Wait 20ms to give the database engine sufficient time to react. + usleep(20000); + + // Verify that we are back to the original connection count. + $this->assertNoConnection($id); + } + } diff -Naur drupal-7.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/modules/simpletest/tests/drupal_system_listing_compatible_test/drupal_system_listing_compatible_test.info 2011-01-05 07:25:56.000000000 +0100 +++ 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 @@ -1,4 +1,3 @@ -; $Id: drupal_system_listing_compatible_test.info,v 1.2 2010/12/20 19:59:43 webchick Exp $ name = "Drupal system listing compatible test" description = "Support module for testing the drupal_system_listing function." package = Testing @@ -6,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/simpletest/tests/drupal_system_listing_compatible_test/drupal_system_listing_compatible_test.module drupal-7.66/modules/simpletest/tests/drupal_system_listing_compatible_test/drupal_system_listing_compatible_test.module --- drupal-7.0/modules/simpletest/tests/drupal_system_listing_compatible_test/drupal_system_listing_compatible_test.module 2010-11-15 01:37:08.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/drupal_system_listing_compatible_test/drupal_system_listing_compatible_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,2 +1 @@ <?php -// $Id: drupal_system_listing_compatible_test.module,v 1.1 2010/11/15 00:37:08 webchick Exp $ diff -Naur drupal-7.0/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.0/modules/simpletest/tests/drupal_system_listing_incompatible_test/drupal_system_listing_incompatible_test.info 2011-01-05 07:25:56.000000000 +0100 +++ 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 @@ -1,4 +1,3 @@ -; $Id: drupal_system_listing_incompatible_test.info,v 1.2 2010/12/20 19:59:43 webchick Exp $ name = "Drupal system listing incompatible test" description = "Support module for testing the drupal_system_listing function." package = Testing @@ -6,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/simpletest/tests/drupal_system_listing_incompatible_test/drupal_system_listing_incompatible_test.module drupal-7.66/modules/simpletest/tests/drupal_system_listing_incompatible_test/drupal_system_listing_incompatible_test.module --- drupal-7.0/modules/simpletest/tests/drupal_system_listing_incompatible_test/drupal_system_listing_incompatible_test.module 2010-11-15 01:37:08.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/drupal_system_listing_incompatible_test/drupal_system_listing_incompatible_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,2 +1 @@ <?php -// $Id: drupal_system_listing_incompatible_test.module,v 1.1 2010/11/15 00:37:08 webchick Exp $ diff -Naur drupal-7.0/modules/simpletest/tests/entity_cache_test.info drupal-7.66/modules/simpletest/tests/entity_cache_test.info --- drupal-7.0/modules/simpletest/tests/entity_cache_test.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/entity_cache_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: entity_cache_test.info,v 1.2 2010/12/20 19:59:43 webchick Exp $ name = "Entity cache test" description = "Support module for testing entity cache." package = Testing @@ -7,8 +6,7 @@ dependencies[] = entity_cache_test_dependency hidden = TRUE -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/simpletest/tests/entity_cache_test.module drupal-7.66/modules/simpletest/tests/entity_cache_test.module --- drupal-7.0/modules/simpletest/tests/entity_cache_test.module 2010-10-15 06:44:08.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/entity_cache_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: entity_cache_test.module,v 1.2 2010/10/15 04:44:08 webchick Exp $ /** * @file diff -Naur drupal-7.0/modules/simpletest/tests/entity_cache_test_dependency.info drupal-7.66/modules/simpletest/tests/entity_cache_test_dependency.info --- drupal-7.0/modules/simpletest/tests/entity_cache_test_dependency.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/entity_cache_test_dependency.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: entity_cache_test_dependency.info,v 1.2 2010/12/20 19:59:43 webchick Exp $ name = "Entity cache test dependency" description = "Support dependency module for testing entity cache." package = Testing @@ -6,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/simpletest/tests/entity_cache_test_dependency.module drupal-7.66/modules/simpletest/tests/entity_cache_test_dependency.module --- drupal-7.0/modules/simpletest/tests/entity_cache_test_dependency.module 2010-09-15 06:34:27.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/entity_cache_test_dependency.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: entity_cache_test_dependency.module,v 1.2 2010/09/15 04:34:27 webchick Exp $ /** * @file @@ -12,7 +11,7 @@ function entity_cache_test_dependency_entity_info() { return array( 'entity_cache_test' => array( - 'label' => 'Entity Cache Test', + 'label' => variable_get('entity_cache_test_label', 'Entity Cache Test'), ), ); } diff -Naur drupal-7.0/modules/simpletest/tests/entity_crud.test drupal-7.66/modules/simpletest/tests/entity_crud.test --- drupal-7.0/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.0/modules/simpletest/tests/entity_crud_hook_test.info drupal-7.66/modules/simpletest/tests/entity_crud_hook_test.info --- drupal-7.0/modules/simpletest/tests/entity_crud_hook_test.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/entity_crud_hook_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: entity_crud_hook_test.info,v 1.2 2010/12/20 19:59:43 webchick Exp $ name = "Entity CRUD Hooks Test" description = "Support module for CRUD hook tests." core = 7.x @@ -6,8 +5,7 @@ version = VERSION hidden = TRUE -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/simpletest/tests/entity_crud_hook_test.module drupal-7.66/modules/simpletest/tests/entity_crud_hook_test.module --- drupal-7.0/modules/simpletest/tests/entity_crud_hook_test.module 2010-12-15 04:39:42.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/entity_crud_hook_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,9 +1,9 @@ <?php -// $Id: entity_crud_hook_test.module,v 1.2 2010/12/15 03:39:42 webchick Exp $ -// -// Presave hooks -// +/** + * @file + * Test module for the Entity CRUD API. + */ /** * Implements hook_entity_presave(). @@ -54,10 +54,6 @@ $_SESSION['entity_crud_hook_test'][] = (__FUNCTION__ . ' called'); } -// -// Insert hooks -// - /** * Implements hook_entity_insert(). */ @@ -107,10 +103,6 @@ $_SESSION['entity_crud_hook_test'][] = (__FUNCTION__ . ' called'); } -// -// Load hooks -// - /** * Implements hook_entity_load(). */ @@ -160,10 +152,6 @@ $_SESSION['entity_crud_hook_test'][] = (__FUNCTION__ . ' called'); } -// -// Update hooks -// - /** * Implements hook_entity_update(). */ @@ -213,10 +201,6 @@ $_SESSION['entity_crud_hook_test'][] = (__FUNCTION__ . ' called'); } -// -// Delete hooks -// - /** * Implements hook_entity_delete(). */ diff -Naur drupal-7.0/modules/simpletest/tests/entity_crud_hook_test.test drupal-7.66/modules/simpletest/tests/entity_crud_hook_test.test --- drupal-7.0/modules/simpletest/tests/entity_crud_hook_test.test 2010-12-15 04:39:42.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/entity_crud_hook_test.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,9 +1,14 @@ <?php -// $Id: entity_crud_hook_test.test,v 1.2 2010/12/15 03:39:42 webchick Exp $ /** - * Test invocation of hooks when inserting, loading, updating or deleting an - * entity. Tested hooks are: + * @file + * CRUD hook tests for the Entity CRUD API. + */ + +/** + * Tests invocation of hooks when performing an action. + * + * Tested hooks are: * - hook_entity_insert() * - hook_entity_load() * - hook_entity_update() @@ -49,7 +54,7 @@ } /** - * Test hook invocations for CRUD operations on comments. + * Tests hook invocations for CRUD operations on comments. */ public function testCommentHooks() { $node = (object) array( @@ -109,7 +114,7 @@ } /** - * Test hook invocations for CRUD operations on files. + * Tests hook invocations for CRUD operations on files. */ public function testFileHooks() { $url = 'public://entity_crud_hook_test.file'; @@ -155,7 +160,7 @@ } /** - * Test hook invocations for CRUD operations on nodes. + * Tests hook invocations for CRUD operations on nodes. */ public function testNodeHooks() { $node = (object) array( @@ -201,7 +206,7 @@ } /** - * Test hook invocations for CRUD operations on taxonomy terms. + * Tests hook invocations for CRUD operations on taxonomy terms. */ public function testTaxonomyTermHooks() { $vocabulary = (object) array( @@ -249,7 +254,7 @@ } /** - * Test hook invocations for CRUD operations on taxonomy vocabularies. + * Tests hook invocations for CRUD operations on taxonomy vocabularies. */ public function testTaxonomyVocabularyHooks() { $vocabulary = (object) array( @@ -289,7 +294,7 @@ } /** - * Test hook invocations for CRUD operations on users. + * Tests hook invocations for CRUD operations on users. */ public function testUserHooks() { $edit = array( diff -Naur drupal-7.0/modules/simpletest/tests/entity_query.test drupal-7.66/modules/simpletest/tests/entity_query.test --- drupal-7.0/modules/simpletest/tests/entity_query.test 2010-11-14 23:07:57.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/entity_query.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,6 +1,5 @@ <?php -// $Id: entity_query.test,v 1.15 2010/11/14 22:07:57 webchick Exp $ /** * @file @@ -21,12 +20,12 @@ } function setUp() { - parent::setUp(array('field_test')); + parent::setUp(array('node', 'field_test', 'entity_query_access_test', 'node_access_test')); - field_attach_create_bundle('test_entity_bundle_key', 'bundle1'); - field_attach_create_bundle('test_entity_bundle_key', 'bundle2'); - field_attach_create_bundle('test_entity', 'test_bundles'); - field_attach_create_bundle('test_entity_bundle', 'test_entity_bundle'); + field_test_create_bundle('bundle1'); + field_test_create_bundle('bundle2'); + field_test_create_bundle('test_bundle'); + field_test_create_bundle('test_entity_bundle'); $instances = array(); $this->fields = array(); @@ -170,7 +169,7 @@ ->entityCondition('entity_id', '5'); $this->assertEntityFieldQuery($query, array( array('test_entity_bundle', 5), - ), t('Test query on an entity type with a generated bundle.')); + ), 'Test query on an entity type with a generated bundle.'); // Test entity_type condition. $query = new EntityFieldQuery(); @@ -182,7 +181,7 @@ array('test_entity_bundle_key', 4), array('test_entity_bundle_key', 5), array('test_entity_bundle_key', 6), - ), t('Test entity entity_type condition.')); + ), 'Test entity entity_type condition.'); // Test entity_id condition. $query = new EntityFieldQuery(); @@ -191,7 +190,7 @@ ->entityCondition('entity_id', '3'); $this->assertEntityFieldQuery($query, array( array('test_entity_bundle_key', 3), - ), t('Test entity entity_id condition.')); + ), 'Test entity entity_id condition.'); $query = new EntityFieldQuery(); $query @@ -199,7 +198,7 @@ ->propertyCondition('ftid', '3'); $this->assertEntityFieldQuery($query, array( array('test_entity_bundle_key', 3), - ), t('Test entity entity_id condition and entity_id property condition.')); + ), 'Test entity entity_id condition and entity_id property condition.'); // Test bundle condition. $query = new EntityFieldQuery(); @@ -211,7 +210,7 @@ array('test_entity_bundle_key', 2), array('test_entity_bundle_key', 3), array('test_entity_bundle_key', 4), - ), t('Test entity bundle condition: bundle1.')); + ), 'Test entity bundle condition: bundle1.'); $query = new EntityFieldQuery(); $query @@ -220,7 +219,7 @@ $this->assertEntityFieldQuery($query, array( array('test_entity_bundle_key', 5), array('test_entity_bundle_key', 6), - ), t('Test entity bundle condition: bundle2.')); + ), 'Test entity bundle condition: bundle2.'); $query = new EntityFieldQuery(); $query @@ -229,7 +228,7 @@ $this->assertEntityFieldQuery($query, array( array('test_entity_bundle_key', 5), array('test_entity_bundle_key', 6), - ), t('Test entity bundle condition and bundle property condition.')); + ), 'Test entity bundle condition and bundle property condition.'); // Test revision_id condition. $query = new EntityFieldQuery(); @@ -238,7 +237,7 @@ ->entityCondition('revision_id', '3'); $this->assertEntityFieldQuery($query, array( array('test_entity', 3), - ), t('Test entity revision_id condition.')); + ), 'Test entity revision_id condition.'); $query = new EntityFieldQuery(); $query @@ -246,7 +245,7 @@ ->propertyCondition('ftvid', '3'); $this->assertEntityFieldQuery($query, array( array('test_entity', 3), - ), t('Test entity revision_id condition and revision_id property condition.')); + ), 'Test entity revision_id condition and revision_id property condition.'); $query = new EntityFieldQuery(); $query @@ -256,7 +255,7 @@ $this->assertEntityFieldQuery($query, array( array('test_entity', 100), array('test_entity', 101), - ), t('Test revision age.')); + ), 'Test revision age.'); // Test that fields attached to the non-revision supporting entity // 'test_entity_bundle_key' are reachable in FIELD_LOAD_REVISION. @@ -275,7 +274,7 @@ array('test_entity', 2), array('test_entity', 3), array('test_entity', 4), - ), t('Test that fields are reachable from FIELD_LOAD_REVISION even for non-revision entities.')); + ), 'Test that fields are reachable from FIELD_LOAD_REVISION even for non-revision entities.'); // Test entity sort by entity_id. $query = new EntityFieldQuery(); @@ -289,7 +288,7 @@ array('test_entity_bundle_key', 4), array('test_entity_bundle_key', 5), array('test_entity_bundle_key', 6), - ), t('Test sort entity entity_id in ascending order.'), TRUE); + ), 'Test sort entity entity_id in ascending order.', TRUE); $query = new EntityFieldQuery(); $query @@ -302,7 +301,7 @@ array('test_entity_bundle_key', 3), array('test_entity_bundle_key', 2), array('test_entity_bundle_key', 1), - ), t('Test sort entity entity_id in descending order.'), TRUE); + ), 'Test sort entity entity_id in descending order.', TRUE); // Test entity sort by entity_id, with a field condition. $query = new EntityFieldQuery(); @@ -317,7 +316,7 @@ array('test_entity_bundle_key', 4), array('test_entity_bundle_key', 5), array('test_entity_bundle_key', 6), - ), t('Test sort entity entity_id in ascending order, with a field condition.'), TRUE); + ), 'Test sort entity entity_id in ascending order, with a field condition.', TRUE); $query = new EntityFieldQuery(); $query @@ -331,7 +330,7 @@ array('test_entity_bundle_key', 3), array('test_entity_bundle_key', 2), array('test_entity_bundle_key', 1), - ), t('Test sort entity entity_id property in descending order, with a field condition.'), TRUE); + ), 'Test sort entity entity_id property in descending order, with a field condition.', TRUE); // Test property sort by entity id. $query = new EntityFieldQuery(); @@ -345,7 +344,7 @@ array('test_entity_bundle_key', 4), array('test_entity_bundle_key', 5), array('test_entity_bundle_key', 6), - ), t('Test sort entity entity_id property in ascending order.'), TRUE); + ), 'Test sort entity entity_id property in ascending order.', TRUE); $query = new EntityFieldQuery(); $query @@ -358,7 +357,7 @@ array('test_entity_bundle_key', 3), array('test_entity_bundle_key', 2), array('test_entity_bundle_key', 1), - ), t('Test sort entity entity_id property in descending order.'), TRUE); + ), 'Test sort entity entity_id property in descending order.', TRUE); // Test property sort by entity id, with a field condition. $query = new EntityFieldQuery(); @@ -373,7 +372,7 @@ array('test_entity_bundle_key', 4), array('test_entity_bundle_key', 5), array('test_entity_bundle_key', 6), - ), t('Test sort entity entity_id property in ascending order, with a field condition.'), TRUE); + ), 'Test sort entity entity_id property in ascending order, with a field condition.', TRUE); $query = new EntityFieldQuery(); $query @@ -387,7 +386,7 @@ array('test_entity_bundle_key', 3), array('test_entity_bundle_key', 2), array('test_entity_bundle_key', 1), - ), t('Test sort entity entity_id property in descending order, with a field condition.'), TRUE); + ), 'Test sort entity entity_id property in descending order, with a field condition.', TRUE); // Test entity sort by bundle. $query = new EntityFieldQuery(); @@ -402,7 +401,7 @@ array('test_entity_bundle_key', 1), array('test_entity_bundle_key', 6), array('test_entity_bundle_key', 5), - ), t('Test sort entity bundle in ascending order, property in descending order.'), TRUE); + ), 'Test sort entity bundle in ascending order, property in descending order.', TRUE); $query = new EntityFieldQuery(); $query @@ -416,7 +415,7 @@ array('test_entity_bundle_key', 2), array('test_entity_bundle_key', 3), array('test_entity_bundle_key', 4), - ), t('Test sort entity bundle in descending order, property in ascending order.'), TRUE); + ), 'Test sort entity bundle in descending order, property in ascending order.', TRUE); // Test entity sort by bundle, with a field condition. $query = new EntityFieldQuery(); @@ -432,7 +431,7 @@ array('test_entity_bundle_key', 1), array('test_entity_bundle_key', 6), array('test_entity_bundle_key', 5), - ), t('Test sort entity bundle in ascending order, property in descending order, with a field condition.'), TRUE); + ), 'Test sort entity bundle in ascending order, property in descending order, with a field condition.', TRUE); $query = new EntityFieldQuery(); $query @@ -447,7 +446,7 @@ array('test_entity_bundle_key', 2), array('test_entity_bundle_key', 3), array('test_entity_bundle_key', 4), - ), t('Test sort entity bundle in descending order, property in ascending order, with a field condition.'), TRUE); + ), 'Test sort entity bundle in descending order, property in ascending order, with a field condition.', TRUE); // Test entity sort by bundle, field. $query = new EntityFieldQuery(); @@ -462,7 +461,7 @@ array('test_entity_bundle_key', 1), array('test_entity_bundle_key', 6), array('test_entity_bundle_key', 5), - ), t('Test sort entity bundle in ascending order, field in descending order.'), TRUE); + ), 'Test sort entity bundle in ascending order, field in descending order.', TRUE); $query = new EntityFieldQuery(); $query @@ -476,7 +475,7 @@ array('test_entity_bundle_key', 2), array('test_entity_bundle_key', 3), array('test_entity_bundle_key', 4), - ), t('Test sort entity bundle in descending order, field in ascending order.'), TRUE); + ), 'Test sort entity bundle in descending order, field in ascending order.', TRUE); // Test entity sort by revision_id. $query = new EntityFieldQuery(); @@ -488,7 +487,7 @@ array('test_entity', 2), array('test_entity', 3), array('test_entity', 4), - ), t('Test sort entity revision_id in ascending order.'), TRUE); + ), 'Test sort entity revision_id in ascending order.', TRUE); $query = new EntityFieldQuery(); $query @@ -499,7 +498,7 @@ array('test_entity', 3), array('test_entity', 2), array('test_entity', 1), - ), t('Test sort entity revision_id in descending order.'), TRUE); + ), 'Test sort entity revision_id in descending order.', TRUE); // Test entity sort by revision_id, with a field condition. $query = new EntityFieldQuery(); @@ -512,7 +511,7 @@ array('test_entity', 2), array('test_entity', 3), array('test_entity', 4), - ), t('Test sort entity revision_id in ascending order, with a field condition.'), TRUE); + ), 'Test sort entity revision_id in ascending order, with a field condition.', TRUE); $query = new EntityFieldQuery(); $query @@ -524,7 +523,7 @@ array('test_entity', 3), array('test_entity', 2), array('test_entity', 1), - ), t('Test sort entity revision_id in descending order, with a field condition.'), TRUE); + ), 'Test sort entity revision_id in descending order, with a field condition.', TRUE); // Test property sort by revision_id. $query = new EntityFieldQuery(); @@ -536,7 +535,7 @@ array('test_entity', 2), array('test_entity', 3), array('test_entity', 4), - ), t('Test sort entity revision_id property in ascending order.'), TRUE); + ), 'Test sort entity revision_id property in ascending order.', TRUE); $query = new EntityFieldQuery(); $query @@ -547,7 +546,7 @@ array('test_entity', 3), array('test_entity', 2), array('test_entity', 1), - ), t('Test sort entity revision_id property in descending order.'), TRUE); + ), 'Test sort entity revision_id property in descending order.', TRUE); // Test property sort by revision_id, with a field condition. $query = new EntityFieldQuery(); @@ -560,7 +559,7 @@ array('test_entity', 2), array('test_entity', 3), array('test_entity', 4), - ), t('Test sort entity revision_id property in ascending order, with a field condition.'), TRUE); + ), 'Test sort entity revision_id property in ascending order, with a field condition.', TRUE); $query = new EntityFieldQuery(); $query @@ -572,7 +571,7 @@ array('test_entity', 3), array('test_entity', 2), array('test_entity', 1), - ), t('Test sort entity revision_id property in descending order, with a field condition.'), TRUE); + ), 'Test sort entity revision_id property in descending order, with a field condition.', TRUE); $query = new EntityFieldQuery(); $query @@ -585,7 +584,7 @@ array('test_entity_bundle_key', 4), array('test_entity_bundle_key', 5), array('test_entity_bundle_key', 6), - ), t('Test sort field in ascending order without field condition.'), TRUE); + ), 'Test sort field in ascending order without field condition.', TRUE); $query = new EntityFieldQuery(); $query @@ -598,7 +597,7 @@ array('test_entity_bundle_key', 3), array('test_entity_bundle_key', 2), array('test_entity_bundle_key', 1), - ), t('Test sort field in descending order without field condition.'), TRUE); + ), 'Test sort field in descending order without field condition.', TRUE); $query = new EntityFieldQuery(); $query @@ -612,7 +611,7 @@ array('test_entity_bundle_key', 4), array('test_entity_bundle_key', 5), array('test_entity_bundle_key', 6), - ), t('Test sort field in ascending order.'), TRUE); + ), 'Test sort field in ascending order.', TRUE); $query = new EntityFieldQuery(); $query @@ -626,7 +625,7 @@ array('test_entity_bundle_key', 3), array('test_entity_bundle_key', 2), array('test_entity_bundle_key', 1), - ), t('Test sort field in descending order.'), TRUE); + ), 'Test sort field in descending order.', TRUE); // Test "in" operation with entity entity_type condition and entity_id // property condition. @@ -638,7 +637,7 @@ array('test_entity_bundle_key', 1), array('test_entity_bundle_key', 3), array('test_entity_bundle_key', 4), - ), t('Test "in" operation with entity entity_type condition and entity_id property condition.')); + ), 'Test "in" operation with entity entity_type condition and entity_id property condition.'); // Test "in" operation with entity entity_type condition and entity_id // property condition. Sort in descending order by entity_id. @@ -651,7 +650,7 @@ array('test_entity_bundle_key', 4), array('test_entity_bundle_key', 3), array('test_entity_bundle_key', 1), - ), t('Test "in" operation with entity entity_type condition and entity_id property condition. Sort entity_id in descending order.'), TRUE); + ), 'Test "in" operation with entity entity_type condition and entity_id property condition. Sort entity_id in descending order.', TRUE); // Test query count $query = new EntityFieldQuery(); @@ -659,7 +658,7 @@ ->entityCondition('entity_type', 'test_entity_bundle_key') ->count() ->execute(); - $this->assertEqual($query_count, 6, t('Test query count on entity condition.')); + $this->assertEqual($query_count, 6, 'Test query count on entity condition.'); $query = new EntityFieldQuery(); $query_count = $query @@ -667,7 +666,7 @@ ->propertyCondition('ftid', '1') ->count() ->execute(); - $this->assertEqual($query_count, 1, t('Test query count on entity and property condition.')); + $this->assertEqual($query_count, 1, 'Test query count on entity and property condition.'); $query = new EntityFieldQuery(); $query_count = $query @@ -675,7 +674,7 @@ ->propertyCondition('ftid', '4', '>') ->count() ->execute(); - $this->assertEqual($query_count, 2, t('Test query count on entity and property condition with operator.')); + $this->assertEqual($query_count, 2, 'Test query count on entity and property condition with operator.'); $query = new EntityFieldQuery(); $query_count = $query @@ -683,7 +682,7 @@ ->fieldCondition($this->fields[0], 'value', 3, '=') ->count() ->execute(); - $this->assertEqual($query_count, 1, t('Test query count on field condition.')); + $this->assertEqual($query_count, 1, 'Test query count on field condition.'); // First, test without options. $query = new EntityFieldQuery(); @@ -697,13 +696,13 @@ array('test_entity_bundle_key', 4), array('test_entity_bundle_key', 5), array('test_entity_bundle_key', 6), - ), t('Test the "contains" operation on a property.')); + ), 'Test the "contains" operation on a property.'); $query = new EntityFieldQuery(); $query->fieldCondition($this->fields[1], 'shape', 'uar', 'CONTAINS'); $this->assertEntityFieldQuery($query, array( array('test_entity_bundle', 5), - ), t('Test the "contains" operation on a field.')); + ), 'Test the "contains" operation on a field.'); $query = new EntityFieldQuery(); $query @@ -711,14 +710,39 @@ ->propertyCondition('ftid', 1, '='); $this->assertEntityFieldQuery($query, array( array('test_entity_bundle_key', 1), - ), t('Test the "equal to" operation on a property.')); + ), 'Test the "equal to" operation on a property.'); $query = new EntityFieldQuery(); $query->fieldCondition($this->fields[0], 'value', 3, '='); $this->assertEntityFieldQuery($query, array( array('test_entity_bundle_key', 3), array('test_entity', 3), - ), t('Test the "equal to" operation on a field.')); + ), 'Test the "equal to" operation on a field.'); + + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity_bundle_key') + ->propertyCondition('ftid', 3, '<>'); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 1), + array('test_entity_bundle_key', 2), + array('test_entity_bundle_key', 4), + array('test_entity_bundle_key', 5), + array('test_entity_bundle_key', 6), + ), 'Test the "not equal to" operation on a property.'); + + $query = new EntityFieldQuery(); + $query->fieldCondition($this->fields[0], 'value', 3, '<>'); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 1), + array('test_entity_bundle_key', 2), + array('test_entity_bundle_key', 4), + array('test_entity_bundle_key', 5), + array('test_entity_bundle_key', 6), + array('test_entity', 1), + array('test_entity', 2), + array('test_entity', 4), + ), 'Test the "not equal to" operation on a field.'); $query = new EntityFieldQuery(); $query @@ -730,7 +754,7 @@ array('test_entity_bundle_key', 4), array('test_entity_bundle_key', 5), array('test_entity_bundle_key', 6), - ), t('Test the "not equal to" operation on a property.')); + ), 'Test the "not equal to" operation on a property.'); $query = new EntityFieldQuery(); $query->fieldCondition($this->fields[0], 'value', 3, '!='); @@ -743,7 +767,7 @@ array('test_entity', 1), array('test_entity', 2), array('test_entity', 4), - ), t('Test the "not equal to" operation on a field.')); + ), 'Test the "not equal to" operation on a field.'); $query = new EntityFieldQuery(); $query @@ -751,14 +775,14 @@ ->propertyCondition('ftid', 2, '<'); $this->assertEntityFieldQuery($query, array( array('test_entity_bundle_key', 1), - ), t('Test the "less than" operation on a property.')); + ), 'Test the "less than" operation on a property.'); $query = new EntityFieldQuery(); $query->fieldCondition($this->fields[0], 'value', 2, '<'); $this->assertEntityFieldQuery($query, array( array('test_entity_bundle_key', 1), array('test_entity', 1), - ), t('Test the "less than" operation on a field.')); + ), 'Test the "less than" operation on a field.'); $query = new EntityFieldQuery(); $query @@ -767,7 +791,7 @@ $this->assertEntityFieldQuery($query, array( array('test_entity_bundle_key', 1), array('test_entity_bundle_key', 2), - ), t('Test the "less than or equal to" operation on a property.')); + ), 'Test the "less than or equal to" operation on a property.'); $query = new EntityFieldQuery(); $query->fieldCondition($this->fields[0], 'value', 2, '<='); @@ -776,7 +800,7 @@ array('test_entity_bundle_key', 2), array('test_entity', 1), array('test_entity', 2), - ), t('Test the "less than or equal to" operation on a field.')); + ), 'Test the "less than or equal to" operation on a field.'); $query = new EntityFieldQuery(); $query @@ -785,7 +809,7 @@ $this->assertEntityFieldQuery($query, array( array('test_entity_bundle_key', 5), array('test_entity_bundle_key', 6), - ), t('Test the "greater than" operation on a property.')); + ), 'Test the "greater than" operation on a property.'); $query = new EntityFieldQuery(); $query->fieldCondition($this->fields[0], 'value', 2, '>'); @@ -796,7 +820,7 @@ array('test_entity_bundle_key', 6), array('test_entity', 3), array('test_entity', 4), - ), t('Test the "greater than" operation on a field.')); + ), 'Test the "greater than" operation on a field.'); $query = new EntityFieldQuery(); $query @@ -806,7 +830,7 @@ array('test_entity_bundle_key', 4), array('test_entity_bundle_key', 5), array('test_entity_bundle_key', 6), - ), t('Test the "greater than or equal to" operation on a property.')); + ), 'Test the "greater than or equal to" operation on a property.'); $query = new EntityFieldQuery(); $query->fieldCondition($this->fields[0], 'value', 3, '>='); @@ -817,7 +841,7 @@ array('test_entity_bundle_key', 6), array('test_entity', 3), array('test_entity', 4), - ), t('Test the "greater than or equal to" operation on a field.')); + ), 'Test the "greater than or equal to" operation on a field.'); $query = new EntityFieldQuery(); $query @@ -828,7 +852,7 @@ array('test_entity_bundle_key', 2), array('test_entity_bundle_key', 5), array('test_entity_bundle_key', 6), - ), t('Test the "not in" operation on a property.')); + ), 'Test the "not in" operation on a property.'); $query = new EntityFieldQuery(); $query->fieldCondition($this->fields[0], 'value', array(3, 4, 100, 101), 'NOT IN'); @@ -839,7 +863,7 @@ array('test_entity_bundle_key', 6), array('test_entity', 1), array('test_entity', 2), - ), t('Test the "not in" operation on a field.')); + ), 'Test the "not in" operation on a field.'); $query = new EntityFieldQuery(); $query @@ -848,7 +872,7 @@ $this->assertEntityFieldQuery($query, array( array('test_entity_bundle_key', 3), array('test_entity_bundle_key', 4), - ), t('Test the "in" operation on a property.')); + ), 'Test the "in" operation on a property.'); $query = new EntityFieldQuery(); $query->fieldCondition($this->fields[0], 'value', array(2, 3), 'IN'); @@ -857,7 +881,7 @@ array('test_entity_bundle_key', 3), array('test_entity', 2), array('test_entity', 3), - ), t('Test the "in" operation on a field.')); + ), 'Test the "in" operation on a field.'); $query = new EntityFieldQuery(); $query @@ -867,7 +891,7 @@ array('test_entity_bundle_key', 1), array('test_entity_bundle_key', 2), array('test_entity_bundle_key', 3), - ), t('Test the "between" operation on a property.')); + ), 'Test the "between" operation on a property.'); $query = new EntityFieldQuery(); $query->fieldCondition($this->fields[0], 'value', array(1, 3), 'BETWEEN'); @@ -878,7 +902,7 @@ array('test_entity', 1), array('test_entity', 2), array('test_entity', 3), - ), t('Test the "between" operation on a field.')); + ), 'Test the "between" operation on a field.'); $query = new EntityFieldQuery(); $query @@ -891,20 +915,20 @@ array('test_entity_bundle_key', 4), array('test_entity_bundle_key', 5), array('test_entity_bundle_key', 6), - ), t('Test the "starts_with" operation on a property.')); + ), 'Test the "starts_with" operation on a property.'); $query = new EntityFieldQuery(); $query->fieldCondition($this->fields[1], 'shape', 'squ', 'STARTS_WITH'); $this->assertEntityFieldQuery($query, array( array('test_entity_bundle', 5), - ), t('Test the "starts_with" operation on a field.')); + ), 'Test the "starts_with" operation on a field.'); $query = new EntityFieldQuery(); $query->fieldCondition($this->fields[0], 'value', 3); $this->assertEntityFieldQuery($query, array( array('test_entity_bundle_key', 3), array('test_entity', 3), - ), t('Test omission of an operator with a single item.')); + ), 'Test omission of an operator with a single item.'); $query = new EntityFieldQuery(); $query->fieldCondition($this->fields[0], 'value', array(2, 3)); @@ -913,7 +937,7 @@ array('test_entity_bundle_key', 3), array('test_entity', 2), array('test_entity', 3), - ), t('Test omission of an operator with multiple items.')); + ), 'Test omission of an operator with multiple items.'); $query = new EntityFieldQuery(); $query @@ -923,7 +947,7 @@ $this->assertEntityFieldQuery($query, array( array('test_entity_bundle_key', 2), array('test_entity_bundle_key', 3), - ), t('Test entity, property and field conditions.')); + ), 'Test entity, property and field conditions.'); $query = new EntityFieldQuery(); $query @@ -933,7 +957,7 @@ ->fieldCondition($this->fields[0], 'value', 4); $this->assertEntityFieldQuery($query, array( array('test_entity_bundle_key', 4), - ), t('Test entity condition with "starts_with" operation, and property and field conditions.')); + ), 'Test entity condition with "starts_with" operation, and property and field conditions.'); $query = new EntityFieldQuery(); $query @@ -943,7 +967,7 @@ $this->assertEntityFieldQuery($query, array( array('test_entity_bundle_key', 1), array('test_entity_bundle_key', 2), - ), t('Test limit on a property.'), TRUE); + ), 'Test limit on a property.', TRUE); $query = new EntityFieldQuery(); $query @@ -954,7 +978,7 @@ $this->assertEntityFieldQuery($query, array( array('test_entity_bundle_key', 1), array('test_entity_bundle_key', 2), - ), t('Test limit on a field.'), TRUE); + ), 'Test limit on a field.', TRUE); $query = new EntityFieldQuery(); $query @@ -964,7 +988,7 @@ $this->assertEntityFieldQuery($query, array( array('test_entity_bundle_key', 5), array('test_entity_bundle_key', 6), - ), t('Test offset on a property.'), TRUE); + ), 'Test offset on a property.', TRUE); $query = new EntityFieldQuery(); $query @@ -977,7 +1001,7 @@ array('test_entity_bundle_key', 4), array('test_entity_bundle_key', 5), array('test_entity_bundle_key', 6), - ), t('Test offset on a field.'), TRUE); + ), 'Test offset on a field.', TRUE); for ($i = 6; $i < 10; $i++) { $entity = new stdClass(); @@ -999,7 +1023,7 @@ array('test_entity', 4), array('test_entity_bundle', 8), array('test_entity_bundle', 9), - ), t('Select a field across multiple entities.')); + ), 'Select a field across multiple entities.'); $query = new EntityFieldQuery(); $query @@ -1007,13 +1031,13 @@ ->fieldCondition($this->fields[1], 'color', 'blue'); $this->assertEntityFieldQuery($query, array( array('test_entity_bundle', 5), - ), t('Test without a delta group.')); + ), 'Test without a delta group.'); $query = new EntityFieldQuery(); $query ->fieldCondition($this->fields[1], 'shape', 'square', '=', 'group') ->fieldCondition($this->fields[1], 'color', 'blue', '=', 'group'); - $this->assertEntityFieldQuery($query, array(), t('Test with a delta group.')); + $this->assertEntityFieldQuery($query, array(), 'Test with a delta group.'); // Test query on a deleted field. field_attach_delete_bundle('test_entity_bundle_key', 'bundle1'); @@ -1022,12 +1046,12 @@ $query->fieldCondition($this->fields[0], 'value', '3'); $this->assertEntityFieldQuery($query, array( array('test_entity_bundle', 8), - ), t('Test query on a field after deleting field from some entities.')); + ), 'Test query on a field after deleting field from some entities.'); field_attach_delete_bundle('test_entity_bundle', 'test_entity_bundle'); $query = new EntityFieldQuery(); $query->fieldCondition($this->fields[0], 'value', '3'); - $this->assertEntityFieldQuery($query, array(), t('Test query on a field after deleting field from all entities.')); + $this->assertEntityFieldQuery($query, array(), 'Test query on a field after deleting field from all entities.'); $query = new EntityFieldQuery(); $query @@ -1037,7 +1061,7 @@ array('test_entity_bundle_key', 3), array('test_entity_bundle', 8), array('test_entity', 3), - ), t('Test query on a deleted field with deleted option set to TRUE.')); + ), 'Test query on a deleted field with deleted option set to TRUE.'); $pass = FALSE; $query = new EntityFieldQuery(); @@ -1047,7 +1071,232 @@ catch (EntityFieldQueryException $exception) { $pass = ($exception->getMessage() == t('For this query an entity type must be specified.')); } - $this->assertTrue($pass, t("Can't query the universe.")); + $this->assertTrue($pass, "Can't query the universe."); + } + + /** + * Tests querying translatable fields. + */ + function testEntityFieldQueryTranslatable() { + + // Make a test field translatable AND cardinality one. + $this->fields[0]['translatable'] = TRUE; + $this->fields[0]['cardinality'] = 1; + field_update_field($this->fields[0]); + field_test_entity_info_translatable('test_entity', TRUE); + + // Create more items with different languages. + $entity = new stdClass(); + $entity->ftid = 1; + $entity->ftvid = 1; + $entity->fttype = 'test_bundle'; + + // Set fields in two languages with one field value. + foreach (array(LANGUAGE_NONE, 'en') as $langcode) { + $entity->{$this->field_names[0]}[$langcode][0]['value'] = 1234; + } + + field_attach_update('test_entity', $entity); + + // Look up number of results when querying a single entity with multilingual + // field values. + $query = new EntityFieldQuery(); + $query_count = $query + ->entityCondition('entity_type', 'test_entity') + ->entityCondition('bundle', 'test_bundle') + ->entityCondition('entity_id', '1') + ->fieldCondition($this->fields[0]) + ->count() + ->execute(); + + $this->assertEqual($query_count, 1, "Count on translatable cardinality one field is correct."); + } + + /** + * Tests field meta conditions. + */ + function testEntityFieldQueryMetaConditions() { + // Make a test field translatable. + $this->fields[0]['translatable'] = TRUE; + field_update_field($this->fields[0]); + field_test_entity_info_translatable('test_entity', TRUE); + + // Create more items with different languages. + $entity = new stdClass(); + $entity->ftid = 1; + $entity->ftvid = 1; + $entity->fttype = 'test_bundle'; + $j = 0; + + foreach (array(LANGUAGE_NONE, 'en') as $langcode) { + for ($i = 0; $i < 4; $i++) { + $entity->{$this->field_names[0]}[$langcode][$i]['value'] = $i + $j; + } + $j += 4; + } + + field_attach_update('test_entity', $entity); + + // Test delta field meta condition. + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity', '=') + ->fieldDeltaCondition($this->fields[0], 0, '>'); + $this->assertEntityFieldQuery($query, array( + array('test_entity', 1), + ), 'Test with a delta meta condition.'); + + // Test language field meta condition. + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity', '=') + ->fieldLanguageCondition($this->fields[0], LANGUAGE_NONE, '<>'); + $this->assertEntityFieldQuery($query, array( + array('test_entity', 1), + ), 'Test with a language meta condition.'); + + // Test language field meta condition. + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity', '=') + ->fieldLanguageCondition($this->fields[0], LANGUAGE_NONE, '!='); + $this->assertEntityFieldQuery($query, array( + array('test_entity', 1), + ), 'Test with a language meta condition.'); + + // Test delta grouping. + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity', '=') + ->fieldCondition($this->fields[0], 'value', 0, '=', 'group') + ->fieldDeltaCondition($this->fields[0], 1, '<', 'group'); + $this->assertEntityFieldQuery($query, array( + array('test_entity', 1), + ), 'Test with a grouped delta meta condition.'); + + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity', '=') + ->fieldCondition($this->fields[0], 'value', 0, '=', 'group') + ->fieldDeltaCondition($this->fields[0], 1, '>=', 'group'); + $this->assertEntityFieldQuery($query, array(), 'Test with a grouped delta meta condition (empty result set).'); + + // Test language grouping. + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity', '=') + ->fieldCondition($this->fields[0], 'value', 0, '=', NULL, 'group') + ->fieldLanguageCondition($this->fields[0], 'en', '<>', NULL, 'group'); + $this->assertEntityFieldQuery($query, array( + array('test_entity', 1), + ), 'Test with a grouped language meta condition.'); + + // Test language grouping. + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity', '=') + ->fieldCondition($this->fields[0], 'value', 0, '=', NULL, 'group') + ->fieldLanguageCondition($this->fields[0], 'en', '!=', NULL, 'group'); + $this->assertEntityFieldQuery($query, array( + array('test_entity', 1), + ), 'Test with a grouped language meta condition.'); + + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity', '=') + ->fieldCondition($this->fields[0], 'value', 0, '=', NULL, 'group') + ->fieldLanguageCondition($this->fields[0], LANGUAGE_NONE, '<>', NULL, 'group'); + $this->assertEntityFieldQuery($query, array(), 'Test with a grouped language meta condition (empty result set).'); + + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity', '=') + ->fieldCondition($this->fields[0], 'value', 0, '=', NULL, 'group') + ->fieldLanguageCondition($this->fields[0], LANGUAGE_NONE, '!=', NULL, 'group'); + $this->assertEntityFieldQuery($query, array(), 'Test with a grouped language meta condition (empty result set).'); + + // Test delta and language grouping. + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity', '=') + ->fieldCondition($this->fields[0], 'value', 0, '=', 'delta', 'language') + ->fieldDeltaCondition($this->fields[0], 1, '<', 'delta', 'language') + ->fieldLanguageCondition($this->fields[0], 'en', '<>', 'delta', 'language'); + $this->assertEntityFieldQuery($query, array( + array('test_entity', 1), + ), 'Test with a grouped delta + language meta condition.'); + + // Test delta and language grouping. + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity', '=') + ->fieldCondition($this->fields[0], 'value', 0, '=', 'delta', 'language') + ->fieldDeltaCondition($this->fields[0], 1, '<', 'delta', 'language') + ->fieldLanguageCondition($this->fields[0], 'en', '!=', 'delta', 'language'); + $this->assertEntityFieldQuery($query, array( + array('test_entity', 1), + ), 'Test with a grouped delta + language meta condition.'); + + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity', '=') + ->fieldCondition($this->fields[0], 'value', 0, '=', 'delta', 'language') + ->fieldDeltaCondition($this->fields[0], 1, '>=', 'delta', 'language') + ->fieldLanguageCondition($this->fields[0], 'en', '<>', 'delta', 'language'); + $this->assertEntityFieldQuery($query, array(), 'Test with a grouped delta + language meta condition (empty result set, delta condition unsatisifed).'); + + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity', '=') + ->fieldCondition($this->fields[0], 'value', 0, '=', 'delta', 'language') + ->fieldDeltaCondition($this->fields[0], 1, '>=', 'delta', 'language') + ->fieldLanguageCondition($this->fields[0], 'en', '!=', 'delta', 'language'); + $this->assertEntityFieldQuery($query, array(), 'Test with a grouped delta + language meta condition (empty result set, delta condition unsatisifed).'); + + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity', '=') + ->fieldCondition($this->fields[0], 'value', 0, '=', 'delta', 'language') + ->fieldDeltaCondition($this->fields[0], 1, '<', 'delta', 'language') + ->fieldLanguageCondition($this->fields[0], LANGUAGE_NONE, '<>', 'delta', 'language'); + $this->assertEntityFieldQuery($query, array(), 'Test with a grouped delta + language meta condition (empty result set, language condition unsatisifed).'); + + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity', '=') + ->fieldCondition($this->fields[0], 'value', 0, '=', 'delta', 'language') + ->fieldDeltaCondition($this->fields[0], 1, '<', 'delta', 'language') + ->fieldLanguageCondition($this->fields[0], LANGUAGE_NONE, '!=', 'delta', 'language'); + $this->assertEntityFieldQuery($query, array(), 'Test with a grouped delta + language meta condition (empty result set, language condition unsatisifed).'); + + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity', '=') + ->fieldCondition($this->fields[0], 'value', 0, '=', 'delta', 'language') + ->fieldDeltaCondition($this->fields[0], 1, '>=', 'delta', 'language') + ->fieldLanguageCondition($this->fields[0], LANGUAGE_NONE, '<>', 'delta', 'language'); + $this->assertEntityFieldQuery($query, array(), 'Test with a grouped delta + language meta condition (empty result set, both conditions unsatisifed).'); + + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity', '=') + ->fieldCondition($this->fields[0], 'value', 0, '=', 'delta', 'language') + ->fieldDeltaCondition($this->fields[0], 1, '>=', 'delta', 'language') + ->fieldLanguageCondition($this->fields[0], LANGUAGE_NONE, '!=', 'delta', 'language'); + $this->assertEntityFieldQuery($query, array(), 'Test with a grouped delta + language meta condition (empty result set, both conditions unsatisifed).'); + + // Test grouping with another field to ensure that grouping cache is reset + // properly. + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity_bundle', '=') + ->fieldCondition($this->fields[1], 'shape', 'circle', '=', 'delta', 'language') + ->fieldCondition($this->fields[1], 'color', 'blue', '=', 'delta', 'language') + ->fieldDeltaCondition($this->fields[1], 1, '=', 'delta', 'language') + ->fieldLanguageCondition($this->fields[1], LANGUAGE_NONE, '=', 'delta', 'language'); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle', 5), + ), 'Test grouping cache.'); } /** @@ -1057,19 +1306,19 @@ // Entity-only query. $query = new EntityFieldQuery(); $query->entityCondition('entity_type', 'test_entity_bundle_key'); - $this->assertIdentical($query->queryCallback(), array($query, 'propertyQuery'), t('Entity-only queries are handled by the propertyQuery handler.')); + $this->assertIdentical($query->queryCallback(), array($query, 'propertyQuery'), 'Entity-only queries are handled by the propertyQuery handler.'); // Field-only query. $query = new EntityFieldQuery(); $query->fieldCondition($this->fields[0], 'value', '3'); - $this->assertIdentical($query->queryCallback(), 'field_sql_storage_field_storage_query', t('Pure field queries are handled by the Field storage handler.')); + $this->assertIdentical($query->queryCallback(), 'field_sql_storage_field_storage_query', 'Pure field queries are handled by the Field storage handler.'); // Mixed entity and field query. $query = new EntityFieldQuery(); $query ->entityCondition('entity_type', 'test_entity_bundle_key') ->fieldCondition($this->fields[0], 'value', '3'); - $this->assertIdentical($query->queryCallback(), 'field_sql_storage_field_storage_query', t('Mixed queries are handled by the Field storage handler.')); + $this->assertIdentical($query->queryCallback(), 'field_sql_storage_field_storage_query', 'Mixed queries are handled by the Field storage handler.'); // Overriding with $query->executeCallback. $query = new EntityFieldQuery(); @@ -1077,7 +1326,7 @@ $query->executeCallback = 'field_test_dummy_field_storage_query'; $this->assertEntityFieldQuery($query, array( array('user', 1), - ), t('executeCallback can override the query handler.')); + ), 'executeCallback can override the query handler.'); // Overriding with $query->executeCallback via hook_entity_query_alter(). $query = new EntityFieldQuery(); @@ -1086,7 +1335,7 @@ $query->alterMyExecuteCallbackPlease = TRUE; $this->assertEntityFieldQuery($query, array( array('user', 1), - ), t('executeCallback can override the query handler when set in a hook_entity_query_alter().')); + ), 'executeCallback can override the query handler when set in a hook_entity_query_alter().'); // Mixed-storage queries. $query = new EntityFieldQuery(); @@ -1101,7 +1350,7 @@ catch (EntityFieldQueryException $exception) { $pass = ($exception->getMessage() == t("Can't handle more than one field storage engine")); } - $this->assertTrue($pass, t('Cannot query across field storage engines.')); + $this->assertTrue($pass, 'Cannot query across field storage engines.'); } /** @@ -1119,7 +1368,7 @@ array('test_entity_bundle_key', 1), array('test_entity_bundle_key', 2), array('test_entity_bundle_key', 3), - ), t('Test pager integration in propertyQuery: page 1.'), TRUE); + ), 'Test pager integration in propertyQuery: page 1.', TRUE); $query = new EntityFieldQuery(); $query @@ -1130,7 +1379,7 @@ array('test_entity_bundle_key', 4), array('test_entity_bundle_key', 5), array('test_entity_bundle_key', 6), - ), t('Test pager integration in propertyQuery: page 2.'), TRUE); + ), 'Test pager integration in propertyQuery: page 2.', TRUE); // Test pager in field storage $_GET['page'] = '0,1'; @@ -1143,7 +1392,7 @@ $this->assertEntityFieldQuery($query, array( array('test_entity_bundle_key', 1), array('test_entity_bundle_key', 2), - ), t('Test pager integration in field storage: page 1.'), TRUE); + ), 'Test pager integration in field storage: page 1.', TRUE); $query = new EntityFieldQuery(); $query @@ -1154,12 +1403,33 @@ $this->assertEntityFieldQuery($query, array( array('test_entity_bundle_key', 3), array('test_entity_bundle_key', 4), - ), t('Test pager integration in field storage: page 2.'), TRUE); + ), 'Test pager integration in field storage: page 2.', TRUE); unset($_GET['page']); } /** + * Tests disabling the pager in EntityFieldQuery. + */ + function testEntityFieldQueryDisablePager() { + // Test enabling a pager and then disabling it. + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity_bundle_key') + ->propertyOrderBy('ftid', 'ASC') + ->pager(1) + ->pager(0); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 1), + array('test_entity_bundle_key', 2), + array('test_entity_bundle_key', 3), + array('test_entity_bundle_key', 4), + array('test_entity_bundle_key', 5), + array('test_entity_bundle_key', 6), + ), 'All test entities are listed when the pager is enabled and then disabled.', TRUE); + } + + /** * Tests the TableSort integration of EntityFieldQuery. */ function testEntityFieldQueryTableSort() { @@ -1181,7 +1451,7 @@ array('test_entity_bundle_key', 4), array('test_entity_bundle_key', 5), array('test_entity_bundle_key', 6), - ), t('Test TableSort by property: ftid ASC in propertyQuery.'), TRUE); + ), 'Test TableSort by property: ftid ASC in propertyQuery.', TRUE); $_GET['sort'] = 'desc'; $_GET['order'] = 'Id'; @@ -1196,7 +1466,7 @@ array('test_entity_bundle_key', 3), array('test_entity_bundle_key', 2), array('test_entity_bundle_key', 1), - ), t('Test TableSort by property: ftid DESC in propertyQuery.'), TRUE); + ), 'Test TableSort by property: ftid DESC in propertyQuery.', TRUE); $_GET['sort'] = 'asc'; $_GET['order'] = 'Type'; @@ -1211,7 +1481,7 @@ array('test_entity_bundle_key', 4), array('test_entity_bundle_key', 5), array('test_entity_bundle_key', 6), - ), t('Test TableSort by entity: bundle ASC in propertyQuery.'), TRUE); + ), 'Test TableSort by entity: bundle ASC in propertyQuery.', TRUE); $_GET['sort'] = 'desc'; $_GET['order'] = 'Type'; @@ -1226,7 +1496,7 @@ array('test_entity_bundle_key', 2), array('test_entity_bundle_key', 3), array('test_entity_bundle_key', 4), - ), t('Test TableSort by entity: bundle DESC in propertyQuery.'), TRUE); + ), 'Test TableSort by entity: bundle DESC in propertyQuery.', TRUE); // Test TableSort in field storage $_GET['sort'] = 'asc'; @@ -1248,7 +1518,7 @@ array('test_entity_bundle_key', 4), array('test_entity_bundle_key', 5), array('test_entity_bundle_key', 6), - ), t('Test TableSort by property: ftid ASC in field storage.'), TRUE); + ), 'Test TableSort by property: ftid ASC in field storage.', TRUE); $_GET['sort'] = 'desc'; $_GET['order'] = 'Id'; @@ -1264,7 +1534,7 @@ array('test_entity_bundle_key', 3), array('test_entity_bundle_key', 2), array('test_entity_bundle_key', 1), - ), t('Test TableSort by property: ftid DESC in field storage.'), TRUE); + ), 'Test TableSort by property: ftid DESC in field storage.', TRUE); $_GET['sort'] = 'asc'; $_GET['order'] = 'Type'; @@ -1281,7 +1551,7 @@ array('test_entity_bundle_key', 1), array('test_entity_bundle_key', 6), array('test_entity_bundle_key', 5), - ), t('Test TableSort by entity: bundle ASC in field storage.'), TRUE); + ), 'Test TableSort by entity: bundle ASC in field storage.', TRUE); $_GET['sort'] = 'desc'; $_GET['order'] = 'Type'; @@ -1298,7 +1568,7 @@ array('test_entity_bundle_key', 2), array('test_entity_bundle_key', 3), array('test_entity_bundle_key', 4), - ), t('Test TableSort by entity: bundle DESC in field storage.'), TRUE); + ), 'Test TableSort by entity: bundle DESC in field storage.', TRUE); $_GET['sort'] = 'asc'; $_GET['order'] = 'Field'; @@ -1314,7 +1584,7 @@ array('test_entity_bundle_key', 4), array('test_entity_bundle_key', 5), array('test_entity_bundle_key', 6), - ), t('Test TableSort by field ASC.'), TRUE); + ), 'Test TableSort by field ASC.', TRUE); $_GET['sort'] = 'desc'; $_GET['order'] = 'Field'; @@ -1330,13 +1600,33 @@ array('test_entity_bundle_key', 3), array('test_entity_bundle_key', 2), array('test_entity_bundle_key', 1), - ), t('Test TableSort by field DESC.'), TRUE); + ), 'Test TableSort by field DESC.', TRUE); unset($_GET['sort']); unset($_GET['order']); } /** + * Tests EntityFieldQuery access on non-node entities. + */ + function testEntityFieldQueryAccess() { + // Test as a user with ability to bypass node access. + $privileged_user = $this->drupalCreateUser(array('bypass node access', 'access content')); + $this->drupalLogin($privileged_user); + $this->drupalGet('entity-query-access/test/' . $this->fields[0]['field_name']); + $this->assertText('Found entity', 'Returned access response with entities.'); + $this->drupalLogout(); + + // Test as a user that does not have ability to bypass node access or view + // all nodes. + $regular_user = $this->drupalCreateUser(array('access content')); + $this->drupalLogin($regular_user); + $this->drupalGet('entity-query-access/test/' . $this->fields[0]['field_name']); + $this->assertText('Found entity', 'Returned access response with entities.'); + $this->drupalLogout(); + } + + /** * Fetches the results of an EntityFieldQuery and compares. * * @param $query @@ -1369,4 +1659,23 @@ $this->fail('Exception thrown: '. $e->getMessage()); } } + + /** + * Tests EFQ table prefixing with multiple conditions and an altered join. + * + * @see field_test_query_efq_table_prefixing_test_alter() + */ + function testTablePrefixing() { + $query = new EntityFieldQuery(); + $query = $query + ->entityCondition('entity_type', 'test_entity') + ->entityCondition('bundle', 'test_bundle') + ->entityCondition('entity_id', '1') + ->addTag('efq_table_prefixing_test'); + + $expected = array(array('test_entity', 1)); + + $this->assertEntityFieldQuery($query, $expected, 'An EntityFieldQuery returns the expected results when altered with an additional join on the base table.'); + } + } diff -Naur drupal-7.0/modules/simpletest/tests/entity_query_access_test.info drupal-7.66/modules/simpletest/tests/entity_query_access_test.info --- drupal-7.0/modules/simpletest/tests/entity_query_access_test.info 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/entity_query_access_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -0,0 +1,11 @@ +name = "Entity query access test" +description = "Support module for checking entity query results." +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.0/modules/simpletest/tests/entity_query_access_test.module drupal-7.66/modules/simpletest/tests/entity_query_access_test.module --- drupal-7.0/modules/simpletest/tests/entity_query_access_test.module 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/entity_query_access_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,54 @@ +<?php + +/** + * @file + * Helper module for testing EntityFieldQuery access on any type of entity. + */ + +/** + * Implements hook_menu(). + */ +function entity_query_access_test_menu() { + $items['entity-query-access/test/%'] = array( + 'title' => "Retrieve a sample of entity query access data", + 'page callback' => 'entity_query_access_test_sample_query', + 'page arguments' => array(2), + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); + + return $items; +} + +/** + * Returns the results from an example EntityFieldQuery. + */ +function entity_query_access_test_sample_query($field_name) { + global $user; + + // Simulate user does not have access to view all nodes. + $access = &drupal_static('node_access_view_all_nodes'); + $access[$user->uid] = FALSE; + + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity_bundle_key') + ->fieldCondition($field_name, 'value', 0, '>') + ->entityOrderBy('entity_id', 'ASC'); + $results = array( + 'items' => array(), + 'title' => t('EntityFieldQuery results'), + ); + foreach ($query->execute() as $entity_type => $entity_ids) { + foreach ($entity_ids as $entity_id => $entity_stub) { + $results['items'][] = format_string('Found entity of type @entity_type with id @entity_id', array('@entity_type' => $entity_type, '@entity_id' => $entity_id)); + } + } + if (count($results['items']) > 0) { + $output = theme('item_list', $results); + } + else { + $output = 'No results found with EntityFieldQuery.'; + } + return $output; +} diff -Naur drupal-7.0/modules/simpletest/tests/error.test drupal-7.66/modules/simpletest/tests/error.test --- drupal-7.0/modules/simpletest/tests/error.test 2010-10-16 02:00:16.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/error.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,10 +1,9 @@ <?php -// $Id: error.test,v 1.9 2010/10/16 00:00:16 webchick Exp $ /** * Tests Drupal error and exception handlers. */ -class DrupalErrorHandlerUnitTest extends DrupalWebTestCase { +class DrupalErrorHandlerTestCase extends DrupalWebTestCase { public static function getInfo() { return array( 'name' => 'Drupal error handlers', @@ -25,28 +24,25 @@ '%type' => 'Notice', '!message' => 'Undefined variable: bananas', '%function' => 'error_test_generate_warnings()', - '%line' => 44, '%file' => drupal_realpath('modules/simpletest/tests/error_test.module'), ); $error_warning = array( '%type' => 'Warning', '!message' => 'Division by zero', '%function' => 'error_test_generate_warnings()', - '%line' => 46, '%file' => drupal_realpath('modules/simpletest/tests/error_test.module'), ); $error_user_notice = array( '%type' => 'User warning', '!message' => 'Drupal is awesome', '%function' => 'error_test_generate_warnings()', - '%line' => 48, '%file' => drupal_realpath('modules/simpletest/tests/error_test.module'), ); // Set error reporting to collect notices. variable_set('error_level', ERROR_REPORTING_DISPLAY_ALL); $this->drupalGet('error-test/generate-warnings'); - $this->assertResponse(200, t('Received expected HTTP status code.')); + $this->assertResponse(200, 'Received expected HTTP status code.'); $this->assertErrorMessage($error_notice); $this->assertErrorMessage($error_warning); $this->assertErrorMessage($error_user_notice); @@ -54,7 +50,7 @@ // Set error reporting to not collect notices. variable_set('error_level', ERROR_REPORTING_DISPLAY_SOME); $this->drupalGet('error-test/generate-warnings'); - $this->assertResponse(200, t('Received expected HTTP status code.')); + $this->assertResponse(200, 'Received expected HTTP status code.'); $this->assertNoErrorMessage($error_notice); $this->assertErrorMessage($error_warning); $this->assertErrorMessage($error_user_notice); @@ -62,7 +58,7 @@ // Set error reporting to not show any errors. variable_set('error_level', ERROR_REPORTING_HIDE); $this->drupalGet('error-test/generate-warnings'); - $this->assertResponse(200, t('Received expected HTTP status code.')); + $this->assertResponse(200, 'Received expected HTTP status code.'); $this->assertNoErrorMessage($error_notice); $this->assertNoErrorMessage($error_warning); $this->assertNoErrorMessage($error_user_notice); @@ -88,33 +84,33 @@ ); $this->drupalGet('error-test/trigger-exception'); - $this->assertTrue(strpos($this->drupalGetHeader(':status'), '500 Service unavailable (with message)'), t('Received expected HTTP status line.')); + $this->assertTrue(strpos($this->drupalGetHeader(':status'), '500 Service unavailable (with message)'), 'Received expected HTTP status line.'); $this->assertErrorMessage($error_exception); $this->drupalGet('error-test/trigger-pdo-exception'); - $this->assertTrue(strpos($this->drupalGetHeader(':status'), '500 Service unavailable (with message)'), t('Received expected HTTP status line.')); + $this->assertTrue(strpos($this->drupalGetHeader(':status'), '500 Service unavailable (with message)'), 'Received expected HTTP status line.'); // We cannot use assertErrorMessage() since the extact error reported // varies from database to database. Check that the SQL string is displayed. - $this->assertText($error_pdo_exception['%type'], t('Found %type in error page.', $error_pdo_exception)); - $this->assertText($error_pdo_exception['!message'], t('Found !message in error page.', $error_pdo_exception)); - $error_details = t('in %function (line %line of %file)', $error_pdo_exception); - $this->assertRaw($error_details, t("Found '!message' in error page.", array('!message' => $error_details))); + $this->assertText($error_pdo_exception['%type'], format_string('Found %type in error page.', $error_pdo_exception)); + $this->assertText($error_pdo_exception['!message'], format_string('Found !message in error page.', $error_pdo_exception)); + $error_details = format_string('in %function (line ', $error_pdo_exception); + $this->assertRaw($error_details, format_string("Found '!message' in error page.", array('!message' => $error_details))); } /** * Helper function: assert that the error message is found. */ function assertErrorMessage(array $error) { - $message = t('%type: !message in %function (line %line of %file).', $error); - $this->assertRaw($message, t('Error !message found.', array('!message' => $message))); + $message = t('%type: !message in %function (line ', $error); + $this->assertRaw($message, format_string('Found error message: !message.', array('!message' => $message))); } /** * Helper function: assert that the error message is not found. */ function assertNoErrorMessage(array $error) { - $message = t('%type: !message in %function (line %line of %file).', $error); - $this->assertNoRaw($message, t('Error !message not found.', array('!message' => $message))); + $message = t('%type: !message in %function (line ', $error); + $this->assertNoRaw($message, format_string('Did not find error message: !message.', array('!message' => $message))); } } diff -Naur drupal-7.0/modules/simpletest/tests/error_test.info drupal-7.66/modules/simpletest/tests/error_test.info --- drupal-7.0/modules/simpletest/tests/error_test.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/error_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: error_test.info,v 1.2 2010/12/20 19:59:43 webchick Exp $ name = "Error test" description = "Support module for error and exception testing." package = Testing @@ -6,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/simpletest/tests/error_test.module drupal-7.66/modules/simpletest/tests/error_test.module --- drupal-7.0/modules/simpletest/tests/error_test.module 2009-12-04 17:49:47.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/error_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: error_test.module,v 1.5 2009/12/04 16:49:47 dries Exp $ /** * Implements hook_menu(). diff -Naur drupal-7.0/modules/simpletest/tests/file.test drupal-7.66/modules/simpletest/tests/file.test --- drupal-7.0/modules/simpletest/tests/file.test 2010-11-30 20:31:46.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/file.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: file.test,v 1.72 2010/11/30 19:31:46 dries Exp $ /** * @file @@ -58,13 +57,13 @@ * File object to compare. */ function assertFileUnchanged($before, $after) { - $this->assertEqual($before->fid, $after->fid, t('File id is the same: %file1 == %file2.', array('%file1' => $before->fid, '%file2' => $after->fid)), 'File unchanged'); - $this->assertEqual($before->uid, $after->uid, t('File owner is the same: %file1 == %file2.', array('%file1' => $before->uid, '%file2' => $after->uid)), 'File unchanged'); - $this->assertEqual($before->filename, $after->filename, t('File name is the same: %file1 == %file2.', array('%file1' => $before->filename, '%file2' => $after->filename)), 'File unchanged'); - $this->assertEqual($before->uri, $after->uri, t('File path is the same: %file1 == %file2.', array('%file1' => $before->uri, '%file2' => $after->uri)), 'File unchanged'); - $this->assertEqual($before->filemime, $after->filemime, t('File MIME type is the same: %file1 == %file2.', array('%file1' => $before->filemime, '%file2' => $after->filemime)), 'File unchanged'); - $this->assertEqual($before->filesize, $after->filesize, t('File size is the same: %file1 == %file2.', array('%file1' => $before->filesize, '%file2' => $after->filesize)), 'File unchanged'); - $this->assertEqual($before->status, $after->status, t('File status is the same: %file1 == %file2.', array('%file1' => $before->status, '%file2' => $after->status)), 'File unchanged'); + $this->assertEqual($before->fid, $after->fid, format_string('File id is the same: %file1 == %file2.', array('%file1' => $before->fid, '%file2' => $after->fid)), 'File unchanged'); + $this->assertEqual($before->uid, $after->uid, format_string('File owner is the same: %file1 == %file2.', array('%file1' => $before->uid, '%file2' => $after->uid)), 'File unchanged'); + $this->assertEqual($before->filename, $after->filename, format_string('File name is the same: %file1 == %file2.', array('%file1' => $before->filename, '%file2' => $after->filename)), 'File unchanged'); + $this->assertEqual($before->uri, $after->uri, format_string('File path is the same: %file1 == %file2.', array('%file1' => $before->uri, '%file2' => $after->uri)), 'File unchanged'); + $this->assertEqual($before->filemime, $after->filemime, format_string('File MIME type is the same: %file1 == %file2.', array('%file1' => $before->filemime, '%file2' => $after->filemime)), 'File unchanged'); + $this->assertEqual($before->filesize, $after->filesize, format_string('File size is the same: %file1 == %file2.', array('%file1' => $before->filesize, '%file2' => $after->filesize)), 'File unchanged'); + $this->assertEqual($before->status, $after->status, format_string('File status is the same: %file1 == %file2.', array('%file1' => $before->status, '%file2' => $after->status)), 'File unchanged'); } /** @@ -76,8 +75,8 @@ * File object to compare. */ function assertDifferentFile($file1, $file2) { - $this->assertNotEqual($file1->fid, $file2->fid, t('Files have different ids: %file1 != %file2.', array('%file1' => $file1->fid, '%file2' => $file2->fid)), 'Different file'); - $this->assertNotEqual($file1->uri, $file2->uri, t('Files have different paths: %file1 != %file2.', array('%file1' => $file1->uri, '%file2' => $file2->uri)), 'Different file'); + $this->assertNotEqual($file1->fid, $file2->fid, format_string('Files have different ids: %file1 != %file2.', array('%file1' => $file1->fid, '%file2' => $file2->fid)), 'Different file'); + $this->assertNotEqual($file1->uri, $file2->uri, format_string('Files have different paths: %file1 != %file2.', array('%file1' => $file1->uri, '%file2' => $file2->uri)), 'Different file'); } /** @@ -89,8 +88,8 @@ * File object to compare. */ function assertSameFile($file1, $file2) { - $this->assertEqual($file1->fid, $file2->fid, t('Files have the same ids: %file1 == %file2.', array('%file1' => $file1->fid, '%file2-fid' => $file2->fid)), 'Same file'); - $this->assertEqual($file1->uri, $file2->uri, t('Files have the same path: %file1 == %file2.', array('%file1' => $file1->uri, '%file2' => $file2->uri)), 'Same file'); + $this->assertEqual($file1->fid, $file2->fid, format_string('Files have the same ids: %file1 == %file2.', array('%file1' => $file1->fid, '%file2-fid' => $file2->fid)), 'Same file'); + $this->assertEqual($file1->uri, $file2->uri, format_string('Files have the same path: %file1 == %file2.', array('%file1' => $file1->uri, '%file2' => $file2->uri)), 'Same file'); } /** @@ -177,7 +176,7 @@ if (!isset($path)) { $path = file_default_scheme() . '://' . $this->randomName(); } - $this->assertTrue(drupal_mkdir($path) && is_dir($path), t('Directory was created successfully.')); + $this->assertTrue(drupal_mkdir($path) && is_dir($path), 'Directory was created successfully.'); return $path; } @@ -197,9 +196,14 @@ * @return * File object. */ - function createFile($filepath = NULL, $contents = NULL, $scheme = 'public') { + function createFile($filepath = NULL, $contents = NULL, $scheme = NULL) { if (!isset($filepath)) { - $filepath = $this->randomName(); + // Prefix with non-latin characters to ensure that all file-related + // tests work with international filenames. + $filepath = 'Файл для тестирования ' . $this->randomName(); + } + if (!isset($scheme)) { + $scheme = file_default_scheme(); } $filepath = $scheme . '://' . $filepath; @@ -208,11 +212,11 @@ } file_put_contents($filepath, $contents); - $this->assertTrue(is_file($filepath), t('The test file exists on the disk.'), 'Create test file'); + $this->assertTrue(is_file($filepath), 'The test file exists on the disk.', 'Create test file'); $file = new stdClass(); $file->uri = $filepath; - $file->filename = basename($file->uri); + $file->filename = drupal_basename($file->uri); $file->filemime = 'text/plain'; $file->uid = 1; $file->timestamp = REQUEST_TIME; @@ -220,7 +224,7 @@ $file->status = 0; // Write the record directly rather than calling file_save() so we don't // invoke the hooks. - $this->assertNotIdentical(drupal_write_record('file_managed', $file), FALSE, t('The file was added to the database.'), 'Create test file'); + $this->assertNotIdentical(drupal_write_record('file_managed', $file), FALSE, 'The file was added to the database.', 'Create test file'); return $file; } @@ -253,19 +257,19 @@ // Determine if there were any expected that were not called. $uncalled = array_diff($expected, $actual); if (count($uncalled)) { - $this->assertTrue(FALSE, t('Expected hooks %expected to be called but %uncalled was not called.', array('%expected' => implode(', ', $expected), '%uncalled' => implode(', ', $uncalled)))); + $this->assertTrue(FALSE, format_string('Expected hooks %expected to be called but %uncalled was not called.', array('%expected' => implode(', ', $expected), '%uncalled' => implode(', ', $uncalled)))); } else { - $this->assertTrue(TRUE, t('All the expected hooks were called: %expected', array('%expected' => empty($expected) ? t('(none)') : implode(', ', $expected)))); + $this->assertTrue(TRUE, format_string('All the expected hooks were called: %expected', array('%expected' => empty($expected) ? t('(none)') : implode(', ', $expected)))); } // Determine if there were any unexpected calls. $unexpected = array_diff($actual, $expected); if (count($unexpected)) { - $this->assertTrue(FALSE, t('Unexpected hooks were called: %unexpected.', array('%unexpected' => empty($unexpected) ? t('(none)') : implode(', ', $unexpected)))); + $this->assertTrue(FALSE, format_string('Unexpected hooks were called: %unexpected.', array('%unexpected' => empty($unexpected) ? t('(none)') : implode(', ', $unexpected)))); } else { - $this->assertTrue(TRUE, t('No unexpected hooks were called.')); + $this->assertTrue(TRUE, 'No unexpected hooks were called.'); } } @@ -284,13 +288,13 @@ if (!isset($message)) { if ($actual_count == $expected_count) { - $message = t('hook_file_@name was called correctly.', array('@name' => $hook)); + $message = format_string('hook_file_@name was called correctly.', array('@name' => $hook)); } elseif ($expected_count == 0) { $message = format_plural($actual_count, 'hook_file_@name was not expected to be called but was actually called once.', 'hook_file_@name was not expected to be called but was actually called @count times.', array('@name' => $hook, '@count' => $actual_count)); } else { - $message = t('hook_file_@name was expected to be called %expected times but was called %actual times.', array('@name' => $hook, '%expected' => $expected_count, '%actual' => $actual_count)); + $message = format_string('hook_file_@name was expected to be called %expected times but was called %actual times.', array('@name' => $hook, '%expected' => $expected_count, '%actual' => $actual_count)); } } $this->assertEqual($actual_count, $expected_count, $message); @@ -370,11 +374,11 @@ $this->image = new stdClass(); $this->image->uri = 'misc/druplicon.png'; - $this->image->filename = basename($this->image->uri); + $this->image->filename = drupal_basename($this->image->uri); $this->non_image = new stdClass(); $this->non_image->uri = 'misc/jquery.js'; - $this->non_image->filename = basename($this->non_image->uri); + $this->non_image->filename = drupal_basename($this->non_image->uri); } /** @@ -384,24 +388,24 @@ $file = new stdClass(); $file->filename = 'asdf.txt'; $errors = file_validate_extensions($file, 'asdf txt pork'); - $this->assertEqual(count($errors), 0, t('Valid extension accepted.'), 'File'); + $this->assertEqual(count($errors), 0, 'Valid extension accepted.', 'File'); $file->filename = 'asdf.txt'; $errors = file_validate_extensions($file, 'exe png'); - $this->assertEqual(count($errors), 1, t('Invalid extension blocked.'), 'File'); + $this->assertEqual(count($errors), 1, 'Invalid extension blocked.', 'File'); } /** * This ensures a specific file is actually an image. */ function testFileValidateIsImage() { - $this->assertTrue(file_exists($this->image->uri), t('The image being tested exists.'), 'File'); + $this->assertTrue(file_exists($this->image->uri), 'The image being tested exists.', 'File'); $errors = file_validate_is_image($this->image); - $this->assertEqual(count($errors), 0, t('No error reported for our image file.'), 'File'); + $this->assertEqual(count($errors), 0, 'No error reported for our image file.', 'File'); - $this->assertTrue(file_exists($this->non_image->uri), t('The non-image being tested exists.'), 'File'); + $this->assertTrue(file_exists($this->non_image->uri), 'The non-image being tested exists.', 'File'); $errors = file_validate_is_image($this->non_image); - $this->assertEqual(count($errors), 1, t('An error reported for our non-image file.'), 'File'); + $this->assertEqual(count($errors), 1, 'An error reported for our non-image file.', 'File'); } /** @@ -411,39 +415,39 @@ function testFileValidateImageResolution() { // Non-images. $errors = file_validate_image_resolution($this->non_image); - $this->assertEqual(count($errors), 0, t("Shouldn't get any errors for a non-image file."), 'File'); + $this->assertEqual(count($errors), 0, 'Should not get any errors for a non-image file.', 'File'); $errors = file_validate_image_resolution($this->non_image, '50x50', '100x100'); - $this->assertEqual(count($errors), 0, t("Don't check the resolution on non files."), 'File'); + $this->assertEqual(count($errors), 0, 'Do not check the resolution on non files.', 'File'); // Minimum size. $errors = file_validate_image_resolution($this->image); - $this->assertEqual(count($errors), 0, t('No errors for an image when there is no minimum or maximum resolution.'), 'File'); + $this->assertEqual(count($errors), 0, 'No errors for an image when there is no minimum or maximum resolution.', 'File'); $errors = file_validate_image_resolution($this->image, 0, '200x1'); - $this->assertEqual(count($errors), 1, t("Got an error for an image that wasn't wide enough."), 'File'); + $this->assertEqual(count($errors), 1, 'Got an error for an image that was not wide enough.', 'File'); $errors = file_validate_image_resolution($this->image, 0, '1x200'); - $this->assertEqual(count($errors), 1, t("Got an error for an image that wasn't tall enough."), 'File'); + $this->assertEqual(count($errors), 1, 'Got an error for an image that was not tall enough.', 'File'); $errors = file_validate_image_resolution($this->image, 0, '200x200'); - $this->assertEqual(count($errors), 1, t('Small images report an error.'), 'File'); + $this->assertEqual(count($errors), 1, 'Small images report an error.', 'File'); // Maximum size. if (image_get_toolkit()) { // Copy the image so that the original doesn't get resized. - copy(drupal_realpath('misc/druplicon.png'), 'temporary://druplicon.png'); + copy('misc/druplicon.png', 'temporary://druplicon.png'); $this->image->uri = 'temporary://druplicon.png'; $errors = file_validate_image_resolution($this->image, '10x5'); - $this->assertEqual(count($errors), 0, t('No errors should be reported when an oversized image can be scaled down.'), 'File'); + $this->assertEqual(count($errors), 0, 'No errors should be reported when an oversized image can be scaled down.', 'File'); $info = image_get_info($this->image->uri); - $this->assertTrue($info['width'] <= 10, t('Image scaled to correct width.'), 'File'); - $this->assertTrue($info['height'] <= 5, t('Image scaled to correct height.'), 'File'); + $this->assertTrue($info['width'] <= 10, 'Image scaled to correct width.', 'File'); + $this->assertTrue($info['height'] <= 5, 'Image scaled to correct height.', 'File'); - drupal_unlink(drupal_realpath('temporary://druplicon.png')); + drupal_unlink('temporary://druplicon.png'); } else { // TODO: should check that the error is returned if no toolkit is available. $errors = file_validate_image_resolution($this->image, '5x10'); - $this->assertEqual(count($errors), 1, t("Oversize images that can't be scaled get an error."), 'File'); + $this->assertEqual(count($errors), 1, 'Oversize images that cannot be scaled get an error.', 'File'); } } @@ -458,17 +462,17 @@ $file->filename = str_repeat('x', 240); $this->assertEqual(strlen($file->filename), 240); $errors = file_validate_name_length($file); - $this->assertEqual(count($errors), 0, t('No errors reported for 240 length filename.'), 'File'); + $this->assertEqual(count($errors), 0, 'No errors reported for 240 length filename.', 'File'); // Add a filename with a length too long and test it. $file->filename = str_repeat('x', 241); $errors = file_validate_name_length($file); - $this->assertEqual(count($errors), 1, t('An error reported for 241 length filename.'), 'File'); + $this->assertEqual(count($errors), 1, 'An error reported for 241 length filename.', 'File'); // Add a filename with an empty string and test it. $file->filename = ''; $errors = file_validate_name_length($file); - $this->assertEqual(count($errors), 1, t('An error reported for 0 length filename.'), 'File'); + $this->assertEqual(count($errors), 1, 'An error reported for 0 length filename.', 'File'); } @@ -476,35 +480,17 @@ * 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, t('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; $errors = file_validate_size($file, 0, 0); - $this->assertEqual(count($errors), 0, t('No limits means no errors.'), 'File'); + $this->assertEqual(count($errors), 0, 'No limits means no errors.', 'File'); $errors = file_validate_size($file, 1, 0); - $this->assertEqual(count($errors), 1, t('Error for the file being over the limit.'), 'File'); + $this->assertEqual(count($errors), 1, 'Error for the file being over the limit.', 'File'); $errors = file_validate_size($file, 0, 1); - $this->assertEqual(count($errors), 1, t('Error for the user being over their limit.'), 'File'); + $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, t('Errors for both the file and their limit.'), 'File'); - - $user = $original_user; - drupal_save_session(TRUE); + $this->assertEqual(count($errors), 2, 'Errors for both the file and their limit.', 'File'); } } @@ -530,20 +516,36 @@ // No filename. $filepath = file_unmanaged_save_data($contents); - $this->assertTrue($filepath, t('Unnamed file saved correctly.')); - $this->assertEqual(file_uri_scheme($filepath), file_default_scheme(), t("File was placed in Drupal's files directory.")); - $this->assertEqual($contents, file_get_contents(drupal_realpath($filepath)), t('Contents of the file are correct.')); + $this->assertTrue($filepath, 'Unnamed file saved correctly.'); + $this->assertEqual(file_uri_scheme($filepath), file_default_scheme(), "File was placed in Drupal's files directory."); + $this->assertEqual($contents, file_get_contents($filepath), 'Contents of the file are correct.'); // Provide a filename. $filepath = file_unmanaged_save_data($contents, 'public://asdf.txt', FILE_EXISTS_REPLACE); - $this->assertTrue($filepath, t('Unnamed file saved correctly.')); - $this->assertEqual('asdf.txt', basename($filepath), t('File was named correctly.')); - $this->assertEqual($contents, file_get_contents(drupal_realpath($filepath)), t('Contents of the file are correct.')); + $this->assertTrue($filepath, 'Unnamed file saved correctly.'); + $this->assertEqual('asdf.txt', drupal_basename($filepath), 'File was named correctly.'); + $this->assertEqual($contents, file_get_contents($filepath), 'Contents of the file are correct.'); $this->assertFilePermissions($filepath, variable_get('file_chmod_file', 0664)); } } /** + * Tests the file_unmanaged_save_data() function on remote filesystems. + */ +class RemoteFileUnmanagedSaveDataTest extends FileUnmanagedSaveDataTest { + public static function getInfo() { + $info = parent::getInfo(); + $info['group'] = 'File API (remote)'; + return $info; + } + + function setUp() { + parent::setUp('file_test'); + variable_set('file_default_scheme', 'dummy-remote'); + } +} + +/** * Test the file_save_upload() function. */ class FileSaveUploadTest extends FileHookTestCase { @@ -579,10 +581,10 @@ $this->image = current($image_files); list(, $this->image_extension) = explode('.', $this->image->filename); - $this->assertTrue(is_file($this->image->uri), t("The image file we're going to upload exists.")); + $this->assertTrue(is_file($this->image->uri), "The image file we're going to upload exists."); $this->phpfile = current($this->drupalGetTestFiles('php')); - $this->assertTrue(is_file($this->phpfile->uri), t("The PHP file we're going to upload exists.")); + $this->assertTrue(is_file($this->phpfile->uri), "The PHP file we're going to upload exists."); $this->maxFidBefore = db_query('SELECT MAX(fid) AS fid FROM {file_managed}')->fetchField(); @@ -592,8 +594,8 @@ 'files[file_test_upload]' => drupal_realpath($this->image->uri), ); $this->drupalPost('file-test/upload', $edit, t('Submit')); - $this->assertResponse(200, t('Received a 200 response for posted test file.')); - $this->assertRaw(t('You WIN!'), t('Found the success message.')); + $this->assertResponse(200, 'Received a 200 response for posted test file.'); + $this->assertRaw(t('You WIN!'), 'Found the success message.'); // Check that the correct hooks were called then clean out the hook // counters. @@ -606,9 +608,9 @@ */ function testNormal() { $max_fid_after = db_query('SELECT MAX(fid) AS fid FROM {file_managed}')->fetchField(); - $this->assertTrue($max_fid_after > $this->maxFidBefore, t('A new file was created.')); + $this->assertTrue($max_fid_after > $this->maxFidBefore, 'A new file was created.'); $file1 = file_load($max_fid_after); - $this->assertTrue($file1, t('Loaded the file.')); + $this->assertTrue($file1, 'Loaded the file.'); // MIME type of the uploaded image may be either image/jpeg or image/png. $this->assertEqual(substr($file1->filemime, 0, 5), 'image', 'A MIME type was set.'); @@ -620,7 +622,7 @@ $image2 = current($this->drupalGetTestFiles('image')); $edit = array('files[file_test_upload]' => drupal_realpath($image2->uri)); $this->drupalPost('file-test/upload', $edit, t('Submit')); - $this->assertResponse(200, t('Received a 200 response for posted test file.')); + $this->assertResponse(200, 'Received a 200 response for posted test file.'); $this->assertRaw(t('You WIN!')); $max_fid_after = db_query('SELECT MAX(fid) AS fid FROM {file_managed}')->fetchField(); @@ -634,8 +636,8 @@ // Load both files using file_load_multiple(). $files = file_load_multiple(array($file1->fid, $file2->fid)); - $this->assertTrue(isset($files[$file1->fid]), t('File was loaded successfully')); - $this->assertTrue(isset($files[$file2->fid]), t('File was loaded successfully')); + $this->assertTrue(isset($files[$file1->fid]), 'File was loaded successfully'); + $this->assertTrue(isset($files[$file2->fid]), 'File was loaded successfully'); // Upload a third file to a subdirectory. $image3 = current($this->drupalGetTestFiles('image')); @@ -646,12 +648,12 @@ 'file_subdir' => $dir, ); $this->drupalPost('file-test/upload', $edit, t('Submit')); - $this->assertResponse(200, t('Received a 200 response for posted test file.')); + $this->assertResponse(200, 'Received a 200 response for posted test file.'); $this->assertRaw(t('You WIN!')); - $this->assertTrue(is_file('temporary://' . $dir . '/' . trim(basename($image3_realpath)))); + $this->assertTrue(is_file('temporary://' . $dir . '/' . trim(drupal_basename($image3_realpath)))); // Check that file_load_multiple() with no arguments returns FALSE. - $this->assertFalse(file_load_multiple(), t('No files were loaded.')); + $this->assertFalse(file_load_multiple(), 'No files were loaded.'); } /** @@ -670,10 +672,10 @@ ); $this->drupalPost('file-test/upload', $edit, t('Submit')); - $this->assertResponse(200, t('Received a 200 response for posted test file.')); - $message = t('Only files with the following extensions are allowed: ') . '<em class="placeholder">' . $extensions . '</em>'; - $this->assertRaw($message, t('Can\'t upload a disallowed extension')); - $this->assertRaw(t('Epic upload FAIL!'), t('Found the failure message.')); + $this->assertResponse(200, 'Received a 200 response for posted test file.'); + $message = t('Only files with the following extensions are allowed:') . ' <em class="placeholder">' . $extensions . '</em>'; + $this->assertRaw($message, 'Cannot upload a disallowed extension'); + $this->assertRaw(t('Epic upload FAIL!'), 'Found the failure message.'); // Check that the correct hooks were called. $this->assertFileHooksCalled(array('validate')); @@ -690,9 +692,9 @@ ); $this->drupalPost('file-test/upload', $edit, t('Submit')); - $this->assertResponse(200, t('Received a 200 response for posted test file.')); - $this->assertNoRaw(t('Only files with the following extensions are allowed:'), t('Can upload an allowed extension.')); - $this->assertRaw(t('You WIN!'), t('Found the success message.')); + $this->assertResponse(200, 'Received a 200 response for posted test file.'); + $this->assertNoRaw(t('Only files with the following extensions are allowed:'), 'Can upload an allowed extension.'); + $this->assertRaw(t('You WIN!'), 'Found the success message.'); // Check that the correct hooks were called. $this->assertFileHooksCalled(array('validate', 'load', 'update')); @@ -707,9 +709,9 @@ 'allow_all_extensions' => TRUE, ); $this->drupalPost('file-test/upload', $edit, t('Submit')); - $this->assertResponse(200, t('Received a 200 response for posted test file.')); - $this->assertNoRaw(t('Only files with the following extensions are allowed:'), t('Can upload any extension.')); - $this->assertRaw(t('You WIN!'), t('Found the success message.')); + $this->assertResponse(200, 'Received a 200 response for posted test file.'); + $this->assertNoRaw(t('Only files with the following extensions are allowed:'), 'Can upload any extension.'); + $this->assertRaw(t('You WIN!'), 'Found the success message.'); // Check that the correct hooks were called. $this->assertFileHooksCalled(array('validate', 'load', 'update')); @@ -729,11 +731,11 @@ ); $this->drupalPost('file-test/upload', $edit, t('Submit')); - $this->assertResponse(200, t('Received a 200 response for posted test file.')); - $message = t('For security reasons, your upload has been renamed to ') . '<em class="placeholder">' . $this->phpfile->filename . '.txt' . '</em>'; - $this->assertRaw($message, t('Dangerous file was renamed.')); - $this->assertRaw(t('File MIME type is text/plain.'), t('Dangerous file\'s MIME type was changed.')); - $this->assertRaw(t('You WIN!'), t('Found the success message.')); + $this->assertResponse(200, 'Received a 200 response for posted test file.'); + $message = t('For security reasons, your upload has been renamed to') . ' <em class="placeholder">' . $this->phpfile->filename . '.txt' . '</em>'; + $this->assertRaw($message, 'Dangerous file was renamed.'); + $this->assertRaw(t('File MIME type is text/plain.'), "Dangerous file's MIME type was changed."); + $this->assertRaw(t('You WIN!'), 'Found the success message.'); // Check that the correct hooks were called. $this->assertFileHooksCalled(array('validate', 'insert')); @@ -745,10 +747,10 @@ file_test_reset(); $this->drupalPost('file-test/upload', $edit, t('Submit')); - $this->assertResponse(200, t('Received a 200 response for posted test file.')); - $this->assertNoRaw(t('For security reasons, your upload has been renamed'), t('Found no security message.')); - $this->assertRaw(t('File name is !filename', array('!filename' => $this->phpfile->filename)), t('Dangerous file was not renamed when insecure uploads is TRUE.')); - $this->assertRaw(t('You WIN!'), t('Found the success message.')); + $this->assertResponse(200, 'Received a 200 response for posted test file.'); + $this->assertNoRaw(t('For security reasons, your upload has been renamed'), 'Found no security message.'); + $this->assertRaw(t('File name is !filename', array('!filename' => $this->phpfile->filename)), 'Dangerous file was not renamed when insecure uploads is TRUE.'); + $this->assertRaw(t('You WIN!'), 'Found the success message.'); // Check that the correct hooks were called. $this->assertFileHooksCalled(array('validate', 'insert')); @@ -779,10 +781,10 @@ $munged_filename .= '_.' . $this->image_extension; $this->drupalPost('file-test/upload', $edit, t('Submit')); - $this->assertResponse(200, t('Received a 200 response for posted test file.')); - $this->assertRaw(t('For security reasons, your upload has been renamed'), t('Found security message.')); - $this->assertRaw(t('File name is !filename', array('!filename' => $munged_filename)), t('File was successfully munged.')); - $this->assertRaw(t('You WIN!'), t('Found the success message.')); + $this->assertResponse(200, 'Received a 200 response for posted test file.'); + $this->assertRaw(t('For security reasons, your upload has been renamed'), 'Found security message.'); + $this->assertRaw(t('File name is !filename', array('!filename' => $munged_filename)), 'File was successfully munged.'); + $this->assertRaw(t('You WIN!'), 'Found the success message.'); // Check that the correct hooks were called. $this->assertFileHooksCalled(array('validate', 'insert')); @@ -797,10 +799,10 @@ ); $this->drupalPost('file-test/upload', $edit, t('Submit')); - $this->assertResponse(200, t('Received a 200 response for posted test file.')); - $this->assertNoRaw(t('For security reasons, your upload has been renamed'), t('Found no security message.')); - $this->assertRaw(t('File name is !filename', array('!filename' => $this->image->filename)), t('File was not munged when allowing any extension.')); - $this->assertRaw(t('You WIN!'), t('Found the success message.')); + $this->assertResponse(200, 'Received a 200 response for posted test file.'); + $this->assertNoRaw(t('For security reasons, your upload has been renamed'), 'Found no security message.'); + $this->assertRaw(t('File name is !filename', array('!filename' => $this->image->filename)), 'File was not munged when allowing any extension.'); + $this->assertRaw(t('You WIN!'), 'Found the success message.'); // Check that the correct hooks were called. $this->assertFileHooksCalled(array('validate', 'insert')); @@ -815,8 +817,8 @@ 'files[file_test_upload]' => drupal_realpath($this->image->uri) ); $this->drupalPost('file-test/upload', $edit, t('Submit')); - $this->assertResponse(200, t('Received a 200 response for posted test file.')); - $this->assertRaw(t('You WIN!'), t('Found the success message.')); + $this->assertResponse(200, 'Received a 200 response for posted test file.'); + $this->assertRaw(t('You WIN!'), 'Found the success message.'); // Check that the correct hooks were called. $this->assertFileHooksCalled(array('validate', 'insert')); @@ -831,8 +833,8 @@ 'files[file_test_upload]' => drupal_realpath($this->image->uri) ); $this->drupalPost('file-test/upload', $edit, t('Submit')); - $this->assertResponse(200, t('Received a 200 response for posted test file.')); - $this->assertRaw(t('You WIN!'), t('Found the success message.')); + $this->assertResponse(200, 'Received a 200 response for posted test file.'); + $this->assertRaw(t('You WIN!'), 'Found the success message.'); // Check that the correct hooks were called. $this->assertFileHooksCalled(array('validate', 'load', 'update')); @@ -847,8 +849,8 @@ 'files[file_test_upload]' => drupal_realpath($this->image->uri) ); $this->drupalPost('file-test/upload', $edit, t('Submit')); - $this->assertResponse(200, t('Received a 200 response for posted test file.')); - $this->assertRaw(t('Epic upload FAIL!'), t('Found the failure message.')); + $this->assertResponse(200, 'Received a 200 response for posted test file.'); + $this->assertRaw(t('Epic upload FAIL!'), 'Found the failure message.'); // Check that the no hooks were called while failing. $this->assertFileHooksCalled(array()); @@ -859,7 +861,23 @@ */ function testNoUpload() { $this->drupalPost('file-test/upload', array(), t('Submit')); - $this->assertNoRaw(t('Epic upload FAIL!'), t('Failure message not found.')); + $this->assertNoRaw(t('Epic upload FAIL!'), 'Failure message not found.'); + } +} + +/** + * Test the file_save_upload() function on remote filesystems. + */ +class RemoteFileSaveUploadTest extends FileSaveUploadTest { + public static function getInfo() { + $info = parent::getInfo(); + $info['group'] = 'File API (remote)'; + return $info; + } + + function setUp() { + parent::setUp('file_test'); + variable_set('file_default_scheme', 'dummy-remote'); } } @@ -880,17 +898,17 @@ */ function testFileCheckDirectoryHandling() { // A directory to operate on. - $directory = file_stream_wrapper_get_instance_by_scheme(file_default_scheme())->getDirectoryPath() . '/' . $this->randomName() . '/' . $this->randomName(); - $this->assertFalse(is_dir($directory), t('Directory does not exist prior to testing.')); + $directory = file_default_scheme() . '://' . $this->randomName() . '/' . $this->randomName(); + $this->assertFalse(is_dir($directory), 'Directory does not exist prior to testing.'); // Non-existent directory. - $this->assertFalse(file_prepare_directory($directory, 0), t('Error reported for non-existing directory.'), 'File'); + $this->assertFalse(file_prepare_directory($directory, 0), 'Error reported for non-existing directory.', 'File'); // Make a directory. - $this->assertTrue(file_prepare_directory($directory, FILE_CREATE_DIRECTORY), t('No error reported when creating a new directory.'), 'File'); + $this->assertTrue(file_prepare_directory($directory, FILE_CREATE_DIRECTORY), 'No error reported when creating a new directory.', 'File'); // Make sure directory actually exists. - $this->assertTrue(is_dir($directory), t('Directory actually exists.'), 'File'); + $this->assertTrue(is_dir($directory), 'Directory actually exists.', 'File'); if (substr(PHP_OS, 0, 3) != 'WIN') { // PHP on Windows doesn't support any kind of useful read-only mode for @@ -900,10 +918,10 @@ // Make directory read only. @drupal_chmod($directory, 0444); - $this->assertFalse(file_prepare_directory($directory, 0), t('Error reported for a non-writeable directory.'), 'File'); + $this->assertFalse(file_prepare_directory($directory, 0), 'Error reported for a non-writeable directory.', 'File'); // Test directory permission modification. - $this->assertTrue(file_prepare_directory($directory, FILE_MODIFY_PERMISSIONS), t('No error reported when making directory writeable.'), 'File'); + $this->assertTrue(file_prepare_directory($directory, FILE_MODIFY_PERMISSIONS), 'No error reported when making directory writeable.', 'File'); } // Test that the directory has the correct permissions. @@ -911,12 +929,12 @@ // Remove .htaccess file to then test that it gets re-created. @drupal_unlink(file_default_scheme() . '://.htaccess'); - $this->assertFalse(is_file(file_default_scheme() . '://.htaccess'), t('Successfully removed the .htaccess file in the files directory.'), 'File'); + $this->assertFalse(is_file(file_default_scheme() . '://.htaccess'), 'Successfully removed the .htaccess file in the files directory.', 'File'); file_ensure_htaccess(); - $this->assertTrue(is_file(file_default_scheme() . '://.htaccess'), t('Successfully re-created the .htaccess file in the files directory.'), 'File'); + $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", t('The .htaccess file contains the proper content.'), 'File'); + $this->assertEqual($file, file_htaccess_lines(FALSE), 'The .htaccess file contains the proper content.', 'File'); } /** @@ -930,14 +948,23 @@ $directory = 'misc'; $original = $directory . '/' . $basename; $path = file_create_filename($basename, $directory); - $this->assertEqual($path, $original, t('New filepath %new equals %original.', array('%new' => $path, '%original' => $original)), 'File'); + $this->assertEqual($path, $original, format_string('New filepath %new equals %original.', array('%new' => $path, '%original' => $original)), 'File'); // Then we test against a file that already exists within that directory. $basename = 'druplicon.png'; $original = $directory . '/' . $basename; $expected = $directory . '/druplicon_0.png'; $path = file_create_filename($basename, $directory); - $this->assertEqual($path, $expected, t('Creating a new filepath from %original equals %new.', array('%new' => $path, '%original' => $original)), 'File'); + $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. } @@ -958,19 +985,27 @@ // First test for non-existent file. $destination = 'misc/xyz.txt'; $path = file_destination($destination, FILE_EXISTS_REPLACE); - $this->assertEqual($path, $destination, t('Non-existing filepath destination is correct with FILE_EXISTS_REPLACE.'), 'File'); + $this->assertEqual($path, $destination, 'Non-existing filepath destination is correct with FILE_EXISTS_REPLACE.', 'File'); $path = file_destination($destination, FILE_EXISTS_RENAME); - $this->assertEqual($path, $destination, t('Non-existing filepath destination is correct with FILE_EXISTS_RENAME.'), 'File'); + $this->assertEqual($path, $destination, 'Non-existing filepath destination is correct with FILE_EXISTS_RENAME.', 'File'); $path = file_destination($destination, FILE_EXISTS_ERROR); - $this->assertEqual($path, $destination, t('Non-existing filepath destination is correct with FILE_EXISTS_ERROR.'), 'File'); + $this->assertEqual($path, $destination, 'Non-existing filepath destination is correct with FILE_EXISTS_ERROR.', 'File'); $destination = 'misc/druplicon.png'; $path = file_destination($destination, FILE_EXISTS_REPLACE); - $this->assertEqual($path, $destination, t('Existing filepath destination remains the same with FILE_EXISTS_REPLACE.'), 'File'); + $this->assertEqual($path, $destination, 'Existing filepath destination remains the same with FILE_EXISTS_REPLACE.', 'File'); $path = file_destination($destination, FILE_EXISTS_RENAME); - $this->assertNotEqual($path, $destination, t('A new filepath destination is created when filepath destination already exists with FILE_EXISTS_RENAME.'), 'File'); + $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, t('An error is returned when filepath destination already exists with FILE_EXISTS_ERROR.'), 'File'); + $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()); + } } /** @@ -980,9 +1015,25 @@ // Start with an empty variable to ensure we have a clean slate. variable_set('file_temporary_path', ''); $tmp_directory = file_directory_temp(); - $this->assertEqual(empty($tmp_directory), FALSE, t('file_directory_temp() returned a non-empty value.')); + $this->assertEqual(empty($tmp_directory), FALSE, 'file_directory_temp() returned a non-empty value.'); $setting = variable_get('file_temporary_path', ''); - $this->assertEqual($setting, $tmp_directory, t("The 'file_temporary_path' variable has the same value that file_directory_temp() returned.")); + $this->assertEqual($setting, $tmp_directory, "The 'file_temporary_path' variable has the same value that file_directory_temp() returned."); + } +} + +/** + * Directory related tests. + */ +class RemoteFileDirectoryTest extends FileDirectoryTest { + public static function getInfo() { + $info = parent::getInfo(); + $info['group'] = 'File API (remote)'; + return $info; + } + + function setUp() { + parent::setUp('file_test'); + variable_set('file_default_scheme', 'dummy-remote'); } } @@ -1011,21 +1062,21 @@ // passed to the callback. $all_files = file_scan_directory($this->path, '/^javascript-/'); ksort($all_files); - $this->assertEqual(2, count($all_files), t('Found two, expected javascript files.')); + $this->assertEqual(2, count($all_files), 'Found two, expected javascript files.'); // Check the first file. $file = reset($all_files); - $this->assertEqual(key($all_files), $file->uri, t('Correct array key was used for the first returned file.')); - $this->assertEqual($file->uri, $this->path . '/javascript-1.txt', t('First file name was set correctly.')); - $this->assertEqual($file->filename, 'javascript-1.txt', t('First basename was set correctly')); - $this->assertEqual($file->name, 'javascript-1', t('First name was set correctly.')); + $this->assertEqual(key($all_files), $file->uri, 'Correct array key was used for the first returned file.'); + $this->assertEqual($file->uri, $this->path . '/javascript-1.txt', 'First file name was set correctly.'); + $this->assertEqual($file->filename, 'javascript-1.txt', 'First basename was set correctly'); + $this->assertEqual($file->name, 'javascript-1', 'First name was set correctly.'); // Check the second file. $file = next($all_files); - $this->assertEqual(key($all_files), $file->uri, t('Correct array key was used for the second returned file.')); - $this->assertEqual($file->uri, $this->path . '/javascript-2.script', t('Second file name was set correctly.')); - $this->assertEqual($file->filename, 'javascript-2.script', t('Second basename was set correctly')); - $this->assertEqual($file->name, 'javascript-2', t('Second name was set correctly.')); + $this->assertEqual(key($all_files), $file->uri, 'Correct array key was used for the second returned file.'); + $this->assertEqual($file->uri, $this->path . '/javascript-2.script', 'Second file name was set correctly.'); + $this->assertEqual($file->filename, 'javascript-2.script', 'Second basename was set correctly'); + $this->assertEqual($file->name, 'javascript-2', 'Second name was set correctly.'); } /** @@ -1034,18 +1085,18 @@ function testOptionCallback() { // When nothing is matched nothing should be passed to the callback. $all_files = file_scan_directory($this->path, '/^NONEXISTINGFILENAME/', array('callback' => 'file_test_file_scan_callback')); - $this->assertEqual(0, count($all_files), t('No files were found.')); + $this->assertEqual(0, count($all_files), 'No files were found.'); $results = file_test_file_scan_callback(); file_test_file_scan_callback_reset(); - $this->assertEqual(0, count($results), t('No files were passed to the callback.')); + $this->assertEqual(0, count($results), 'No files were passed to the callback.'); // Grab a listing of all the JavaSscript files and check that they're // passed to the callback. $all_files = file_scan_directory($this->path, '/^javascript-/', array('callback' => 'file_test_file_scan_callback')); - $this->assertEqual(2, count($all_files), t('Found two, expected javascript files.')); + $this->assertEqual(2, count($all_files), 'Found two, expected javascript files.'); $results = file_test_file_scan_callback(); file_test_file_scan_callback_reset(); - $this->assertEqual(2, count($results), t('Files were passed to the callback.')); + $this->assertEqual(2, count($results), 'Files were passed to the callback.'); } /** @@ -1054,11 +1105,11 @@ function testOptionNoMask() { // Grab a listing of all the JavaSscript files. $all_files = file_scan_directory($this->path, '/^javascript-/'); - $this->assertEqual(2, count($all_files), t('Found two, expected javascript files.')); + $this->assertEqual(2, count($all_files), 'Found two, expected javascript files.'); // Now use the nomast parameter to filter out the .script file. $filtered_files = file_scan_directory($this->path, '/^javascript-/', array('nomask' => '/.script$/')); - $this->assertEqual(1, count($filtered_files), t('Filtered correctly.')); + $this->assertEqual(1, count($filtered_files), 'Filtered correctly.'); } /** @@ -1069,25 +1120,25 @@ $expected = array($this->path . '/javascript-1.txt', $this->path . '/javascript-2.script'); $actual = array_keys(file_scan_directory($this->path, '/^javascript-/', array('key' => 'filepath'))); sort($actual); - $this->assertEqual($expected, $actual, t('Returned the correct values for the filename key.')); + $this->assertEqual($expected, $actual, 'Returned the correct values for the filename key.'); // "basename", for the basename of the file. $expected = array('javascript-1.txt', 'javascript-2.script'); $actual = array_keys(file_scan_directory($this->path, '/^javascript-/', array('key' => 'filename'))); sort($actual); - $this->assertEqual($expected, $actual, t('Returned the correct values for the basename key.')); + $this->assertEqual($expected, $actual, 'Returned the correct values for the basename key.'); // "name" for the name of the file without an extension. $expected = array('javascript-1', 'javascript-2'); $actual = array_keys(file_scan_directory($this->path, '/^javascript-/', array('key' => 'name'))); sort($actual); - $this->assertEqual($expected, $actual, t('Returned the correct values for the name key.')); + $this->assertEqual($expected, $actual, 'Returned the correct values for the name key.'); // Invalid option that should default back to "filename". $expected = array($this->path . '/javascript-1.txt', $this->path . '/javascript-2.script'); $actual = array_keys(file_scan_directory($this->path, '/^javascript-/', array('key' => 'INVALID'))); sort($actual); - $this->assertEqual($expected, $actual, t('An invalid key defaulted back to the default.')); + $this->assertEqual($expected, $actual, 'An invalid key defaulted back to the default.'); } /** @@ -1095,10 +1146,10 @@ */ function testOptionRecurse() { $files = file_scan_directory(drupal_get_path('module', 'simpletest'), '/^javascript-/', array('recurse' => FALSE)); - $this->assertTrue(empty($files), t("Without recursion couldn't find javascript files.")); + $this->assertTrue(empty($files), "Without recursion couldn't find javascript files."); $files = file_scan_directory(drupal_get_path('module', 'simpletest'), '/^javascript-/', array('recurse' => TRUE)); - $this->assertEqual(2, count($files), t('With recursion we found the expected javascript files.')); + $this->assertEqual(2, count($files), 'With recursion we found the expected javascript files.'); } @@ -1108,13 +1159,28 @@ */ function testOptionMinDepth() { $files = file_scan_directory($this->path, '/^javascript-/', array('min_depth' => 0)); - $this->assertEqual(2, count($files), t('No minimum-depth gets files in current directory.')); + $this->assertEqual(2, count($files), 'No minimum-depth gets files in current directory.'); $files = file_scan_directory($this->path, '/^javascript-/', array('min_depth' => 1)); - $this->assertTrue(empty($files), t("Minimum-depth of 1 successfully excludes files from current directory.")); + $this->assertTrue(empty($files), "Minimum-depth of 1 successfully excludes files from current directory."); } } +/** + * Tests the file_scan_directory() function on remote filesystems. + */ +class RemoteFileScanDirectoryTest extends FileScanDirectoryTest { + public static function getInfo() { + $info = parent::getInfo(); + $info['group'] = 'File API (remote)'; + return $info; + } + + function setUp() { + parent::setUp('file_test'); + variable_set('file_default_scheme', 'dummy-remote'); + } +} /** * Deletion related tests. @@ -1136,8 +1202,8 @@ $file = $this->createFile(); // Delete a regular file - $this->assertTrue(file_unmanaged_delete($file->uri), t('Deleted worked.')); - $this->assertFalse(file_exists($file->uri), t('Test file has actually been deleted.')); + $this->assertTrue(file_unmanaged_delete($file->uri), 'Deleted worked.'); + $this->assertFalse(file_exists($file->uri), 'Test file has actually been deleted.'); } /** @@ -1145,7 +1211,7 @@ */ function testMissing() { // Try to delete a non-existing file - $this->assertTrue(file_unmanaged_delete(file_default_scheme() . '/' . $this->randomName()), t('Returns true when deleting a non-existent file.')); + $this->assertTrue(file_unmanaged_delete(file_default_scheme() . '/' . $this->randomName()), 'Returns true when deleting a non-existent file.'); } /** @@ -1156,11 +1222,26 @@ $directory = $this->createDirectory(); // Try to delete a directory - $this->assertFalse(file_unmanaged_delete($directory), t('Could not delete the delete directory.')); - $this->assertTrue(file_exists($directory), t('Directory has not been deleted.')); + $this->assertFalse(file_unmanaged_delete($directory), 'Could not delete the delete directory.'); + $this->assertTrue(file_exists($directory), 'Directory has not been deleted.'); } } +/** + * Deletion related tests on remote filesystems. + */ +class RemoteFileUnmanagedDeleteTest extends FileUnmanagedDeleteTest { + public static function getInfo() { + $info = parent::getInfo(); + $info['group'] = 'File API (remote)'; + return $info; + } + + function setUp() { + parent::setUp('file_test'); + variable_set('file_default_scheme', 'dummy-remote'); + } +} /** * Deletion related tests. @@ -1183,8 +1264,8 @@ file_put_contents($filepath, ''); // Delete the file. - $this->assertTrue(file_unmanaged_delete_recursive($filepath), t('Function reported success.')); - $this->assertFalse(file_exists($filepath), t('Test file has been deleted.')); + $this->assertTrue(file_unmanaged_delete_recursive($filepath), 'Function reported success.'); + $this->assertFalse(file_exists($filepath), 'Test file has been deleted.'); } /** @@ -1195,8 +1276,8 @@ $directory = $this->createDirectory(); // Delete the directory. - $this->assertTrue(file_unmanaged_delete_recursive($directory), t('Function reported success.')); - $this->assertFalse(file_exists($directory), t('Directory has been deleted.')); + $this->assertTrue(file_unmanaged_delete_recursive($directory), 'Function reported success.'); + $this->assertFalse(file_exists($directory), 'Directory has been deleted.'); } /** @@ -1211,10 +1292,10 @@ file_put_contents($filepathB, ''); // Delete the directory. - $this->assertTrue(file_unmanaged_delete_recursive($directory), t('Function reported success.')); - $this->assertFalse(file_exists($filepathA), t('Test file A has been deleted.')); - $this->assertFalse(file_exists($filepathB), t('Test file B has been deleted.')); - $this->assertFalse(file_exists($directory), t('Directory has been deleted.')); + $this->assertTrue(file_unmanaged_delete_recursive($directory), 'Function reported success.'); + $this->assertFalse(file_exists($filepathA), 'Test file A has been deleted.'); + $this->assertFalse(file_exists($filepathB), 'Test file B has been deleted.'); + $this->assertFalse(file_exists($directory), 'Directory has been deleted.'); } /** @@ -1230,14 +1311,29 @@ file_put_contents($filepathB, ''); // Delete the directory. - $this->assertTrue(file_unmanaged_delete_recursive($directory), t('Function reported success.')); - $this->assertFalse(file_exists($filepathA), t('Test file A has been deleted.')); - $this->assertFalse(file_exists($filepathB), t('Test file B has been deleted.')); - $this->assertFalse(file_exists($subdirectory), t('Subdirectory has been deleted.')); - $this->assertFalse(file_exists($directory), t('Directory has been deleted.')); + $this->assertTrue(file_unmanaged_delete_recursive($directory), 'Function reported success.'); + $this->assertFalse(file_exists($filepathA), 'Test file A has been deleted.'); + $this->assertFalse(file_exists($filepathB), 'Test file B has been deleted.'); + $this->assertFalse(file_exists($subdirectory), 'Subdirectory has been deleted.'); + $this->assertFalse(file_exists($directory), 'Directory has been deleted.'); } } +/** + * Deletion related tests on remote filesystems. + */ +class RemoteFileUnmanagedDeleteRecursiveTest extends FileUnmanagedDeleteRecursiveTest { + public static function getInfo() { + $info = parent::getInfo(); + $info['group'] = 'File API (remote)'; + return $info; + } + + function setUp() { + parent::setUp('file_test'); + variable_set('file_default_scheme', 'dummy-remote'); + } +} /** * Unmanaged move related tests. @@ -1261,21 +1357,21 @@ // Moving to a new name. $desired_filepath = 'public://' . $this->randomName(); $new_filepath = file_unmanaged_move($file->uri, $desired_filepath, FILE_EXISTS_ERROR); - $this->assertTrue($new_filepath, t('Move was successful.')); - $this->assertEqual($new_filepath, $desired_filepath, t('Returned expected filepath.')); - $this->assertTrue(file_exists($new_filepath), t('File exists at the new location.')); - $this->assertFalse(file_exists($file->uri), t('No file remains at the old location.')); + $this->assertTrue($new_filepath, 'Move was successful.'); + $this->assertEqual($new_filepath, $desired_filepath, 'Returned expected filepath.'); + $this->assertTrue(file_exists($new_filepath), 'File exists at the new location.'); + $this->assertFalse(file_exists($file->uri), 'No file remains at the old location.'); $this->assertFilePermissions($new_filepath, variable_get('file_chmod_file', 0664)); // Moving with rename. $desired_filepath = 'public://' . $this->randomName(); - $this->assertTrue(file_exists($new_filepath), t('File exists before moving.')); - $this->assertTrue(file_put_contents($desired_filepath, ' '), t('Created a file so a rename will have to happen.')); + $this->assertTrue(file_exists($new_filepath), 'File exists before moving.'); + $this->assertTrue(file_put_contents($desired_filepath, ' '), 'Created a file so a rename will have to happen.'); $newer_filepath = file_unmanaged_move($new_filepath, $desired_filepath, FILE_EXISTS_RENAME); - $this->assertTrue($newer_filepath, t('Move was successful.')); - $this->assertNotEqual($newer_filepath, $desired_filepath, t('Returned expected filepath.')); - $this->assertTrue(file_exists($newer_filepath), t('File exists at the new location.')); - $this->assertFalse(file_exists($new_filepath), t('No file remains at the old location.')); + $this->assertTrue($newer_filepath, 'Move was successful.'); + $this->assertNotEqual($newer_filepath, $desired_filepath, 'Returned expected filepath.'); + $this->assertTrue(file_exists($newer_filepath), 'File exists at the new location.'); + $this->assertFalse(file_exists($new_filepath), 'No file remains at the old location.'); $this->assertFilePermissions($newer_filepath, variable_get('file_chmod_file', 0664)); // TODO: test moving to a directory (rather than full directory/file path) @@ -1288,7 +1384,7 @@ function testMissing() { // Move non-existent file. $new_filepath = file_unmanaged_move($this->randomName(), $this->randomName()); - $this->assertFalse($new_filepath, t('Moving a missing file fails.')); + $this->assertFalse($new_filepath, 'Moving a missing file fails.'); } /** @@ -1300,17 +1396,32 @@ // Move the file onto itself without renaming shouldn't make changes. $new_filepath = file_unmanaged_move($file->uri, $file->uri, FILE_EXISTS_REPLACE); - $this->assertFalse($new_filepath, t('Moving onto itself without renaming fails.')); - $this->assertTrue(file_exists($file->uri), t('File exists after moving onto itself.')); + $this->assertFalse($new_filepath, 'Moving onto itself without renaming fails.'); + $this->assertTrue(file_exists($file->uri), 'File exists after moving onto itself.'); // Move the file onto itself with renaming will result in a new filename. $new_filepath = file_unmanaged_move($file->uri, $file->uri, FILE_EXISTS_RENAME); - $this->assertTrue($new_filepath, t('Moving onto itself with renaming works.')); - $this->assertFalse(file_exists($file->uri), t('Original file has been removed.')); - $this->assertTrue(file_exists($new_filepath), t('File exists after moving onto itself.')); + $this->assertTrue($new_filepath, 'Moving onto itself with renaming works.'); + $this->assertFalse(file_exists($file->uri), 'Original file has been removed.'); + $this->assertTrue(file_exists($new_filepath), 'File exists after moving onto itself.'); } } +/** + * Unmanaged move related tests on remote filesystems. + */ +class RemoteFileUnmanagedMoveTest extends FileUnmanagedMoveTest { + public static function getInfo() { + $info = parent::getInfo(); + $info['group'] = 'File API (remote)'; + return $info; + } + + function setUp() { + parent::setUp('file_test'); + variable_set('file_default_scheme', 'dummy-remote'); + } +} /** * Unmanaged copy related tests. @@ -1334,20 +1445,20 @@ // Copying to a new name. $desired_filepath = 'public://' . $this->randomName(); $new_filepath = file_unmanaged_copy($file->uri, $desired_filepath, FILE_EXISTS_ERROR); - $this->assertTrue($new_filepath, t('Copy was successful.')); - $this->assertEqual($new_filepath, $desired_filepath, t('Returned expected filepath.')); - $this->assertTrue(file_exists($file->uri), t('Original file remains.')); - $this->assertTrue(file_exists($new_filepath), t('New file exists.')); + $this->assertTrue($new_filepath, 'Copy was successful.'); + $this->assertEqual($new_filepath, $desired_filepath, 'Returned expected filepath.'); + $this->assertTrue(file_exists($file->uri), 'Original file remains.'); + $this->assertTrue(file_exists($new_filepath), 'New file exists.'); $this->assertFilePermissions($new_filepath, variable_get('file_chmod_file', 0664)); // Copying with rename. $desired_filepath = 'public://' . $this->randomName(); - $this->assertTrue(file_put_contents($desired_filepath, ' '), t('Created a file so a rename will have to happen.')); + $this->assertTrue(file_put_contents($desired_filepath, ' '), 'Created a file so a rename will have to happen.'); $newer_filepath = file_unmanaged_copy($file->uri, $desired_filepath, FILE_EXISTS_RENAME); - $this->assertTrue($newer_filepath, t('Copy was successful.')); - $this->assertNotEqual($newer_filepath, $desired_filepath, t('Returned expected filepath.')); - $this->assertTrue(file_exists($file->uri), t('Original file remains.')); - $this->assertTrue(file_exists($newer_filepath), t('New file exists.')); + $this->assertTrue($newer_filepath, 'Copy was successful.'); + $this->assertNotEqual($newer_filepath, $desired_filepath, 'Returned expected filepath.'); + $this->assertTrue(file_exists($file->uri), 'Original file remains.'); + $this->assertTrue(file_exists($newer_filepath), 'New file exists.'); $this->assertFilePermissions($newer_filepath, variable_get('file_chmod_file', 0664)); // TODO: test copying to a directory (rather than full directory/file path) @@ -1360,9 +1471,9 @@ function testNonExistent() { // Copy non-existent file $desired_filepath = $this->randomName(); - $this->assertFalse(file_exists($desired_filepath), t("Randomly named file doesn't exists.")); + $this->assertFalse(file_exists($desired_filepath), "Randomly named file doesn't exists."); $new_filepath = file_unmanaged_copy($desired_filepath, $this->randomName()); - $this->assertFalse($new_filepath, t('Copying a missing file fails.')); + $this->assertFalse($new_filepath, 'Copying a missing file fails.'); } /** @@ -1374,33 +1485,49 @@ // Copy the file onto itself with renaming works. $new_filepath = file_unmanaged_copy($file->uri, $file->uri, FILE_EXISTS_RENAME); - $this->assertTrue($new_filepath, t('Copying onto itself with renaming works.')); - $this->assertNotEqual($new_filepath, $file->uri, t('Copied file has a new name.')); - $this->assertTrue(file_exists($file->uri), t('Original file exists after copying onto itself.')); - $this->assertTrue(file_exists($new_filepath), t('Copied file exists after copying onto itself.')); + $this->assertTrue($new_filepath, 'Copying onto itself with renaming works.'); + $this->assertNotEqual($new_filepath, $file->uri, 'Copied file has a new name.'); + $this->assertTrue(file_exists($file->uri), 'Original file exists after copying onto itself.'); + $this->assertTrue(file_exists($new_filepath), 'Copied file exists after copying onto itself.'); $this->assertFilePermissions($new_filepath, variable_get('file_chmod_file', 0664)); // Copy the file onto itself without renaming fails. $new_filepath = file_unmanaged_copy($file->uri, $file->uri, FILE_EXISTS_ERROR); - $this->assertFalse($new_filepath, t('Copying onto itself without renaming fails.')); - $this->assertTrue(file_exists($file->uri), t('File exists after copying onto itself.')); + $this->assertFalse($new_filepath, 'Copying onto itself without renaming fails.'); + $this->assertTrue(file_exists($file->uri), 'File exists after copying onto itself.'); // Copy the file into same directory without renaming fails. $new_filepath = file_unmanaged_copy($file->uri, drupal_dirname($file->uri), FILE_EXISTS_ERROR); - $this->assertFalse($new_filepath, t('Copying onto itself fails.')); - $this->assertTrue(file_exists($file->uri), t('File exists after copying onto itself.')); + $this->assertFalse($new_filepath, 'Copying onto itself fails.'); + $this->assertTrue(file_exists($file->uri), 'File exists after copying onto itself.'); // Copy the file into same directory with renaming works. $new_filepath = file_unmanaged_copy($file->uri, drupal_dirname($file->uri), FILE_EXISTS_RENAME); - $this->assertTrue($new_filepath, t('Copying into same directory works.')); - $this->assertNotEqual($new_filepath, $file->uri, t('Copied file has a new name.')); - $this->assertTrue(file_exists($file->uri), t('Original file exists after copying onto itself.')); - $this->assertTrue(file_exists($new_filepath), t('Copied file exists after copying onto itself.')); + $this->assertTrue($new_filepath, 'Copying into same directory works.'); + $this->assertNotEqual($new_filepath, $file->uri, 'Copied file has a new name.'); + $this->assertTrue(file_exists($file->uri), 'Original file exists after copying onto itself.'); + $this->assertTrue(file_exists($new_filepath), 'Copied file exists after copying onto itself.'); $this->assertFilePermissions($new_filepath, variable_get('file_chmod_file', 0664)); } } /** + * Unmanaged copy related tests on remote filesystems. + */ +class RemoteFileUnmanagedCopyTest extends FileUnmanagedCopyTest { + public static function getInfo() { + $info = parent::getInfo(); + $info['group'] = 'File API (remote)'; + return $info; + } + + function setUp() { + parent::setUp('file_test'); + variable_set('file_default_scheme', 'dummy-remote'); + } +} + +/** * Deletion related tests. */ class FileDeleteTest extends FileHookTestCase { @@ -1419,11 +1546,11 @@ $file = $this->createFile(); // Check that deletion removes the file and database record. - $this->assertTrue(is_file($file->uri), t('File exists.')); - $this->assertIdentical(file_delete($file), TRUE, t('Delete worked.')); + $this->assertTrue(is_file($file->uri), 'File exists.'); + $this->assertIdentical(file_delete($file), TRUE, 'Delete worked.'); $this->assertFileHooksCalled(array('delete')); - $this->assertFalse(file_exists($file->uri), t('Test file has actually been deleted.')); - $this->assertFalse(file_load($file->fid), t('File was removed from the database.')); + $this->assertFalse(file_exists($file->uri), 'Test file has actually been deleted.'); + $this->assertFalse(file_load($file->fid), 'File was removed from the database.'); } /** @@ -1437,9 +1564,9 @@ file_usage_delete($file, 'testing', 'test', 1); file_delete($file); $usage = file_usage_list($file); - $this->assertEqual($usage['testing']['test'], array('id' => 1, 'count' => 1), t('Test file is still in use.')); - $this->assertTrue(file_exists($file->uri), t('File still exists on the disk.')); - $this->assertTrue(file_load($file->fid), t('File still exists in the database.')); + $this->assertEqual($usage['testing']['test'], array(1 => 1), 'Test file is still in use.'); + $this->assertTrue(file_exists($file->uri), 'File still exists on the disk.'); + $this->assertTrue(file_load($file->fid), 'File still exists in the database.'); // Clear out the call to hook_file_load(). file_test_reset(); @@ -1448,9 +1575,9 @@ file_delete($file); $usage = file_usage_list($file); $this->assertFileHooksCalled(array('delete')); - $this->assertTrue(empty($usage), t('File usage data was removed.')); - $this->assertFalse(file_exists($file->uri), t('File has been deleted after its last usage was removed.')); - $this->assertFalse(file_load($file->fid), t('File was removed from the database.')); + $this->assertTrue(empty($usage), 'File usage data was removed.'); + $this->assertFalse(file_exists($file->uri), 'File has been deleted after its last usage was removed.'); + $this->assertFalse(file_load($file->fid), 'File was removed from the database.'); } } @@ -1480,20 +1607,20 @@ $result = file_move(clone $source, $desired_filepath, FILE_EXISTS_ERROR); // Check the return status and that the contents changed. - $this->assertTrue($result, t('File moved sucessfully.')); + $this->assertTrue($result, 'File moved successfully.'); $this->assertFalse(file_exists($source->uri)); - $this->assertEqual($contents, file_get_contents($result->uri), t('Contents of file correctly written.')); + $this->assertEqual($contents, file_get_contents($result->uri), 'Contents of file correctly written.'); // Check that the correct hooks were called. $this->assertFileHooksCalled(array('move', 'load', 'update')); // Make sure we got the same file back. - $this->assertEqual($source->fid, $result->fid, t("Source file id's' %fid is unchanged after move.", array('%fid' => $source->fid))); + $this->assertEqual($source->fid, $result->fid, format_string("Source file id's' %fid is unchanged after move.", array('%fid' => $source->fid))); // Reload the file from the database and check that the changes were // actually saved. $loaded_file = file_load($result->fid, TRUE); - $this->assertTrue($loaded_file, t('File can be loaded from the database.')); + $this->assertTrue($loaded_file, 'File can be loaded from the database.'); $this->assertFileUnchanged($result, $loaded_file); } @@ -1512,9 +1639,9 @@ $result = file_move(clone $source, $target->uri, FILE_EXISTS_RENAME); // Check the return status and that the contents changed. - $this->assertTrue($result, t('File moved sucessfully.')); + $this->assertTrue($result, 'File moved successfully.'); $this->assertFalse(file_exists($source->uri)); - $this->assertEqual($contents, file_get_contents($result->uri), t('Contents of file correctly written.')); + $this->assertEqual($contents, file_get_contents($result->uri), 'Contents of file correctly written.'); // Check that the correct hooks were called. $this->assertFileHooksCalled(array('move', 'load', 'update')); @@ -1528,8 +1655,8 @@ // Compare the source and results. $loaded_source = file_load($source->fid, TRUE); - $this->assertEqual($loaded_source->fid, $result->fid, t("Returned file's id matches the source.")); - $this->assertNotEqual($loaded_source->uri, $source->uri, t("Returned file path has changed from the original.")); + $this->assertEqual($loaded_source->fid, $result->fid, "Returned file's id matches the source."); + $this->assertNotEqual($loaded_source->uri, $source->uri, 'Returned file path has changed from the original.'); } /** @@ -1547,9 +1674,9 @@ $result = file_move(clone $source, $target->uri, FILE_EXISTS_REPLACE); // Look at the results. - $this->assertEqual($contents, file_get_contents($result->uri), t('Contents of file were overwritten.')); + $this->assertEqual($contents, file_get_contents($result->uri), 'Contents of file were overwritten.'); $this->assertFalse(file_exists($source->uri)); - $this->assertTrue($result, t('File moved sucessfully.')); + $this->assertTrue($result, 'File moved successfully.'); // Check that the correct hooks were called. $this->assertFileHooksCalled(array('move', 'update', 'delete', 'load')); @@ -1575,8 +1702,8 @@ // Copy the file over itself. Clone the object so we don't have to worry // about the function changing our reference copy. $result = file_move(clone $source, $source->uri, FILE_EXISTS_REPLACE); - $this->assertFalse($result, t('File move failed.')); - $this->assertEqual($contents, file_get_contents($source->uri), t('Contents of file were not altered.')); + $this->assertFalse($result, 'File move failed.'); + $this->assertEqual($contents, file_get_contents($source->uri), 'Contents of file were not altered.'); // Check that no hooks were called while failing. $this->assertFileHooksCalled(array()); @@ -1601,9 +1728,9 @@ $result = file_move(clone $source, $target->uri, FILE_EXISTS_ERROR); // Check the return status and that the contents did not change. - $this->assertFalse($result, t('File move failed.')); + $this->assertFalse($result, 'File move failed.'); $this->assertTrue(file_exists($source->uri)); - $this->assertEqual($contents, file_get_contents($target->uri), t('Contents of file were not altered.')); + $this->assertEqual($contents, file_get_contents($target->uri), 'Contents of file were not altered.'); // Check that no hooks were called while failing. $this->assertFileHooksCalled(array()); @@ -1641,16 +1768,16 @@ $result = file_copy(clone $source, $desired_uri, FILE_EXISTS_ERROR); // Check the return status and that the contents changed. - $this->assertTrue($result, t('File copied sucessfully.')); - $this->assertEqual($contents, file_get_contents($result->uri), t('Contents of file were copied correctly.')); + $this->assertTrue($result, 'File copied successfully.'); + $this->assertEqual($contents, file_get_contents($result->uri), 'Contents of file were copied correctly.'); // Check that the correct hooks were called. $this->assertFileHooksCalled(array('copy', 'insert')); $this->assertDifferentFile($source, $result); - $this->assertEqual($result->uri, $desired_uri, t('The copied file object has the desired filepath.')); - $this->assertTrue(file_exists($source->uri), t('The original file still exists.')); - $this->assertTrue(file_exists($result->uri), t('The copied file exists.')); + $this->assertEqual($result->uri, $desired_uri, 'The copied file object has the desired filepath.'); + $this->assertTrue(file_exists($source->uri), 'The original file still exists.'); + $this->assertTrue(file_exists($result->uri), 'The copied file exists.'); // Reload the file from the database and check that the changes were // actually saved. @@ -1672,9 +1799,9 @@ $result = file_copy(clone $source, $target->uri, FILE_EXISTS_RENAME); // Check the return status and that the contents changed. - $this->assertTrue($result, t('File copied sucessfully.')); - $this->assertEqual($contents, file_get_contents($result->uri), t('Contents of file were copied correctly.')); - $this->assertNotEqual($result->uri, $source->uri, t('Returned file path has changed from the original.')); + $this->assertTrue($result, 'File copied successfully.'); + $this->assertEqual($contents, file_get_contents($result->uri), 'Contents of file were copied correctly.'); + $this->assertNotEqual($result->uri, $source->uri, 'Returned file path has changed from the original.'); // Check that the correct hooks were called. $this->assertFileHooksCalled(array('copy', 'insert')); @@ -1712,8 +1839,8 @@ $result = file_copy(clone $source, $target->uri, FILE_EXISTS_REPLACE); // Check the return status and that the contents changed. - $this->assertTrue($result, t('File copied sucessfully.')); - $this->assertEqual($contents, file_get_contents($result->uri), t('Contents of file were overwritten.')); + $this->assertTrue($result, 'File copied successfully.'); + $this->assertEqual($contents, file_get_contents($result->uri), 'Contents of file were overwritten.'); $this->assertDifferentFile($source, $result); // Check that the correct hooks were called. @@ -1750,8 +1877,8 @@ $result = file_copy(clone $source, $target->uri, FILE_EXISTS_ERROR); // Check the return status and that the contents were not changed. - $this->assertFalse($result, t('File copy failed.')); - $this->assertEqual($contents, file_get_contents($target->uri), t('Contents of file were not altered.')); + $this->assertFalse($result, 'File copy failed.'); + $this->assertEqual($contents, file_get_contents($target->uri), 'Contents of file were not altered.'); // Check that the correct hooks were called. $this->assertFileHooksCalled(array()); @@ -1778,7 +1905,7 @@ * Try to load a non-existent file by fid. */ function testLoadMissingFid() { - $this->assertFalse(file_load(-1), t("Try to load an invalid fid fails.")); + $this->assertFalse(file_load(-1), "Try to load an invalid fid fails."); $this->assertFileHooksCalled(array()); } @@ -1787,7 +1914,7 @@ */ function testLoadMissingFilepath() { $files = file_load_multiple(array(), array('uri' => 'foobar://misc/druplicon.png')); - $this->assertFalse(reset($files), t("Try to load a file that doesn't exist in the database fails.")); + $this->assertFalse(reset($files), "Try to load a file that doesn't exist in the database fails."); $this->assertFileHooksCalled(array()); } @@ -1796,7 +1923,7 @@ */ function testLoadInvalidStatus() { $files = file_load_multiple(array(), array('status' => -99)); - $this->assertFalse(reset($files), t("Trying to load a file with an invalid status fails.")); + $this->assertFalse(reset($files), "Trying to load a file with an invalid status fails."); $this->assertFileHooksCalled(array()); } @@ -1809,13 +1936,13 @@ $by_fid_file = file_load($file->fid); $this->assertFileHookCalled('load'); - $this->assertTrue(is_object($by_fid_file), t('file_load() returned an object.')); - $this->assertEqual($by_fid_file->fid, $file->fid, t("Loading by fid got the same fid."), 'File'); - $this->assertEqual($by_fid_file->uri, $file->uri, t("Loading by fid got the correct filepath."), 'File'); - $this->assertEqual($by_fid_file->filename, $file->filename, t("Loading by fid got the correct filename."), 'File'); - $this->assertEqual($by_fid_file->filemime, $file->filemime, t("Loading by fid got the correct MIME type."), 'File'); - $this->assertEqual($by_fid_file->status, $file->status, t("Loading by fid got the correct status."), 'File'); - $this->assertTrue($by_fid_file->file_test['loaded'], t('file_test_file_load() was able to modify the file during load.')); + $this->assertTrue(is_object($by_fid_file), 'file_load() returned an object.'); + $this->assertEqual($by_fid_file->fid, $file->fid, 'Loading by fid got the same fid.', 'File'); + $this->assertEqual($by_fid_file->uri, $file->uri, 'Loading by fid got the correct filepath.', 'File'); + $this->assertEqual($by_fid_file->filename, $file->filename, 'Loading by fid got the correct filename.', 'File'); + $this->assertEqual($by_fid_file->filemime, $file->filemime, 'Loading by fid got the correct MIME type.', 'File'); + $this->assertEqual($by_fid_file->status, $file->status, 'Loading by fid got the correct status.', 'File'); + $this->assertTrue($by_fid_file->file_test['loaded'], 'file_test_file_load() was able to modify the file during load.'); } /** @@ -1829,19 +1956,19 @@ file_test_reset(); $by_path_files = file_load_multiple(array(), array('uri' => $file->uri)); $this->assertFileHookCalled('load'); - $this->assertEqual(1, count($by_path_files), t('file_load_multiple() returned an array of the correct size.')); + $this->assertEqual(1, count($by_path_files), 'file_load_multiple() returned an array of the correct size.'); $by_path_file = reset($by_path_files); - $this->assertTrue($by_path_file->file_test['loaded'], t('file_test_file_load() was able to modify the file during load.')); - $this->assertEqual($by_path_file->fid, $file->fid, t("Loading by filepath got the correct fid."), 'File'); + $this->assertTrue($by_path_file->file_test['loaded'], 'file_test_file_load() was able to modify the file during load.'); + $this->assertEqual($by_path_file->fid, $file->fid, 'Loading by filepath got the correct fid.', 'File'); // Load by fid. file_test_reset(); $by_fid_files = file_load_multiple(array($file->fid), array()); $this->assertFileHookCalled('load'); - $this->assertEqual(1, count($by_fid_files), t('file_load_multiple() returned an array of the correct size.')); + $this->assertEqual(1, count($by_fid_files), 'file_load_multiple() returned an array of the correct size.'); $by_fid_file = reset($by_fid_files); - $this->assertTrue($by_fid_file->file_test['loaded'], t('file_test_file_load() was able to modify the file during load.')); - $this->assertEqual($by_fid_file->uri, $file->uri, t("Loading by fid got the correct filepath."), 'File'); + $this->assertTrue($by_fid_file->file_test['loaded'], 'file_test_file_load() was able to modify the file during load.'); + $this->assertEqual($by_fid_file->uri, $file->uri, 'Loading by fid got the correct filepath.', 'File'); } } @@ -1876,13 +2003,13 @@ // Check that the correct hooks were called. $this->assertFileHooksCalled(array('insert')); - $this->assertNotNull($saved_file, t("Saving the file should give us back a file object."), 'File'); - $this->assertTrue($saved_file->fid > 0, t("A new file ID is set when saving a new file to the database."), 'File'); + $this->assertNotNull($saved_file, 'Saving the file should give us back a file object.', 'File'); + $this->assertTrue($saved_file->fid > 0, 'A new file ID is set when saving a new file to the database.', 'File'); $loaded_file = db_query('SELECT * FROM {file_managed} f WHERE f.fid = :fid', array(':fid' => $saved_file->fid))->fetch(PDO::FETCH_OBJ); - $this->assertNotNull($loaded_file, t("Record exists in the database.")); - $this->assertEqual($loaded_file->status, $file->status, t("Status was saved correctly.")); - $this->assertEqual($saved_file->filesize, filesize($file->uri), t("File size was set correctly."), 'File'); - $this->assertTrue($saved_file->timestamp > 1, t("File size was set correctly."), 'File'); + $this->assertNotNull($loaded_file, 'Record exists in the database.'); + $this->assertEqual($loaded_file->status, $file->status, 'Status was saved correctly.'); + $this->assertEqual($saved_file->filesize, filesize($file->uri), 'File size was set correctly.', 'File'); + $this->assertTrue($saved_file->timestamp > 1, 'File size was set correctly.', 'File'); // Resave the file, updating the existing record. @@ -1893,11 +2020,24 @@ // Check that the correct hooks were called. $this->assertFileHooksCalled(array('load', 'update')); - $this->assertEqual($resaved_file->fid, $saved_file->fid, t("The file ID of an existing file is not changed when updating the database."), 'File'); - $this->assertTrue($resaved_file->timestamp >= $saved_file->timestamp, t("Timestamp didn't go backwards."), 'File'); + $this->assertEqual($resaved_file->fid, $saved_file->fid, 'The file ID of an existing file is not changed when updating the database.', 'File'); + $this->assertTrue($resaved_file->timestamp >= $saved_file->timestamp, "Timestamp didn't go backwards.", 'File'); $loaded_file = db_query('SELECT * FROM {file_managed} f WHERE f.fid = :fid', array(':fid' => $saved_file->fid))->fetch(PDO::FETCH_OBJ); - $this->assertNotNull($loaded_file, t("Record still exists in the database."), 'File'); - $this->assertEqual($loaded_file->status, $saved_file->status, t("Status was saved correctly.")); + $this->assertNotNull($loaded_file, 'Record still exists in the database.', 'File'); + $this->assertEqual($loaded_file->status, $saved_file->status, 'Status was saved correctly.'); + + // Try to insert a second file with the same name apart from case insensitivity + // to ensure the 'uri' index allows for filenames with different cases. + $file = (object) array( + 'uid' => 1, + 'filename' => 'DRUPLICON.txt', + 'uri' => 'public://DRUPLICON.txt', + 'filemime' => 'text/plain', + 'timestamp' => 1, + 'status' => FILE_STATUS_PERMANENT, + ); + file_put_contents($file->uri, 'hello world'); + file_save($file); } } @@ -1939,11 +2079,11 @@ $usage = file_usage_list($file); - $this->assertEqual(count($usage['testing']), 2, t('Returned the correct number of items.')); - $this->assertEqual($usage['testing']['foo']['id'], 1, t('Returned the correct id.')); - $this->assertEqual($usage['testing']['bar']['id'], 2, t('Returned the correct id.')); - $this->assertEqual($usage['testing']['foo']['count'], 1, t('Returned the correct count.')); - $this->assertEqual($usage['testing']['bar']['count'], 2, t('Returned the correct count.')); + $this->assertEqual(count($usage['testing']), 2, 'Returned the correct number of items.'); + $this->assertTrue(isset($usage['testing']['foo'][1]), 'Returned the correct id.'); + $this->assertTrue(isset($usage['testing']['bar'][2]), 'Returned the correct id.'); + $this->assertEqual($usage['testing']['foo'][1], 1, 'Returned the correct count.'); + $this->assertEqual($usage['testing']['bar'][2], 2, 'Returned the correct count.'); } /** @@ -1962,13 +2102,13 @@ ->condition('f.fid', $file->fid) ->execute() ->fetchAllAssoc('id'); - $this->assertEqual(count($usage), 2, t('Created two records')); - $this->assertEqual($usage[1]->module, 'testing', t('Correct module')); - $this->assertEqual($usage[2]->module, 'testing', t('Correct module')); - $this->assertEqual($usage[1]->type, 'foo', t('Correct type')); - $this->assertEqual($usage[2]->type, 'bar', t('Correct type')); - $this->assertEqual($usage[1]->count, 1, t('Correct count')); - $this->assertEqual($usage[2]->count, 2, t('Correct count')); + $this->assertEqual(count($usage), 2, 'Created two records'); + $this->assertEqual($usage[1]->module, 'testing', 'Correct module'); + $this->assertEqual($usage[2]->module, 'testing', 'Correct module'); + $this->assertEqual($usage[1]->type, 'foo', 'Correct type'); + $this->assertEqual($usage[2]->type, 'bar', 'Correct type'); + $this->assertEqual($usage[1]->count, 1, 'Correct count'); + $this->assertEqual($usage[2]->count, 2, 'Correct count'); } /** @@ -1993,7 +2133,7 @@ ->condition('f.fid', $file->fid) ->execute() ->fetchField(); - $this->assertEqual(2, $count, t('The count was decremented correctly.')); + $this->assertEqual(2, $count, 'The count was decremented correctly.'); // Multiple decrement and removal. file_usage_delete($file, 'testing', 'bar', 2, 2); @@ -2002,7 +2142,7 @@ ->condition('f.fid', $file->fid) ->execute() ->fetchField(); - $this->assertIdentical(FALSE, $count, t('The count was removed entirely when empty.')); + $this->assertIdentical(FALSE, $count, 'The count was removed entirely when empty.'); // Non-existent decrement. file_usage_delete($file, 'testing', 'bar', 2); @@ -2011,7 +2151,7 @@ ->condition('f.fid', $file->fid) ->execute() ->fetchField(); - $this->assertIdentical(FALSE, $count, t('Decrementing non-exist record complete.')); + $this->assertIdentical(FALSE, $count, 'Decrementing non-exist record complete.'); } } @@ -2034,7 +2174,7 @@ $file = $this->createFile(); // Empty validators. - $this->assertEqual(file_validate($file, array()), array(), t('Validating an empty array works succesfully.')); + $this->assertEqual(file_validate($file, array()), array(), 'Validating an empty array works successfully.'); $this->assertFileHooksCalled(array('validate')); // Use the file_test.module's test validator to ensure that passing tests @@ -2042,14 +2182,14 @@ file_test_reset(); file_test_set_return('validate', array()); $passing = array('file_test_validator' => array(array())); - $this->assertEqual(file_validate($file, $passing), array(), t('Validating passes.')); + $this->assertEqual(file_validate($file, $passing), array(), 'Validating passes.'); $this->assertFileHooksCalled(array('validate')); // Now test for failures in validators passed in and by hook_validate. file_test_reset(); file_test_set_return('validate', array('Epic fail')); $failing = array('file_test_validator' => array(array('Failed', 'Badly'))); - $this->assertEqual(file_validate($file, $failing), array('Failed', 'Badly', 'Epic fail'), t('Validating returns errors.')); + $this->assertEqual(file_validate($file, $failing), array('Failed', 'Badly', 'Epic fail'), 'Validating returns errors.'); $this->assertFileHooksCalled(array('validate')); } } @@ -2073,13 +2213,13 @@ $contents = $this->randomName(8); $result = file_save_data($contents); - $this->assertTrue($result, t('Unnamed file saved correctly.')); + $this->assertTrue($result, 'Unnamed file saved correctly.'); - $this->assertEqual(file_default_scheme(), file_uri_scheme($result->uri), t("File was placed in Drupal's files directory.")); - $this->assertEqual($result->filename, basename($result->uri), t("Filename was set to the file's basename.")); - $this->assertEqual($contents, file_get_contents($result->uri), t('Contents of the file are correct.')); - $this->assertEqual($result->filemime, 'application/octet-stream', t('A MIME type was set.')); - $this->assertEqual($result->status, FILE_STATUS_PERMANENT, t("The file's status was set to permanent.")); + $this->assertEqual(file_default_scheme(), file_uri_scheme($result->uri), "File was placed in Drupal's files directory."); + $this->assertEqual($result->filename, drupal_basename($result->uri), "Filename was set to the file's basename."); + $this->assertEqual($contents, file_get_contents($result->uri), 'Contents of the file are correct.'); + $this->assertEqual($result->filemime, 'application/octet-stream', 'A MIME type was set.'); + $this->assertEqual($result->status, FILE_STATUS_PERMANENT, "The file's status was set to permanent."); // Check that the correct hooks were called. $this->assertFileHooksCalled(array('insert')); @@ -2094,14 +2234,17 @@ function testWithFilename() { $contents = $this->randomName(8); - $result = file_save_data($contents, 'public://' . 'asdf.txt'); - $this->assertTrue($result, t('Unnamed file saved correctly.')); + // Using filename with non-latin characters. + $filename = 'Текстовый файл.txt'; - $this->assertEqual('public', file_uri_scheme($result->uri), t("File was placed in Drupal's files directory.")); - $this->assertEqual('asdf.txt', basename($result->uri), t('File was named correctly.')); - $this->assertEqual($contents, file_get_contents($result->uri), t('Contents of the file are correct.')); - $this->assertEqual($result->filemime, 'text/plain', t('A MIME type was set.')); - $this->assertEqual($result->status, FILE_STATUS_PERMANENT, t("The file's status was set to permanent.")); + $result = file_save_data($contents, 'public://' . $filename); + $this->assertTrue($result, 'Unnamed file saved correctly.'); + + $this->assertEqual('public', file_uri_scheme($result->uri), "File was placed in Drupal's files directory."); + $this->assertEqual($filename, drupal_basename($result->uri), 'File was named correctly.'); + $this->assertEqual($contents, file_get_contents($result->uri), 'Contents of the file are correct.'); + $this->assertEqual($result->filemime, 'text/plain', 'A MIME type was set.'); + $this->assertEqual($result->status, FILE_STATUS_PERMANENT, "The file's status was set to permanent."); // Check that the correct hooks were called. $this->assertFileHooksCalled(array('insert')); @@ -2119,13 +2262,13 @@ $contents = $this->randomName(8); $result = file_save_data($contents, $existing->uri, FILE_EXISTS_RENAME); - $this->assertTrue($result, t("File saved sucessfully.")); + $this->assertTrue($result, 'File saved successfully.'); - $this->assertEqual('public', file_uri_scheme($result->uri), t("File was placed in Drupal's files directory.")); - $this->assertEqual($result->filename, $existing->filename, t("Filename was set to the basename of the source, rather than that of the renamed file.")); - $this->assertEqual($contents, file_get_contents($result->uri), t("Contents of the file are correct.")); - $this->assertEqual($result->filemime, 'application/octet-stream', t("A MIME type was set.")); - $this->assertEqual($result->status, FILE_STATUS_PERMANENT, t("The file's status was set to permanent.")); + $this->assertEqual('public', file_uri_scheme($result->uri), "File was placed in Drupal's files directory."); + $this->assertEqual($result->filename, $existing->filename, 'Filename was set to the basename of the source, rather than that of the renamed file.'); + $this->assertEqual($contents, file_get_contents($result->uri), 'Contents of the file are correct.'); + $this->assertEqual($result->filemime, 'application/octet-stream', 'A MIME type was set.'); + $this->assertEqual($result->status, FILE_STATUS_PERMANENT, "The file's status was set to permanent."); // Check that the correct hooks were called. $this->assertFileHooksCalled(array('insert')); @@ -2147,13 +2290,13 @@ $contents = $this->randomName(8); $result = file_save_data($contents, $existing->uri, FILE_EXISTS_REPLACE); - $this->assertTrue($result, t('File saved sucessfully.')); + $this->assertTrue($result, 'File saved successfully.'); - $this->assertEqual('public', file_uri_scheme($result->uri), t("File was placed in Drupal's files directory.")); - $this->assertEqual($result->filename, $existing->filename, t('Filename was set to the basename of the existing file, rather than preserving the original name.')); - $this->assertEqual($contents, file_get_contents($result->uri), t('Contents of the file are correct.')); - $this->assertEqual($result->filemime, 'application/octet-stream', t('A MIME type was set.')); - $this->assertEqual($result->status, FILE_STATUS_PERMANENT, t("The file's status was set to permanent.")); + $this->assertEqual('public', file_uri_scheme($result->uri), "File was placed in Drupal's files directory."); + $this->assertEqual($result->filename, $existing->filename, 'Filename was set to the basename of the existing file, rather than preserving the original name.'); + $this->assertEqual($contents, file_get_contents($result->uri), 'Contents of the file are correct.'); + $this->assertEqual($result->filemime, 'application/octet-stream', 'A MIME type was set.'); + $this->assertEqual($result->status, FILE_STATUS_PERMANENT, "The file's status was set to permanent."); // Check that the correct hooks were called. $this->assertFileHooksCalled(array('load', 'update')); @@ -2174,8 +2317,8 @@ // Check the overwrite error. $result = file_save_data('asdf', $existing->uri, FILE_EXISTS_ERROR); - $this->assertFalse($result, t('Overwriting a file fails when FILE_EXISTS_ERROR is specified.')); - $this->assertEqual($contents, file_get_contents($existing->uri), t('Contents of existing file were unchanged.')); + $this->assertFalse($result, 'Overwriting a file fails when FILE_EXISTS_ERROR is specified.'); + $this->assertEqual($contents, file_get_contents($existing->uri), 'Contents of existing file were unchanged.'); // Check that no hooks were called while failing. $this->assertFileHooksCalled(array()); @@ -2210,17 +2353,20 @@ // Test generating an URL to a created file. $file = $this->createFile(); $url = file_create_url($file->uri); - $this->assertEqual($GLOBALS['base_url'] . '/' . file_stream_wrapper_get_instance_by_scheme('public')->getDirectoryPath() . '/' . $file->filename, $url, t('Correctly generated a URL for a created file.')); + // URLs can't contain characters outside the ASCII set so $filename has to be + // encoded. + $filename = $GLOBALS['base_url'] . '/' . file_stream_wrapper_get_instance_by_scheme('public')->getDirectoryPath() . '/' . rawurlencode($file->filename); + $this->assertEqual($filename, $url, 'Correctly generated a URL for a created file.'); $this->drupalHead($url); - $this->assertResponse(200, t('Confirmed that the generated URL is correct by downloading the created file.')); + $this->assertResponse(200, 'Confirmed that the generated URL is correct by downloading the created file.'); // Test generating an URL to a shipped file (i.e. a file that is part of // Drupal core, a module or a theme, for example a JavaScript file). $filepath = 'misc/jquery.js'; $url = file_create_url($filepath); - $this->assertEqual($GLOBALS['base_url'] . '/' . $filepath, $url, t('Correctly generated a URL for a shipped file.')); + $this->assertEqual($GLOBALS['base_url'] . '/' . $filepath, $url, 'Correctly generated a URL for a shipped file.'); $this->drupalHead($url); - $this->assertResponse(200, t('Confirmed that the generated URL is correct by downloading the shipped file.')); + $this->assertResponse(200, 'Confirmed that the generated URL is correct by downloading the shipped file.'); } /** @@ -2230,25 +2376,29 @@ // Set file downloads to private so handler functions get called. // Create a file. - $file = $this->createFile(NULL, NULL, 'private'); + $contents = $this->randomName(8); + $file = $this->createFile(NULL, $contents, 'private'); $url = file_create_url($file->uri); // Set file_test access header to allow the download. file_test_set_return('download', array('x-foo' => 'Bar')); - $this->drupalHead($url); + $this->drupalGet($url); $headers = $this->drupalGetHeaders(); - $this->assertEqual($headers['x-foo'] , 'Bar', t('Found header set by file_test module on private download.')); - $this->assertResponse(200, t('Correctly allowed access to a file when file_test provides headers.')); + $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 transferred correctly. + $this->assertEqual($contents, $this->content, 'Contents of the file are correct.'); // Deny access to all downloads via a -1 header. file_test_set_return('download', -1); $this->drupalHead($url); - $this->assertResponse(403, t('Correctly denied access to a file when file_test sets the header to -1.')); + $this->assertResponse(403, 'Correctly denied access to a file when file_test sets the header to -1.'); // Try non-existent file. $url = file_create_url('private://' . $this->randomName()); $this->drupalHead($url); - $this->assertResponse(404, t('Correctly returned 404 response for a non-existent file.')); + $this->assertResponse(404, 'Correctly returned 404 response for a non-existent file.'); } /** @@ -2302,7 +2452,7 @@ $file = $this->createFile($filepath, NULL, $scheme); $url = file_create_url($file->uri); - $this->assertEqual($url, $expected_url, t('Generated URL matches expected URL.')); + $this->assertEqual($url, $expected_url, 'Generated URL matches expected URL.'); if ($scheme == 'private') { // Tell the implementation of hook_file_download() in file_test.module @@ -2312,7 +2462,7 @@ $this->drupalGet($url); if ($this->assertResponse(200) == 'pass') { - $this->assertRaw(file_get_contents($file->uri), t('Contents of the file are correct.')); + $this->assertRaw(file_get_contents($file->uri), 'Contents of the file are correct.'); } file_delete($file); @@ -2346,28 +2496,28 @@ variable_set('file_test_hook_file_url_alter', 'cdn'); $filepath = 'misc/jquery.js'; $url = file_create_url($filepath); - $this->assertEqual(FILE_URL_TEST_CDN_1 . '/' . $filepath, $url, t('Correctly generated a CDN URL for a shipped file.')); + $this->assertEqual(FILE_URL_TEST_CDN_1 . '/' . $filepath, $url, 'Correctly generated a CDN URL for a shipped file.'); $filepath = 'misc/favicon.ico'; $url = file_create_url($filepath); - $this->assertEqual(FILE_URL_TEST_CDN_2 . '/' . $filepath, $url, t('Correctly generated a CDN URL for a shipped file.')); + $this->assertEqual(FILE_URL_TEST_CDN_2 . '/' . $filepath, $url, 'Correctly generated a CDN URL for a shipped file.'); // Test alteration of file URLs to use root-relative URLs. variable_set('file_test_hook_file_url_alter', 'root-relative'); $filepath = 'misc/jquery.js'; $url = file_create_url($filepath); - $this->assertEqual(base_path() . '/' . $filepath, $url, t('Correctly generated a root-relative URL for a shipped file.')); + $this->assertEqual(base_path() . '/' . $filepath, $url, 'Correctly generated a root-relative URL for a shipped file.'); $filepath = 'misc/favicon.ico'; $url = file_create_url($filepath); - $this->assertEqual(base_path() . '/' . $filepath, $url, t('Correctly generated a root-relative URL for a shipped file.')); + $this->assertEqual(base_path() . '/' . $filepath, $url, 'Correctly generated a root-relative URL for a shipped file.'); // Test alteration of file URLs to use protocol-relative URLs. variable_set('file_test_hook_file_url_alter', 'protocol-relative'); $filepath = 'misc/jquery.js'; $url = file_create_url($filepath); - $this->assertEqual('/' . base_path() . '/' . $filepath, $url, t('Correctly generated a protocol-relative URL for a shipped file.')); + $this->assertEqual('/' . base_path() . '/' . $filepath, $url, 'Correctly generated a protocol-relative URL for a shipped file.'); $filepath = 'misc/favicon.ico'; $url = file_create_url($filepath); - $this->assertEqual('/' . base_path() . '/' . $filepath, $url, t('Correctly generated a protocol-relative URL for a shipped file.')); + $this->assertEqual('/' . base_path() . '/' . $filepath, $url, 'Correctly generated a protocol-relative URL for a shipped file.'); } /** @@ -2381,19 +2531,19 @@ $file = $this->createFile(); $url = file_create_url($file->uri); $public_directory_path = file_stream_wrapper_get_instance_by_scheme('public')->getDirectoryPath(); - $this->assertEqual(FILE_URL_TEST_CDN_2 . '/' . $public_directory_path . '/' . $file->filename, $url, t('Correctly generated a CDN URL for a created file.')); + $this->assertEqual(FILE_URL_TEST_CDN_2 . '/' . $public_directory_path . '/' . $file->filename, $url, 'Correctly generated a CDN URL for a created file.'); // Test alteration of file URLs to use root-relative URLs. variable_set('file_test_hook_file_url_alter', 'root-relative'); $file = $this->createFile(); $url = file_create_url($file->uri); - $this->assertEqual(base_path() . '/' . $public_directory_path . '/' . $file->filename, $url, t('Correctly generated a root-relative URL for a created file.')); + $this->assertEqual(base_path() . '/' . $public_directory_path . '/' . $file->filename, $url, 'Correctly generated a root-relative URL for a created file.'); // Test alteration of file URLs to use a protocol-relative URLs. variable_set('file_test_hook_file_url_alter', 'protocol-relative'); $file = $this->createFile(); $url = file_create_url($file->uri); - $this->assertEqual('/' . base_path() . '/' . $public_directory_path . '/' . $file->filename, $url, t('Correctly generated a protocol-relative URL for a created file.')); + $this->assertEqual('/' . base_path() . '/' . $public_directory_path . '/' . $file->filename, $url, 'Correctly generated a protocol-relative URL for a created file.'); } } @@ -2413,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'; } /** @@ -2423,8 +2574,17 @@ variable_set('allow_insecure_uploads', 0); $munged_name = file_munge_filename($this->name, '', TRUE); $messages = drupal_get_messages(); - $this->assertTrue(in_array(t('For security reasons, your upload has been renamed to %filename.', array('%filename' => $munged_name)), $messages['status']), t('Alert properly set when a file is renamed.')); - $this->assertNotEqual($munged_name, $this->name, t('The new filename (%munged) has been modified from the original (%original)', array('%munged' => $munged_name, '%original' => $this->name))); + $this->assertTrue(in_array(t('For security reasons, your upload has been renamed to %filename.', array('%filename' => $munged_name)), $messages['status']), 'Alert properly set when a file is renamed.'); + $this->assertNotEqual($munged_name, $this->name, format_string('The new filename (%munged) has been modified from the original (%original)', array('%munged' => $munged_name, '%original' => $this->name))); + } + + /** + * Tests munging with a null byte in the filename. + */ + function testMungeNullByte() { + $prefix = $this->randomName(); + $filename = $prefix . '.' . $this->bad_extension . "\0.txt"; + $this->assertEqual(file_munge_filename($filename, ''), $prefix . '.' . $this->bad_extension . '_.txt', 'A filename with a null byte is correctly munged to remove the null byte.'); } /** @@ -2434,16 +2594,20 @@ function testMungeIgnoreInsecure() { variable_set('allow_insecure_uploads', 1); $munged_name = file_munge_filename($this->name, ''); - $this->assertIdentical($munged_name, $this->name, t('The original filename (%original) matches the munged filename (%munged) when insecure uploads are enabled.', array('%munged' => $munged_name, '%original' => $this->name))); + $this->assertIdentical($munged_name, $this->name, format_string('The original filename (%original) matches the munged filename (%munged) when insecure uploads are enabled.', array('%munged' => $munged_name, '%original' => $this->name))); } /** * 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, t('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))); } /** @@ -2452,7 +2616,7 @@ function testUnMunge() { $munged_name = file_munge_filename($this->name, '', FALSE); $unmunged_name = file_unmunge_filename($munged_name); - $this->assertIdentical($unmunged_name, $this->name, t('The unmunged (%unmunged) filename matches the original (%original)', array('%unmunged' => $unmunged_name, '%original' => $this->name))); + $this->assertIdentical($unmunged_name, $this->name, format_string('The unmunged (%unmunged) filename matches the original (%original)', array('%unmunged' => $unmunged_name, '%original' => $this->name))); } } @@ -2476,7 +2640,7 @@ * Test mapping of mimetypes from filenames. */ public function testFileMimeTypeDetection() { - $prefix = 'simpletest://'; + $prefix = 'public://'; $test_case = array( 'test.jar' => 'application/java-archive', @@ -2492,17 +2656,18 @@ 'foo.file_test_1' => 'madeup/file_test_1', 'foo.file_test_2' => 'madeup/file_test_2', 'foo.doc' => 'madeup/doc', + 'test.ogg' => 'audio/ogg', ); // Test using default mappings. foreach ($test_case as $input => $expected) { // Test stream [URI]. $output = file_get_mimetype($prefix . $input); - $this->assertIdentical($output, $expected, t('Mimetype for %input is %output (expected: %expected).', array('%input' => $input, '%output' => $output, '%expected' => $expected))); + $this->assertIdentical($output, $expected, format_string('Mimetype for %input is %output (expected: %expected).', array('%input' => $input, '%output' => $output, '%expected' => $expected))); // Test normal path equivalent $output = file_get_mimetype($input); - $this->assertIdentical($output, $expected, t('Mimetype (using default mappings) for %input is %output (expected: %expected).', array('%input' => $input, '%output' => $output, '%expected' => $expected))); + $this->assertIdentical($output, $expected, format_string('Mimetype (using default mappings) for %input is %output (expected: %expected).', array('%input' => $input, '%output' => $output, '%expected' => $expected))); } // Now test passing in the map. @@ -2530,11 +2695,12 @@ 'foo.file_test_1' => 'application/octet-stream', 'foo.file_test_2' => 'application/octet-stream', 'foo.doc' => 'application/octet-stream', + 'test.ogg' => 'application/octet-stream', ); foreach ($test_case as $input => $expected) { $output = file_get_mimetype($input, $mapping); - $this->assertIdentical($output, $expected, t('Mimetype (using passed-in mappings) for %input is %output (expected: %expected).', array('%input' => $input, '%output' => $output, '%expected' => $expected))); + $this->assertIdentical($output, $expected, format_string('Mimetype (using passed-in mappings) for %input is %output (expected: %expected).', array('%input' => $input, '%output' => $output, '%expected' => $expected))); } } } @@ -2570,9 +2736,9 @@ */ function testGetClassName() { // Check the dummy scheme. - $this->assertEqual($this->classname, file_stream_wrapper_get_class($this->scheme), t('Got correct class name for dummy scheme.')); + $this->assertEqual($this->classname, file_stream_wrapper_get_class($this->scheme), 'Got correct class name for dummy scheme.'); // Check core's scheme. - $this->assertEqual('DrupalPublicStreamWrapper', file_stream_wrapper_get_class('public'), t('Got correct class name for public scheme.')); + $this->assertEqual('DrupalPublicStreamWrapper', file_stream_wrapper_get_class('public'), 'Got correct class name for public scheme.'); } /** @@ -2580,10 +2746,10 @@ */ function testGetInstanceByScheme() { $instance = file_stream_wrapper_get_instance_by_scheme($this->scheme); - $this->assertEqual($this->classname, get_class($instance), t('Got correct class type for dummy scheme.')); + $this->assertEqual($this->classname, get_class($instance), 'Got correct class type for dummy scheme.'); $instance = file_stream_wrapper_get_instance_by_scheme('public'); - $this->assertEqual('DrupalPublicStreamWrapper', get_class($instance), t('Got correct class type for public scheme.')); + $this->assertEqual('DrupalPublicStreamWrapper', get_class($instance), 'Got correct class type for public scheme.'); } /** @@ -2591,30 +2757,90 @@ */ function testUriFunctions() { $instance = file_stream_wrapper_get_instance_by_uri($this->scheme . '://foo'); - $this->assertEqual($this->classname, get_class($instance), t('Got correct class type for dummy URI.')); + $this->assertEqual($this->classname, get_class($instance), 'Got correct class type for dummy URI.'); $instance = file_stream_wrapper_get_instance_by_uri('public://foo'); - $this->assertEqual('DrupalPublicStreamWrapper', get_class($instance), t('Got correct class type for public URI.')); + $this->assertEqual('DrupalPublicStreamWrapper', get_class($instance), 'Got correct class type for public URI.'); // Test file_uri_target(). - $this->assertEqual(file_uri_target('public://foo/bar.txt'), 'foo/bar.txt', t('Got a valid stream target from public://foo/bar.txt.')); - $this->assertFalse(file_uri_target('foo/bar.txt'), t('foo/bar.txt is not a valid stream.')); + $this->assertEqual(file_uri_target('public://foo/bar.txt'), 'foo/bar.txt', 'Got a valid stream target from public://foo/bar.txt.'); + $this->assertFalse(file_uri_target('foo/bar.txt'), 'foo/bar.txt is not a valid stream.'); // Test file_build_uri() and DrupalLocalStreamWrapper::getDirectoryPath(). - $this->assertEqual(file_build_uri('foo/bar.txt'), 'public://foo/bar.txt', t('Expected scheme was added.')); - $this->assertEqual(file_stream_wrapper_get_instance_by_scheme('public')->getDirectoryPath(), variable_get('file_public_path'), t('Expected default directory path was returned.')); - $this->assertEqual(file_stream_wrapper_get_instance_by_scheme('temporary')->getDirectoryPath(), variable_get('file_temporary_path'), t('Expected temporary directory path was returned.')); + $this->assertEqual(file_build_uri('foo/bar.txt'), 'public://foo/bar.txt', 'Expected scheme was added.'); + $this->assertEqual(file_stream_wrapper_get_instance_by_scheme('public')->getDirectoryPath(), variable_get('file_public_path'), 'Expected default directory path was returned.'); + $this->assertEqual(file_stream_wrapper_get_instance_by_scheme('temporary')->getDirectoryPath(), variable_get('file_temporary_path'), 'Expected temporary directory path was returned.'); variable_set('file_default_scheme', 'private'); - $this->assertEqual(file_build_uri('foo/bar.txt'), 'private://foo/bar.txt', t('Got a valid URI from foo/bar.txt.')); + $this->assertEqual(file_build_uri('foo/bar.txt'), 'private://foo/bar.txt', 'Got a valid URI from foo/bar.txt.'); } /** * Test the scheme functions. */ function testGetValidStreamScheme() { - $this->assertEqual('foo', file_uri_scheme('foo://pork//chops'), t('Got the correct scheme from foo://asdf')); - $this->assertTrue(file_stream_wrapper_valid_scheme(file_uri_scheme('public://asdf')), t('Got a valid stream scheme from public://asdf')); - $this->assertFalse(file_stream_wrapper_valid_scheme(file_uri_scheme('foo://asdf')), t('Did not get a valid stream scheme from foo://asdf')); + $this->assertEqual('foo', file_uri_scheme('foo://pork//chops'), 'Got the correct scheme from foo://asdf'); + $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.0/modules/simpletest/tests/file_test.info drupal-7.66/modules/simpletest/tests/file_test.info --- drupal-7.0/modules/simpletest/tests/file_test.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/file_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: file_test.info,v 1.1 2008/09/20 07:35:53 webchick Exp $ name = "File test" description = "Support module for file handling tests." package = Testing @@ -7,8 +6,7 @@ files[] = file_test.module hidden = TRUE -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/simpletest/tests/file_test.module drupal-7.66/modules/simpletest/tests/file_test.module --- drupal-7.0/modules/simpletest/tests/file_test.module 2010-08-22 15:52:58.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/file_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: file_test.module,v 1.25 2010/08/22 13:52:58 dries Exp $ /** * @file @@ -38,6 +37,11 @@ 'class' => 'DrupalDummyStreamWrapper', 'description' => t('Dummy wrapper for simpletest.'), ), + 'dummy-remote' => array( + 'name' => t('Dummy files (remote)'), + 'class' => 'DrupalDummyRemoteStreamWrapper', + 'description' => t('Dummy wrapper for simpletest (remote).'), + ), ); } @@ -134,7 +138,7 @@ /** * Reset/initialize the history of calls to the file_* hooks. * - * @see file_test_get_calls() + * @see file_test_get_calls() * @see file_test_reset() */ function file_test_reset() { @@ -443,3 +447,15 @@ } } +/** + * Helper class for testing the stream wrapper registry. + * + * Dummy remote stream wrapper implementation (dummy-remote://). + * + * Basically just the public scheme but not returning a local file for realpath. + */ +class DrupalDummyRemoteStreamWrapper extends DrupalPublicStreamWrapper { + function realpath() { + return FALSE; + } +} diff -Naur drupal-7.0/modules/simpletest/tests/filetransfer.test drupal-7.66/modules/simpletest/tests/filetransfer.test --- drupal-7.0/modules/simpletest/tests/filetransfer.test 2010-09-01 22:08:17.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/filetransfer.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: filetransfer.test,v 1.9 2010/09/01 20:08:17 dries Exp $ class FileTranferTest extends DrupalWebTestCase { diff -Naur drupal-7.0/modules/simpletest/tests/filter_test.info drupal-7.66/modules/simpletest/tests/filter_test.info --- drupal-7.0/modules/simpletest/tests/filter_test.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/filter_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: filter_test.info,v 1.2 2010/12/20 19:59:43 webchick Exp $ name = Filter test module description = Tests filter hooks and functions. package = Testing @@ -6,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/simpletest/tests/filter_test.module drupal-7.66/modules/simpletest/tests/filter_test.module --- drupal-7.0/modules/simpletest/tests/filter_test.module 2010-09-18 04:18:35.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/filter_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: filter_test.module,v 1.6 2010/09/18 02:18:35 dries Exp $ /** * @file @@ -45,6 +44,8 @@ } /** + * Implements callback_filter_process(). + * * Process handler for filter_test_replace filter. * * Replaces all text with filter and text format information. diff -Naur drupal-7.0/modules/simpletest/tests/form.test drupal-7.66/modules/simpletest/tests/form.test --- drupal-7.0/modules/simpletest/tests/form.test 2010-12-30 23:52:24.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/form.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: form.test,v 1.79 2010/12/30 22:52:24 webchick Exp $ /** * @file @@ -53,7 +52,7 @@ $elements['radios']['element'] = array('#title' => $this->randomName(), '#type' => 'radios', '#options' => array('' => t('None'), $this->randomName(), $this->randomName(), $this->randomName())); $elements['radios']['empty_values'] = $empty_arrays; - $elements['checkbox']['element'] = array('#title' => $this->randomName(), '#type' => 'checkbox', '#required' => TRUE, '#title' => $this->randomName()); + $elements['checkbox']['element'] = array('#title' => $this->randomName(), '#type' => 'checkbox', '#required' => TRUE); $elements['checkbox']['empty_values'] = $empty_checkbox; $elements['checkboxes']['element'] = array('#title' => $this->randomName(), '#type' => 'checkboxes', '#options' => array($this->randomName(), $this->randomName(), $this->randomName())); @@ -83,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(); @@ -123,6 +126,106 @@ } /** + * Tests validation for required checkbox, select, and radio elements. + * + * Submits a test form containing several types of form elements. The form + * is submitted twice, first without values for required fields and then + * with values. Each submission is checked for relevant error messages. + * + * @see form_test_validate_required_form() + */ + function testRequiredCheckboxesRadio() { + $form = $form_state = array(); + $form = form_test_validate_required_form($form, $form_state); + + // Attempt to submit the form with no required fields set. + $edit = array(); + $this->drupalPost('form-test/validate-required', $edit, 'Submit'); + + // The only error messages that should appear are the relevant 'required' + // messages for each field. + $expected = array(); + foreach (array('textfield', 'checkboxes', 'select', 'radios') as $key) { + $expected[] = t('!name field is required.', array('!name' => $form[$key]['#title'])); + } + + // Check the page for error messages. + $errors = $this->xpath('//div[contains(@class, "error")]//li'); + foreach ($errors as $error) { + $expected_key = array_search($error[0], $expected); + // If the error message is not one of the expected messages, fail. + if ($expected_key === FALSE) { + $this->fail(format_string("Unexpected error message: @error", array('@error' => $error[0]))); + } + // Remove the expected message from the list once it is found. + else { + unset($expected[$expected_key]); + } + } + + // Fail if any expected messages were not found. + foreach ($expected as $not_found) { + $this->fail(format_string("Found error message: @error", array('@error' => $not_found))); + } + + // Verify that input elements are still empty. + $this->assertFieldByName('textfield', ''); + $this->assertNoFieldChecked('edit-checkboxes-foo'); + $this->assertNoFieldChecked('edit-checkboxes-bar'); + $this->assertOptionSelected('edit-select', ''); + $this->assertNoFieldChecked('edit-radios-foo'); + $this->assertNoFieldChecked('edit-radios-bar'); + $this->assertNoFieldChecked('edit-radios-optional-foo'); + $this->assertNoFieldChecked('edit-radios-optional-bar'); + $this->assertNoFieldChecked('edit-radios-optional-default-value-false-foo'); + $this->assertNoFieldChecked('edit-radios-optional-default-value-false-bar'); + + // Submit again with required fields set and verify that there are no + // error messages. + $edit = array( + 'textfield' => $this->randomString(), + 'checkboxes[foo]' => TRUE, + 'select' => 'foo', + 'radios' => 'bar', + ); + $this->drupalPost(NULL, $edit, 'Submit'); + $this->assertNoFieldByXpath('//div[contains(@class, "error")]', FALSE, 'No error message is displayed when all required fields are filled.'); + $this->assertRaw("The form_test_validate_required_form form was submitted successfully.", 'Validation form submitted successfully.'); + } + + /** + * Tests validation for required textfield element without title. + * + * Submits a test form containing a textfield form elements without title. + * The form is submitted twice, first without value for the required field + * and then with value. Each submission is checked for relevant error + * messages. + * + * @see form_test_validate_required_form_no_title() + */ + function testRequiredTextfieldNoTitle() { + $form = $form_state = array(); + $form = form_test_validate_required_form_no_title($form, $form_state); + + // Attempt to submit the form with no required field set. + $edit = array(); + $this->drupalPost('form-test/validate-required-no-title', $edit, 'Submit'); + $this->assertNoRaw("The form_test_validate_required_form_no_title form was submitted successfully.", 'Validation form submitted successfully.'); + + // Check the page for the error class on the textfield. + $this->assertFieldByXPath('//input[contains(@class, "error")]', FALSE, 'Error input form element class found.'); + + // Submit again with required fields set and verify that there are no + // error messages. + $edit = array( + 'textfield' => $this->randomString(), + ); + $this->drupalPost(NULL, $edit, 'Submit'); + $this->assertNoFieldByXpath('//input[contains(@class, "error")]', FALSE, 'No error input form element class found.'); + $this->assertRaw("The form_test_validate_required_form_no_title form was submitted successfully.", 'Validation form submitted successfully.'); + } + + /** * Test default value handling for checkboxes. * * @see _form_test_checkbox() @@ -131,7 +234,7 @@ // First, try to submit without the required checkbox. $edit = array(); $this->drupalPost('form-test/checkbox', $edit, t('Submit')); - $this->assertRaw(t('!name field is required.', array('!name' => 'required_checkbox')), t('A required checkbox is actually mandatory')); + $this->assertRaw(t('!name field is required.', array('!name' => 'required_checkbox')), 'A required checkbox is actually mandatory'); // Now try to submit the form correctly. $values = drupal_json_decode($this->drupalPost(NULL, array('required_checkbox' => 1), t('Submit'))); @@ -144,7 +247,7 @@ 'zero_checkbox_off' => '', ); foreach ($expected_values as $widget => $expected_value) { - $this->assertEqual($values[$widget], $expected_value, t('Checkbox %widget returns expected value (expected: %expected, got: %value)', array( + $this->assertEqual($values[$widget], $expected_value, format_string('Checkbox %widget returns expected value (expected: %expected, got: %value)', array( '%widget' => var_export($widget, TRUE), '%expected' => var_export($expected_value, TRUE), '%value' => var_export($values[$widget], TRUE), @@ -208,7 +311,7 @@ 'multiple_no_default_required' => array('three' => 'three'), ); foreach ($expected as $key => $value) { - $this->assertIdentical($values[$key], $value, t('@name: @actual is equal to @expected.', array( + $this->assertIdentical($values[$key], $value, format_string('@name: @actual is equal to @expected.', array( '@name' => $key, '@actual' => var_export($values[$key], TRUE), '@expected' => var_export($value, TRUE), @@ -259,7 +362,7 @@ // All the elements should be marked as disabled, including the ones below // the disabled container. - $this->assertEqual(count($disabled_elements), 32, t('The correct elements have the disabled property in the HTML code.')); + $this->assertEqual(count($disabled_elements), 32, 'The correct elements have the disabled property in the HTML code.'); $this->drupalPost(NULL, $edit, t('Submit')); $returned_values['hijacked'] = drupal_json_decode($this->content); @@ -288,7 +391,7 @@ // Checkboxes values are not filtered out. $values[$key] = array_filter($values[$key]); } - $this->assertIdentical($expected_value, $values[$key], t('Default value for %type: expected %expected, returned %returned.', array('%type' => $key, '%expected' => var_export($expected_value, TRUE), '%returned' => var_export($values[$key], TRUE)))); + $this->assertIdentical($expected_value, $values[$key], format_string('Default value for %type: expected %expected, returned %returned.', array('%type' => $key, '%expected' => var_export($expected_value, TRUE), '%returned' => var_export($values[$key], TRUE)))); } // Recurse children. @@ -339,7 +442,7 @@ ':div-class' => $class, ':value' => isset($item['#value']) ? $item['#value'] : '', )); - $this->assertTrue(isset($element[0]), t('Disabled form element class found for #type %type.', array('%type' => $item['#type']))); + $this->assertTrue(isset($element[0]), format_string('Disabled form element class found for #type %type.', array('%type' => $item['#type']))); } // Verify special element #type text-format. @@ -347,12 +450,12 @@ ':name' => 'text_format[value]', ':div-class' => 'form-disabled', )); - $this->assertTrue(isset($element[0]), t('Disabled form element class found for #type %type.', array('%type' => 'text_format[value]'))); + $this->assertTrue(isset($element[0]), format_string('Disabled form element class found for #type %type.', array('%type' => 'text_format[value]'))); $element = $this->xpath('//div[contains(@class, :div-class)]/descendant::select[@name=:name]', array( ':name' => 'text_format[format]', ':div-class' => 'form-disabled', )); - $this->assertTrue(isset($element[0]), t('Disabled form element class found for #type %type.', array('%type' => 'text_format[format]'))); + $this->assertTrue(isset($element[0]), format_string('Disabled form element class found for #type %type.', array('%type' => 'text_format[format]'))); } /** @@ -365,7 +468,65 @@ $checkbox = $this->xpath('//input[@name="checkboxes[two]"]'); $checkbox[0]['value'] = 'FORGERY'; $this->drupalPost(NULL, array('checkboxes[one]' => TRUE, 'checkboxes[two]' => TRUE), t('Submit')); - $this->assertText('An illegal choice has been detected.', t('Input forgery was detected.')); + $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))); } } @@ -422,7 +583,7 @@ ':id' => 'edit-' . $type . '-foo', ':class' => 'description', )); - $this->assertTrue(count($elements), t('Custom %type option description found.', array( + $this->assertTrue(count($elements), format_string('Custom %type option description found.', array( '%type' => $type, ))); } @@ -459,7 +620,7 @@ 'system_form_form_test_alter_form_alter() executed.', ); $content = preg_replace('/\s+/', ' ', filter_xss($this->content, array())); - $this->assert(strpos($content, implode(' ', $expected)) !== FALSE, t('Form alter hooks executed in the expected order.')); + $this->assert(strpos($content, implode(' ', $expected)) !== FALSE, 'Form alter hooks executed in the expected order.'); } } @@ -490,8 +651,8 @@ 'name' => 'element_validate', ); $this->drupalPost(NULL, $edit, 'Save'); - $this->assertFieldByName('name', '#value changed by #element_validate', t('Form element #value was altered.')); - $this->assertText('Name value: value changed by form_set_value() in #element_validate', t('Form element value in $form_state was altered.')); + $this->assertFieldByName('name', '#value changed by #element_validate', 'Form element #value was altered.'); + $this->assertText('Name value: value changed by form_set_value() in #element_validate', 'Form element value in $form_state was altered.'); // Verify that #validate handlers can alter the form and submitted // form values. @@ -499,8 +660,8 @@ 'name' => 'validate', ); $this->drupalPost(NULL, $edit, 'Save'); - $this->assertFieldByName('name', '#value changed by #validate', t('Form element #value was altered.')); - $this->assertText('Name value: value changed by form_set_value() in #validate', t('Form element value in $form_state was altered.')); + $this->assertFieldByName('name', '#value changed by #validate', 'Form element #value was altered.'); + $this->assertText('Name value: value changed by form_set_value() in #validate', 'Form element value in $form_state was altered.'); // Verify that #element_validate handlers can make form elements // inaccessible, but values persist. @@ -508,13 +669,33 @@ 'name' => 'element_validate_access', ); $this->drupalPost(NULL, $edit, 'Save'); - $this->assertNoFieldByName('name', t('Form element was hidden.')); - $this->assertText('Name value: element_validate_access', t('Value for inaccessible form element exists.')); + $this->assertNoFieldByName('name', 'Form element was hidden.'); + $this->assertText('Name value: element_validate_access', 'Value for inaccessible form element exists.'); // Verify that value for inaccessible form element persists. $this->drupalPost(NULL, array(), 'Save'); - $this->assertNoFieldByName('name', t('Form element was hidden.')); - $this->assertText('Name value: element_validate_access', t('Value for inaccessible form element exists.')); + $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.'); } /** @@ -522,7 +703,7 @@ */ function testValidateLimitErrors() { $edit = array( - 'test' => 'invalid', + 'test' => 'invalid', 'test_numeric_index[0]' => 'invalid', 'test_substring[foo]' => 'invalid', ); @@ -558,6 +739,17 @@ $this->assertText(t('!name field is required.', array('!name' => 'Title'))); $this->assertText('Test element is invalid'); } + + /** + * Tests error border of multiple fields with same name in a page. + */ + function testMultiFormSameNameErrorClass() { + $this->drupalGet('form-test/double-form'); + $edit = array(); + $this->drupalPost(NULL, $edit, t('Save')); + $this->assertFieldByXpath('//input[@id="edit-name" and contains(@class, "error")]', NULL, 'Error input form element class found for first element.'); + $this->assertNoFieldByXpath('//input[@id="edit-name--2" and contains(@class, "error")]', NULL, 'No error input form element class found for second element.'); + } } /** @@ -587,49 +779,63 @@ // Check that the checkbox/radio processing is not interfering with // basic placement. $elements = $this->xpath('//input[@id="edit-form-checkboxes-test-third-checkbox"]/following-sibling::label[@for="edit-form-checkboxes-test-third-checkbox" and @class="option"]'); - $this->assertTrue(isset($elements[0]), t("Label follows field and label option class correct for regular checkboxes.")); + $this->assertTrue(isset($elements[0]), "Label follows field and label option class correct for regular checkboxes."); + + // Make sure the label is rendered for checkboxes. + $elements = $this->xpath('//input[@id="edit-form-checkboxes-test-0"]/following-sibling::label[@for="edit-form-checkboxes-test-0" and @class="option"]'); + $this->assertTrue(isset($elements[0]), "Label 0 found checkbox."); $elements = $this->xpath('//input[@id="edit-form-radios-test-second-radio"]/following-sibling::label[@for="edit-form-radios-test-second-radio" and @class="option"]'); - $this->assertTrue(isset($elements[0]), t("Label follows field and label option class correct for regular radios.")); + $this->assertTrue(isset($elements[0]), "Label follows field and label option class correct for regular radios."); + + // Make sure the label is rendered for radios. + $elements = $this->xpath('//input[@id="edit-form-radios-test-0"]/following-sibling::label[@for="edit-form-radios-test-0" and @class="option"]'); + $this->assertTrue(isset($elements[0]), "Label 0 found radios."); // Exercise various defaults for checkboxes and modifications to ensure - // appropriate override and correct behaviour. + // appropriate override and correct behavior. $elements = $this->xpath('//input[@id="edit-form-checkbox-test"]/following-sibling::label[@for="edit-form-checkbox-test" and @class="option"]'); - $this->assertTrue(isset($elements[0]), t("Label follows field and label option class correct for a checkbox by default.")); + $this->assertTrue(isset($elements[0]), "Label follows field and label option class correct for a checkbox by default."); // Exercise various defaults for textboxes and modifications to ensure - // appropriate override and correct behaviour. + // appropriate override and correct behavior. $elements = $this->xpath('//label[@for="edit-form-textfield-test-title-and-required"]/child::span[@class="form-required"]/parent::*/following-sibling::input[@id="edit-form-textfield-test-title-and-required"]'); - $this->assertTrue(isset($elements[0]), t("Label preceeds textfield, with required marker inside label.")); + $this->assertTrue(isset($elements[0]), "Label precedes textfield, with required marker inside label."); $elements = $this->xpath('//input[@id="edit-form-textfield-test-no-title-required"]/preceding-sibling::label[@for="edit-form-textfield-test-no-title-required"]/span[@class="form-required"]'); - $this->assertTrue(isset($elements[0]), t("Label tag with required marker preceeds required textfield with no title.")); + $this->assertTrue(isset($elements[0]), "Label tag with required marker precedes required textfield with no title."); $elements = $this->xpath('//input[@id="edit-form-textfield-test-title-invisible"]/preceding-sibling::label[@for="edit-form-textfield-test-title-invisible" and @class="element-invisible"]'); - $this->assertTrue(isset($elements[0]), t("Label preceeding field and label class is element-invisible.")); + $this->assertTrue(isset($elements[0]), "Label preceding field and label class is element-invisible."); $elements = $this->xpath('//input[@id="edit-form-textfield-test-title"]/preceding-sibling::span[@class="form-required"]'); - $this->assertFalse(isset($elements[0]), t("No required marker on non-required field.")); + $this->assertFalse(isset($elements[0]), "No required marker on non-required field."); $elements = $this->xpath('//input[@id="edit-form-textfield-test-title-after"]/following-sibling::label[@for="edit-form-textfield-test-title-after" and @class="option"]'); - $this->assertTrue(isset($elements[0]), t("Label after field and label option class correct for text field.")); + $this->assertTrue(isset($elements[0]), "Label after field and label option class correct for text field."); $elements = $this->xpath('//label[@for="edit-form-textfield-test-title-no-show"]'); - $this->assertFalse(isset($elements[0]), t("No label tag when title set not to display.")); + $this->assertFalse(isset($elements[0]), "No label tag when title set not to display."); // Check #field_prefix and #field_suffix placement. $elements = $this->xpath('//span[@class="field-prefix"]/following-sibling::div[@id="edit-form-radios-test"]'); - $this->assertTrue(isset($elements[0]), t("Properly placed the #field_prefix element after the label and before the field.")); + $this->assertTrue(isset($elements[0]), "Properly placed the #field_prefix element after the label and before the field."); $elements = $this->xpath('//span[@class="field-suffix"]/preceding-sibling::div[@id="edit-form-radios-test"]'); - $this->assertTrue(isset($elements[0]), t("Properly places the #field_suffix element immediately after the form field.")); + $this->assertTrue(isset($elements[0]), "Properly places the #field_suffix element immediately after the form field."); // Check #prefix and #suffix placement. $elements = $this->xpath('//div[@id="form-test-textfield-title-prefix"]/following-sibling::div[contains(@class, \'form-item-form-textfield-test-title\')]'); - $this->assertTrue(isset($elements[0]), t("Properly places the #prefix element before the form item.")); + $this->assertTrue(isset($elements[0]), "Properly places the #prefix element before the form item."); $elements = $this->xpath('//div[@id="form-test-textfield-title-suffix"]/preceding-sibling::div[contains(@class, \'form-item-form-textfield-test-title\')]'); - $this->assertTrue(isset($elements[0]), t("Properly places the #suffix element before the form item.")); + $this->assertTrue(isset($elements[0]), "Properly places the #suffix element before the form item."); + + // Check title attribute for radios and checkboxes. + $elements = $this->xpath('//div[@id="edit-form-checkboxes-title-attribute"]'); + $this->assertEqual($elements[0]['title'], 'Checkboxes test' . ' (' . t('Required') . ')', 'Title attribute found.'); + $elements = $this->xpath('//div[@id="edit-form-radios-title-attribute"]'); + $this->assertEqual($elements[0]['title'], 'Radios test' . ' (' . t('Required') . ')', 'Title attribute found.'); } } @@ -658,14 +864,14 @@ $this->drupalGet('form_test/tableselect/multiple-true'); - $this->assertNoText(t('Empty text.'), t('Empty text should not be displayed.')); + $this->assertNoText(t('Empty text.'), 'Empty text should not be displayed.'); // Test for the presence of the Select all rows tableheader. - $this->assertFieldByXPath('//th[@class="select-all"]', NULL, t('Presence of the "Select all" checkbox.')); + $this->assertFieldByXPath('//th[@class="select-all"]', NULL, 'Presence of the "Select all" checkbox.'); $rows = array('row1', 'row2', 'row3'); foreach ($rows as $row) { - $this->assertFieldByXPath('//input[@type="checkbox"]', $row, t('Checkbox for value @row.', array('@row' => $row))); + $this->assertFieldByXPath('//input[@type="checkbox"]', $row, format_string('Checkbox for value @row.', array('@row' => $row))); } } @@ -675,14 +881,14 @@ function testMultipleFalse() { $this->drupalGet('form_test/tableselect/multiple-false'); - $this->assertNoText(t('Empty text.'), t('Empty text should not be displayed.')); + $this->assertNoText(t('Empty text.'), 'Empty text should not be displayed.'); // Test for the absence of the Select all rows tableheader. - $this->assertNoFieldByXPath('//th[@class="select-all"]', '', t('Absence of the "Select all" checkbox.')); + $this->assertNoFieldByXPath('//th[@class="select-all"]', '', 'Absence of the "Select all" checkbox.'); $rows = array('row1', 'row2', 'row3'); foreach ($rows as $row) { - $this->assertFieldByXPath('//input[@type="radio"]', $row, t('Radio button for value @row.', array('@row' => $row))); + $this->assertFieldByXPath('//input[@type="radio"]', $row, format_string('Radio button for value @row.', array('@row' => $row))); } } @@ -691,7 +897,7 @@ */ function testEmptyText() { $this->drupalGet('form_test/tableselect/empty-text'); - $this->assertText(t('Empty text.'), t('Empty text should be displayed.')); + $this->assertText(t('Empty text.'), 'Empty text should be displayed.'); } /** @@ -704,18 +910,18 @@ $edit['tableselect[row1]'] = TRUE; $this->drupalPost('form_test/tableselect/multiple-true', $edit, 'Submit'); - $this->assertText(t('Submitted: row1 = row1'), t('Checked checkbox row1')); - $this->assertText(t('Submitted: row2 = 0'), t('Unchecked checkbox row2.')); - $this->assertText(t('Submitted: row3 = 0'), t('Unchecked checkbox row3.')); + $this->assertText(t('Submitted: row1 = row1'), 'Checked checkbox row1'); + $this->assertText(t('Submitted: row2 = 0'), 'Unchecked checkbox row2.'); + $this->assertText(t('Submitted: row3 = 0'), 'Unchecked checkbox row3.'); // Test a submission with multiple checkboxes checked. $edit['tableselect[row1]'] = TRUE; $edit['tableselect[row3]'] = TRUE; $this->drupalPost('form_test/tableselect/multiple-true', $edit, 'Submit'); - $this->assertText(t('Submitted: row1 = row1'), t('Checked checkbox row1.')); - $this->assertText(t('Submitted: row2 = 0'), t('Unchecked checkbox row2.')); - $this->assertText(t('Submitted: row3 = row3'), t('Checked checkbox row3.')); + $this->assertText(t('Submitted: row1 = row1'), 'Checked checkbox row1.'); + $this->assertText(t('Submitted: row2 = 0'), 'Unchecked checkbox row2.'); + $this->assertText(t('Submitted: row3 = row3'), 'Checked checkbox row3.'); } @@ -725,7 +931,7 @@ function testMultipleFalseSubmit() { $edit['tableselect'] = 'row1'; $this->drupalPost('form_test/tableselect/multiple-false', $edit, 'Submit'); - $this->assertText(t('Submitted: row1'), t('Selected radio button')); + $this->assertText(t('Submitted: row1'), 'Selected radio button'); } /** @@ -734,18 +940,18 @@ function testAdvancedSelect() { // When #multiple = TRUE a Select all checkbox should be displayed by default. $this->drupalGet('form_test/tableselect/advanced-select/multiple-true-default'); - $this->assertFieldByXPath('//th[@class="select-all"]', NULL, t('Display a "Select all" checkbox by default when #multiple is TRUE.')); + $this->assertFieldByXPath('//th[@class="select-all"]', NULL, 'Display a "Select all" checkbox by default when #multiple is TRUE.'); // When #js_select is set to FALSE, a "Select all" checkbox should not be displayed. $this->drupalGet('form_test/tableselect/advanced-select/multiple-true-no-advanced-select'); - $this->assertNoFieldByXPath('//th[@class="select-all"]', NULL, t('Do not display a "Select all" checkbox when #js_select is FALSE.')); + $this->assertNoFieldByXPath('//th[@class="select-all"]', NULL, 'Do not display a "Select all" checkbox when #js_select is FALSE.'); // A "Select all" checkbox never makes sense when #multiple = FALSE, regardless of the value of #js_select. $this->drupalGet('form_test/tableselect/advanced-select/multiple-false-default'); - $this->assertNoFieldByXPath('//th[@class="select-all"]', NULL, t('Do not display a "Select all" checkbox when #multiple is FALSE.')); + $this->assertNoFieldByXPath('//th[@class="select-all"]', NULL, 'Do not display a "Select all" checkbox when #multiple is FALSE.'); $this->drupalGet('form_test/tableselect/advanced-select/multiple-false-advanced-select'); - $this->assertNoFieldByXPath('//th[@class="select-all"]', NULL, t('Do not display a "Select all" checkbox when #multiple is FALSE, even when #js_select is TRUE.')); + $this->assertNoFieldByXPath('//th[@class="select-all"]', NULL, 'Do not display a "Select all" checkbox when #multiple is FALSE, even when #js_select is TRUE.'); } @@ -764,11 +970,11 @@ // Test with a valid value. list($processed_form, $form_state, $errors) = $this->formSubmitHelper($form, array('tableselect' => array('row1' => 'row1'))); - $this->assertFalse(isset($errors['tableselect']), t('Option checker allows valid values for checkboxes.')); + $this->assertFalse(isset($errors['tableselect']), 'Option checker allows valid values for checkboxes.'); // Test with an invalid value. list($processed_form, $form_state, $errors) = $this->formSubmitHelper($form, array('tableselect' => array('non_existing_value' => 'non_existing_value'))); - $this->assertTrue(isset($errors['tableselect']), t('Option checker disallows invalid values for checkboxes.')); + $this->assertTrue(isset($errors['tableselect']), 'Option checker disallows invalid values for checkboxes.'); } @@ -789,13 +995,33 @@ // Test with a valid value. list($processed_form, $form_state, $errors) = $this->formSubmitHelper($form, array('tableselect' => 'row1')); - $this->assertFalse(isset($errors['tableselect']), t('Option checker allows valid values for radio buttons.')); + $this->assertFalse(isset($errors['tableselect']), 'Option checker allows valid values for radio buttons.'); // Test with an invalid value. list($processed_form, $form_state, $errors) = $this->formSubmitHelper($form, array('tableselect' => 'non_existing_value')); - $this->assertTrue(isset($errors['tableselect']), t('Option checker disallows invalid values for radio buttons.')); + $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. @@ -817,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); @@ -860,7 +1090,7 @@ $this->drupalGet('form_test/vertical-tabs'); $position1 = strpos($this->content, 'misc/vertical-tabs.js'); $position2 = strpos($this->content, 'misc/collapse.js'); - $this->assertTrue($position1 !== FALSE && $position2 !== FALSE && $position1 < $position2, t('vertical-tabs.js is included before collapse.js')); + $this->assertTrue($position1 !== FALSE && $position2 !== FALSE && $position1 < $position2, 'vertical-tabs.js is included before collapse.js'); } } @@ -913,7 +1143,7 @@ $this->drupalPost(NULL, $edit, 'Save'); $this->assertText('Form constructions: 4'); - $this->assertText('Title: new', t('The form storage has stored the values.')); + $this->assertText('Title: new', 'The form storage has stored the values.'); } /** @@ -937,7 +1167,7 @@ $this->drupalPost(NULL, $edit, 'Save'); $this->assertText('Form constructions: 3'); - $this->assertText('Title: new', t('The form storage has stored the values.')); + $this->assertText('Title: new', 'The form storage has stored the values.'); } /** @@ -945,7 +1175,7 @@ */ function testValidation() { $this->drupalPost('form_test/form-storage', array('title' => '', 'value' => 'value_is_set'), 'Continue submit'); - $this->assertPattern('/value_is_set/', t('The input values have been kept.')); + $this->assertPattern('/value_is_set/', 'The input values have been kept.'); } /** @@ -1012,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.'); + } } /** @@ -1035,8 +1494,8 @@ */ function testWrapperCallback() { $this->drupalGet('form_test/wrapper-callback'); - $this->assertText('Form wrapper callback element output.', t('The form contains form wrapper elements.')); - $this->assertText('Form builder element output.', t('The form contains form builder elements.')); + $this->assertText('Form wrapper callback element output.', 'The form contains form wrapper elements.'); + $this->assertText('Form builder element output.', 'The form contains form builder elements.'); } } @@ -1069,22 +1528,67 @@ ); // Verify that all internal Form API elements were removed. - $this->assertFalse(isset($values['form_id']), t('%element was removed.', array('%element' => 'form_id'))); - $this->assertFalse(isset($values['form_token']), t('%element was removed.', array('%element' => 'form_token'))); - $this->assertFalse(isset($values['form_build_id']), t('%element was removed.', array('%element' => 'form_build_id'))); - $this->assertFalse(isset($values['op']), t('%element was removed.', array('%element' => 'op'))); + $this->assertFalse(isset($values['form_id']), format_string('%element was removed.', array('%element' => 'form_id'))); + $this->assertFalse(isset($values['form_token']), format_string('%element was removed.', array('%element' => 'form_token'))); + $this->assertFalse(isset($values['form_build_id']), format_string('%element was removed.', array('%element' => 'form_build_id'))); + $this->assertFalse(isset($values['op']), format_string('%element was removed.', array('%element' => 'op'))); // Verify that all buttons were removed. - $this->assertFalse(isset($values['foo']), t('%element was removed.', array('%element' => 'foo'))); - $this->assertFalse(isset($values['bar']), t('%element was removed.', array('%element' => 'bar'))); - $this->assertFalse(isset($values['baz']['foo']), t('%element was removed.', array('%element' => 'foo'))); - $this->assertFalse(isset($values['baz']['baz']), t('%element was removed.', array('%element' => 'baz'))); + $this->assertFalse(isset($values['foo']), format_string('%element was removed.', array('%element' => 'foo'))); + $this->assertFalse(isset($values['bar']), format_string('%element was removed.', array('%element' => 'bar'))); + $this->assertFalse(isset($values['baz']['foo']), format_string('%element was removed.', array('%element' => 'foo'))); + $this->assertFalse(isset($values['baz']['baz']), format_string('%element was removed.', array('%element' => 'baz'))); // Verify that nested form value still exists. - $this->assertTrue(isset($values['baz']['beer']), t('Nested form value still exists.')); + $this->assertTrue(isset($values['baz']['beer']), 'Nested form value still exists.'); // Verify that actual form values equal resulting form values. - $this->assertEqual($values, $result, t('Expected form values equal actual form values.')); + $this->assertEqual($values, $result, 'Expected form values equal actual form values.'); + } +} + +/** + * Tests $form_state clearance with form elements having buttons. + */ +class FormStateValuesCleanAdvancedTestCase extends DrupalWebTestCase { + /** + * An image file path for uploading. + */ + protected $image; + + public static function getInfo() { + return array( + 'name' => 'Form state values clearance (advanced)', + 'description' => 'Test proper removal of submitted form values using form_state_values_clean() when having forms with elements containing buttons like "managed_file".', + 'group' => 'Form API', + ); + } + + function setUp() { + parent::setUp('form_test'); + } + + /** + * Tests form_state_values_clean(). + */ + function testFormStateValuesCleanAdvanced() { + + // Get an image for uploading. + $image_files = $this->drupalGetTestFiles('image'); + $this->image = current($image_files); + + // Check if the physical file is there. + $this->assertTrue(is_file($this->image->uri), "The image file we're going to upload exists."); + + // "Browse" for the desired file. + $edit = array('files[image]' => drupal_realpath($this->image->uri)); + + // Post the form. + $this->drupalPost('form_test/form-state-values-clean-advanced', $edit, t('Submit')); + + // Expecting a 200 HTTP code. + $this->assertResponse(200, 'Received a 200 response for posted test file.'); + $this->assertRaw(t('You WIN!'), 'Found the success message.'); } } @@ -1121,24 +1625,24 @@ $this->drupalPost('form-test/form-rebuild-preserve-values', $edit, 'Add more'); // Verify that initial elements retained their submitted values. - $this->assertFieldChecked('edit-checkbox-1-default-off', t('A submitted checked checkbox retained its checked state during a rebuild.')); - $this->assertNoFieldChecked('edit-checkbox-1-default-on', t('A submitted unchecked checkbox retained its unchecked state during a rebuild.')); - $this->assertFieldById('edit-text-1', 'foo', t('A textfield retained its submitted value during a rebuild.')); + $this->assertFieldChecked('edit-checkbox-1-default-off', 'A submitted checked checkbox retained its checked state during a rebuild.'); + $this->assertNoFieldChecked('edit-checkbox-1-default-on', 'A submitted unchecked checkbox retained its unchecked state during a rebuild.'); + $this->assertFieldById('edit-text-1', 'foo', 'A textfield retained its submitted value during a rebuild.'); // Verify that newly added elements were initialized with their default values. - $this->assertFieldChecked('edit-checkbox-2-default-on', t('A newly added checkbox was initialized with a default checked state.')); - $this->assertNoFieldChecked('edit-checkbox-2-default-off', t('A newly added checkbox was initialized with a default unchecked state.')); - $this->assertFieldById('edit-text-2', 'DEFAULT 2', t('A newly added textfield was initialized with its default value.')); + $this->assertFieldChecked('edit-checkbox-2-default-on', 'A newly added checkbox was initialized with a default checked state.'); + $this->assertNoFieldChecked('edit-checkbox-2-default-off', 'A newly added checkbox was initialized with a default unchecked state.'); + $this->assertFieldById('edit-text-2', 'DEFAULT 2', 'A newly added textfield was initialized with its default value.'); } /** - * Tests that a form's action is retained after an AJAX submission. + * Tests that a form's action is retained after an Ajax submission. * - * The 'action' attribute of a form should not change after an AJAX submission - * followed by a non-AJAX submission, which triggers a validation error. + * The 'action' attribute of a form should not change after an Ajax submission + * followed by a non-Ajax submission, which triggers a validation error. */ function testPreserveFormActionAfterAJAX() { - // Create a multi-valued field for 'page' nodes to use for AJAX testing. + // Create a multi-valued field for 'page' nodes to use for Ajax testing. $field_name = 'field_ajax_test'; $field = array( 'field_name' => $field_name, @@ -1157,32 +1661,107 @@ $this->web_user = $this->drupalCreateUser(array('create page content')); $this->drupalLogin($this->web_user); - // Get the form for adding a 'page' node. Submit an "add another item" AJAX + // Get the form for adding a 'page' node. Submit an "add another item" Ajax // submission and verify it worked by ensuring the updated page has two text // field items in the field for which we just added an item. $this->drupalGet('node/add/page'); $this->drupalPostAJAX(NULL, array(), array('field_ajax_test_add_more' => t('Add another item')), 'system/ajax', array(), array(), 'page-node-form'); - $this->assert(count($this->xpath('//div[contains(@class, "field-name-field-ajax-test")]//input[@type="text"]')) == 2, t('AJAX submission succeeded.')); + $this->assert(count($this->xpath('//div[contains(@class, "field-name-field-ajax-test")]//input[@type="text"]')) == 2, 'AJAX submission succeeded.'); - // Submit the form with the non-AJAX "Save" button, leaving the title field + // Submit the form with the non-Ajax "Save" button, leaving the title field // blank to trigger a validation error, and ensure that a validation error // occurred, because this test is for testing what happens when a form is // re-rendered without being re-built, which is what happens when there's // a validation error. $this->drupalPost(NULL, array(), t('Save')); - $this->assertText('Title field is required.', t('Non-AJAX submission correctly triggered a validation error.')); + $this->assertText('Title field is required.', 'Non-AJAX submission correctly triggered a validation error.'); // Ensure that the form contains two items in the multi-valued field, so we // know we're testing a form that was correctly retrieved from cache. - $this->assert(count($this->xpath('//form[contains(@id, "page-node-form")]//div[contains(@class, "form-item-field-ajax-test")]//input[@type="text"]')) == 2, t('Form retained its state from cache.')); + $this->assert(count($this->xpath('//form[contains(@id, "page-node-form")]//div[contains(@class, "form-item-field-ajax-test")]//input[@type="text"]')) == 2, 'Form retained its state from cache.'); // Ensure that the form's action is correct. $forms = $this->xpath('//form[contains(@class, "node-page-form")]'); - $this->assert(count($forms) == 1 && $forms[0]['action'] == url('node/add/page'), t('Re-rendered form contains the correct action value.')); + $this->assert(count($forms) == 1 && $forms[0]['action'] == url('node/add/page'), 'Re-rendered form contains the correct action value.'); } } /** + * Tests form redirection. + */ +class FormsRedirectTestCase extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Form redirecting', + 'description' => 'Tests functionality of drupal_redirect_form().', + 'group' => 'Form API', + ); + } + + function setUp() { + parent::setUp(array('form_test')); + } + + /** + * Tests form redirection. + */ + function testRedirect() { + $path = 'form-test/redirect'; + $options = array('query' => array('foo' => 'bar')); + $options['absolute'] = TRUE; + + // Test basic redirection. + $edit = array( + 'redirection' => TRUE, + 'destination' => $this->randomName(), + ); + $this->drupalPost($path, $edit, t('Submit')); + $this->assertUrl($edit['destination'], array(), 'Basic redirection works.'); + + + // Test without redirection. + $edit = array( + 'redirection' => FALSE, + ); + $this->drupalPost($path, $edit, t('Submit')); + $this->assertUrl($path, array(), 'When redirect is set to FALSE, there should be no redirection.'); + + // Test redirection with query parameters. + $edit = array( + 'redirection' => TRUE, + 'destination' => $this->randomName(), + ); + $this->drupalPost($path, $edit, t('Submit'), $options); + $this->assertUrl($edit['destination'], array(), 'Redirection with query parameters works.'); + + // Test without redirection but with query parameters. + $edit = array( + 'redirection' => FALSE, + ); + $this->drupalPost($path, $edit, t('Submit'), $options); + $this->assertUrl($path, $options, 'When redirect is set to FALSE, there should be no redirection, and the query parameters should be passed along.'); + + // Test redirection back to the original path. + $edit = array( + 'redirection' => TRUE, + 'destination' => '', + ); + $this->drupalPost($path, $edit, t('Submit')); + $this->assertUrl($path, array(), 'When using an empty redirection string, there should be no redirection.'); + + // Test redirection back to the original path with query parameters. + $edit = array( + 'redirection' => TRUE, + 'destination' => '', + ); + $this->drupalPost($path, $edit, t('Submit'), $options); + $this->assertUrl($path, $options, 'When using an empty redirection string, there should be no redirection, and the query parameters should be passed along.'); + } + +} + +/** * Test the programmatic form submission behavior. */ class FormsProgrammaticTestCase extends DrupalWebTestCase { @@ -1222,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 @@ -1244,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. @@ -1257,15 +1854,18 @@ '%values' => print_r($values, TRUE), '%errors' => $valid_form ? t('None') : implode(' ', $errors), ); - $this->assertTrue($valid_input == $valid_form, t('Input values: %values<br/>Validation handler errors: %errors', $args)); + $this->assertTrue($valid_input == $valid_form, format_string('Input values: %values<br/>Validation handler errors: %errors', $args)); // We check submitted values only if we have a valid input. if ($valid_input) { // 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) { - $this->assertTrue(isset($stored_values[$key]) && $stored_values[$key] == $value, t('Submission handler correctly executed: %stored_key is %stored_value', array('%stored_key' => $key, '%stored_value' => print_r($value, TRUE)))); + 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)))); } } } @@ -1302,24 +1902,24 @@ // $form_state['triggering_element'] and the form submit handler not // running. $this->drupalPost($path, $edit, NULL, array(), array(), $form_html_id); - $this->assertText('There is no clicked button.', t('$form_state[\'triggering_element\'] set to NULL.')); - $this->assertNoText('Submit handler for form_test_clicked_button executed.', t('Form submit handler did not execute.')); + $this->assertText('There is no clicked button.', '$form_state[\'triggering_element\'] set to NULL.'); + $this->assertNoText('Submit handler for form_test_clicked_button executed.', 'Form submit handler did not execute.'); // Ensure submitting a form with one or more submit buttons results in // $form_state['triggering_element'] being set to the first one the user has // access to. An argument with 'r' in it indicates a restricted // (#access=FALSE) button. $this->drupalPost($path . '/s', $edit, NULL, array(), array(), $form_html_id); - $this->assertText('The clicked button is button1.', t('$form_state[\'triggering_element\'] set to only button.')); - $this->assertText('Submit handler for form_test_clicked_button executed.', t('Form submit handler executed.')); + $this->assertText('The clicked button is button1.', '$form_state[\'triggering_element\'] set to only button.'); + $this->assertText('Submit handler for form_test_clicked_button executed.', 'Form submit handler executed.'); $this->drupalPost($path . '/s/s', $edit, NULL, array(), array(), $form_html_id); - $this->assertText('The clicked button is button1.', t('$form_state[\'triggering_element\'] set to first button.')); - $this->assertText('Submit handler for form_test_clicked_button executed.', t('Form submit handler executed.')); + $this->assertText('The clicked button is button1.', '$form_state[\'triggering_element\'] set to first button.'); + $this->assertText('Submit handler for form_test_clicked_button executed.', 'Form submit handler executed.'); $this->drupalPost($path . '/rs/s', $edit, NULL, array(), array(), $form_html_id); - $this->assertText('The clicked button is button2.', t('$form_state[\'triggering_element\'] set to first available button.')); - $this->assertText('Submit handler for form_test_clicked_button executed.', t('Form submit handler executed.')); + $this->assertText('The clicked button is button2.', '$form_state[\'triggering_element\'] set to first available button.'); + $this->assertText('Submit handler for form_test_clicked_button executed.', 'Form submit handler executed.'); // Ensure submitting a form with buttons of different types results in // $form_state['triggering_element'] being set to the first button, @@ -1327,16 +1927,16 @@ // submit handler not executing. The types are 's'(ubmit), 'b'(utton), and // 'i'(mage_button). $this->drupalPost($path . '/s/b/i', $edit, NULL, array(), array(), $form_html_id); - $this->assertText('The clicked button is button1.', t('$form_state[\'triggering_element\'] set to first button.')); - $this->assertText('Submit handler for form_test_clicked_button executed.', t('Form submit handler executed.')); + $this->assertText('The clicked button is button1.', '$form_state[\'triggering_element\'] set to first button.'); + $this->assertText('Submit handler for form_test_clicked_button executed.', 'Form submit handler executed.'); $this->drupalPost($path . '/b/s/i', $edit, NULL, array(), array(), $form_html_id); - $this->assertText('The clicked button is button1.', t('$form_state[\'triggering_element\'] set to first button.')); - $this->assertNoText('Submit handler for form_test_clicked_button executed.', t('Form submit handler did not execute.')); + $this->assertText('The clicked button is button1.', '$form_state[\'triggering_element\'] set to first button.'); + $this->assertNoText('Submit handler for form_test_clicked_button executed.', 'Form submit handler did not execute.'); $this->drupalPost($path . '/i/s/b', $edit, NULL, array(), array(), $form_html_id); - $this->assertText('The clicked button is button1.', t('$form_state[\'triggering_element\'] set to first button.')); - $this->assertText('Submit handler for form_test_clicked_button executed.', t('Form submit handler executed.')); + $this->assertText('The clicked button is button1.', '$form_state[\'triggering_element\'] set to first button.'); + $this->assertText('Submit handler for form_test_clicked_button executed.', 'Form submit handler executed.'); } /** @@ -1364,8 +1964,8 @@ // because negative assertions alone can be brittle. See // testNoButtonInfoInPost() for why the triggering element gets set to // 'button2'. - $this->assertNoText('The clicked button is button1.', t('$form_state[\'triggering_element\'] not set to a restricted button.')); - $this->assertText('The clicked button is button2.', t('$form_state[\'triggering_element\'] not set to a restricted button.')); + $this->assertNoText('The clicked button is button1.', '$form_state[\'triggering_element\'] not set to a restricted button.'); + $this->assertText('The clicked button is button2.', '$form_state[\'triggering_element\'] not set to a restricted button.'); } } @@ -1515,7 +2115,7 @@ $checked = ($default_value === '1foobar'); } $checked_in_html = strpos($form, 'checked') !== FALSE; - $message = t('#default_value is %default_value #return_value is %return_value.', array('%default_value' => var_export($default_value, TRUE), '%return_value' => var_export($return_value, TRUE))); + $message = format_string('#default_value is %default_value #return_value is %return_value.', array('%default_value' => var_export($default_value, TRUE), '%return_value' => var_export($return_value, TRUE))); $this->assertIdentical($checked, $checked_in_html, $message); } } @@ -1523,12 +2123,12 @@ // Ensure that $form_state['values'] is populated correctly for a checkboxes // group that includes a 0-indexed array of options. $results = json_decode($this->drupalPost('form-test/checkboxes-zero', array(), 'Save')); - $this->assertIdentical($results->checkbox_off, array(0, 0, 0), t('All three in checkbox_off are zeroes: off.')); - $this->assertIdentical($results->checkbox_zero_default, array('0', 0, 0), t('The first choice is on in checkbox_zero_default')); - $this->assertIdentical($results->checkbox_string_zero_default, array('0', 0, 0), t('The first choice is on in checkbox_string_zero_default')); + $this->assertIdentical($results->checkbox_off, array(0, 0, 0), 'All three in checkbox_off are zeroes: off.'); + $this->assertIdentical($results->checkbox_zero_default, array('0', 0, 0), 'The first choice is on in checkbox_zero_default'); + $this->assertIdentical($results->checkbox_string_zero_default, array('0', 0, 0), 'The first choice is on in checkbox_string_zero_default'); $edit = array('checkbox_off[0]' => '0'); $results = json_decode($this->drupalPost('form-test/checkboxes-zero', $edit, 'Save')); - $this->assertIdentical($results->checkbox_off, array('0', 0, 0), t('The first choice is on in checkbox_off but the rest is not')); + $this->assertIdentical($results->checkbox_off, array('0', 0, 0), 'The first choice is on in checkbox_off but the rest is not'); // Ensure that each checkbox is rendered correctly for a checkboxes group // that includes a 0-indexed array of options. @@ -1537,7 +2137,7 @@ foreach ($checkboxes as $checkbox) { $checked = isset($checkbox['checked']); $name = (string) $checkbox['name']; - $this->assertIdentical($checked, $name == 'checkbox_zero_default[0]' || $name == 'checkbox_string_zero_default[0]', t('Checkbox %name correctly checked', array('%name' => $name))); + $this->assertIdentical($checked, $name == 'checkbox_zero_default[0]' || $name == 'checkbox_string_zero_default[0]', format_string('Checkbox %name correctly checked', array('%name' => $name))); } $edit = array('checkbox_off[0]' => '0'); $this->drupalPost('form-test/checkboxes-zero/0', $edit, 'Save'); @@ -1545,7 +2145,71 @@ foreach ($checkboxes as $checkbox) { $checked = isset($checkbox['checked']); $name = (string) $checkbox['name']; - $this->assertIdentical($checked, $name == 'checkbox_off[0]' || $name == 'checkbox_zero_default[0]' || $name == 'checkbox_string_zero_default[0]', t('Checkbox %name correctly checked', array('%name' => $name))); + $this->assertIdentical($checked, $name == 'checkbox_off[0]' || $name == 'checkbox_zero_default[0]' || $name == 'checkbox_string_zero_default[0]', format_string('Checkbox %name correctly checked', array('%name' => $name))); + } + } +} + +/** + * Tests uniqueness of generated HTML IDs. + */ +class HTMLIdTestCase extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Unique HTML IDs', + 'description' => 'Tests functionality of drupal_html_id().', + 'group' => 'Form API', + ); + } + + function setUp() { + parent::setUp('form_test'); + } + + /** + * Tests that HTML IDs do not get duplicated when form validation fails. + */ + function testHTMLId() { + $this->drupalGet('form-test/double-form'); + $this->assertNoDuplicateIds('There are no duplicate IDs'); + + // Submit second form with empty title. + $edit = array(); + $this->drupalPost(NULL, $edit, 'Save', array(), array(), 'form-test-html-id--2'); + $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.0/modules/simpletest/tests/form_test.file.inc drupal-7.66/modules/simpletest/tests/form_test.file.inc --- drupal-7.0/modules/simpletest/tests/form_test.file.inc 2010-09-19 20:39:18.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/form_test.file.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: form_test.file.inc,v 1.4 2010/09/19 18:39:18 dries Exp $ /** * @file @@ -11,7 +10,7 @@ * hook_menu(). */ function form_test_load_include_menu($form, &$form_state) { - // Submit the form via AJAX. That way the FAPI has to care about including + // Submit the form via Ajax. That way the FAPI has to care about including // the file specified in hook_menu(). $ajax_wrapper_id = drupal_html_id('form-test-load-include-menu-ajax-wrapper'); $form['ajax_wrapper'] = array( @@ -43,7 +42,7 @@ function form_test_load_include_menu_ajax($form) { // We don't need to return anything, since #ajax['method'] is 'append', which // does not remove the original #ajax['wrapper'] element, and status messages - // are automatically added by the AJAX framework as long as there's a wrapper + // are automatically added by the Ajax framework as long as there's a wrapper // element to add them to. return ''; } diff -Naur drupal-7.0/modules/simpletest/tests/form_test.info drupal-7.66/modules/simpletest/tests/form_test.info --- drupal-7.0/modules/simpletest/tests/form_test.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/form_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: form_test.info,v 1.3 2010/12/20 19:59:43 webchick Exp $ name = "FormAPI Test" description = "Support module for Form API tests." package = Testing @@ -6,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/simpletest/tests/form_test.module drupal-7.66/modules/simpletest/tests/form_test.module --- drupal-7.0/modules/simpletest/tests/form_test.module 2010-12-30 23:52:24.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/form_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: form_test.module,v 1.59 2010/12/30 22:52:24 webchick Exp $ /** * @file @@ -24,6 +23,27 @@ 'access arguments' => array('access content'), 'type' => MENU_CALLBACK, ); + $items['form-test/validate-required'] = array( + 'title' => 'Form #required validation', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('form_test_validate_required_form'), + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); + $items['form-test/validate-required-no-title'] = array( + 'title' => 'Form #required validation without #title', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('form_test_validate_required_form_no_title'), + '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', @@ -77,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', @@ -93,6 +128,14 @@ 'type' => MENU_CALLBACK, ); + $items['form_test/form-state-values-clean-advanced'] = array( + 'title' => 'Form state values clearance advanced test', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('form_test_form_state_values_clean_advanced_form'), + 'access arguments' => array('access content'), + 'type' => MENU_CALLBACK, + ); + $items['form-test/checkbox'] = array( 'title' => t('Form test'), 'page callback' => 'drupal_get_form', @@ -137,6 +180,14 @@ 'type' => MENU_CALLBACK, ); + $items['form-test/redirect'] = array( + 'title' => 'Redirect test', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('form_test_redirect'), + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); + $items['form_test/form-labels'] = array( 'title' => 'Form label test', 'page callback' => 'drupal_get_form', @@ -172,6 +223,12 @@ 'type' => MENU_CALLBACK, ); } + $items['form-test/double-form'] = array( + 'title' => 'Double form test', + 'page callback' => 'form_test_double_form', + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); $items['form-test/load-include-menu'] = array( 'title' => 'FAPI test loading includes', @@ -333,6 +390,99 @@ } /** + * Form constructor to test the #required property. + */ +function form_test_validate_required_form($form, &$form_state) { + $options = drupal_map_assoc(array('foo', 'bar')); + + $form['textfield'] = array( + '#type' => 'textfield', + '#title' => 'Textfield', + '#required' => TRUE, + ); + $form['checkboxes'] = array( + '#type' => 'checkboxes', + '#title' => 'Checkboxes', + '#options' => $options, + '#required' => TRUE, + ); + $form['select'] = array( + '#type' => 'select', + '#title' => 'Select', + '#options' => $options, + '#required' => TRUE, + ); + $form['radios'] = array( + '#type' => 'radios', + '#title' => 'Radios', + '#options' => $options, + '#required' => TRUE, + ); + $form['radios_optional'] = array( + '#type' => 'radios', + '#title' => 'Radios (optional)', + '#options' => $options, + ); + $form['radios_optional_default_value_false'] = array( + '#type' => 'radios', + '#title' => 'Radios (optional, with a default value of FALSE)', + '#options' => $options, + '#default_value' => FALSE, + ); + $form['actions'] = array('#type' => 'actions'); + $form['actions']['submit'] = array('#type' => 'submit', '#value' => 'Submit'); + return $form; +} + +/** + * Form submission handler for form_test_validate_required_form(). + */ +function form_test_validate_required_form_submit($form, &$form_state) { + drupal_set_message('The form_test_validate_required_form form was submitted successfully.'); +} + +/** + * Form constructor to test the #required property without #title. + */ +function form_test_validate_required_form_no_title($form, &$form_state) { + $form['textfield'] = array( + '#type' => 'textfield', + '#required' => TRUE, + ); + $form['actions'] = array('#type' => 'actions'); + $form['actions']['submit'] = array('#type' => 'submit', '#value' => 'Submit'); + return $form; +} + +/** + * Form submission handler for form_test_validate_required_form_no_title(). + */ +function form_test_validate_required_form_no_title_submit($form, &$form_state) { + drupal_set_message('The form_test_validate_required_form_no_title form was submitted successfully.'); +} + +/** + * 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) { @@ -467,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( @@ -576,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 @@ -639,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. @@ -679,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() { @@ -689,6 +947,7 @@ 'first-checkbox' => t('First checkbox'), 'second-checkbox' => t('Second checkbox'), 'third-checkbox' => t('Third checkbox'), + '0' => t('0'), ), ); $form['form_radios_test'] = array( @@ -698,6 +957,7 @@ 'first-radio' => t('First radio'), 'second-radio' => t('Second radio'), 'third-radio' => t('Third radio'), + '0' => t('0'), ), // Test #field_prefix and #field_suffix placement. '#field_prefix' => '<span id="form-test-radios-field-prefix">' . t('Radios #field_prefix element') . '</span>', @@ -714,7 +974,7 @@ ); $form['form_textfield_test_no_title_required'] = array( '#type' => 'textfield', - // We use an empty title, since not setting #title supresses the label + // We use an empty title, since not setting #title suppresses the label // and required marker. '#title' => '', '#required' => TRUE, @@ -737,10 +997,31 @@ '#title' => t('Textfield test for invisible title'), '#title_display' => 'invisible', ); - // Textfield test for title set not to display + // Textfield test for title set not to display. $form['form_textfield_test_title_no_show'] = array( '#type' => 'textfield', ); + // Checkboxes & radios with title as attribute. + $form['form_checkboxes_title_attribute'] = array( + '#type' => 'checkboxes', + '#title' => 'Checkboxes test', + '#options' => array( + 'first-checkbox' => 'First checkbox', + 'second-checkbox' => 'Second checkbox', + ), + '#title_display' => 'attribute', + '#required' => TRUE, + ); + $form['form_radios_title_attribute'] = array( + '#type' => 'radios', + '#title' => 'Radios test', + '#options' => array( + 'first-radio' => 'First radio', + 'second-radio' => 'Second radio', + ), + '#title_display' => 'attribute', + '#required' => TRUE, + ); return $form; } @@ -798,6 +1079,33 @@ } /** + * Form constructor for the form_state_values_clean() test. + */ +function form_test_form_state_values_clean_advanced_form($form, &$form_state) { + // Build an example form containing a managed file and a submit form element. + $form['image'] = array( + '#type' => 'managed_file', + '#title' => t('Image'), + '#upload_location' => 'public://', + '#default_value' => 0, + ); + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Submit'), + ); + return $form; +} + +/** + * Form submission handler for form_test_form_state_values_clean_advanced_form(). + */ +function form_test_form_state_values_clean_advanced_form_submit($form, &$form_state) { + form_state_values_clean($form_state); + print t('You WIN!'); + exit; +} + +/** * Build a form to test a checkbox. */ function _form_test_checkbox($form, &$form_state) { @@ -1391,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)', @@ -1528,6 +1845,43 @@ } /** + * Form builder to detect form redirect. + */ +function form_test_redirect($form, &$form_state) { + $form['redirection'] = array( + '#type' => 'checkbox', + '#title' => t('Use redirection'), + ); + $form['destination'] = array( + '#type' => 'textfield', + '#title' => t('Redirect destination'), + '#states' => array( + 'visible' => array( + ':input[name="redirection"]' => array('checked' => TRUE), + ), + ), + ); + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Submit'), + ); + + return $form; +} + +/** + * Form submit handler to test different redirect behaviours. + */ +function form_test_redirect_submit(&$form, &$form_state) { + if (!empty($form_state['values']['redirection'])) { + $form_state['redirect'] = !empty($form_state['values']['destination']) ? $form_state['values']['destination'] : NULL; + } + else { + $form_state['redirect'] = FALSE; + } +} + +/** * Implements hook_form_FORM_ID_alter() for the registration form. */ function form_test_form_user_register_form_alter(&$form, &$form_state) { @@ -1626,3 +1980,29 @@ function _form_test_checkboxes_zero_no_redirect($form, &$form_state) { $form_state['redirect'] = FALSE; } + +/** + * Menu callback returns two instances of the same form. + */ +function form_test_double_form() { + return array( + 'form1' => drupal_get_form('form_test_html_id'), + 'form2' => drupal_get_form('form_test_html_id'), + ); +} + +/** + * Builds a simple form to test duplicate HTML IDs. + */ +function form_test_html_id($form, &$form_state) { + $form['name'] = array( + '#type' => 'textfield', + '#title' => 'name', + '#required' => TRUE, + ); + $form['submit'] = array( + '#type' => 'submit', + '#value' => 'Save', + ); + return $form; +} diff -Naur drupal-7.0/modules/simpletest/tests/graph.test drupal-7.66/modules/simpletest/tests/graph.test --- drupal-7.0/modules/simpletest/tests/graph.test 2010-09-04 15:33:53.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/graph.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: graph.test,v 1.10 2010/09/04 13:33:53 dries Exp $ /** * @file @@ -70,7 +69,7 @@ $this->assertReversePaths($graph, $expected_reverse_paths); // Assert that DFS didn't created "missing" vertexes automatically. - $this->assertFALSE(isset($graph[6]), t('Vertex 6 has not been created')); + $this->assertFALSE(isset($graph[6]), 'Vertex 6 has not been created'); $expected_components = array( array(1, 2, 3, 4, 5, 7), @@ -116,7 +115,7 @@ // Build an array with keys = $paths and values = TRUE. $expected = array_fill_keys($paths, TRUE); $result = isset($graph[$vertex]['paths']) ? $graph[$vertex]['paths'] : array(); - $this->assertEqual($expected, $result, t('Expected paths for vertex @vertex: @expected-paths, got @paths', array('@vertex' => $vertex, '@expected-paths' => $this->displayArray($expected, TRUE), '@paths' => $this->displayArray($result, TRUE)))); + $this->assertEqual($expected, $result, format_string('Expected paths for vertex @vertex: @expected-paths, got @paths', array('@vertex' => $vertex, '@expected-paths' => $this->displayArray($expected, TRUE), '@paths' => $this->displayArray($result, TRUE)))); } } @@ -134,7 +133,7 @@ // Build an array with keys = $paths and values = TRUE. $expected = array_fill_keys($paths, TRUE); $result = isset($graph[$vertex]['reverse_paths']) ? $graph[$vertex]['reverse_paths'] : array(); - $this->assertEqual($expected, $result, t('Expected reverse paths for vertex @vertex: @expected-paths, got @paths', array('@vertex' => $vertex, '@expected-paths' => $this->displayArray($expected, TRUE), '@paths' => $this->displayArray($result, TRUE)))); + $this->assertEqual($expected, $result, format_string('Expected reverse paths for vertex @vertex: @expected-paths, got @paths', array('@vertex' => $vertex, '@expected-paths' => $this->displayArray($expected, TRUE), '@paths' => $this->displayArray($result, TRUE)))); } } @@ -154,9 +153,9 @@ $result_components[] = $graph[$vertex]['component']; unset($unassigned_vertices[$vertex]); } - $this->assertEqual(1, count(array_unique($result_components)), t('Expected one unique component for vertices @vertices, got @components', array('@vertices' => $this->displayArray($component), '@components' => $this->displayArray($result_components)))); + $this->assertEqual(1, count(array_unique($result_components)), format_string('Expected one unique component for vertices @vertices, got @components', array('@vertices' => $this->displayArray($component), '@components' => $this->displayArray($result_components)))); } - $this->assertEqual(array(), $unassigned_vertices, t('Vertices not assigned to a component: @vertices', array('@vertices' => $this->displayArray($unassigned_vertices, TRUE)))); + $this->assertEqual(array(), $unassigned_vertices, format_string('Vertices not assigned to a component: @vertices', array('@vertices' => $this->displayArray($unassigned_vertices, TRUE)))); } /** @@ -171,7 +170,7 @@ foreach ($expected_orders as $order) { $previous_vertex = array_shift($order); foreach ($order as $vertex) { - $this->assertTrue($graph[$previous_vertex]['weight'] < $graph[$vertex]['weight'], t('Weights of @previous-vertex and @vertex are correct relative to each other', array('@previous-vertex' => $previous_vertex, '@vertex' => $vertex))); + $this->assertTrue($graph[$previous_vertex]['weight'] < $graph[$vertex]['weight'], format_string('Weights of @previous-vertex and @vertex are correct relative to each other', array('@previous-vertex' => $previous_vertex, '@vertex' => $vertex))); } } } diff -Naur drupal-7.0/modules/simpletest/tests/http.php drupal-7.66/modules/simpletest/tests/http.php --- drupal-7.0/modules/simpletest/tests/http.php 2010-11-05 20:05:02.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/http.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: http.php,v 1.1 2010/11/05 19:05:02 dries Exp $ /** * @file diff -Naur drupal-7.0/modules/simpletest/tests/https.php drupal-7.66/modules/simpletest/tests/https.php --- drupal-7.0/modules/simpletest/tests/https.php 2010-11-05 20:05:02.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/https.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,15 +1,14 @@ <?php -// $Id: https.php,v 1.3 2010/11/05 19:05:02 dries Exp $ /** * @file - * Fake an https request, for use during testing. + * Fake an HTTPS request, for use during testing. */ // Set a global variable to indicate a mock HTTPS request. $is_https_mock = empty($_SERVER['HTTPS']); -// Change to https. +// Change to HTTPS. $_SERVER['HTTPS'] = 'on'; foreach ($_SERVER as $key => $value) { $_SERVER[$key] = str_replace('modules/simpletest/tests/https.php', 'index.php', $value); diff -Naur drupal-7.0/modules/simpletest/tests/image.test drupal-7.66/modules/simpletest/tests/image.test --- drupal-7.0/modules/simpletest/tests/image.test 2010-09-01 22:08:17.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/image.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: image.test,v 1.16 2010/09/01 20:08:17 dries Exp $ /** * @file @@ -15,7 +14,13 @@ protected $image; function setUp() { - parent::setUp('image_test'); + $modules = func_get_args(); + if (isset($modules[0]) && is_array($modules[0])) { + $modules = $modules[0]; + } + $modules[] = 'image_test'; + + parent::setUp($modules); // Use the image_test.module's test toolkit. $this->toolkit = 'test'; @@ -50,19 +55,19 @@ // Determine if there were any expected that were not called. $uncalled = array_diff($expected, $actual); if (count($uncalled)) { - $this->assertTrue(FALSE, t('Expected operations %expected to be called but %uncalled was not called.', array('%expected' => implode(', ', $expected), '%uncalled' => implode(', ', $uncalled)))); + $this->assertTrue(FALSE, format_string('Expected operations %expected to be called but %uncalled was not called.', array('%expected' => implode(', ', $expected), '%uncalled' => implode(', ', $uncalled)))); } else { - $this->assertTrue(TRUE, t('All the expected operations were called: %expected', array('%expected' => implode(', ', $expected)))); + $this->assertTrue(TRUE, format_string('All the expected operations were called: %expected', array('%expected' => implode(', ', $expected)))); } // Determine if there were any unexpected calls. $unexpected = array_diff($actual, $expected); if (count($unexpected)) { - $this->assertTrue(FALSE, t('Unexpected operations were called: %unexpected.', array('%unexpected' => implode(', ', $unexpected)))); + $this->assertTrue(FALSE, format_string('Unexpected operations were called: %unexpected.', array('%unexpected' => implode(', ', $unexpected)))); } else { - $this->assertTrue(TRUE, t('No unexpected operations were called.')); + $this->assertTrue(TRUE, 'No unexpected operations were called.'); } } } @@ -85,8 +90,8 @@ */ function testGetAvailableToolkits() { $toolkits = image_get_available_toolkits(); - $this->assertTrue(isset($toolkits['test']), t('The working toolkit was returned.')); - $this->assertFalse(isset($toolkits['broken']), t('The toolkit marked unavailable was not returned')); + $this->assertTrue(isset($toolkits['test']), 'The working toolkit was returned.'); + $this->assertFalse(isset($toolkits['broken']), 'The toolkit marked unavailable was not returned'); $this->assertToolkitOperationsCalled(array()); } @@ -95,8 +100,8 @@ */ function testLoad() { $image = image_load($this->file, $this->toolkit); - $this->assertTrue(is_object($image), t('Returned an object.')); - $this->assertEqual($this->toolkit, $image->toolkit, t('Image had toolkit set.')); + $this->assertTrue(is_object($image), 'Returned an object.'); + $this->assertEqual($this->toolkit, $image->toolkit, 'Image had toolkit set.'); $this->assertToolkitOperationsCalled(array('load', 'get_info')); } @@ -104,7 +109,7 @@ * Test the image_save() function. */ function testSave() { - $this->assertFalse(image_save($this->image), t('Function returned the expected value.')); + $this->assertFalse(image_save($this->image), 'Function returned the expected value.'); $this->assertToolkitOperationsCalled(array('save')); } @@ -112,13 +117,13 @@ * Test the image_resize() function. */ function testResize() { - $this->assertTrue(image_resize($this->image, 1, 2), t('Function returned the expected value.')); + $this->assertTrue(image_resize($this->image, 1, 2), 'Function returned the expected value.'); $this->assertToolkitOperationsCalled(array('resize')); // Check the parameters. $calls = image_test_get_all_calls(); - $this->assertEqual($calls['resize'][0][1], 1, t('Width was passed correctly')); - $this->assertEqual($calls['resize'][0][2], 2, t('Height was passed correctly')); + $this->assertEqual($calls['resize'][0][1], 1, 'Width was passed correctly'); + $this->assertEqual($calls['resize'][0][2], 2, 'Height was passed correctly'); } /** @@ -126,69 +131,69 @@ */ function testScale() { // TODO: need to test upscaling - $this->assertTrue(image_scale($this->image, 10, 10), t('Function returned the expected value.')); + $this->assertTrue(image_scale($this->image, 10, 10), 'Function returned the expected value.'); $this->assertToolkitOperationsCalled(array('resize')); // Check the parameters. $calls = image_test_get_all_calls(); - $this->assertEqual($calls['resize'][0][1], 10, t('Width was passed correctly')); - $this->assertEqual($calls['resize'][0][2], 5, t('Height was based off aspect ratio and passed correctly')); + $this->assertEqual($calls['resize'][0][1], 10, 'Width was passed correctly'); + $this->assertEqual($calls['resize'][0][2], 5, 'Height was based off aspect ratio and passed correctly'); } /** * Test the image_scale_and_crop() function. */ function testScaleAndCrop() { - $this->assertTrue(image_scale_and_crop($this->image, 5, 10), t('Function returned the expected value.')); + $this->assertTrue(image_scale_and_crop($this->image, 5, 10), 'Function returned the expected value.'); $this->assertToolkitOperationsCalled(array('resize', 'crop')); // Check the parameters. $calls = image_test_get_all_calls(); - $this->assertEqual($calls['crop'][0][1], 7.5, t('X was computed and passed correctly')); - $this->assertEqual($calls['crop'][0][2], 0, t('Y was computed and passed correctly')); - $this->assertEqual($calls['crop'][0][3], 5, t('Width was computed and passed correctly')); - $this->assertEqual($calls['crop'][0][4], 10, t('Height was computed and passed correctly')); + $this->assertEqual($calls['crop'][0][1], 7.5, 'X was computed and passed correctly'); + $this->assertEqual($calls['crop'][0][2], 0, 'Y was computed and passed correctly'); + $this->assertEqual($calls['crop'][0][3], 5, 'Width was computed and passed correctly'); + $this->assertEqual($calls['crop'][0][4], 10, 'Height was computed and passed correctly'); } /** * Test the image_rotate() function. */ function testRotate() { - $this->assertTrue(image_rotate($this->image, 90, 1), t('Function returned the expected value.')); + $this->assertTrue(image_rotate($this->image, 90, 1), 'Function returned the expected value.'); $this->assertToolkitOperationsCalled(array('rotate')); // Check the parameters. $calls = image_test_get_all_calls(); - $this->assertEqual($calls['rotate'][0][1], 90, t('Degrees were passed correctly')); - $this->assertEqual($calls['rotate'][0][2], 1, t('Background color was passed correctly')); + $this->assertEqual($calls['rotate'][0][1], 90, 'Degrees were passed correctly'); + $this->assertEqual($calls['rotate'][0][2], 1, 'Background color was passed correctly'); } /** * Test the image_crop() function. */ function testCrop() { - $this->assertTrue(image_crop($this->image, 1, 2, 3, 4), t('Function returned the expected value.')); + $this->assertTrue(image_crop($this->image, 1, 2, 3, 4), 'Function returned the expected value.'); $this->assertToolkitOperationsCalled(array('crop')); // Check the parameters. $calls = image_test_get_all_calls(); - $this->assertEqual($calls['crop'][0][1], 1, t('X was passed correctly')); - $this->assertEqual($calls['crop'][0][2], 2, t('Y was passed correctly')); - $this->assertEqual($calls['crop'][0][3], 3, t('Width was passed correctly')); - $this->assertEqual($calls['crop'][0][4], 4, t('Height was passed correctly')); + $this->assertEqual($calls['crop'][0][1], 1, 'X was passed correctly'); + $this->assertEqual($calls['crop'][0][2], 2, 'Y was passed correctly'); + $this->assertEqual($calls['crop'][0][3], 3, 'Width was passed correctly'); + $this->assertEqual($calls['crop'][0][4], 4, 'Height was passed correctly'); } /** * Test the image_desaturate() function. */ function testDesaturate() { - $this->assertTrue(image_desaturate($this->image), t('Function returned the expected value.')); + $this->assertTrue(image_desaturate($this->image), 'Function returned the expected value.'); $this->assertToolkitOperationsCalled(array('desaturate')); // Check the parameters. $calls = image_test_get_all_calls(); - $this->assertEqual(count($calls['desaturate'][0]), 1, t('Only the image was passed.')); + $this->assertEqual(count($calls['desaturate'][0]), 1, 'Only the image was passed.'); } } @@ -202,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; @@ -256,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; @@ -269,6 +277,7 @@ $files = array( 'image-test.png', 'image-test.gif', + 'image-test-no-transparency.gif', 'image-test.jpg', ); @@ -326,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. @@ -342,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), @@ -357,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. @@ -374,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) ), ), ); @@ -389,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; } } @@ -421,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: @@ -446,16 +494,87 @@ $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, t('Image %file after %action action has proper dimensions.', array('%file' => $file, '%action' => $op))); - $this->assertTrue($correct_dimensions_object, t('Image %file object after %action action is reporting the proper height and width values.', array('%file' => $file, '%action' => $op))); + $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))); // JPEG colors will always be messed up due to compression. if ($image->info['extension'] != 'jpg') { - $this->assertTrue($correct_colors, t('Image %file object after %action action has the correct color placement.', array('%file' => $file, '%action' => $op))); + $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))); + } } } + +/** + * Tests the file move function for managed files. + */ +class ImageFileMoveTest extends ImageToolkitTestCase { + public static function getInfo() { + return array( + 'name' => 'Image moving', + 'description' => 'Tests the file move function for managed files.', + 'group' => 'Image', + ); + } + + /** + * Tests moving a randomly generated image. + */ + function testNormal() { + // Pick a file for testing. + $file = current($this->drupalGetTestFiles('image')); + + // Create derivative image. + $style = image_style_load(key(image_styles())); + $derivative_uri = image_style_path($style['name'], $file->uri); + image_style_create_derivative($style, $file->uri, $derivative_uri); + + // Check if derivative image exists. + $this->assertTrue(file_exists($derivative_uri), 'Make sure derivative image is generated successfully.'); + + // Clone the object so we don't have to worry about the function changing + // our reference copy. + $desired_filepath = 'public://' . $this->randomName(); + $result = file_move(clone $file, $desired_filepath, FILE_EXISTS_ERROR); + + // Check if image has been moved. + $this->assertTrue(file_exists($result->uri), 'Make sure image is moved successfully.'); + + // Check if derivative image has been flushed. + $this->assertFalse(file_exists($derivative_uri), 'Make sure derivative image has been flushed.'); + } +} + diff -Naur drupal-7.0/modules/simpletest/tests/image_test.info drupal-7.66/modules/simpletest/tests/image_test.info --- drupal-7.0/modules/simpletest/tests/image_test.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/image_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: image_test.info,v 1.2 2010/12/20 19:59:43 webchick Exp $ name = "Image test" description = "Support module for image toolkit tests." package = Testing @@ -6,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/simpletest/tests/image_test.module drupal-7.66/modules/simpletest/tests/image_test.module --- drupal-7.0/modules/simpletest/tests/image_test.module 2010-03-26 18:14:45.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/image_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: image_test.module,v 1.6 2010/03/26 17:14:45 dries Exp $ /** * @file diff -Naur drupal-7.0/modules/simpletest/tests/lock.test drupal-7.66/modules/simpletest/tests/lock.test --- drupal-7.0/modules/simpletest/tests/lock.test 2010-08-06 01:53:38.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/lock.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: lock.test,v 1.3 2010/08/05 23:53:38 webchick Exp $ /** * Tests for the lock system. @@ -24,35 +23,35 @@ function testLockAcquire() { $lock_acquired = 'TRUE: Lock successfully acquired in system_test_lock_acquire()'; $lock_not_acquired = 'FALSE: Lock not acquired in system_test_lock_acquire()'; - $this->assertTrue(lock_acquire('system_test_lock_acquire'), t('Lock acquired by this request.'), t('Lock')); - $this->assertTrue(lock_acquire('system_test_lock_acquire'), t('Lock extended by this request.'), t('Lock')); + $this->assertTrue(lock_acquire('system_test_lock_acquire'), 'Lock acquired by this request.', 'Lock'); + $this->assertTrue(lock_acquire('system_test_lock_acquire'), 'Lock extended by this request.', 'Lock'); lock_release('system_test_lock_acquire'); // Cause another request to acquire the lock. $this->drupalGet('system-test/lock-acquire'); - $this->assertText($lock_acquired, t('Lock acquired by the other request.'), t('Lock')); + $this->assertText($lock_acquired, 'Lock acquired by the other request.', 'Lock'); // The other request has finished, thus it should have released its lock. - $this->assertTrue(lock_acquire('system_test_lock_acquire'), t('Lock acquired by this request.'), t('Lock')); + $this->assertTrue(lock_acquire('system_test_lock_acquire'), 'Lock acquired by this request.', 'Lock'); // This request holds the lock, so the other request cannot acquire it. $this->drupalGet('system-test/lock-acquire'); - $this->assertText($lock_not_acquired, t('Lock not acquired by the other request.'), t('Lock')); + $this->assertText($lock_not_acquired, 'Lock not acquired by the other request.', 'Lock'); lock_release('system_test_lock_acquire'); // Try a very short timeout and lock breaking. - $this->assertTrue(lock_acquire('system_test_lock_acquire', 0.5), t('Lock acquired by this request.'), t('Lock')); + $this->assertTrue(lock_acquire('system_test_lock_acquire', 0.5), 'Lock acquired by this request.', 'Lock'); sleep(1); // The other request should break our lock. $this->drupalGet('system-test/lock-acquire'); - $this->assertText($lock_acquired, t('Lock acquired by the other request, breaking our lock.'), t('Lock')); + $this->assertText($lock_acquired, 'Lock acquired by the other request, breaking our lock.', 'Lock'); // We cannot renew it, since the other thread took it. - $this->assertFalse(lock_acquire('system_test_lock_acquire'), t('Lock cannot be extended by this request.'), t('Lock')); + $this->assertFalse(lock_acquire('system_test_lock_acquire'), 'Lock cannot be extended by this request.', 'Lock'); // Check the shut-down function. $lock_acquired_exit = 'TRUE: Lock successfully acquired in system_test_lock_exit()'; $lock_not_acquired_exit = 'FALSE: Lock not acquired in system_test_lock_exit()'; $this->drupalGet('system-test/lock-exit'); - $this->assertText($lock_acquired_exit, t('Lock acquired by the other request before exit.'), t('Lock')); - $this->assertTrue(lock_acquire('system_test_lock_exit'), t('Lock acquired by this request after the other request exits.'), t('Lock')); + $this->assertText($lock_acquired_exit, 'Lock acquired by the other request before exit.', 'Lock'); + $this->assertTrue(lock_acquire('system_test_lock_exit'), 'Lock acquired by this request after the other request exits.', 'Lock'); } } diff -Naur drupal-7.0/modules/simpletest/tests/mail.test drupal-7.66/modules/simpletest/tests/mail.test --- drupal-7.0/modules/simpletest/tests/mail.test 2010-08-06 01:53:38.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/mail.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,7 +1,7 @@ <?php -// $Id: mail.test,v 1.5 2010/08/05 23:53:38 webchick Exp $ /** + * @file * Test the Drupal mailing system. */ class MailTestCase extends DrupalWebTestCase implements MailSystemInterface { @@ -22,7 +22,7 @@ } function setUp() { - parent::setUp(); + parent::setUp(array('simpletest')); // Set MailTestCase (i.e. this class) as the SMTP library variable_set('mail_system', array('default-system' => 'MailTestCase')); @@ -35,10 +35,28 @@ global $language; // Use MailTestCase for sending a message. - $message = drupal_mail('simpletest', 'mail_test', 'testing@drupal.org', $language); + $message = drupal_mail('simpletest', 'mail_test', 'testing@example.com', $language); // Assert whether the message was sent through the send function. - $this->assertEqual(self::$sent_message['to'], 'testing@drupal.org', t('Pluggable mail system is extendable.')); + $this->assertEqual(self::$sent_message['to'], 'testing@example.com', 'Pluggable mail system is extendable.'); + } + + /** + * Test that message sending may be canceled. + * + * @see simpletest_mail_alter() + */ + function testCancelMessage() { + global $language; + + // Reset the class variable holding a copy of the last sent message. + self::$sent_message = NULL; + + // Send a test message that simpletest_mail_alter should cancel. + $message = drupal_mail('simpletest', 'cancel_test', 'cancel@example.com', $language); + + // Assert that the message was not actually sent. + $this->assertNull(self::$sent_message, 'Message was canceled.'); } /** @@ -64,3 +82,379 @@ } } +/** + * Unit tests for drupal_html_to_text(). + */ +class DrupalHtmlToTextTestCase extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'HTML to text conversion', + 'description' => 'Tests drupal_html_to_text().', + 'group' => 'Mail', + ); + } + + /** + * Converts a string to its PHP source equivalent for display in test messages. + * + * @param $text + * The text string to convert. + * + * @return + * An HTML representation of the text string that, when displayed in a + * browser, represents the PHP source code equivalent of $text. + */ + function stringToHtml($text) { + return '"' . + str_replace( + array("\n", ' '), + array('\n', ' '), + check_plain($text) + ) . '"'; + } + + /** + * Helper function for testing drupal_html_to_text(). + * + * @param $html + * The source HTML string to be converted. + * @param $text + * The expected result of converting $html to text. + * @param $message + * A text message to display in the assertion message. + * @param $allowed_tags + * (optional) An array of allowed tags, or NULL to default to the full + * set of tags supported by drupal_html_to_text(). + */ + function assertHtmlToText($html, $text, $message, $allowed_tags = NULL) { + preg_match_all('/<([a-z0-6]+)/', drupal_strtolower($html), $matches); + $tested_tags = implode(', ', array_unique($matches[1])); + $message .= ' (' . $tested_tags . ')'; + $result = drupal_html_to_text($html, $allowed_tags); + $pass = $this->assertEqual($result, $text, check_plain($message)); + $verbose = 'html = <pre>' . $this->stringToHtml($html) + . '</pre><br />' . 'result = <pre>' . $this->stringToHtml($result) + . '</pre><br />' . 'expected = <pre>' . $this->stringToHtml($text) + . '</pre>'; + $this->verbose($verbose); + if (!$pass) { + $this->pass("Previous test verbose info:<br />$verbose"); + } + } + + /** + * Test all supported tags of drupal_html_to_text(). + */ + function testTags() { + global $base_path, $base_url; + $tests = array( + // @todo Trailing linefeeds should be trimmed. + '<a href = "http://drupal.org">Drupal.org</a>' => "Drupal.org [1]\n\n[1] http://drupal.org\n", + // @todo Footer URLs should be absolute. + "<a href = \"$base_path\">Homepage</a>" => "Homepage [1]\n\n[1] $base_url/\n", + '<address>Drupal</address>' => "Drupal\n", + // @todo The <address> tag is currently not supported. + '<address>Drupal</address><address>Drupal</address>' => "DrupalDrupal\n", + '<b>Drupal</b>' => "*Drupal*\n", + // @todo There should be a space between the '>' and the text. + '<blockquote>Drupal</blockquote>' => ">Drupal\n", + '<blockquote>Drupal</blockquote><blockquote>Drupal</blockquote>' => ">Drupal\n>Drupal\n", + '<br />Drupal<br />Drupal<br /><br />Drupal' => "Drupal\nDrupal\nDrupal\n", + '<br/>Drupal<br/>Drupal<br/><br/>Drupal' => "Drupal\nDrupal\nDrupal\n", + // @todo There should be two line breaks before the paragraph. + '<br/>Drupal<br/>Drupal<br/><br/>Drupal<p>Drupal</p>' => "Drupal\nDrupal\nDrupal\nDrupal\n\n", + '<div>Drupal</div>' => "Drupal\n", + // @todo The <div> tag is currently not supported. + '<div>Drupal</div><div>Drupal</div>' => "DrupalDrupal\n", + '<em>Drupal</em>' => "/Drupal/\n", + '<h1>Drupal</h1>' => "======== DRUPAL ==============================================================\n\n", + '<h1>Drupal</h1><p>Drupal</p>' => "======== DRUPAL ==============================================================\n\nDrupal\n\n", + '<h2>Drupal</h2>' => "-------- DRUPAL --------------------------------------------------------------\n\n", + '<h2>Drupal</h2><p>Drupal</p>' => "-------- DRUPAL --------------------------------------------------------------\n\nDrupal\n\n", + '<h3>Drupal</h3>' => ".... Drupal\n\n", + '<h3>Drupal</h3><p>Drupal</p>' => ".... Drupal\n\nDrupal\n\n", + '<h4>Drupal</h4>' => ".. Drupal\n\n", + '<h4>Drupal</h4><p>Drupal</p>' => ".. Drupal\n\nDrupal\n\n", + '<h5>Drupal</h5>' => "Drupal\n\n", + '<h5>Drupal</h5><p>Drupal</p>' => "Drupal\n\nDrupal\n\n", + '<h6>Drupal</h6>' => "Drupal\n\n", + '<h6>Drupal</h6><p>Drupal</p>' => "Drupal\n\nDrupal\n\n", + '<hr />Drupal<hr />' => "------------------------------------------------------------------------------\nDrupal\n------------------------------------------------------------------------------\n", + '<hr/>Drupal<hr/>' => "------------------------------------------------------------------------------\nDrupal\n------------------------------------------------------------------------------\n", + '<hr/>Drupal<hr/><p>Drupal</p>' => "------------------------------------------------------------------------------\nDrupal\n------------------------------------------------------------------------------\nDrupal\n\n", + '<i>Drupal</i>' => "/Drupal/\n", + '<p>Drupal</p>' => "Drupal\n\n", + '<p>Drupal</p><p>Drupal</p>' => "Drupal\n\nDrupal\n\n", + '<strong>Drupal</strong>' => "*Drupal*\n", + // @todo Tables are currently not supported. + '<table><tr><td>Drupal</td><td>Drupal</td></tr><tr><td>Drupal</td><td>Drupal</td></tr></table>' => "DrupalDrupalDrupalDrupal\n", + '<table><tr><td>Drupal</td></tr></table><p>Drupal</p>' => "Drupal\nDrupal\n\n", + // @todo The <u> tag is currently not supported. + '<u>Drupal</u>' => "Drupal\n", + '<ul><li>Drupal</li></ul>' => " * Drupal\n\n", + '<ul><li>Drupal <em>Drupal</em> Drupal</li></ul>' => " * Drupal /Drupal/ Drupal\n\n", + // @todo Lines containing nothing but spaces should be trimmed. + '<ul><li>Drupal</li><li><ol><li>Drupal</li><li>Drupal</li></ol></li></ul>' => " * Drupal\n * 1) Drupal\n 2) Drupal\n \n\n", + '<ul><li>Drupal</li><li><ol><li>Drupal</li></ol></li><li>Drupal</li></ul>' => " * Drupal\n * 1) Drupal\n \n * Drupal\n\n", + '<ul><li>Drupal</li><li>Drupal</li></ul>' => " * Drupal\n * Drupal\n\n", + '<ul><li>Drupal</li></ul><p>Drupal</p>' => " * Drupal\n\nDrupal\n\n", + '<ol><li>Drupal</li></ol>' => " 1) Drupal\n\n", + '<ol><li>Drupal</li><li><ul><li>Drupal</li><li>Drupal</li></ul></li></ol>' => " 1) Drupal\n 2) * Drupal\n * Drupal\n \n\n", + '<ol><li>Drupal</li><li>Drupal</li></ol>' => " 1) Drupal\n 2) Drupal\n\n", + '<ol>Drupal</ol>' => "Drupal\n\n", + '<ol><li>Drupal</li></ol><p>Drupal</p>' => " 1) Drupal\n\nDrupal\n\n", + '<dl><dt>Drupal</dt></dl>' => "Drupal\n\n", + '<dl><dt>Drupal</dt><dd>Drupal</dd></dl>' => "Drupal\n Drupal\n\n", + '<dl><dt>Drupal</dt><dd>Drupal</dd><dt>Drupal</dt><dd>Drupal</dd></dl>' => "Drupal\n Drupal\nDrupal\n Drupal\n\n", + '<dl><dt>Drupal</dt><dd>Drupal</dd></dl><p>Drupal</p>' => "Drupal\n Drupal\n\nDrupal\n\n", + '<dl><dt>Drupal<dd>Drupal</dl>' => "Drupal\n Drupal\n\n", + '<dl><dt>Drupal</dt></dl><p>Drupal</p>' => "Drupal\n\nDrupal\n\n", + // @todo Again, lines containing only spaces should be trimmed. + '<ul><li>Drupal</li><li><dl><dt>Drupal</dt><dd>Drupal</dd><dt>Drupal</dt><dd>Drupal</dd></dl></li><li>Drupal</li></ul>' => " * Drupal\n * Drupal\n Drupal\n Drupal\n Drupal\n \n * Drupal\n\n", + // Tests malformed HTML tags. + '<br>Drupal<br>Drupal' => "Drupal\nDrupal\n", + '<hr>Drupal<hr>Drupal' => "------------------------------------------------------------------------------\nDrupal\n------------------------------------------------------------------------------\nDrupal\n", + '<ol><li>Drupal<li>Drupal</ol>' => " 1) Drupal\n 2) Drupal\n\n", + '<ul><li>Drupal <em>Drupal</em> Drupal</ul></ul>' => " * Drupal /Drupal/ Drupal\n\n", + '<ul><li>Drupal<li>Drupal</ol>' => " * Drupal\n * Drupal\n\n", + '<ul><li>Drupal<li>Drupal</ul>' => " * Drupal\n * Drupal\n\n", + '<ul>Drupal</ul>' => "Drupal\n\n", + 'Drupal</ul></ol></dl><li>Drupal' => "Drupal\n * Drupal\n", + '<dl>Drupal</dl>' => "Drupal\n\n", + '<dl>Drupal</dl><p>Drupal</p>' => "Drupal\n\nDrupal\n\n", + '<dt>Drupal</dt>' => "Drupal\n", + // Tests some unsupported HTML tags. + '<html>Drupal</html>' => "Drupal\n", + // @todo Perhaps the contents of <script> tags should be dropped. + '<script type="text/javascript">Drupal</script>' => "Drupal\n", + ); + + foreach ($tests as $html => $text) { + $this->assertHtmlToText($html, $text, 'Supported tags'); + } + } + + /** + * Test $allowed_tags argument of drupal_html_to_text(). + */ + function testDrupalHtmlToTextArgs() { + // The second parameter of drupal_html_to_text() overrules the allowed tags. + $this->assertHtmlToText( + 'Drupal <b>Drupal</b> Drupal', + "Drupal *Drupal* Drupal\n", + 'Allowed <b> tag found', + array('b') + ); + $this->assertHtmlToText( + 'Drupal <h1>Drupal</h1> Drupal', + "Drupal Drupal Drupal\n", + 'Disallowed <h1> tag not found', + array('b') + ); + + $this->assertHtmlToText( + 'Drupal <p><em><b>Drupal</b></em><p> Drupal', + "Drupal Drupal Drupal\n", + 'Disallowed <p>, <em>, and <b> tags not found', + array('a', 'br', 'h1') + ); + + $this->assertHtmlToText( + '<html><body>Drupal</body></html>', + "Drupal\n", + 'Unsupported <html> and <body> tags not found', + array('html', 'body') + ); + } + + /** + * 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() { + $input = "<p>Drupal Drupal\n\nDrupal<pre>Drupal Drupal\n\nDrupal</pre>Drupal Drupal\n\nDrupal</p>"; + // @todo The whitespace should be collapsed. + $collapsed = "Drupal Drupal\n\nDrupalDrupal Drupal\n\nDrupalDrupal Drupal\n\nDrupal\n\n"; + $this->assertHtmlToText( + $input, + $collapsed, + 'Whitespace is collapsed', + array('p') + ); + } + + /** + * Test that text separated by block-level tags in HTML get separated by + * (at least) a newline in the plaintext version. + */ + function testDrupalHtmlToTextBlockTagToNewline() { + $input = '[text]' + . '<blockquote>[blockquote]</blockquote>' + . '<br />[br]' + . '<dl><dt>[dl-dt]</dt>' + . '<dt>[dt]</dt>' + . '<dd>[dd]</dd>' + . '<dd>[dd-dl]</dd></dl>' + . '<h1>[h1]</h1>' + . '<h2>[h2]</h2>' + . '<h3>[h3]</h3>' + . '<h4>[h4]</h4>' + . '<h5>[h5]</h5>' + . '<h6>[h6]</h6>' + . '<hr />[hr]' + . '<ol><li>[ol-li]</li>' + . '<li>[li]</li>' + . '<li>[li-ol]</li></ol>' + . '<p>[p]</p>' + . '<ul><li>[ul-li]</li>' + . '<li>[li-ul]</li></ul>' + . '[text]'; + $output = drupal_html_to_text($input); + $pass = $this->assertFalse( + preg_match('/\][^\n]*\[/s', $output), + 'Block-level HTML tags should force newlines' + ); + if (!$pass) { + $this->verbose($this->stringToHtml($output)); + } + $output_upper = drupal_strtoupper($output); + $upper_input = drupal_strtoupper($input); + $upper_output = drupal_html_to_text($upper_input); + $pass = $this->assertEqual( + $upper_output, + $output_upper, + 'Tag recognition should be case-insensitive' + ); + if (!$pass) { + $this->verbose( + $upper_output + . '<br />should be equal to <br />' + . $output_upper + ); + } + } + + /** + * Test that headers are properly separated from surrounding text. + */ + function testHeaderSeparation() { + $html = 'Drupal<h1>Drupal</h1>Drupal'; + // @todo There should be more space above the header than below it. + $text = "Drupal\n======== DRUPAL ==============================================================\n\nDrupal\n"; + $this->assertHtmlToText($html, $text, + 'Text before and after <h1> tag'); + $html = '<p>Drupal</p><h1>Drupal</h1>Drupal'; + // @todo There should be more space above the header than below it. + $text = "Drupal\n\n======== DRUPAL ==============================================================\n\nDrupal\n"; + $this->assertHtmlToText($html, $text, + 'Paragraph before and text after <h1> tag'); + $html = 'Drupal<h1>Drupal</h1><p>Drupal</p>'; + // @todo There should be more space above the header than below it. + $text = "Drupal\n======== DRUPAL ==============================================================\n\nDrupal\n\n"; + $this->assertHtmlToText($html, $text, + 'Text before and paragraph after <h1> tag'); + $html = '<p>Drupal</p><h1>Drupal</h1><p>Drupal</p>'; + $text = "Drupal\n\n======== DRUPAL ==============================================================\n\nDrupal\n\n"; + $this->assertHtmlToText($html, $text, + 'Paragraph before and after <h1> tag'); + } + + /** + * Test that footnote references are properly generated. + */ + function testFootnoteReferences() { + global $base_path, $base_url; + $source = '<a href="http://www.example.com/node/1">Host and path</a>' + . '<br /><a href="http://www.example.com">Host, no path</a>' + . '<br /><a href="' . $base_path . 'node/1">Path, no host</a>' + . '<br /><a href="node/1">Relative path</a>'; + // @todo Footnote URLs should be absolute. + $tt = "Host and path [1]" + . "\nHost, no path [2]" + // @todo The following two references should be combined. + . "\nPath, no host [3]" + . "\nRelative path [4]" + . "\n" + . "\n[1] http://www.example.com/node/1" + . "\n[2] http://www.example.com" + // @todo The following two references should be combined. + . "\n[3] $base_url/node/1" + . "\n[4] node/1\n"; + $this->assertHtmlToText($source, $tt, 'Footnotes'); + } + + /** + * Test that combinations of paragraph breaks, line breaks, linefeeds, + * and spaces are properly handled. + */ + function testDrupalHtmlToTextParagraphs() { + $tests = array(); + $tests[] = array( + 'html' => "<p>line 1<br />\nline 2<br />line 3\n<br />line 4</p><p>paragraph</p>", + // @todo Trailing line breaks should be trimmed. + 'text' => "line 1\nline 2\nline 3\nline 4\n\nparagraph\n\n", + ); + $tests[] = array( + 'html' => "<p>line 1<br /> line 2</p> <p>line 4<br /> line 5</p> <p>0</p>", + // @todo Trailing line breaks should be trimmed. + 'text' => "line 1\nline 2\n\nline 4\nline 5\n\n0\n\n", + ); + foreach ($tests as $test) { + $this->assertHtmlToText($test['html'], $test['text'], 'Paragraph breaks'); + } + } + + /** + * Tests that drupal_html_to_text() wraps before 1000 characters. + * + * RFC 3676 says, "The Text/Plain media type is the lowest common + * denominator of Internet email, with lines of no more than 998 characters." + * + * RFC 2046 says, "SMTP [RFC-821] allows a maximum of 998 octets before the + * next CRLF sequence." + * + * RFC 821 says, "The maximum total length of a text line including the + * <CRLF> is 1000 characters." + */ + function testVeryLongLineWrap() { + $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); + // We must use strlen() rather than drupal_strlen() in order to count + // octets rather than characters. + $line_length_limit = 1000 - drupal_strlen($eol); + $maximum_line_length = 0; + foreach (explode($eol, $output) as $line) { + // We must use strlen() rather than drupal_strlen() in order to count + // octets rather than characters. + $maximum_line_length = max($maximum_line_length, strlen($line . $eol)); + } + $verbose = 'Maximum line length found was ' . $maximum_line_length . ' octets.'; + $this->assertTrue($maximum_line_length <= 1000, $verbose); + } +} diff -Naur drupal-7.0/modules/simpletest/tests/menu.test drupal-7.66/modules/simpletest/tests/menu.test --- drupal-7.0/modules/simpletest/tests/menu.test 2010-12-02 18:34:24.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/menu.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,11 +1,126 @@ <?php -// $Id: menu.test,v 1.42 2010/12/02 17:34:24 webchick Exp $ /** * @file * Provides SimpleTests for menu.inc. */ +class MenuWebTestCase extends DrupalWebTestCase { + function setUp() { + $modules = func_get_args(); + if (isset($modules[0]) && is_array($modules[0])) { + $modules = $modules[0]; + } + parent::setUp($modules); + } + + /** + * Assert that a given path shows certain breadcrumb links. + * + * @param string $goto + * (optional) A system path to pass to DrupalWebTestCase::drupalGet(). + * @param array $trail + * An associative array whose keys are expected breadcrumb link paths and + * whose values are expected breadcrumb link texts (not sanitized). + * @param string $page_title + * (optional) A page title to additionally assert via + * DrupalWebTestCase::assertTitle(). Without site name suffix. + * @param array $tree + * (optional) An associative array whose keys are link paths and whose + * values are link titles (not sanitized) of an expected active trail in a + * menu tree output on the page. + * @param $last_active + * (optional) Whether the last link in $tree is expected to be active (TRUE) + * or just to be in the active trail (FALSE). + */ + protected function assertBreadcrumb($goto, array $trail, $page_title = NULL, array $tree = array(), $last_active = TRUE) { + if (isset($goto)) { + $this->drupalGet($goto); + } + // Compare paths with actual breadcrumb. + $parts = $this->getParts(); + $pass = TRUE; + foreach ($trail as $path => $title) { + $url = url($path); + $part = array_shift($parts); + $pass = ($pass && $part['href'] === $url && $part['text'] === check_plain($title)); + } + // No parts must be left, or an expected "Home" will always pass. + $pass = ($pass && empty($parts)); + + $this->assertTrue($pass, format_string('Breadcrumb %parts found on @path.', array( + '%parts' => implode(' » ', $trail), + '@path' => $this->getUrl(), + ))); + + // Additionally assert page title, if given. + if (isset($page_title)) { + $this->assertTitle(strtr('@title | Drupal', array('@title' => $page_title))); + } + + // Additionally assert active trail in a menu tree output, if given. + if ($tree) { + end($tree); + $active_link_path = key($tree); + $active_link_title = array_pop($tree); + $xpath = ''; + if ($tree) { + $i = 0; + foreach ($tree as $link_path => $link_title) { + $part_xpath = (!$i ? '//' : '/following-sibling::ul/descendant::'); + $part_xpath .= 'li[contains(@class, :class)]/a[contains(@href, :href) and contains(text(), :title)]'; + $part_args = array( + ':class' => 'active-trail', + ':href' => url($link_path), + ':title' => $link_title, + ); + $xpath .= $this->buildXPathQuery($part_xpath, $part_args); + $i++; + } + $elements = $this->xpath($xpath); + $this->assertTrue(!empty($elements), 'Active trail to current page was found in menu tree.'); + + // Append prefix for active link asserted below. + $xpath .= '/following-sibling::ul/descendant::'; + } + else { + $xpath .= '//'; + } + $xpath_last_active = ($last_active ? 'and contains(@class, :class-active)' : ''); + $xpath .= 'li[contains(@class, :class-trail)]/a[contains(@href, :href) ' . $xpath_last_active . 'and contains(text(), :title)]'; + $args = array( + ':class-trail' => 'active-trail', + ':class-active' => 'active', + ':href' => url($active_link_path), + ':title' => $active_link_title, + ); + $elements = $this->xpath($xpath, $args); + $this->assertTrue(!empty($elements), format_string('Active link %title was found in menu tree, including active trail links %tree.', array( + '%title' => $active_link_title, + '%tree' => implode(' » ', $tree), + ))); + } + } + + /** + * Returns the breadcrumb contents of the current page in the internal browser. + */ + protected function getParts() { + $parts = array(); + $elements = $this->xpath('//div[@class="breadcrumb"]/a'); + if (!empty($elements)) { + foreach ($elements as $element) { + $parts[] = array( + 'text' => (string) $element, + 'href' => (string) $element['href'], + 'title' => (string) $element['title'], + ); + } + } + return $parts; + } +} + class MenuRouterTestCase extends DrupalWebTestCase { public static function getInfo() { return array( @@ -30,8 +145,20 @@ */ function testTitleCallbackFalse() { $this->drupalGet('node'); - $this->assertText('A title with @placeholder', t('Raw text found on the page')); - $this->assertNoText(t('A title with @placeholder', array('@placeholder' => 'some other text')), t('Text with placeholder substitutions not found.')); + $this->assertText('A title with @placeholder', 'Raw text found on the page'); + $this->assertNoText(t('A title with @placeholder', array('@placeholder' => 'some other text')), 'Text with placeholder substitutions not found.'); + } + + /** + * Tests page title of MENU_CALLBACKs. + */ + function testTitleMenuCallback() { + // Verify that the menu router item title is not visible. + $this->drupalGet(''); + $this->assertNoText(t('Menu Callback Title')); + // Verify that the menu router item title is output as page title. + $this->drupalGet('menu_callback_title'); + $this->assertText(t('Menu Callback Title')); } /** @@ -39,8 +166,8 @@ */ function testThemeCallbackAdministrative() { $this->drupalGet('menu-test/theme-callback/use-admin-theme'); - $this->assertText('Custom theme: seven. Actual theme: seven.', t('The administrative theme can be correctly set in a theme callback.')); - $this->assertRaw('seven/style.css', t("The administrative theme's CSS appears on the page.")); + $this->assertText('Custom theme: seven. Actual theme: seven.', 'The administrative theme can be correctly set in a theme callback.'); + $this->assertRaw('seven/style.css', "The administrative theme's CSS appears on the page."); } /** @@ -48,8 +175,8 @@ */ function testThemeCallbackInheritance() { $this->drupalGet('menu-test/theme-callback/use-admin-theme/inheritance'); - $this->assertText('Custom theme: seven. Actual theme: seven. Theme callback inheritance is being tested.', t('Theme callback inheritance correctly uses the administrative theme.')); - $this->assertRaw('seven/style.css', t("The administrative theme's CSS appears on the page.")); + $this->assertText('Custom theme: seven. Actual theme: seven. Theme callback inheritance is being tested.', 'Theme callback inheritance correctly uses the administrative theme.'); + $this->assertRaw('seven/style.css', "The administrative theme's CSS appears on the page."); } /** @@ -58,7 +185,7 @@ */ function testFileInheritance() { $this->drupalGet('admin/config/development/file-inheritance'); - $this->assertText('File inheritance test description', t('File inheritance works.')); + $this->assertText('File inheritance test description', 'File inheritance works.'); } /** @@ -81,14 +208,14 @@ // For a regular user, the fact that the site is in maintenance mode means // we expect the theme callback system to be bypassed entirely. $this->drupalGet('menu-test/theme-callback/use-admin-theme'); - $this->assertRaw('bartik/css/style.css', t("The maintenance theme's CSS appears on the page.")); + $this->assertRaw('bartik/css/style.css', "The maintenance theme's CSS appears on the page."); // An administrator, however, should continue to see the requested theme. $admin_user = $this->drupalCreateUser(array('access site in maintenance mode')); $this->drupalLogin($admin_user); $this->drupalGet('menu-test/theme-callback/use-admin-theme'); - $this->assertText('Custom theme: seven. Actual theme: seven.', t('The theme callback system is correctly triggered for an administrator when the site is in maintenance mode.')); - $this->assertRaw('seven/style.css', t("The administrative theme's CSS appears on the page.")); + $this->assertText('Custom theme: seven. Actual theme: seven.', 'The theme callback system is correctly triggered for an administrator when the site is in maintenance mode.'); + $this->assertRaw('seven/style.css', "The administrative theme's CSS appears on the page."); } /** @@ -115,13 +242,13 @@ $loggedInUser = $this->drupalCreateUser(array()); $this->drupalLogin($loggedInUser); - $this->DrupalGet('user/login'); + $this->drupalGet('user/login'); // Check that we got to 'user'. - $this->assertTrue($this->url == url('user', array('absolute' => TRUE)), t("Logged-in user redirected to q=user on accessing q=user/login")); + $this->assertTrue($this->url == url('user', array('absolute' => TRUE)), "Logged-in user redirected to q=user on accessing q=user/login"); // user/register should redirect to user/UID/edit. - $this->DrupalGet('user/register'); - $this->assertTrue($this->url == url('user/' . $this->loggedInUser->uid . '/edit', array('absolute' => TRUE)), t("Logged-in user redirected to q=user/UID/edit on accessing q=user/register")); + $this->drupalGet('user/register'); + $this->assertTrue($this->url == url('user/' . $this->loggedInUser->uid . '/edit', array('absolute' => TRUE)), "Logged-in user redirected to q=user/UID/edit on accessing q=user/register"); } /** @@ -130,14 +257,14 @@ function testThemeCallbackOptionalTheme() { // Request a theme that is not enabled. $this->drupalGet('menu-test/theme-callback/use-stark-theme'); - $this->assertText('Custom theme: NONE. Actual theme: bartik.', t('The theme callback system falls back on the default theme when a theme that is not enabled is requested.')); - $this->assertRaw('bartik/css/style.css', t("The default theme's CSS appears on the page.")); + $this->assertText('Custom theme: NONE. Actual theme: bartik.', 'The theme callback system falls back on the default theme when a theme that is not enabled is requested.'); + $this->assertRaw('bartik/css/style.css', "The default theme's CSS appears on the page."); // Now enable the theme and request it again. theme_enable(array('stark')); $this->drupalGet('menu-test/theme-callback/use-stark-theme'); - $this->assertText('Custom theme: stark. Actual theme: stark.', t('The theme callback system uses an optional theme once it has been enabled.')); - $this->assertRaw('stark/layout.css', t("The optional theme's CSS appears on the page.")); + $this->assertText('Custom theme: stark. Actual theme: stark.', 'The theme callback system uses an optional theme once it has been enabled.'); + $this->assertRaw('stark/layout.css', "The optional theme's CSS appears on the page."); } /** @@ -145,8 +272,8 @@ */ function testThemeCallbackFakeTheme() { $this->drupalGet('menu-test/theme-callback/use-fake-theme'); - $this->assertText('Custom theme: NONE. Actual theme: bartik.', t('The theme callback system falls back on the default theme when a theme that does not exist is requested.')); - $this->assertRaw('bartik/css/style.css', t("The default theme's CSS appears on the page.")); + $this->assertText('Custom theme: NONE. Actual theme: bartik.', 'The theme callback system falls back on the default theme when a theme that does not exist is requested.'); + $this->assertRaw('bartik/css/style.css', "The default theme's CSS appears on the page."); } /** @@ -154,8 +281,8 @@ */ function testThemeCallbackNoThemeRequested() { $this->drupalGet('menu-test/theme-callback/no-theme-requested'); - $this->assertText('Custom theme: NONE. Actual theme: bartik.', t('The theme callback system falls back on the default theme when no theme is requested.')); - $this->assertRaw('bartik/css/style.css', t("The default theme's CSS appears on the page.")); + $this->assertText('Custom theme: NONE. Actual theme: bartik.', 'The theme callback system falls back on the default theme when no theme is requested.'); + $this->assertRaw('bartik/css/style.css', "The default theme's CSS appears on the page."); } /** @@ -170,8 +297,8 @@ // Visit a page that does not implement a theme callback. The above request // should be honored. $this->drupalGet('menu-test/no-theme-callback'); - $this->assertText('Custom theme: stark. Actual theme: stark.', t('The result of hook_custom_theme() is used as the theme for the current page.')); - $this->assertRaw('stark/layout.css', t("The Stark theme's CSS appears on the page.")); + $this->assertText('Custom theme: stark. Actual theme: stark.', 'The result of hook_custom_theme() is used as the theme for the current page.'); + $this->assertRaw('stark/layout.css', "The Stark theme's CSS appears on the page."); } /** @@ -186,8 +313,8 @@ // The menu "theme callback" should take precedence over a value set in // hook_custom_theme(). $this->drupalGet('menu-test/theme-callback/use-admin-theme'); - $this->assertText('Custom theme: seven. Actual theme: seven.', t('The result of hook_custom_theme() does not override what was set in a theme callback.')); - $this->assertRaw('seven/style.css', t("The Seven theme's CSS appears on the page.")); + $this->assertText('Custom theme: seven. Actual theme: seven.', 'The result of hook_custom_theme() does not override what was set in a theme callback.'); + $this->assertRaw('seven/style.css', "The Seven theme's CSS appears on the page."); } /** @@ -221,19 +348,19 @@ menu_link_maintain('menu_test', 'update', 'menu_test_maintain/1', 'Menu link updated'); // Load a different page to be sure that we have up to date information. $this->drupalGet('menu_test_maintain/1'); - $this->assertLink(t('Menu link updated'), 0, t('Found updated menu link')); - $this->assertNoLink(t('Menu link #1'), 0, t('Not found menu link #1')); - $this->assertNoLink(t('Menu link #1'), 0, t('Not found menu link #1-1')); - $this->assertLink(t('Menu link #2'), 0, t('Found menu link #2')); + $this->assertLink(t('Menu link updated'), 0, 'Found updated menu link'); + $this->assertNoLink(t('Menu link #1'), 0, 'Not found menu link #1'); + $this->assertNoLink(t('Menu link #1'), 0, 'Not found menu link #1-1'); + $this->assertLink(t('Menu link #2'), 0, 'Found menu link #2'); // Delete all links for the given path. menu_link_maintain('menu_test', 'delete', 'menu_test_maintain/1', ''); // Load a different page to be sure that we have up to date information. $this->drupalGet('menu_test_maintain/2'); - $this->assertNoLink(t('Menu link updated'), 0, t('Not found deleted menu link')); - $this->assertNoLink(t('Menu link #1'), 0, t('Not found menu link #1')); - $this->assertNoLink(t('Menu link #1'), 0, t('Not found menu link #1-1')); - $this->assertLink(t('Menu link #2'), 0, t('Found menu link #2')); + $this->assertNoLink(t('Menu link updated'), 0, 'Not found deleted menu link'); + $this->assertNoLink(t('Menu link #1'), 0, 'Not found menu link #1'); + $this->assertNoLink(t('Menu link #1'), 0, 'Not found menu link #1-1'); + $this->assertLink(t('Menu link #2'), 0, 'Found menu link #2'); } /** @@ -270,7 +397,7 @@ $sql = "SELECT menu_name FROM {menu_links} WHERE router_path = 'menu_name_test'"; $name = db_query($sql)->fetchField(); - $this->assertEqual($name, 'original', t('Menu name is "original".')); + $this->assertEqual($name, 'original', 'Menu name is "original".'); // Change the menu_name parameter in menu_test.module, then force a menu // rebuild. @@ -279,7 +406,7 @@ $sql = "SELECT menu_name FROM {menu_links} WHERE router_path = 'menu_name_test'"; $name = db_query($sql)->fetchField(); - $this->assertEqual($name, 'changed', t('Menu name was successfully changed after rebuild.')); + $this->assertEqual($name, 'changed', 'Menu name was successfully changed after rebuild.'); } /** @@ -290,8 +417,8 @@ $child_link = db_query('SELECT * FROM {menu_links} WHERE link_path = :link_path', array(':link_path' => 'menu-test/hierarchy/parent/child'))->fetchAssoc(); $unattached_child_link = db_query('SELECT * FROM {menu_links} WHERE link_path = :link_path', array(':link_path' => 'menu-test/hierarchy/parent/child2/child'))->fetchAssoc(); - $this->assertEqual($child_link['plid'], $parent_link['mlid'], t('The parent of a directly attached child is correct.')); - $this->assertEqual($unattached_child_link['plid'], $parent_link['mlid'], t('The parent of a non-directly attached child is correct.')); + $this->assertEqual($child_link['plid'], $parent_link['mlid'], 'The parent of a directly attached child is correct.'); + $this->assertEqual($unattached_child_link['plid'], $parent_link['mlid'], 'The parent of a non-directly attached child is correct.'); } /** @@ -311,40 +438,40 @@ $plid = $parent['mlid']; $link = $links['menu-test/hidden/menu/list']; - $this->assertEqual($link['depth'], $depth, t('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); - $this->assertEqual($link['plid'], $plid, t('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); + $this->assertEqual($link['depth'], $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); + $this->assertEqual($link['plid'], $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); $link = $links['menu-test/hidden/menu/add']; - $this->assertEqual($link['depth'], $depth, t('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); - $this->assertEqual($link['plid'], $plid, t('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); + $this->assertEqual($link['depth'], $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); + $this->assertEqual($link['plid'], $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); $link = $links['menu-test/hidden/menu/settings']; - $this->assertEqual($link['depth'], $depth, t('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); - $this->assertEqual($link['plid'], $plid, t('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); + $this->assertEqual($link['depth'], $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); + $this->assertEqual($link['plid'], $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); $link = $links['menu-test/hidden/menu/manage/%']; - $this->assertEqual($link['depth'], $depth, t('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); - $this->assertEqual($link['plid'], $plid, t('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); + $this->assertEqual($link['depth'], $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); + $this->assertEqual($link['plid'], $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); $parent = $links['menu-test/hidden/menu/manage/%']; $depth = $parent['depth'] + 1; $plid = $parent['mlid']; $link = $links['menu-test/hidden/menu/manage/%/list']; - $this->assertEqual($link['depth'], $depth, t('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); - $this->assertEqual($link['plid'], $plid, t('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); + $this->assertEqual($link['depth'], $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); + $this->assertEqual($link['plid'], $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); $link = $links['menu-test/hidden/menu/manage/%/add']; - $this->assertEqual($link['depth'], $depth, t('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); - $this->assertEqual($link['plid'], $plid, t('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); + $this->assertEqual($link['depth'], $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); + $this->assertEqual($link['plid'], $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); $link = $links['menu-test/hidden/menu/manage/%/edit']; - $this->assertEqual($link['depth'], $depth, t('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); - $this->assertEqual($link['plid'], $plid, t('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); + $this->assertEqual($link['depth'], $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); + $this->assertEqual($link['plid'], $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); $link = $links['menu-test/hidden/menu/manage/%/delete']; - $this->assertEqual($link['depth'], $depth, t('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); - $this->assertEqual($link['plid'], $plid, t('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); + $this->assertEqual($link['depth'], $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); + $this->assertEqual($link['plid'], $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); // Verify links for two dynamic arguments. $links = db_select('menu_links', 'ml') @@ -359,28 +486,36 @@ $plid = $parent['mlid']; $link = $links['menu-test/hidden/block/list']; - $this->assertEqual($link['depth'], $depth, t('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); - $this->assertEqual($link['plid'], $plid, t('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); + $this->assertEqual($link['depth'], $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); + $this->assertEqual($link['plid'], $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); $link = $links['menu-test/hidden/block/add']; - $this->assertEqual($link['depth'], $depth, t('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); - $this->assertEqual($link['plid'], $plid, t('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); + $this->assertEqual($link['depth'], $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); + $this->assertEqual($link['plid'], $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); $link = $links['menu-test/hidden/block/manage/%/%']; - $this->assertEqual($link['depth'], $depth, t('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); - $this->assertEqual($link['plid'], $plid, t('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); + $this->assertEqual($link['depth'], $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); + $this->assertEqual($link['plid'], $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); $parent = $links['menu-test/hidden/block/manage/%/%']; $depth = $parent['depth'] + 1; $plid = $parent['mlid']; $link = $links['menu-test/hidden/block/manage/%/%/configure']; - $this->assertEqual($link['depth'], $depth, t('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); - $this->assertEqual($link['plid'], $plid, t('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); + $this->assertEqual($link['depth'], $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); + $this->assertEqual($link['plid'], $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); $link = $links['menu-test/hidden/block/manage/%/%/delete']; - $this->assertEqual($link['depth'], $depth, t('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); - $this->assertEqual($link['plid'], $plid, t('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); + $this->assertEqual($link['depth'], $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); + $this->assertEqual($link['plid'], $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); + } + + /** + * Test menu_get_item() with empty ancestors. + */ + function testMenuGetItemNoAncestors() { + variable_set('menu_masks', array()); + $this->drupalGet(''); } /** @@ -389,7 +524,7 @@ function testMenuSetItem() { $item = menu_get_item('node'); - $this->assertEqual($item['path'], 'node', t("Path from menu_get_item('node') is equal to 'node'"), 'menu'); + $this->assertEqual($item['path'], 'node', "Path from menu_get_item('node') is equal to 'node'", 'menu'); // Modify the path for the item then save it. $item['path'] = 'node_test'; @@ -397,22 +532,22 @@ menu_set_item('node', $item); $compare_item = menu_get_item('node'); - $this->assertEqual($compare_item, $item, t('Modified menu item is equal to newly retrieved menu item.'), 'menu'); + $this->assertEqual($compare_item, $item, 'Modified menu item is equal to newly retrieved menu item.', 'menu'); } /** - * Test menu maintainance hooks. + * Test menu maintenance hooks. */ function testMenuItemHooks() { // Create an item. menu_link_maintain('menu_test', 'insert', 'menu_test_maintain/4', 'Menu link #4'); - $this->assertEqual(menu_test_static_variable(), 'insert', t('hook_menu_link_insert() fired correctly')); + $this->assertEqual(menu_test_static_variable(), 'insert', 'hook_menu_link_insert() fired correctly'); // Update the item. menu_link_maintain('menu_test', 'update', 'menu_test_maintain/4', 'Menu link updated'); - $this->assertEqual(menu_test_static_variable(), 'update', t('hook_menu_link_update() fired correctly')); + $this->assertEqual(menu_test_static_variable(), 'update', 'hook_menu_link_update() fired correctly'); // Delete the item. menu_link_maintain('menu_test', 'delete', 'menu_test_maintain/4', ''); - $this->assertEqual(menu_test_static_variable(), 'delete', t('hook_menu_link_delete() fired correctly')); + $this->assertEqual(menu_test_static_variable(), 'delete', 'hook_menu_link_delete() fired correctly'); } /** @@ -437,8 +572,8 @@ // Load front page. $this->drupalGet('node'); - $this->assertRaw('title="Test title attribute"', t('Title attribute of a menu link renders.')); - $this->assertRaw('testparam=testvalue', t('Query parameter added to menu link.')); + $this->assertRaw('title="Test title attribute"', 'Title attribute of a menu link renders.'); + $this->assertRaw('testparam=testvalue', 'Query parameter added to menu link.'); } /** @@ -464,14 +599,14 @@ } /** - * Get a url and assert the title given a case number. If override is true, + * Get a URL and assert the title given a case number. If override is true, * the title is asserted to begin with "Alternative". */ private function menuItemTitlesCasesHelper($case_no, $override = FALSE) { $this->drupalGet('menu-title-test/case' . $case_no); $this->assertResponse(200); $asserted_title = $override ? 'Alternative example title - Case ' . $case_no : 'Example title - Case ' . $case_no; - $this->assertTitle($asserted_title . ' | Drupal', t('Menu title is') . ': ' . $asserted_title, 'Menu'); + $this->assertTitle($asserted_title . ' | Drupal', format_string('Menu title is: %title.', array('%title' => $asserted_title)), 'Menu'); } /** @@ -528,7 +663,7 @@ foreach ($expected as $router_path => $load_functions) { $router_item = $this->menuLoadRouter($router_path); - $this->assertIdentical(unserialize($router_item['load_functions']), $load_functions, t('Expected load functions for router %router_path' , array('%router_path' => $router_path))); + $this->assertIdentical(unserialize($router_item['load_functions']), $load_functions, format_string('Expected load functions for router %router_path' , array('%router_path' => $router_path))); } } } @@ -609,7 +744,7 @@ $menu_link = menu_link_load($mlid); menu_link_save($menu_link); - $this->assertEqual($menu_link['plid'], $plid, t('Menu link %mlid has parent of %plid, expected %expected_plid.', array('%mlid' => $mlid, '%plid' => $menu_link['plid'], '%expected_plid' => $plid))); + $this->assertEqual($menu_link['plid'], $plid, format_string('Menu link %mlid has parent of %plid, expected %expected_plid.', array('%mlid' => $mlid, '%plid' => $menu_link['plid'], '%expected_plid' => $plid))); } } @@ -770,14 +905,14 @@ function testMenuRebuildByVariable() { // Check if 'admin' path exists. $admin_exists = db_query('SELECT path from {menu_router} WHERE path = :path', array(':path' => 'admin'))->fetchField(); - $this->assertEqual($admin_exists, 'admin', t("The path 'admin/' exists prior to deleting.")); + $this->assertEqual($admin_exists, 'admin', "The path 'admin/' exists prior to deleting."); // Delete the path item 'admin', and test that the path doesn't exist in the database. $delete = db_delete('menu_router') ->condition('path', 'admin') ->execute(); $admin_exists = db_query('SELECT path from {menu_router} WHERE path = :path', array(':path' => 'admin'))->fetchField(); - $this->assertFalse($admin_exists, t("The path 'admin/' has been deleted and doesn't exist in the database.")); + $this->assertFalse($admin_exists, "The path 'admin/' has been deleted and doesn't exist in the database."); // Now we enable the rebuild variable and trigger menu_execute_active_handler() // to rebuild the menu item. Now 'admin' should exist. @@ -785,7 +920,7 @@ // menu_execute_active_handler() should trigger the rebuild. $this->drupalGet('<front>'); $admin_exists = db_query('SELECT path from {menu_router} WHERE path = :path', array(':path' => 'admin'))->fetchField(); - $this->assertEqual($admin_exists, 'admin', t("The menu has been rebuilt, the path 'admin' now exists again.")); + $this->assertEqual($admin_exists, 'admin', "The menu has been rebuilt, the path 'admin' now exists again."); } } @@ -820,12 +955,12 @@ $tree = menu_tree_data($this->links); // Validate that parent items #1, #2, and #5 exist on the root level. - $this->assertSameLink($this->links[1], $tree[1]['link'], t('Parent item #1 exists.')); - $this->assertSameLink($this->links[2], $tree[2]['link'], t('Parent item #2 exists.')); - $this->assertSameLink($this->links[5], $tree[5]['link'], t('Parent item #5 exists.')); + $this->assertSameLink($this->links[1], $tree[1]['link'], 'Parent item #1 exists.'); + $this->assertSameLink($this->links[2], $tree[2]['link'], 'Parent item #2 exists.'); + $this->assertSameLink($this->links[5], $tree[5]['link'], 'Parent item #5 exists.'); // Validate that child item #4 exists at the correct location in the hierarchy. - $this->assertSameLink($this->links[4], $tree[2]['below'][3]['below'][4]['link'], t('Child item #4 exists in the hierarchy.')); + $this->assertSameLink($this->links[4], $tree[2]['below'][3]['below'][4]['link'], 'Child item #4 exists in the hierarchy.'); } /** @@ -841,14 +976,72 @@ * TRUE if the assertion succeeded, FALSE otherwise. */ protected function assertSameLink($link1, $link2, $message = '') { - return $this->assert($link1['mlid'] == $link2['mlid'], $message ? $message : t('First link is identical to second link')); + return $this->assert($link1['mlid'] == $link2['mlid'], $message ? $message : 'First link is identical to second link'); + } +} + +/** + * Menu tree output related tests. + */ +class MenuTreeOutputTestCase extends DrupalWebTestCase { + /** + * Dummy link structure acceptable for menu_tree_output(). + */ + var $tree_data = array( + '1'=> array( + 'link' => array( 'menu_name' => 'main-menu', 'mlid' => 1, 'hidden'=>0, 'has_children' => 1, 'title' => 'Item 1', 'in_active_trail' => 1, 'access'=>1, 'href' => 'a', 'localized_options' => array('attributes' => array('title' =>'')) ), + 'below' => array( + '2' => array('link' => array( 'menu_name' => 'main-menu', 'mlid' => 2, 'hidden'=>0, 'has_children' => 1, 'title' => 'Item 2', 'in_active_trail' => 1, 'access'=>1, 'href' => 'a/b', 'localized_options' => array('attributes' => array('title' =>'')) ), + 'below' => array( + '3' => array('link' => array( 'menu_name' => 'main-menu', 'mlid' => 3, 'hidden'=>0, 'has_children' => 0, 'title' => 'Item 3', 'in_active_trail' => 0, 'access'=>1, 'href' => 'a/b/c', 'localized_options' => array('attributes' => array('title' =>'')) ), + 'below' => array() ), + '4' => array('link' => array( 'menu_name' => 'main-menu', 'mlid' => 4, 'hidden'=>0, 'has_children' => 0, 'title' => 'Item 4', 'in_active_trail' => 0, 'access'=>1, 'href' => 'a/b/d', 'localized_options' => array('attributes' => array('title' =>'')) ), + 'below' => array() ) + ) + ) + ) + ), + '5' => array('link' => array( 'menu_name' => 'main-menu', 'mlid' => 5, 'hidden'=>1, 'has_children' => 0, 'title' => 'Item 5', 'in_active_trail' => 0, 'access'=>1, 'href' => 'e', 'localized_options' => array('attributes' => array('title' =>'')) ), 'below' => array( ) ), + '6' => array('link' => array( 'menu_name' => 'main-menu', 'mlid' => 6, 'hidden'=>0, 'has_children' => 0, 'title' => 'Item 6', 'in_active_trail' => 0, 'access'=>0, 'href' => 'f', 'localized_options' => array('attributes' => array('title' =>'')) ), 'below' => array( ) ), + '7' => array('link' => array( 'menu_name' => 'main-menu', 'mlid' => 7, 'hidden'=>0, 'has_children' => 0, 'title' => 'Item 7', 'in_active_trail' => 0, 'access'=>1, 'href' => 'g', 'localized_options' => array('attributes' => array('title' =>'')) ), 'below' => array( ) ) + ); + + public static function getInfo() { + return array( + 'name' => 'Menu tree output', + 'description' => 'Tests menu tree output functions.', + 'group' => 'Menu', + ); + } + + function setUp() { + parent::setUp(); + } + + /** + * Validate the generation of a proper menu tree output. + */ + function testMenuTreeData() { + $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 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'); + // Validate that the hidden and no access items are missing + $this->assertFalse( isset($output['5']), 'Hidden item should be missing'); + $this->assertFalse( isset($output['6']), 'False access should be missing'); + // Item 7 is after a couple hidden items. Just to make sure that 5 and 6 are skipped and 7 still included + $this->assertTrue( isset($output['7']), 'Item after hidden items is present'); } } /** * Menu breadcrumbs related tests. */ -class MenuBreadcrumbTestCase extends DrupalWebTestCase { +class MenuBreadcrumbTestCase extends MenuWebTestCase { public static function getInfo() { return array( 'name' => 'Breadcrumbs', @@ -858,7 +1051,12 @@ } function setUp() { - parent::setUp(array('menu_test')); + $modules = func_get_args(); + if (isset($modules[0]) && is_array($modules[0])) { + $modules = $modules[0]; + } + $modules[] = 'menu_test'; + parent::setUp($modules); $perms = array_keys(module_invoke_all('permission')); $this->admin_user = $this->drupalCreateUser($perms); $this->drupalLogin($this->admin_user); @@ -941,6 +1139,7 @@ $trail += array( 'admin/structure/menu/manage/navigation' => t('Navigation'), ); + $this->assertBreadcrumb("admin/structure/menu/item/6/edit", $trail); $this->assertBreadcrumb('admin/structure/menu/manage/navigation/edit', $trail); $this->assertBreadcrumb('admin/structure/menu/manage/navigation/add', $trail); @@ -1172,8 +1371,7 @@ $tree += array( $link['link_path'] => $link['link_title'], ); - // @todo Normally, you'd expect $term->name as page title here. - $this->assertBreadcrumb($link['link_path'], $trail, $link['link_title'], $tree); + $this->assertBreadcrumb($link['link_path'], $trail, $term->name, $tree); $this->assertRaw(check_plain($parent->title), 'Tagged node found.'); // Additionally make sure that this link appears only once; i.e., the @@ -1333,110 +1531,210 @@ $this->assertBreadcrumb('admin/reports/dblog', $trail, t('Recent log messages')); $this->assertNoResponse(403); } +} + +/** + * Tests active menu trails. + */ +class MenuTrailTestCase extends MenuWebTestCase { + public static function getInfo() { + return array( + 'name' => 'Active trail', + 'description' => 'Tests active menu trails and alteration functionality.', + 'group' => 'Menu', + ); + } + + function setUp() { + $modules = func_get_args(); + if (isset($modules[0]) && is_array($modules[0])) { + $modules = $modules[0]; + } + $modules[] = 'menu_test'; + parent::setUp($modules); + $this->admin_user = $this->drupalCreateUser(array('administer site configuration', 'access administration pages')); + $this->drupalLogin($this->admin_user); + + // This test puts menu links in the Navigation menu and then tests for + // their presence on the page, so we need to ensure that the Navigation + // block will be displayed in all active themes. + db_update('block') + ->fields(array( + // Use a region that is valid for all themes. + 'region' => 'content', + 'status' => 1, + )) + ->condition('module', 'system') + ->condition('delta', 'navigation') + ->execute(); + + // This test puts menu links in the Management menu and then tests for + // their presence on the page, so we need to ensure that the Management + // block will be displayed in all active themes. + db_update('block') + ->fields(array( + // Use a region that is valid for all themes. + 'region' => 'content', + 'status' => 1, + )) + ->condition('module', 'system') + ->condition('delta', 'management') + ->execute(); + } /** - * Assert that a given path shows certain breadcrumb links. - * - * @param string $goto - * (optional) A system path to pass to DrupalWebTestCase::drupalGet(). - * @param array $trail - * An associative array whose keys are expected breadcrumb link paths and - * whose values are expected breadcrumb link texts (not sanitized). - * @param string $page_title - * (optional) A page title to additionally assert via - * DrupalWebTestCase::assertTitle(). Without site name suffix. - * @param array $tree - * (optional) An associative array whose keys are link paths and whose - * values are link titles (not sanitized) of an expected active trail in a - * menu tree output on the page. - * @param $last_active - * (optional) Whether the last link in $tree is expected to be active (TRUE) - * or just to be in the active trail (FALSE). + * Tests active trails are properly affected by menu_tree_set_path(). */ - protected function assertBreadcrumb($goto, array $trail, $page_title = NULL, array $tree = array(), $last_active = TRUE) { - if (isset($goto)) { - $this->drupalGet($goto); - } - // Compare paths with actual breadcrumb. - $parts = $this->getParts(); - $pass = TRUE; - foreach ($trail as $path => $title) { - $url = url($path); - $part = array_shift($parts); - $pass = ($pass && $part['href'] === $url && $part['text'] === check_plain($title)); - } - // No parts must be left, or an expected "Home" will always pass. - $pass = ($pass && empty($parts)); + function testMenuTreeSetPath() { + $home = array('<front>' => 'Home'); + $config_tree = array( + 'admin' => t('Administration'), + 'admin/config' => t('Configuration'), + ); + $config = $home + $config_tree; - $this->assertTrue($pass, t('Breadcrumb %parts found on @path.', array( - '%parts' => implode(' » ', $trail), - '@path' => $this->getUrl(), - ))); + // The menu_test_menu_tree_set_path system variable controls whether or not + // the menu_test_menu_trail_callback() callback (used by all paths in these + // tests) issues an overriding call to menu_trail_set_path(). + $test_menu_path = array( + 'menu_name' => 'management', + 'path' => 'admin/config/system/site-information', + ); - // Additionally assert page title, if given. - if (isset($page_title)) { - $this->assertTitle(strtr('@title | Drupal', array('@title' => $page_title))); - } + $breadcrumb = $home + array( + 'menu-test' => t('Menu test root'), + ); + $tree = array( + 'menu-test' => t('Menu test root'), + 'menu-test/menu-trail' => t('Menu trail - Case 1'), + ); - // Additionally assert active trail in a menu tree output, if given. - if ($tree) { - end($tree); - $active_link_path = key($tree); - $active_link_title = array_pop($tree); - $xpath = ''; - if ($tree) { - $i = 0; - foreach ($tree as $link_path => $link_title) { - $part_xpath = (!$i ? '//' : '/following-sibling::ul/descendant::'); - $part_xpath .= 'li[contains(@class, :class)]/a[contains(@href, :href) and contains(text(), :title)]'; - $part_args = array( - ':class' => 'active-trail', - ':href' => url($link_path), - ':title' => $link_title, - ); - $xpath .= $this->buildXPathQuery($part_xpath, $part_args); - $i++; - } - $elements = $this->xpath($xpath); - $this->assertTrue(!empty($elements), t('Active trail to current page was found in menu tree.')); + // Test the tree generation for the Navigation menu. + variable_del('menu_test_menu_tree_set_path'); + $this->assertBreadcrumb('menu-test/menu-trail', $breadcrumb, t('Menu trail - Case 1'), $tree); - // Append prefix for active link asserted below. - $xpath .= '/following-sibling::ul/descendant::'; - } - else { - $xpath .= '//'; - } - $xpath_last_active = ($last_active ? 'and contains(@class, :class-active)' : ''); - $xpath .= 'li[contains(@class, :class-trail)]/a[contains(@href, :href) ' . $xpath_last_active . 'and contains(text(), :title)]'; - $args = array( - ':class-trail' => 'active-trail', - ':class-active' => 'active', - ':href' => url($active_link_path), - ':title' => $active_link_title, - ); - $elements = $this->xpath($xpath, $args); - $this->assertTrue(!empty($elements), t('Active link %title was found in menu tree, including active trail links %tree.', array( - '%title' => $active_link_title, - '%tree' => implode(' » ', $tree), - ))); - } + // Override the active trail for the Management tree; it should not affect + // the Navigation tree. + variable_set('menu_test_menu_tree_set_path', $test_menu_path); + $this->assertBreadcrumb('menu-test/menu-trail', $breadcrumb, t('Menu trail - Case 1'), $tree); + + $breadcrumb = $config + array( + 'admin/config/development' => t('Development'), + ); + $tree = $config_tree + array( + 'admin/config/development' => t('Development'), + 'admin/config/development/menu-trail' => t('Menu trail - Case 2'), + ); + + $override_breadcrumb = $config + array( + 'admin/config/system' => t('System'), + 'admin/config/system/site-information' => t('Site information'), + ); + $override_tree = $config_tree + array( + 'admin/config/system' => t('System'), + 'admin/config/system/site-information' => t('Site information'), + ); + + // Test the tree generation for the Management menu. + variable_del('menu_test_menu_tree_set_path'); + $this->assertBreadcrumb('admin/config/development/menu-trail', $breadcrumb, t('Menu trail - Case 2'), $tree); + + // Override the active trail for the Management tree; it should affect the + // breadcrumbs and Management tree. + variable_set('menu_test_menu_tree_set_path', $test_menu_path); + $this->assertBreadcrumb('admin/config/development/menu-trail', $override_breadcrumb, t('Menu trail - Case 2'), $override_tree); } /** - * Returns the breadcrumb contents of the current page in the internal browser. + * Tests that the active trail works correctly on custom 403 and 404 pages. */ - protected function getParts() { - $parts = array(); - $elements = $this->xpath('//div[@class="breadcrumb"]/a'); - if (!empty($elements)) { - foreach ($elements as $element) { - $parts[] = array( - 'text' => (string) $element, - 'href' => (string) $element['href'], - 'title' => (string) $element['title'], - ); + function testCustom403And404Pages() { + // Set the custom 403 and 404 pages we will use. + variable_set('site_403', 'menu-test/custom-403-page'); + variable_set('site_404', 'menu-test/custom-404-page'); + + // Define the paths we'll visit to trigger 403 and 404 responses during + // this test, and the expected active trail for each case. + $paths = array( + 403 => 'admin/config', + 404 => $this->randomName(), + ); + // For the 403 page, the initial trail during the Drupal bootstrap should + // include the page that the user is trying to visit, while the final trail + // should reflect the custom 403 page that the user was redirected to. + $expected_trail[403]['initial'] = array( + '<front>' => 'Home', + 'admin/config' => 'Configuration', + ); + $expected_trail[403]['final'] = array( + '<front>' => 'Home', + 'menu-test' => 'Menu test root', + 'menu-test/custom-403-page' => 'Custom 403 page', + ); + // For the 404 page, the initial trail during the Drupal bootstrap should + // only contain the link back to "Home" (since the page the user is trying + // to visit doesn't have any menu items associated with it), while the + // final trail should reflect the custom 404 page that the user was + // redirected to. + $expected_trail[404]['initial'] = array( + '<front>' => 'Home', + ); + $expected_trail[404]['final'] = array( + '<front>' => 'Home', + 'menu-test' => 'Menu test root', + 'menu-test/custom-404-page' => 'Custom 404 page', + ); + + // Visit each path as an anonymous user so that we will actually get a 403 + // on admin/config. + $this->drupalLogout(); + foreach (array(403, 404) as $status_code) { + // Before visiting the page, trigger the code in the menu_test module + // that will record the active trail (so we can check it in this test). + variable_set('menu_test_record_active_trail', TRUE); + $this->drupalGet($paths[$status_code]); + $this->assertResponse($status_code); + + // Check that the initial trail (during the Drupal bootstrap) matches + // what we expect. + $initial_trail = variable_get('menu_test_active_trail_initial', array()); + $this->assertEqual(count($initial_trail), count($expected_trail[$status_code]['initial']), format_string('The initial active trail for a @status_code page contains the expected number of items (expected: @expected, found: @found).', array( + '@status_code' => $status_code, + '@expected' => count($expected_trail[$status_code]['initial']), + '@found' => count($initial_trail), + ))); + foreach (array_keys($expected_trail[$status_code]['initial']) as $index => $path) { + $this->assertEqual($initial_trail[$index]['href'], $path, format_string('Element number @number of the initial active trail for a @status_code page contains the correct path (expected: @expected, found: @found)', array( + '@number' => $index + 1, + '@status_code' => $status_code, + '@expected' => $path, + '@found' => $initial_trail[$index]['href'], + ))); } + + // Check that the final trail (after the user has been redirected to the + // custom 403/404 page) matches what we expect. + $final_trail = variable_get('menu_test_active_trail_final', array()); + $this->assertEqual(count($final_trail), count($expected_trail[$status_code]['final']), format_string('The final active trail for a @status_code page contains the expected number of items (expected: @expected, found: @found).', array( + '@status_code' => $status_code, + '@expected' => count($expected_trail[$status_code]['final']), + '@found' => count($final_trail), + ))); + foreach (array_keys($expected_trail[$status_code]['final']) as $index => $path) { + $this->assertEqual($final_trail[$index]['href'], $path, format_string('Element number @number of the final active trail for a @status_code page contains the correct path (expected: @expected, found: @found)', array( + '@number' => $index + 1, + '@status_code' => $status_code, + '@expected' => $path, + '@found' => $final_trail[$index]['href'], + ))); + } + + // Check that the breadcrumb displayed on the final custom 403/404 page + // matches what we expect. (The last item of the active trail represents + // the current page, which is not supposed to appear in the breadcrumb, + // so we need to remove it from the array before checking.) + array_pop($expected_trail[$status_code]['final']); + $this->assertBreadcrumb(NULL, $expected_trail[$status_code]['final']); } - return $parts; } } diff -Naur drupal-7.0/modules/simpletest/tests/menu_test.info drupal-7.66/modules/simpletest/tests/menu_test.info --- drupal-7.0/modules/simpletest/tests/menu_test.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/menu_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: menu_test.info,v 1.2 2010/12/20 19:59:43 webchick Exp $ name = "Hook menu tests" description = "Support module for menu hook testing." package = Testing @@ -6,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/simpletest/tests/menu_test.module drupal-7.66/modules/simpletest/tests/menu_test.module --- drupal-7.0/modules/simpletest/tests/menu_test.module 2010-12-02 18:34:24.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/menu_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: menu_test.module,v 1.21 2010/12/02 17:34:24 webchick Exp $ /** * @file @@ -16,6 +15,13 @@ 'page callback' => 'node_save', 'menu_name' => menu_test_menu_name(), ); + // This item is of type MENU_CALLBACK with no parents to test title. + $items['menu_callback_title'] = array( + 'title' => 'Menu Callback Title', + 'page callback' => 'menu_test_callback', + 'type' => MENU_CALLBACK, + 'access arguments' => array('access content'), + ); // Use FALSE as 'title callback' to bypass t(). $items['menu_no_title_callback'] = array( 'title' => 'A title with @placeholder', @@ -211,6 +217,30 @@ 'type' => MENU_LOCAL_TASK, ) + $base; + // Menu trail tests. + // @see MenuTrailTestCase + $items['menu-test/menu-trail'] = array( + 'title' => 'Menu trail - Case 1', + 'page callback' => 'menu_test_menu_trail_callback', + 'access arguments' => array('access content'), + ); + $items['admin/config/development/menu-trail'] = array( + 'title' => 'Menu trail - Case 2', + 'description' => 'Tests menu_tree_set_path()', + 'page callback' => 'menu_test_menu_trail_callback', + 'access arguments' => array('access administration pages'), + ); + $items['menu-test/custom-403-page'] = array( + 'title' => 'Custom 403 page', + 'page callback' => 'menu_test_custom_403_404_callback', + 'access arguments' => array('access content'), + ); + $items['menu-test/custom-404-page'] = array( + 'title' => 'Custom 404 page', + 'page callback' => 'menu_test_custom_403_404_callback', + 'access arguments' => array('access content'), + ); + // File inheritance tests. This menu item should inherit the page callback // system_admin_menu_block_page() and therefore render its children as links // on the page. @@ -339,6 +369,43 @@ } /** + * Callback that test menu_test_menu_tree_set_path(). + */ +function menu_test_menu_trail_callback() { + $menu_path = variable_get('menu_test_menu_tree_set_path', array()); + if (!empty($menu_path)) { + menu_tree_set_path($menu_path['menu_name'], $menu_path['path']); + } + return 'This is menu_test_menu_trail_callback().'; +} + +/** + * Implements hook_init(). + */ +function menu_test_init() { + // When requested by one of the MenuTrailTestCase tests, record the initial + // active trail during Drupal's bootstrap (before the user is redirected to a + // custom 403 or 404 page). See menu_test_custom_403_404_callback(). + if (variable_get('menu_test_record_active_trail', FALSE)) { + variable_set('menu_test_active_trail_initial', menu_get_active_trail()); + } +} + +/** + * Callback for our custom 403 and 404 pages. + */ +function menu_test_custom_403_404_callback() { + // When requested by one of the MenuTrailTestCase tests, record the final + // active trail now that the user has been redirected to the custom 403 or + // 404 page. See menu_test_init(). + if (variable_get('menu_test_record_active_trail', FALSE)) { + variable_set('menu_test_active_trail_final', menu_get_active_trail()); + } + + return 'This is menu_test_custom_403_404_callback().'; +} + +/** * Page callback to use when testing the theme callback functionality. * * @param $inherited diff -Naur drupal-7.0/modules/simpletest/tests/module.test drupal-7.66/modules/simpletest/tests/module.test --- drupal-7.0/modules/simpletest/tests/module.test 2010-11-27 21:41:38.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/module.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: module.test,v 1.26 2010/11/27 20:41:38 dries Exp $ /** * @file @@ -26,7 +25,7 @@ $profile_info = install_profile_info('standard', 'en'); $module_list = $profile_info['dependencies']; - // Install profile is a module that is expected to be loaded. + // Installation profile is a module that is expected to be loaded. $module_list[] = 'standard'; sort($module_list); @@ -77,9 +76,9 @@ */ protected function assertModuleList(Array $expected_values, $condition) { $expected_values = array_combine($expected_values, $expected_values); - $this->assertEqual($expected_values, module_list(), t('@condition: module_list() returns correct results', array('@condition' => $condition))); + $this->assertEqual($expected_values, module_list(), format_string('@condition: module_list() returns correct results', array('@condition' => $condition))); ksort($expected_values); - $this->assertIdentical($expected_values, module_list(FALSE, FALSE, TRUE), t('@condition: module_list() returns correctly sorted results', array('@condition' => $condition))); + $this->assertIdentical($expected_values, module_list(FALSE, FALSE, TRUE), format_string('@condition: module_list() returns correctly sorted results', array('@condition' => $condition))); } /** @@ -88,16 +87,16 @@ function testModuleImplements() { // Clear the cache. cache_clear_all('module_implements', 'cache_bootstrap'); - $this->assertFalse(cache_get('module_implements', 'cache_bootstrap'), t('The module implements cache is empty.')); + $this->assertFalse(cache_get('module_implements', 'cache_bootstrap'), 'The module implements cache is empty.'); $this->drupalGet(''); - $this->assertTrue(cache_get('module_implements', 'cache_bootstrap'), t('The module implements cache is populated after requesting a page.')); + $this->assertTrue(cache_get('module_implements', 'cache_bootstrap'), 'The module implements cache is populated after requesting a page.'); // Test again with an authenticated user. $this->user = $this->drupalCreateUser(); $this->drupalLogin($this->user); cache_clear_all('module_implements', 'cache_bootstrap'); $this->drupalGet(''); - $this->assertTrue(cache_get('module_implements', 'cache_bootstrap'), t('The module implements cache is populated after requesting a page.')); + $this->assertTrue(cache_get('module_implements', 'cache_bootstrap'), 'The module implements cache is populated after requesting a page.'); // Make sure group include files are detected properly even when the file is // already loaded when the cache is rebuilt. @@ -118,7 +117,7 @@ module_enable(array('module_test'), FALSE); $this->resetAll(); $this->drupalGet('module-test/hook-dynamic-loading-invoke'); - $this->assertText('success!', t('module_invoke() dynamically loads a hook defined in hook_hook_info().')); + $this->assertText('success!', 'module_invoke() dynamically loads a hook defined in hook_hook_info().'); } /** @@ -128,7 +127,7 @@ module_enable(array('module_test'), FALSE); $this->resetAll(); $this->drupalGet('module-test/hook-dynamic-loading-invoke-all'); - $this->assertText('success!', t('module_invoke_all() dynamically loads a hook defined in hook_hook_info().')); + $this->assertText('success!', 'module_invoke_all() dynamically loads a hook defined in hook_hook_info().'); } /** @@ -139,79 +138,100 @@ // are not already enabled. (If they were, the tests below would not work // correctly.) module_enable(array('module_test'), FALSE); - $this->assertTrue(module_exists('module_test'), t('Test module is enabled.')); - $this->assertFalse(module_exists('forum'), t('Forum module is disabled.')); - $this->assertFalse(module_exists('poll'), t('Poll module is disabled.')); - $this->assertFalse(module_exists('php'), t('PHP module is disabled.')); + $this->assertTrue(module_exists('module_test'), 'Test module is enabled.'); + $this->assertFalse(module_exists('forum'), 'Forum module is disabled.'); + $this->assertFalse(module_exists('poll'), 'Poll module is disabled.'); + $this->assertFalse(module_exists('php'), 'PHP module is disabled.'); // First, create a fake missing dependency. Forum depends on poll, which // depends on a made-up module, foo. Nothing should be installed. variable_set('dependency_test', 'missing dependency'); drupal_static_reset('system_rebuild_module_data'); $result = module_enable(array('forum')); - $this->assertFalse($result, t('module_enable() returns FALSE if dependencies are missing.')); - $this->assertFalse(module_exists('forum'), t('module_enable() aborts if dependencies are missing.')); + $this->assertFalse($result, 'module_enable() returns FALSE if dependencies are missing.'); + $this->assertFalse(module_exists('forum'), 'module_enable() aborts if dependencies are missing.'); // Now, fix the missing dependency. Forum module depends on poll, but poll // depends on the PHP module. module_enable() should work. variable_set('dependency_test', 'dependency'); drupal_static_reset('system_rebuild_module_data'); $result = module_enable(array('forum')); - $this->assertTrue($result, t('module_enable() returns the correct value.')); + $this->assertTrue($result, 'module_enable() returns the correct value.'); // Verify that the fake dependency chain was installed. - $this->assertTrue(module_exists('poll') && module_exists('php'), t('Dependency chain was installed by module_enable().')); + $this->assertTrue(module_exists('poll') && module_exists('php'), 'Dependency chain was installed by module_enable().'); // Verify that the original module was installed. - $this->assertTrue(module_exists('forum'), t('Module installation with unlisted dependencies succeeded.')); + $this->assertTrue(module_exists('forum'), 'Module installation with unlisted dependencies succeeded.'); // Finally, verify that the modules were enabled in the correct order. - $this->assertEqual(variable_get('test_module_enable_order', array()), array('php', 'poll', 'forum'), t('Modules were enabled in the correct order by module_enable().')); + $this->assertEqual(variable_get('test_module_enable_order', array()), array('php', 'poll', 'forum'), 'Modules were enabled in the correct order by module_enable().'); // Now, disable the PHP module. Both forum and poll should be disabled as // well, in the correct order. module_disable(array('php')); - $this->assertTrue(!module_exists('forum') && !module_exists('poll'), t('Depedency chain was disabled by module_disable().')); - $this->assertFalse(module_exists('php'), t('Disabling a module with unlisted dependents succeeded.')); - $this->assertEqual(variable_get('test_module_disable_order', array()), array('forum', 'poll', 'php'), t('Modules were disabled in the correct order by module_disable().')); - - // Disable a module that is listed as a dependency by the install profile. - // Make sure that the profile itself is not on the list of dependent - // modules to be disabled. + $this->assertTrue(!module_exists('forum') && !module_exists('poll'), 'Depedency chain was disabled by module_disable().'); + $this->assertFalse(module_exists('php'), 'Disabling a module with unlisted dependents succeeded.'); + $this->assertEqual(variable_get('test_module_disable_order', array()), array('forum', 'poll', 'php'), 'Modules were disabled in the correct order by module_disable().'); + + // Disable a module that is listed as a dependency by the installation + // profile. Make sure that the profile itself is not on the list of + // dependent modules to be disabled. $profile = drupal_get_profile(); $info = install_profile_info($profile); - $this->assertTrue(in_array('comment', $info['dependencies']), t('Comment module is listed as a dependency of the install profile.')); - $this->assertTrue(module_exists('comment'), t('Comment module is enabled.')); + $this->assertTrue(in_array('comment', $info['dependencies']), 'Comment module is listed as a dependency of the installation profile.'); + $this->assertTrue(module_exists('comment'), 'Comment module is enabled.'); module_disable(array('comment')); - $this->assertFalse(module_exists('comment'), t('Comment module was disabled.')); + $this->assertFalse(module_exists('comment'), 'Comment module was disabled.'); $disabled_modules = variable_get('test_module_disable_order', array()); - $this->assertTrue(in_array('comment', $disabled_modules), t('Comment module is in the list of disabled modules.')); - $this->assertFalse(in_array($profile, $disabled_modules), t('The installation profile is not in the list of disabled modules.')); + $this->assertTrue(in_array('comment', $disabled_modules), 'Comment module is in the list of disabled modules.'); + $this->assertFalse(in_array($profile, $disabled_modules), 'The installation profile is not in the list of disabled modules.'); // Try to uninstall the PHP module by itself. This should be rejected, // since the modules which it depends on need to be uninstalled first, and // that is too destructive to perform automatically. $result = drupal_uninstall_modules(array('php')); - $this->assertFalse($result, t('Calling drupal_uninstall_modules() on a module whose dependents are not uninstalled fails.')); + $this->assertFalse($result, 'Calling drupal_uninstall_modules() on a module whose dependents are not uninstalled fails.'); foreach (array('forum', 'poll', 'php') as $module) { - $this->assertNotEqual(drupal_get_installed_schema_version($module), SCHEMA_UNINSTALLED, t('The @module module was not uninstalled.', array('@module' => $module))); + $this->assertNotEqual(drupal_get_installed_schema_version($module), SCHEMA_UNINSTALLED, format_string('The @module module was not uninstalled.', array('@module' => $module))); } // Now uninstall all three modules explicitly, but in the incorrect order, // and make sure that drupal_uninstal_modules() uninstalled them in the // correct sequence. $result = drupal_uninstall_modules(array('poll', 'php', 'forum')); - $this->assertTrue($result, t('drupal_uninstall_modules() returns the correct value.')); + $this->assertTrue($result, 'drupal_uninstall_modules() returns the correct value.'); foreach (array('forum', 'poll', 'php') as $module) { - $this->assertEqual(drupal_get_installed_schema_version($module), SCHEMA_UNINSTALLED, t('The @module module was uninstalled.', array('@module' => $module))); + $this->assertEqual(drupal_get_installed_schema_version($module), SCHEMA_UNINSTALLED, format_string('The @module module was uninstalled.', array('@module' => $module))); } - $this->assertEqual(variable_get('test_module_uninstall_order', array()), array('forum', 'poll', 'php'), t('Modules were uninstalled in the correct order by drupal_uninstall_modules().')); + $this->assertEqual(variable_get('test_module_uninstall_order', array()), array('forum', 'poll', 'php'), 'Modules were uninstalled in the correct order by drupal_uninstall_modules().'); // Uninstall the profile module from above, and make sure that the profile // itself is not on the list of dependent modules to be uninstalled. $result = drupal_uninstall_modules(array('comment')); - $this->assertTrue($result, t('drupal_uninstall_modules() returns the correct value.')); - $this->assertEqual(drupal_get_installed_schema_version('comment'), SCHEMA_UNINSTALLED, t('Comment module was uninstalled.')); + $this->assertTrue($result, 'drupal_uninstall_modules() returns the correct value.'); + $this->assertEqual(drupal_get_installed_schema_version('comment'), SCHEMA_UNINSTALLED, 'Comment module was uninstalled.'); $uninstalled_modules = variable_get('test_module_uninstall_order', array()); - $this->assertTrue(in_array('comment', $uninstalled_modules), t('Comment module is in the list of uninstalled modules.')); - $this->assertFalse(in_array($profile, $uninstalled_modules), t('The installation profile is not in the list of uninstalled modules.')); + $this->assertTrue(in_array('comment', $uninstalled_modules), 'Comment module is in the list of uninstalled modules.'); + $this->assertFalse(in_array($profile, $uninstalled_modules), 'The installation profile is not in the list of uninstalled modules.'); + + // Enable forum module again, which should enable both the poll module and + // php module. But, this time do it with poll module declaring a dependency + // on a specific version of php module in its info file. Make sure that + // module_enable() still works. + variable_set('dependency_test', 'version dependency'); + drupal_static_reset('system_rebuild_module_data'); + $result = module_enable(array('forum')); + $this->assertTrue($result, 'module_enable() returns the correct value.'); + // Verify that the fake dependency chain was installed. + $this->assertTrue(module_exists('poll') && module_exists('php'), 'Dependency chain was installed by module_enable().'); + // Verify that the original module was installed. + $this->assertTrue(module_exists('forum'), 'Module installation with version dependencies succeeded.'); + // Finally, verify that the modules were enabled in the correct order. + $enable_order = variable_get('test_module_enable_order', array()); + $php_position = array_search('php', $enable_order); + $poll_position = array_search('poll', $enable_order); + $forum_position = array_search('forum', $enable_order); + $php_before_poll = $php_position !== FALSE && $poll_position !== FALSE && $php_position < $poll_position; + $poll_before_forum = $poll_position !== FALSE && $forum_position !== FALSE && $poll_position < $forum_position; + $this->assertTrue($php_before_poll && $poll_before_forum, 'Modules were enabled in the correct order by module_enable().'); } } @@ -247,8 +267,8 @@ // Check for data that was inserted using drupal_write_record() while the // 'module_test' module was being installed and enabled. $data = db_query("SELECT data FROM {module_test}")->fetchCol(); - $this->assertTrue(in_array('Data inserted in hook_install()', $data), t('Data inserted using drupal_write_record() in hook_install() is correctly saved.')); - $this->assertTrue(in_array('Data inserted in hook_enable()', $data), t('Data inserted using drupal_write_record() in hook_enable() is correctly saved.')); + $this->assertTrue(in_array('Data inserted in hook_install()', $data), 'Data inserted using drupal_write_record() in hook_install() is correctly saved.'); + $this->assertTrue(in_array('Data inserted in hook_enable()', $data), 'Data inserted using drupal_write_record() in hook_enable() is correctly saved.'); } } @@ -279,6 +299,48 @@ // Are the perms defined by module_test removed from {role_permission}. $count = db_query("SELECT COUNT(rid) FROM {role_permission} WHERE permission = :perm", array(':perm' => 'module_test perm'))->fetchField(); - $this->assertEqual(0, $count, t('Permissions were all removed.')); + $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.0/modules/simpletest/tests/module_test.file.inc drupal-7.66/modules/simpletest/tests/module_test.file.inc --- drupal-7.0/modules/simpletest/tests/module_test.file.inc 2010-11-27 21:41:38.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/module_test.file.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: module_test.file.inc,v 1.3 2010/11/27 20:41:38 dries Exp $ /** * @file diff -Naur drupal-7.0/modules/simpletest/tests/module_test.implementations.inc drupal-7.66/modules/simpletest/tests/module_test.implementations.inc --- drupal-7.0/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.0/modules/simpletest/tests/module_test.info drupal-7.66/modules/simpletest/tests/module_test.info --- drupal-7.0/modules/simpletest/tests/module_test.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/module_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: module_test.info,v 1.3 2010/12/20 19:59:43 webchick Exp $ name = "Module test" description = "Support module for module system testing." package = Testing @@ -6,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/simpletest/tests/module_test.install drupal-7.66/modules/simpletest/tests/module_test.install --- drupal-7.0/modules/simpletest/tests/module_test.install 2010-02-26 19:31:29.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/module_test.install 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: module_test.install,v 1.1 2010/02/26 18:31:29 dries Exp $ /** * @file diff -Naur drupal-7.0/modules/simpletest/tests/module_test.module drupal-7.66/modules/simpletest/tests/module_test.module --- drupal-7.0/modules/simpletest/tests/module_test.module 2010-11-27 21:41:38.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/module_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: module_test.module,v 1.10 2010/11/27 20:41:38 dries Exp $ /** * Implements hook_permission(). @@ -36,6 +35,20 @@ $info['dependencies'][] = 'php'; } } + elseif (variable_get('dependency_test', FALSE) == 'version dependency') { + if ($file->name == 'forum') { + // Make the forum module depend on poll. + $info['dependencies'][] = 'poll'; + } + elseif ($file->name == 'poll') { + // Make poll depend on a specific version of php module. + $info['dependencies'][] = 'php (1.x)'; + } + elseif ($file->name == 'php') { + // Set php module to a version compatible with the above. + $info['version'] = '7.x-1.0'; + } + } if ($file->name == 'seven' && $type == 'theme') { $info['regions']['test_region'] = t('Test region'); } @@ -116,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.0/modules/simpletest/tests/pager.test drupal-7.66/modules/simpletest/tests/pager.test --- drupal-7.0/modules/simpletest/tests/pager.test 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/pager.test 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,159 @@ +<?php + +/** + * @file + * Tests for pager functionality. + */ + +/** + * Tests pager functionality. + */ +class PagerFunctionalWebTestCase extends DrupalWebTestCase { + protected $profile = 'testing'; + + public static function getInfo() { + return array( + 'name' => 'Pager functionality', + 'description' => 'Tests pager functionality.', + 'group' => 'Pager', + ); + } + + function setUp() { + parent::setUp(array('dblog')); + + // Insert 300 log messages. + for ($i = 0; $i < 300; $i++) { + watchdog('pager_test', $this->randomString(), NULL, WATCHDOG_DEBUG); + } + + $this->admin_user = $this->drupalCreateUser(array( + 'access site reports', + )); + $this->drupalLogin($this->admin_user); + } + + /** + * Tests markup and CSS classes of pager links. + */ + function testActiveClass() { + // Verify first page. + $this->drupalGet('admin/reports/dblog'); + $current_page = 0; + $this->assertPagerItems($current_page); + + // Verify any page but first/last. + $current_page++; + $this->drupalGet('admin/reports/dblog', array('query' => array('page' => $current_page))); + $this->assertPagerItems($current_page); + + // Verify last page. + $elements = $this->xpath('//li[contains(@class, :class)]/a', array(':class' => 'pager-last')); + preg_match('@page=(\d+)@', $elements[0]['href'], $matches); + $current_page = (int) $matches[1]; + $this->drupalGet($GLOBALS['base_root'] . $elements[0]['href'], array('external' => TRUE)); + $this->assertPagerItems($current_page); + } + + /** + * Asserts pager items and links. + * + * @param int $current_page + * The current pager page the internal browser is on. + */ + protected function assertPagerItems($current_page) { + $elements = $this->xpath('//ul[@class=:class]/li', array(':class' => 'pager')); + $this->assertTrue(!empty($elements), 'Pager found.'); + + // Make current page 1-based. + $current_page++; + + // Extract first/previous and next/last items. + // first/previous only exist, if the current page is not the first. + if ($current_page > 1) { + $first = array_shift($elements); + $previous = array_shift($elements); + } + // next/last always exist, unless the current page is the last. + if ($current_page != count($elements)) { + $last = array_pop($elements); + $next = array_pop($elements); + } + + // Verify items and links to pages. + foreach ($elements as $page => $element) { + // Make item/page index 1-based. + $page++; + if ($current_page == $page) { + $this->assertClass($element, 'pager-current', 'Item for current page has .pager-current class.'); + $this->assertFalse(isset($element->a), 'Item for current page has no link.'); + } + else { + $this->assertNoClass($element, 'pager-current', "Item for page $page has no .pager-current class."); + $this->assertClass($element, 'pager-item', "Item for page $page has .pager-item class."); + $this->assertTrue($element->a, "Link to page $page found."); + $this->assertNoClass($element->a, 'active', "Link to page $page is not active."); + } + unset($elements[--$page]); + } + // Verify that no other items remain untested. + $this->assertTrue(empty($elements), 'All expected items found.'); + + // Verify first/previous and next/last items and links. + if (isset($first)) { + $this->assertClass($first, 'pager-first', 'Item for first page has .pager-first class.'); + $this->assertTrue($first->a, 'Link to first page found.'); + $this->assertNoClass($first->a, 'active', 'Link to first page is not active.'); + } + if (isset($previous)) { + $this->assertClass($previous, 'pager-previous', 'Item for first page has .pager-previous class.'); + $this->assertTrue($previous->a, 'Link to previous page found.'); + $this->assertNoClass($previous->a, 'active', 'Link to previous page is not active.'); + } + if (isset($next)) { + $this->assertClass($next, 'pager-next', 'Item for next page has .pager-next class.'); + $this->assertTrue($next->a, 'Link to next page found.'); + $this->assertNoClass($next->a, 'active', 'Link to next page is not active.'); + } + if (isset($last)) { + $this->assertClass($last, 'pager-last', 'Item for last page has .pager-last class.'); + $this->assertTrue($last->a, 'Link to last page found.'); + $this->assertNoClass($last->a, 'active', 'Link to last page is not active.'); + } + } + + /** + * Asserts that an element has a given class. + * + * @param SimpleXMLElement $element + * The element to test. + * @param string $class + * The class to assert. + * @param string $message + * (optional) A verbose message to output. + */ + protected function assertClass(SimpleXMLElement $element, $class, $message = NULL) { + if (!isset($message)) { + $message = "Class .$class found."; + } + $this->assertTrue(strpos($element['class'], $class) !== FALSE, $message); + } + + /** + * Asserts that an element does not have a given class. + * + * @param SimpleXMLElement $element + * The element to test. + * @param string $class + * The class to assert. + * @param string $message + * (optional) A verbose message to output. + */ + protected function assertNoClass(SimpleXMLElement $element, $class, $message = NULL) { + if (!isset($message)) { + $message = "Class .$class not found."; + } + $this->assertTrue(strpos($element['class'], $class) === FALSE, $message); + } +} + diff -Naur drupal-7.0/modules/simpletest/tests/password.test drupal-7.66/modules/simpletest/tests/password.test --- drupal-7.0/modules/simpletest/tests/password.test 2010-12-18 01:56:18.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/password.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: password.test,v 1.1 2010/12/18 00:56:18 dries Exp $ /** * @file @@ -36,26 +35,47 @@ $password = 'baz'; $account = (object) array('name' => 'foo', 'pass' => md5($password)); // The md5 password should be flagged as needing an update. - $this->assertTrue(user_needs_new_hash($account), t('User with md5 password needs a new hash.')); + $this->assertTrue(user_needs_new_hash($account), 'User with md5 password needs a new hash.'); // Re-hash the password. $old_hash = $account->pass; $account->pass = user_hash_password($password); - $this->assertIdentical(_password_get_count_log2($account->pass), DRUPAL_MIN_HASH_COUNT, t('Re-hashed password has the minimum number of log2 iterations.')); - $this->assertTrue($account->pass != $old_hash, t('Password hash changed.')); - $this->assertTrue(user_check_password($password, $account), t('Password check succeeds.')); + $this->assertIdentical(_password_get_count_log2($account->pass), DRUPAL_MIN_HASH_COUNT, 'Re-hashed password has the minimum number of log2 iterations.'); + $this->assertTrue($account->pass != $old_hash, 'Password hash changed.'); + $this->assertTrue(user_check_password($password, $account), 'Password check succeeds.'); // Since the log2 setting hasn't changed and the user has a valid password, // user_needs_new_hash() should return FALSE. - $this->assertFalse(user_needs_new_hash($account), t('User does not need a new hash.')); + $this->assertFalse(user_needs_new_hash($account), 'User does not need a new hash.'); // Increment the log2 iteration to MIN + 1. variable_set('password_count_log2', DRUPAL_MIN_HASH_COUNT + 1); - $this->assertTrue(user_needs_new_hash($account), t('User needs a new hash after incrementing the log2 count.')); + $this->assertTrue(user_needs_new_hash($account), 'User needs a new hash after incrementing the log2 count.'); // Re-hash the password. $old_hash = $account->pass; $account->pass = user_hash_password($password); - $this->assertIdentical(_password_get_count_log2($account->pass), DRUPAL_MIN_HASH_COUNT + 1, t('Re-hashed password has the correct number of log2 iterations.')); - $this->assertTrue($account->pass != $old_hash, t('Password hash changed again.')); + $this->assertIdentical(_password_get_count_log2($account->pass), DRUPAL_MIN_HASH_COUNT + 1, 'Re-hashed password has the correct number of log2 iterations.'); + $this->assertTrue($account->pass != $old_hash, 'Password hash changed again.'); // Now the hash should be OK. - $this->assertFalse(user_needs_new_hash($account), t('Re-hashed password does not need a new hash.')); - $this->assertTrue(user_check_password($password, $account), t('Password check succeeds with re-hashed password.')); + $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.0/modules/simpletest/tests/path.test drupal-7.66/modules/simpletest/tests/path.test --- drupal-7.0/modules/simpletest/tests/path.test 2010-11-30 02:05:24.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/path.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: path.test,v 1.8 2010/11/30 01:05:24 dries Exp $ /** * @file @@ -42,7 +41,7 @@ foreach ($tests as $patterns => $cases) { foreach ($cases as $path => $expected_result) { $actual_result = drupal_match_path($path, $patterns); - $this->assertIdentical($actual_result, $expected_result, t('Tried matching the path <code>@path</code> to the pattern <pre>@patterns</pre> - expected @expected, got @actual.', array('@path' => $path, '@patterns' => $patterns, '@expected' => var_export($expected_result, TRUE), '@actual' => var_export($actual_result, TRUE)))); + $this->assertIdentical($actual_result, $expected_result, format_string('Tried matching the path <code>@path</code> to the pattern <pre>@patterns</pre> - expected @expected, got @actual.', array('@path' => $path, '@patterns' => $patterns, '@expected' => var_export($expected_result, TRUE), '@actual' => var_export($actual_result, TRUE)))); } } } @@ -172,7 +171,7 @@ $this->assertUrlInboundAlter('alias/test2', "user/$uid/edit"); $this->assertUrlOutboundAlter("user/$uid/edit", 'alias/test2'); - // Test a non-existant user is not altered. + // Test a non-existent user is not altered. $uid++; $this->assertUrlInboundAlter("user/$uid", "user/$uid"); $this->assertUrlOutboundAlter("user/$uid", "user/$uid"); @@ -197,8 +196,16 @@ */ function testCurrentUrlRequestedPath() { $this->drupalGet('url-alter-test/bar'); - $this->assertRaw('request_path=url-alter-test/bar', t('request_path() returns the requested path.')); - $this->assertRaw('current_path=url-alter-test/foo', t('current_path() returns the internal path.')); + $this->assertRaw('request_path=url-alter-test/bar', 'request_path() returns the requested path.'); + $this->assertRaw('current_path=url-alter-test/foo', 'current_path() returns the internal path.'); + } + + /** + * Tests that $_GET['q'] is initialized when the request path is empty. + */ + function testGetQInitialized() { + $this->drupalGet(''); + $this->assertText("\$_GET['q'] is non-empty with an empty request path.", "\$_GET['q'] is initialized with an empty request path."); } /** @@ -216,7 +223,7 @@ $result = url($original); $base_path = base_path() . (variable_get('clean_url', '0') ? '' : '?q='); $result = substr($result, strlen($base_path)); - $this->assertIdentical($result, $final, t('Altered outbound URL %original, expected %final, and got %result.', array('%original' => $original, '%final' => $final, '%result' => $result))); + $this->assertIdentical($result, $final, format_string('Altered outbound URL %original, expected %final, and got %result.', array('%original' => $original, '%final' => $final, '%result' => $result))); } /** @@ -233,7 +240,7 @@ protected function assertUrlInboundAlter($original, $final) { // Test inbound altering. $result = drupal_get_normal_path($original); - $this->assertIdentical($result, $final, t('Altered inbound URL %original, expected %final, and got %result.', array('%original' => $original, '%final' => $final, '%result' => $result))); + $this->assertIdentical($result, $final, format_string('Altered inbound URL %original, expected %final, and got %result.', array('%original' => $original, '%final' => $final, '%result' => $result))); } } @@ -264,8 +271,8 @@ 'alias' => 'foo', ); path_save($path); - $this->assertEqual(drupal_lookup_path('alias', $path['source']), $path['alias'], t('Basic alias lookup works.')); - $this->assertEqual(drupal_lookup_path('source', $path['alias']), $path['source'], t('Basic source lookup works.')); + $this->assertEqual(drupal_lookup_path('alias', $path['source']), $path['alias'], 'Basic alias lookup works.'); + $this->assertEqual(drupal_lookup_path('source', $path['alias']), $path['source'], 'Basic source lookup works.'); // Create a language specific alias for the default language (English). $path = array( @@ -274,8 +281,8 @@ 'language' => 'en', ); path_save($path); - $this->assertEqual(drupal_lookup_path('alias', $path['source']), $path['alias'], t('English alias overrides language-neutral alias.')); - $this->assertEqual(drupal_lookup_path('source', $path['alias']), $path['source'], t('English source overrides language-neutral source.')); + $this->assertEqual(drupal_lookup_path('alias', $path['source']), $path['alias'], 'English alias overrides language-neutral alias.'); + $this->assertEqual(drupal_lookup_path('source', $path['alias']), $path['source'], 'English source overrides language-neutral source.'); // Create a language-neutral alias for the same path, again. $path = array( @@ -283,7 +290,7 @@ 'alias' => 'bar', ); path_save($path); - $this->assertEqual(drupal_lookup_path('alias', $path['source']), "users/$name", t('English alias still returned after entering a language-neutral alias.')); + $this->assertEqual(drupal_lookup_path('alias', $path['source']), "users/$name", 'English alias still returned after entering a language-neutral alias.'); // Create a language-specific (xx-lolspeak) alias for the same path. $path = array( @@ -292,9 +299,9 @@ 'language' => 'xx-lolspeak', ); path_save($path); - $this->assertEqual(drupal_lookup_path('alias', $path['source']), "users/$name", t('English alias still returned after entering a LOLspeak alias.')); + $this->assertEqual(drupal_lookup_path('alias', $path['source']), "users/$name", 'English alias still returned after entering a LOLspeak alias.'); // The LOLspeak alias should be returned if we really want LOLspeak. - $this->assertEqual(drupal_lookup_path('alias', $path['source'], 'xx-lolspeak'), 'LOL', t('LOLspeak alias returned if we specify xx-lolspeak to drupal_lookup_path().')); + $this->assertEqual(drupal_lookup_path('alias', $path['source'], 'xx-lolspeak'), 'LOL', 'LOLspeak alias returned if we specify xx-lolspeak to drupal_lookup_path().'); // Create a new alias for this path in English, which should override the // previous alias for "user/$uid". @@ -304,8 +311,8 @@ 'language' => 'en', ); path_save($path); - $this->assertEqual(drupal_lookup_path('alias', $path['source']), $path['alias'], t('Recently created English alias returned.')); - $this->assertEqual(drupal_lookup_path('source', $path['alias']), $path['source'], t('Recently created English source returned.')); + $this->assertEqual(drupal_lookup_path('alias', $path['source']), $path['alias'], 'Recently created English alias returned.'); + $this->assertEqual(drupal_lookup_path('source', $path['alias']), $path['source'], 'Recently created English source returned.'); // Remove the English aliases, which should cause a fallback to the most // recently created language-neutral alias, 'bar'. @@ -313,7 +320,7 @@ ->condition('language', 'en') ->execute(); drupal_clear_path_cache(); - $this->assertEqual(drupal_lookup_path('alias', $path['source']), 'bar', t('Path lookup falls back to recently created language-neutral alias.')); + $this->assertEqual(drupal_lookup_path('alias', $path['source']), 'bar', 'Path lookup falls back to recently created language-neutral alias.'); // Test the situation where the alias and language are the same, but // the source differs. The newer alias record should be returned. @@ -323,6 +330,52 @@ 'alias' => 'bar', ); path_save($path); - $this->assertEqual(drupal_lookup_path('source', $path['alias']), $path['source'], t('Newer alias record is returned when comparing two LANGUAGE_NONE paths with the same alias.')); + $this->assertEqual(drupal_lookup_path('source', $path['alias']), $path['source'], 'Newer alias record is returned when comparing two LANGUAGE_NONE paths with the same alias.'); + } +} + +/** + * Tests the path_save() function. + */ +class PathSaveTest extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => t('Path save'), + 'description' => t('Tests that path_save() exposes the previous alias value.'), + 'group' => t('Path API'), + ); + } + + function setUp() { + // Enable a helper module that implements hook_path_update(). + parent::setUp('path_test'); + path_test_reset(); + } + + /** + * Tests that path_save() makes the original path available to modules. + */ + function testDrupalSaveOriginalPath() { + $account = $this->drupalCreateUser(); + $uid = $account->uid; + $name = $account->name; + + // Create a language-neutral alias. + $path = array( + 'source' => "user/$uid", + 'alias' => 'foo', + ); + $path_original = $path; + path_save($path); + + // Alter the path. + $path['alias'] = 'bar'; + path_save($path); + + // Test to see if the original alias is available to modules during + // hook_path_update(). + $results = variable_get('path_test_results', array()); + $this->assertIdentical($results['hook_path_update']['original']['alias'], $path_original['alias'], 'Old path alias available to modules during hook_path_update.'); + $this->assertIdentical($results['hook_path_update']['original']['source'], $path_original['source'], 'Old path alias available to modules during hook_path_update.'); } } diff -Naur drupal-7.0/modules/simpletest/tests/path_test.info drupal-7.66/modules/simpletest/tests/path_test.info --- drupal-7.0/modules/simpletest/tests/path_test.info 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/path_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -0,0 +1,11 @@ +name = "Hook path tests" +description = "Support module for path hook 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.0/modules/simpletest/tests/path_test.module drupal-7.66/modules/simpletest/tests/path_test.module --- drupal-7.0/modules/simpletest/tests/path_test.module 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/path_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,23 @@ +<?php + +/** + * @file + * Helper module for the path tests. + */ + +/** + * Resets the path test results. + */ +function path_test_reset() { + variable_set('path_test_results', array()); +} + +/** + * Implements hook_path_update(). + */ +function path_test_path_update($path) { + $results = variable_get('path_test_results', array()); + $results['hook_path_update'] = $path; + variable_set('path_test_results', $results); +} + diff -Naur drupal-7.0/modules/simpletest/tests/psr_0_test/lib/Drupal/psr_0_test/Tests/ExampleTest.php drupal-7.66/modules/simpletest/tests/psr_0_test/lib/Drupal/psr_0_test/Tests/ExampleTest.php --- drupal-7.0/modules/simpletest/tests/psr_0_test/lib/Drupal/psr_0_test/Tests/ExampleTest.php 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/psr_0_test/lib/Drupal/psr_0_test/Tests/ExampleTest.php 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,18 @@ +<?php + +namespace Drupal\psr_0_test\Tests; + +class ExampleTest extends \DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'PSR0 example test: PSR-0 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.0/modules/simpletest/tests/psr_0_test/lib/Drupal/psr_0_test/Tests/Nested/NestedExampleTest.php drupal-7.66/modules/simpletest/tests/psr_0_test/lib/Drupal/psr_0_test/Tests/Nested/NestedExampleTest.php --- drupal-7.0/modules/simpletest/tests/psr_0_test/lib/Drupal/psr_0_test/Tests/Nested/NestedExampleTest.php 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/psr_0_test/lib/Drupal/psr_0_test/Tests/Nested/NestedExampleTest.php 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,18 @@ +<?php + +namespace Drupal\psr_0_test\Tests\Nested; + +class NestedExampleTest extends \DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'PSR0 example test: PSR-0 in nested subfolders.', + 'description' => 'We want to assert that this PSR-0 test case is being discovered.', + 'group' => 'SimpleTest', + ); + } + + function testArithmetics() { + $this->assert(1 + 1 == 2, '1 + 1 == 2'); + } +} diff -Naur drupal-7.0/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.0/modules/simpletest/tests/psr_0_test/psr_0_test.info 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/psr_0_test/psr_0_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -0,0 +1,11 @@ +name = PSR-0 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.0/modules/simpletest/tests/psr_0_test/psr_0_test.module drupal-7.66/modules/simpletest/tests/psr_0_test/psr_0_test.module --- drupal-7.0/modules/simpletest/tests/psr_0_test/psr_0_test.module 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/psr_0_test/psr_0_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1 @@ +<?php diff -Naur drupal-7.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/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.0/modules/simpletest/tests/registry.test drupal-7.66/modules/simpletest/tests/registry.test --- drupal-7.0/modules/simpletest/tests/registry.test 2010-09-01 22:08:17.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/registry.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: registry.test,v 1.21 2010/09/01 20:08:17 dries Exp $ class RegistryParseFileTestCase extends DrupalWebTestCase { public static function getInfo() { diff -Naur drupal-7.0/modules/simpletest/tests/requirements1_test.info drupal-7.66/modules/simpletest/tests/requirements1_test.info --- drupal-7.0/modules/simpletest/tests/requirements1_test.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/requirements1_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,13 +1,11 @@ -; $Id: requirements1_test.info,v 1.2 2010/12/20 19:59:43 webchick Exp $ name = Requirements 1 Test description = "Tests that a module is not installed when it fails hook_requirements('install')." -package = Core +package = Testing version = VERSION core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/simpletest/tests/requirements1_test.install drupal-7.66/modules/simpletest/tests/requirements1_test.install --- drupal-7.0/modules/simpletest/tests/requirements1_test.install 2010-05-26 09:31:46.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/requirements1_test.install 2019-04-17 22:20:46.000000000 +0200 @@ -1,12 +1,11 @@ <?php -// $Id: requirements1_test.install,v 1.1 2010/05/26 07:31:46 dries Exp $ /** * Implements hook_requirements(). */ function requirements1_test_requirements($phase) { $requirements = array(); - // Ensure translations don't break at install time. + // Ensure translations don't break during installation. $t = get_t(); // Always fails requirements. diff -Naur drupal-7.0/modules/simpletest/tests/requirements1_test.module drupal-7.66/modules/simpletest/tests/requirements1_test.module --- drupal-7.0/modules/simpletest/tests/requirements1_test.module 2010-05-26 09:31:46.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/requirements1_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: requirements1_test.module,v 1.1 2010/05/26 07:31:46 dries Exp $ /** * @file diff -Naur drupal-7.0/modules/simpletest/tests/requirements2_test.info drupal-7.66/modules/simpletest/tests/requirements2_test.info --- drupal-7.0/modules/simpletest/tests/requirements2_test.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/requirements2_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,15 +1,13 @@ -; $Id: requirements2_test.info,v 1.2 2010/12/20 19:59:43 webchick Exp $ name = Requirements 2 Test description = "Tests that a module is not installed when the one it depends on fails hook_requirements('install)." dependencies[] = requirements1_test dependencies[] = comment -package = Core +package = Testing version = VERSION core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/simpletest/tests/requirements2_test.module drupal-7.66/modules/simpletest/tests/requirements2_test.module --- drupal-7.0/modules/simpletest/tests/requirements2_test.module 2010-05-26 09:31:47.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/requirements2_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: requirements2_test.module,v 1.1 2010/05/26 07:31:47 dries Exp $ /** * @file diff -Naur drupal-7.0/modules/simpletest/tests/schema.test drupal-7.66/modules/simpletest/tests/schema.test --- drupal-7.0/modules/simpletest/tests/schema.test 2010-12-08 07:38:59.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/schema.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: schema.test,v 1.24 2010/12/08 06:38:59 webchick Exp $ /** * @file @@ -45,7 +44,7 @@ db_create_table('test_table', $table_specification); // Assert that the table exists. - $this->assertTrue(db_table_exists('test_table'), t('The table exists.')); + $this->assertTrue(db_table_exists('test_table'), 'The table exists.'); // Assert that the table comment has been set. $this->checkSchemaComment($table_specification['description'], 'test_table'); @@ -54,46 +53,46 @@ $this->checkSchemaComment($table_specification['fields']['test_field']['description'], 'test_table', 'test_field'); // An insert without a value for the column 'test_table' should fail. - $this->assertFalse($this->tryInsert(), t('Insert without a default failed.')); + $this->assertFalse($this->tryInsert(), 'Insert without a default failed.'); // Add a default value to the column. db_field_set_default('test_table', 'test_field', 0); // The insert should now succeed. - $this->assertTrue($this->tryInsert(), t('Insert with a default succeeded.')); + $this->assertTrue($this->tryInsert(), 'Insert with a default succeeded.'); // Remove the default. db_field_set_no_default('test_table', 'test_field'); // The insert should fail again. - $this->assertFalse($this->tryInsert(), t('Insert without a default failed.')); + $this->assertFalse($this->tryInsert(), 'Insert without a default failed.'); // Test for fake index and test for the boolean result of indexExists(). $index_exists = Database::getConnection()->schema()->indexExists('test_table', 'test_field'); - $this->assertIdentical($index_exists, FALSE, t('Fake index does not exists')); + $this->assertIdentical($index_exists, FALSE, 'Fake index does not exists'); // Add index. db_add_index('test_table', 'test_field', array('test_field')); // Test for created index and test for the boolean result of indexExists(). $index_exists = Database::getConnection()->schema()->indexExists('test_table', 'test_field'); - $this->assertIdentical($index_exists, TRUE, t('Index created.')); + $this->assertIdentical($index_exists, TRUE, 'Index created.'); // Rename the table. db_rename_table('test_table', 'test_table2'); // Index should be renamed. $index_exists = Database::getConnection()->schema()->indexExists('test_table2', 'test_field'); - $this->assertTrue($index_exists, t('Index was renamed.')); + $this->assertTrue($index_exists, 'Index was renamed.'); // We need the default so that we can insert after the rename. db_field_set_default('test_table2', 'test_field', 0); - $this->assertFalse($this->tryInsert(), t('Insert into the old table failed.')); - $this->assertTrue($this->tryInsert('test_table2'), t('Insert into the new table succeeded.')); + $this->assertFalse($this->tryInsert(), 'Insert into the old table failed.'); + $this->assertTrue($this->tryInsert('test_table2'), 'Insert into the new table succeeded.'); // We should have successfully inserted exactly two rows. $count = db_query('SELECT COUNT(*) FROM {test_table2}')->fetchField(); - $this->assertEqual($count, 2, t('Two fields were successfully inserted.')); + $this->assertEqual($count, 2, 'Two fields were successfully inserted.'); // Try to drop the table. db_drop_table('test_table2'); - $this->assertFalse(db_table_exists('test_table2'), t('The dropped table does not exist.')); + $this->assertFalse(db_table_exists('test_table2'), 'The dropped table does not exist.'); // Recreate the table. db_create_table('test_table', $table_specification); @@ -109,14 +108,14 @@ // Assert that the column comment has been set. $this->checkSchemaComment('Changed column description.', 'test_table', 'test_serial'); - $this->assertTrue($this->tryInsert(), t('Insert with a serial succeeded.')); + $this->assertTrue($this->tryInsert(), 'Insert with a serial succeeded.'); $max1 = db_query('SELECT MAX(test_serial) FROM {test_table}')->fetchField(); - $this->assertTrue($this->tryInsert(), t('Insert with a serial succeeded.')); + $this->assertTrue($this->tryInsert(), 'Insert with a serial succeeded.'); $max2 = db_query('SELECT MAX(test_serial) FROM {test_table}')->fetchField(); - $this->assertTrue($max2 > $max1, t('The serial is monotone.')); + $this->assertTrue($max2 > $max1, 'The serial is monotone.'); $count = db_query('SELECT COUNT(*) FROM {test_table}')->fetchField(); - $this->assertEqual($count, 2, t('There were two rows.')); + $this->assertEqual($count, 2, 'There were two rows.'); // Use database specific data type and ensure that table is created. $table_specification = array( @@ -135,7 +134,7 @@ db_create_table('test_timestamp', $table_specification); } catch (Exception $e) {} - $this->assertTrue(db_table_exists('test_timestamp'), t('Table with database specific datatype was created.')); + $this->assertTrue(db_table_exists('test_timestamp'), 'Table with database specific datatype was created.'); } function tryInsert($table = 'test_table') { @@ -163,7 +162,7 @@ function checkSchemaComment($description, $table, $column = NULL) { if (method_exists(Database::getConnection()->schema(), 'getComment')) { $comment = Database::getConnection()->schema()->getComment($table, $column); - $this->assertEqual($comment, $description, t('The comment matches the schema description.')); + $this->assertEqual($comment, $description, 'The comment matches the schema description.'); } } @@ -194,8 +193,8 @@ // Finally, check each column and try to insert invalid values into them. foreach ($table_spec['fields'] as $column_name => $column_spec) { - $this->assertTrue(db_field_exists($table_name, $column_name), t('Unsigned @type column was created.', array('@type' => $column_spec['type']))); - $this->assertFalse($this->tryUnsignedInsert($table_name, $column_name), t('Unsigned @type column rejected a negative value.', array('@type' => $column_spec['type']))); + $this->assertTrue(db_field_exists($table_name, $column_name), format_string('Unsigned @type column was created.', array('@type' => $column_spec['type']))); + $this->assertFalse($this->tryUnsignedInsert($table_name, $column_name), format_string('Unsigned @type column rejected a negative value.', array('@type' => $column_spec['type']))); } } @@ -313,7 +312,7 @@ 'primary key' => array('serial_column'), ); db_create_table($table_name, $table_spec); - $this->pass(t('Table %table created.', array('%table' => $table_name))); + $this->pass(format_string('Table %table created.', array('%table' => $table_name))); // Check the characteristics of the field. $this->assertFieldCharacteristics($table_name, 'test_field', $field_spec); @@ -330,7 +329,7 @@ 'primary key' => array('serial_column'), ); db_create_table($table_name, $table_spec); - $this->pass(t('Table %table created.', array('%table' => $table_name))); + $this->pass(format_string('Table %table created.', array('%table' => $table_name))); // Insert some rows to the table to test the handling of initial values. for ($i = 0; $i < 3; $i++) { @@ -340,7 +339,7 @@ } db_add_field($table_name, 'test_field', $field_spec); - $this->pass(t('Column %column created.', array('%column' => 'test_field'))); + $this->pass(format_string('Column %column created.', array('%column' => 'test_field'))); // Check the characteristics of the field. $this->assertFieldCharacteristics($table_name, 'test_field', $field_spec); @@ -363,7 +362,7 @@ ->countQuery() ->execute() ->fetchField(); - $this->assertEqual($count, 0, t('Initial values filled out.')); + $this->assertEqual($count, 0, 'Initial values filled out.'); } // Check that the default value has been registered. @@ -377,7 +376,7 @@ ->condition('serial_column', $id) ->execute() ->fetchField(); - $this->assertEqual($field_value, $field_spec['default'], t('Default value registered.')); + $this->assertEqual($field_value, $field_spec['default'], 'Default value registered.'); } db_drop_field($table_name, $field_name); diff -Naur drupal-7.0/modules/simpletest/tests/session.test drupal-7.66/modules/simpletest/tests/session.test --- drupal-7.0/modules/simpletest/tests/session.test 2010-11-13 18:40:09.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/session.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: session.test,v 1.35 2010/11/13 17:40:09 webchick Exp $ /** * @file @@ -23,11 +22,11 @@ * Tests for drupal_save_session() and drupal_session_regenerate(). */ function testSessionSaveRegenerate() { - $this->assertFalse(drupal_save_session(), t('drupal_save_session() correctly returns FALSE (inside of testing framework) when initially called with no arguments.'), t('Session')); - $this->assertFalse(drupal_save_session(FALSE), t('drupal_save_session() correctly returns FALSE when called with FALSE.'), t('Session')); - $this->assertFalse(drupal_save_session(), t('drupal_save_session() correctly returns FALSE when saving has been disabled.'), t('Session')); - $this->assertTrue(drupal_save_session(TRUE), t('drupal_save_session() correctly returns TRUE when called with TRUE.'), t('Session')); - $this->assertTrue(drupal_save_session(), t('drupal_save_session() correctly returns TRUE when saving has been enabled.'), t('Session')); + $this->assertFalse(drupal_save_session(), 'drupal_save_session() correctly returns FALSE (inside of testing framework) when initially called with no arguments.', 'Session'); + $this->assertFalse(drupal_save_session(FALSE), 'drupal_save_session() correctly returns FALSE when called with FALSE.', 'Session'); + $this->assertFalse(drupal_save_session(), 'drupal_save_session() correctly returns FALSE when saving has been disabled.', 'Session'); + $this->assertTrue(drupal_save_session(TRUE), 'drupal_save_session() correctly returns TRUE when called with TRUE.', 'Session'); + $this->assertTrue(drupal_save_session(), 'drupal_save_session() correctly returns TRUE when saving has been enabled.', 'Session'); // Test session hardening code from SA-2008-044. $user = $this->drupalCreateUser(array('access content')); @@ -37,7 +36,7 @@ // Make sure the session cookie is set as HttpOnly. $this->drupalLogin($user); - $this->assertTrue(preg_match('/HttpOnly/i', $this->drupalGetHeader('Set-Cookie', TRUE)), t('Session cookie is set as HttpOnly.')); + $this->assertTrue(preg_match('/HttpOnly/i', $this->drupalGetHeader('Set-Cookie', TRUE)), 'Session cookie is set as HttpOnly.'); $this->drupalLogout(); // Verify that the session is regenerated if a module calls exit @@ -47,7 +46,7 @@ $this->drupalGet('session-test/id'); $matches = array(); preg_match('/\s*session_id:(.*)\n/', $this->drupalGetContent(), $matches); - $this->assertTrue(!empty($matches[1]) , t('Found session ID before logging in.')); + $this->assertTrue(!empty($matches[1]) , 'Found session ID before logging in.'); $original_session = $matches[1]; // We cannot use $this->drupalLogin($user); because we exit in @@ -58,19 +57,18 @@ ); $this->drupalPost('user', $edit, t('Log in')); $this->drupalGet('user'); - $pass = $this->assertText($user->name, t('Found name: %name', array('%name' => $user->name)), t('User login')); + $pass = $this->assertText($user->name, format_string('Found name: %name', array('%name' => $user->name)), 'User login'); $this->_logged_in = $pass; $this->drupalGet('session-test/id'); $matches = array(); preg_match('/\s*session_id:(.*)\n/', $this->drupalGetContent(), $matches); - $this->assertTrue(!empty($matches[1]) , t('Found session ID after logging in.')); - $this->assertTrue($matches[1] != $original_session, t('Session ID changed after login.')); + $this->assertTrue(!empty($matches[1]) , 'Found session ID after logging in.'); + $this->assertTrue($matches[1] != $original_session, 'Session ID changed after login.'); } /** - * 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')); @@ -81,48 +79,48 @@ $value_1 = $this->randomName(); $this->drupalGet('session-test/set/' . $value_1); - $this->assertText($value_1, t('The session value was stored.'), t('Session')); + $this->assertText($value_1, 'The session value was stored.', 'Session'); $this->drupalGet('session-test/get'); - $this->assertText($value_1, t('Session correctly returned the stored data for an authenticated user.'), t('Session')); + $this->assertText($value_1, 'Session correctly returned the stored data for an authenticated user.', 'Session'); // Attempt to write over val_1. If drupal_save_session(FALSE) is working. // properly, val_1 will still be set. $value_2 = $this->randomName(); $this->drupalGet('session-test/no-set/' . $value_2); - $this->assertText($value_2, t('The session value was correctly passed to session-test/no-set.'), t('Session')); + $this->assertText($value_2, 'The session value was correctly passed to session-test/no-set.', 'Session'); $this->drupalGet('session-test/get'); - $this->assertText($value_1, t('Session data is not saved for drupal_save_session(FALSE).'), t('Session')); + $this->assertText($value_1, 'Session data is not saved for drupal_save_session(FALSE).', 'Session'); // Switch browser cookie to anonymous user, then back to user 1. $this->sessionReset(); $this->sessionReset($user->uid); - $this->assertText($value_1, t('Session data persists through browser close.'), t('Session')); + $this->assertText($value_1, 'Session data persists through browser close.', 'Session'); // Logout the user and make sure the stored value no longer persists. $this->drupalLogout(); $this->sessionReset(); $this->drupalGet('session-test/get'); - $this->assertNoText($value_1, t("After logout, previous user's session data is not available."), t('Session')); + $this->assertNoText($value_1, "After logout, previous user's session data is not available.", 'Session'); // Now try to store some data as an anonymous user. $value_3 = $this->randomName(); $this->drupalGet('session-test/set/' . $value_3); - $this->assertText($value_3, t('Session data stored for anonymous user.'), t('Session')); + $this->assertText($value_3, 'Session data stored for anonymous user.', 'Session'); $this->drupalGet('session-test/get'); - $this->assertText($value_3, t('Session correctly returned the stored data for an anonymous user.'), t('Session')); + $this->assertText($value_3, 'Session correctly returned the stored data for an anonymous user.', 'Session'); // Try to store data when drupal_save_session(FALSE). $value_4 = $this->randomName(); $this->drupalGet('session-test/no-set/' . $value_4); - $this->assertText($value_4, t('The session value was correctly passed to session-test/no-set.'), t('Session')); + $this->assertText($value_4, 'The session value was correctly passed to session-test/no-set.', 'Session'); $this->drupalGet('session-test/get'); - $this->assertText($value_3, t('Session data is not saved for drupal_save_session(FALSE).'), t('Session')); + $this->assertText($value_3, 'Session data is not saved for drupal_save_session(FALSE).', 'Session'); // Login, the data should persist. $this->drupalLogin($user); $this->sessionReset($user->uid); $this->drupalGet('session-test/get'); - $this->assertNoText($value_1, t('Session has persisted for an authenticated user after logging out and then back in.'), t('Session')); + $this->assertNoText($value_1, 'Session has persisted for an authenticated user after logging out and then back in.', 'Session'); // Change session and create another user. $user2 = $this->drupalCreateUser(array('access content')); @@ -144,29 +142,29 @@ $this->drupalGet(''); $this->assertSessionCookie(FALSE); $this->assertSessionEmpty(TRUE); - $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS', t('Page was not cached.')); + $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS', 'Page was not cached.'); // Start a new session by setting a message. $this->drupalGet('session-test/set-message'); $this->assertSessionCookie(TRUE); - $this->assertTrue($this->drupalGetHeader('Set-Cookie'), t('New session was started.')); + $this->assertTrue($this->drupalGetHeader('Set-Cookie'), 'New session was started.'); // Display the message, during the same request the session is destroyed // and the session cookie is unset. $this->drupalGet(''); $this->assertSessionCookie(FALSE); $this->assertSessionEmpty(FALSE); - $this->assertFalse($this->drupalGetHeader('X-Drupal-Cache'), t('Caching was bypassed.')); - $this->assertText(t('This is a dummy message.'), t('Message was displayed.')); - $this->assertTrue(preg_match('/SESS\w+=deleted/', $this->drupalGetHeader('Set-Cookie')), t('Session cookie was deleted.')); + $this->assertFalse($this->drupalGetHeader('X-Drupal-Cache'), 'Caching was bypassed.'); + $this->assertText(t('This is a dummy message.'), 'Message was displayed.'); + $this->assertTrue(preg_match('/SESS\w+=deleted/', $this->drupalGetHeader('Set-Cookie')), 'Session cookie was deleted.'); // Verify that session was destroyed. $this->drupalGet(''); $this->assertSessionCookie(FALSE); $this->assertSessionEmpty(TRUE); - $this->assertNoText(t('This is a dummy message.'), t('Message was not cached.')); - $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', t('Page was cached.')); - $this->assertFalse($this->drupalGetHeader('Set-Cookie'), t('New session was not started.')); + $this->assertNoText(t('This is a dummy message.'), 'Message was not cached.'); + $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', 'Page was cached.'); + $this->assertFalse($this->drupalGetHeader('Set-Cookie'), 'New session was not started.'); // Verify that no session is created if drupal_save_session(FALSE) is called. $this->drupalGet('session-test/set-message-but-dont-save'); @@ -177,7 +175,7 @@ $this->drupalGet(''); $this->assertSessionCookie(FALSE); $this->assertSessionEmpty(TRUE); - $this->assertNoText(t('This is a dummy message.'), t('The message was not saved.')); + $this->assertNoText(t('This is a dummy message.'), 'The message was not saved.'); } /** @@ -197,29 +195,29 @@ sleep(1); $this->drupalGet('session-test/set/foo'); $times2 = db_query($sql, array(':uid' => $user->uid))->fetchObject(); - $this->assertEqual($times2->access, $times1->access, t('Users table was not updated.')); - $this->assertNotEqual($times2->timestamp, $times1->timestamp, t('Sessions table was updated.')); + $this->assertEqual($times2->access, $times1->access, 'Users table was not updated.'); + $this->assertNotEqual($times2->timestamp, $times1->timestamp, 'Sessions table was updated.'); // Write the same value again, i.e. do not modify the session. sleep(1); $this->drupalGet('session-test/set/foo'); $times3 = db_query($sql, array(':uid' => $user->uid))->fetchObject(); - $this->assertEqual($times3->access, $times1->access, t('Users table was not updated.')); - $this->assertEqual($times3->timestamp, $times2->timestamp, t('Sessions table was not updated.')); + $this->assertEqual($times3->access, $times1->access, 'Users table was not updated.'); + $this->assertEqual($times3->timestamp, $times2->timestamp, 'Sessions table was not updated.'); // Do not change the session. sleep(1); $this->drupalGet(''); $times4 = db_query($sql, array(':uid' => $user->uid))->fetchObject(); - $this->assertEqual($times4->access, $times3->access, t('Users table was not updated.')); - $this->assertEqual($times4->timestamp, $times3->timestamp, t('Sessions table was not updated.')); + $this->assertEqual($times4->access, $times3->access, 'Users table was not updated.'); + $this->assertEqual($times4->timestamp, $times3->timestamp, 'Sessions table was not updated.'); // Force updating of users and sessions table once per second. variable_set('session_write_interval', 0); $this->drupalGet(''); $times5 = db_query($sql, array(':uid' => $user->uid))->fetchObject(); - $this->assertNotEqual($times5->access, $times4->access, t('Users table was updated.')); - $this->assertNotEqual($times5->timestamp, $times4->timestamp, t('Sessions table was updated.')); + $this->assertNotEqual($times5->access, $times4->access, 'Users table was updated.'); + $this->assertNotEqual($times5->timestamp, $times4->timestamp, 'Sessions table was updated.'); } /** @@ -229,7 +227,7 @@ $user = $this->drupalCreateUser(array('access content')); $this->drupalLogin($user); $this->drupalGet('session-test/is-logged-in'); - $this->assertResponse(200, t('User is logged in.')); + $this->assertResponse(200, 'User is logged in.'); // Reset the sid in {sessions} to a blank string. This may exist in the // wild in some cases, although we normally prevent it from happening. @@ -240,10 +238,10 @@ $this->curlClose(); $this->additionalCurlOptions[CURLOPT_COOKIE] = rawurlencode($this->session_name) . '=;'; $this->drupalGet('session-test/id-from-cookie'); - $this->assertRaw("session_id:\n", t('Session ID is blank as sent from cookie header.')); + $this->assertRaw("session_id:\n", 'Session ID is blank as sent from cookie header.'); // Assert that we have an anonymous session now. $this->drupalGet('session-test/is-logged-in'); - $this->assertResponse(403, t('An empty session ID is not allowed.')); + $this->assertResponse(403, 'An empty session ID is not allowed.'); } /** @@ -261,7 +259,7 @@ $this->additionalCurlOptions[CURLOPT_COOKIEFILE] = $this->cookieFile; $this->additionalCurlOptions[CURLOPT_COOKIESESSION] = TRUE; $this->drupalGet('session-test/get'); - $this->assertResponse(200, t('Session test module is correctly enabled.'), t('Session')); + $this->assertResponse(200, 'Session test module is correctly enabled.', 'Session'); } /** @@ -269,10 +267,10 @@ */ function assertSessionCookie($sent) { if ($sent) { - $this->assertNotNull($this->session_id, t('Session cookie was sent.')); + $this->assertNotNull($this->session_id, 'Session cookie was sent.'); } else { - $this->assertNull($this->session_id, t('Session cookie was not sent.')); + $this->assertNull($this->session_id, 'Session cookie was not sent.'); } } @@ -281,23 +279,23 @@ */ function assertSessionEmpty($empty) { if ($empty) { - $this->assertIdentical($this->drupalGetHeader('X-Session-Empty'), '1', t('Session was empty.')); + $this->assertIdentical($this->drupalGetHeader('X-Session-Empty'), '1', 'Session was empty.'); } else { - $this->assertIdentical($this->drupalGetHeader('X-Session-Empty'), '0', t('Session was not empty.')); + $this->assertIdentical($this->drupalGetHeader('X-Session-Empty'), '0', 'Session was not empty.'); } } } /** - * Ensure that when running under https two session cookies are generated. + * Ensure that when running under HTTPS two session cookies are generated. */ class SessionHttpsTestCase extends DrupalWebTestCase { public static function getInfo() { return array( - 'name' => 'Session https handling', - 'description' => 'Ensure that when running under https two session cookies are generated.', + 'name' => 'Session HTTPS handling', + 'description' => 'Ensure that when running under HTTPS two session cookies are generated.', 'group' => 'Session' ); } @@ -385,7 +383,7 @@ $this->cookies = array(); if ($is_https) { - // The functionality does not make sense when running on https. + // The functionality does not make sense when running on HTTPS. return; } @@ -456,7 +454,7 @@ } } - // Test that session data saved before login is not available using the + // Test that session data saved before login is not available using the // pre-login anonymous cookie. $this->cookies = array(); $this->drupalGet('session-test/get', array('Cookie: ' . $anonymous_cookie)); @@ -472,19 +470,64 @@ $this->drupalGet('user'); $form = $this->xpath('//form[@id="user-login"]'); $form[0]['action'] = $this->httpsUrl('user'); - $this->drupalPost(NULL, $edit, t('Log in'), array(), array('Cookie: ' . $secure_session_name . '=' . $this->cookies[$secure_session_name]['value'])); - - // Get the insecure session cookie set by the secure login POST request. - $headers = $this->drupalGetHeaders(TRUE); - strtok($headers[0]['set-cookie'], ';='); - $session_id = strtok(';='); + $this->drupalPost(NULL, $edit, t('Log in')); // Test that the user is also authenticated on the insecure site. - $this->drupalGet("user/{$user->uid}/edit", array(), array('Cookie: ' . $insecure_session_name . '=' . $session_id)); + $this->drupalGet("user/{$user->uid}/edit"); $this->assertResponse(200); } /** + * 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.0/modules/simpletest/tests/session_test.info drupal-7.66/modules/simpletest/tests/session_test.info --- drupal-7.0/modules/simpletest/tests/session_test.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/session_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: session_test.info,v 1.2 2010/12/20 19:59:43 webchick Exp $ name = "Session test" description = "Support module for session data testing." package = Testing @@ -6,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/simpletest/tests/session_test.module drupal-7.66/modules/simpletest/tests/session_test.module --- drupal-7.0/modules/simpletest/tests/session_test.module 2010-11-13 18:40:09.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/session_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: session_test.module,v 1.16 2010/11/13 17:40:09 webchick Exp $ /** * Implements hook_menu(). diff -Naur drupal-7.0/modules/simpletest/tests/system.base.css drupal-7.66/modules/simpletest/tests/system.base.css --- drupal-7.0/modules/simpletest/tests/system.base.css 2010-10-06 05:34:09.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/system.base.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -/* $Id: system.base.css,v 1.1 2010/10/06 03:34:09 webchick Exp $ */ /** * This file is for testing CSS file override in diff -Naur drupal-7.0/modules/simpletest/tests/system_dependencies_test.info drupal-7.66/modules/simpletest/tests/system_dependencies_test.info --- drupal-7.0/modules/simpletest/tests/system_dependencies_test.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/system_dependencies_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: system_dependencies_test.info,v 1.2 2010/12/20 19:59:43 webchick Exp $ name = "System dependency test" description = "Support module for testing system dependencies." package = Testing @@ -7,8 +6,7 @@ hidden = TRUE dependencies[] = _missing_dependency -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/simpletest/tests/system_dependencies_test.module drupal-7.66/modules/simpletest/tests/system_dependencies_test.module --- drupal-7.0/modules/simpletest/tests/system_dependencies_test.module 2009-12-08 07:39:34.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/system_dependencies_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,2 +1 @@ <?php -// $Id: system_dependencies_test.module,v 1.1 2009/12/08 06:39:34 webchick Exp $ diff -Naur drupal-7.0/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.0/modules/simpletest/tests/system_incompatible_core_version_dependencies_test.info 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/system_incompatible_core_version_dependencies_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -0,0 +1,12 @@ +name = "System incompatible core version dependencies test" +description = "Support module for testing system dependencies." +package = Testing +version = VERSION +core = 7.x +hidden = TRUE +dependencies[] = system_incompatible_core_version_test + +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" +project = "drupal" +datestamp = "1555533576" diff -Naur drupal-7.0/modules/simpletest/tests/system_incompatible_core_version_dependencies_test.module drupal-7.66/modules/simpletest/tests/system_incompatible_core_version_dependencies_test.module --- drupal-7.0/modules/simpletest/tests/system_incompatible_core_version_dependencies_test.module 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/system_incompatible_core_version_dependencies_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1 @@ +<?php diff -Naur drupal-7.0/modules/simpletest/tests/system_incompatible_core_version_test.info drupal-7.66/modules/simpletest/tests/system_incompatible_core_version_test.info --- drupal-7.0/modules/simpletest/tests/system_incompatible_core_version_test.info 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/system_incompatible_core_version_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -0,0 +1,11 @@ +name = "System incompatible core version test" +description = "Support module for testing system dependencies." +package = Testing +version = VERSION +core = 5.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.0/modules/simpletest/tests/system_incompatible_core_version_test.module drupal-7.66/modules/simpletest/tests/system_incompatible_core_version_test.module --- drupal-7.0/modules/simpletest/tests/system_incompatible_core_version_test.module 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/system_incompatible_core_version_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1 @@ +<?php diff -Naur drupal-7.0/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.0/modules/simpletest/tests/system_incompatible_module_version_dependencies_test.info 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/system_incompatible_module_version_dependencies_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -0,0 +1,13 @@ +name = "System incompatible module version dependencies test" +description = "Support module for testing system dependencies." +package = Testing +version = VERSION +core = 7.x +hidden = TRUE +; 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 2019-04-17 +version = "7.66" +project = "drupal" +datestamp = "1555533576" diff -Naur drupal-7.0/modules/simpletest/tests/system_incompatible_module_version_dependencies_test.module drupal-7.66/modules/simpletest/tests/system_incompatible_module_version_dependencies_test.module --- drupal-7.0/modules/simpletest/tests/system_incompatible_module_version_dependencies_test.module 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/system_incompatible_module_version_dependencies_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1 @@ +<?php diff -Naur drupal-7.0/modules/simpletest/tests/system_incompatible_module_version_test.info drupal-7.66/modules/simpletest/tests/system_incompatible_module_version_test.info --- drupal-7.0/modules/simpletest/tests/system_incompatible_module_version_test.info 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/system_incompatible_module_version_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -0,0 +1,11 @@ +name = "System incompatible module version test" +description = "Support module for testing system dependencies." +package = Testing +version = 1.0 +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.0/modules/simpletest/tests/system_incompatible_module_version_test.module drupal-7.66/modules/simpletest/tests/system_incompatible_module_version_test.module --- drupal-7.0/modules/simpletest/tests/system_incompatible_module_version_test.module 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/system_incompatible_module_version_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1 @@ +<?php diff -Naur drupal-7.0/modules/simpletest/tests/system_project_namespace_test.info drupal-7.66/modules/simpletest/tests/system_project_namespace_test.info --- drupal-7.0/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.0/modules/simpletest/tests/system_project_namespace_test.module drupal-7.66/modules/simpletest/tests/system_project_namespace_test.module --- drupal-7.0/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.0/modules/simpletest/tests/system_test.info drupal-7.66/modules/simpletest/tests/system_test.info --- drupal-7.0/modules/simpletest/tests/system_test.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/system_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: system_test.info,v 1.3 2008/10/24 23:32:44 webchick Exp $ name = System test description = Support module for system testing. package = Testing @@ -7,8 +6,7 @@ files[] = system_test.module hidden = TRUE -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/simpletest/tests/system_test.install drupal-7.66/modules/simpletest/tests/system_test.install --- drupal-7.0/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.0/modules/simpletest/tests/system_test.module drupal-7.66/modules/simpletest/tests/system_test.module --- drupal-7.0/modules/simpletest/tests/system_test.module 2010-12-01 01:23:36.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/system_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: system_test.module,v 1.33 2010/12/01 00:23:36 webchick Exp $ /** * Implements hook_menu(). @@ -29,6 +28,13 @@ 'access arguments' => array('access content'), 'type' => MENU_CALLBACK, ); + $items['system-test/multiple-redirects/%'] = array( + 'title' => 'Redirect', + 'page callback' => 'system_test_multiple_redirects', + 'page arguments' => array(2), + 'access arguments' => array('access content'), + 'type' => MENU_CALLBACK, + ); $items['system-test/set-header'] = array( 'page callback' => 'system_test_set_header', 'access arguments' => array('access content'), @@ -72,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', @@ -100,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; } @@ -108,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; } @@ -123,6 +179,30 @@ return ''; } +/** + * Menu callback; sends a redirect header to itself until $count argument is 0. + * + * Emulates the variable number of redirects (given by initial $count argument) + * to the final destination URL by continuous sending of 301 HTTP redirect + * headers to itself together with decrementing the $count parameter until the + * $count parameter reaches 0. After that it returns an empty string to render + * the final destination page. + * + * @param $count + * The count of redirects left until the final destination page. + * + * @returns + * The location redirect if the $count > 0, otherwise an empty string. + */ +function system_test_multiple_redirects($count) { + $count = (int) $count; + if ($count > 0) { + header("location: " . url('system-test/multiple-redirects/' . --$count, array('absolute' => TRUE)), TRUE, 301); + exit; + } + return ''; +} + function system_test_set_header() { drupal_add_http_header($_GET['name'], $_GET['value']); return t('The following header was set: %name: %value', array('%name' => $_GET['name'], '%value' => $_GET['value'])); @@ -147,8 +227,10 @@ * Implements hook_modules_installed(). */ function system_test_modules_installed($modules) { - if (in_array('aggregator', $modules)) { - drupal_set_message(t('hook_modules_installed fired for aggregator')); + if (variable_get('test_verbose_module_hooks')) { + foreach ($modules as $module) { + drupal_set_message(t('hook_modules_installed fired for @module', array('@module' => $module))); + } } } @@ -156,8 +238,10 @@ * Implements hook_modules_enabled(). */ function system_test_modules_enabled($modules) { - if (in_array('aggregator', $modules)) { - drupal_set_message(t('hook_modules_enabled fired for aggregator')); + if (variable_get('test_verbose_module_hooks')) { + foreach ($modules as $module) { + drupal_set_message(t('hook_modules_enabled fired for @module', array('@module' => $module))); + } } } @@ -165,8 +249,10 @@ * Implements hook_modules_disabled(). */ function system_test_modules_disabled($modules) { - if (in_array('aggregator', $modules)) { - drupal_set_message(t('hook_modules_disabled fired for aggregator')); + if (variable_get('test_verbose_module_hooks')) { + foreach ($modules as $module) { + drupal_set_message(t('hook_modules_disabled fired for @module', array('@module' => $module))); + } } } @@ -174,8 +260,10 @@ * Implements hook_modules_uninstalled(). */ function system_test_modules_uninstalled($modules) { - if (in_array('aggregator', $modules)) { - drupal_set_message(t('hook_modules_uninstalled fired for aggregator')); + if (variable_get('test_verbose_module_hooks')) { + foreach ($modules as $module) { + drupal_set_message(t('hook_modules_uninstalled fired for @module', array('@module' => $module))); + } } } @@ -222,10 +310,21 @@ } } + 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; } + if (in_array($file->name, array( + 'system_incompatible_module_version_dependencies_test', + 'system_incompatible_core_version_dependencies_test', + 'system_incompatible_module_version_test', + 'system_incompatible_core_version_test', + ))) { + $info['hidden'] = FALSE; + } if ($file->name == 'requirements1_test' || $file->name == 'requirements2_test') { $info['hidden'] = FALSE; } @@ -359,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.0/modules/simpletest/tests/tablesort.test drupal-7.66/modules/simpletest/tests/tablesort.test --- drupal-7.0/modules/simpletest/tests/tablesort.test 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/tablesort.test 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,166 @@ +<?php + +/** + * @file + * Various tablesort tests. + */ + +/** + * Test unicode handling features implemented in unicode.inc. + */ +class TableSortTest extends DrupalUnitTestCase { + + /** + * Storage for initial value of $_GET. + * + * @var array + */ + protected $GET = array(); + + public static function getInfo() { + return array( + 'name' => 'Tablesort', + 'description' => 'Tests table sorting.', + 'group' => 'System', + ); + } + + function setUp() { + // Save the original $_GET to be restored later. + $this->GET = $_GET; + + parent::setUp(); + } + + function tearDown() { + // Revert $_GET. + $_GET = $this->GET; + + parent::tearDown(); + } + + /** + * Test tablesort_init(). + */ + function testTableSortInit() { + + // Test simple table headers. + + $headers = array('foo', 'bar', 'baz'); + // Reset $_GET to prevent parameters from Simpletest and Batch API ending + // up in $ts['query']. + $_GET = array('q' => 'jahwohl'); + $expected_ts = array( + 'name' => 'foo', + 'sql' => '', + 'sort' => 'asc', + 'query' => array(), + ); + $ts = tablesort_init($headers); + $this->verbose(strtr('$ts: <pre>!ts</pre>', array('!ts' => check_plain(var_export($ts, TRUE))))); + $this->assertEqual($ts, $expected_ts, 'Simple table headers sorted correctly.'); + + // Test with simple table headers plus $_GET parameters that should _not_ + // override the default. + + $_GET = array( + 'q' => 'jahwohl', + // This should not override the table order because only complex + // headers are overridable. + 'order' => 'bar', + ); + $ts = tablesort_init($headers); + $this->verbose(strtr('$ts: <pre>!ts</pre>', array('!ts' => check_plain(var_export($ts, TRUE))))); + $this->assertEqual($ts, $expected_ts, 'Simple table headers plus non-overriding $_GET parameters sorted correctly.'); + + // Test with simple table headers plus $_GET parameters that _should_ + // override the default. + + $_GET = array( + 'q' => 'jahwohl', + 'sort' => 'DESC', + // Add an unrelated parameter to ensure that tablesort will include + // it in the links that it creates. + 'alpha' => 'beta', + ); + $expected_ts['sort'] = 'desc'; + $expected_ts['query'] = array('alpha' => 'beta'); + $ts = tablesort_init($headers); + $this->verbose(strtr('$ts: <pre>!ts</pre>', array('!ts' => check_plain(var_export($ts, TRUE))))); + $this->assertEqual($ts, $expected_ts, 'Simple table headers plus $_GET parameters sorted correctly.'); + + // Test complex table headers. + + $headers = array( + 'foo', + array( + 'data' => '1', + 'field' => 'one', + 'sort' => 'asc', + 'colspan' => 1, + ), + array( + 'data' => '2', + 'field' => 'two', + 'sort' => 'desc', + ), + ); + // Reset $_GET from previous assertion. + $_GET = array( + 'q' => 'jahwohl', + 'order' => '2', + ); + $ts = tablesort_init($headers); + $expected_ts = array( + 'name' => '2', + 'sql' => 'two', + 'sort' => 'desc', + 'query' => array(), + ); + $this->verbose(strtr('$ts: <pre>!ts</pre>', array('!ts' => check_plain(var_export($ts, TRUE))))); + $this->assertEqual($ts, $expected_ts, 'Complex table headers sorted correctly.'); + + // Test complex table headers plus $_GET parameters that should _not_ + // override the default. + + $_GET = array( + 'q' => 'jahwohl', + // This should not override the table order because this header does not + // exist. + 'order' => 'bar', + ); + $ts = tablesort_init($headers); + $expected_ts = array( + 'name' => '1', + 'sql' => 'one', + 'sort' => 'asc', + 'query' => array(), + ); + $this->verbose(strtr('$ts: <pre>!ts</pre>', array('!ts' => check_plain(var_export($ts, TRUE))))); + $this->assertEqual($ts, $expected_ts, 'Complex table headers plus non-overriding $_GET parameters sorted correctly.'); + unset($_GET['sort'], $_GET['order'], $_GET['alpha']); + + // Test complex table headers plus $_GET parameters that _should_ + // override the default. + + $_GET = array( + 'q' => 'jahwohl', + 'order' => '1', + 'sort' => 'ASC', + // Add an unrelated parameter to ensure that tablesort will include + // it in the links that it creates. + 'alpha' => 'beta', + ); + $expected_ts = array( + 'name' => '1', + 'sql' => 'one', + 'sort' => 'asc', + 'query' => array('alpha' => 'beta'), + ); + $ts = tablesort_init($headers); + $this->verbose(strtr('$ts: <pre>!ts</pre>', array('!ts' => check_plain(var_export($ts, TRUE))))); + $this->assertEqual($ts, $expected_ts, 'Complex table headers plus $_GET parameters sorted correctly.'); + unset($_GET['sort'], $_GET['order'], $_GET['alpha']); + + } +} diff -Naur drupal-7.0/modules/simpletest/tests/taxonomy_test.info drupal-7.66/modules/simpletest/tests/taxonomy_test.info --- drupal-7.0/modules/simpletest/tests/taxonomy_test.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/taxonomy_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: taxonomy_test.info,v 1.4 2010/12/20 19:59:43 webchick Exp $ name = "Taxonomy test module" description = "Tests functions and hooks not used in core". package = Testing @@ -7,8 +6,7 @@ hidden = TRUE dependencies[] = taxonomy -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/simpletest/tests/taxonomy_test.install drupal-7.66/modules/simpletest/tests/taxonomy_test.install --- drupal-7.0/modules/simpletest/tests/taxonomy_test.install 2009-12-04 17:49:47.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/taxonomy_test.install 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: taxonomy_test.install,v 1.10 2009/12/04 16:49:47 dries Exp $ /** * @file diff -Naur drupal-7.0/modules/simpletest/tests/taxonomy_test.module drupal-7.66/modules/simpletest/tests/taxonomy_test.module --- drupal-7.0/modules/simpletest/tests/taxonomy_test.module 2009-12-04 17:49:47.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/taxonomy_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,9 +1,10 @@ <?php -// $Id: taxonomy_test.module,v 1.8 2009/12/04 16:49:47 dries Exp $ /** * @file * Test module for Taxonomy hooks and functions not used in core. + * + * @see TaxonomyHooksTestCase::testTaxonomyTermHooks() */ /** @@ -56,6 +57,34 @@ } /** + * Implements hook_taxonomy_term_view(). + */ +function taxonomy_test_taxonomy_term_view($term, $view_mode, $langcode) { + if ($view_mode == 'full') { + $term->content['taxonomy_test_term_view_check'] = array( + '#prefix' => '<div>', + '#markup' => t('The antonym is %antonym', array('%antonym' => $term->antonym)), + '#suffix' => '</div>', + '#weight' => 10, + ); + } +} + +/** + * Implements hook_entity_view(). + */ +function taxonomy_test_entity_view($entity, $type, $view_mode, $langcode) { + if ($type == 'taxonomy_term' && $view_mode == 'full') { + $entity->content['taxonomy_test_entity_view_check'] = array( + '#prefix' => '<div>', + '#markup' => t('The antonym is %antonym', array('%antonym' => $entity->antonym)), + '#suffix' => '</div>', + '#weight' => 20, + ); + } +} + +/** * Implements hook_form_alter(). */ function taxonomy_test_form_alter(&$form, $form_state, $form_id) { @@ -80,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.0/modules/simpletest/tests/theme.test drupal-7.66/modules/simpletest/tests/theme.test --- drupal-7.0/modules/simpletest/tests/theme.test 2010-11-14 22:04:45.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/theme.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ <?php -// $Id: theme.test,v 1.23 2010/11/14 21:04:45 webchick Exp $ /** * @file @@ -9,7 +8,9 @@ /** * Unit tests for the Theme API. */ -class ThemeUnitTest extends DrupalWebTestCase { +class ThemeTestCase extends DrupalWebTestCase { + protected $profile = 'testing'; + public static function getInfo() { return array( 'name' => 'Theme API', @@ -32,25 +33,38 @@ variable_set('site_frontpage', 'nobody-home'); $args = array('node', '1', 'edit'); $suggestions = theme_get_suggestions($args, 'page'); - $this->assertEqual($suggestions, array('page__node', 'page__node__%', 'page__node__1', 'page__node__edit'), t('Found expected node edit page suggestions')); + $this->assertEqual($suggestions, array('page__node', 'page__node__%', 'page__node__1', 'page__node__edit'), 'Found expected node edit page suggestions'); // Check attack vectors. $args = array('node', '\\1'); $suggestions = theme_get_suggestions($args, 'page'); - $this->assertEqual($suggestions, array('page__node', 'page__node__%', 'page__node__1'), t('Removed invalid \\ from suggestions')); + $this->assertEqual($suggestions, array('page__node', 'page__node__%', 'page__node__1'), 'Removed invalid \\ from suggestions'); $args = array('node', '1/'); $suggestions = theme_get_suggestions($args, 'page'); - $this->assertEqual($suggestions, array('page__node', 'page__node__%', 'page__node__1'), t('Removed invalid / from suggestions')); + $this->assertEqual($suggestions, array('page__node', 'page__node__%', 'page__node__1'), 'Removed invalid / from suggestions'); $args = array('node', "1\0"); $suggestions = theme_get_suggestions($args, 'page'); - $this->assertEqual($suggestions, array('page__node', 'page__node__%', 'page__node__1'), t('Removed invalid \\0 from suggestions')); + $this->assertEqual($suggestions, array('page__node', 'page__node__%', 'page__node__1'), 'Removed invalid \\0 from suggestions'); + // Define path with hyphens to be used to generate suggestions. + $args = array('node', '1', 'hyphen-path'); + $result = array('page__node', 'page__node__%', 'page__node__1', 'page__node__hyphen_path'); + $suggestions = theme_get_suggestions($args, 'page'); + $this->assertEqual($suggestions, $result, 'Found expected page suggestions for paths containing hyphens.'); } /** - * Preprocess functions for the base hook should run even for suggestion implementations. - */ + * Ensures preprocess functions run even for suggestion implementations. + * + * The theme hook used by this test has its base preprocess function in a + * separate file, so this test also ensures that that file is correctly loaded + * when needed. + */ function testPreprocessForSuggestions() { - $this->drupalGet('theme-test/suggestion'); - $this->assertText('test_theme_breadcrumb__suggestion: 1', t('Theme hook suggestion ran with data available from a preprocess function for the base hook.')); + // Test with both an unprimed and primed theme registry. + drupal_theme_rebuild(); + for ($i = 0; $i < 2; $i++) { + $this->drupalGet('theme-test/suggestion'); + $this->assertText('Theme hook implementor=test_theme_theme_test__suggestion(). Foo=template_preprocess_theme_test', 'Theme hook suggestion ran with data available from a preprocess function for the base hook.'); + } } /** @@ -64,7 +78,7 @@ $suggestions = theme_get_suggestions(explode('/', $_GET['q']), 'page'); // Set it back to not annoy the batch runner. $_GET['q'] = $q; - $this->assertTrue(in_array('page__front', $suggestions), t('Front page template was suggested.')); + $this->assertTrue(in_array('page__front', $suggestions), 'Front page template was suggested.'); } /** @@ -72,7 +86,7 @@ */ function testAlter() { $this->drupalGet('theme-test/alter'); - $this->assertText('The altered data is test_theme_theme_test_alter_alter was invoked.', t('The theme was able to implement an alter hook during page building before anything was rendered.')); + $this->assertText('The altered data is test_theme_theme_test_alter_alter was invoked.', 'The theme was able to implement an alter hook during page building before anything was rendered.'); } /** @@ -87,7 +101,7 @@ // test theme. First we test with CSS aggregation disabled. variable_set('preprocess_css', 0); $this->drupalGet('theme-test/suggestion'); - $this->assertNoText('system.base.css', t('The theme\'s .info file is able to override a module CSS file from being added to the page.')); + $this->assertNoText('system.base.css', 'The theme\'s .info file is able to override a module CSS file from being added to the page.'); // Also test with aggregation enabled, simply ensuring no PHP errors are // triggered during drupal_build_css_cache() when a source file doesn't @@ -97,12 +111,65 @@ $this->drupalGet('theme-test/suggestion'); variable_set('preprocess_css', 0); } + + /** + * Ensures the theme registry is rebuilt when modules are disabled/enabled. + */ + function testRegistryRebuild() { + $this->assertIdentical(theme('theme_test_foo', array('foo' => 'a')), 'a', 'The theme registry contains theme_test_foo.'); + + module_disable(array('theme_test'), FALSE); + $this->assertIdentical(theme('theme_test_foo', array('foo' => 'b')), '', 'The theme registry does not contain theme_test_foo, because the module is disabled.'); + + module_enable(array('theme_test'), FALSE); + $this->assertIdentical(theme('theme_test_foo', array('foo' => 'c')), 'c', 'The theme registry contains theme_test_foo again after re-enabling the module.'); + } + + /** + * Test the list_themes() function. + */ + function testListThemes() { + $themes = list_themes(); + // Check if drupal_theme_access() retrieves enabled themes properly from list_themes(). + $this->assertTrue(drupal_theme_access('test_theme'), 'Enabled theme detected'); + // Check if list_themes() returns disabled themes. + $this->assertTrue(array_key_exists('test_basetheme', $themes), 'Disabled theme detected'); + // Check for base theme and subtheme lists. + $base_theme_list = array('test_basetheme' => 'Theme test base theme'); + $sub_theme_list = array('test_subtheme' => 'Theme test subtheme'); + $this->assertIdentical($themes['test_basetheme']->sub_themes, $sub_theme_list, 'Base theme\'s object includes list of subthemes.'); + $this->assertIdentical($themes['test_subtheme']->base_themes, $base_theme_list, 'Subtheme\'s object includes list of base themes.'); + // Check for theme engine in subtheme. + $this->assertIdentical($themes['test_subtheme']->engine, 'phptemplate', 'Subtheme\'s object includes the theme engine.'); + // Check for theme engine prefix. + $this->assertIdentical($themes['test_basetheme']->prefix, 'phptemplate', 'Base theme\'s object includes the theme engine prefix.'); + $this->assertIdentical($themes['test_subtheme']->prefix, 'phptemplate', 'Subtheme\'s object includes the theme engine prefix.'); + } + + /** + * Test the theme_get_setting() function. + */ + function testThemeGetSetting() { + $GLOBALS['theme_key'] = 'test_theme'; + $this->assertIdentical(theme_get_setting('theme_test_setting'), 'default value', 'theme_get_setting() uses the default theme automatically.'); + $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'); + } } /** * Unit tests for theme_table(). */ -class ThemeTableUnitTest extends DrupalWebTestCase { +class ThemeTableTestCase extends DrupalWebTestCase { public static function getInfo() { return array( 'name' => 'Theme Table', @@ -119,8 +186,8 @@ $rows = array(array(1,2,3), array(4,5,6), array(7,8,9)); $this->content = theme('table', array('header' => $header, 'rows' => $rows)); $js = drupal_add_js(); - $this->assertTrue(isset($js['misc/tableheader.js']), t('tableheader.js was included when $sticky = TRUE.')); - $this->assertRaw('sticky-enabled', t('Table has a class of sticky-enabled when $sticky = TRUE.')); + $this->assertTrue(isset($js['misc/tableheader.js']), 'tableheader.js was included when $sticky = TRUE.'); + $this->assertRaw('sticky-enabled', 'Table has a class of sticky-enabled when $sticky = TRUE.'); drupal_static_reset('drupal_add_js'); } @@ -135,8 +202,8 @@ $colgroups = array(); $this->content = theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => $attributes, 'caption' => $caption, 'colgroups' => $colgroups, 'sticky' => FALSE)); $js = drupal_add_js(); - $this->assertFalse(isset($js['misc/tableheader.js']), t('tableheader.js was not included because $sticky = FALSE.')); - $this->assertNoRaw('sticky-enabled', t('Table does not have a class of sticky-enabled because $sticky = FALSE.')); + $this->assertFalse(isset($js['misc/tableheader.js']), 'tableheader.js was not included because $sticky = FALSE.'); + $this->assertNoRaw('sticky-enabled', 'Table does not have a class of sticky-enabled because $sticky = FALSE.'); drupal_static_reset('drupal_add_js'); } @@ -153,10 +220,24 @@ ), ); $this->content = theme('table', array('header' => $header, 'rows' => array(), 'empty' => t('No strings available.'))); - $this->assertRaw('<tr class="odd"><td colspan="3" class="empty message">No strings available.</td>', t('Correct colspan was set on empty message.')); - $this->assertRaw('<thead><tr><th>Header 1</th>', t('Table header was printed.')); + $this->assertRaw('<tr class="odd"><td colspan="3" class="empty message">No strings available.</td>', 'Correct colspan was set on empty message.'); + $this->assertRaw('<thead><tr><th>Header 1</th>', 'Table header was printed.'); } + /** + * Tests that the 'no_striping' option works correctly. + */ + function testThemeTableWithNoStriping() { + $rows = array( + array( + 'data' => array(1), + 'no_striping' => TRUE, + ), + ); + $this->content = theme('table', array('rows' => $rows)); + $this->assertNoRaw('class="odd"', 'Odd/even classes were not added because $no_striping = TRUE.'); + $this->assertNoRaw('no_striping', 'No invalid no_striping HTML attribute was printed.'); + } } /** @@ -172,25 +253,26 @@ } /** - * Test nested list rendering. + * Test item list rendering. */ - function testNestedList() { - $items = array('a', array('data' => 'b', 'children' => array('c', 'd')), 'e'); + function testItemList() { + $items = array('a', array('data' => 'b', 'children' => array('c' => 'c', 'd' => 'd', 'e' => 'e')), 'f'); $expected = '<div class="item-list"><ul><li class="first">a</li> <li>b<div class="item-list"><ul><li class="first">c</li> -<li class="last">d</li> -</ul></div></li> +<li>d</li> <li class="last">e</li> +</ul></div></li> +<li class="last">f</li> </ul></div>'; $output = theme('item_list', array('items' => $items)); - $this->assertIdentical($expected, $output, 'Nested list is rendered correctly.'); + $this->assertIdentical($expected, $output, 'Item list is rendered correctly.'); } } /** * Unit tests for theme_links(). */ -class ThemeLinksUnitTest extends DrupalUnitTestCase { +class ThemeLinksTest extends DrupalWebTestCase { public static function getInfo() { return array( 'name' => 'Links', @@ -262,14 +344,14 @@ $html = drupal_render($render_array); $dom = new DOMDocument(); $dom->loadHTML($html); - $this->assertEqual($dom->getElementsByTagName('ul')->length, 1, t('One "ul" tag found in the rendered HTML.')); + $this->assertEqual($dom->getElementsByTagName('ul')->length, 1, 'One "ul" tag found in the rendered HTML.'); $list_elements = $dom->getElementsByTagName('li'); - $this->assertEqual($list_elements->length, 3, t('Three "li" tags found in the rendered HTML.')); - $this->assertEqual($list_elements->item(0)->nodeValue, 'Parent link original', t('First expected link found.')); - $this->assertEqual($list_elements->item(1)->nodeValue, 'First child link', t('Second expected link found.')); - $this->assertEqual($list_elements->item(2)->nodeValue, 'Second child link', t('Third expected link found.')); - $this->assertIdentical(strpos($html, 'Parent link copy'), FALSE, t('"Parent link copy" link not found.')); - $this->assertIdentical(strpos($html, 'Third child link'), FALSE, t('"Third child link" link not found.')); + $this->assertEqual($list_elements->length, 3, 'Three "li" tags found in the rendered HTML.'); + $this->assertEqual($list_elements->item(0)->nodeValue, 'Parent link original', 'First expected link found.'); + $this->assertEqual($list_elements->item(1)->nodeValue, 'First child link', 'Second expected link found.'); + $this->assertEqual($list_elements->item(2)->nodeValue, 'Second child link', 'Third expected link found.'); + $this->assertIdentical(strpos($html, 'Parent link copy'), FALSE, '"Parent link copy" link not found.'); + $this->assertIdentical(strpos($html, 'Third child link'), FALSE, '"Third child link" link not found.'); // Now render 'first_child', followed by the rest of the links, and make // sure we get two separate <ul>'s with the appropriate links contained @@ -280,28 +362,28 @@ // First check the child HTML. $dom = new DOMDocument(); $dom->loadHTML($child_html); - $this->assertEqual($dom->getElementsByTagName('ul')->length, 1, t('One "ul" tag found in the rendered child HTML.')); + $this->assertEqual($dom->getElementsByTagName('ul')->length, 1, 'One "ul" tag found in the rendered child HTML.'); $list_elements = $dom->getElementsByTagName('li'); - $this->assertEqual($list_elements->length, 2, t('Two "li" tags found in the rendered child HTML.')); - $this->assertEqual($list_elements->item(0)->nodeValue, 'Parent link copy', t('First expected link found.')); - $this->assertEqual($list_elements->item(1)->nodeValue, 'First child link', t('Second expected link found.')); + $this->assertEqual($list_elements->length, 2, 'Two "li" tags found in the rendered child HTML.'); + $this->assertEqual($list_elements->item(0)->nodeValue, 'Parent link copy', 'First expected link found.'); + $this->assertEqual($list_elements->item(1)->nodeValue, 'First child link', 'Second expected link found.'); // Then check the parent HTML. $dom = new DOMDocument(); $dom->loadHTML($parent_html); - $this->assertEqual($dom->getElementsByTagName('ul')->length, 1, t('One "ul" tag found in the rendered parent HTML.')); + $this->assertEqual($dom->getElementsByTagName('ul')->length, 1, 'One "ul" tag found in the rendered parent HTML.'); $list_elements = $dom->getElementsByTagName('li'); - $this->assertEqual($list_elements->length, 2, t('Two "li" tags found in the rendered parent HTML.')); - $this->assertEqual($list_elements->item(0)->nodeValue, 'Parent link original', t('First expected link found.')); - $this->assertEqual($list_elements->item(1)->nodeValue, 'Second child link', t('Second expected link found.')); - $this->assertIdentical(strpos($parent_html, 'First child link'), FALSE, t('"First child link" link not found.')); - $this->assertIdentical(strpos($parent_html, 'Third child link'), FALSE, t('"Third child link" link not found.')); + $this->assertEqual($list_elements->length, 2, 'Two "li" tags found in the rendered parent HTML.'); + $this->assertEqual($list_elements->item(0)->nodeValue, 'Parent link original', 'First expected link found.'); + $this->assertEqual($list_elements->item(1)->nodeValue, 'Second child link', 'Second expected link found.'); + $this->assertIdentical(strpos($parent_html, 'First child link'), FALSE, '"First child link" link not found.'); + $this->assertIdentical(strpos($parent_html, 'Third child link'), FALSE, '"Third child link" link not found.'); } } /** * Functional test for initialization of the theme system in hook_init(). */ -class ThemeHookInitUnitTest extends DrupalWebTestCase { +class ThemeHookInitTestCase extends DrupalWebTestCase { public static function getInfo() { return array( 'name' => 'Theme initialization in hook_init()', @@ -319,8 +401,8 @@ */ function testThemeInitializationHookInit() { $this->drupalGet('theme-test/hook-init'); - $this->assertRaw('Themed output generated in hook_init()', t('Themed output generated in hook_init() correctly appears on the page.')); - $this->assertRaw('bartik/css/style.css', t("The default theme's CSS appears on the page when the theme system is initialized in hook_init().")); + $this->assertRaw('Themed output generated in hook_init()', 'Themed output generated in hook_init() correctly appears on the page.'); + $this->assertRaw('bartik/css/style.css', "The default theme's CSS appears on the page when the theme system is initialized in hook_init()."); } } @@ -347,6 +429,251 @@ function testUserAutocomplete() { $this->drupalLogin($this->account); $this->drupalGet('user/autocomplete/' . $this->account->name); - $this->assertText('registry not initialized', t('The registry was not initialized')); + $this->assertText('registry not initialized', 'The registry was not initialized'); } } + +/** + * Tests the markup of core render element types passed to drupal_render(). + */ +class RenderElementTypesTestCase extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'Render element types', + 'description' => 'Tests the markup of core render element types passed to drupal_render().', + 'group' => 'Theme', + ); + } + + /** + * 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 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' => '<meta name="description" content="Drupal test" />' . "\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>title test' . "\n", + ), + ); + + $this->assertElements($elements); + } +} + +/** + * Tests for the ThemeRegistry class. + */ +class ThemeRegistryTestCase extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'ThemeRegistry', + 'description' => 'Tests the behavior of the ThemeRegistry class', + 'group' => 'Theme', + ); + } + function setUp() { + parent::setUp('theme_test'); + } + + /** + * Tests the behavior of the theme registry class. + */ + function testRaceCondition() { + $_SERVER['REQUEST_METHOD'] = 'GET'; + $cid = 'test_theme_registry'; + + // Directly instantiate the theme registry, this will cause a base cache + // entry to be written in __construct(). + $registry = new ThemeRegistry($cid, 'cache'); + + $this->assertTrue(cache_get($cid), 'Cache entry was created.'); + + // Trigger a cache miss for an offset. + $this->assertTrue($registry['theme_test_template_test'], 'Offset was returned correctly from the theme registry.'); + // This will cause the ThemeRegistry class to write an updated version of + // the cache entry when it is destroyed, usually at the end of the request. + // Before that happens, manually delete the cache entry we created earlier + // so that the new entry is written from scratch. + cache_clear_all($cid, 'cache'); + + // Destroy the class so that it triggers a cache write for the offset. + unset($registry); + + $this->assertTrue(cache_get($cid), 'Cache entry was created.'); + + // Create a new instance of the class. Confirm that both the offset + // requested previously, and one that has not yet been requested are both + // available. + $registry = new ThemeRegistry($cid, 'cache'); + + $this->assertTrue($registry['theme_test_template_test'], 'Offset was returned correctly from the theme registry'); + $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.0/modules/simpletest/tests/theme_test.inc drupal-7.66/modules/simpletest/tests/theme_test.inc --- drupal-7.0/modules/simpletest/tests/theme_test.inc 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/theme_test.inc 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,15 @@ + 'theme_test.inc', + 'variables' => array('foo' => ''), + ); + $items['theme_test_template_test'] = array( + 'template' => 'theme_test.template_test', + ); + $items['theme_test_template_test_2'] = array( + 'template' => 'theme_test.template_test', + ); + $items['theme_test_foo'] = array( + 'variables' => array('foo' => NULL), + ); + return $items; +} + +/** + * Implements hook_system_theme_info(). + */ +function theme_test_system_theme_info() { + $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(). @@ -24,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; } @@ -94,14 +143,29 @@ * Page callback, calls a theme hook suggestion. */ function _theme_test_suggestion() { - return theme(array('breadcrumb__suggestion', 'breadcrumb'), array()); + return theme(array('theme_test__suggestion', 'theme_test'), array()); } /** - * Implements hook_preprocess_breadcrumb(). - * - * Set a variable that can later be tested to see if this function ran. + * 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_test_preprocess_breadcrumb(&$variables) { - $variables['theme_test_preprocess_breadcrumb'] = 1; +function theme_theme_test_foo($variables) { + return $variables['foo']; } diff -Naur drupal-7.0/modules/simpletest/tests/theme_test.template_test.tpl.php drupal-7.66/modules/simpletest/tests/theme_test.template_test.tpl.php --- drupal-7.0/modules/simpletest/tests/theme_test.template_test.tpl.php 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/theme_test.template_test.tpl.php 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,2 @@ + +Fail: Template not overridden. diff -Naur drupal-7.0/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.0/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.0/modules/simpletest/tests/themes/test_basetheme/test_basetheme.info drupal-7.66/modules/simpletest/tests/themes/test_basetheme/test_basetheme.info --- drupal-7.0/modules/simpletest/tests/themes/test_basetheme/test_basetheme.info 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/themes/test_basetheme/test_basetheme.info 2019-04-17 22:39:36.000000000 +0200 @@ -0,0 +1,12 @@ +name = Theme test base theme +description = Test theme which acts as a base theme for other test subthemes. +core = 7.x +hidden = TRUE + +settings[basetheme_only] = base theme value +settings[subtheme_override] = base theme value + +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" +project = "drupal" +datestamp = "1555533576" diff -Naur drupal-7.0/modules/simpletest/tests/themes/test_subtheme/test_subtheme.info drupal-7.66/modules/simpletest/tests/themes/test_subtheme/test_subtheme.info --- drupal-7.0/modules/simpletest/tests/themes/test_subtheme/test_subtheme.info 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/themes/test_subtheme/test_subtheme.info 2019-04-17 22:39:36.000000000 +0200 @@ -0,0 +1,12 @@ +name = Theme test subtheme +description = Test theme which uses test_basetheme as the base theme. +core = 7.x +base theme = test_basetheme +hidden = TRUE + +settings[subtheme_override] = subtheme value + +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" +project = "drupal" +datestamp = "1555533576" diff -Naur drupal-7.0/modules/simpletest/tests/themes/test_theme/template.php drupal-7.66/modules/simpletest/tests/themes/test_theme/template.php --- drupal-7.0/modules/simpletest/tests/themes/test_theme/template.php 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/themes/test_theme/template.php 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,19 @@ + +Node Content Dummy diff -Naur drupal-7.0/modules/simpletest/tests/themes/test_theme/test_theme.info drupal-7.66/modules/simpletest/tests/themes/test_theme/test_theme.info --- drupal-7.0/modules/simpletest/tests/themes/test_theme/test_theme.info 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/themes/test_theme/test_theme.info 2019-04-17 22:39:36.000000000 +0200 @@ -0,0 +1,23 @@ +name = Test theme +description = Theme for testing the theme system +core = 7.x +hidden = TRUE + +; Normally, themes may list CSS files like this, and if they exist in the theme +; folder, then they get added to the page. If they have the same file name as a +; module CSS file, then the theme's version overrides the module's version, so +; that the module's version is not added to the page. Additionally, a theme may +; have an entry like this one, without having the corresponding CSS file in the +; theme's folder, and in this case, it just stops the module's version from +; being loaded, and does not replace it with an alternate version. We have this +; here in order for a test to ensure that this correctly prevents the module +; version from being loaded, and that errors aren't caused by the lack of this +; file within the theme folder. +stylesheets[all][] = system.base.css + +settings[theme_test_setting] = default value + +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" +project = "drupal" +datestamp = "1555533576" diff -Naur drupal-7.0/modules/simpletest/tests/themes/test_theme/theme-settings.php drupal-7.66/modules/simpletest/tests/themes/test_theme/theme-settings.php --- drupal-7.0/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.0/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.0/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.0/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.0/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.0/modules/simpletest/tests/unicode.test drupal-7.66/modules/simpletest/tests/unicode.test --- drupal-7.0/modules/simpletest/tests/unicode.test 2010-08-11 12:58:22.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/unicode.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ $output) { - $this->assertEqual(drupal_strtolower($input), $output, t('%input is lowercased as %output', array('%input' => $input, '%output' => $output))); + $this->assertEqual(drupal_strtolower($input), $output, format_string('%input is lowercased as %output', array('%input' => $input, '%output' => $output))); } } @@ -95,7 +94,7 @@ } foreach ($testcase as $input => $output) { - $this->assertEqual(drupal_strtoupper($input), $output, t('%input is uppercased as %output', array('%input' => $input, '%output' => $output))); + $this->assertEqual(drupal_strtoupper($input), $output, format_string('%input is uppercased as %output', array('%input' => $input, '%output' => $output))); } } @@ -111,7 +110,7 @@ } foreach ($testcase as $input => $output) { - $this->assertEqual(drupal_ucfirst($input), $output, t('%input is ucfirst-ed as %output', array('%input' => $input, '%output' => $output))); + $this->assertEqual(drupal_ucfirst($input), $output, format_string('%input is ucfirst-ed as %output', array('%input' => $input, '%output' => $output))); } } @@ -122,7 +121,7 @@ ); foreach ($testcase as $input => $output) { - $this->assertEqual(drupal_strlen($input), $output, t('%input length is %output', array('%input' => $input, '%output' => $output))); + $this->assertEqual(drupal_strlen($input), $output, format_string('%input length is %output', array('%input' => $input, '%output' => $output))); } } @@ -182,7 +181,7 @@ foreach ($testcase as $test) { list($input, $start, $length, $output) = $test; $result = drupal_substr($input, $start, $length); - $this->assertEqual($result, $output, t('%input substring at offset %offset for %length characters is %output (got %result)', array('%input' => $input, '%offset' => $start, '%length' => $length, '%output' => $output, '%result' => $result))); + $this->assertEqual($result, $output, format_string('%input substring at offset %offset for %length characters is %output (got %result)', array('%input' => $input, '%offset' => $start, '%length' => $length, '%output' => $output, '%result' => $result))); } } @@ -214,7 +213,7 @@ '€' => '€', ); foreach ($testcase as $input => $output) { - $this->assertEqual(decode_entities($input), $output, t('Make sure the decoded entity of @input is @output', array('@input' => $input, '@output' => $output))); + $this->assertEqual(decode_entities($input), $output, format_string('Make sure the decoded entity of @input is @output', array('@input' => $input, '@output' => $output))); } } @@ -300,7 +299,7 @@ foreach ($cases as $case) { list($input, $max_length, $expected) = $case; $output = truncate_utf8($input, $max_length, $wordsafe, $ellipsis); - $this->assertEqual($output, $expected, t('%input truncate to %length characters with %wordsafe, %ellipsis is %expected (got %output)', array('%input' => $input, '%length' => $max_length, '%output' => $output, '%expected' => $expected, '%wordsafe' => ($wordsafe ? 'word-safe' : 'not word-safe'), '%ellipsis' => ($ellipsis ? 'ellipsis' : 'not ellipsis')))); + $this->assertEqual($output, $expected, format_string('%input truncate to %length characters with %wordsafe, %ellipsis is %expected (got %output)', array('%input' => $input, '%length' => $max_length, '%output' => $output, '%expected' => $expected, '%wordsafe' => ($wordsafe ? 'word-safe' : 'not word-safe'), '%ellipsis' => ($ellipsis ? 'ellipsis' : 'not ellipsis')))); } } } diff -Naur drupal-7.0/modules/simpletest/tests/update.test drupal-7.66/modules/simpletest/tests/update.test --- drupal-7.0/modules/simpletest/tests/update.test 2010-08-06 01:53:38.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/update.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ assertEqual($expected_updates, $actual_updates, t('Updates within a single module run in the correct order.')); + $this->assertEqual($expected_updates, $actual_updates, 'Updates within a single module run in the correct order.'); } /** @@ -50,9 +49,9 @@ $update_order = array_keys(update_resolve_dependencies($starting_updates)); // Make sure that each dependency is satisfied. $first_dependency_satisfied = array_search('update_test_2_update_7000', $update_order) < array_search('update_test_3_update_7000', $update_order); - $this->assertTrue($first_dependency_satisfied, t('The dependency of the second module on the first module is respected by the update function order.')); + $this->assertTrue($first_dependency_satisfied, 'The dependency of the second module on the first module is respected by the update function order.'); $second_dependency_satisfied = array_search('update_test_3_update_7000', $update_order) < array_search('update_test_2_update_7001', $update_order); - $this->assertTrue($second_dependency_satisfied, t('The dependency of the first module on the second module is respected by the update function order.')); + $this->assertTrue($second_dependency_satisfied, 'The dependency of the first module on the second module is respected by the update function order.'); } } @@ -80,9 +79,9 @@ 'update_test_2' => 7000, ); $update_graph = update_resolve_dependencies($starting_updates); - $this->assertTrue($update_graph['update_test_2_update_7000']['allowed'], t("The module's first update function is allowed to run, since it does not have any missing dependencies.")); - $this->assertFalse($update_graph['update_test_2_update_7001']['allowed'], t("The module's second update function is not allowed to run, since it has a direct dependency on a missing update.")); - $this->assertFalse($update_graph['update_test_2_update_7002']['allowed'], t("The module's third update function is not allowed to run, since it has an indirect dependency on a missing update.")); + $this->assertTrue($update_graph['update_test_2_update_7000']['allowed'], "The module's first update function is allowed to run, since it does not have any missing dependencies."); + $this->assertFalse($update_graph['update_test_2_update_7001']['allowed'], "The module's second update function is not allowed to run, since it has a direct dependency on a missing update."); + $this->assertFalse($update_graph['update_test_2_update_7002']['allowed'], "The module's third update function is not allowed to run, since it has an indirect dependency on a missing update."); } } @@ -108,9 +107,9 @@ */ function testHookUpdateDependencies() { $update_dependencies = update_retrieve_dependencies(); - $this->assertTrue($update_dependencies['system'][7000]['update_test_1'] == 7000, t('An update function that has a dependency on two separate modules has the first dependency recorded correctly.')); - $this->assertTrue($update_dependencies['system'][7000]['update_test_2'] == 7001, t('An update function that has a dependency on two separate modules has the second dependency recorded correctly.')); - $this->assertTrue($update_dependencies['system'][7001]['update_test_1'] == 7002, t('An update function that depends on more than one update from the same module only has the dependency on the higher-numbered update function recorded.')); + $this->assertTrue($update_dependencies['system'][7000]['update_test_1'] == 7000, 'An update function that has a dependency on two separate modules has the first dependency recorded correctly.'); + $this->assertTrue($update_dependencies['system'][7000]['update_test_2'] == 7001, 'An update function that has a dependency on two separate modules has the second dependency recorded correctly.'); + $this->assertTrue($update_dependencies['system'][7001]['update_test_1'] == 7002, 'An update function that depends on more than one update from the same module only has the dependency on the higher-numbered update function recorded.'); } } diff -Naur drupal-7.0/modules/simpletest/tests/update_script_test.info drupal-7.66/modules/simpletest/tests/update_script_test.info --- drupal-7.0/modules/simpletest/tests/update_script_test.info 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/update_script_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -0,0 +1,11 @@ +name = "Update script test" +description = "Support module for update script 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.0/modules/simpletest/tests/update_script_test.install drupal-7.66/modules/simpletest/tests/update_script_test.install --- drupal-7.0/modules/simpletest/tests/update_script_test.install 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/update_script_test.install 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,58 @@ + 'Update script test', + 'value' => 'Warning', + 'description' => 'This is a requirements warning provided by the update_script_test module.', + 'severity' => REQUIREMENT_WARNING, + ); + break; + case REQUIREMENT_ERROR: + $requirements['update_script_test'] = array( + 'title' => 'Update script test', + 'value' => 'Error', + 'description' => 'This is a requirements error provided by the update_script_test module.', + '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; + } + } + + return $requirements; +} + +/** + * Dummy update function to run during the tests. + */ +function update_script_test_update_7000() { + return t('The update_script_test_update_7000() update was executed successfully.'); +} diff -Naur drupal-7.0/modules/simpletest/tests/update_script_test.module drupal-7.66/modules/simpletest/tests/update_script_test.module --- drupal-7.0/modules/simpletest/tests/update_script_test.module 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/update_script_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,18 @@ + '0', 'login' => '0', 'status' => '0', - 'timezone' => NULL, + 'timezone' => '-21600', 'language' => '', 'picture' => '', 'init' => '', @@ -7923,7 +7922,7 @@ 'access' => '1277671612', 'login' => '1277671612', 'status' => '1', - 'timezone' => NULL, + 'timezone' => '-21600', 'language' => '', 'picture' => '', 'init' => 'admin@example.com', diff -Naur drupal-7.0/modules/simpletest/tests/upgrade/drupal-6.duplicate-permission.database.php drupal-7.66/modules/simpletest/tests/upgrade/drupal-6.duplicate-permission.database.php --- drupal-7.0/modules/simpletest/tests/upgrade/drupal-6.duplicate-permission.database.php 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/upgrade/drupal-6.duplicate-permission.database.php 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,8 @@ +fields(array( + 'perm' => 'access content, access content', +)) +->condition('pid', 1) +->execute(); diff -Naur drupal-7.0/modules/simpletest/tests/upgrade/drupal-6.filled.database.php drupal-7.66/modules/simpletest/tests/upgrade/drupal-6.filled.database.php --- drupal-7.0/modules/simpletest/tests/upgrade/drupal-6.filled.database.php 2010-12-30 04:44:39.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/upgrade/drupal-6.filled.database.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ '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', @@ -19933,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', @@ -19946,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', @@ -19959,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', @@ -19972,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', @@ -19985,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', @@ -19998,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', @@ -20011,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', @@ -20024,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', @@ -20037,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', @@ -20050,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', @@ -20063,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', @@ -20076,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', @@ -20089,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', @@ -20102,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', @@ -20115,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', @@ -20128,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', @@ -20141,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', @@ -20154,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', @@ -20167,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', @@ -20180,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', @@ -20193,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', @@ -20206,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', @@ -20219,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.0/modules/simpletest/tests/upgrade/drupal-6.forum.database.php drupal-7.66/modules/simpletest/tests/upgrade/drupal-6.forum.database.php --- drupal-7.0/modules/simpletest/tests/upgrade/drupal-6.forum.database.php 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/upgrade/drupal-6.forum.database.php 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,274 @@ + array( + 'nid' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'vid' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'tid' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + ), + 'indexes' => array( + 'nid' => array( + 'nid', + ), + 'tid' => array( + 'tid', + ), + ), + 'primary key' => array( + 'vid', + ), + 'module' => 'forum', + 'name' => 'forum', +)); +db_insert('forum')->fields(array( + 'nid', + 'vid', + 'tid', +)) +->values(array( + 'nid' => '51', + 'vid' => '61', + 'tid' => '81', +)) +->execute(); + +db_insert('node')->fields(array( + 'nid', + 'vid', + 'type', + 'language', + 'title', + 'uid', + 'status', + 'created', + 'changed', + 'comment', + 'promote', + 'moderate', + 'sticky', + 'tnid', + 'translate', +)) +->values(array( + 'nid' => '51', + 'vid' => '61', + 'type' => 'forum', + 'language' => '', + 'title' => 'Apples', + 'uid' => '1', + 'status' => '1', + 'created' => '1298363952', + 'changed' => '1298363952', + 'comment' => '2', + 'promote' => '0', + 'moderate' => '0', + 'sticky' => '0', + 'tnid' => '0', + 'translate' => '0', +)) +->execute(); + +db_insert('node_revisions')->fields(array( + 'nid', + 'vid', + 'uid', + 'title', + 'body', + 'teaser', + 'log', + 'timestamp', + 'format', +)) +->values(array( + 'nid' => '51', + 'vid' => '61', + 'uid' => '1', + 'title' => 'Apples', + 'body' => 'A fruit.', + 'teaser' => 'A fruit.', + 'log' => '', + 'timestamp' => '1298363952', + 'format' => '1', +)) +->execute(); + +db_insert('node_comment_statistics')->fields(array( + 'nid', + 'last_comment_timestamp', + 'last_comment_name', + 'last_comment_uid', + 'comment_count', +)) +->values(array( + 'nid' => '51', + 'last_comment_timestamp' => '1298363952', + 'last_comment_name' => NULL, + 'last_comment_uid' => '1', + 'comment_count' => '0', +)) +->execute(); + +db_insert('node_type')->fields(array( + 'type', + 'name', + 'module', + 'description', + 'help', + 'has_title', + 'title_label', + 'has_body', + 'body_label', + 'min_word_count', + 'custom', + 'modified', + 'locked', + 'orig_type', +)) +->values(array( + 'type' => 'forum', + 'name' => 'Forum topic', + 'module' => 'forum', + 'description' => 'A forum topic is the initial post to a new discussion thread within a forum.', + 'help' => '', + 'has_title' => '1', + 'title_label' => 'Subject', + 'has_body' => '1', + 'body_label' => 'Body', + 'min_word_count' => '0', + 'custom' => '0', + 'modified' => '0', + 'locked' => '1', + 'orig_type' => 'forum', +)) +->execute(); + +db_update('system')->fields(array( + 'schema_version' => '6000', + 'status' => '1', +)) +->condition('filename', 'modules/forum/forum.module') +->execute(); + +db_insert('term_data')->fields(array( + 'tid', + 'vid', + 'name', + 'description', + 'weight', +)) +->values(array( + 'tid' => '81', + 'vid' => '101', + 'name' => 'Fruits', + 'description' => 'Fruits.', + 'weight' => '0', +)) +->execute(); + +db_insert('term_hierarchy')->fields(array( + 'tid', + 'parent', +)) +->values(array( + 'tid' => '81', + 'parent' => '0', +)) +->execute(); + +db_insert('term_node')->fields(array( + 'nid', + 'vid', + 'tid', +)) +->values(array( + 'nid' => '51', + 'vid' => '61', + 'tid' => '81', +)) +->execute(); + +db_insert('variable')->fields(array( + 'name', + 'value', +)) +->values(array( + 'name' => 'forum_nav_vocabulary', + 'value' => 's:3:"101";', +)) +->values(array( + 'name' => 'forum_containers', + 'value' => 'a:1:{i:0;s:3:"101";}', +)) +->execute(); + +db_insert('vocabulary')->fields(array( + 'vid', + 'name', + 'description', + 'help', + 'relations', + 'hierarchy', + 'multiple', + 'required', + 'tags', + 'module', + 'weight', +)) +->values(array( + 'vid' => '101', + 'name' => 'Upgrade test for forums', + 'description' => 'Vocabulary used for Forums. The name is changed from the default "Forums" so that the upgrade path may be tested.', + 'help' => '', + 'relations' => '1', + 'hierarchy' => '1', + 'multiple' => '0', + 'required' => '0', + 'tags' => '0', + 'module' => 'forum', + 'weight' => '-10', +)) +->execute(); + +db_insert('vocabulary_node_types')->fields(array( + 'vid', + 'type', +)) +->values(array( + 'vid' => '101', + 'type' => 'forum', +)) +->execute(); + +// Provide all users with the ability to create forum topics. +$results = db_select('permission', 'p') + ->fields('p') + ->execute(); + +foreach ($results as $result) { + $permissions = $result->perm . ', create forum topics'; + db_update('permission') + ->fields(array( + 'perm' => $permissions, + )) + ->condition('rid', $result->rid) + ->execute(); +} diff -Naur drupal-7.0/modules/simpletest/tests/upgrade/drupal-6.locale.database.php drupal-7.66/modules/simpletest/tests/upgrade/drupal-6.locale.database.php --- drupal-7.0/modules/simpletest/tests/upgrade/drupal-6.locale.database.php 2010-10-05 22:04:19.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/upgrade/drupal-6.locale.database.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ fields(array( + 'name', + 'value', +)) +->values(array( + 'name' => 'menu_default_node_menu', + 'value' => 's:15:"secondary-links";', +)) +->values(array( + 'name' => 'menu_primary_links_source', + 'value' => 's:15:"secondary-links";', +)) +->values(array( + 'name' => 'menu_secondary_links_source', + 'value' => 's:13:"primary-links";', +)) +->execute(); + +// Add some links to the menus. +db_insert('menu_links')->fields(array( + 'menu_name', + 'mlid', + 'plid', + 'link_path', + 'router_path', + 'link_title', + 'options', + 'module', + 'hidden', + 'external', + 'has_children', + 'expanded', + 'weight', + 'depth', + 'customized', + 'p1', + 'p2', + 'p3', + 'p4', + 'p5', + 'p6', + 'p7', + 'p8', + 'p9', + 'updated', +)) +->values(array( + 'menu_name' => 'navigation', + 'mlid' => '201', + 'plid' => '0', + 'link_path' => 'node/add', + 'router_path' => 'node/add', + 'link_title' => 'nodeadd-navigation', + 'options' => 'a:0:{}', + 'module' => 'menu', + 'hidden' => '0', + 'external' => '0', + 'has_children' => '1', + 'expanded' => '0', + 'weight' => '1', + 'depth' => '1', + 'customized' => '0', + 'p1' => '201', + 'p2' => '0', + 'p3' => '0', + 'p4' => '0', + 'p5' => '0', + 'p6' => '0', + 'p7' => '0', + 'p8' => '0', + 'p9' => '0', + 'updated' => '0', +)) +->values(array( + 'menu_name' => 'primary-links', + 'mlid' => '204', + 'plid' => '0', + 'link_path' => 'node/add', + 'router_path' => 'node/add', + 'link_title' => 'nodeadd-primary', + 'options' => 'a:0:{}', + 'module' => 'menu', + 'hidden' => '0', + 'external' => '0', + 'has_children' => '1', + 'expanded' => '0', + 'weight' => '1', + 'depth' => '1', + 'customized' => '0', + 'p1' => '204', + 'p2' => '0', + 'p3' => '0', + 'p4' => '0', + 'p5' => '0', + 'p6' => '0', + 'p7' => '0', + 'p8' => '0', + 'p9' => '0', + 'updated' => '0', +)) +->values(array( + 'menu_name' => 'secondary-links', + 'mlid' => '205', + 'plid' => '0', + 'link_path' => 'node/add', + 'router_path' => 'node/add', + 'link_title' => 'nodeadd-secondary', + 'options' => 'a:0:{}', + 'module' => 'menu', + 'hidden' => '0', + 'external' => '0', + 'has_children' => '1', + 'expanded' => '0', + 'weight' => '1', + 'depth' => '1', + 'customized' => '0', + 'p1' => '205', + 'p2' => '0', + 'p3' => '0', + 'p4' => '0', + 'p5' => '0', + 'p6' => '0', + 'p7' => '0', + 'p8' => '0', + 'p9' => '0', + 'updated' => '0', +)) +->values(array( + 'menu_name' => 'secondary-links', + 'mlid' => '206', + 'plid' => '0', + 'link_path' => 'node', + 'router_path' => 'node', + 'link_title' => 'node-page-with-query', + 'options' => 'a:2:{s:5:"query";s:14:"page=1&node=10";s:10:"attributes";a:1:{s:5:"title";s:0:"";}}', + 'module' => 'menu', + 'hidden' => '0', + 'external' => '0', + 'has_children' => '0', + 'expanded' => '0', + 'weight' => '2', + 'depth' => '1', + 'customized' => '1', + 'p1' => '206', + 'p2' => '0', + 'p3' => '0', + 'p4' => '0', + 'p5' => '0', + 'p6' => '0', + 'p7' => '0', + 'p8' => '0', + 'p9' => '0', + 'updated' => '0', +)) +->execute(); +db_insert('blocks')->fields(array( + 'bid', + 'module', + 'delta', + 'theme', + 'status', + 'weight', + 'region', + 'custom', + 'throttle', + 'visibility', + 'pages', + 'title', + 'cache', +)) +->values(array( + 'bid' => '4', + 'module' => 'menu', + 'delta' => 'primary-links', + 'theme' => 'garland', + 'status' => '1', + 'weight' => '0', + 'region' => 'left', + 'custom' => '0', + 'throttle' => '0', + 'visibility' => '0', + 'pages' => '', + 'title' => 'My Primary Links', + 'cache' => '-1', +)) +->values(array( + 'bid' => '5', + 'module' => 'menu', + 'delta' => 'secondary-links', + 'theme' => 'garland', + 'status' => '1', + 'weight' => '0', + 'region' => 'left', + 'custom' => '0', + 'throttle' => '0', + 'visibility' => '0', + 'pages' => '', + 'title' => 'My Secondary Links', + 'cache' => '-1', +)) +->execute(); diff -Naur drupal-7.0/modules/simpletest/tests/upgrade/drupal-6.node_type_broken.database.php drupal-7.66/modules/simpletest/tests/upgrade/drupal-6.node_type_broken.database.php --- drupal-7.0/modules/simpletest/tests/upgrade/drupal-6.node_type_broken.database.php 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/upgrade/drupal-6.node_type_broken.database.php 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,34 @@ +fields(array( + 'cid', + 'pid', + 'nid', + 'uid', + 'subject', + 'comment', + 'hostname', + 'timestamp', + 'status', + 'format', + 'thread', + 'name', + 'mail', + 'homepage', +)) +->values(array( + 'cid' => 1, + 'pid' => 0, + 'nid' => 37, + 'uid' => 3, + 'subject' => 'Comment title 1', + 'comment' => 'Comment body 1 - Comment body 1 - Comment body 1 - Comment body 1 - Comment body 1 - Comment body 1 - Comment body 1 - Comment body 1', + 'hostname' => '127.0.0.1', + 'timestamp' => 1008617630, + 'status' => 0, + 'format' => 1, + 'thread' => '01/', + 'name' => NULL, + 'mail' => NULL, + 'homepage' => '', +)) +->execute(); diff -Naur drupal-7.0/modules/simpletest/tests/upgrade/drupal-6.translatable.database.php drupal-7.66/modules/simpletest/tests/upgrade/drupal-6.translatable.database.php --- drupal-7.0/modules/simpletest/tests/upgrade/drupal-6.translatable.database.php 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/upgrade/drupal-6.translatable.database.php 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,125 @@ +fields(array( + 'nid', + 'vid', + 'type', + 'language', + 'title', + 'uid', + 'status', + 'created', + 'changed', + 'comment', + 'promote', + 'moderate', + 'sticky', + 'tnid', + 'translate', +)) +->values(array( + 'nid' => '53', + 'vid' => '63', + 'type' => 'translatable_page', + 'language' => 'fr', + 'title' => 'First translatable page', + 'uid' => '1', + 'status' => '1', + 'created' => '1298363952', + 'changed' => '1298363952', + 'comment' => '2', + 'promote' => '0', + 'moderate' => '0', + 'sticky' => '0', + 'tnid' => '0', + 'translate' => '0', +)) +->execute(); + +db_insert('node_revisions')->fields(array( + 'nid', + 'vid', + 'uid', + 'title', + 'body', + 'teaser', + 'log', + 'timestamp', + 'format', +)) +->values(array( + 'nid' => '53', + 'vid' => '63', + 'uid' => '1', + 'title' => 'First translatable page', + 'body' => 'Body of the first translatable page.', + 'teaser' => 'Teaser of the first translatable page.', + 'log' => '', + 'timestamp' => '1298363952', + 'format' => '1', +)) +->execute(); + +db_insert('node_comment_statistics')->fields(array( + 'nid', + 'last_comment_timestamp', + 'last_comment_name', + 'last_comment_uid', + 'comment_count', +)) +->values(array( + 'nid' => '53', + 'last_comment_timestamp' => '1298363952', + 'last_comment_name' => NULL, + 'last_comment_uid' => '1', + 'comment_count' => '0', +)) +->execute(); + +db_insert('node_type')->fields(array( + 'type', + 'name', + 'module', + 'description', + 'help', + 'has_title', + 'title_label', + 'has_body', + 'body_label', + 'min_word_count', + 'custom', + 'modified', + 'locked', + 'orig_type', +)) +->values(array( + 'type' => 'translatable_page', + 'name' => 'Translatable page', + 'module' => 'node', + 'description' => 'A translatable page is like a normal page, but with multilanguage support.', + 'help' => '', + 'has_title' => '1', + 'title_label' => 'Title', + 'has_body' => '1', + 'body_label' => 'Body', + 'min_word_count' => '0', + 'custom' => '0', + 'modified' => '0', + 'locked' => '1', + 'orig_type' => '', +)) +->execute(); + +db_insert('variable')->fields(array( + 'name', + 'value', +)) +->values(array( + 'name' => 'language_content_type_translatable_page', + 'value' => 's:1:"1";', +)) +->execute(); diff -Naur drupal-7.0/modules/simpletest/tests/upgrade/drupal-6.trigger.database.php drupal-7.66/modules/simpletest/tests/upgrade/drupal-6.trigger.database.php --- drupal-7.0/modules/simpletest/tests/upgrade/drupal-6.trigger.database.php 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/upgrade/drupal-6.trigger.database.php 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,82 @@ + array( + 'hook' => array( + 'type' => 'varchar', + 'length' => 32, + 'not null' => TRUE, + 'default' => '', + ), + 'op' => array( + 'type' => 'varchar', + 'length' => 32, + 'not null' => TRUE, + 'default' => '', + ), + 'aid' => array( + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'weight' => array( + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + ), + 'primary key' => array('hook', 'op', 'aid'), + 'module' => 'trigger', + 'name' => 'trigger_assignments', +)); + + +// Add several trigger configurations. +db_insert('trigger_assignments')->fields(array( + 'hook', + 'op', + 'aid', + 'weight', +)) +->values(array( + 'hook' => 'node', + 'op' => 'presave', + 'aid' => 'node_publish_action', + 'weight' => '1', +)) +->values(array( + 'hook' => 'comment', + 'op' => 'presave', + 'aid' => 'comment_publish_action', + 'weight' => '1', +)) +->values(array( + 'hook' => 'comment_delete', + 'op' => 'presave', + 'aid' => 'node_save_action', + 'weight' => '1', +)) +->values(array( + 'hook' => 'nodeapi', + 'op' => 'presave', + 'aid' => 'node_make_sticky_action', + 'weight' => '1', +)) +->values(array( + 'hook' => 'nodeapi', + 'op' => 'somehow_nodeapi_got_a_very_long', + 'aid' => 'node_save_action', + 'weight' => '1', +)) +->execute(); + +db_update('system')->fields(array( + 'schema_version' => '6000', + 'status' => '1', +)) +->condition('filename', 'modules/trigger/trigger.module') +->execute(); diff -Naur drupal-7.0/modules/simpletest/tests/upgrade/drupal-6.upload.database.php drupal-7.66/modules/simpletest/tests/upgrade/drupal-6.upload.database.php --- drupal-7.0/modules/simpletest/tests/upgrade/drupal-6.upload.database.php 2010-11-13 02:48:14.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/upgrade/drupal-6.upload.database.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ fields(array( 'fid', @@ -115,6 +114,51 @@ 'status' => '1', 'timestamp' => '1285708957', )) +/* + * This is a case where the path is repeated twice. + */ +->values(array( + 'fid' => '11', + 'uid' => '1', + 'filename' => 'crazy-basename.png', + 'filepath' => '/drupal-6/file/directory/path/drupal-6/file/directory/path/crazy-basename.png', + 'filemime' => 'image/png', + 'filesize' => '329', + '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( @@ -185,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( @@ -236,8 +297,30 @@ 'vid' => '53', 'uid' => '1', 'title' => 'node title 40 revision 53', - 'body' => "Attachments:\r\nforum-hot-new.png\r\nforum-hot.png\r\nforum-sticky.png\r\nforum-new.png", - 'teaser' => "Attachments:\r\nforum-hot-new.png\r\nforum-hot.png\r\nforum-sticky.png\r\nforum-new.png", + 'body' => "Attachments:\r\nforum-hot-new.png\r\nforum-hot.png\r\nforum-sticky.png\r\nforum-new.png\r\ncrazy-basename.png", + 'teaser' => "Attachments:\r\nforum-hot-new.png\r\nforum-hot.png\r\nforum-sticky.png\r\nforum-new.png\r\ncrazy-basename.png", + 'log' => '', + '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', @@ -395,4 +478,67 @@ 'list' => '1', 'weight' => '-1', )) +->values(array( + 'fid' => '11', + 'nid' => '40', + 'vid' => '53', + 'description' => 'crazy-basename.png', + '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. +for ($i = 30; $i < 250; $i += 2) { + db_insert('upload')->fields(array( + 'fid', + 'nid', + 'vid', + 'description', + 'list', + 'weight', + )) + // Invalid fid, invalid vid. + ->values(array( + 'fid' => $i, + 'nid' => '40', + 'vid' => 26 + $i, + 'description' => 'crazy-basename.png', + 'list' => '1', + 'weight' => '0', + )) + // Valid fid, invalid vid. + ->values(array( + 'fid' => 2, + 'nid' => '40', + 'vid' => 26 + $i + 1, + 'description' => 'crazy-basename.png', + 'list' => '1', + 'weight' => '0', + )) + ->execute(); +} diff -Naur drupal-7.0/modules/simpletest/tests/upgrade/drupal-6.user-no-password-token.database.php drupal-7.66/modules/simpletest/tests/upgrade/drupal-6.user-no-password-token.database.php --- drupal-7.0/modules/simpletest/tests/upgrade/drupal-6.user-no-password-token.database.php 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/upgrade/drupal-6.user-no-password-token.database.php 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,10 @@ +fields(array( + 'name', + 'value', +)) +->values(array( + 'name' => 'user_mail_register_no_approval_required_body', + 'value' => 's:86:"!username, !site, !uri, !uri_brief, !mailto, !date, !login_uri, !edit_uri, !login_url.";', +)) +->execute(); diff -Naur drupal-7.0/modules/simpletest/tests/upgrade/drupal-6.user-password-token.database.php drupal-7.66/modules/simpletest/tests/upgrade/drupal-6.user-password-token.database.php --- drupal-7.0/modules/simpletest/tests/upgrade/drupal-6.user-password-token.database.php 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/upgrade/drupal-6.user-password-token.database.php 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,55 @@ +fields(array( + 'name', + 'value', +)) +->values(array( + 'name' => 'user_mail_register_no_approval_required_body', + 'value' => 's:97:"!password, !username, !site, !uri, !uri_brief, !mailto, !date, !login_uri, !edit_uri, !login_url.";', +)) +->execute(); + +db_insert('users')->fields(array( + 'uid', + 'name', + 'pass', + 'mail', + 'mode', + 'sort', + 'threshold', + 'theme', + 'signature', + 'signature_format', + 'created', + 'access', + 'login', + 'status', + 'timezone', + 'language', + 'picture', + 'init', + 'data', +)) +->values(array( + 'uid' => 3, + 'name' => 'hashtester', + // This is not a valid D7 hash, but a truncated one. + 'pass' => '$S$DAK00p3Dkojkf4O/UizYxenguXnjv', + 'mail' => 'hashtester@example.com', + 'mode' => '0', + 'sort' => '0', + 'threshold' => '0', + 'theme' => '', + 'signature' => '', + 'signature_format' => '0', + 'created' => '1277671599', + 'access' => '1277671612', + 'login' => '1277671612', + 'status' => '1', + 'timezone' => '-21600', + 'language' => '', + 'picture' => '', + 'init' => 'hashtester@example.com', + 'data' => 'a:0:{}', +)) +->execute(); diff -Naur drupal-7.0/modules/simpletest/tests/upgrade/drupal-7.aggregator.database.php drupal-7.66/modules/simpletest/tests/upgrade/drupal-7.aggregator.database.php --- drupal-7.0/modules/simpletest/tests/upgrade/drupal-7.aggregator.database.php 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/upgrade/drupal-7.aggregator.database.php 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,149 @@ +fields(array( + 'fid', + 'title', + 'url', + 'refresh', + 'checked', + 'queued', + 'link', + 'description', + 'image', + 'hash', + 'etag', + 'modified', + 'block', +)) + ->values(array( + 'fid' => '1', + 'title' => 'Drupal commit log', + 'url' => 'http://drupal.org/commitlog/feed', + 'refresh' => '3600', + 'checked' => '1347209523', + 'queued' => '0', + 'link' => 'http://drupal.org/versioncontrol/garbage/path', + 'description' => '', + 'image' => '', + 'hash' => '84f57ae5bffa7fd56942a6293be91244d8551cd18204a7c7de6a17065ea4d54d', + 'etag' => '"1347206975"', + 'modified' => '1347206975', + 'block' => '5', +)) + ->execute(); + +db_insert('aggregator_item')->fields(array( + 'iid', + 'fid', + 'title', + 'link', + 'author', + 'description', + 'timestamp', + 'guid', +)) + ->values(array( + 'iid' => '1', + 'fid' => '1', + 'title' => 'Domain Access: Commit b904022 on 7.x-2.x authored by bforchhammer, committed by agentrickard', + 'link' => 'http://drupal.org/commitlog/commit/2%2C410/b90402243b4a9dee0d2e2c4a729dcb2f58dc53c0', + 'author' => 'bforchhammer', + 'description' => "
      \n \n \n \n
      \n
      \n \n \n \n
      \n 1 addition & 1 deletion\n
      \n \n
      \n +- \n
      \n
      \n
      \n \n \n \n
      \n 13 additions & 1 deletion\n
      \n \n
      \n ++++++-\n
      \n
      \n
      \n \n \n \n \n \n \n
      \n
      Patch #1685658 by bforchhammer. Better handling of current domain for Domain Source.\n
      ", + 'timestamp' => '1347206044', + 'guid' => 'VCS Operation 3936918 at http://drupal.org', +)) + ->values(array( + 'iid' => '2', + 'fid' => '1', + 'title' => 'Video: Commit b0b7ff0 on 7.x-2.x by Jorrit', + 'link' => 'http://drupal.org/commitlog/commit/846/b0b7ff08fed89c76454aa54627cc219361365d7b', + 'author' => 'Jorrit', + 'description' => "
      \n \n \n \n
      \n
      \n \n \n \n
      \n 5 additions & 5 deletions\n
      \n \n
      \n +++--- \n
      \n
      \n
      \n \n \n \n
      \n 21 additions & 7 deletions\n
      \n \n
      \n +++++--\n
      \n
      \n
      \n \n \n \n
      \n 31 additions & 22 deletions\n
      \n \n
      \n ++++---\n
      \n
      \n
      \n \n \n \n \n \n \n
      \n
      Issue #1492296 by Jorrit: Added support for avconv binaries instead of FFmpeg.\n
      ", + 'timestamp' => '1347206397', + 'guid' => 'VCS Operation 3936924 at http://drupal.org', +)) + ->values(array( + 'iid' => '3', + 'fid' => '1', + 'title' => 'Remove Login Tabs: Commit 6e1eb5a on 7.x-1.x by highrockmedia', + 'link' => 'http://drupal.org/commitlog/commit/41%2C610/6e1eb5a4a952db3264e7696e840ac3d797f4b477', + 'author' => 'highrockmedia', + 'description' => "
      \n \n \n \n
      \n
      \n \n \n \n
      \n 10 additions & 2 deletions\n
      \n \n
      \n ++++++-\n
      \n
      \n
      \n \n \n \n \n \n \n
      \n
      Updating readme\n
      ", + 'timestamp' => '1347206401', + 'guid' => 'VCS Operation 3936920 at http://drupal.org', +)) + ->values(array( + 'iid' => '4', + 'fid' => '1', + 'title' => 'TimeGroup: Commit 6ed4c08 on 7.x-1.x by Sweetchuck', + 'link' => 'http://drupal.org/commitlog/commit/40%2C448/6ed4c085e5d9a8d33e091e1b8a65c73eab2dc99e', + 'author' => 'Sweetchuck', + 'description' => "
      \n \n \n \n
      \n
      \n \n \n \n
      \n 1 addition & 1 deletion\n
      \n \n
      \n +- \n
      \n
      \n
      \n \n \n \n \n \n \n
      \n
      CTools UI - Wrong default value for timeoffset fix.\n
      ", + 'timestamp' => '1347206533', + 'guid' => 'VCS Operation 3936942 at http://drupal.org', +)) + ->values(array( + 'iid' => '5', + 'fid' => '1', + 'title' => 'Domain Access: Commit 1140172 on 6.x-2.x authored by bforchhammer, committed by agentrickard', + 'link' => 'http://drupal.org/commitlog/commit/2%2C410/11401723f5c5d11032dd141ba4939ed889a7a915', + 'author' => 'bforchhammer', + 'description' => "
      \n \n \n \n
      \n
      \n \n \n \n
      \n 33 additions & 1 deletion\n
      \n \n
      \n ++++++ \n
      \n
      \n
      \n \n \n \n
      \n 28 additions & 0 deletions\n
      \n \n
      \n +++++++\n
      \n
      \n
      \n \n \n \n \n \n \n
      \n
      Patch #1685658 by bforchhammer. Better handling of current domain for Domain Source.\n
      ", + 'timestamp' => '1347206541', + 'guid' => 'VCS Operation 3936926 at http://drupal.org', +)) + ->values(array( + 'iid' => '6', + 'fid' => '1', + 'title' => 'Domain Access: Commit 19b1c36 on 7.x-2.x by agentrickard', + 'link' => 'http://drupal.org/commitlog/commit/2%2C410/19b1c366d86cecd8a9f6e1a6e835c0566f5c02db', + 'author' => 'agentrickard', + 'description' => "
      \n \n \n \n
      \n
      \n \n \n \n
      \n 28 additions & 0 deletions\n
      \n \n
      \n +++++++\n
      \n
      \n
      \n \n \n \n \n \n \n
      \n
      Adds new Views file to Domain Source.\n
      ", + 'timestamp' => '1347206601', + 'guid' => 'VCS Operation 3936928 at http://drupal.org', +)) + ->values(array( + 'iid' => '7', + 'fid' => '1', + 'title' => 'Domain Access: Commit d2d5456 on 7.x-3.x by agentrickard', + 'link' => 'http://drupal.org/commitlog/commit/2%2C410/d2d5456cad6ca57bb72e743da6a7112a74d7a331', + 'author' => 'agentrickard', + 'description' => "
      \n \n \n \n
      \n
      \n \n \n \n
      \n 29 additions & 0 deletions\n
      \n \n
      \n +++++++\n
      \n
      \n
      \n \n \n \n \n \n \n
      \n
      Adds new Views file to Domain Source.\n
      ", + 'timestamp' => '1347206620', + 'guid' => 'VCS Operation 3936930 at http://drupal.org', +)) + ->values(array( + 'iid' => '8', + 'fid' => '1', + 'title' => 'Skarabee: Commit 400b519 on 7.x-1.x by sboersma', + 'link' => 'http://drupal.org/commitlog/commit/23%2C278/400b5190f59b1cb58d6b27fa10ac668e9580aa73', + 'author' => 'sboersma', + 'description' => "
      \n \n \n \n
      \n
      \n \n \n \n
      \n 3 additions & 3 deletions\n
      \n \n
      \n +++--- \n
      \n
      \n
      \n \n \n \n \n \n \n
      \n
      sboersma: Changed variable deletion method.\n
      ", + 'timestamp' => '1347206709', + 'guid' => 'VCS Operation 3936932 at http://drupal.org', +)) + ->values(array( + 'iid' => '9', + 'fid' => '1', + 'title' => 'Config entity listing plugin API: Commit dd3fa73 on 8.x-list by damiankloip', + 'link' => 'http://drupal.org/commitlog/commit/43%2C586/dd3fa73b0bcdca833bbde1d1ddb3cefe42003693', + 'author' => 'damiankloip', + 'description' => "
      \n \n \n \n
      \n
      \n \n \n \n
      \n 10 additions & 2 deletions\n
      \n \n
      \n ++++++-\n
      \n
      \n
      \n \n \n \n \n \n \n
      \n
      Added tests for getList() method\n
      ", + 'timestamp' => '1347206738', + 'guid' => 'VCS Operation 3936936 at http://drupal.org', +)) + ->values(array( + 'iid' => '10', + 'fid' => '1', + 'title' => 'AutoSlave: Commit 76891da on 7.x-1.x by gielfeldt', + 'link' => 'http://drupal.org/commitlog/commit/42%2C968/76891daf3cea9c294daf56a26760cb1bf33ea58a', + 'author' => 'gielfeldt', + 'description' => "
      \n \n \n \n
      \n
      \n \n \n \n
      \n 10 additions & 7 deletions\n
      \n \n
      \n ++++---\n
      \n
      \n
      \n \n \n \n
      \n 10 additions & 2 deletions\n
      \n \n
      \n ++++++-\n
      \n
      \n
      \n \n \n \n \n \n \n
      \n
      Keep track of affected tables per commit.\n
      ", + 'timestamp' => '1347206751', + 'guid' => 'VCS Operation 3936934 at http://drupal.org', +)) + ->execute(); diff -Naur drupal-7.0/modules/simpletest/tests/upgrade/drupal-7.bare.minimal.database.php.gz drupal-7.66/modules/simpletest/tests/upgrade/drupal-7.bare.minimal.database.php.gz --- drupal-7.0/modules/simpletest/tests/upgrade/drupal-7.bare.minimal.database.php.gz 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/upgrade/drupal-7.bare.minimal.database.php.gz 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,172 @@ +PNdrupal-7.bare.minimal.database.php}6/>n9vP H/3hI{Z_$"& =~.]xc;~/F>;R(ٛזEԋ:| HYEXuỮ\ƥa"oZ1]$~pq/~/y;#? 4F4o0+q/Yçĕ6aqlsn͡\3baϞ+$)3FQL76q==)֒ϞE.%+ #m\|IMdFu]׵Z`\Mb}:fHX(`04"fM?'\S.1{f<[QDK_Ĺ>KdTD)+G)čfp +a׼װ@ Ϳ<_Oޱ>=9R[I\aL<xIs Q!k>(.{t+C J ~A7ycN܉GinHeHvs[黃'Z򇷒Pn{h{/x+ď5d.h™; +b)#H< +퀆!v&hm!kv=t|\Z~JDYJ&zsQ+Twl#l402!j=m.ɴ8ܼxΟvPhWwQRet[3ٔU´c,ݬUB?CtKT2bFa^LY&ldA<3F8+j7oջv: R8bܝ(C`,/ ɞj;J9^,b6*1&= bzuzzڄ:Q?8c:Б@^ l,-"':.I"qj~nrlhU*ޙJHB["h*>[)g)\ QNTfZ.\.Ž)7)!GK[bX>[ iU.c + {"'. r>eC~$Ve' +nN4ŷSLlFfϮ흠;!.pA0_8Ax#= f [ j?Q0m MA4n +M:!Է %vd M>{㝭:FrΑhӷ5\x nz~"1VHpV#‹+ :s6lGqcf4QdiHwH)7c葀Q w&vgNhN+duP3Ԟ;FKUcM~F1FUUF픈ref*S]uE96$7/k'8tGnKv8ch(pE5sEe?V\atW\g?z( {\أFiد 7eճnƅ7I.gQV~h683ި8k9OC_8O3:;Z/q'F:Έm1pƅ3)ΈD*ĽwqRK$D}nLF;tS%RJ(_ФJU#jDX@k5B}Jcd߫F*^ާ-Tl馭2\yS%9jDtHh,㳽+\xJHȃ7V"GݴU"ED!~90KD>ݖd\hNX`Ԫ0Gk%RwdȐLGV[ 6,[astJj|] 6Iwxh1VEm2?Vo흼cBN + xy5GӪPH`;i=PqA.m5}nV)H (hɬ +M8.iEH\ZFyfaQ1+W<ьofp8]EQ/Aޛs'ʧIߒ8WfA*oC ~$R)\< S61v8ʴnޒNKp8'l;`圴D"'=asZr5z`& }ON +D֭(g"JYdĩ;H+GNl†{zigcY-PZkqEPT2>޳%nY 8A  $̫h!Õ<\ 3"t'X=N7vϩnPm F䅙O^bTS{~,C&yS/[)/IHȜ# ZՔxyN V{q|NCiBOMP 9W`bb :{:4}2 NQJ&Žt]B[I䬽< ++*+Ƽe̋ U툸x9q.ۜ^$%-ɝ1R. +s}EߺNSF4GuwZhމY%r$dH#j@) JSKS+U* u* K'VɞuQbi}vl{,GD2|Rs>5Oj6r5R^FsLCo91':wQ`(֠ϿH%%cؠ7yhRT/I8k5-ҕ&Q6]o@~!?r]RVOOtMkztǧkzr'k'=;D}9n !!&uZrVG#(%A$WNdf )kL~ko]=~{,Hm 8ȕFrv9.e6FbپNN7 ]x[wb{E2~X:W"Ƀ˸͂\/;z*dΰLVeK0{id/Gqrf.e l R:> +$Ey % +]yUW/[eg4u<9#νĨ)2fa7? yWk?JZc/qv]Q[6Ch}o X8z-Ɵva+EBm3sC%9A$ozMI Ujxˌ +uVE MW/S[u)0 + o%Z/^~=9ԮGiX3ڸ ֒=8Mj)pEi9/YM] X3֤%RTN *5[cP o8 Pr5f?DV&gJzGV,|&LDT NYlϵ^w;XT3|Y&7#N|jkZG@\iaI2C-DgkWhx+!I+Zhq[(CDRC{ЬzQ֩ >`Uْ:y\&jn~phFv c|j /'UA^rJHBNҬv{r<.Ke;!\4>Ws1ir3<1m]AT2ux F^[\"F3#WӅ8r&aJ1'Ls{HXm`cE^jr-CI ,?"Vp)\b?YLׇ +Gvq"~~R<>E}Dڮ(s.5& K,[#N(a9kB8*놩fKʏLrꮙfq5c鞦p:YyI"Ɨ MMV-Alx3qW'qjN%3VGB<;n,\:YQVv.pҴ,@OburLH\QX+Ċ,腮+[U0RP1ʝB +(d-Ua32D( 4D3?mђƎΧJ;!2/H|Y73GVR# ZAE r76bP-UxZ"Ydž1®J<'Е1f5X!^n-7 +ɣ'V`"fyjUC[^KŽM^I9AW~FN!UNr<~NJ(G,XxjAD@DOFO1QJ.vAF*[XR@ݺᬝR,p3\1 NebX@{ʭ{ʮH1`j"ڵr{#cPCGCDt@h~/`!ZS[)DjXXXÛa <2B.!fˀql:Y ,o9.pth"U/B.WaG܍kmE0d9`?TG%d>V,KbR=೸$olO,0` +Ρ.֪1fIGŤݏrF֓a= KH򙚺E,R.ĻR@*yk:GQb8>\Cww3筟]^/k#}p4vy"3nj4ȮHs[<"5o Bc4Wrf +VC4"] +&Qxϓw~[?SW>8u@:׷`KwՖ#٣03Jp]3uG͙oódB?B>+r#!W-n.2Göp ` +1̲||Ԕ##@7ڸRtѡ\0mF!xp-,Kq_T5vSXO{(4qv`}O ׯI5u ˔##7\-1~7wM^7ƑqvB(嚰dM K)JxI-u0-2?P3{bR!nun/^V/q.:q?uVK(TP|In(q"|qB/_ł!4ɅϕpPsBq?OE:cTh҄U[(xٖjce)n; +=RaFU.=Anϝ[rLnF̠7_֏?[H2Īp[$/ºQv}&c'|٧_B(?t^)\??)D&S1sT [7S +-wq~y=MaGb;X; Km<&ĶꂶQt`{B }Z78$?؊o`iT3^aDenϗኇΫg4Nx3·ysƞ,M=gSTd={fٚ}'u'&N9"%pq^ i6#IQƎӒV;5y(|m'S(:v[W ~|H$bV$V /NR"i?Vgu'vLIeLIfkvlLvggJqMWX*Gu x:po\-OkNW*LA jTM.zo {u0nۉ{XK >Īߺk{G:~Ua"a@{2݈ȸ- %RN jғ-MtQxe +yoTOBo-á s5A!3Wb` aAI@XVXJưW*~l@4MX„-"J"Om2N2#@@.Z'^R| a-ȷB:paaoImR/`2`l"H`.Llu8k?Ceu-Nz zj;q^BaDMpb +ɷxZ_<(ɩF L܁7j>{i&@ԺHP|[椶-ǿeA+/d 5$z9}7nϞ+)um>4Ml0z&f.XK0zXK{fmϏoaᏟ.!=_l<+N!϶57$IJh`)j NP߹AA7y.k>}DFGhSyc>rD;B hbm۬L&-{Sp7Q*D*6@R:/])ALԛP$"(4!SFv/ҋ.N 4٥-1XvdvygaTlV2x Fl-W '*Ub>s<;ƿH]\sZ.BΖ4ž[Ί'XnϧU/Ӕ/^Kx! 3_!yIkyC+nހoBh Mo9v;MawbcHv1+5^+&d9?41JBF+ 9tRR/=+0VK漤by|Ĺ o00 +X 4zl%csIzNN,q2r{Y +/ql.zN=a(?Ar/B rɭMg+tL*GE J/ʨg0˴\Wךyzn +`iVX16A0xpUB.+|K4KKhbY]7Yv`g0pW]?,ܳcSDU.b MԻw<^[.d ^.[HYV+Ne@=*B sJžZⰐG|_/1B+ Y<(BY>s%AP(hm⸉5ᵆ#(xTHW1n~ +G҈m /u]r^O)@:5<8.{0S%nuFXߢ\+:p(U Us1 tIo!k9{T%+6(W-V%{k syԎoB>ŗo1?K(^WCcS lAܼ70T=;WލryGqWz!]9-rRe.QЛMnbSa͊q + +T("H$O"hvi[nѲ-^$=6oW6!X_. 6ќk9.NWTh2`4/ilyp,@έ& |V |(,1<xΌR_a!zW +#n2 P.a1+"fu$2C/SԃʥNZb +%ԕB{1\ƅ=0bxBt!uygfЂ@tܵ_6JfNF?`-%✉9/N>7{>m̢ʢ&&=|/'*{tΎmƉE<Ťr^}rQ"y!XPۤg6`|Xp7 ~B>CTJLOō7zi:BT.cȮZE ."E\v* NAWN'lNH?dl/cLPn"[s9&`۞#TLMTNxF~<ï7%*}^ |`O2`T=* + +g :s[*_hHsC y`knREY boXT;q@*-M=QC7q)gj&%~\/[(O92S~]/}K &"WK\e,YbRf13a^o23fU*mwByRÖ$W̠ fΤq;d&ˋYjT*kj^ܳp*_%|J ?.P]mF[OL< Ao?NY7k^}}wp B+.SgҚǵ;Zz|ȇ @y_ofz [ϡ(ˀ.}v /0Կ~+ ^Ō+Yl%&XR(w8 ̰W_>T,)X x!mvV7Sٕ)֠ta8,kD$v JcgeñI^K_KH4&o<0\I/J!k]'o hnv%7bP +KoJ6Ljg5Y_3f~Rk`K80fIVجh$;lVj$KlVmR[lc3̱Yl$l&,͘d H̲)0e⸳y=Er3rc(ׯWGoK Ά ͥgY!Ke)LJ3?g*t\Ȍ*<{M 4m6a%G 3*+sbP$a׍iv2|jn:'5!>w%eEle@݈1#5ǒU~̴A&*Z96ʙZ<#]6 +AbEBEOn)Cb>Uxhj^ІQQ#0ݾ([c_[#=65%F舿f1M\31YZ:!TJ tI@rjMrII1Rr&Iq&wt RKLV^̗FK怯o,ۨM\~KSV tܚX}v,xf$(q|7eCr: + ]>!>iyH706rوd~P-dJ@wcy$6[7|">IdeN|/wN2{fO'fO2~ԛݓa)~L܋ORocA8qj?W8C،~X_cɯׁСs0j + i-~t;;mPōFwď)FO4`Seq^ROZ, ]KX] dh3F,>Xr'wl͓]OK3Aݟ 'pEm.^X~{"ކkR1pc2P/H]WxK%8F#?6k!_(@:a)QR8;]#rz;:SMb}1}qlU2x+7,_/z0x$#gy5%y\y`7n?9W њEv< &wg,{ÑGuo +WXiGMLc2ꄹ| xR+IFkxALq:ep0΂7&b/n+#xʅC] xMPn7>er;W/v匵;#?\ᬯҏY\Q~}#rg:) $ݡ闢t!dCp47 0E߉<ހvbg#S6W<-Ww+xpgƁkS-Ji,~u6~3Nhy ?xpCWerEo^ci ATY<,߿ոX@G?fZ=,8UzaqɹQ֔8ó0YE iFm) eH0 UxAiE#rj$.?>6)7b%D3 o74Ŏ7֒Buzir?Bߛh9a` A(Kpc#}JDBQS))엨qz>KTSzl z1]'\c_)#R;uT3x}wyҩ$T9;woC*T֘F9xl1*߸b𰬓f,XG[\>S BcdLXU;[a)mͱk4%^ sA'܂} !Lf ~˔H3[?.y7?VF5&W0XXz9~ {x|u4DZˎ,x| /vay~kPnb#+窍s=W6=OV/.@4D,ئs*iSp;݁6$(09RYʁL\V/3ђD +\'-Ezv'kg?p OdD^R,b+ J)~+χL#k;qRځ!H~5&<Nḱv;{ֈoy%!/}@{~m#\ tM,wҵI"ab| }D2!Ϫ)&_қٳ=~Ͳ pKl!j4@z ÈL,[sX^,QԠn'Rٝl3d<.Û>X8whBqQ;ؾءhe("1 {[VҢdXD Yj1' :9?UEiE>qfQwȃ?еtz{J&fjAY(v;L~sOq4D7a6ڇv@ G~ѿin0]v~ٰp_])>♀UO1G9Á𼪭ҟ_ϙ-oM(RVߚ>A{$-)C@IŘHz̸ebv_)N?Oƞ|no!t|NƛsB>; 27b!{1pղό8.KS+YyOߐ!xq/`ͬ~FMB-wqᕎN? Ǭs$}P2&٣fY/hd-%nOrţbRL+5|O'+}ٚK㩏 mnh<(81är?A3fhc?\|CF%Kc>`cE8 Nivtn02sŒ~^u}<9uKl:O #3:ssNvc#K v>jPH#돾rxQk@7$@kJPq:б1`{@y}Jn'- +7Ne'%*:2mo<8Y6Uٯ]W*vy`חY/#Fx}-B?{4`UwS([:\4Tx^w +-[8M90\BMWתxDŽ <¿B]IcBR..}+}Rhq ύq 䊠PQ ^_DZ +%&(KJq0X ` + o@d2tn»qك*q3:_^ցGc1ئ44HU~e})lo]Rbs{ ʱmƷm] (7N>+ \*p*lxMZZ<ŋҶإm`EҶx(~J\_ٸ!GD&}JWTh2`4/⩜ο(@kA s7pyiCacs<[;L ^t_6*ɔs%ºK{VFNnѤд6!w.O5=z$pIĺS>,y,Jii'~b-WjF5ձ t(#1MY<*\L?6iw./1]Aa}\_d*j%xXT^&էFrҼ̤A YNjddiҢ! yGQA)j]LTxMO~Fzoe, pvXI!bڃ332@9@o,1FZ_$G \0 + +gLS r)*0pɪ > > > > > >{d >-]Γu"p] /K\ LqIaLG\RX],"Y +VXV.Փ0%^Y nqIvIvIv1П~Iު>M$rN}^L2%%%T0lc؟">9>aQg>Mғ@,K<CDTGd؁ߗ{ֹ`>3BǺ ]YdJ|uDZA +n.2uʆSbm'jb%쓌7눑8O̗.ⱱ9kT8|\3'LxL7*f_urAZ&~<< .h:t-Y!.7f6^[ .Ne 6Id-iu +y;,@)uCoyUowz9 5G|O[З2UrZp`^C8*a& +}1w3Eg_dC5o|{ +MPI;?p&F'RH駶AnڦSw{Yu)Yyظ%+.e(7yORֶAΝv: +VH~i^ #nMk%'GsE5?d!A%/~w_N=6_<=wz J uq=:%@89ciPkfQ{8Hށ8ra(CΏ^үrhvX6DU$߳>Ջ̛mB\bRl$o:颢2<']/- _|YGQڇgWSNݝ~Ó] 4?ȗcpeHNI&zVo_Uv8oeic; mC):_һ@tn=rA5VqVBd)H̔$8Ɵ/z8s(QP}`tVw5iXߞKiw%WMAnj +<=,O{O 7vS7d[-0; $bSWI;K`۷rK0!*Io7,Zw}21bf4]zkgJFnoGFC-7^e<4 *MbˠU_!,>6O+1+Wςǒu4워NEx|$1 +"z^[䴓\:M$HJEjzb ue/if:'2g&-_3%$aEڌ=w;.]ayo2Bu8ѡ\={\}|u>D))>=nοW;{}xD⦣$<ݱ(yR&7s ?t:i&a v)]&2~P!(??7xG?+JQbngdK ;;:_O4Ee֣37GiH#v9 _G$g&-js{n*8Y 6}gszӥߋ\JC]Irϱ2-;O:}I烊ܸnÖ!vkOlI+U?Z %޽ֲgϲĻߠ+ܳKa4TC7f8xݷ +~2Y=1FmS/tI=PE6(EȪdUM\R;n].5cT%_$_fxޠBC_f_[U:'Y8)˿_Qʚ)m`7KMM +xx'X` m,dd(&glPYy%FR"|&ZA}hڍPlgց½3`Zi?~?}3Kz¶ -{ĈM8vxu)ﱔ4q @?9zE\s. %@< +%h32D,AN iK?\K#)F7/2HAXTšMשLxF\4+PK(ȼ3OV,l ~ m!._O}m,נT.}! +`P֝Lj\ùUt)9)Cʭs`} HJnQ튤myp\9No2nE0HHqKiͪU.[{C vSCT.1[f6aƻkm†{*z (^WB*\ͱQ$enVjdEȩ˯e7C8KvfLۆJXҎw/!Ŀq?+sXj)wN +F0(.$HtwpbDGSJXvqԫGb r>t%ԊxÍ<rŒuCߘ<  +\\$قV`Hd9#ab&B|$ k]< +82^o~]wtO`?I{އ[u{l9O&?ɷ1Y}vDWB%x8Z{Rq ς1݃L]G,{XBSNtJOZwܻ5Oߡο߃2׿;0+Vdf/莴ɧ!_5>8jFnxHvNahdJvHhD(^cƟWD9.&a%N< S# ?!/vEY?m:'0ڦs2#]HajDoP{B"/kױ+?'l9}&JoDz~LB}6>αPԄ[A+ۑ4>; ws??]b=xhJd+<˞8gѳo4pɺAgFݟaPJXSr=>(2`.}cc뷴;q CISx{.~0ގә +KxߞщC7KeD ЗQ{;"As-|J!T* 79h7_㧞(f'QiDm=';g1wNs2vuM!25l5A[X$u5ŝxĝYzxF{lVa_߿$]̸]qGv^q) +$ cogS x=B?7'u{RWnc"s+v"܀jHB:  *M˶wYDXe4 "?Ps1]a7J4Z~`WKJ^nic#} +{ȶhz}6 ܓ4L _XavK;EY2k0 0atGbo98N(xGv%#g]b^W;5 <&/wg~np荿D"ysO0^Ҟ5!فM s`r<ݜBj C8$pd+CvgH]jE'=TKߵin;EFC([W"z<ǠDg]2D䡒vJ2&^<ۼ` AydWs:?9'6?П^B[~QTIZ +OzNꡡ79F9v矴47xq+s$zN'JO~C\$qإwx'-PZv +fRpc'9}M♨_q쟊Ƣ),h6n^/@{'Z{ng랾^K;& zG|{ΗtL!ݒƚ`w@ +ļބb~&lsn1#pΦ޾p8Ǧ=W`S !{Wr3OTD"`a$=?R벂<[ps;af?ց['XiO:v~xDAn_jD*g`EiMLҝƓDusdT][ؚ Э ,ҡn.5}Jp00Z򥌎1=!S1nCu84u'=Zє)f[Sݦdamk0nMy)-CK'6YX#2#cݠ]Ux@>kd4], jlmai7ꐴ%^SF.xdðĞNM:@H#&hpÑmӶĘ$s+aㅽT c8(MI:hiPS5m2 f¸ T ɭ:iM c`M-d0UsfNDU5bO'1wA{r|?B{]F@Z޴4Ek2jZ 1-lm4ҭ)T^-}0NSqJ]J٪n,jp +=de,bMu0.c h@ߌ0dPuAeLFUuS:. ݰT:\S}1]И}4 +8[/F}9Ym +0ddӆOAiS8BnnMӉFu5Wl3%h</t8^LFcmOfԜ55wOgu6l t- A 8~Fh:cu15c5)EsM2i#ˆS @G6!h)%6- L0>YkRbY:% **ЛtdP} z|8- +u1RcA ykd@DŠpc: hTS?p0aYń9ԁ:dHO& Uۙ0>a]ː*@>xlhMQbۣ)U fZc$EF,ec {V' UJnp9ևP`A +a aXXD&Ӂiڤ$DPA@pA5j=8VUu20H ံV-(6T[6RA@Ӊ1bv.ؠ- (n0#PwM6e Nt]覽 x"hGi.(쁩>p:ѧtA+;AˍZ1`ԜбScD@! Sj@f` B5:w&X͢`,ЎcӴ0s@ LjaY0 kRm^ ;!j!=x Vx46tu0d`|OHVͱ  \`ZS*8-7)Q'9MJdlh_ `ژ`.v'/Ɉ 4@t,v-sji3 & X2邂m.Ĉ[,4V'M' :Ms`G] Lz c01˜D洴xj`z$I8c:xhhj`g&аNFNPOFH|xh Lv^`PvP).  8m-Զ Àwo*y@?s~2`;)7b&#m +_a& Fzur1XYS`C06)eG &;hрC82M\Ok?I4caAGLMm : 6=(Ƽf=pπ<B` b<?5U 4QaDFtjU +A0:PR0Ü>Yp-0Qdbf{ѱrVc7:d0X(Ǻ +:Ն*x55DSs@״FK-})qlI0M HGyFFB 9д֚.K\Te8ZLp5з=D + 0(t߄&Zfgh6RE:6&~M5`50- um<o[T挡&ôc  |bOM1*=ЦCpAAfQcP +S~*C,/F0ɘ!BmP- +*%715) chbh ˭[:6GdO##:i@ns͘j5㉂l00v[BX-x1Ku bt0͑m@=,px>gxںGbRzwUAB$0e4'@`h`ph0jc #Аg9UfKLmb냑54`NZO8_t c̱fYSY=9  bč)Zc!KMZ P'Ycv(h0/XR1b `}a-iA!WJ 2@D0SlSA }Olp`=1](T#:jAk8`4SL膱 +|t66ϭ͉?Y?XmcƧ!-,djid2 ӄ ˫5۔5 P1,P}A_A'jT5 Z6_x|Ҟi|g[؂ت"ZlpUP1 R'xxPZ8fk+mK *ƶji ݜ (.T영 ݈Se<lړfšg?˅X0,T +tlL}aVd#wc, CxZIT̉EAZhM:ݢ@j `g  +hSfm'v.A s&T"cu~52wC Դ5:Cu\'la6VH7Ǡk:Ä=Gd\u0`M(ke +_@a!8].(g {+Sld +rVtL֓ %evAa Xmkb2dfH71Pim`K 1&`1i&dN> \[`lÁh ?̉Z)sBViR  МnZĦ&`IOI|ӠD ֲ7c zD# xHlddW̥`W8M}@-`[ڨxLX)n `  03N]J + ' _hZC+ )DcpSKIoi t( I):3@Um@ |GK_ ǺpPgi +&ɱ{o)9# n,{`дA t`4Mx0X#@෱&(?c^b)6 +241(2i.QxXw?0TP`M@4eDɻ$SD&xզ^v+*x'd8BxHpDP +oX/)Z0bJv<S.*N~pux>a74!9`É +f<0Db4,@"́t0p (د KYQ @nF?N"n09Wh<XTF=3{X'St<Љ +DFLc86&8 T])Hu{F:żjLƺ +z +M&CpPqmU`8Q5`"5չui + m,s6p +j6(A@1YѸ-1}uk^q ]6 ` 07`RRolH-\c b_6L̀tpLPTڒIHi{hY0p m}F 0 Pc`:Z?ޒd%C':p;0:0 x.vQjXĦ`"}mGo,  -0 sS }H'JWsb9j maL 4`UE!F@P4642M\Nx4Npx/n.MufxΊl`yr$PKAqQr*l`Ͱ/=qߎ7ۅ8v"ouOJ6n{tu ^[}o[ޖ&Yܰ_*hv;UQ爳Z0(lxTIv4e#t<pRܼV)PQuLںX+6*ń-a gji߄JMw;;7CV̮xژ9d#!4ϝTiWVpG^`Ѷ:3X\mޔ!f1\n0v*DܱaUnV^EIߘKKLjAk%G3gX%I}^^+"B"% |6- z/k''`DOQ`:[y 䊂V¨~b X+ˠFgc r{O]ݼ+ L4MˈKbQob[ZH鸝-rc^s"g+Yt>Pێ0mt}7ݥI膂 L0l ՎBRrF$moxfk6DΙUka_,*a *"Gf-|yvvJ{vv6%p=J2t-ن9E8صZ"1q\vbu@NJs3w Rh<|Br FxuR77]_ AOWr=4' AH>=6&L2HD6SR"+n >V˃2ꟑ[UWx;Bj^UB/^4dyxh-"|7>=[y+eA_ϯܳ6y<@+jb X +q{&2^‡˹ pWgz5sgLxo+;!D00zFq_ɥ:̴oajCU +,__YKZ|B{1a ,^b'||Ezc7z<=9lkmOpt{uY ɫ_eK%+q0SkCK\4o՗!s?ZsΛj[8,ZFD@ï(XZPTyp qs6>5T3ERꍅDʏJ@eDLhZ +k +=+35YNtMd?ns,\CK-N`/p(r>xggT}Fy!х~cUx05IpF`}*NھS5PԻ.8@h^ +B1nrbezPYuε:0LiD4%foE8R|do*YY`<íƃl\y~_sI"j E̒>C/YY%^wpkLuU\a 7X7X +x7e/*g LG2K1D\KcsO9u"}(+PTkc˯DIH7 ;*xpNL:B&P`ۧGT?Nkƽw4c 5>+W^ǚض'gi0@} +Pa@GPeHj XI7[c۫qj'!ц&?|%?M~ :_KÿF ڎwNI1t{5[8|yMi.8VJ|Oʗ}%wNBp<&)H@U5v5w/O>vHZ&&.Q>^L12A6_7Ten׮,K@=*lhj7 \`fe@/zwFiI*bhX)59",[Q{@-ɥh)"=\ +Otv|D `rBDFL}‘(c)MT!,:;M Q,gfZKO2<'4g%H+N6Bi"v7A5gXw'j9^'Q ;U$V΅30QY06G-.gT=zrDVKQJ$i]zO#[w(sau.KaK[L+XlV ?جU0² *J͌%A"֝ lڰ' ^Dz"jOl!jb!GiiJ<$FqLӒkO$w,1{UV/oUeBv2 +E kL0q2Xh w&_W5CIORVs+ݣM 5.H>&G]Q!|$0q-d\T@0XY8CDl4ul5eU%c0փ ?->b<#\VFU^v)K.$ "8,J.PYb]É䥡C Oٻǵ0VKANݴMNFJ~j$C%k=)>&v5`; xmXa%0<1!n +?b(J({$J do:Òlb9UVibz^H>VzdKq7KtjMu:VGP`/ +@IP^3~zSݟ؆ۗ>05Y]QM|O_ܿPPר?z?ʋ"0?3tg+N$25+Κ' Q8&8zBň4s15 0[Z:S^?Qc(8M` }"~F5K9n4Z}'A\lH! )AZb:s4ak?$ʩ?JPY~O!|)zl [=tz *Ax +Td)S@:Dɏ*PcAl\I8q6<$ƒ܈?'@ܑd4Wr?:y'y#ϼK?T?mW(Q<f'&d6ó+kDA]Pp\q 6B&_s`a8 po297/-KR4|).4Ivέ!Խ(G.)ee&e|5(W+K +'xanK(9VE![We\?74xLji$ P2=r,cTz~}4|=vCy 3oLIlE7-f҅Af`8px隄!: iy)(3jxK:02g XgeL Ǚgti$O2 o8t)rrC(}ϦߴLVUf؍^U==jc~ڞ:vnT+6ZJ6k'vLNCu5;@٬xJs׵_Q"_Hv<%sf5G9xJ"OQ1cPB OFU?4cgzG~fd+ړ*5m*[p!LISHڎ&ئ?ƴI y i"Sv֮Jg!-eY`{%_{HR?#k\Zx, MV>3Fr\W KeYd%3yA-$g +ҽU.H^ Ss0(DNM\Q "3C>H+*3.f.3/d7-I+Nx>V>/Yr'}LT}ËG,֌fV'gG3DWRъ+/3L, l5HKf2f)fnWnMp(Ž2/ H7")E6'#}fL#*WP\;ĭҝN[N1Q+U qKIjܻҼ]GYh>?yzxmGyar;]Uqz=ա%Sx C8BYxєbiZOa8ɜxZ!6֢&`#һb%w.j&2u?;hMs" ""/smuB`f0fOM8Vuj8w޿[0._ ?ɺϷ{DZA;6PUpM-g!>тgXϪGR nGBo~a|Y 5IaFlrD,VcOu +{CUG!J@QK]FeA3l)Ia<'|Ճ?FYb]FlP-.͸E !H,M؎0ʤc>RI1%XQRnh\o $/d n'F +M\ 'GH8fl+˓1 kI7-#8LWib8BIi# 掵$eHJ|5I/JItI)!kb +AԮeYEi W:{ > [^zָš"7ZxXF|h1&Dh:=%Dt&;0g(?#[J03S16YJRu٥7ֿ(Q4HH +gMSHtWY$xvb\([Ib.7.\IBǹGe$6ia4S\%J/.v"{ڍ G>NƼl&H+}Y\l " =WTHȗ{s"l;=ɔ&U%'GwyD!xJ\ެ{ܡ+e4hŞ1 J4 =_i @u/ e5EyP8/04Z9sP ,8x0#Q?êrUhK.Ohb8يErX}cNɣ]ٲPypBTӧ^!m7ٙ9jv~W4Ř<?_xč vIyБ,.N}V},\x ̔]9_I\w^ǹX,Ƹ@qɣIٱ-Ŝx5<8.{0S%nuFXߢ\쭘̙[4|WY5%67mr1w-~?'8PTԏ/ܛxXG`gU;F<6{ ^g7%[SǦezbp0yo*aZ3zv|7!<^CMQm]풔ޤnr;lV^xJ8JH<Ŀ&-”)^$?-.m7A,mIqi4>~^. 6ќ[s\(Y[L +,؋V_Os]-ȃcq7(xxɾx)lD];l5|X Z(dz5#W/ C 7ڛ>ϠƐD MKŚ5˵蘽 + )QlVyKnYF\,/#Լ$p^^QvۙWо*{6n\s1,fQڸY<*\0C4 M}\<5Y86hp;Bڛ1TJT0s LNō74gB-YĆ&vR# EF;QR*jVU"|%!KmVm mL_U $X*s%B da<#vs;X/t+U&p%RyQ-'LiAhk"{7O +L5:סS儼ay"+QOB79D̡Z)^ Jq:cpgaV F$-Xrb*J1y3f~N Xn +զ+` +v4TqB `*o?NY7kfSz|SgV~6ϧ'^ypW@9,r(ˀ.}v8( S 1z ^d%i=&gh37TqƲxoc "33!;%X% 5YVheSv&lYΘ i9˚IB'ׯkRٱ%7l7%BɸMoVur6r9+S1ޔLغD:rys&[гTD' 限3ILϊrz& YIzV-gz&YN^b=$Ld2;acFܳ5ȵҰ-k.JZb0_+bK, 8Ojx^AbQd)wKcioKNE ?VftY 00!?Ig20ao7&zp wH 7̫r TؕdX?`̄7yB+,㯬ZJ)P,E +j +`-{E\^e2oKKZ~ᄈ9OR1g!ߋ =WdyJ]K&^;v^F08b+xsQY2us%??BẌ)2"ђj%WŲ5%wڏ3LC:pV#'X +ـ"`X7㏙G/ӓ;q[y|l{@Q79$, +W7arh~ʍ ۋ`OI;dq >,oͫwwV$ ")>6FH7*45纝xK6$0 31$ΊvǨ^Kpqb˴!Psay[۾f7f[Ic: +r{kmSM$I\H7熢U$BqۭMƊ!LA"DVdḑwA$X%\~Ҽ +\ fuh?\eR,);[J4{)~2OrY>^I6]A!&gUbC3 +=$}tҍ){[ZK^XF5$-dloO'Bu零tB0ߏlq w?G7wtߙ>"c~C\Wka qq`D:F(_TPCUecW@ޟK{jTnW൚IZZ'sZIR,mP`Iq/HZ1\Ca46^q&kw +3TqJdJ:'@(Qy#))?RwJ6_xMʏq2&/ |o]|Pkԧ3q͍˻=p^ů70 9Ji0#<(poh黍cq~y + ܼ+MM`]l-Ow~|[}py߻kѨa,Ę Ț[֠n^׮R o~{habNq]X2pWk'j"%jnpIZy8I ? bW9"amTseM@څ Y%PŜЦlB lϹl~hl~l~ l7i6ES|xިX+:IAVձG0|5bG矗ND]D4CLÒHxQPƳF0aK,GE^mּol#|Q")bS%ɿؠ zQ.kMҢĬ*htg8!M6wMdZxvs)-|<];M +UDʀ˹-:bWK07VL&D{s7.CWIl7n7'dzx cIC@ F;TG s@kO&¨ nx3u\x_xW۲Ax4sT b,0ca ;LjHK{?ۍ xh,`R5 WqIQ^!u_)oMzO~uPbi|Crs^Bu{R8>%:کgӣ{gx&|.oJï3;v)#xv=K=V1b(?[xgX%pDNOEe( u X>L2ò"r +γ= NTTIM hMK= :0FR`Lms5"Q 0OeK6,xA<('\L/r/&BP +!״7tG<19`6Slws_+2r}H_\W}Di%#,5ue8Ϊa1mҋwWUS#5P|$FI}9:21D_/˱zaF)p*?g('%)M켵v٫" i_PuIggYn+5~YO 'HU\۠Vł-(6}&O&~A} Y0k+۟^\Z^Xg ^!BeRaq=/TaH8G+63hafJHY/m:Y3RFf`zj#tFXJО2&ȹI@袰b!Lk7%3 0J +#mUO;am ^ekz}S#ȬeCRc>ZKj/G(JO4&iMYU{#S+|u@5tE〺svUKH(|1}Z`ívv=x![4<p @6 +D^VK@h'iv&B? sY|zw"BR|(GQjR?⋦gڗbR4ѿEAodi`1 J5tvHK4+X R}a_b`_Ho_UTA݄h +nGlFfn0tىuu:vk^{}EV1= -:/\蛞y]1" v}͘0}!*ń>{ Ynt[v텱xe{PFE -.3%֡z(R Њڢc=) =5tYY;nm)i:D`[v 囗ibT#RI)4gK7p0] d)P"u/p pPp<|;4#֤83BqW 6|E_v-ӰP1NcI!ڰGtqHFK)=P, +Bﻡ2VD3*1{04b90{ M؍}iQ1rP?*dEImIiĩJ`^#pZ3MDң>('^}1 IvVX՗(޹ +5_`)}ҌEA`2U 2KE`,dBqM{Z2 RZOQDjcq3FӚAojkar+JJvٙ4N;D6łCtfmڐd4o>[wf&ؾ(rξvpNa 6_&vւShǨ ]~*Sl*j/(#Y*{tS9%b*X'+~UnIpb7uo>dYx-H\A:#`{bD!T;jTדd+o;E +=f~~\vJwj?R+))a+zB[4lְmb9أg쑡4Xgᆁ?B-b(5Lz77LO=~6w}o queW۷8fXc`c "X1pqFwdqig<8댓v>pW^zzvZ|67޸:-⍰,;Dϓ)T ퟉㞷BZ8I]Pn 4ڷ/JQ7H|qz/d:XZD +C;FdӸF}DAi0[3c^Vv*Po? C5wX`Et\E4?iMƈȌm3O +{N +w;ϥ(XSwF] ƘA4 T r ~)-ODB'jr*:7J'*[5#Sɗ6Y +#mgԤ8.|fNʀv⤚@tVMO1>OpӶ$WD[*ݪAdg^ѰO&zGD,*F3JҐ$7tc&H/5kDrVlu`dYfkc(?hd*р~tv8d/Xq-ǩ»!,dے—S\ gO;MFn֞}|d-#E#ҍ _e_Irro8.T;ľ/ݲ DČ +nXv#:֮z͔P +ma bdC@U _?[]`^db\}fx?m-|h&89iE1j_ZR_ZR_Z_E0/K/[!轆i,A5)O TQ~,m1 +V<5eZ_ ܏钽BG_xη&Bc; ʏiַAj}N >869ab$d@d;HL Do@am3Ƶ@DzH | F-]@(H 6>&!lsۖ9tF^w4E^+V‹z2[(Eyd;fAo߻x\r Vxɠ├ A n +4 K sw\Hs_!ʒ‚Bc]YqPRP#2h +ŵ\)I+F97GܰHzc"}蟏7gZ -/F^@_T$LN+.qm$x¶AGc=Gb|\͍_&"Uc Ӄz~Owظ#!Wy)MxE?S.{mG@r퐃, W$0Gj `UM~Bv#3h>fƣG[Sg {>NCt~`BiiDj.6 ]B+|bz +~qPŐ}GRxam1b& 3ndE)Hё16O + +x5 -2TM:k.}&DÉtمmW*d.悐iPGM7h4I5ERxm{YE=݄ܢXL;x5qлK'DƼAU80' 9КRRLzHbBK o㸡$"6J$RAyvl 4i^B5s/Zڔ /r-\ˈt\T~)UDuIup%gPd;xB">; +ηԾ >c=>t[ + ٱo"iCߺ+e/wS\<{?xcycL:;6[ڏIy q + 2,QE[دW>e@~2mUWTK!`V +w0\+ӫmO`tyjb;#$iW,;7vʿN{wTrH_RO"yQаąkéB'WnGQ5o%M߄ 4~&{nh*taco\׸"z4})CqqkykqRC}JȢ"@ڔ=RN^ŁРz\s1H$JbɊ Z%G5I,0:`z))^o Dnži~u۵F?uaKŝp1{C({c7{#*ƀe\ "y1 Hlm-f6v[RwFE؋`\{ +ig<ŃmX<v`z&_>r ٫Q44ZgzޭC[Ge~,~O^% +tI ߃0PcHB2\ ґ'$W*#.=Q+:--l)7G0ڹ1<仓d͖4RChz٠=>n8y]7M;hBjDiނ謥Ӿў&N?s5r,.vPȃBr7 +aMo,.L'FIլq[f# uznFF9_bZ?nUL^*ImRP=ht@߷ +D*/ ڕсoZT\  jpPF &oЅ.ׅA#UN+Nm՘#$ÑVhعvrh%/`.*i!kWC֠Y3Ct֠U=g5>52CV;xV!nk+s+ga[jO5\ch!kPo@נ%eҒCW}Y<΋+p<]:M̚gN_PaG2}Fb/f"|8 +M@Or5 ΛSAo3,gCG^C\\a,RY0M,55\j~PVS"[DM@ÏgLCZ9 $l]2;8ڱTy9BRP_j4nKG'M?pg-dFAYLCG>_b3rGN̸3y96w1 4 yۿQe3bɈH`e-hbHTsș-t#'(#7i֩a&fXNMɧ07c[{?ڗmr:s6cGI To<I'xelLJ'ݨ=VП#F}\M=bĎ.H x<:&[rR 9xW0EG5ɀ}5 [ޯH|:6ZxImo+R8}#]X/8؝2}E:<_֙J+WVT knq:RA"5ԉ]ˤ:J%G,Z0ErΆɥyd G\SV{QUPbѥβȺ bd-_LפIm|lVI8<+4BfPɬaREGLD~_޳_̑wI]zNȐu>?`E_%|<]>^ 5|(QIGNp2#1֖vB Y&*vqMe#=,6#T,Y"lۊ<$_g +r ;B(1(6.8~!vl@-4v7r= d۲N18ڶNQͶSlf몖JGv 89hg| lc.x +JdM("_5n`~r1*9ƚ: &}߉7IEo'[=Ĩ 6XcbJ+s^ZaH7T_Cn\hv.arT>d%gL"oى>"L +Fe lK 2nY Mj&X_waS )VE8'r"؅\2 m5B0𯎆o&̱9ZzO?P؎bkq&N(#8Y((!|4ȅB坭{Æ4CvL]Oh(pPi>7=ڞ{+Eh + u%}4T` rr4Di2?0!?G1Hмlf`Ǔ Bӧa.{<ݪ;&i^ҬӨ'(Az4~:O(VDŻwm[ VVqぱߨ"_Ctc$DXtFS}ޞTY^iPtF=zgd~&m٩&QyVjrSZ˻O+i2fF's-1[4*Znf^d.}-n3s-'M[Lf1VqF0bYvv=P|+~DCcWƦ$Q_coe4fb$zbH:3B'#AOwTob6"1WMQI_}> 3"++]'dZϊkxU<[?OَRo;r{ `ƪIGcOg*([/hVPibfӎG2\!sS/naBq(J%4N(&V'VpJ`v7{RBh +Ceo @-}A&[db% ڍ륷A&q{HD(HbWyBWbA"1Hz"ُe4*Ǚ3Ț8!ٜT.ST[ۄX{ݧeI#2ڶbVvŮ6Bt&hԝKIR$DGb  U.ǧŔirOO/>=ܧr^]}|kPe{Ͱ!&6?XȯÛLHc40V UT :%%cpx'f>KV0Dcc=e~Ϙsd~2rY#Q`N(ZV~:ʸƲrLXoX&Cӕ$S,PB'cPE GG"4HP +^c2(F+z{7Yzx;re"AbxP\>0,g}kU4ir/er\.Y 'vLFeAH@ 6%&6lj7n߶QAXA1#0c2-F?#Q;ZzBiEUXL b<!vըq)TUQ1.T̋zHla K~](1g! ,J¡kJbG8O0I8p ;tBZ\#Q.5#06=1 %/)ODPc>>;x177qqs6)fs`g&*IBѐ>- +3<(#BsSBV=ֺ$"ŔZ@ϗcd%DsJ[bΗNIwr= p#E22ۮUp„=,7B2\6K +{ڪf` k%a},P'4jEaHHpz 6 a:ИI(~nǫ01&UPQO]0JVHGMI=CAo089a "B ]ƽtf 6HL/la[H1\3d U2iH{toȓsӡ<2%͕ǥ'dl2[%LYE13 ы +pVk29K߆7K"yf I{ǥ\:َbux֝u_zNF;lj/s=y3W'uT1p +5y7q D5/G"@/ r/`aǴ%@P45˄ D:3p9ʋM> hKC.Q"3,!\"wHł% y YcCi[3Eg B? Ɲ5ς| աԤ9:#wpA}DY{$K̆z3y (dkigHKf tC&|kMaښj!k rvf20+G#Kc8wC) +[k ++̹uE"%!etMy*Ke +X0ӯw}79SKyXuU!F0Lσ--6Uw';CzG[ibb1݂Ü_p(2&lU>.s8,tG??VnO渑)Z]H1[[Zxhjʛ[WY`G+Go3592eUUi@Yׅ40%9tuK~yEܲH!H]l0L>$ +o>'x:z~"ӛs/F/FؖDbȜPPxȭe{;lC!<ɶЊsS ٵw(s~H82,l`Jmx-Midx˼w*HNiUSsNx ^MԩjA:ڦW0Zg:tm#0onm#ӓj!ʳ&_-<}¼d> Lp5^ +&[W}_Q|4%WGCَ.?VרFrOۥO&CPuOegi2).XiЏ B9i;j:.*1[9Xp+],.z,C#\QZM,jGkѪ.=8/*8g!݆qUę; d~11O ">k8c[jh'S}0SߒeFce8?HޙzjF]>S w@b-{F +QngxvCI~8 Tw'Pu@ <[ƩNص +ߣ'*=+lf3sK]_sٽjNXmsI-|٥%/ {)^G +I~'sX_7Nd&<0;TF?PTuxŪVf I:Z_ʿ4 +[s Q<_R6{,kU*qgv ظ;CN;Y׾j +gɿH)ܟU)"YH}OZ 0yVK,uR)yb=-/_6XreK@&Q_&ۿӬ0:w87^Ac?1EKLvh*w<9:#d\WoahY8Orԁrpȑp_V3μDO+ Vuqj`K˓bp[+ cBIJCY`:2(YD=GV(Oo>_h.Yw;k\®喜N +}Ql דbі/X~re{BY]QAjx{%Ƣ0ASDS `ղ#d.w> lOl$ -ͨ9x˿TJf❰i)g\yX]5ռd+eVܢt-wVF["U_,;>/ŬE|Mwޞe>ʥ SUcY% +3)1Nq|:l⣋ HD{: Co|`M]g;۝MJ]+WyV*OB9jJ|] {!r8Pm!켳9ϒxZI"_Y ;%PbC8FE5p`.̈́ϫT2ށYāv LVbQ!LR a'h5)1bw&zr_NJ5pTbwn n0JY?ʡZ$C竧jT>{zF ka^v74Hgsi?!]/Oݻok J؏? +^'GwKK{Iu+rɺr A3⓸^tnd̳ͭ|s\BR)O8wZ5Qv$mqoF*]l$%[/ɂZ ,g%V?NASj͔<}BIC 3|XӋ[Xm[~( Pm^E$h#nE +jx=&E +y~ $E&8{d+l̞-gk;ےWG}Q|T[hD\Z?bc\Sˋv4^^h0BD }q `Ja-;/'.ٿ$D%ALN+%Gc$38wγ\2yOs)X2*0N׶V׭μ<6,a/oxKMyo:8yC:2nhB0M/,4簵˘JFrEV9ve?ۿ]"X#WɄ3_[?{{?rͮpp$6 x{s~9`t|g_px|>9W|EqIytQ[.̮=t<3hlTANG&pS? )HuLg.kD0T,t,q'BO g) !4?~N}>.-">+Na=I3k d'j.}<gb"I>Bz Sa3x{I/{5D%T Tx?<%tSa|4tRs*>j=? +>D9CgYMGg5j[ag4Rznp43Xզ*kE?/Bu(kuf荒 +VUYw1ej;Aj쌍مƧO$m9g#p&wD,n9f;9FsSFc&z|C@S)~,}wS^Wk,g7.(4&U$? wY3 S_ +}Ll.'S$WY] +3hK(8`GXjDnN*4q8dX{u>kGWƠeҷ1qM`DΜ2|4S)r +;4U6go]S2B)a)h +/jGTu)NP&_>pf`)gC SLJC[ ,ud1}Y`=T%!U4h_yttUf9s U +ZM+!p +0IQڹ`/{.)bfQڃ R\6S^&/ $T xl$g5Nh +!5;|&%hJ:.7`܂D.|[LFsWPsrQ=Ɛ{O;{.#n)b$њ ^,GJ + q1`=8O㗒m̓t E8U X[Ò!c٢! /_w R`!FOvS J `ܼ(:>F9ERƟ-䏬j-tVS kCܞCpaC!L "qI!Atޤ2/6xq66t$Q ѼCq.2 SaUlu F|مVp\ecjA%XːA e+THqT +<5r2T +ӻY )sQgK,kBϊvv<ȎQUޱ eJ篢9X: RTTنLdUO|Xucu4艫qxq?T9"qH²DVvcSL1V=mXKP-74Hް zko;ofk "aժ+!!\FGEthKzUIb=]:֣Ghlt6U'֢㚥E|q4Qguzίl'/=}.;f;^cH6UI'|=B%!mijI0E/ !!?5B|E%Lɤ8ᓴaD +@%^GKef?|{ڪ}晨j.◅yZiD8DE/ !:"p("#DSB%qmnG᫽,}EQt6')LP"w6 *wTrNH!HըujLb59J_T\]Wq e.uk-IxYW*s[q{8%/ s[=6jk"eukFOa$BIG8g"b`UEG ꩂ~;KRbH +"%D? P;x2@NKqI#MmM&4r K'1V<#(*޺޳׌sJg\CakT̮\D*AM-'X$旅\?lj_U_|AH%t=곤*7y4^^072$tK):-'qH̒}~&yXF|GǼ(J52EA&zTqDYp<; G1<__"XcMȻY 2#j"9а`Xvy\Ϣ؝o)#a잕:xIY0ɹKH\4 Ki-A + r Llǰyl7{QPv'*=m_4LK٘@ E#H|Dş;R2zǼWТ'w@v)k4^B}JH`%>z+]Z bG4i+iS'xQz:9?W&,I^9Oa_!ZŢ vH;YQgõ D@,\Uzpօl5Ivco7RWdxgޗ#S~q$ &K(oKB:>%AsRy)#tu niLt(َ#a].xT SݚHU̘<9; +AQ@&E2M&"Y!8 g$J}Hԟ5pn ܄#p+YI NmT9iճCcgN,dpEDar h2"OhHɒ[:.(d +`%K 'WzwdY(Hq%HS%=_]dy3BK30m?kRFT4^2c_L-2(+PYm̖rg)nK_DCͲ,KPV @V= EY~L?/j%VGF@5cBƃYV/yQ p쓉TMH"* Ur]-q"?<%9VZF dٚ.0URѰRTPI'\E@S'_6JK'w6M,#<N=F _Ǫ02j=QJ!R S^^@IW ++^.V^_yڙ|B}uI`/fOʮib}eEԯd\r{/֪*xGyukkwY+tY$r+d2u \~[s_-)vxw{#)ʪpU|ՕJ:(֜=aKR]U4Y_eE +:,bPVn{C_?B2C/jb) ;Aq+zpL>}W bxkh~UM)gK~TI_@AnmLбUdDLͥ@^uR]ԃʔ0m1-_ -~e=q,N@d)qJ)\F1nv:~S*5֗t$Wq[hOq~1\Gq#1u/u=yخ9-ITP +Jm|`dL~^R,_T/(?ȵ[ Gf2_6ֆc.Kzk#)% xAN[ *> +RoIa)]$d㟎uVRRkx6q^ʭpxKeؘ_j R%^2c2A=UX*hsY:sK9YoNPKflPuܺBS>gx0wh +nYOs M K+y0^fa-74^a .à>{AnA5} +$&&$R;x0 !I|,p) /i 8@I .`um bfZJPx^WPAS2&*p+B_QeTaO^ 48Ǹ;9SPqEMvpPYDL׆G-NB(%Wh4}v5 bx}I+2 f &*' +v+cbzm]qLA$wNĢ#24ao;J"^E2Je{ٝRIl8E-Cs +ˀDkCufy0l4D$xƪ >;Z[#VK^;L#&)yDf=0m:GX!䊔"@IT}(Y'Q2J4lݩ~.#ZqW*qǽoTs-ֿ߽=!/~o~w%x6=Su(?<"Y!wsU9!ǵBƛoŴ"p8mWuD:jyWt R#mφr0.j%t S t ^3#n9ros,U,ra +.Aa+WiUy)sy)噜bu +yUj/򥳣 I1'jI|WA"6&Sر"]o9zt'Ŕ1/W8x\9hjJw@gccUYYQIqK,!ys캬Q%dh X߼UuHmFՅOºdxT7J׊miHLAgtrrGxq<7- Bboɂu>nm ,H' sWe2LH1ڇꠓ f7:S{ +yb? 4`ϳ hT=0-Bh`.)*pDlF4/KsKڙ=Q%vL8NJڹ 3nl"rpbjr')*)$}AB2[Ld_)(> E(LTl9HWJִ㳐m삐W!.^Cm.x/@AN'Ւa^B$uf!?,2!fv&p^#lɲ jsAR/ eD,sAr:VJ#,xYXaJa#=,VXCGZ`u##xp>r huPI,H% 0UܭzZvH.DLܭ*VBWeNEkv&I +_N}]T(rVxWxY'vQ[$`c)|Y8^POSp"^'A[ vVJRFC%3uYD:KbkQ(rcP*ԇ^6r%R8B2ϗ]R"=詤0Bus;2 +Ffp.RBvD_0pDd䗜+9X,''t=H).Y6?q6bE1şPFUBew Esie.é8y,ᝎ"w +nҫ&=)Sh 31y +̭%tP5^2Nɨ̃S3 Yyi&RRu,YʍN@<P.%.1./BE1 c@ClﮡO }TlhZ9O'SpfI +u\Wϯ?U/{C9\yN|ȵL2ϲ$\@83UkOɆ{CIKݜ|fu TMyQ> wdNCP =q8g-<%)4)wR42Åȗl H8%W |ƪϕ&~,W?VO|#Tr62GПJ/#XkܱRΪ+m?yB2=d%~OaStuS-- 4^5,Sw'T4ADyʼns„}|> -qJGV) >[vTTk쭖)6mGNIϚ)}6t0aHAwzUaj%V2{2uuMG ;ڋ@&l$kB$H<|JB>x$eyK$eB/Tjbhyow0=x{dit]}lmC 쟒3cI+=x@N]Ī"Z,HK e!=#yۯm!kZB(B oE!e/š_V !hH Pw,V(X>~΋6[4mdI&ޗ^F1Hӓy_5 Txj:S +D&8x!hz99pYZFK~+v+0zT=AD[+ +|8. .BT}J3$NkG(x|U%ZtQRx1:W|ũc\?_ +W׬b%a_>T3V K3:~: KlE7\MutZQ[999)QΒ:S^  f#~ήX8 R^R2"KL,t}ɰ +>)L"#Z[J@*,yWf)t0,yJޝϧ3 dtV3~M5cet|E WX: c~9TĂO pP!*j,˜8S]4K_2R:( ghNLlhZ2!|䗆YRpjj*w(?7_ bгB@5aZdWa!KJlIG~:t4*yqۇ_7ks(H?Т}ISr5& +@KFw@VqL^Z[TC(\taq!#\/jܳU +#WP?ई&&m"C; +ɳSȺ+ +.`l@0NjQڪ~9ԑ~}AТ|?:„/AAGHqo_4 Qe+K\\d!0V}W@}x}smm +mx"\b$8"#/ $}5N7ܓ>ߝHCT$ϋW [iܦ( +Yݭ+ґy EdlyC_irWKHVUP;~!xW0o'}65_()^V" dtÛ䘛p3G*"E&3%3YG9DBG ԑi<"YF /끝PqKԋӉ?w] J#jXm n&PLc%!4.}RLL,K)"EiO X_" p]#vcy4#aHn[?bX $l].foB.  I"FIcUY|T5my/S +r'!s!OqF84ٛP ѭD5ޘ#r'0F(qRc%Jz^" d b4F• HKF3Z2؏`܏p?d]C'a_hncI5t "'Mlᅧ9s9(`m`EJAl;* BQ*F5޴x%F1 _@d^Syx7ZM*_y#ovQߩR +(p/A08ni4[Y'rq`D;;fY/>m1( |ܴ;.Q + Q6\?v ,sztIL#* 6gfIg# /7 +hŠus>f ^B&Kqq?| ŀOY?6)zժ1sV?oѭ)4wt!=Yg1]DR^aܑt@_%~b-ɕ/|5+-J~~I +ڌ#R*%yYaXFoN[O0l]QVG<0Bo\юM4.gPA?铲!$OJRA)h{>L yc; +4d?vO3dyjV}z떜g}ǘRβ'Ӧ +n}7E7 +9Ae˩Ө˩4Qs1V9߸Q0g:`jlQ1{VH$fXP{'О` ~GvA<:̿tXhw;"(YB<`”ڿTjXG7K|N*dgSIV~yvڣ<c5IeXԅf_-$_Yp/&zYSfYm{Kڽd_. \iRW%^b-)-O!E96Vӕ3 S@o1s?b[ȟVH +:6K"}ln/Q0_ش$1RμIfj^11ⳳ~.k"t'm1GLJ91P=-\®ÂHʼl뚕5>kѾ"sg8=չ|MR|MŇV|Z|y KnsN(E=1|@x|vʋȀL1Uq#6nߎR$ŏ WA?όp>#4|O+7 +/#|">}R}|pf) /;lφ\Z!,5дaHApsolgGvcpVzUr6uB1 +jAEZ5CF<:^n~S@u )Ao!|-,<či!.[1טj^+ %i")qsFb‹i!tddVF7c<ꚸ[["SϘCs_x-އ@lއ}އ D`>c>c>}H"!!a?Fc?fpR+BHl;53C  %4Ř┡b8SĒE'E$P3!~Hڱ7tnHcZ.gH#6ޣ]ђ.9։i SnFVqf o,n&9K+l';űIJy!CCl.*\%`aU5\|r*3g*i$X֤~u)Db"Y1$0neGkq1NQ"3՚x?X-4-SA +c' ?筟9!" YҶ 6С`,624S 9F]p%Dy( $)Fq ^A7OtA6XdM8WJla7bħrqs2Ӽ{_ (x'MֹR[7XNh "K_^Sߙ1ǹt N`QH~zvLl(qZd4sUA7wHF!dFsHR)w;B?'"xv]GmF$x|_QvJҢ>Ȓ8H)l66B[戰q)2%^Xn h9i%}SxRhVg5&>J3QveobXlsqG~Pi?Y +:Fyk6*\)AnOh}1s:ᣖ>;c\SFFi8oc1E \?ZTY%Yf̧!xi &bF)(|w4-R0^E FZNZ%'7zIse!LglV{R@ݣr֫!Ynri=">ǘL1nTX+HX&Шxmg;ջ^L7ߙ~z/>RAA iB7)VJ'X8#?&! -&Cj"?ߑQ:}oc1)0*8C<يnv]@k=m5kEWUVQ5Lion +X10(Ƴg6Lw̟c51KT&}~IȀۻiXtc<.d/mz=їQ'2d/'c냰L.">H&f$ae[G9y$9_,&G?t.a] mH`9UdqH1F8u曦Jə-6V 2/|<[t$u|;R{m44>2C?qT=.^G峎2|!-Nx{ E9/߮ |C| 1,N¾qantǥ/]8=BaB۬-NbC^nvDM.dZZ|qISl0ГO#"X0jB,;A\$͹O1kkH7TcXJKܕKI2V">薾J_{" ".eL.L3<ۊaAHCu/YQ4c2[@xP.MކD>ftF|(?3!-D(IxЉ#lilzWA7ܓ>75M!/6/DV䙪0נ䘔O-ñUiT dɥ`Bα M"<_!7d{ѡCub>\p$\GV.WҘ*y*>2; +%gZ#/a/&tXNq` +ike,s>+ŪyU"'aoҮMo#!jXҝ>[}' F&sx/RR Wb|~2ai8^N-Yj%NRFn9=,~ &I9/,/KD|}qJr#ZCc*/9%w[ L1PiYϕb/Dq0H*쉟G|1LG(8~_!NE0x<_*;ض.c$c/ H1vxgrPz@`.$َOYͺc!Бؖm#\s#쳕ǎ!(M-dg‡QH7#ĺiݞ:کx0;:lY|O(CI3@qL$5› \%4cw~u,ƔdɄqڜ0Z4eJ?fc,Y!(1\x{JnWGuCqۄP&RUsh "bYpwxK`E) "oFl=9)]i8xw6^/\bگw])L.Ͻmomw=~݆mb([iIJ1H2ZGcHY6+Q䡴Q=u"N ڲR%> ;7Δ"ެvo7o>OR=ˆL+6'z@`V,V^Q%X5k%~穉|_/g,d[Ҫue9K7_]060m%(e|ʓٲr-#?ֈNV$8mSV`PuqAHd;G~2=_&iu8T5x6]}X3H^~SiԑWCVC,CT ǰc@4$-h +?͓!Z1QL%`5u]ĝz3Rl|Z@p?lp?Ƚ mX}zD\<7qbOmCce"b51A*S#ehExn%VZ_rtzRG:*0a- e4VS]Z\ +4'M|ĴU'f4vRW0:㺙o2_8 ~;"]d!!Ů'ɱ]OƊڅNYU8s v*놟?wzvm9y mV3r<ՂbPJᢶm-G^A +Vlri_z_<NB.FR¢4Wˬ򸉾7 01{9 b8f\9EF,njol-(q]AfV{M]JyŽ_Br_`K +cUY0]c[ǻ0mQNYbEe1&=tT^_~>73H}<}0>,c]@Cgy[] FSHNZcQJ95g#lv/~\d;͟'|UAӯhqUO/Ʊ Qi + + y唟W)8E"ǰ0oUX.B_^0K|SUt "HH,&n#T]ԋg:8v@=M8aN`zg~-N1;"6PC\R $~bc*i9Na}(77u.wڣHr /Z/y~GFX@͸u c'{5nI\1ꥎ";3qBsC=4AeA#9`f}c +CJFh /~3I@^*OtjKwb{E {B7> U_@}Q4MnB KG_ƃ7yKЧOcs BIG~ˣITh%>.L-'?hH=6 A?Vʥ+>۶b=zH xj!M%PA{ճazMQ `a!eTc?aT,qOO̤ g6guoD"r{O(l [_5xvy*ͭ{2^I+Up,~nųZ_=+ŏEqΈh1(EbKr =I/iiU +.x'xuƖlR̢-X +yrqR[ɮX ({%d:aEOkn0;,`cr/ۉ|0,`*9].N. 5J 8(97-dX:bۨ(UFl6VT1ޤ#i}f`9Lr0M |B13ӺՂZdVU|+/masvr^Aڼ/y㙆4;0hCڽb[wEiX $&NJp8H:{Q{TquTKƮS[˜R#Set ؆^x Y'ANUd-^+8TT]w5״:W6w""?Oumm;#;b3tLb=l)q3yp%PTߵ8rW@\㗒jzJF'|`CwEv=-9qES`oڎbRJwЬ^痖1,FߖQW;T˔=uLwE r1@?ǐ8O:?_"ؒBdI +*J5wf7trl@3:70{>t^9C)`&_^DŽQfݢ@$oN3{U ` + IA~ `'Wfs(^ 7W6~^sak !C2Tj^t6@P"z?@PiKa);BH?g'-+D4˸c6"=O~BLeq++E\OViod_x1qqFhMYR%kejX'Ӥ8jD\TK="`h\ES-| ?b[qSV7ڑ 3XolAza` Dfdg4kQ#k)by0= 0e/|z̓t[٩e^()#!hșg$ ORaZA+`?;S?GM'kmc'2Yq`p/,ح)C3Q/ df 'M~@ѻw?`7y؍VoaW,x!`Л;W _Y_48: 5pg82yg 4 -1Bj͆⛰'_秓1{HЧڎ s7g/d?ue8'dH!>^D;~A`_<92D:<}ϠC\~_)0ZqlkEt儜'BWvY. +Rv 9[F;6b;u]>-5p;4@VcyMB1Wb';(QR JFLb FLJJi6= f030qH4ܝX& ؜ m&Z-Tgb|M;B-73 `500֩D-#yC&Q&Crf/E wLe{;l \EV+RztDtěhԙ9E*` ӎ>SbJ`'K¹X+eL9g +9m:A;`J\m'X@T*!x,A/ں񳸿I9bÝ)z>MI4c`9 'ˇ`D41a(1}, ?۾7G*.Kny* %UyYё4+'ޚ4o羅p [ w#"mm?}8cH*ыڞ S7OUfe`"hqH}LpG7[͑h;4g5B>> ,ϖ(Jp7%`ҾOW>6 +lG0IcTt^Ml|$F {'U2iwsT3H3NG@0&ls:xJU*8w>6ǮP+ +,V4 +%8g!y=dI4!$#H-Ѐ"'DRVy&nM)6Rf( eg!{EC:|@U7On88A\.#PǿLv +j(zIfm.`!6^I׻{^+xX4!8'gl!\F ?ٟY` dυf`l0w?d儛w"?iC TP r m}X`=WS{ZKQpdI?ݤ_GGJ]1,ݯo\Og{s?.;Ry~C74m{PQ>-3x~CDFް[ &h7]×;G7~|Bù-m`GhT -.l! >,[egDF۴KZJ>Ӿ\)xg;/`,Dj~rk@=z0x 5T| +vw~NY%;#Kу*Ig_dArr #Ai wi`KIό`@H)bK̓'nSfޟ_ [cpT:L< )~ !͞\\.z]kDo*YTZzp7a?kM {Lݣ& +nS6ްnxw +9X9 g ?J/Ecѷov`yh"xʿ1ak9 >7ot1%팞1,|(# +(f +!|5G &~=؎=LiX_"i^$ Db>{5{<+t>UD8 +Jg~琘Vb6 oV1}Z+_j0VRb/CA *`1- >Z0]9YaXFoN[O0l,QVé@~o7gwu'qE;6CBJC!Ȍ,_AQb'%)N4=N&c&:ʱX 2SCf `K&F : vv6,EO3{O8SlլN-Ò.4jq'%2 5ςݽ}1ح˪zݘػ_tj-^gf홽; ٌPOٺs,ե)h]̱G|m!}xxy徎٪c#N`(X%JIbc$Ofx5Gh!eٌ-Zvi':aJi-){G^Hb$o\ڸx խID\m߽ugCOu.?rB!BBM#*䇛"P+7up3zR!2ecLUt ۷.g~U,GOaA3#ş;J18,b)"W@Գ)?}r7O!'7g[1X'B'!N4z kE$E7;7#o݃ Mg§A_Nۛ6/llc`VԂkԇ"y5 p襠7VjS B}>Mws2O|[!@ +sn%u?F;5S|<?+2T7Șn2&x5q8xDoa? "1Fi}܇@,݇}l݇ D`>c>c>m}Hz"!{!a>c>oR+ĬߐBH,; 3VC  %4ŘMᔡas8O6W6Ը~ȴu;}q7Ϙ>ϑ5OdQugg<}N*iSs.FVc f +nghGPUb[WmVj`R{ƶZ=XnVuCC֘rúúu8E θ. cM;SOz0i B'uҽ}$/U#'xR&]9p╯֞+B4Lr1e P?p4?yb{ygz-nx` Kp4.ĂQ(te8Xnͷ #%aHpIגZF/ 38ϒA7(>AݫI;o*Bv/F|na23&lj&B7hm[*Wd:I.l=u~OG|̫ oH5$y ɼd^]O&d^C2M< A2r\u踝^Ӧ6Ⱥ? ѐ!q,i]0$ܺymG1m[U޶y-4ҧ,RB[c8ڦk2y%l\2 uCJNu3Mj%݋$Q(S$٦Hla5S ԍC!u㐺qH8nR7ԍ%班gohFǗ1$Dǎb]z%aC!4;YW^ؐ1.-10}i7raz$K;̑:f!ww;̄gBC^fKO_.L7{_ĬF, l\L3rW90'|N4Քm'wu%Ww?j+s>Bd=<{J}yd 8 ZųMC=H2% kq +`#[D$ Sc)/e@'T=M.&̈2|si-$;V Xt:P'"bi*BvJr#ntVM^Nw0fzq +4hJ-[P%Mijp_IaSrP VO軭] GL콺8'Pv] $AH_V-2"Z$td]yZO{dү}ڙgȤ,P_iz5ctx*8ǐnlң_n+ǪkAzr:ghn)TUךsVPٗ׌v3u eM~:bpˣ۳TC{'_ dbVìN!yi`s+˛ + 0XاKy!7GU;g r]lɛ: Ӓg֑Ee28.h+gF;Q;1wݴOmX%gSvf S/穌_#F!M T߳xnGy뼿; c|Z}u\ +Y>@+\Pz/3^fynJr?$ +fM3^6cEìGwWudN/7k:v`R<ȆY`MNo"GCs p5k1-\2o#: tZ߉#FyrZEOcB‚ Lʞ-7N -W~* =5$ySAFOF4o:NB1o$q;> + +!?Žf ƓoIqU2c %0IڭDEiQh ]4h1[eH?b$oR}vmEն Yga˒;PGo8~/ѥGCY Ӿnz$E`?z.znv,|2qkl7)oX#nc>HZ,Awcgm߲ Wu!W_Ñ0_3L@6Jp/S.lͿKy?[,xzJ@|4UN?M jJbyýz;]NݍA>l2N*~5@ yǻ0w`qFy'tGߐV]{Gmfnj=XjozzPZt,ӧ33mv^g'7l뮉]3"-U/`{WfgŚwU7p> e7!53cy%ͺ&^guf({1Z8u@*I$|/aQ \dz9EE}rn NқsOϝGԖr)m$YWhQz#kӓ>M)e4q̡Sq*ysֱ.)XT%SL.pxfM\TkLsaH1;L٩(pm8IjNVVG8}5v1#T7sdĿ}=iőܲQgUf!""صώւ(57HQnԂ`.HgsܴmPU:>K{E%7K#J{N&h%ʳ Fܠ/F zgp|Vr|FS)HlZ٪vh|R=ufLk<9[lb}CjFe>q[7 +rvYKD+3f{p&-c7SNB58O> l4-BGX,z"?yr q9"5SKM%;tlg%W麩h}"kVKoĵ:2tjٰ)Pm6 Aϖظad6.Swv<9y'J ڡl:KΔB1Ps᳹M̂$Q?l4L|a%1 L R;8K0%$fr:riX_4 KAl (f]ߔ Rɱ:F:8jc +e4KoAG IYhTy;e]-)} +D{^eV,"|:c쟫, E@dy΂Wp9RlwT + ` AG\5Ns굟%(+n1oq!g&*^IɺAoˋneS C3K2P n )>yJ,ZCpt bnmycLܿNo)HݩR3~Φ()`7&; ƹ=~Ym-%zEO1iVu ƞ+pl<D1v8N/z6{)"QSyŧ$լJG?&;5Dް2 ¨-lp9A,*A?`\ +_1cP.+@ȍ! \r:jp !C }LRhoH@71o8SJ7==pn4X 8t,Ct%ľg* qꜯ憊Q܅o+;>*$l:L ,7m"FfrhW ?鱜A @Cw_nN_ iEr!@džǐ5]j! O&q3ӭlIR3 5k;F이g n&(xT W7/k83*(/Ov=w ^iY0,4O+?3Ghn '\a75ɠ0NMl.Hy]sw0ffXAa̕{u9,eZq`O`8 duY<"7%_oW:nXJ/',{{o\>4E~?p hϷ8|GDO|:ԇ_|'2}Y؏2o0=sặGEpJ7fk7k*m'"}Ijg_>8휄b}?TIatܨu2铒8 Da{%" /֘ kl2P}BSJOApcpe(( Jߔt2 Sb(-wDT ˈTmÅyaK驆ncz +Kf_6dNdpzzLd2 #D\+dYrLQPH}٢U~'q 3o.g'f>#y榠:GTNY☕T4P!? ,RHR8б`& Hx׳2^E%-HRaOVީu^c<~.- +wx^,W%sҀZ3l =v`Qo&l\3lDIؗ +Fcg%)m8mr(\8AQa4,=mgik6n+̶ňr#8٩S\;î ~rMEO#ٌۺ ?'qZ ئ +b_ :9da7![HP68".LFiÄU\* Ѽ/eB*YDJxJ"/곪(Z.GU'XE[Ɠ,:*2U }ۆ9w߷Y}2Ml(w*uVۊOQpva~M{. @H\JwoC9vȀ]I^vLhlD5X;fw + y|v7;7fc^~yVXP&A>ֶDw4nq _g/vD{bڎ󤃏x+1( +l" ,hI}#4g D( b_lb a]nF]n;۝Z퀗~ObCKQ㱱b7pŴU?p#SlO{+~uc54gqB,F +YjXA P#􈫁!Fܛ %}U@1f1xޱ#cWGIvHPI&Oa&cA*Gڕew1K0Uh +{ho7ie8WSbYR$Z B[ihbOl59cQ%tW7,W~N +amr6݃x+|oqi [1.Q}?~0RR1>Ɋi#Qʮ72+hu4Tw;dJqp*AFٵ^.kb\4|a-L0ba.w}vJ=QgڮCd.b\خ,d^r+=K4\DUmimF'Iza+Mk3ѰQ"Q2sD&{oAEJ +n5Dd,Ud %\l=_n9T4OD)>E18p8 B'7ߑC en Q"h)wV#f#1; :g솴8JDL?Il_<61qtF_zE_v_Oҷzp=E6_^S9;:mIx'Ś_6Ta-[>?% *loW92|T+d `v?9T'"DޗIɏzZ3-V`\{|~2Wa=sq#n +o+rreC7/-˓ +0G\Sm'UE( JtӇR&(3 |>IjQWR#MtʒWYgF>s L+2!q;[̶/虇؄x,d,{H9/Y3=t.ϰOz))q~Tr1$n4Ш"r%h1@+"" N KJ#Gn zYk̝0"^ +9>^G2U*#I|bn?7D2p ^1Pˏ)#1%?}里?sDPY({T#)`*1cU)gC3q$ Ž˱YUD:I$Vg2x1a[a{y;[c>K@]KK51c3 ѼYn!oi5=:Y,,fMa7,1i-= s 9 j2|EZo3.gyO>M0闘V{=P|u_ntҥ.S"SZfήggثk1@Pe}޴/nxX.y1]D8[B[ψڤHC_F~~f,Adrɲ>YJ +{r壐~ (Yyir*`l.Ϲ ;;n]dሾGWL u_+'ovsWg0!RNoaoBu<#ς1)dx? Rd92-9B'Gf"+G`. ĵ78<)#UT%HyJj#QOil_n{Jcx?pUٞ ӻ5,` KJCNQQ:)%Ξ]&yC 4f,~7>Zr400qxK.z'0"8ZLw<FIUȃzy/dm 57߆f}vh -ƂgO#䛛~@h>}M<@('01G;H]*=`2H# +JoM{¨LL"S +әMC|v#3Ĵ`>@k|Iۢ\̂.4Ced$n->S/.1RC_f7zUsC2Z$oþL{ysR/!ljz|3 +plP}r WQNeaIL'|ZMh22Pq?D7]hݍōƀm@:ی9>Ѵ'ջj +S +8%\ ϟ@!0> zO|'6#?_^,9<$[8sP]0;?"~O*:ſ\ /~-_~f.%g'PٶUV ɕrT-` {r#߅sGd?tI?QzPL#o qvC5D1Hd(.#=*%f̜o{U/-35IiuFE?}pYE/3tòj]x>+݄@! Ëf"8_Ϳ'"OS?}Pv g:E&wY9 CQg4.Goeow~@^7W慂z:Z߅QWt8׊ +(ԗ':F<L-MEb泯eE!Yx~x@zEE9 ACA0{:=3&нwzk6V,|&G=uf|/ EvҠW/ hDOVwڙ/aCn#өZЯҁLrs:qIre +6ASq/uug'Aՠ +1 ^-F_rE=&tqSMBLH羱hy:fxJFۯ ĭ}*X6Rq4.|wiwD$HT=Z|@.` EaSGeiķE+?خҌ =ޗdo6?a2Qx[ǂhwg쓬܏dg +7HY "ߩAf~O'PZҪs/.$ӵHm0xFVY9v/x{[/x: +C1\d ZOȁ_P/qyUO\EYh#{$\w9tw&RfڤqDx{ǟGx]R]MաW?yB^zV7zAxIo]8CeJIo +}*i$"_SA#yM7N⪔Z|Ϯ4pBլ$#8%~<[d쥧O_t ulDWDaGMI򭸒:NقY.Jޙ8 IP`pd$OlAGt2}ߙf)ufԶ}g}_&Wf2HS},=T.$HdxIulFtrѳa@2>  FR ˫L0\'M.d |HOl(2cg< BZMDnD௶=cj {XYu洯G,y+bSWG_Js~{tB/ 8 +lgyOzL{?I*mp6>W/|:+ u.ڂ<OGAN{$WYOq>ӏ=z1jIyARI\j^x7Tr>дu2NfSS 潳驛T؄I#Nf7:>yZ# ɦ;-=?fǖmONUʐFȚ9%u-ًej:UK9ajtkxL54jf&SI(:lG%)d{J5>R9Rޏ*P#2C_O%W{kS+j[[D=~z,ma ]vrXBͭGFf$r:7-zr$E OL +w^|rCF܈Ƭ|D&R-w! {i0et'լӼۍ.>wA9졾(d/\x4HEH*MW͸;ʋz,+l}C &[#qlDbrBO*>ip*?_jqm1BߑQ{D%Oٽ.zg$n _~7xokszNUTK WfA E벁zh0EYݻdZ;ƒmF7mЖ,l4Zv:ܦHnTScML^<(auC(yD~ 7z[^bYQۆw76WiH&ˊLͻ4`k|BXAzF 9ڧN!U?"vgȚ<^:nC z~5 +uΦZl~sN*ֳ &T~YzRk86ƩuToW]{@멖z?.>TOƝ"?lF Ůp$ƥd7[F 5zltE?Z0 FGl?2p6 9:@~{A%F,?p^-nhNb\VOͬjmM Ց-?4P%=X2mU2Q>&b!:#z4j%R)%#Ji{T} :]hY2`K:~13ԏAp +[HO'j{AloZ?]ԏ퍮m䤶.PCa?QkQqDfPDpwN돟?ԥ;H_=d e{T53;"ӧj纍n47ɐb=1LB{51wUe)e}!nКmZGOۣFjm:4gVDj-OaR"'33[ aO= †ZZ۟Z'Ѵm]n +bs:8e^OR`ek75[gkxxG=5;jEfmB٨+.ՄS_uدͨpYE"UȖSqhnnǯkiBr?aO +q"Y; o.Lݙ@5>h8],+@>'٬?/IjPn +)ld@GdN_o-wǔmbxoІ"div)>َۇ_W. ؽQ3K<`j(VIlVIpG:Aͱj…?uw}Ųe/Q9Ʃ/Xq>&?WҍPj[wwW|޽W H<-QL +l =P҉SOI Wwɨzm^J~J<@P}z2<* 4M/Ǝ^$_W=CvM,h]fN65t0 ~ i`#֧a}q:\,qQuj+?F#[&WQph1ps&_@-Y[wlb:{tgYZ0ZvSBKC8%vTCvҵN{5tA,=&2Ͻ Nъ=}";հ%zdEi9#80@֎S%0H-!i S'Yk"+q$!TfږTJI5W[ҝږ &2,M^q&-FOIa5;!Jd"CJ  ֜R+ݐOT=F} +-TxɈho*٤8zP&W>[.ea"?80+@Ovaƴ>᧛T$d$a*xCt\lQm0N>5OƲhFwb:+w ^BI!'^ /SSÝJdCmI;h<>F5NoD C|ا F١ +PB¤#6zk'x'4(NYm%Z%'ij][]w >”Ia8Z%+ =DRhd)1}2cO֔%4"ii-:1oϞ  Iα*snT +H[$'&aGJDq8~}-ζt۶rIԛNp]~66uܣgy|w~۳WM;_ +PRF\}==}vITLv1qtj6v$8M< 87qHE{!vdM7dzZNhۄZ;$ƌ*qoq ]f?:!S[L"NԜiZ'?Y"2ILLӉ=uW[3t#^V]`lLvilP6$VOwjбҷp=erigNG~q=Z?-ғyt)̀_-=WN0⨎^+gTHְFtc;$g~rNKVriR&'vA$jD/{)Ύk<  lI_ ?m:ʈǣWz(+?Aϩg_۪˩e"iD24C5 ^uvd|Jx;UNMrsVO_;5(z4*ſ UM>pmg%UwH;{/|?ۍF&5/S6W};zt~Wx3I6Sk#*e2RdTp2-Si|C:wB!:9WG|x&F[IonF{/l3_=ĔFE!awZfj/v/j6wSmRGl#4_|;Vh4/Mʿԯ":l9_+u ?t;gtfw +Ntm=Ցg6ZbލV+}m:9^oZ3 HBUWlHGlqkalvwh5'H]f:bd6O&j-'[ZOcjEB;YLl@Rj|M'vMNjdmWD+}\A;QyrlȓdF>Π;sZ/jә:ӗbOV'c]-h^%FS⩦)"h " +*AZm^`>:MF&X@b@@0P4k=`,WiSAWޜ Z/h=>[ź[|nWv^Z$d pGK4RGxXGf|7״nnȧb2Su5C`,WxR0ɐcz}\[_0ns$.F[wѬ&iY"}>" 8r9Ax:X: $ v؂BRWv%iv#2f*.cfz;{qNx<^`$ȎvsV(6[.@b Tt/XݠKW:h(,`*~ݡLgmnm>d:p[}j0uO' մvtU'A;bizBd/5P#*0ZM+ +DC`.[ ێNS LXN:ηsm6F`S}7/ՁDS z`:'&r>V:|Z#\>.uVhTCpi#j"Xa2]"[WL"Vs80#h6[wIYtZ;{Z@π[V`,;!L!hn:.mYtlCMac}<wtu փ`m309ryPah^o l>V;kv|N^_-`::ƤAk¤~bK` +8]v԰R܁]=@ `׺6+fp֦, g9ZT`l ә))fi-VZ]@x-鞽txWZ5M&`i0#;آ bpSZ"3<1N[x;Tλ)SlzP( 5Ouk `z3 on` DZi\/|jGPf 1 +`-X:L-e)EtVX.`9oS'9hc`huN&20QXǺLj9SWq4(Ů7BhF X^N@t&1jBLR[Od 3wtMieRr}Gĩ`K1^l ;滺\@is0 +F]&*ZAB&Қ.N'&U#hnP`>} ܮ3LTݷRXIjiɴ5UIt-L&:/%ȀT0>YFMGtTS+4[m[|76_%̡*d=)(l0jASvs1,&X*0|=S0MA:RbKnNgs4-v!wԓغE97QsP02[Ms|2_O'bJe 1^:/UT;tbhnv JCiv|92t&f;Bz`1T|Z/ ̠L&0q `=Žٮ4R(|hHgAWx%Xe:aU0auUr vUz pm!?`s9 +2fh{v^H-ZNC  j?S3k+iK_ozk}Z-ZSg~_`EU m"e/Tl;>ñ"mmZ- T˚lϏN٢P`wsuې .I`tPkb!,O(ʥ fAdm:3Ȱ" { aVYgK,RٸɊ8 +nl*vؕWF:\3&e(n4Yǁ 6|6,;p2p#m[ ROOALVI YTY'}q*mfo}C~&9qv7&D +QpjT13Ir0b9Q *dkwÿ,]PI]ԠW&!V0b"`Y&ulx9> %>`ʜpo MVkgڋ=![ ^ϯ/3TZ%mƀkVj8;b$H8 +OWNJ[Q뤛$* lh;) C#.;roN0cϟOMmnA WSGG؁Jpe7jvPVx6i}?^U"A ,ЙLmTU0z1nLX5շۺ[Yϫ߃>i X$g"hBL?J7w?#{w*54 i9CjqD;X0daJ ό:89&hw-:t`D`O[} +_9첱DO|O@ 4T-VchꡊWF6O_Z@P a7UshKW{:" +h[zVbynos\ˆRkNB܇@zHv#㮥\V[s׻S_p z|~D4Wf F#CIճ@߲AuoP'aݭjwCeXHNlaq]x66b#sgF!䯖~ -TBzVBJ(+j/%n~>8?e5UbK2"#6r-A%y,R`? +A L6D[af{_rY!KzwaSr' k>j&e󐻦lC*a5'vgW n}1˷?>m41 "m'T 8.zQ{ӟLJs¼ZJ(i |}ؑ N\O3qQzn[#P{7~$Fg-n +Zӕ++c ].TxKS9ty` n*ee$@djѕu6Br-%-)8Z(;8N `*`UL=Aϕ;RTBq&}y8sx "جS~sr\!K; =kmheTn 28&gjsk=sU6-41w7+n3 2:] +\;#Y \0#ֻu cSi s`wG%Ϙ[vN=I50IpNEw 󷖽4Ҟri,SOhyg0!rb6"x5 +S8F3˜slQdup:"CJIA{\2@1p +9ܕժ +PSd{M{p5@0Ц/2mWt WVW9-s4cSÿ[:{#BJ5`E'a8U:Sӭ'ynὪ뚥O!!$jHE^e\ox7Sh٢mJ9$*VZ tLeB|<ݗN=NMHN!*)`j-q~C޽O"`:$(9?fȀJNo;^nU} +GZ/c3M)Sv-fUQ9%ojSA钹X+ETUwVAPhMlqsoeTz&%k{:B) *{~)'nlQTeSUvk1ܽ7o,*%!YtW0:L]fie2+=RyaG--`B19Ѻ{aj &TKGPLrW/\&hpZ8$DTA@|P9`w UZ6HewycS)1OJZvo"|8PҤgͨ:K+h״ eS~8V+uX.h@a% :PAc/]ɁQj>iW~,B +O{t!5$ 6ք0ih/rLtO5wK]i)ӀpKݳ^>\BLj #ul4ӡ5ﻃӘ +pIT7V,YQUv-(i +X<4h͆aBJ]ZikxOiGz\u.qi BfR!+J]4 0m/Ҥpy~aw*:TBJ)ձo~z O+n<{]%(<5f1-MJ+}C۶qSMBlRxK zo6 - EaڨXˡʢaXT3_EP"\zw(ks6 +@V9 ⨮]RxCllUAU e-i+m]B%2-Bha[#WTǬԧh`a5< :E(I M䌂eerԭ5u1A=EW5=Bk>˗WnWLxQRk &3tjJpLjT-ܘbfӹOLs*lRPfh=dc5)͜:*؄:!VkAh#N!fܖǂg,gWbP˰k]+hG(ygmK!$ީux^q[^uu$'~+R hfVF"gsVn4ec lηj+'MCrs&NXۇ̪pws+ PIF%Tō9:*h. ray+~чRk_E xgQ/Brt[#:6:Xƚg.3@R-KĴ +AzO|tnn}W uJ۬Sb*oEf{7 9%Ƶ +zk>ucCіnbԗ6}<|Э;J8oPۇQvM3Dm-2?_^i0oJF\6ejFC3Y#[d% + ࣺ%`Uy_ϩmǯyZ+]<u??v Mzo7_~~5~_;'?~p}?Og~pe?;{MωzzٳƯ˯o~CO%?RO:eǗ}3:ga~a~Z^xmAe~m$+Œ7x,tg<(ئH5;AWNVQBneUWgsfOzRvmڀup;pʁ .q/eX&&v͍պ +v+K8l{zx"k]ihdc*@;յZWNUM6X,wC'঱9vKP/W&TGߦΌh䆞Hgsw*dN) zt~w)ħQ"w W'Ņjd]b}_Oowrftԝ[9i;Ւq8aҩVLة|񲐃= V9Ze'g;D Ur#`W.dYz1u2Ngfc_C CV+izM1Zo,]%2 +tn<Lqc> u4NgOw/ڏ$'MHJMd 42dY4{ 2iY^ЬY0ϩ$xݺFݴ%k[L^ mt( | FjQ#^S:nCp|/F @CɄ Ln t\gZ9].diXmęA[ ۗ ҡr7hǭMq9rCX`Fk-if2|fڙz>5ǍuK ݁NnvLmaKTl`CKTӜm@Th5 + JWL{"5],œU'Yc*cQYPɇt~ [;&UsddMFP~b)TP ܩAdw2 @~HDsjGHpRmy6SR!Z0HǪh<|,XYS ~qo{yw9OVyԍ1!kIѓnاZV'߆*hTVQDci0Wx8c]*Oa{[FˊeVa<:W} +!;+뙅kC>.avڒ - =|\g<|Y*Ru +#AkNdM3\܎k-|o̬E!{>u1l|M<(r*xahmg4taTppZ|Oj NvP@D>bU㭳 zeچ>\Ď}\E*O +?r?ϭ#!Pop$ ^YCRsUk\Z'xCJTp=k"NHMȂfrUR~^qe-0 +Yizo 1e1;$p|hүA:uWlTf5x86_T?p6:INO#۽Ozuvu=y^u;W'/DIi9<L~w[?|@#yKz)@_k"V/RKuտ:9?,S~f\dž#W :\T'ϿWPnJ+%3g)xqwJ=3i/Ij`o +FӀsoXjb=N;soۣ:huKak tw6E7ǭv^_6N/Z~6:@_칓!J!H1L78}x''WcC]!4h p  #42ݳ(K0S7A +ީ&yzdF3ǥytaEZOA8u~dYߙĄ;LCD \q5'x]֟`N݉Bg$=A6i7c-/0R߫~ZSqT !o1<> *4\G:&0g8sa{5"uD *iJ.%8ʎkM0ު .I0ä" [ \9dFpk +!PYssEѩBt`U1[ GqHDdچ"i N2(M%%Ǵmv!ECڊ)u@Dz.)6\ez_o:*|96ͲH_` w!) 鑟SȴA|D0 +[Rێ2erlEOBp'iNPؙYoN$VJ>-"а&K:4`l quP˖3_m|/K0_m$*t⬢UV2C0I]_ZExbg\FS5( /<_*mD%_~ 8[5|tXdjZB?(h<ڛ JzCH\+? +y5г\R`TH4LD뜧n~i ?; Cj̪yT=ڸJ˼~* ]>ު(K{kD@MmDaqBeOX0ͣX576Fsv!'g't*dx4)Ns}k|cNF-fxv&2w~ (!=oI3mR/nQ_9%^p,ึoj]Qtey֛Lro9gHyi`=8 a;@ï<+cTfXn"`e2Z$%U:ӎ5șKt#;A Vb#(p34Va+fk?1Xs1V:9"ݑ "N~ NȨDqȄuxC~ +MDˠZnbR}֓>vyiX i8 +iϯl$|-?ԳG@K:fشhJ?*Qxry笨ж +rX~fJkl#< "oM3)aR)TY&WIWzmicOjX?,I2=kHp(/™ݾrVr +qVcc!Ÿ R҇YˉOHKS(X|z{B$Q9$FN1Ua8<QޝdEsze #]ڍ^_>Bha/V-damٵ+$7 ,\2Ծak{5TPْ|Ʈ2 p +c0"k)q TXwqC.Y<̅$%{ABދnAO~|`<|P!\1cc*.>P1Łqd"@2|~8 y }xzQ CD)w""AogagNqVk"gمc(~Xxo~뺽ZDJY AY4̸[cP/ Wum? ";<"䰮h08Z.S QBЇȘ z,F/mU;{F) +g|G^clve +3bx~,>z4) ,cwaΐyP9 3y>vP ^~y_1_FkV#-J \| d\F,`?ϴx tO^ ~T[niC»^F21!}F_&d~%Qr+ oU<|nc2Ai`JD{z)_sfkc+ 30ka%48W~@ teY},q{Bxr&Mq@Af)aSA7-Wf}`>5bڭZ.H##D%QiEb2R/:!59.J:Q uHWۅ9 e}tD3QԌtHd}Q]"g _Gb# +lD5Hk#l#֫ =[o ./6őE!Ќ y5R^#găXTefiPm'_ĵNCC<\6jvUBĒp=OTRR6mPV`4~K1zk=&uI9l:)O@kMSZ< \̚\@|FF[AP :i "!pLK<.0~nsnGYZ{ /-8O._q"zѯwDcw==/աfjȠaEE˻OuJ,[eX7mvcR ./5W[$F笀,^= [1+$vI/zfufIdؠUj ":ysz' "44h˨ +^P W n +Xw @X$ndɅ?{4T1Ad#ϙDrD -8jwųsGD:BUlSRExzP `Ԉ|s%gj?M[K_RaA646 ]@3 +?"ifq wuPR{=η /ӝviu>5^Zydڮt.q<3 )&rʒpGw1,ό!bvEI4]* +Hx&xO>2188-.vtY禋I5Hs~1HӇ#G#?a##5ʗ? {( p+Y,&֛+oȃ5w2JvGj3q w",?y BڝQ 1X}鐁fk/B`' +'Vu |DsW8>#h}E6>_Xd_AKm+I穃ȶ+`7Otu(g`qdJFٵ3Րto:s:`]UslZ4O슣]ߊ{MiU4 .W'ns%QUڇ,txmb?,/wXB92gj9 +z]Wn{J}0ڼ^M1=T^G%_ Y$DB[P9x"R>ƈD N99QBƍ2G ~R+A)o'UYp":+T/^.+ԏG.%o-4}jA]ˇnU,B'V"UlGEaFwn}Zvm1\@q.A}ny,,HR}ȇg2gJy)Ϫ?wX+3{;P.*YyWrl+ Eˈ[Y` +m'hod6ao$5YWXy-u$!x#~1i5C Ǟ:?ǜlfyXcON!Ey'=/i6;R S)e gwCɸX0ǂ/nX:;7#5t2MHK! U'YfR:zB::(As+_b-7 y87PAϱ(TK: +}р+31nYB\%JtFdMsj5HNa!HtC]c6p.$"4&2?~Y.<{,0 R*ƥE@5lN7߃q;wS" ?%8't+ɉ0<")y~@+%!1dq&'GnT.ك +#d8$\AEQu2іQ9YD TYdUjI i-E^wyLafi䏘2#ͼE]Р'#a:n2(4LKGK@>[bc1SPޫ3 rVl%0Sw|Q%;= YyqmM&n *c`Pe?A z^Ry <0?uhG)y:s$%+Է^~P&q|wYķx)B;"NJ`rS8&o\g3:Yd e~'spCտX ~3oM1TH9y5%P>Bi 4Dj}(td +D +iH2dcK#]?5u,~T~cTϷOoO좢ɇo ϰp%~?.p)q8VRPrhtB8blxT#ƈGe)Éx1㑁v()r#G8B`lq$ 3k)qTf#;qǑA$I9Rx$GK69b|tF)ֱ9 9-H㗣"9)g1!e9TXL6*|ښJ_qP>bT\YwV XYQ2\ ! vƇk xyL56N3pRǸ8$99]O //}K/U#Ζ͖4?&cn~1#>z=JCBW| #q<?~y-R ^ca9`B>}L=ʎX7ĺ_Vɛ>UrDwڙ߾-nj+i[|xwԄqyQZ>"!r;Cgm搭3) H$Q]ҮW;Ǹ6KiU,MA% PZ[nrC}sE/V x `~1Hl`r](2peC-$)$of)}*Eh`I$ ;F$P7ԝ!)&P^E//vzabDT>rlݥ_ $زj Z^+܂$h !ADTf$~Cd{T_ܬ-c J?eS&C2n$V/hrU&[,nT~}&dZYE6t#[(oIX9kUe-g h3C; Y< +}3[X^+MXb%$R 2u@e>eVIS$ILym c +bOOga9ĪmTIqXĵ0c\UG!@K4e>6mUhm|ԫ?kPĈ8}ZZ>'}4* +B*aӾ0t#Q-H@ Ǧ0P$ٚRQF飥-#edIk~ߊߟfd9W?_;{e~8Xt/n z~ybxeyI7~*Hr/ 8C*ZrܗĤWrڈpaN|o8̍%V8چzN4"#RdTgSlC"V˖jpYJ"ń )ATW@r4WQ$nƷ7OK%X7 Z*Q4h'N!bJuJ9:e$q4ӿ:}߼˧/?M{pJu@wcTo6[y_ϯͿ'?_/ŏKZ0 \ 0{dĞXBv4}ϲ^WjndN鵦ع:Yps^+ #xᢴzcZZ s376 /[(+CjT-fIf!%ՠe咿Aogx &4|g:ċmיwߠnb%" g>b_J4xm5dBBakAV߃=Y"V; ]FbQr,/{lH}6^36},1*YkmFeP pUhpVrpeddD6>DŽ"|Jl𖘸ّMS<`&zm-oCu<$EK,78 \ No newline at end of file diff -Naur drupal-7.0/modules/simpletest/tests/upgrade/drupal-7.field.database.php drupal-7.66/modules/simpletest/tests/upgrade/drupal-7.field.database.php --- drupal-7.0/modules/simpletest/tests/upgrade/drupal-7.field.database.php 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/upgrade/drupal-7.field.database.php 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,16 @@ +fields(array( + 'name', + 'value', +)) +->values(array( + 'name' => 'field_bundle_settings', + 'value' => 'a:1:{s:4:"node";a:1:{s:4:"poll";a:1:{s:12:"extra_fields";a:1:{s:7:"display";a:2:{s:16:"poll_view_voting";a:1:{s:7:"default";a:2:{s:6:"weight";s:1:"0";s:7:"visible";b:1;}}s:17:"poll_view_results";a:1:{s:7:"default";a:2:{s:6:"weight";s:1:"0";s:7:"visible";b:0;}}}}}}}', +)) +->execute(); diff -Naur drupal-7.0/modules/simpletest/tests/upgrade/drupal-7.filled.minimal.database.php.gz drupal-7.66/modules/simpletest/tests/upgrade/drupal-7.filled.minimal.database.php.gz --- drupal-7.0/modules/simpletest/tests/upgrade/drupal-7.filled.minimal.database.php.gz 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/upgrade/drupal-7.filled.minimal.database.php.gz 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,155 @@ +PNdrupal-7.filled.minimal.database.phpk66}~r-D3{q8~vKU*^T=Sg-$HI(E$놅?O?Of MI*mѕ+Ӄ?ɱvH#<3ms C?I3JQLd7:{p7~\zكK3|<)/im?%yLK07^^7/>U _a\-x(6CsE0T;y½/|3-l?!6|>J(f5c/b>pL%a0M#WFa6 ~Oy0 )sy2?qΈV+|u< 7~%$.o=i|A7W{ !Un{(%7P(54o,ύ;\vC -̇Vm=d' #"W?ߓw2csu TVj7}w8!"TMR>t?Mv#sOZ&^!4kzn) ѝ-r):թ Eɞ:Zl| A=b(-C:M[3lDY؁Yګ&#y颪MPAK& +U2c{fY]н]cwXjM:rG;˷w- hrhu}hy,k{oX9QT1XUo=yv4mbzA${Ȋ+4滋4]Oq8$霬Mq 4!"vS0 BhgfzB8۴r~*rtxU)JIL[ h:6[Osjj$#rolv-MB !,6&0a٠JϞH3 L~VQ5D߳"\acckͥke6y &u )1gb4{v-o .t"! ޛϻu<[wȒxc#{ {`\u)?zu'Zuµ|.0ZvAH.aoI9>8> ݀X漠ЦiA}PGVP>t^oc{q!%f];{ivl48sVV/F]Yl"tOUK/jhglroj3 َ=rA)49Qee%R"z`4( ŽZSnE.vr@4a8sw*T=tWT}I^5N]UbdM-scU3}$T oPF>;}찉mkm_nyV3/4+쎳"[h֑Yqg6+*%S#lV1qg6=9Bge~\W8?8GR<5p3en\W97'/kAj527.sF^`832ef\fƓ<مw8ᰰ%ҽAQ!$ *K5vXf˄]B^^||ƭ!<{݊8fUz/Axݭ|acЯ͜a9Hh +Q ħÚUm +NRnv *J@ 7la)“ zuHon]ũnVgQʟY87!!fÇ9^ѢoѸ~o]_2_ԏҁ4-_)y'~_/}|9Hت?$wq+8D}TwOUpK|YCU([ |U0 ZSk  +Z󺹼ϷJoU1:α׷*Xƪy*؇ǃx<*H +ȃ"VIyU0-`!mX:r]CcMldU0Z=#klD* +.:U\Gs<0|ѹÎ۰ 2۝;ɾWR#Rn>i$Kx+iu^־ة-cvzBI{Pĝ>q&3F-ɲj l3t\얔=PqA };f/}P +[2=^q*㤤D(gm +KN:W\孛QEbXiN V+}!8zyD~rxo"T*Z&}Mx^;%\_zav8K-@.Ljh*+͚v^,lM+$ 81;r7꩙׳si[59-+Q=n'ah< +#GIl{zIc^s ԖPpYPi)lg]J 7A)%$h!LWޑk{1 sO2#B_yQwiףԫzmAV  lD |Zr渝sķ7tU)rpO~*S IIȝ#Zݒ@Lן9[enn`#*9z-@iގn_+SUd/'+Oġo.5۫?LVO?l7&Ac ;pƃs91(oxw e(_ wGD;CbAWog$5)SZAɐ ٚ@HQ30 [V q"TvCZ:}U6gElw-Zil0浍/חS/vP ҩZG$ԉLHr4,0#HYI጑jNW--(@={pZ:ᎣnCoviYLu'-V!`&}H&N-tL#W.SXXfjuZ ~_8 +C,6oG[ujDc^ '5\ci}1+X m_ыC_iNh *!'D529Z|/sXHcMbLpw/hn/] OkH_twz 걅ȵnKZ>Z+zOk~OOitӓ}%=; Gk}Pd9nKp2 yzSp|-- +?gbIN) ҫT wrAgM(LKS1YU&օc!]8Fn9.E Fʪ|> oeƊ NLx+w"Vx- 3]4F3r$H"AD +RP,Uɂ]޴}f:+/n`>{E.:rvX׀"I˙͒4.Ww=M|XG[SjR_q_Nܥ(!b(3((9^BBVTP"vU]\q^yu+,F-aq,wr^2v`a71; + E +V "RVc/v!]Q~]ڡO@E,]\=5z7 iqYp$;cR#69¡^ k7zM̐ﱫU*KJ(W)8] >]}EWw#v,PKl&$c x ^\}P좩kF{q&V)$ &L[aDib:&P,́5L0#+SZTkr.ְDbeUQ;AkrЪEd)oBrOYD *ꔐEG$|jO2`dO Q!M9P!:I /Htؘr;4E[P^r6H\E 0F";f6sIlsV)pWu|{sRJџPZ ,U[:y)?s%<=PA=iܒӜӔMfMqN(ۑhp 37c\ +`4ϩż&ʍ"|08v Σ;:C¸Y^,Ս^:CaۧdGקK~jmFѧ [ǪS@gL2_u.aO8ÍofWU]x',7sX(R~?$9wqJ&fY 5p ]+"&Etĺ77l/i M,۩_"o~ u@)TS%o{1J,siBMiϬ`®JcpKӇ epؖvJ Z+QP6D +宅up7p|u-U/PaG&Onwp aL+OckCe{QH1IQՏiY7@ oM (x( B)EvCO hHuKiɮ g݄Ron.uq*!cCCl ۗ&G84Ya +NMx &F,"Kޣd32FQ鰞wɳ(.|n oI{qr{5[#>A$>\Cv歞^-9=!yp$vu2sȮH [<< o"cWbf +Z\$6]٪-2&اalPu@8׷`KwcѢMp|ĕ)Z]z#e7옡V^LhcGgEn1$ĪeXq5H_˟GK$q3U1H1#[CEݲVZh#"% +ej@Xץaq'?vv Z9M\l0L.ۥ +>kx9x=j_֩%T@Fѷ}qij[ J!~_/UT“[?9NYVQ*JPyڥ>C\9-hg5Zx4 + Y[7Dž񤆺%JH'{.ᖘ tM/wX̶|H?;VjIX`yD=a#PT%UZY^Rz{n9;/*䏍n+p?:=Oeऊڥ;*8viAYp;6(N_~D%jwK |QAYW0;ʬz=lFon`ִY/ؗ(e٪f*5{jv%s\fS1#qH0.-M=V.RUvDV2"<ǵ+  tڱl# ,)%n"ڴ**fd =%nZ[Rb.1.KtZݚZ#Wz] \=7m0}xGH܏05*Fy=BT# Y)3__ -7e"c3]Kx"o7k_2bbXR<;hE\ʟ0$dZķ7|H)[H:c)Wc sQɆf_obt;2Ϲat8N1-y'Ho>Vҋv?]u<<^7녨9cGF&SQSg@ٖͤ@Y2T~bn-%ѪVE*Fr`hc폵K6Ս2Oz$[ jk֤~;'@ZȓB*'gwƤ|,x5LT-{*9ƭANhH)h%XUn833}[$m@*tʥG7msKnߞ>퓒]&9Uh$C%y@= ?~ݶtX _='])oѐ*"GPqNm;&OJxT1Ru>0OI@Wϴ-U +!'b&QjӒc}3qϝ~ӸCOWf^Z/>Xr~ O$A!!dכd)v4?[~>~ݶ#Nґ;YNۃ=4[OgLtm=ӭ~t}[4|Em4gKhʊc/W_tiٱߙuK_;8%i9 /K_ %(>oZ߫r_ <\n⑸U[O2n mrw ,MzD=QyAuN-91  =ueCrF(*IE.;vtW_5`a^GG%)Uqv* ޠU'܎^Tx􂦶l=qvt4ug7(4|v~ UWm] +W%)a{n z5JiZOk`+邵^lX}t4{?_+:۟: +O X8{&f8-6KN^{<@gͲ d[/NLs.M +6 +YNFmqG6%jwU?VkPX^Q:/Ju>O$bV4VԯNxĞ곺H;$QHF|"@;5{y%óիg [vf6(,&Rl}&N";kWӚaJ;lP낫Gk0Րi jQOm> m;ѲqKk F;[>]I3g}d7(P&-g) ? p R f:wA.Bj(wS'2Eg U₀\ZMѬ; +=dMW%FK>2\"=3~G tuo5>]_U t }V ˄xk i$R~ꈟ#$+)N =a\&!C,R H +7'jv{Ir0\0~p`u.-lM8$ CeuT-E'=Gzj;q^DQlƛHb4H$Jf[Y=~Ųy`D@N]˰m6PP EbA[ZoӟknF7 Hl\=2-\9lhқd腛pЯ>4CJ-ϟQt=o<6lcZ␅p|<ә/6 g>7fxǛM&eOwhm|]k`>Ch":sH8F.ܸ.>1߬,-nUd48} 6$bc!=SJ 2.ICpHd.KDyS[`D7u_繾m2_[I2A"Y(emZ.ֱܸEXK~ͻ4fJc>U3KSM +׏H獉oljMVT齒NhfƔXCsjhvP l(#<@%AQ +XpX1 nkhc蕀'LAJzh"YIc{;.U+VI$QitݗA;)&i$|(Ya( (_RQML+zVPqr9^cHVV-U1((', +l2W^^kP-U`wp' &qV9nK=/BhX#ciKp4xVo,2ƯM9vF=?e8k''D蘸>?c\87\Wm`U@Zlƙ!Z$]܇_!4%A; +:fmz{qKr -Kq{+6<ЦՀѺi5'lq$Q 3 +s@;[~xKs0(OiY[vO"b gr sGʷW9'sSΊ$4+n5݅kK8T:f*1 x2s 9v^yf]t&ܸ$JMB?N۫8W9>$L6)wz=m!Y1od;//G/P sE6^]] 3kB] pA#]!K!/3BK ԌGmM-05 (pN $@ \2^$Dڼ +9!(KBbW@ @ ^t.>E7 %~?iL]#IA\䋴3/uPC׏] n3 B/_h_>/3m0Kb: p&а[o Ȉ8H./qhqJn*[K@#NrMCbbڠpi$Uv=F;vSnjY;ݤjnP>{uuV* U6޴nz;9 lVtf^F/dE(}eߊnT~Tk[+'O LS_-07I9Xg\ }M4)03v}4q`@>HlZ|pmd@"2|V0i51<xNKS_a{V(Jc.nrP›ú+Ie:&h_ KM:,:YY*W gcPaLe9ecCK<YӅs7rf,9^g#(9":_]/`t:O>7}>o1oFy1U1UzhOY+tztV`zLf'6}"b^8}rQb,ZhP"'6|X6vL6EɔJٲVP^ݧF +\JAipNjiҲB yKKax=BJڤu^[/eBxi;,eLP箍$ju:׽1Y@R4Q`79@߳mKtj .i`M2D +| 23]*_HSuC) yknREyA|W;5@j5M5C71**\k/E l-T'9R3~]&/ +}G &6GK\4Y2f*13ҹm`R5fD*wByZÖWTqfM1=dƕьYd*ܰk9}/LY|7Dߥ͗A3#K7nW`Q!?Ϣ4:͇_J+xfMݫ$a_H2^Zkz LJhg³z$s0Nhy.1 y'f4?6AH/(`#ozK~ׁG8%ͻy9>Bv378ۤ66z1;a>(ע?z*πq8 K;L"a'y:Ϣq9v>gce2)7X!7c:Q7TƯE}ht6jmky/gPs`9&Ci h2t?=~ǰ$CWؐcQʲai?R^fgQ^ |`cY&YDs`Ѡ:#?l`av16=Y of2XHIQ 3dHX[ɡ[-)RMs1͈Dk .zّ2q@rvMG#$5hҎ `_ 5-AVF&#$}Z$-T8 6b(nM^Hظ!~.k\).*k|rk1x-`ss2NP=vwCYtYᮯ2YСH? loyѲ*bJC!Z&iwHu%h~{|`v#R.͢/ ĤLс8y;mg~6欣gcijR7uxs>X t`W9t)Ē Nj^5${I7@&*s El y}8d$n0uZƙaɌc *(/0;E-~7Z{ci;bZ짛<4$*M\vR5^du}&9$z4'I *-̝_hKFF:(+ַ,XgKGVj%ѫĪC;nIo0?`kFb8b3qlED&Nt@i?s GyLal>1vuskrl޶Bčt{ Wfƀ6z[bRifYll3N<_LJlЇsH?a1熠ͱʪ'~8M÷ب?d"*U/7|ʚ`xQ w3ݐ熏m3mED^a\JJ+-g3[Fb쏵yJ3mݏ rk8#_oBiʔ1P68,~vҤ"߈M%caR;V4Ro%9ԑ'L`\wɧvz%-PPa:i:YƂqiyq`a-4Ǔ0*}Ne]:|eZƵrb,]++A^0WB +Ȝ5[$1āś`+\+/K"0o~|'p'`XG8`)\8X0l`cmb38dkkgѭӜ'B/="J莳3c(:y + k.vϗK|sE\INfYlMfZTX)=܎9w Nkd9RUځ&4xXӊK3d2iӫw;yXa[ >]`D~HĨr+v 9ͅ.ru?SP)J*?٤pR7͇l\#Ú]Ř07c2;F ;#P|?B}|9 nr-K9k SYog-pΛ`C\U+&RM[2ճ=~M 0Ml!n@ooAw8Jyy3k|J'5h}eɆyGv'{[/63ֿ{wfέp;=/qp>T0R/xWAj? ,#HMH"z"6YFtM1CY:i͡bޔ9ٹΡZIs9t5pfy,p} §^ +cɭnb{CcZ =Db.SIHr &LrT9YSjX,^,g6;A,v|3#$SZ. FnM\2,LӮ.pt^Q ^`"FN#bv~Ǎ֞Joݏ +a2|Oc3U(}Pچt.51Hr\D:0 P +=at!Jw9rc:CXJv;bgFխ #'r^!eo?/+(+zG &࿃O> iĿw}Bcw@&8%U0?^ȷW2—ʨUچOspu @ne. xSli g^J |v-M{pqУGA7CaXz|68*M*ccğ&V2{)6^^3YK%+[x$عGBꛉ@?nޅ4 o:`81*8Ȭk PLyk%U\}o4ͫT v VL^&/-+d'=6)DilW %൰F a?VsV"L{pfjN3J=Jӑ=U໤XG+rW= WK1zSQ +U}ѯ>%s") 'Jas%N^-A2yHtݟuqZBU3tDkA*"U"wVU̜/"\thi5Rwɗrz,U\f2KU]f &2cbv;<-!K{+̸3̘1̌IhƸ,Sg +e2RWiXC,>n OEPvqç h3́07^,[ioܬݝ C y }i|B7Oㅠki3Y~ P-ٔ!Yjۛ"oۮA\Q?o̿1Q0KԮPJ G`Vsև kTnH]e 5U`bv%`~P[J:WIfiDf4M3\KK_ H4:o^.i SvMFӋ7ń4SV(c{e7pƝQTtUf5KԮYw30Ѽf\ ׬|kV~kV2 l`3AY5l")bL*c)R:69fzVpU4uVǐv'_\\sCZAs|:p8k_ +ȇj U~$MYj~*CP>@TDRA1ټ=PX䕟Fe\T~U/͸ywpŦӞ^5jME+5:LuyЇRrppppppurx6n~~pA}+YK{ƨHHiB&+娟`ۻMQ)wP +UҢLyNr"+JQ1[.43ٴo'dғē -ǝP5(ޡ̦syhV &f +eI C=9%Bk'M6Voԡ_=%Y/u/tNĠaJm~.-B)H0XIIV-N >e ].t ҩo u3I ΋b|nx'Bϲc-]dd(J|uDRAi +m-uS?:nR$dfb%'!o Dja?ZʺvYZ-'@g:i˔Gu(]W@|*sfq>-JŬƩ6N]zHv}^Zl !3l =ݝ,&$xx4 h:9T% JͻזGD{9X#gyǯ%-3|9j~Qᡁ7b5,i] +>xdt͑.Pp,en%kLBi83yG?/@qE+馂-ntzzG9>y-2@|?}ET*\I{$ʨvTzb8Bf.h\ЭQ֙|JqOߝaI˪]tYeGՄQq%ohκA[Όn:kau'$B0ܴ} < 1>I9HtҠ2<' 50^|^yGQ8g_KNݝI. _cp0 $ZZ}F_ˎK=+7/H}4[|R\жd#% "s'#=-tDoᴂܷJ_])߾#1SbC~ B̏4eX0,p!^"Tȿ1i6l˥;EAT( .!K|&Ybm6Mv3m'݂-0ϊn [EdGӧZGIҞ;K`;HO5Jۍm6i^w}"1|e4]OzgJFAoo܇GC-7Ze<,@sLP ;Դ:Mbˠ󦆫dHL %pPyZ֏X8ϯYL0mJ7or2qߓT)}K2Npǒ\EfZݕTr P)Q ]bfz~a=cnV}-q( R%#31ltM|5J |G pql g}Rzmp/7,MO]LIrOy2cQ XHn~udmFѧ <|@>Kx$-KTdlPq `< +'\| (O1gi?3yi¯B'"ۣ4"1=jB/T*ƑجU윶8N)?1ѯRfɱc(#>J>{oOrMz#\RK<$:{]-Ғ>a}xix^[Ö':,mNR3fs Ug_L~R͞]>tFuwS` ++dfb]cq^XhgۓrQ`IInPIJ*ۭfE| [|=:ٰdGIreK3zSj`,(wE#dӲ6 ֶRrC~\إQB:)3*d}Y'&##h )%!s_2IӾTKgˢ}Kf!)T}95sqGʄ{_j"h)Ӄ%3(b_eq6\4Ԓιs_2HѾ4K,se R%3TnNKqeIsߵ/f +}  hA*ٕQ=T9G)?jٹ/FRޣ/uczvR{ԃ= +{T:T{:xݣ`v=JЗgˢݣ+ >,t߸uPPݛ?ȷе~[5~߸{ۘJq.@]4v@' 1T9$fohO3 bgTT?@SxXw2}e;*bg~'p*I"eF٧'<3=iQ@H">,p،{ۆ w?x}F?+qE@Y{촸N8\~*l +.r-/(ΫlZ.:b+{ M3qDۦsܴ\T5 Ψfq#,dUyw4v/ڤ]KM/9\x]<]n!q}nKM}wk~Өw\ye!vC^\_j@gy!jt\8|e!톼~ @Eːb+ۭ6r0n6*]4ւʖnPkn؛6TZԸʖ8tvqt[5*[n q]i7MTJ|/6)*tZS䷔T|2D%nͦo)~z|iW\T!򵻁 }㖟RЗ/lҩlF4r`̷ ٴnLVc8LqX5 |]6 )騤~y}68*]9\5@!Tƿ[yArq~EZa|{ r@Ќj-_*eFۏ6_^„>Fǘ6;ٵ+|ꊝΎb类"3Džz(,q{6= +B Ro0'eG Gs +z'Dagb˨T݌uW!Ӄt+!]lv>!@1M׿u߯x;-Gxs{ h-o%`x1 {7+ U]zP??P? +~WCe=Ji0v?tG +Í_IwE9G@ƚӲPYvqKn*`~'>1GVV!;/NyZ 28n -X;dWQë XGtJo[z;{1MKK*w-;#Ei;%ƥR`'} +gh3?sxNc ^xų\ S pٳ0n Oe"ZoL~`H`{v[3uuCq?f}or Y_.ݎ|ո?JHBM.U)l"_r۟w8L^~V%[' YagN0'|bI]Ͻ 5 'Ώm> o2"v}c r}<{Av[. oӜP}ujg #7 _?$3ƇPMɍ΁1< C1 MP +?\.:~:{z|K컐E}Et*>]?M닲hxHNaMdjI(C6 +_6?~^{ƌ|Ж_z^鬚+rNt/ÐI&y)!u=;Bi 6Z#il-` +wB wk??дEXv A}Qwа};?L `ZKQ>bQiEܻ0y$ydi (]3l7Zicг~$<Zވ3vd~>2Kv +7H`k||ڝs)du߅? ڷY̟zzNgJaPJXb=5xp|YWlr3=1ق!4S=c+T]`se=ϣ=:?ۗx#q?~/:$wEݛ=ɋs-lB + ,܇Wa$6x2"ٶ⁓&>_;boB7~8{ gq:cM!N䏍0w߄[E;3p,O&&\`@ցb{%0Ľ84!=w5W^:NzvU3ix3H+!pHȚհށGp12NؓƸY8ܮڽso}ޙb7H6K.ە_' 9ki+׬&؋OrI(h%V i*q{yN= #P8iAZɠLXaz}]=W |9KkF/io[?}TYZ8L.;`;3sLq(w\}S\L?J=l! 4$E;ks{<&;,sW~tXop_uD")@0 >5!]FAxm95Lwqi6aI#CvgD8xޙPL`aCb'r;Dw!%Ij}H +F$q!eL}09z#u%=yoj$ұ&fGF~ K J^mbb^m8XazBM>{oOrM9]zY?칔5.D]"BAIE<EA ]OoA,ϣ#+y4k>Cy]\ߒ>g<<F;|& +*vaWYOyVbb|fL:Q˪ #Þh#Y^11GhDS3"1RNJnɦ>F#3)2?W1'O9#bɚfi/xjS2z-űC̅9L}-M՘NF,k12:1 s<],tbea+&odLKSE#/F#1L]w4dL:@+eN]@e2r6vҕ̝C-;p +m4ѵBD-SS@[Q4EVɄd2 łq +3w$S!FQY%eOt}dOm/haB#Ś.,+381vAj2'#x, d:v&IJU`2TdY:L']9.#HVYDvMlkla#M[hAXdyAUL6Us1Ud4]nD3 sd-.̅OMu$3[6cҽg*++ \OTslS4S7DSŲ) i*cUuq& +Qγai23caN5c11mBmL;>87% +vꤑԧөLq,kА)tMӉ1UT(aSkj{jL[n*;@,LY4E7 fx:ʆEɖB,LARm; = ̀Iocel;0Sg*v1M$ٔ) `2 ]Y1]SU]XNLӶUb @n*D!ӱNTfL0 y{l静@XʬJN 22GQEc&HsnĴ'k#P͉F@0&L[n2!@>1Ʀ1ihOc;)㦛D 8mGhLBi{uY% rn MiȪ:ZHVka@2'ӑe9fgNڼ/ Ma.:d{)@nj8,OF:( 0#HgA\BQ2m1@X^JQN'Ă]:q@Z:Q`&CY5!ܶA;QUZ´0SqTX>jMyA@d,u aujNXHFڊN 1dEcs +0%x $PLԝH (/ ԣaY6f6e`T-EwlSt[ h,FX4'22zRZ16tUMƲ9ҁ?Skkpdp6h9L 3`*ѢxELy[c25 A%Xq~Mp;I,N&c.P]cԷ@&L1`[,M'02t + +z<`v.s +ڃiA&`T&wHsGt &+0@ϲ,aMuma)cB&('c0Ge$ Zchdw ,U~01BqzH]!o{1܀{SV;xOT`ќQADfJƓ2VX 0ZaNA)LCP +:)mE *;рA8,L@ŧ3-$Ӱ#-L zPYuwOm3 OGaP;1$ 0S2h% ̀M2ȘLug,S +P1Q-0-䮔*:@* 0 tP̉l*ȓ&SC uu=-&fNtn>P:VF&i{ٲ@EQul*T1LE; t3ZZlˈ3AeRyOMp>Ԁ4PMc g@(&Z8CWzYxjs *8h +[펣!{:( ob'#K3T8l(Ys@G~Cmt؀cb@G#RPT{ʝ1Ѥ[f(A_(0'%~g -GTx6(5:!\Jw=e*:b<`0&Lp}S0SE@䚨M,dBt2Z!~j:Bd؜S̱FƲ OODM(S#P+! +N*a +j12^Xرd@ 6IXMn`,7{$Q , lWjh[&~٠Pp(#tjHirȖn s  d42lXTj0̩c:hlЗ`4I%7 +*36Ul2 +<2۞g)a4h$ kCƝj75hCgl<dx4%ذ4Sݰed:65 1/Ŵ3-*)!c`U:34ijMA7U`$0A &'t:XB*PQP,y2  1u2PK/ed D~˟(-F_laʧ!m4dbi2 @ͫ3T5y6>]7^p@&*b*[`؀FNl5Ma띕C oES#b; RX,ucLL6F`DXB-0Vpj,Tk +E t![njT0N@F5<%0]`˙tVf} +B,˂JeˠM }:t'3t/NaB Yq&6j7d:Ϊ5).0Y:>ڹOUJY<6@ƌu_x8*l;haj٣x +UM׉6 [ UY (&9L&``#cf7!_kTcTމlh`t`F; V:fK&SSԚ`Ϡ:t^Lx- j7W4?[#пYLccd.@,Y{ ֙Ƹ }bjmMh䃹`ڂ=c3F6г5R6Lv@ȶ&NgYe_* -seA0MzbX0*&Nl0+4yoczD12cc\HҴ`tЇc({r:+ߵ1 ?֭ &+h(1M;j&iII@x Q)X,EgLBS)@mɠtmA +3"@y2Y@'M0FrczB4˷U 042Q6UHZ. ԙRأYk('C`|ȠDC/i)pxc`Ԁ kr&vgXY˞S :qtC̙O OKSzyykZm+0}?:-f +L"ّ*R6P=!&H+XdTx:D +LcݦX4~*"#Hl91P$%,N+FOkD:9`BOmuA&cj<:cUcTPDm%ABܳ+ɱ4tmĪam5_!0VѲl 4xbe֬7yaJ^pٖN!9XDYT ,M}HPg@ +6!Җ W%{},۠O$t40ۑ`(f TO6FxHpDP +oϣ8AY/ߩZPlS} !kEnAE)6F3NkI MdPa"X*..(A‚n$ɲl0`E@5vO$$ +xBP-;xz:l<^&5BWa[_,N֖yFT_Pɔ,F=5F2(bk hmSe  CWGUO` 0VeT#ǜh`ϲ(N0h4T2)͛PA`am:.A-pY`}>ًѕWᚢic(#T'8xd\$iA}$~9Y`>:Fي:6ʹ /l(OP@Sڮ8%P#D1Op9v:2A){1@8a0x8;f(Po +k/bz4`fP0'S tu"/9Lƅu0xw pK6Ƣ_5t˦ :FNe) 8eȓӕ+mI$pKb'p䴣6 BGm[];$;Z02bK h2d¬& X + q-WDMWad,}f׎X, h6n #KS ƿ ԜؠZ.]YSauX5mhDC|f bS 6!xM0,Y]XZ[-ox#Y2 ^Pl9AHp$`KAȀqQr2l`Mw`^#`EӼ'h 't=I},-푰0-8{mپJ~}[gq~MTG@è9h poPy j'ݹzhVIuOc֥rRPQ)!wMNU0w zD@ Ss oB_7t諔QUvmV~E9 mLǥỊAkG;Pg&=WU+iEDde0mChLXK'>vf^` + 2r ,~\vܮv%3꼟XU%ŷgl?'niC *+bMɧyD%όsiߛі;[2X$ + Ws@l{qW,/j>ۻ;U%#2,ZC[[=7鷷`<5[hzTjFK/i@6&LNdžd˼<ƕzɯFؑZ-T*M7WxǮ +L+i.._t<=sύ-+\ҫCLT3xEjnpEՕߞ +(ħ>)JG>a87Cr ?s #. /MlJ_DB%&ܭϮj'g,'nګ[yt{2ޑoѼzY%Iѣp/#w$H߼{.5Oߑ^JZD߾khoh^Wbīؼ +q{E2ǽ!pW\gKt;7 ~^ALLxn\;?D((~- Rz}чU Z *+k<ҷ]ֺ/krBаWa'|,"1=+ ۣUĶhko6uIuPyRH< +Tn3u8|}:[g?Zйj`+͇Vĵ?4wYMkIHn1=ڸ#*ѦF) ɥTzc#&>= +@K"e!=.ZsxMz,&m*)d̈́ W;P`d\nbXY* + E%>q0ޅ i=ǧ?c}?QYc|_7o?t ƽF? }L)# v|ԍ0 nK41Z3c[/=/aBQ9MTH룰6K6M2Tikp8l(TҲxtćWkt\Ysi6D+QSkVk3v-Hl +$2*kAK2p(Mi'q#{Ezcy.LsUm "r9'MRB NX69PapǦ'kqLKm0 hr_!70#x)ve #T1TQ) l:)x;G Fq 7}lr{?6nH%R6Q˘SL绠TodoXsAўot+' +u2@ç{ۄ/5SQ*@M +$dB|W.ΌQ+$ ק29dX~\wèIy~YgTCr*0)e01 +omrV:)MKi@UeL 0v",U2Vo@8ȵ<*"!SV88, (@H9.3192,,[^:x@JDL y.G ?a6鸁dm:{X!cZ#!HIұJU&^4$*]L" Yl> +0e{HJ:UB*H=#' D:i-|?q($0:@_A7v a& #̔x{Ohv:g2D)a)2q3G4~W.LXF r2>&(}<*&M{8 ԖM"zAٔ[*Ņ1B\kjjR!c!Y![DT8nC}*$S)KN$$FuLg#7+olx̤j +)3Hx͆4=absGFs}}a Q  ?Z,;M$Mn4'4-@ + YBbUyF>kK=2*3`գM7U~^5, +jMV7`qxGsh .p6&G-]5H^3nsգgiw TojҒq 1%K'dˆ^׳\8*Ɗblv[r[2!->xF^:N$%IKw4o|&M@ÊW1P=\%)RyxP0.扩3GCr!8JsHy#qirW3oN5C'7|ܠāKiLMJ<,#M/>P@@J݉+! M^|#ioJ$zkv3 AU=z@`Ǝg^'>N0*64 gzG:~dKƓ*%m"[0w$mG@l[` cʤc8BBxBV;/_kR`HI1?Hoh&BמE@d!V>.0vGQpXdIӞ.hjXGE5+m4}^'YuӘb*Ĥ5y-OVpQG>1y{1I:qIA9WN*>ZX,d mֈ>3Ft㧋 kCZ~hCÃl_VqRꙬY3/^:zɝO<&(FrvRaA*ȌbdF1LL 9MjK3+n$M3~=`&U:Sd)qdgoC<]jE!RS}p5;) AɲQTx[obR9/O:!L2uwO=1<*`]ɾ& /1'XN<ĉCU)% +S2ie2ta#j +Ç : ? .{ź59X~$ y6كD̕P.xBP[ B kGDm Jg DRC;U4msyEMj8^5E+ѐɓk| th; u0,Ekb t)<c?i3 )u?ij5iaDlsD,VcO +C(uGN ;RHDK0}FN3R + > 9yOFUb]JlP ͂qOfܢYix5Mf;7'0svTzZLs{"k!Z iciA>@ʼn؈ +w [mUy2?j8FfzBj}}fO _]e{rڬ/Ɂ²(:P|P +۶[P<@ի (vv@ZEhBר`F{$`gXiu%?mt=vL1}Ϗ6As%6h}cN*9]in$=dJ%OhBJm3wJxH|ģʄ,"JIa(ug;tL@N~" 4`LIa@)`@cG0PP^yH# .ݓaIɂk՚tEi'ɱr= M%_"Xy:*Uz u-s JN7$gBͫi$&vR+eJ[%^ *ju,K}9b%+6-6VyX" ;r\f^=ϱŽ$ ` j)W"x)LTIF9WZb"MSu|h!zډd!DJ (µ(bpgfڕ f[|U!2ecf\X[K7jCV0)ّHbDZ>Wzᗷ +޸YSݝĘ C y }i|sX> =z|(0ʯ.+Ϧ gW8^r_=˩x}\LH۟7_JOP3aq:svC/6!2W>#tνf;3,θ.;+(3-gyu6 g+ᚦzlz&uvI[((7N/$:Ќ/7"cƦq]wrY=(ekzt&LzƹL`ӳ2 zVɩgYƬg zV׳a=,a}xi %o^BE׷}I¨XB]3XIfI}(<[ +bw䳉/`xȪ"622ydFXTPCUN`KFU#?L>}zۯ|c/{F_ѻ/_#_cdT1es8$W?Nfzo}Uӿxg6_7orj#&Oq?NZ8)U,8FLG>o>Z??+ +_ooI8)G'}I,8V7&o?~uͩ/\|5qR5N8eI(8IW]ou7W&?hogO ]8iG1I,8W_mk"?>|׋ٿkN ǿ:6?k][OǪqҏ4NX#IJSz0]᩾WsT%ZriMNm,@kVж2/mcljez2 X4W`W2~_'R5-HCkZcuJbV#YdҍV0}iR`?,L0x8Qo֢ orlJC!Ro'tD5x +oQt `Tb4ݬ]5mԉFJJoɌo׫>u HߵSPjp(0,UM52sQ>ٻ";;n$cOoVsq:}0po9F]a>{΂_/gM^m q|~~ځϡp{8k>+7ҿ}q0* +_?0 &A_Al_郭B'f?{%^1▞_iOo_{q3@ L m48/78]qՍ|-up].Bg}N]'du]%cֽ|.{ĉZbݧ?KCp?:$r6\WcOx ̆wE'-?aD.xi Xwtl6n {}ry,Mi|icVE/A<ǿ|afQ_|> +‰nP=kjlg's/ϖKv?|Ӈ +~4t|n;v5.w9bu ^@Kn k*n4xHdtPe|&ne" +R8D`UެOS5ts&W/#xx,+qP*Q@t:'=^S)/ ˜Ha)*60$TMU|f ui>)Vͧ1G҅M xS㊈dox/|Kvݖ?K R#.L_7J +u?l5F(=,&ƣ_^V?: 袻5Yn* ${_ v7v^}nF^Dc^4g'w >js_oE+8Zmz 4~}f~]OvaRbnY6~)nM?v}梙~YbgЦsO޿.{Z;a9wwY ҪYXguϮsSM'H{lA|&Sj$1yf: 2F0e'=3llrU]ذ +z)ZC;fyg?O1NJMlnm=r ;wɕ--*ވ*cjx;,?ހ=M Ĝlj? lA + +tN_J +) \o/T/esLvl .&M +/M +s40) Tڤux;D7AFS N0$ ZNhOC +@bAg.ɍ`),CeRHONىѓH/:W~P Ey0M#t%f{y.a y\O;q6;~:-0mtHJ]I6a(D-m\Js#hNS5+cUa%`?fKfI6IAIaLB œ57vHVKR5(˽dS!JO%:;]HY{YՔ`V"Dž$&lvye7Œ1$5`߰3W6= +u'^9=Ed1ɧ}MUϦ­ )=ތ ?|7BRXLΫc9 +9^=uJk½{htq;3Eb.%k 5[|~&8(S[dKWWc͙R7K&;.۹N xt/)_Ʊ+#Ԡq$F9w˹䍈%t-r7: 5 {= 2ʅJ]N] +^ <yQsMe@U=Tƣ4 7'&cř0NݧK\ ^Tdt{/?g*e.KCOn*|$`:K,5{DoZlgFY8X0DgY'ɪGeGWg3΂o8a/n[^BIYBR5X6>y[W:u"u? ^ȷßļq2_ۻ[/K@|gqO?OBHM Z5یONOkga- MG:cJt1WL)Bv !GxT@W[h5ŸhfF7^? 醶{ -ВA)Z˃ m$ ], =ֿ)K-M/g-%|Foy"bɈtn!%{gՊ}k h!(@[yޫ5qzcdCh`]/ik?i%8Y?I^m1DFj!>8Cx99xc 9)-9xc !)e #j9o'k HB4f^h!FqdaGk )RFc RȁcHwx gj6 [1RA<$_LiV^H#yI0Y{G=ʀةA59o9, +'z$n+p7ېdrPPy[0kg%+B)DT_ZIR15ˍI=ʊEB_*eYs|y =A})3ix3J-e=c~|-?{np 9q ZJow x;q^b +Η0!n_Ge?8%`͕ ~ݮ0bƾyx 7s 'Bk|aiuk\$l?HQ;_uTJ \ Z,xCd:!2Ú(`=SL=㙢16TNwux1hԳv<ױ @`Nzy'۱% rGAu:u0~f%i%x!Y5P9gV"G/wckVfk۰$6ckL'6z_t'g$>ckL'>l6f`l fC/g! F[$1[h:1|VqziL5| Մ9v2Vf4{(FckM'Fj+X|v5|&>Ӊ_j"1Xv3&FӉц泦Xҙz76-˞j0t61!6Q!ڪkpt lCZ{Mh4^kxM'^|akb׶_a68tbyEX&HD5 Z˵mE1]l:1м"T^m,"^Ӊ_ZطkHc68tbyEȈ}MЈ4^kxM'^|ak6b߯m8"G0N64ݬm;"^Ӊ_ZHc68tbyMVzfG]k: [|=3#Ҙ a6mh^k<2[g&xD5u\؄owO$5\5|t)fYQ׉cUz;N麟ulxϻE/~Attކ,{nj]S%$Ӆ4oJJgwuC0P*X|Fp)wp1(ī!^؎` +myVsZzwe6O-Kn YڭeT3Xw!/WpNw}q~1Oʌ0;}' +?ǰ˯g ECⅈbbnz0 vPphƆmuuFggΆt i^3@ *Zkg9-pHL\tUGf`AZ:ggzxXd]lYf7gTՂlAoB;Qm|#Z/dJ(Fod2-,ɇLbH>[Cj>"yg7Yq&3؎nJL.[g[ТՒ߻ [[0dcX܌&ňs^Fs4opwָc:7rF0^. "zE@؃!!a1 wǘw0z%vQ0;R53 + +mOIぽI塬j`gDfk 0RD tIr{\>r3 +rԸl5X&t3&&\X"t4&6Ѓ c*QG3i ӄd|Lt\`zpA5d&2\ N  Y\p^`&\5d:ו d\Lt\`ǚp}, :N lL4DilL>d&:M.0[MA5d&2\ N 8ܭoG0A.=5.0dM@D} 4ްlbeDD&N>`{eA'3irhḽA'3]v\ /Ĺ~}7rݩq klD.f&:M.0cM>Dר 2uvdV ]24 4 Yޠp.@;I.`\`߬Kl`@>?5>0{d=CCք>d| l,: FMFM8G ΂L|&%KAY#hlo=O^{eh4)3 95N0eM8Q f3Sք>e j8:VM&GMxY΅LН&v0e1љ, wl͂p19dm u^i8>5N0fM8d  H' ɬ 'R9DY695Y~Հt`4aY.3t3 (+͂&6,t3 Nl' lo] Nr9a8avjm&5߷:K' Ӭ +0=ͳ\gX`{yFe{ܩ\et}r*`<]Xk5ʾ]R8ʖzsV0nMXlȌm߭m)cRc+a[V`zg-k[J*+_%M0MXl8T+mD|j=܁%9Q5_pz^jR 2гR]Cq + +Ǖf +9/V~i8ix(!ՑcLTJ5Z +s D=7kC>GǢaqFyxJK+,XVd'`U ,,&l1uCXV}-ƄmZV~+zVlg}含}i]%6kYZ?Z,'rRe㡱XK{0CnGH5e"q kZ +F7ˠY282Xa>y- VUQKc]CGjkHY7{ZҬ.4˦Y6[,U︐<܊Zע쥕Z' X/([\ ^Skg~-8 +Je4#R{-،]۶PRcղm(zKP8b'ViˈFLjXRʤ(LjR(%MZ)irݙJISJ\R$&論6:zePn} +PܶJMi«e[#zK'#1'b*#bv;-xn +ewR&ռtΤZT:gR JL>s&$tΤS:grJ'M91&׎tR:krMJgM#&tQ:kr+JgM&gv-yS;orA7>&@ݼc.Pa2~L4~L~LȆƏiI)LH)er+JIPJ\KR䚳&櫔4ɆQ5M^F1B#1B)5BA64F(ch[BF{#m9FRcjELʈFLJ)eRPJ)jRJ\#R&4F(5B)%MJ-mrPjikv e1B#1B)4BA64F(ch[BF{#m9FRcjELʈONcm)jRJT#R&4F(5B)%MJ)iPjikRK\#TmPwePn} +PܶJMi«e[#zK'#1'b*#b>]})%aP6rjP6ڢQjThMhjUF`7eڢQ^)&UޔiFNpW)- ”RnfbOT +HI?iRY) V +i HJ)7UR@'M[@JPkO6}1)74G-&f8WI!Uj1)7{ZL &{ݯbaBL( S +Є™Ppھ7%cP2%%ĆƤdLJLJ\pShmV-ŰTnrDRZ4.42/ +"#_pݗ2N~ͨɴ3IΙLӠ9*Jɴ{3 ΙTIvHI5Y5yΚTYFIu75Y^ΚTgY +EuM6yv 6y7>&5Dc 7~LTDŽlhƏяɃ>~Lnr&cղ yxz_ {TʤzR&'2#(%MB)ir-N*i'@ jikF(nrP&4Bղ +1BXJT#RʤR&2F(5B)%MJ)irPJIkRJd#Z&UG[/#|  #1Bu4BíO!۶#T1PxlkBod"DPeD,ƽyQJ)eRPJ)jP6]rPJ'MJ)irPJIkRJ\#R$&6F:zePn} +PܶJMi«e[#zK'#1'b*#bqBiE+N8es))kƉ%LmqLj*X|֛2m(@R*EcoʴE\R8M߫Ir]aJ)73R@'M[@u`*ܤJٟ4m)rI+dҴ`*[) - %x5M[H@r3dŤܐ*]-&B&% 3pj&! WNm +C8 [IlW#0neQo$Y'LlI +S7[R&Ll BE&KOޒxnFݨ/q)KnPnl/Q KnHW7_r=KO\NYC\+1b3&ןp:S38_ܯhJ xy9ԦPkWlpDo#Z![z;yI[ߕp:^%,dI7oTAR- +jUH\˦J¤U&N&9i6Y\9ɘs1'u3'qéM9׮s$B4'w42'~&$SOd%Ӝ,$tI5')K9I!]RI +jNRH\sJ¤T&՜&9i^s1'sJs`BcN2n$x8)T6'ڕbN"ќVȖ$FbOÜTB¸id%Ӝ,$dI5')K9I!]RI +jNRHTsR.ıbOR8crI* jObǞZ/=ؓ=I= 0'{R7{<*ZۓxJ' hOB+dK{|G#{R1aO*!E'Aٴd/5PfP8yJʴتġ̄*q(i8ibWC^UP*q('RتDԋUBQK%U EWFTϣJ(JM=R/V EbPUY%^R*(5aJ(J Z%Fd(5lLS0ʽZ)(3yKQj)R }n+fLʀ5&`u X〇SBE5^RG XC+dˀ5FkŤO(`ε*P.*WITJ¤:#U&յ05+L W)eŔ51-ݭoiɘiIi 01-R7<*ZxJ1- hZB+dK|G#R1aZ*!.tqeƽuML|<3rdUΗ̳u*KS|=JŔRSjJM)5ԔRSJWZLmgu[myhyXyCci=:顲M)5ԔRSjJM)5:vPiJ)տt-S~rMfS0?ylylI>qxl,/昸?aDy?7\7p5;،z1j%أeغ:fDۨwd +]8+8>*o%[9)w!5 ]u=h=>Ne7;_7KYR=RMc[.wդw_kهm`Zn-tn,zVL`}z@iMo$Kl-`T-XZ4ȁPBb]L/a ~ur6.ɋ#^Ɯ.0RǽY iȱ +UҬVIA1j[/Eh5$qkq>`}7YG:::ZaB߂*(^Khíu-R'Z}%WWm)%xVGA #0bByۄͨ@7GX]y*#l^Cj-j:awBkG٪ɋ#}]e uM^ w~ v~ , im&0k!܌9ʘ9J9 +rG5cGŭOE۶TɱmRx bBod"`p"f22U'bRJTÔRʤZR&44)5N)%MuJ)irSJIlRK\ZZhm1Q:Bcj3D%l![K{m9&RcDDEDLTed.'D-NbZ)[JTR4L[4L[4J5+EcoʴEcoʴETR4L[4L[4u(cҴcҴ\GR@'M[@'M[@uo*dҴdҴ\R@'M[@'M[@ %P +i i H jiЦ/&ddŤ܀+OO[LaM 3r @F1cx֧>P۶@Rc5KzS@9 +iedVR&5J)eRxR&5D)er &E4Nu)%MF)iMjikVK\bmMT mc2&*cRi\hLTbƘMT&*nrLT&6Q5 +ȘFLJ)eRMTJ)jRJ\R䚨&D4&*5Q)%MJ-mrMTjikv 1Q1Q3QA.4&F1cLT&*)7Qqۖc*59 +]LTMLT NDUF|b7-kKTRʤR&D4&*5Q)%MJ)irMTJIlRK\Z䚨hm6&*c2&*&*ȅD(fJDC>&*nrLT&6Q5 +Ș|f_x*/e%+M,hU){S-Qj +ZhMhjWFu7eڢQE) Vi H2Y) - 7RngOV +Hy?iRn(R@MѭI@ԚӦ-$M_L QIb?mbRnZLiށrk\@9(gA.4rb ŭO}m9r&kh@9Nr &txȘJ)2 J)X4.juv*%ML)ir0JIlWK\ZjMTMm~[1JƘ1 ĩMٍӮ&7nv1t2哯f'9n7mՑ%z,CudɵKP]r-k.TG\[5YP!ar + kFc׆6YƊfhƊЊŌѸ֧ږo[5t `QojdS#ap +2#ߣޗ2Α +eQ)i[S;g233Cs&3o9iU;g2k3&_&5YjV;kR55z&5YP;kRS5&5/Yu$O. $\VXF2>\It*L?P)erfJIZRJZbUƽ[kI)iNN蜋Zj#r]KJIPK\[Zڃhm3LLL!i16Sa)QXLm˱f.6SN6S'b3-#c26SI*LT)ermJI!'irmJIkXTJ\R$[&6ַ:z[k8foXŌ [x֧m[7v7u08[q/Aj}SJTRoT(jvE91r-JIkVTJ\R&׬4fE5+MYf3h̊Ƭh̊̊ YQY(n} +,͊ܶKMmVkh"zS'"fArXL,MRKSvZ):- 'jl״)ӖzSւH?hs&ՙ4mZٸ7eT7R4J@){S-:Q J؟4m)mrP +i Hv{R@'M[@ P +H(dҴ{R@'M[@JGRkO6}1)7L-&ރirbR/j1ٟ.6MЦ T6ycaB6C6ĩM}&]9ku/ʽ>̋$"ƽ@j#I$ S7[R&L Y8&L(-Ab&KO!7JtɍR7_rc͗#u%7G|ɍQ7_r#T͗u%7C|ɍP7_57(13&pΘ\8szkExX7 +9sĩM׮1Ȟcnv4?!+G+!tR+$KwK!Y2kH&dL^"Kx%J]S;f']R@ +ņToBz%0FjI k\Msc4&O&O(dS6&O^RLd#<Ѻ YLiQQ*Q4>F!c|>J8)Qڕ$G>JF>b'%Dط%ӗ.>/t KE%aR] * jWITõJ¤wU& 2BI5P`x @[sמ>J5gX\ |1[.[j8F=T N5ߪ j[جyl$# 9/:y ]D)9L؂i9p#md\Ϝ61Mf-N=m+!E=Ԭ-jWa,ijdQ/gjѳu &<J$–Y*Y9]X׍3@/B6AHOٹ D==p r`}ٵ`;ԛ7 '?|i4'xo9 +F59b3f\FSʍYd7.%5bvƄH{n׻d_"gP 7.+d'mN'~g>\|?..( ^/q?+6} яM}m z z޹kerY>CqqFIˆ17ڍ7#BF>L@j(9L|;Fd@1{tu\Q=;7m7Kߎ=4l?z=*w'[O:Ӄj˕ +q/X'ȎE#ıeRJ%-$"Mlvfɤȟ Dwnlv-<:":ln7¶2M6;;k695dk/ynI3gUe3+n2pFMdRR!iǟ"[lwUzOHtizhDQq+>,_I0հLnUFR #ޗ9|)N4#Q +|v1O0Õ5>g:u0:E+Pm B;do2pu(-PwQ."+E8 %Z #ӈ0T&}a5ݙ'Z +iorRv谕QG {ZųǪ HN6)~ +`(6pl> tiUyfa~{w8J\+s3Z#yw<{&"u[YVrm"9G#Tf6L~k$26bw,X>rƦ똊Fw!LI>ӑo0&is\D+p*/`lw^eI[2(bY#XV9%'W^NbOyc:ƒh\r&COKG^}`kkܕtI޿n-wL/@_ +n717m;Cq.ܺvH['󜔯g_"+KIr9SL2VD +vA? BJVVrśmt<8Ֆ)"9~~ˍ0E}$㳡;:Bp>X2f^ϥw䂝iXЏb9O JAG14߹OǗ/xрZ86ˡXus5t7? 0sd,b}xL{?vop]ǐ}0s<} ?G: Cɉ;֊waeЅf"AR+`|w޲NIam'|u/}?[w0x^QVT&N +,hTMm5{ԛ4d6VFh*%Qy囹z[>VocͦO9 kY3{ĸuܑf_Lڻq9I(ߊM4*R^CI[35̖Ŝ˫)j Yr鄡?ѩ[z6ث_ U%ՑsPX(lyu>l6Nf^VB糕οȊ59g{~$nv^9X+Ժĉ Y |r"A{#έĉ?-:JQmGk s 'a~B5^HUT%O#К6 pU6aaK6~%]dtzG5)5K@EI[]߇˟+<~!!Pc>q&N:jj<`WO** 1@?m,LZtcQ++f"14UWZҶ3xy6R9%dD + (ロl:5=WB]KuM5};uM+kxض;M7D[͠F&o}.Ӑ"bli (*k՜1(X% OJY`il{A$^߷Q@V̈́!|2PL7]LoeζdߩUW\RJO@I ? 9 \L]):2dd!CB\8tR1D6:},om/jYex aʬK> AcAmE3J +xV'|Xfe1ysY*-+/$WNZ9l_joPU,mI m O^|`- 7ʂ Zc6E`T-j[2҃Goqupa(V^t`hvxT 4aZE^[BT\tEPCP+)*F +yZj1vC@^L[sk)/)¡~q[̈́D\;DN`5j<*,]ӾfDh"PAч` +뻛S Agލ_|rs6Zes;qݵN.*JPFm]sSKEVw! Xw}K` S=: hծl²kVU^*>K؝ v? _a8T1 u侠i1_dgUu5־J #L>z#cl<)uSDu +6V&ߕʢyjPdz(RridFYy`RQߋ"ϕ}7&fDE9K:" hͦ=`!"/QdOY6(/oH# GN?]߫Qn!+?CZh%$UoZmHB +OonƲJ+ۭ 7<|2Y_bf~^P@мC#bx@\ɳHLݲl2-G!VSۯda]w; J +5IP{iXi_8ZwuhSXy{i + I|$5R +@WQ9&`L; &-$@|& Vgt2 9Fn"լ(bb&L0`8 zZŊUZyћnf'8/]*,nQ6&XzFX@6i^@8[0|Di j`R[t J O&E7-)r +n*E(I呰<FGlUyf{3O;k}9 !(&fW0'e ٺO94΍,2=r!IGԪ%v̀Yرy2Pl~PA+L:Ex~S=BR/׭9Wɋq|:q,R=;{tp$*hOX(LzSΝ\_w>ǵWQPOr~Ci6J9i!t:kVO[D|77΢bS>ZlX[Adlrw| z Z yF^[j}HOM1ZlƲmXND;DRs+s1ɬ[JJG@J,lb0_gᛯYQwar0;@OlTU)жpsmbROrrYQ%8 Ȩ35ocWWt%i|^95PC +: JȾ{ elOQ4?pQ5rTlW-is AYKT@DuQiAE\gG9"U~-hQAֆ^+z(˃RU7bz<ٮ?I܈pn7pr +t*X: 704]IJbrEcBtEpxQu_m+g#[LΦjS/s6ԋ4c?K +d9:5w8ԻғvpQÓF/NKt-8dC_LΆlO`O)! lAFM2!h3Aġr(C3`E[]ur +qZ@hrn@ۅ> +G[cCjry0͂q}C~gFM:wA9X&:4Wƽs^] Ö׮_\]ReĪԒ"eXqSͩKc"w#g?4=\_-)pG䦗 zr;2hHWb8,iCHףb~%WX o+WdM* *>7q]L*s +&|b7.6jC>I2,^l5°> Fj"wYbzbe yp7޳z4aXNᰤ"* ɵ`YaӠ o._+~r@K?C-T9'xM˜'K>*X 孊ɹ>]\l5DM +DYץ`35\~aC^Z-[dIE#6]i\$7*([xK^s` XZ$!K@8ё Lc N+Lxc)+UOv S(Kڋ"r hp>>XS|\ctPm5e99ƲASEU>; wrޠp:]u C?umnc=?IL68yS %jʳ_b<^N²e>"&炉ѕ= +G!z6 љu8o*NdTTFL|]Iib5=Qn}O~X'8u)ԕJ{~dpfy?!dXTQV(+X^gQ"0dBťV#gaPlONxMQ扲OCZ>7Ç6(zNpu|A3%*?,~Z7GT6Q+|5%}@|2N1/ +|: ą{:쇄wNj?#\ʾ^{R.EsUװ3W׽E%8;s@4|-ŒlK l⃘*|5'}ˁH:Y{*.)2;9P _>ee~|J<|*t~8 +o}Qs$KB L0LpCg M=|zvD|m3K2W[ ^ʼn|!*lFvV-eo ?|j8Tj#Fb)@߈K[׽ة0/Vb|Hw},E5pK9oO(Oy^0GnmJG4]. B34T{F;&=Y`"6}JIqK9)ܟU1"#.'ZD%n)D+'zcJvM[۠d*Y9N7U;?祄oԹ(B|-1:eK9^{ stW0BKOnVˉ?@W"RRR{~v#rua_#̼^Mɉaj`,OCu3ih-sPRY '>궫Eyp#t6|~aV%|BMɓdŎC-®?v)G[D;`zZ\Y 悲4l"S`'=X~gonz%L;%)x*\% Iغ.+ w@)YN;)3bt}}zuP{>BUaV9ݝ|]1w+fuW<[4TaVt]R`V&q48\tc(tuww..<[ |6_'5_%Zr;m[c4T!aNڋĉڟ0&b]{05vHJ>ZEGPng %]c.fx*|Η*|1-i-^p8&そv^ 4[2gwk矬y[qǖ Xt1Qګ.KK|#%:;.dɽ~{o樄"kޚu6{}/Fz qB U`U V5]i&5[rPߦ)Ř)A~1Z!>"AOC!*RXkmn)Gg!E[o!KU}rC'ϟ9G;K.mkgaT +\K>U*+l^ٔǵV,u|/kf%_ӹ}CλX679jVdi (a/!΋t@_{d2fR0ڼѼE~K"Ǿ 9;2 |51YLNx|<Ѩv哣e`ooz<#4MH0:^3oƓM;ڿTDCK("Y&/YjW+ǃV ^Ҷ1qbqnr CPli˯R!4_^jt{&ڂp^,jI״2u"߃KH =]1)CS:Q> ECGE^mEnm' E6e`Ͻэ1b/"Q)Bz鄩 QW -i'OW i'[zແA)DEscYOCg75|MYLVx깹?/ 8 +mM=6u5ym~,lJ;!~-p?_$Na\uAg_c ly =)t:s0[5>)lqIҖyiw b2lb׼(|vvsybδZ)+w3{v +pH3)0c{$Hx?ȁPC}&v[=>@NiV.]3h[Ix01!, 5.&N+4Q~[?=/)"u5^_N] n>9sj 8)r U wocǹn9F%ljֿZF%vu)?A|MNhbPƆI)@+. luz)v ` aj{c1Z߳<[/v| /S…U ZC-&U-&Nf[p}vnM.`>L8{p |ZXuLm3t@^K6ݴqtEG]| )3p;q_dDrm_:a2e#ޣO)"hZL ͂aﴏˮL"K#td*(rVFo4 &iÖ:@xI1:)wqmE1.+8@|0T0 *|Q4^.uCo/&XRΊ(of=#G5]%crIf"qD"n0B9ZYghXMaq5a#Y|^ٱ&XP6,ZҤДPWzkfE"B*՟ ??G`JR 6~ [JK8 K:@:fW3d!}e]QY3F ڸd龆D Atެ2.6h1p;FQ8ns@\OGܹX(]"/j{}vtom:ƀwm<9* +|G(-[SI/a߮0l:AVyƭyQ11Vn3<]R2ТNuboc$@/ +>}$ 1Az{ ~f(}ʿK-+yWe]/P +f`,Y ,<0LbHRT"2 P;xvsCS.WX 0g[o\:!7X`qpU.gVH5,F:weVsk֓ oܢ{ @]E^|7MsAuo.y_7aX[JƨK;Uu &JYOkf7!9x'҄M@!$yʕzd+_gG2/23΍, U- يX]TѠ3b)v%"% L˳qżf"K)T%{$7+kW.*ծKJ.&I|B6FUr;!c4 {{[ ؃zzÍAa D^nvQP'*%lR5KB(rH|l%)WX$cNY!m +>}?_;f6:Z,xj١I*&T+QN^'*xo21gof&8Ֆ$8ΤV̼Npw +Y]|phbM1-pבW-/ K>mNRmMdi()A(q.ޚVr0P!5K[^f!ʊ[i,K8{>ZwM]<rP4-teoomjDg*(CŻDA}UK·.G[LP4,>=, +|6[r(hO +o ku'do*_S\)Ċ9V4:u}7Y*C9rhi|?OdߨJl뀵|]lA+U{EŴEX|$fͽj4&"M[T$U1 ώJ~kL]nKh1L,alTXN#(bJl`th%tD,E븴곲.@M1`4^$",3'n,`}ur*4EA3fM= 3ڳ#Xj0׍?}OV80k7 sB~}~v7;2JB[g8\o[ ++Wdfy@R~;oU92#YzBBk5.OUP?G^s| t =51u)|wRh; c r@-T{x]#`x$Sɒn|Nn[JWPrerWn\%"]s %Q݋Hhgy켄; +0m;=mSFT1_:cv” rK,L -l}/Lxjhe+eEDE$DA ,MBes0j~ +QR5Pnu2vGْW;7<لHHz株**y +QceԳ[J^ *cbI4`T>sZtSy}v~Qб*D+Obb‹>-)!BjU̬Z|is7\gʮyſbSw:.v{ku@hCywm|ie:ZZ:)\Z}F Oyb7'ږvhs{f#ikpU |5 T +{ƞF۰N|=7(\3Ge۴kkU2:X?ۛP2/q@b3- FA,;ZPL>|W K3pso;5 k0ӱ@.rޚS5#舺>[j::z׉ݨoP6VLGf{e[i0p'r[xo٢Ghb[2,d;AƂV.2< /;Nx}Zהj{0q:C] ֱbhbGFu틛\A䨯TߨUT0`k 6[ hclO窝 ,&zd|і^ IgI\ hS/`κ|3 [m;QwYAAYvVM  sᮘo euTߛ 1 P{Q~n'W]*t{^RFwd+A'r#*Y)s@Fٴ['ʺ9Z|̍RM`3/%s6}HׁMruߞYf)?2(Pg|MڝG7<ոl0{k{_4Z\`z/^}×(EUL !䤎` Y|^م/<P:E@ .DApwj lΦN.V-YxR`?10+\k2V*Pګnj}|} 4<hPq64踯AQF ^Ag1O CXRUQ (9A!e4f ͧR*#; ¶Θ&hϖueT*:}+!Yf R tABbglQ(Ї0Zj}]j/R)!*^ m+4*YX*D/?*Ver e-B*U6O Ue1Ӆel9Q)g Ս2n +qx6"F", }$Paq:Uyo`b/q/c kV#܅0F-(/O$ҷze"\P4$/UI^hWI$I05DTPÄ[/pZHo퇋o#X;1 q [ǃUlM\\% 'un$è|5)OQê0M/H.v\|xqDhC`Vc"%;9z4iybN:.&6ymE&.BC,dlN[ ;,|].<U:U;3 > n쮲&o_TQ6uxePGptUl,n& ʐߠHq"ZuXV)( +֜<5 $R:@z]x40ظ,0kuW `?/ʋ ۀr#5Grݢ݄{PNJu7pT夰yP#5z%tŚޢBGX~t8QLfI`E]yucUaGh}:OAB !DԤn QU65 $ K7SZH&(@ISO}@}F!RJΚcĄ.9: n +B\rE +k2 Z9ci.FTV騇 f%y +:e8ʃ]eb̲|d7I tj x:Bd01 ;GYRwA P-^^oI'kӀAo@=ޒr# ؠp)8iIY YRE:ʤJ[{J;?\7';[Eʠ)G#yL`֪dǓ|zhbԢ Y4g'Klb:4A]Wfi6ڵQץmbnt}av F@ž9[CR bzTCvїiK .trাj,ߡ2 g.27#!FЪؔ~̊<- vNe4,odMY^2L[MjW!k4ޏv%:;žh:_&+РDi{GYz,kT +U "*zHy +-Kq0w:Up%X}O.(9XTg1ҟwn`%mqU@7.2!rcRZdl3).j0XkvG0H}X(zݾYmby@8Mfr%UT1Yz7=S@X4_ Hk[ϧdZ3%v{͏4 ҩl-1p#Tx)R+W:lڝ=%(6hfBjvP::l:޾^wY3Buش/" +p@"ā6cJO;# 5 Uz S {|iwФW=ZњnfM*LTMIhGi;zkBx"j~9mYfP0a™}$V5O̤t uDd6q\gX T]0{kz2yH118d8m]"7>IU\`Op&DC}fvzE +I5M3NLG6HHHן`@0δmuSCFc~3m3F–4;˜a`$x6`Fz! CD `C'f ?wDZgoԝ+뵗uo,PQ _1ˮhJv ny>DH(i&@׭/+ґmEEmYB&Oف#$  cOMJy?aHc<] +?{œ? f#PV_hsp*%}g\揃0㯿}hKl +Hu `벩Iv_ƷO±K{ol}Z"~/[ +/()o& C缵"QJ8zZUɇP„Q̡lz)#e'F<'o}'/ltR2SjxQEVwp pz/Jz;R| ^{-=.OZJrQ {ZvC zo@p%>U~e:hdn\g~Xc(F$ax3 '?IGJ·s٣ˏ  ~@ I}[#{[lQ?ZEQ3P Ӭji٭K_>%$6`aIT&ω@>wSV{6? \)pZV--Tn(nɎ."^[eo?~:)t-|zP'T *j3x!-*x 켺Y_•#W~Rք/Ļ0J6dW5 0/B#)|_l (}v^ Ì\yNTH'j˃aeJ&U*_`Hey8X{xS]b[ +q8vKҰ(^RQF+]Fg)E Ԯ0+^Bo \ၭ:!'\L!{TiWwh_]?z[&PB{ܽB}Z+ +&D9Kߧ8_Hu׈ I +04HBzZy7eϲ;"Q+Yө߽q4jݗݟy*4 {|[|K%U#U7{ѹxZ>6Z.fǫ;ɛy6KۿQ=g2btڵM7,`*~WUNSw sV1LyRoI妛FjQywvm$nwQ? 7MeOƈ>呦krzVWH;+|?DBy*ѩ+ +ulZj(iX芏j9ANj}Ep"RSTy|z#Y)aJ$&^׮^ HG k/")xAU7/$ yMV|)ƐXR5P<EAo^+/L ]Oǒcᱬd=ǒo]ɻjeBbL翿%~^c/COiPD$ކᏕ ԊC3AYCB/<P?y@Zd8sIA49$P2Nk^c(ab#:%]' ֌[1مϲd[ n(ȉRr\f7R0,s\k[P\:)Rw=[AQm+)Z?s^E)jkBY-eiV9Ht;=1;y5!S5g$:Xڇ-šlT7,NU+\|`i:v!g\c,Wk}j}bkL& tmRCZ뇇1dfԂezi?D$ };BpE FLp_ZӠy8)V򴑋~,n܆ D9ڃ(<&DJ!<LBk=?pl-:#?^Yry%Zrxp#_+{EtEjYc} K6` #As0x{ ?nl'^"D z_ ̘4gx)9|4?8Rsf+U% <8IH Ao]FF ^KnE9YB +b@]S̈́׏K\8Dn|B_cP٬S_g%AshH\Vb\S~N#d*gdYx'^z>hs3frzaU xz-wT]DX4п^Owv88Yfp5G0̢bg;C+Nex !.6/v@6dsW8<5/]zP5.lìc = m98π; a[d?hBtqlD}layj>;$h9ڑgH^Ǧ}zq-EE٭>ѱ'&^qflzcYUyBnoG_57#"6F`&\Wч: Gְ̈́0]v]V}~v7 π'` wOC{o+PfQY+V-kލ$r*&JL|[(Luqb;+X0WBeV@rJx z +`m1)_LmaY+J cډӗ7vq9,z: /6UTX^DS1jrNl::F-Ud'^9|,N\1 PkԖZ+I 8WT(DD0擧4z}')ĩ0&rӥz1ʷ0e.b]ۛb>;ȓ V+= o^EPɽI3Ϟo0&Zzٓac+g4ym1GY2<:%=φVe,^3Fy,%> Oi 18o]P9|ae]~mIc;/;]²ft`߲<ߑ8ت8?} ~¾(Flsáy㧭~;x4X"$ْ"Z2OBlW{0M" +° ]m|^B'LL)ex}ȞZhoRA+8W BnXUetV_RUs.ïާV} qNxBTu(q^q>;oDU`0m'(t|Rqpb#HWDXvn"a]&cJYꦂ ·b+-u~L96I^)^-۱KWf49A҇JԖҰb! +4hz))A(VwcN8{,0'ݩ>hXf3qu*ZZqXqAݼ녓 *zqlj?$ 1O2 %tMLPJu,ZuHD$X ܥ =,zv6r +]VDq\Zn&,~APpk^Cc4nuˉDUf*EDee,Nl1^_ʹG~b&slyltGl`x MfT:z)أg,UN*gE/ҁXLI$SSYnlTJ Fivnsi6 BlL44&nseI6?ژ&-0 h~QwGxL$ #W*N(Q5 Eyde_Cs' pJnjg$&XS82[z1sfd^ FB,EdwyoV(݁O[!(ݹ75ovrR7etNE-;iC4={&{%=!OXRV4LUꥏ}O}s wfA9m2JT Ni-LͻψMm^o| ^1ʨf+XdO/'ߵ}/FnN#[x/+K<5כ ghֱ Su=+uxLDK"W'9?=_.Qq)`U-7IJ5Xf #r|iv +v>Տʆ)u! -ex[Oo`.hGr3 r/yt"p-Pou!ƾ't4c6hT:CVQ7&- Cfa~?mH@_$Oktv¾-[EQ)L +Z{T"u ~Pb 6nid OqF̘>YB֊appw ~&z\"0/yy' #.AA 8jf&,׊%. +jR3:DQO(Xvg+|oX^bB;nB|}>F>AWEU /iߦy]~ [g ?#,f^R^Єuޡx@zWW=L5U]x`Au:_Y0`G?g'"zV_?OLS{4fVns9_-=7u4)aJY4zcM0`*Υ.g]%kۑV6hKc;t(p6=d3]$ k{&ueَ1Xi(4EⷨLj;Kt +z^q,>PFc#]=rQ_oTŠr/RL`asY +3wlJooT'RZh`H(n &8aAi9jD&~ko,j8aA11<։;dZo0l$Fӳ~AI3=)!o &,Z3+1?mbzƈց9 dWd?ު@*xbIkO@PKt^lMfK ݈;诟I0:J~XQbViN +(UgHO!Ηqy/j;(OO ljt$ṖJ/.n![EW IMVUm3TC,^K9H JS \zf8B'o{<Ye\_TeF^2H^li~=x*g,1ޖ .!$Nq=  LZ%keX7rij|w hk睷٥ݵ6Nﷁ'nA [vt`y +Kz2AyH&0Cz9HPTR^ b@]E;9 Fa q0%i>ɞ|#st)XCRt?e}ve qQRĻ,U4 jusp:л' cyL'͗9Ez)=`hFKJ S@`/nv59s-V 첰 }/1zO |%Lё>7 f8HbPqq?sP2UdBNg N 1)1ӶV}j˂F ++/1|O |kỎ$r (+2=UX03Vl(@ + 8tVa%}#h~Mac`&7V +b +HV{"F/3p`]ooKLS_|uɏ{˩oqC>\U^bHV 8o1z+ vY%bLY1U^bHV3=JL`A0J!LLB LY7b &'1܀b X! [yhxBwG; p }sS-r [~hn}6U |䖟ಀN1Rs@zlD.piHL{&>S6ApqA,'Η0ƳY P(08f΀J 3@`00a%'D0܀ fbsb(Qfbj0Pr1 \|~zlDpldb \yh[l/-vkk62nV^ +/hPZ{>+ x:7|SܺQv:xph/ sA+$.M矩jVY[/FDl$i'H[P \*HE߸: .h~+'+0oX9EreOg5/8Q!󠽸}%!ᭁ^CD`n/pPaLn8 +@)g LOV OgcI̕0/ɷÞuEYӟ'eMO_Yo76-R۫h +vBan@CmX+#9z>3B jxygE!; ^C0M`Z4A㡾D{h_Ǥ*%lc,nސd5Xm0F%rT볗쬷Y쮨_I@m"f% :7TϚD j&kqIh=fŮbf!Ӊ5"ActqJMmtdSڕfj PΊGpIϯAOUw;8_4;HkgXԡ]9162u~C1:5ck+ѲfulbQGT*@TOu+=u1<g,bq9o|h=pS}V3`~7ϥH oZ\/~f靅a/0jhT~^c]msuƬ/$[Z_ #{*4ԞvQ*irV:[pFqrGhL?q<_!:́tb~?{͔x+!qd>1%g!chzc}[I +RYO@B^ܭoi tLH$Ӈ[60p eD1%[8%NNc!6LWR+|0VX &Jf;w:^4%S|kZy˫bQftv*A,/ZZtXS5ɘMihYf9. -SZ+_- +U-`1#G@ {ڡׄ}LF +-S`j U] m.0Z<5WB"밋Xwѿ:D7y:H}䃰"^81@I̔>TVOҗ; sDMy_3*OG#m#09i2`wEѬ1KV}d9"p 6W 9난) +8?V$=OI4T5,"^GZS|DJ ()֏z#~e!ܷ>rٹ`NcNp*3z29.Us CnmMxTf/V:9@II٨JP/zƿy ];&k +踁I4P3N6Ce2Jt^^gYpZ?na2AOqBsn|ԯQ5`$Ƀ?;F֋& +I8W4rŷ ?!pp4xA)F(|!DdE PgP7ȵ!5tBz$mM=?:0L^-tdo}x%lB { ?.dŃ ٻˇ ?`4 =K,9-gsFJm7xŭ5,G`F`-a?\{22T + hYY pqe˜ ` 5>.og=;x19~8:sq ޻g6\I{mi +z%P\#{;A5Xc:t >E/޲^,UZD[$r AӁz=h,1G#s&t:[G?aHeN?0I}U)?y0A~cC=9?R_kQ?ZEQ3P Ӭji٭K_>%$wye DQ߼n +s7% `GOJp/rQ(+u."_+Jv^eJm=e-1Ek8['=N]7|=u7Etf~/%_etxzL} eMB dsL;־|*B#)| KY^Ϋ{ L ࠘V6<<_|®o=\%U ʁTMjn*೻0 +6M- %`et~~r?^ЀAe LzKTwWx`k0Ǹ,xI<&Ӯw ]pJ5JU)#,> 6p 0+$wJ%R1VIB Tc5qFt+ ۥp$rKe@t-͘[&@ +{&>Ļ:QWy@8T7[xfaQl`1YOnxgmk@E +q\%i\/(,~>|"Y*\k4$) g]=˾J8f~rCSdU~Ѥv]w}kp,h45neq\m\WwKzVe 5ff; .\~u15Q2zj WMuwVQ񻒮ZhAc!WqS8^sQ}WIuVR)%ɫI9͐hx:DͮDW_-u@0'/P +1Q!ͷFi)J$ԸzbLj%JHEZ I뇉)(量Xhꈏ9ANj}Ep"RS}Te|#X)aʪ#&i8( myb@~wVώxK5{F/kW/@#ok/xA7/$ (yMV|)ƐXHP<KEѫAo^!/L ]OǒZ=ǒ]|ꚏejb8翿%~^c3jOlJPD$Ꮥ ԊC3WA~Y(CB/<#\?#<#K~L5GB2f\~L-XQtXX(T}$GZ}$TGH(Zc>z#>V4L}$tT +%7cIdDpT~,)U-3+lLpg0p:Fb-DnVcE.}9U \}}\hsD>pNY}QB?TAbOcZ^,*WHG@ða*ga-8*P:+rqZ{)9W2OT<[,4{rSz6~08qZQMNXUQtvX3S$/*\+<ɤD<N# =тsu՜T@UĖx6r||J3+F4YhrdJ!Cq7ʣ&Q$kUFyNEb}U=epɓİLEMJgsc|b*I ⎴!8DW.dXa[cܭt1:6Zt7YwMFrU0V4{k`؝a +VQwrK;4SG*L/kgt |g"{riuf+%(I הařϐKpq>|,9b^G\\5ľz +XbVE[t=UxFNٽYY6" АP8#ܧKԻIw` (7 +`@/ED_-1=R's!cXAd% s?A&؋uxe`;o6-V 4lBILEJXe=֑IF֤d,YS) Gܝ<, @bȪ23TN8c9 c3:Y~z<kX|]9' 3xuz ܡg4A1@,,Y'D5ޡ=M ' +m0K*"Oo$%WtqA=qrel| ' "Rp&y/6[3Aa4fM+HPk:nh\*g{cƂjx(6^5@|vƒ/БLkɭZ ttYL4O DcWtsВ6!f_S(ENo6аwʡ=yu@xgրl 8=eB*c +CG(afʩg2lٚ`ZNqv} +n45 Mizp_iSM~sP D15[OvŜ1J9'Lp%6]M $AwuPF+2W,#= O}la3hӏfR~Q4/>Рg8r58OG߁4=9]*FO@H3x]?(; k*\V Cfc14`.-po뽳yr]Ms;G?ip͌WԄa&А10K^ х59'dDa#w)w*܁iwr!p|Y=b;m(G?3Ƈ'9]' +iV2u_ g^ؓ}n1 Jzߗ$MTi;\Ħy3Vq=?ڮw'uf΄ٯ1߿5 s|Wl+$CěKM/ +{]C s $\r*¢ +W~e]wNB2BZdYNɕ#]f¤Š L-gZkH酉* H-ӓëze*_{[+D'%3Xi^i>+߈nq3uH-pgqdTS]XQ#H0 K9kL5[ =/Ƃ1zF佼qu]#!Ҿ[^IQ}2?Wo֋O[wnopcf6iR<ٿC`z!'[x##X Y_|'Sqn9B>@rOg`72-n L1{퐯S1O?kyf?$x&Nƀ&#^*'V&ot^ଃj=Gr :F|vr'{v"koqY1CI"^C\m,-xɞCr\7d+M; 曰IU`!X&8_j˃Ynj(N;A҇]4!weZL_t`/8&GM9Iw5];tzʲr쨻VoU&9tV'[ + ur)s5 ~5I3XP/G7{6>A,ód>.Xrps._ +'ş$~+srGTgEn(g^bd~#@N=Ne{((66 + f;+u:{}"iq@ʺEvBLjRo<~xϟh_j:f"{#hr!S+ -\K9%ұOO//$R:p?ֳn=Yw+vO WFy-f?4s)(%|'5KMuUV?@bjɜ笢)BQ KMzlOX)}m2<빀)o"K'MSz͚ٔ|8%lY:1.t5DpJe.2morQ'SH*UrOH0;5L٩`8IkNWV|l[m.t {Nc2{?!#Y uPsӪL9prrc`FU)Arz깱 +YԐzSMO$PG0VK~j`_?pxƛ["GN'{h%ڳgvfг +:1Nς{ĭ0 +~a,ʓlvom>!Hit3!Ͳa9XhJ6qO~28;E.gS,k?}X*v. {ZyN\+Y[ˁU YΡX +4 ?Mk-_jcLbӔva9(0PU!dmzQ /-4Wlz>E6޲H&|N TM^Q]WJ_A|"H⻍ǢvzZ~\ +@>>,g9˾rp H)+z ~0!{|$.D[q y +9 %^MA- Xn q6Eb42` ým]+Uʼ6P7ݖ~ [M54-4)g4ڵ/sx:E+|}X4Q%r8$e&r X9`M:CO. hK/X=`]Rx |:v{ӓד -'ƘL ++ q)k"G?#KaP,bhppEV2|>;G">A OMlO;`1xr!?Iz&hnAe _G^,|Wq!~zfu[v힯 +ά.([&l p&,CD ̧?yFalVNdnsHgUNZN0xo[ˬVYM5ey\'=77dg0,35L89܄8~ udC):BUa'yᵜtS7 L_*`31'Nʼnុ*ja.A$#Fɾc1Ug(ľ3u5 +2$3aѓ,)cL.(<'kbbkn t/&-d'=LX4˴;||Y0>d ,LyP+ǷSr@k#w  x45`p<*~:ܻ Cmy L=`عOxZ<֟_RJ?_ؽx{*vW7cL>Vw"w؍-PkHz0=î "i@XBtr'4s!ZɋĖSKTCz ]u١B[pm@٬°sRXOGOynD{+㛝l !9&a9.#p2=&:{PԄ?8$CBI/~vmբ=M&{,v)4x vm;+>nv$R]7_k=]x"~'~)?ԔҕR`߱X35ޓt|@*￳`vwԩ=&7 @'~^x6I) ٱAsqf:vM +nm;0ކȅ(j\><1BTlMuS-(O;܂I+K@'i?yGZ|72^i5`MɼA5؜8\SNJ̺D—7%\vz"-0oz9 Dq~zD&MQYd2r蹉؅?vE%J? %4 > LߔApbH8֏4gqOsF}n.ZZ)5~%[M"q"UŰ=/Q0"h&Hq2O좿&Ez܋d C'הfG>@U/{40''%]V~[K~8M%46|4"ŷ B<Ji\ M{o +s7rBT[r+ʼnsqXL"oR<@Rۖ%OH͏Vx)}nj؋JZ<(l~n#"F- +ZeYF\'[]gYŽ0rG9)z?*cOK@ora' zu(q|-Ƃ Mޅ?ʹ:v!>͊brC03sDk]qJDxQ^%%5KsED>mTu:Wlȇg+kh.` g YЌ kEmXj@ӓV> 2++<Sl}u:KI?uVX@汤ֈ(<ߒ39aSGMvLevAP*Oa*oA$Gs&Exv`.W%?ܡ V­NI)!F_ഹi>g"z ^﵏|̹J)9K=%1tw$WqO2&`c srK dsLY4%b-ԔyyI%? yL +t?*)R+ÉnJe㗗}'x9b}{PpOgem CBwFr#d!xJgrX^+(܇ zCL&IRJ`ľ#w$J-XX ظYIZJ/=G@8fkVQb='Qtʪ߷EWf t"[B)븀 +.d2 +~mFCBƟ2вwE.tY{P,UZ*$:ƞ@ˆ>bv)$f.Cu`d`Q8 +/? +@$U< 7WKBE"} .%v. |UK)-) +4fx϶nΪ!PR餚p,۲-/?y+̚R^ڳv)Lݶb6/5rΞkk:ӆUs1````.Ҟ 3Ws5``.`\uj\Tn\9O`of sY7{4h4h.̾Nf8mf1l9kZx +Byh +x D*Onc 9}f%!o/zb +{#9QRۣe\ܬn/`cF3(٫X.מžȮny,h~Xh +n`cDsnKS.I1u,gx341@`Na4oHgeڳMa3nXޡKX.o藺U==3ci1r +Yu3l39̨}O ಺SC8n:> ZSqXip{jfNk:]W_Mw}Lvl{f +[Ծu݂ṝ&l\a6vcw{3Z!70/YPcW +Bh7L1v@k\U jq>jQ[{Ow}Ƃ6v=n1hb1Ծ +.ݔ[Mac1n,m,; Ww jg@tJ:WC^yDc2c(l.$ } $^muLnΥҵ%$_r Lkt:AU1O)::$r%8͔r5e#C ,ē<~*sS4E.de~uOU|.k.7mԂI>0wc6#Jao~=Q'p%j{Z#5`j70sHUyG]=-sN RjeMbUĨ]6\1d 8?[ŖYMe!Lju4VoEx)vz>N/;bo)v^Q}:j'kUkb^ZZժدת~V.뵪_U^V4뵪Wg~V6뵪_UzjmIg4&kMr8qjmI7'6'6UzjmkUk㪹"-x!uqM[dϓrYyӠ_{wuP'%߬pD~icB_s1XAG\ɤu aGL*hZ ,<_K“$FP'Z('P*q +(/˽TJBJSFLae"s^jvnu3mAt<!"tS>S9]Ly)?n[לvŁؔc@.}ʻ_=Z=m͝Lg:ry)i߶@)WOV:st?\^s]ؔ A)WVstE)¼R 9\Ayu E+ |i7ĦwEy6uԸLW:{%!bλ ^S~h˕`R~#]f"6K(5w5"w0)QtXoVTIjw+w0iQFlwPkT;WnmaoiFp#+STp@{&Zibք$ɜZ_5$W ڲUs>#%]N;gӆs[2w?THiН{rb!rHniƛ;$Ћka)Mݹ>8puپއ$Qkyr}masD#hM8jy?|R֋ᴨj>uՠxMCq;cqu$.oHnY7)g$_),$}m 2P_i>L9P|jP! _v$=H|ѭ.,ŮgwFTb{u}rV.Glw8Y@"3]*W/?OȳzI\ڥQ'UGf|B sӄÅ4A`!P}bPy8YE{8g￁:|@'~^b5dV ,.kpE߸`"X9 +sln nmNIx ЃY"֝)#kck bGӠH\ѽY3vRS^J.2j|`ބaN=JP&e"܅>NOMO:ԂJfQǞ,sNuV@n67oRW{|y/مSnlяtCH[oH3i/nL DE +F\ %JC`'hZuN\{jz<׉{MkNC`wC\'w+~\'!0!xXЉ~rB'8Љ!G`Љf7/tsHIB' XO`uCB'sa-V xPE/b=B-#!Ћ؉cPُB3֏& X`,8/tc=IL 1zc$4Ŭ$˵V5ug1ރiS> l#[M[Q ^4HAm4vаRnv*S)zds#*0ݡ09woX^vt<7C6j{{+ʠ7^j?䠶2= zzk &BmAuAm_ʽAmn[ b~z?&f=%&pzz1ٿCs/Xe&5+~$e⶞y+z<73>x<)>l?V%rhe1vFYí\6H >϶nANNWN<]-L"J^F'e?y$LE3Drkg>y U M4V6gp^z _ + tx6[qɇM@c!|gձ/?Wwgw!O.djgO^}bhBR^wr}ur.+|6;Jձ'n?fYe][&xu"~˷>U(K<|_z\7\oZ~(A?|Y*/7[-le& ELO-]qWu܆ƍPdp~ӎ^^[7m'ydիh^a6B)EBJˌ d2߇XUL|RؙRD ;yӓR赵Mټ +ZT73,"씺RUU|DjeSrg~6g{;3W8.-\~^P~y(x! OMS$?Ll~QOgKْ֒7i5]~.Mgٴik;vrwO+CgUFWn?2O*(Yl&?TեE\.//&LMtO@! 4!|OaCbgjSrZ)qkyؾr ]򶙈|7wr5:^~k#,n΋5XtmZ'nI,{+PzM1ؙՉٯŃ.[-݆"IʗWѮCEtѰs^hHfa MYRL+ubYfyhLE5|\S45#S`Trzd8OVِ4)0UBi4 +ׂ8hfӎTVWL"yA^2M1'm࠲!:c*buדIco?ʛ^,rVCWNX,uc &,R]4|P][ +Y-I1Qݣz1;ٳ*h njhI &\6+SP=Zʷ5 +]D(ԏD/E暅>?o9d7"/rh#7?]hJ6'04I$F"wJr'Zo]t)G2ΖȱdC*<Yce⑹nn\Ϭ9q4q02 S\61mYzf>%i^ЋjEg1:q9wwIm lzk1SJF-)*׸Ձf@ѹn(z/,ou2E|C ++F^lnQ\]jѲݲmk޹PƘJ]}!(tXA&X-6w}~>$ THixoW`$7 x_ƜŰ W/$ޭXg,Y>3 +.UXKo<Ʃ`0%#)b> +ACmq)MPQ^X+ Vo',5S1#X}p;"бf +-S.!\ie<#]UbNȃՎv4q&,[OϙGֶS[KE&; 5wb[`u6SV{7gw愡ޡ5{g}sK/;{-Rěkt8|˲VIϋyrM\tT^ Jx$3q1&Z*|}q-q>XsV*tp,IlwN< '+:gן̬ X GeON[=C/(dx"8SkS爳$&O.U3WMGIJ |v'=MZL q ͛4 +kAW}eQgYm. Ql*_)&Wll sqcEw.׮*M}a4c6걩q #j#"MTb:6d3 {,+Zk]D%5DK&yAvN⮽d*/Q=erÏK <+I$j83uڅ j}0NKxz>GQ}ғq?9*^k09 wQbȄ, s|Sֻ,Z%?]ly$8G>>\]<>}$?]]^4.>{C3Kt|pzla`l#)`7G9_&y 60 {Ac/#-j詸bq۞l$ꎤu2u]G%(I8.bmt5oeLqtY[`B1$>@y9Z:.dqZ7X/A9Vc0r ӫ/Y,-k 1NQakKWڂilq|Ғn9)HCR[G[X){җ/"[еʞI.ɓoHOuz~_E7wb.Ԡlv4?+^kչxbga1n1WqpZIeS C¡^{R=T?XRlxoJѶ:(7<5e{ di)lwxdDrg8ؖQm0ԂLtsJ=@8ߓPJ1"@d(H{1hAs:c2ЕE&smjNEDbNfğbqit;b^-N^i,k$6vX]4#=o-^r.ZVgqTDbCY*ET 4ǛufZѐ{ P+3DD,:$HvZeyΘjNgb^"RX1`ܯmUw-dU},r@ |Fhj1>\i^-:A! +8@!%XWP/uh8՜M6iXN}[ƚgr~v/`X+"}]>×$*$թab..R,jqJU R3p7i5k39JX H>pOY[Sw!sk,YXTt63Su+S%3.!jFyT&k:4\6W3U!r~G:|%wIK8`oE/bsF9Grt?$i5<0{S HPˣ@iy펢#"'jJ9,Cc>nG'4Ţc>awbzL]#S@L\x^MrnfxpXr~zH~\}M7('y1Rel j,K(` C/T+,!B5n EOR{B]uX֮Ե}Eg[$}hOY(~^78\Ӫu6V1`FY 0tZKMm'( t*n8qQxMLJiizE*냚N^)>g6Ң,-h9~ ["vŭXV*k!]_FjF곱h Wz$mO'=aOr!.7cl6 bTӛTˣ6渞z&e?(Xv>HΠdWMK^[<а9\ugcB~gxU>"1{׻vFZ@*6 +V@q17>ǭ9NEz4U6'=(O,?LM:1Tu8?g#jWVjE}P Fzpi&ͫw ֬1x vY1clV7KMoWnMn7*R shtJXh>1a̴VwKv#&31d@ew&a28Aɑx.;Yry4".r  jjhr3U'Fx{aVpۋ;5ߗWgoHguJ|)LܖRp<_ 7V_V-/ծj|Qu['\ #25xL6B;FL>1hvF$rܐ؆iX MVe~Kv YE" q`AYqJ -JGٮڜeKѭz=:)8!aУg~DBǏϷdB8e3 ץLs;bG5̇ ahrNi0RL_(v( ,vHC_6)<+*/w9[1-DZ\ -޷ / MߌnI-A"m`il0Ge<4eF؜,CXY4 ƀBw4$kD Wh2 1[oÄݴ%B;0Q +7"4guVϸ vU% _M-č,Dۧ]V$57a8hiF$pT$A"AhPњt-XՅ" DzBB1>}nf`pPE:8?-^+em܄ILh+ ,|7ia-]nn{uy:mQi ĶMMyDo@T30=|- 5KE$j;mX9ފ#yې7-3t=B(A)5؍j˰I?*> ` aĈ eBQ#.uc +xz`Ջi1BS*z4)߇W=ߵM#LbO~H h(&9ğ1 U/L'!bh@wV ΆS +ԓeDI:aA#3\CB)ÔBL.\@-ؔu\aD^tOS +ߕ"[" v@`n2ΓbΔ +ǂs2m j>@tےb‹(85a߃!L!&]P~xtT.Kҝ<4,Į&J&LF,/7C`!pr816dG-)QL$",$ *i8bX 91eI!l,ٞy@6m1gX({vVix굴}TwC=+َ,@|([nσ!s<#jؑ3S0zq`Qx4Tr;IUBq"=<*)!|SCyâ"$;|bl3V% C[ 9P 5|t>W1"[Q_ hwirmH SF–t>BP4.<+/`<qeTS@xn +J*<M(4)@ ppoĹseG !-ycݯSm@wM8 =5Ns zGbBk' Aa{w@tQ\hQPgнm#g+z?^S@=u8z0aG6"TmnY ^%[< +<7Hҳ:hiGk"U.7dQ4ep?mnB`P'𳝐V!?( ͮP;DM 'A܁PW0D션#~ +hݛ ?,E"!pٸLÁ_I(ZCڜ +p@NeQC?Ļ[X.LpRR +xtpaCAȀd*](`̇Wda! ə +)М _j?px%~[.UQ!6Z l#wBfCSHðaew-aIyV#9kxzkp0|`\^vB-'ȱ^~v~8hCs,BVy2;mQxmP-z å1`@x̄`4}#D F){aB7 b-"8hܫ01JnБcc3*K@,ãl* قPDZFFI:${4>Њu} mS90vBR!?x@o ˫;\"Ws}Fɘ .l/<#`;P[Q!qܘ&$団X %L !D\ wƇ r H=˨a8ޚߦU*Tkm$Y{w|?^ذ/&-"}_sIEE1p1Ů f㜌Ɵ >!e\&C#҅ !!Nk7J5{%*9s8:e\37?pVv8 +++(ha 5nqBM:58lсX1]U78n7I_xJ-IeD\O?\Cu:N]X*#p^k;.jyhQE1=y͚Ikt9*G)jjG>|yk<.&MRV+yA- +vU<5DCjr֛jo[~<+sZ6E%u  k1Hu-[pۃ&P$@N֬VRu9+"M7 fn;; +@Ő|i@zV`G=.{;vv<ײ`K^9{3FtНdnᤕ37 Qvj0O<ݮ%X D$X-;ƷC +0h'"mwK^..ra2H,]Xi 7;e*2z:v}A˷QqLض +`@:dyb)&b:btV/[i) +vc0_^z[6tuvS0P.֐#g +}K-NثU!(*85:82VE +^`Թ~^S@FJcaލ64/᠒1IBnVX\4-&-1{S-d Z+`<@,]2衿exjpW:\0!r(Pe Z_ m5*S +ڙh.֩x쉻^GK,t, "KL;a+_;?US{\b!Va!~ڀC$`E4lj0j!bm%Aπ Z`=fA9uT ! 3K3('rv!Uw[pDeqrjz?㸲 +hE0$DҰ@2̪+X" 'TQ֫cTY F,6J"6vE + ){"Ne"==L[]1YX"4T# +/p=%w9꩸vgw@Z;V҈e,@-z& @;ZN ؐnf]tR Z$ f%*3]=) pX_M S`c3EȲ[B)8SkoCtS(;BRdɩdiu-/ËQ$ ]hU-eNBBGpjOAIʩy><Ïbi83b(`(C<0Z9Z(LY)SDXl"*4Py[:rZteD{X&xXd;[d2KPBƳQ]cmC +M#X N % +5ATU!Dgy+?#eDJFZ:n,`Ql2]6 # XdB~&خ{vԑ)8ޝ< ZRv41 +iG1xCm +2U{X&&T)6h6!@L%p c)X+XX`'ɬ@[pv%桤вL#BN՟r"Z6S/F7 69۩#|e  +T01WJ>!0 BeR9Sb]Et3}A)4N3I(/zeIkNXtw,ӓ@0nr AԴhIƞ,8Ne]$<@`N͆h$"/Xz3iӁ3K4,@Ikh"$C@c(cpz*m pɛNa01d`S@C,Uk,@i),& k|` &~TdrV4[@o v郆a[&n-[DV13&}ɫ{~. ++255Steڞ5G/Yy'(oA--s17W6HaD$~eWo'愨B6Nnv+V tz.EMݙ8b׵rea DʙfyX!(g6k\b-PK; @ec*UZ [T. &nF`SM&4`ZQ}Jp" ,.!ڀD6j:9lWm;Q0wLZf-bPBp4=XtST#+ڡut1?l#zF.t鮂7!N Ũ&F=[.H)X +-K€&S@p4Xh_1^fC]h)K!K8^s̑3̵7U8CXL%l>Kxu!Y.p٠|M_\x +Tū.dT L\Uk1.&()DKHxd\ c2WϷ=K&IK]g!|ab,80\,T곩N,75)Fp'}/\ȭYK2$2/l?$NTb;TBXW +_x @0Q+wC(! #̟wO\ZG8lmTդzd?98٨X <[7t"@@7fA/ZJn*Թ Hza:.[j8@PbO[6^2Mj' UM cAuPj|8f`n7w_ZԋVȾu.ZW:,dW*a+q)W :ӄ"cZ%j,&`|(&£ _f;ˏb9f#})A2'?mӘ'e2lEɃ +#+M>}Ɇmʮ\&35PL ~>ut6'h (zv nMmPbk +{08`0L=O] +]SEۦ-`Ͳ"OW.h \#ة*6e(%pLsQh21 kvm +sgaO"K!|d!yKbVT`LOyGPP^#6hn^TCGNєM9,&(,.u=6= EaDpHUyd sr{`a$\{ JgPX璀*'^umFP@ڗ s*O~;t;1V1jkSB:Òh?ҋT 'kh/M05PJC )9C`\Y[nj#R4h F\1t#ıB.ेyv  +aF(h +xJLYiB:6!U,mT5Li L})յi%Lq(UG`nZmi6=d+&cJ>zT`: ,}%YۗйkAԾeN]/~_:EQ\g02:kp2+˓P ClLbrưP\O< A \:$<ڂvHxxn *,sO[e_+/|MOϦ?إM+ڟͫ\1[>nLA ;q xC oO.+d|dI1it_忾pя#[~N|NVcg|\?~,>pOuvϗW ,^AQ_~ +hBhp#8>z};_[Nog+,tdt8Uezxv7Zv2ї1r=̻rY9w%6~|m<׆;|sL}U 7$6[}:kwjgwlYsuOo\>~l%;̝V\ '!6W;֡C|Kxl?6F>MukJ?χ8^uL=n{vsG#y],'G8-t.v9ܞcy1cYbXƵWgwH|kFٮ׫Go8^uB7u`†^sGك_փܮ4!Y>K[.n!}ƾ߿e0;AoGt:<"!t>6߿C0QkbGUbVNNF 2nB7G#1}&vg/;uO%۬Gzn]F +}ou5<W*ٻqQ#>yHϫEZ-f &lF,0|d bo+ ܊^S̚?~_pwJyN!uHpv͜ w!s_~f\^6C5;FH?'qiD!_< GnC oau掫1q: sq +}ooӺp{Xu{͸wd$k))z$1 :xi-x7.$;)P0C2g1iw*@?~Wd|cڇc~}\l>#1O O/y|mNjpL㶍$gn1 v/9|7缌] f>alFtܵmqt_|s : gah_*==] m(Dk*ͱnF]ⴾR euo7 Wh~շbt/{' 8}N4tR%:ީ]_rNFϱLU +U?%?Q67-qa[O>q7/ix4|٩qa?5>nyC ?Uޏ|ޒ4$mUgKO_OYL3 +;_8~rZƶ-޿RIwHh+Ez}źzK*}|&S}T㥱ލסƹqJomnG~~u2S8E7֛dvLSbQ-qBrl忿$LZehAw4 H{jQ&2AG6=+HւP@&{m﷋FbU':0 +gt6'>)@eժ-NL,aWB$ # "=wixT$fMԺ#Vu4wB)͌&rҡӳVʵJEFϸ680?YL徖BcrC%| )vzbWBOBPx0pJ/0 lٙwv{(;g'Ydf4BKp\d +3+.I8z +>ܷ?{vi=Γ >), YO$F/d|: Y$ zxqv"6<7:ꂧ\@=[v6%㦘(` 1|?`dEsW: sJ0JzAl6v0w~Cv|xz;`loьu{M c&l*v\p;>x*7:;A[i#RR>Dch pph~f?2?BpтO 0h$ݸC Dy++~@Ѵ$Ю(ѩ(^%PP+PbKO'$KZ{+9gb2ECI +T>X_9"й-tB01NZXEU+r,B|x&x< )B^'ZdOU׬ià8/8 f8hNUGѩAFD +aҰ~ZBPpv">%DidžʪBxxPY +Ya@Sjjئ +ż@0ocolTt4j3ZY'qqyG #@S%_d y=x*1b Xt6c I,TaH:eKeX*  RDp*:TyEA"7W +* Jp.YN@PA.I}˛GI)J0-VM|0/`]>J5 aWgQ!&AM">σ>kxa)LsHz&E|Cn4kyD5M~PZfL]ds +|c\?{en r#^$8vIiqM"9Gb!r|TIWgF. -d?>E -3_ n딩"89u8a +JNpC Vԛ Y=¨ Zcǿ7ŨR-+} (C[t}£\ ,bÔ'̲*_@cApѢ$dFYD FK@R~YSi3}t2J>ϑs`+0,"Q1a*oAK}Y,-}U2A%Hc*Ss,7TE7> Tjp΃x>c jn䊸so,ׄ/JcDO?ܰTbqgr!^=f4$7D2vqp}/WI_3܀WeOe7b5`]ef̉ gOv(m(ߓ&$|{h,;M)FJ:wCJt 잗24` o}KXxmen׶2S .[t񧤳=G2 F*q_- nOס50/9W"ǭ݄(}]+9HF5Z7aG X eX2-䔃)݌pe<*A_! ߵDV }L Fp9 Zrkȭu]'%h .jl8yRmn% t[va^˰AEAf sBf~(W@*HƘ[03k0R*@&M,ZUz )be]t-6V>C0[ˈ|66Eq>d[kGry*Lq6X!&q r ε@JWj7SPu LS#?`[ BWp~ E]7*TjW5m>FeW!Vqa0SI@n2NcJH 䐸{DxEګ ELjS-C KIhIIw^쥑RWPןDƤ~,?NV8;Ҡtfd6Q'h,?FryaSTXfseP +74'D˒w#^`PzGba0 8g]¸2}v=K;|v#H\ߤ.C06_ނzJ5>WZ(VX@OѯRե/Z 8/n!K vvK$FuM $чp%@@Cu:vj`)vEFU&9MO +,I_{h*#=KTd=t %e(ĩg +uEeEGuGh/іh;j&e*|89  ^l8}M#@/DTo<@' 8 +u0H=5Cx:aܧ~ t^ Z`jE{yySoP3B\y4DN5AM)NnwõF,tO'ةUVh"K..7~#-^(tFt +O@Xj/y s A65tX ThúJ0#="]@}ta5(@+Un?'nС MP"(6(8j9:CCtTфͺfMW*]1ʃdh`E`d}F"fp|bB\K1R +lwA_9sa2H =hLG1%y g]t6$ʞюcһ!K?»QHZ$[8!D/q=}C<8 +HKQ(}^yRm4eAASj!?R0 m_?cihrf@T*wiH6AirZeP_&8{lqGLD⤄l +">iAg (f8j-qA䤀 ad1sUld\\f<^<Ħfr+X2ku+q./pk sD1,=ޏ3 fv׳D h2.hFp98J(0A0fiڌ۬@ی͹ps6 %w2 +/҄@@16*a>+K}fV}%UpPeSaW;NW LJ,RBD>`K[2eRV$5~l>$"DoW˛W3n L>涙wK;];h]ܓ +-<Ǐx0#-L=!Q M)5Z7)pJ%ެA a2iַfw>=LX|BUnyŋ;p#!@wD4N(gMY} le%dkEէ; Pq ׯT[a, IDMJ%cm-a Uv":'AWh~Weh$_ g'Xef \eγEe_0TSX'gP\ߝ5*'w.@EeǦ.4$ABkk)1H#Jibc4/Y`qBPgxF C(M؎0*E}b^T"Y2 +EPKkCpy,-X(sٞb h(45L54d_< oehl2?2_X%N8PJ+^DF@+ݷcxr0p0[)Se5TgKx a8?ӝ85`k:-$lg˓݃rYEfdCK ;@10e}ʠBB*L$7 4߇|OuDIC6tz# (&l:0n(܃/OLl?wP/X^0E]4z=;WRq)*ތc>v|ZX?fЭqUx wv8q֋=ȜhڈK-k,/g`~5VrwP/!7RB@{Maw\[:(lȋs8aѶ oe +2x~a&' %3 W0ڤ7*аAsk\@ǏP),^S|>E%œ'O((*n(>}@ 3 +ۿMAPWh+j-vpD1/.E,ph~Ww7. +/b7P"5p`u ŅYgmr1۸?nI/ VepqIk2 Jy¹][UQاe7M 9iWV9$ `fwBHڮmákP;0+TAe:>Wm)ة̬'Z7+-r$ϋ[u\,pM]a{]M>T# Y0>$63/Ah|,@8cS85ZT%(!~{፝<YGɿS +J^.)4܂)Xl?0- bK!r[qx ! +lqU'DalW-n +L38ٳ%徭[fSD8XB)R|9;а=XS2QSN[*) +F`b{9lٹ +2$},  ΰ2;J!aW 2C a+H|fo@ 4GnAbiXC[Ǩ(0T8quWd(9$ nr^(6gB&IS7-IVR82z(Db\2Z8.Cz1r:FƟ0_+dž[}G:.΋o :zGl!{_΁ܾ9?&GK`)-s(үٔb&'E.T䁢"P>jjFҒe6- +sbϪIvBiIiG{力ևwy:2P? \[8 #Dxʡ(~IUN2VVdދSS<}r`ý\Jijn^7.QơvSlV7+*(XL9#/v>qulV&Sn`흌 `x1^22[P/̾Y/?~em@o=JXq[k|AQ@KSz 0[Y\: 1;Y!4IW8Іp'ԆVq;fpX<-$6UBd)÷o +Vu2Bf٬,+#ٌhYev6#jiV1dO41c?_Xfk×؏4\CzxiiʂpF𬦇gDNDgD LDnT⬢gH). jqVE&&f8t㬮gv ӏ3rV#g%9ZrƩѓqUVl!嬪0gU)@ +әbL0)z7G5'7Bl*SҘ`0zxTij&.MzC7+p_H +=QsUX2+kqDt ӆw.b{̓MRZo==weN#5kmzs(p +0K!VP TN#`d @))}pD5TrFGGi*īYbѥU;H]n>lT645C`\4#4`epp@H+8˚ag+~KK)P}WK}2\,>]?wFFexyk0^bB--Jz'~Emsk㸞BV{P? mx+}%"/ D_ꎶWIܾxhe:;&B,vbiQJp[sx%| Q +mWWVqDjzmUz)KcUYg2xo(dг46` e0!MEP[-e0a:zֆS5lsEC&izGpPl(Ä1 aYN\͝3 e0#lj02LӘ;zVS5pƘ ׳;`60aLu,f570aLCܻz[R 4Ľgy5wP cճ<ܚ`h(Ä1 jR&2L֬sWGC 5`WpkV tĽ֬TTa˜׳<ܚ`i(Ä1qgyU1w 4a˜kӳ<ܪՀ+e6e0!==ëZ ϣ Ue #ӳ<<0aLC{zWhaKņ2Lն[;s0aLg殍ط.Ä1 ׀gyxU1߽P c:^Vch(Ä1 #ӳ<<0q#,f5xRQ] cLB_Ga +7 _]V.w4lGNꝽҚZAdf\N\`=:\1A|^Ÿ*zt#c.KXP c6'ؘR@V>i-ج93*?&:lN1܇WYq*GVL,?57 J[WTi!+3lna)d"hXBe 27*C_p2ldZFYTp +*F֋eٞoG +!YBU&[Hmn&U(d KVJ6fl-cTp5,&[e2Ȫ&{fSAHְd#ֲo[O +!YÀrTl[˄mnDU(d ;QQsl-C%Up5l)GeK9ql)s[B GPVBXr|kL$+V nU=@Z-Upa) +nU,G˖r<1*B2+i6Y:KۆNqC( [-veBU+Z|JWm#x2K *Ac;1X{YdZcdnM*# z*Zh+ 5jCg:/ycմ,N]sn'z:T)v70Ӌ.vM  l;5;nmnۍ>T1-U,-U,-b(8n``wG.V.$8 f9݌2$ f݌2$h(oB7Þ_+X=]4taL0{ryQ.syT˃VP酊 Cf =fib1q/T&Pqa釛iJ7NLU2wD*!7nO}S49[Fd2޸Yp׍VLU +tiJnȸU)ѪS73vLƭJn}e]ʴC\t h >M3:I>WZ']=mw;hpZxKj +tО`q aק}AZP5id>]d$7G?bh7}`h5H'8jF2Cǘ'hyO K*MuqL-DTJ?_CZR} ~heNjdvFxM +-VeD J:J@Ի C QWItGѱ"opMJDm{@: {*I#5`˄RTeRiLC-KRɥ;""l`ꖪL2MT!aDSTe ١"&p꘬bRZ/.LR[2d+-wlL@uLV&lN!‘([mRL`@V&lzLHJM F,RJHR)?4!dRJ +<!+RR&<+)MfٝRJy HɤRh4<R2)(7"2)ȥ.I(g0 }g TA&D$=rFz\sɤ#шӞTbvZ**vۓJ\K\C!^{R 6;k4s +')f@\ˤ+ψ5ɡLףʤ+7q{2i:z\Z&\uHOړI3zr8R8BIJ=ùJno L8ΆRyd7FRknM84&<=1K]|1FCե]i`;o(nMzbpU:H |E|k +! +߇UMpYz+Fsy &↷!hwhh >I毟}oO?amwuqQ/ 68<]$ӟzдiI|WU0Z%/].^t9^qu2ɳ CZT袟Q?-_~y>zu8|?o~rzXvTg]+tïwo_q|zS4\tǓǟW~rij_'.'OO돿O&'?/~%ؽߝ7'g_bS?OO|S>~_?|z0z0_ߟe1ׯ/yo~Oj?G?5;4ãwɧ7E[W@jY$syI[Dop1XD|uA`I mn a3$*]z۔ěEFt)tb]I_,QiyF@{=Y4,EpH +]>*Z^v6<;‹p{Hy|8NK6~V+18 eg |7ݢ( jʊ(mߧ+$#r :eևx㞝yx^O?;k?|g}WeH.\P"HVIY4ڮD=~] twK ~lG1f^]_Z. _?~a=ܭwy\Yqpv8\lxU .5!hDZ ##(jOlA4*1b!2+*HF6^@b; +]3(Iu *fxIVwEkt6yS|:oS/o Sз̍O L}&|oscrB2#@/"4Ql +DɁ 3; )!"i $* +?KCuI{iB$_Z *+AleFyh0`¦WKeUB5y`~r0>SPXuq$PH@'@"N-6L +-ˀԖTKTTҚk0s߀v4X0ˁ#kep*xlE[Xl̃4,Y!`_!li+isdIh*^aQR 򗠏 T.`.yB-N&yY~%lEP|̷/KM+B/AX/a@S0jTHgeM;Qv_8T[]ە&in&ga^{qmOggCW S`:vqHV Ay UգVcBh)(/g \ No newline at end of file diff -Naur drupal-7.0/modules/simpletest/tests/upgrade/drupal-7.trigger.database.php drupal-7.66/modules/simpletest/tests/upgrade/drupal-7.trigger.database.php --- drupal-7.0/modules/simpletest/tests/upgrade/drupal-7.trigger.database.php 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/upgrade/drupal-7.trigger.database.php 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,28 @@ +fields(array( + 'hook', + 'aid', + 'weight', +)) +->values(array( + 'hook' => 'node_presave', + 'aid' => 'node_publish_action', + 'weight' => '1', +)) +->values(array( + 'hook' => 'comment_presave', + 'aid' => 'comment_publish_action', + 'weight' => '1', +)) +->values(array( + 'hook' => 'comment_delete', + 'aid' => 'node_save_action', + 'weight' => '1', +)) +->execute(); diff -Naur drupal-7.0/modules/simpletest/tests/upgrade/update.aggregator.test drupal-7.66/modules/simpletest/tests/upgrade/update.aggregator.test --- drupal-7.0/modules/simpletest/tests/upgrade/update.aggregator.test 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/upgrade/update.aggregator.test 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,47 @@ + 'Aggregator update path', + 'description' => 'Aggregator update path tests.', + 'group' => 'Upgrade path', + ); + } + + public function setUp() { + // Use the normal installation and add our feed data. + $path = drupal_get_path('module', 'simpletest') . '/tests/upgrade'; + $this->databaseDumpFiles = array( + $path . '/drupal-7.bare.standard_all.database.php.gz', + $path . '/drupal-7.aggregator.database.php', + ); + parent::setUp(); + + // Our test data only relies on aggregator.module. + $this->uninstallModulesExcept(array('aggregator')); + } + + /** + * Tests that the aggregator.module update is successful. + */ + public function testAggregatorUpdate() { + // Get a selection of the fields affected by the schema update. + $query = db_select('aggregator_feed', 'af'); + $query->join('aggregator_item', 'ai', 'af.fid = ai.fid'); + $query + ->fields('af', array('url', 'link')) + ->fields('ai', array('link', 'guid')); + + $pre_update_data = $query->execute()->fetchAll(); + $this->assertTrue($this->performUpgrade(), 'The update was completed successfully.'); + $post_update_data = $query->execute()->fetchAll(); + + $this->assertTrue($pre_update_data == $post_update_data, 'Feed data was preserved during the update.'); + } + +} diff -Naur drupal-7.0/modules/simpletest/tests/upgrade/update.field.test drupal-7.66/modules/simpletest/tests/upgrade/update.field.test --- drupal-7.0/modules/simpletest/tests/upgrade/update.field.test 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/upgrade/update.field.test 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,61 @@ + 7.x update path. + */ +class FieldUpdatePathTestCase extends UpdatePathTestCase { + public static function getInfo() { + return array( + 'name' => 'Field update path', + 'description' => 'Field update path tests.', + 'group' => 'Upgrade path', + ); + } + + public function setUp() { + // Use the filled update path and our field data. + $path = drupal_get_path('module', 'simpletest') . '/tests/upgrade'; + $this->databaseDumpFiles = array( + $path . '/drupal-7.filled.standard_all.database.php.gz', + $path . '/drupal-7.field.database.php', + ); + parent::setUp(); + + // Our test data includes poll extra field settings. + $this->uninstallModulesExcept(array('field', 'poll')); + } + + /** + * Tests that the update is successful. + */ + public function testFilledUpgrade() { + $this->assertTrue($this->performUpgrade(), 'The update was completed successfully.'); + $expected_settings = array( + 'extra_fields' => array( + 'display' => array( + 'poll_view_voting' => array( + 'default' => array( + 'weight' => '0', + 'visible' => TRUE, + ), + ), + 'poll_view_results' => array( + 'default' => array( + 'weight' => '0', + 'visible' => FALSE, + ), + ), + ), + 'form' => array(), + ), + 'view_modes' => array(), + ); + $actual_settings = field_bundle_settings('node', 'poll'); + $this->assertEqual($expected_settings, $actual_settings, 'Settings stored in field_bundle_settings were updated to per-bundle settings.'); + } +} diff -Naur drupal-7.0/modules/simpletest/tests/upgrade/update.trigger.test drupal-7.66/modules/simpletest/tests/upgrade/update.trigger.test --- drupal-7.0/modules/simpletest/tests/upgrade/update.trigger.test 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/upgrade/update.trigger.test 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,37 @@ + 7.x upgrade path. + */ +class TriggerUpdatePathTestCase extends UpdatePathTestCase { + public static function getInfo() { + return array( + 'name' => 'Trigger update path', + 'description' => 'Trigger update path tests.', + 'group' => 'Upgrade path', + ); + } + + public function setUp() { + // Use the filled upgrade path and our trigger data. + $this->databaseDumpFiles = array( + drupal_get_path('module', 'simpletest') . '/tests/upgrade/drupal-7.filled.standard_all.database.php.gz', + drupal_get_path('module', 'simpletest') . '/tests/upgrade/drupal-7.trigger.database.php', + ); + parent::setUp(); + + // Our test data includes node and comment trigger assignments. + $this->uninstallModulesExcept(array('comment', 'trigger')); + } + + /** + * Tests that the upgrade is successful. + */ + public function testFilledUpgrade() { + $this->assertTrue($this->performUpgrade(), 'The upgrade was completed successfully.'); + } +} diff -Naur drupal-7.0/modules/simpletest/tests/upgrade/update.user.test drupal-7.66/modules/simpletest/tests/upgrade/update.user.test --- drupal-7.0/modules/simpletest/tests/upgrade/update.user.test 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/upgrade/update.user.test 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,35 @@ + 7.x update path. + */ +class UserUpdatePathTestCase extends UpdatePathTestCase { + public static function getInfo() { + return array( + 'name' => 'User update path', + 'description' => 'User update path tests.', + 'group' => 'Upgrade path', + ); + } + + public function setUp() { + // Use the filled update path and our field data. + $this->databaseDumpFiles = array( + drupal_get_path('module', 'simpletest') . '/tests/upgrade/drupal-7.filled.standard_all.database.php.gz', + ); + parent::setUp(); + } + + /** + * Tests that the update is successful. + */ + public function testFilledUpgrade() { + $this->assertTrue($this->performUpgrade(), 'The update was completed successfully.'); + $this->assertTrue(db_index_exists('users', 'picture'), 'The {users}.picture column has an index.'); + } +} diff -Naur drupal-7.0/modules/simpletest/tests/upgrade/upgrade.comment.test drupal-7.66/modules/simpletest/tests/upgrade/upgrade.comment.test --- drupal-7.0/modules/simpletest/tests/upgrade/upgrade.comment.test 2010-09-11 23:52:59.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/upgrade/upgrade.comment.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ assertTrue($this->performUpgrade(), t('The upgrade was completed successfully.')); + $this->assertTrue($this->performUpgrade(), 'The upgrade was completed successfully.'); } } diff -Naur drupal-7.0/modules/simpletest/tests/upgrade/upgrade.filter.test drupal-7.66/modules/simpletest/tests/upgrade/upgrade.filter.test --- drupal-7.0/modules/simpletest/tests/upgrade/upgrade.filter.test 2010-11-09 18:43:10.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/upgrade/upgrade.filter.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ assertTrue($this->performUpgrade(), t('The upgrade was completed successfully.')); + $this->assertTrue($this->performUpgrade(), 'The upgrade was completed successfully.'); $format = filter_format_load('1'); - $this->assertTrue($format->format == '1', t('Filter format found.')); + $this->assertTrue($format->format == '1', 'Filter format found.'); $format->format = 'test_filter'; $format->name = 'Test filter'; filter_format_save($format); $format = filter_format_load('test_filter'); - $this->assertTrue($format->format == 'test_filter', t('Saved a filter format with machine name.')); + $this->assertTrue($format->format == 'test_filter', 'Saved a filter format with machine name.'); $account = user_load(4); user_save($account, array('signature_format' => 'test_filter')); $account = user_load(4); - $this->assertTrue($account->signature_format == 'test_filter', t('Signature format changed successfully to a filter format with machine name.')); + $this->assertTrue($account->signature_format == 'test_filter', 'Signature format changed successfully to a filter format with machine name.'); $delta = db_insert('block_custom') ->fields(array( @@ -51,6 +50,6 @@ 'format' => 'test_filter', )) ->execute(); - $this->assertTrue($delta > 0, t('Created a custom block using a filter format with machine name.')); + $this->assertTrue($delta > 0, 'Created a custom block using a filter format with machine name.'); } } diff -Naur drupal-7.0/modules/simpletest/tests/upgrade/upgrade.forum.test drupal-7.66/modules/simpletest/tests/upgrade/upgrade.forum.test --- drupal-7.0/modules/simpletest/tests/upgrade/upgrade.forum.test 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/upgrade/upgrade.forum.test 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,64 @@ + 'Forum upgrade path', + 'description' => 'Upgrade path tests for the Forum module.', + 'group' => 'Upgrade path', + ); + } + + public function setUp() { + // Path to the database dump files. + $this->databaseDumpFiles = array( + drupal_get_path('module', 'simpletest') . '/tests/upgrade/drupal-6.filled.database.php', + drupal_get_path('module', 'simpletest') . '/tests/upgrade/drupal-6.forum.database.php', + ); + parent::setUp(); + + $this->uninstallModulesExcept(array('comment', 'forum', 'taxonomy')); + } + + /** + * Test a successful upgrade (no negotiation). + */ + public function testForumUpgrade() { + $this->assertTrue($this->performUpgrade(), 'The upgrade was completed successfully.'); + + // Work around http://drupal.org/node/931512 + $this->drupalPost('admin/structure/types/manage/forum/fields', array(), t('Save')); + + // The D6 database forum vocabulary contains the term "Fruits" with id 81. + $tid = 81; + $this->drupalGet("forum/$tid"); + + // There is one forum topic in Fruits, with the title "Apples". + $this->clickLink('Apples'); + $this->clickLink('Edit'); + + // Add a forum topic "Bananas" to the "Fruits" forum. + $edit = array( + 'title' => $title = 'Bananas', + 'body[' . LANGUAGE_NONE . '][0][value]' => $body = 'It is another fruit.', + ); + $this->drupalPost("node/add/forum/$tid", $edit, t('Save')); + $type = t('Forum topic'); + $this->assertRaw(t('@type %title has been created.', array('@type' => $type, '%title' => $title)), 'Forum topic was created'); + + // Retrieve node object, ensure that the topic was created and in the proper forum. + $node = $this->drupalGetNodeByTitle($title); + $this->assertTrue($node != NULL, format_string('Node @title was loaded', array('@title' => $title))); + $this->assertEqual($node->taxonomy_forums[LANGUAGE_NONE][0]['tid'], $tid, 'Saved forum topic was in the expected forum'); + + $this->drupalGet("forum/$tid"); + $this->assertText('Bananas'); + $this->drupalLogout(); + + $this->drupalGet("node/add/forum/$tid"); + $this->assertResponse(200, 'User can access forum creation page.'); + } +} diff -Naur drupal-7.0/modules/simpletest/tests/upgrade/upgrade.locale.test drupal-7.66/modules/simpletest/tests/upgrade/upgrade.locale.test --- drupal-7.0/modules/simpletest/tests/upgrade/upgrade.locale.test 2010-10-06 22:38:29.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/upgrade/upgrade.locale.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ assertTrue($this->performUpgrade(), t('The upgrade was completed successfully.')); + $this->assertTrue($this->performUpgrade(), 'The upgrade was completed successfully.'); // The home page should be in French. $this->assertPageInLanguage('', 'fr'); @@ -47,13 +46,13 @@ // LANGUAGE_NEGOTIATION_PATH_DEFAULT. $this->variable_set('language_negotiation', 1); - $this->assertTrue($this->performUpgrade(), t('The upgrade was completed successfully.')); + $this->assertTrue($this->performUpgrade(), 'The upgrade was completed successfully.'); // The home page should be in French. $this->assertPageInLanguage('', 'fr'); // The language switcher block should be displayed. - $this->assertRaw('block-locale-language', t('The language switcher block is displayed.')); + $this->assertRaw('block-locale-language', 'The language switcher block is displayed.'); // The French prefix should not be active because French is the default language. $this->drupalGet('fr'); @@ -76,7 +75,7 @@ ->condition('uid', 1) ->execute(); - $this->assertTrue($this->performUpgrade(), t('The upgrade was completed successfully.')); + $this->assertTrue($this->performUpgrade(), 'The upgrade was completed successfully.'); // Both prefixes should be active. $this->assertPageInLanguage('fr', 'fr'); @@ -86,7 +85,7 @@ $this->assertPageInLanguage('', 'en'); // The language switcher block should be displayed. - $this->assertRaw('block-locale-language', t('The language switcher block is displayed.')); + $this->assertRaw('block-locale-language', 'The language switcher block is displayed.'); } /** @@ -96,13 +95,13 @@ // LANGUAGE_NEGOTIATION_DOMAIN. $this->variable_set('language_negotiation', 3); - $this->assertTrue($this->performUpgrade(), t('The upgrade was completed successfully.')); + $this->assertTrue($this->performUpgrade(), 'The upgrade was completed successfully.'); // The home page should be in French. $this->assertPageInLanguage('', 'fr'); // The language switcher block should be displayed. - $this->assertRaw('block-locale-language', t('The language switcher block is displayed.')); + $this->assertRaw('block-locale-language', 'The language switcher block is displayed.'); // The language switcher block should point to http://en.example.com. $language_links = $this->xpath('//ul[contains(@class, :class)]/li/a', array(':class' => 'language-switcher-locale-url')); @@ -112,7 +111,7 @@ $found_english_link = TRUE; } } - $this->assertTrue($found_english_link, t('The English link points to the correct domain.')); + $this->assertTrue($found_english_link, 'The English link points to the correct domain.'); // Both prefixes should be inactive. $this->drupalGet('en'); diff -Naur drupal-7.0/modules/simpletest/tests/upgrade/upgrade.menu.test drupal-7.66/modules/simpletest/tests/upgrade/upgrade.menu.test --- drupal-7.0/modules/simpletest/tests/upgrade/upgrade.menu.test 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/upgrade/upgrade.menu.test 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,83 @@ + 'Menu upgrade path', + 'description' => 'Menu upgrade path tests.', + 'group' => 'Upgrade path', + ); + } + + public function setUp() { + // Path to the database dump files. + $this->databaseDumpFiles = array( + drupal_get_path('module', 'simpletest') . '/tests/upgrade/drupal-6.filled.database.php', + drupal_get_path('module', 'simpletest') . '/tests/upgrade/drupal-6.menu.database.php', + ); + parent::setUp(); + + $this->uninstallModulesExcept(array('menu')); + } + + /** + * Test a successful upgrade. + */ + public function testMenuUpgrade() { + $this->assertTrue($this->performUpgrade(), 'The upgrade was completed successfully.'); + + // Test the migration of "Default menu for content" setting to individual + // node types. + $this->drupalGet('admin/structure/types/manage/page/edit'); + $this->assertNoFieldChecked('edit-menu-options-management', 'Management menu is not selected as available menu'); + $this->assertNoFieldChecked('edit-menu-options-navigation', 'Navigation menu is not selected as available menu'); + $this->assertNoFieldChecked('edit-menu-options-main-menu', 'Main menu is not selected as available menu'); + $this->assertFieldChecked('edit-menu-options-secondary-menu', 'Secondary menu is selected as available menu'); + $this->assertNoFieldChecked('edit-menu-options-user-menu', 'User menu is not selected as available menu'); + $this->assertOptionSelected('edit-menu-parent', 'secondary-menu:0', 'Secondary menu is selected as default parent item'); + + $this->assertEqual(variable_get('menu_default_node_menu'), NULL, 'Redundant variable menu_default_node_menu has been removed'); + + // Verify Primary/Secondary Links have been renamed. + $this->drupalGet('admin/structure/menu'); + $this->assertNoLinkByHref('admin/structure/menu/manage/primary-links'); + $this->assertLinkByHref('admin/structure/menu/manage/main-menu'); + $this->assertNoLinkByHref('admin/structure/menu/manage/secondary-links'); + $this->assertLinkByHref('admin/structure/menu/manage/secondary-menu'); + + // Verify the existence of all system-defined (default) menus. + foreach (menu_list_system_menus() as $menu_name => $title) { + $this->assertLinkByHref('admin/structure/menu/manage/' . $menu_name, 0, 'Found default menu: ' . $title); + } + + // Verify a few known links are still present, plus the ones created here. + $test_menus = array( + 'navigation' => array('Add content', 'nodeadd-navigation'), + 'management' => array('Administration', 'Account settings'), + 'user-menu' => array('My account', 'Log out'), + 'main-menu' => array('nodeadd-primary'), + 'secondary-menu' => array('nodeadd-secondary'), + ); + + foreach ($test_menus as $menu_name => $links) { + $this->drupalGet('admin/structure/menu/manage/' . $menu_name); + $this->assertResponse(200, 'Access menu management for ' . $menu_name); + foreach ($links as $link_text) { + $this->assertLink(t($link_text)); + } + } + + // Check the "source for primary/secondary links" setting. + $this->drupalGet('admin/structure/menu/settings'); + $this->assertOptionSelected('edit-menu-main-links-source', 'secondary-menu'); + $this->assertOptionSelected('edit-menu-secondary-links-source', 'main-menu'); + + // Check that both primary/secondary links blocks are visible. + $this->drupalGet('node'); + $this->assertText('My Primary Links', '(Formerly) Primary Links block is still visible'); + $this->assertText('My Secondary Links', '(Formerly) Secondary Links block is still visible'); + } +} diff -Naur drupal-7.0/modules/simpletest/tests/upgrade/upgrade.node.test drupal-7.66/modules/simpletest/tests/upgrade/upgrade.node.test --- drupal-7.0/modules/simpletest/tests/upgrade/upgrade.node.test 2010-12-18 03:04:36.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/upgrade/upgrade.node.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ assertTrue($this->performUpgrade(), t('The upgrade was completed successfully.')); + $this->assertTrue($this->performUpgrade(), 'The upgrade was completed successfully.'); + + $instance = field_info_instance('node', 'body', 'story'); + $this->assertIdentical($instance['required'], 0, 'The required setting was preserved during the upgrade path.'); + $this->assertTrue($instance['description'], 'The description was preserved during the upgrade path'); + $this->drupalGet("content/1263769200"); $this->assertText('node body (broken) - 37'); @@ -47,6 +51,38 @@ } /** + * Tests the upgrade path for node disabled node types. + * + * Load a filled installation of Drupal 6 and run the upgrade process on it. + */ +class DisabledNodeTypeTestCase extends UpgradePathTestCase { + public static function getInfo() { + return array( + 'name' => 'Disabled node type upgrade path', + 'description' => 'Disabled node type upgrade path tests.', + 'group' => 'Upgrade path', + ); + } + + public function setUp() { + // Path to the database dump. + $this->databaseDumpFiles = array( + drupal_get_path('module', 'simpletest') . '/tests/upgrade/drupal-6.filled.database.php', + drupal_get_path('module', 'simpletest') . '/tests/upgrade/drupal-6.node_type_broken.database.php', + ); + parent::setUp(); + } + + /** + * Tests a successful upgrade. + */ + public function testDisabledNodeTypeUpgrade() { + $this->assertTrue($this->performUpgrade(), 'The upgrade was completed successfully.'); + $this->assertTrue(field_info_instance('comment', 'comment_body', 'comment_node_broken'), 'Comment body field instance was created for comments attached to the disabled broken node type'); + } +} + +/** * Upgrade test for node type poll. * * Load a bare installation of Drupal 6 and run the upgrade process on it. @@ -78,7 +114,7 @@ * Test a successful upgrade. */ public function testPollUpgrade() { - $this->assertTrue($this->performUpgrade(), t('The upgrade was completed successfully.')); + $this->assertTrue($this->performUpgrade(), 'The upgrade was completed successfully.'); // Check modules page for poll $this->drupalGet('admin/modules'); @@ -90,14 +126,14 @@ $nbchoices = ($i % 4) + 2; for ($c = 0; $c < $nbchoices; $c++) { - $this->assertText("Choice $c for poll $i", t('Choice text is displayed correctly on poll view')); + $this->assertText("Choice $c for poll $i", 'Choice text is displayed correctly on poll view'); } // Now check that the votes are correct $this->clickLink(t('Results')); for ($c = 0; $c < $nbchoices; $c++) { - $this->assertText("Choice $c for poll $i", t('Choice text is displayed correctly on result view')); + $this->assertText("Choice $c for poll $i", 'Choice text is displayed correctly on result view'); } $nbvotes = floor (($i % 4) + 5); @@ -105,7 +141,7 @@ for ($c = 0; $c < $nbchoices; $c++) { $votes = floor($nbvotes / $nbchoices); if (($nbvotes % $nbchoices) > $c) $votes++; - $this->assertTrue(preg_match("/$votes vote/", $elements[$c]), t('The number of votes is displayed correctly: expected ' . $votes . ', got ' . $elements[$c])); + $this->assertTrue(preg_match("/$votes vote/", $elements[$c]), 'The number of votes is displayed correctly: expected ' . $votes . ', got ' . $elements[$c]); } } } diff -Naur drupal-7.0/modules/simpletest/tests/upgrade/upgrade.poll.test drupal-7.66/modules/simpletest/tests/upgrade/upgrade.poll.test --- drupal-7.0/modules/simpletest/tests/upgrade/upgrade.poll.test 2010-09-13 07:50:09.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/upgrade/upgrade.poll.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ assertTrue($this->performUpgrade(), t('The upgrade was completed successfully.')); + $this->assertTrue($this->performUpgrade(), 'The upgrade was completed successfully.'); // Check modules page for poll $this->drupalGet('admin/modules'); @@ -45,14 +44,14 @@ $nbchoices = ($i % 4) + 2; for ($c = 0; $c < $nbchoices; $c++) { - $this->assertText("Choice $c for poll $i", t('Choice text is displayed correctly on poll view')); + $this->assertText("Choice $c for poll $i", 'Choice text is displayed correctly on poll view'); } // Now check that the votes are correct $this->clickLink(t('Results')); for ($c = 0; $c < $nbchoices; $c++) { - $this->assertText("Choice $c for poll $i", t('Choice text is displayed correctly on result view')); + $this->assertText("Choice $c for poll $i", 'Choice text is displayed correctly on result view'); } $nbvotes = floor (($i % 4) + 5); @@ -60,7 +59,7 @@ for ($c = 0; $c < $nbchoices; $c++) { $votes = floor($nbvotes / $nbchoices); if (($nbvotes % $nbchoices) > $c) $votes++; - $this->assertTrue(preg_match("/$votes vote/", $elements[$c]), t('The number of votes is displayed correctly: expected ' . $votes . ', got ' . $elements[$c])); + $this->assertTrue(preg_match("/$votes vote/", $elements[$c]), 'The number of votes is displayed correctly: expected ' . $votes . ', got ' . $elements[$c]); } } } diff -Naur drupal-7.0/modules/simpletest/tests/upgrade/upgrade.taxonomy.test drupal-7.66/modules/simpletest/tests/upgrade/upgrade.taxonomy.test --- drupal-7.0/modules/simpletest/tests/upgrade/upgrade.taxonomy.test 2010-10-06 23:53:41.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/upgrade/upgrade.taxonomy.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ assertTrue($this->performUpgrade(), t('The upgrade was completed successfully.')); + $this->assertTrue($this->performUpgrade(), 'The upgrade was completed successfully.'); // Visit the front page to assert for PHP warning and errors. $this->drupalGet(''); // Check that taxonomy_vocabulary_node_type and taxonomy_term_node have been // removed. - $this->assertFalse(db_table_exists('taxonomy_vocabulary_node_type'), t('taxonomy_vocabulary_node_type has been removed.')); - $this->assertFalse(db_table_exists('taxonomy_term_node'), t('taxonomy_term_node has been removed.')); + $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. @@ -67,7 +71,14 @@ $inst_keys = array_keys($instances); sort($voc_keys); sort($inst_keys); - $this->assertEqual($voc_keys, $inst_keys, t('Node type page has instances for every vocabulary.')); + $this->assertEqual($voc_keys, $inst_keys, 'Node type page has instances for every vocabulary.'); + + // Ensure instance variables are getting through. + 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 // each node of type 'story' was associated to one or more terms. @@ -75,13 +86,13 @@ // the taxonomyextra field. $instances = $this->instanceVocabularies('node', 'story'); $field_names = array_flip($instances); - $this->assertEqual(count($field_names), 1, t('Only one taxonomy term field instance exists for story nodes')); - $this->assertEqual(key($field_names), 'taxonomyextra', t('Only the excess taxonomy term field is used on story nodes')); + $this->assertEqual(count($field_names), 1, 'Only one taxonomy term field instance exists for story nodes'); + $this->assertEqual(key($field_names), 'taxonomyextra', 'Only the excess taxonomy term field is used on story nodes'); // Check that the node type 'poll' has been associated to no taxonomy // reference field. $instances = $this->instanceVocabularies('node', 'poll'); - $this->assertTrue(empty($instances), t('Node type poll has no taxonomy term reference field instances.')); + $this->assertTrue(empty($instances), 'Node type poll has no taxonomy term reference field instances.'); // Check that each node of type 'page' and 'story' is associated to all the // terms except terms whose ID is equal to the node ID or is equal to the @@ -142,14 +153,14 @@ if (!$should_be_displayed) { // Look for any link with the term path. $links = $this->xpath('//a[@href=:term_path]', array(':term_path' => $term_path)); - $this->assertFalse($links, t('Term %name (@field) is not displayed on node %nid', $args)); + $this->assertFalse($links, format_string('Term %name (@field) is not displayed on node %nid', $args)); } else { // Look for a link with the term path inside the correct field. // We search for "SPACE + class + SPACE" to avoid matching a substring // of the class. $links = $this->xpath('//div[contains(concat(" ", normalize-space(@class), " "), :field_class)]//a[@href=:term_path]', array(':field_class' => ' ' . $field_class . ' ', ':term_path' => $term_path)); - $this->assertTrue($links, t('Term %name (@field) is displayed on node %nid', $args)); + $this->assertTrue($links, format_string('Term %name (@field) is displayed on node %nid', $args)); } } @@ -157,7 +168,7 @@ // ID 0. Make sure we ignored this instead of generating a bogus term. if ($node->nid == 1) { $link = l($term->name, 'taxonomy/term/0'); - $this->assertNoRaw($link, t('Bogus term (tid 0) is not displayed on node 1 vid %old_vid.', $args)); + $this->assertNoRaw($link, format_string('Bogus term (tid 0) is not displayed on node 1 vid %old_vid.', $args)); } // The first 12 nodes have two revisions. For nodes with @@ -165,7 +176,7 @@ // to terms whose ID is equal to the node ID or 49 less the node ID. $revisions = node_revision_list($node); if ($node->nid < 13) { - $this->assertEqual(count($revisions), 2, t('Node %nid has two revisions.', $args)); + $this->assertEqual(count($revisions), 2, format_string('Node %nid has two revisions.', $args)); $last_rev = end($revisions); $args['%old_vid'] = $last_rev->vid; @@ -178,13 +189,13 @@ $term = $terms[$node->nid]; $link = l($term->name, 'taxonomy/term/' . $term->tid); - $this->assertRaw($link, t('Term %name (@field) is displayed on node %nid vid %old_vid.', $args)); + $this->assertRaw($link, format_string('Term %name (@field) is displayed on node %nid vid %old_vid.', $args)); $term = $terms[49-$node->nid]; $link = l($term->name, 'taxonomy/term/' . $term->tid); - $this->assertRaw($link, t('Term %name (@field) is displayed on node %nid %old_vid.', $args)); + $this->assertRaw($link, format_string('Term %name (@field) is displayed on node %nid %old_vid.', $args)); } else { - $this->assertEqual(count($revisions), 1, t('Node %nid has one revision.', $args)); + $this->assertEqual(count($revisions), 1, format_string('Node %nid has one revision.', $args)); } } } diff -Naur drupal-7.0/modules/simpletest/tests/upgrade/upgrade.test drupal-7.66/modules/simpletest/tests/upgrade/upgrade.test --- drupal-7.0/modules/simpletest/tests/upgrade/upgrade.test 2010-12-28 22:46:23.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/upgrade/upgrade.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ zlibInstalled = function_exists('gzopen'); + } + + /** + * Prepares the appropriate session for the release of Drupal being upgraded. + */ + protected function prepareD7Session() { + // Generate and set a D6-compatible session cookie. + $this->curlInitialize(); + $sid = drupal_hash_base64(uniqid(mt_rand(), TRUE) . drupal_random_bytes(55)); + $session_name = update_get_d6_session_name(); + curl_setopt($this->curlHandle, CURLOPT_COOKIE, rawurlencode($session_name) . '=' . rawurlencode($sid)); + + // Force our way into the session of the child site. + drupal_save_session(TRUE); + // A session cannot be written without the ssid column which is missing on + // Drupal 6 sites. + db_add_field('sessions', 'ssid', array('description' => "Secure session ID. The value is generated by Drupal's session handlers.", 'type' => 'varchar', 'length' => 128, 'not null' => TRUE, 'default' => '')); + _drupal_session_write($sid, ''); + // Remove the temporarily added ssid column. + db_drop_field('sessions', 'ssid'); + drupal_save_session(FALSE); + } + + /** + * Overrides DrupalWebTestCase::setUp() for upgrade testing. + * + * @see DrupalWebTestCase::prepareDatabasePrefix() + * @see DrupalWebTestCase::changeDatabasePrefix() + * @see DrupalWebTestCase::prepareEnvironment() */ protected function setUp() { + // We are going to set a missing zlib requirement property for usage + // during the performUpgrade() and tearDown() methods. Also set that the + // tests failed. + if (!$this->zlibInstalled) { + parent::setUp(); + return; + } + global $user, $language, $conf; // Load the Update API. @@ -43,145 +97,64 @@ $this->loadedModules = module_list(); - // Generate a temporary prefixed database to ensure that tests have a clean starting point. - $this->databasePrefix = 'simpletest' . mt_rand(1000, 1000000); - db_update('simpletest_test_id') - ->fields(array('last_prefix' => $this->databasePrefix)) - ->condition('test_id', $this->testId) - ->execute(); + // Create the database prefix for this test. + $this->prepareDatabasePrefix(); - // Clone the current connection and replace the current prefix. - $connection_info = Database::getConnectionInfo('default'); - Database::renameConnection('default', 'simpletest_original_default'); - foreach ($connection_info as $target => $value) { - $connection_info[$target]['prefix'] = array( - 'default' => $value['prefix']['default'] . $this->databasePrefix, - ); - } - Database::addConnectionInfo('default', 'default', $connection_info['default']); - - // Store necessary current values before switching to prefixed database. - $this->originalLanguage = $language; - $this->originalLanguageDefault = variable_get('language_default'); - $this->originalFileDirectory = variable_get('file_public_path', conf_path() . '/files'); - $this->originalProfile = drupal_get_profile(); - $clean_url_original = variable_get('clean_url', 0); + // Prepare the environment for running tests. + $this->prepareEnvironment(); + if (!$this->setupEnvironment) { + return FALSE; + } + + // Reset all statics and variables to perform tests in a clean environment. + $conf = array(); + drupal_static_reset(); + + // Change the database prefix. + // All static variables need to be reset before the database prefix is + // changed, since DrupalCacheArray implementations attempt to + // write back to persistent caches when they are destructed. + $this->changeDatabasePrefix(); + if (!$this->setupDatabasePrefix) { + return FALSE; + } // Unregister the registry. // This is required to make sure that the database layer works properly. spl_autoload_unregister('drupal_autoload_class'); spl_autoload_unregister('drupal_autoload_interface'); - // Create test directories ahead of installation so fatal errors and debug - // information can be logged during installation process. - // Use mock files directories with the same prefix as the database. - $public_files_directory = $this->originalFileDirectory . '/simpletest/' . substr($this->databasePrefix, 10); - $private_files_directory = $public_files_directory . '/private'; - $temp_files_directory = $private_files_directory . '/temp'; - - // Create the directories. - file_prepare_directory($public_files_directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS); - file_prepare_directory($private_files_directory, FILE_CREATE_DIRECTORY); - file_prepare_directory($temp_files_directory, FILE_CREATE_DIRECTORY); - $this->generatedTestFiles = FALSE; - - // Log fatal errors. - ini_set('log_errors', 1); - ini_set('error_log', $public_files_directory . '/error.log'); - - // Reset all statics and variables to perform tests in a clean environment. - $conf = array(); - // Load the database from the portable PHP dump. + // The files may be gzipped. foreach ($this->databaseDumpFiles as $file) { + if (substr($file, -3) == '.gz') { + $file = "compress.zlib://$file"; + } require $file; } // Set path variables. - $this->variable_set('file_public_path', $public_files_directory); - $this->variable_set('file_private_path', $private_files_directory); - $this->variable_set('file_temporary_path', $temp_files_directory); + $this->variable_set('file_public_path', $this->public_files_directory); + $this->variable_set('file_private_path', $this->private_files_directory); + $this->variable_set('file_temporary_path', $this->temp_files_directory); $this->pass('Finished loading the dump.'); - // Load user 1. - $this->originalUser = $user; + // Ensure that the session is not written to the new environment and replace + // the global $user session with uid 1 from the new test site. drupal_save_session(FALSE); + // Login as uid 1. $user = db_query('SELECT * FROM {users} WHERE uid = :uid', array(':uid' => 1))->fetchObject(); // Generate and set a D6-compatible session cookie. - $this->curlInitialize(); - $sid = drupal_hash_base64(uniqid(mt_rand(), TRUE) . drupal_random_bytes(55)); - $session_name = update_get_d6_session_name(); - curl_setopt($this->curlHandle, CURLOPT_COOKIE, rawurlencode($session_name) . '=' . rawurlencode($sid)); - - // Force our way into the session of the child site. - drupal_save_session(TRUE); - // A session cannot be written without the ssid column which is missing on - // Drupal 6 sites. - db_add_field('sessions', 'ssid', array('description' => "Secure session ID. The value is generated by Drupal's session handlers.", 'type' => 'varchar', 'length' => 128, 'not null' => TRUE, 'default' => '')); - _drupal_session_write($sid, ''); - // Remove the temporarily added ssid column. - db_drop_field('sessions', 'ssid'); - drupal_save_session(FALSE); + $this->prepareD7Session(); // Restore necessary variables. - $this->variable_set('clean_url', $clean_url_original); + $this->variable_set('clean_url', $this->originalCleanUrl); $this->variable_set('site_mail', 'simpletest@example.com'); drupal_set_time_limit($this->timeLimit); - } - - /** - * Override of DrupalWebTestCase::tearDown() specialized for upgrade testing. - */ - protected function tearDown() { - global $user, $language; - - // In case a fatal error occured that was not in the test process read the - // log to pick up any fatal errors. - simpletest_log_read($this->testId, $this->databasePrefix, get_class($this), TRUE); - - // Delete temporary files directory. - file_unmanaged_delete_recursive($this->originalFileDirectory . '/simpletest/' . substr($this->databasePrefix, 10)); - - // Get back to the original connection. - Database::removeConnection('default'); - Database::renameConnection('simpletest_original_default', 'default'); - - // Remove all prefixed tables. - $tables = db_find_tables($this->databasePrefix . '%'); - foreach ($tables as $table) { - db_drop_table($table); - } - - // Return the user to the original one. - $user = $this->originalUser; - drupal_save_session(TRUE); - - // Ensure that internal logged in variable and cURL options are reset. - $this->loggedInUser = FALSE; - $this->additionalCurlOptions = array(); - - // Reload module list and implementations to ensure that test module hooks - // aren't called after tests. - module_list(TRUE); - module_implements('', FALSE, TRUE); - - // Reset the Field API. - field_cache_clear(); - - // Rebuild caches. - parent::refreshVariables(); - - // Reset language. - $language = $this->originalLanguage; - if ($this->originalLanguageDefault) { - $GLOBALS['conf']['language_default'] = $this->originalLanguageDefault; - } - - // Close the CURL handler. - $this->curlClose(); + $this->setup = TRUE; } /** @@ -232,6 +205,11 @@ * TRUE if the upgrade succeeded, FALSE otherwise. */ protected function performUpgrade($register_errors = TRUE) { + if (!$this->zlibInstalled) { + $this->fail(t('Missing zlib requirement for upgrade tests.')); + return FALSE; + } + $update_url = $GLOBALS['base_url'] . '/update.php'; // Load the first update screen. @@ -246,6 +224,14 @@ return FALSE; } + // The test should pass if there are no pending updates. + $content = $this->drupalGetContent(); + if (strpos($content, t('No pending updates.')) !== FALSE) { + $this->pass(t('No pending updates and therefore no upgrade process to test.')); + $this->pendingUpdates = FALSE; + return TRUE; + } + // Go! $this->drupalPost(NULL, array(), t('Apply pending updates')); if (!$this->assertResponse(200)) { @@ -270,7 +256,7 @@ // Check if there still are pending updates. $this->drupalGet($update_url, array('external' => TRUE)); $this->drupalPost(NULL, array(), t('Continue')); - if (!$this->assertText(t('No pending updates.'), t('No pending updates at the end of the update process.'))) { + if (!$this->assertText(t('No pending updates.'), 'No pending updates at the end of the update process.')) { return FALSE; } @@ -320,6 +306,26 @@ } /** + * Performs end-to-end point test of the release update path. + */ +abstract class UpdatePathTestCase extends UpgradePathTestCase { + /** + * Overrides UpgradePathTestCase::prepareD7Session(). + */ + protected function prepareD7Session() { + // Generate and set a D7-compatible session cookie. + $this->curlInitialize(); + $sid = drupal_hash_base64(uniqid(mt_rand(), TRUE) . drupal_random_bytes(55)); + curl_setopt($this->curlHandle, CURLOPT_COOKIE, rawurlencode(session_name()) . '=' . rawurlencode($sid)); + + // Force our way into the session of the child site. + drupal_save_session(TRUE); + _drupal_session_write($sid, ''); + drupal_save_session(FALSE); + } +} + +/** * Perform basic upgrade tests. * * Load a bare installation of Drupal 6 and run the upgrade process on it. @@ -352,14 +358,338 @@ // Destroy a table that the upgrade process needs. db_drop_table('access'); // Assert that the upgrade fails. - $this->assertFalse($this->performUpgrade(FALSE), t('A failed upgrade should return messages.')); + $this->assertFalse($this->performUpgrade(FALSE) && $this->pendingUpdates, 'A failed upgrade should return messages.'); } /** * Test a successful upgrade. */ public function testBasicUpgrade() { - $this->assertTrue($this->performUpgrade(), t('The upgrade was completed successfully.')); + $this->assertTrue($this->performUpgrade(), 'The upgrade was completed successfully.'); + + // Hit the frontpage. + $this->drupalGet(''); + $this->assertResponse(200); + + // Verify that we are still logged in. + $this->drupalGet('user'); + $this->clickLink(t('Edit')); + $this->assertEqual($this->getUrl(), url('user/1/edit', array('absolute' => TRUE)), 'We are still logged in as admin at the end of the upgrade.'); + + // Logout and verify that we can login back in with our initial password. + $this->drupalLogout(); + $this->drupalLogin((object) array( + 'uid' => 1, + 'name' => 'admin', + 'pass_raw' => 'admin', + )); + + // The previous login should've triggered a password rehash, so login one + // more time to make sure the new hash is readable. + $this->drupalLogout(); + $this->drupalLogin((object) array( + 'uid' => 1, + 'name' => 'admin', + 'pass_raw' => 'admin', + )); + + // Test that the site name is correctly displayed. + $this->assertText('Drupal 6', 'The site name is correctly displayed.'); + + // Verify that the main admin sections are available. + $this->drupalGet('admin'); + $this->assertText(t('Content')); + $this->assertText(t('Appearance')); + $this->assertText(t('People')); + $this->assertText(t('Configuration')); + $this->assertText(t('Reports')); + $this->assertText(t('Structure')); + $this->assertText(t('Modules')); + + // Confirm that no {menu_links} entry exists for user/autocomplete. + $result = db_query('SELECT COUNT(*) FROM {menu_links} WHERE link_path = :user_autocomplete', array(':user_autocomplete' => 'user/autocomplete'))->fetchField(); + $this->assertFalse($result, 'No {menu_links} entry exists for user/autocomplete'); + + // Test that the environment after the upgrade is in a consistent status. + $update_d6 = variable_get('update_d6', FALSE); + $this->assertFalse($update_d6, 'The D6 upgrade flag variable has been correctly disabled.'); + } +} + +/** + * Performs point release update tests on a bare database. + * + * Loads an installation of Drupal 7.0 and runs the update process on it. + * + * The install contains the standard profile (plus all optional) modules + * without any content so that an update from any of the modules under this + * profile installation can be wholly tested. + */ +class BasicStandardUpdatePath extends UpdatePathTestCase { + public static function getInfo() { + return array( + 'name' => 'Basic standard + all profile update path', + 'description' => 'Basic update path tests for a standard profile install with all enabled modules.', + 'group' => 'Upgrade path', + ); + } + + public function setUp() { + // Path to the database dump files. + $this->databaseDumpFiles = array( + drupal_get_path('module', 'simpletest') . '/tests/upgrade/drupal-7.bare.standard_all.database.php.gz', + ); + parent::setUp(); + } + + /** + * Tests a successful point release update. + */ + public function testBasicStandardUpdate() { + $this->assertTrue($this->performUpgrade(), 'The upgrade was completed successfully.'); + + // Hit the frontpage. + $this->drupalGet(''); + $this->assertResponse(200); + + // Verify that we are still logged in. + $this->drupalGet('user'); + $this->clickLink(t('Edit')); + $this->assertEqual($this->getUrl(), url('user/1/edit', array('absolute' => TRUE)), 'We are still logged in as admin at the end of the upgrade.'); + + // Logout and verify that we can login back in with our initial password. + $this->drupalLogout(); + $this->drupalLogin((object) array( + 'uid' => 1, + 'name' => 'admin', + 'pass_raw' => 'admin', + )); + + // The previous login should've triggered a password rehash, so login one + // more time to make sure the new hash is readable. + $this->drupalLogout(); + $this->drupalLogin((object) array( + 'uid' => 1, + 'name' => 'admin', + 'pass_raw' => 'admin', + )); + + // Test that the site name is correctly displayed. + $this->assertText('Drupal', 'The site name is correctly displayed.'); + + // Verify that the main admin sections are available. + $this->drupalGet('admin'); + $this->assertText(t('Content')); + $this->assertText(t('Appearance')); + $this->assertText(t('People')); + $this->assertText(t('Configuration')); + $this->assertText(t('Reports')); + $this->assertText(t('Structure')); + $this->assertText(t('Modules')); + + // Confirm that no {menu_links} entry exists for user/autocomplete. + $result = db_query('SELECT COUNT(*) FROM {menu_links} WHERE link_path = :user_autocomplete', array(':user_autocomplete' => 'user/autocomplete'))->fetchField(); + $this->assertFalse($result, 'No {menu_links} entry exists for user/autocomplete'); + } +} + +/** + * Performs point release update tests on a bare database. + * + * Loads an installation of Drupal 7.0 and runs the update process on it. + * + * The install contains the minimal profile modules (without any generated + * content) so that an update from of a site under this profile may be tested. + */ +class BasicMinimalUpdatePath extends UpdatePathTestCase { + public static function getInfo() { + return array( + 'name' => 'Basic minimal profile update path', + 'description' => 'Basic update path tests for a minimal profile install.', + 'group' => 'Upgrade path', + ); + } + + public function setUp() { + // Path to the database dump files. + $this->databaseDumpFiles = array( + drupal_get_path('module', 'simpletest') . '/tests/upgrade/drupal-7.bare.minimal.database.php.gz', + ); + parent::setUp(); + } + + /** + * Tests a successful point release update. + */ + public function testBasicMinimalUpdate() { + $this->assertTrue($this->performUpgrade(), 'The upgrade was completed successfully.'); + + // Hit the frontpage. + $this->drupalGet(''); + $this->assertResponse(200); + + // Verify that we are still logged in. + $this->drupalGet('user'); + $this->clickLink(t('Edit')); + $this->assertEqual($this->getUrl(), url('user/1/edit', array('absolute' => TRUE)), 'We are still logged in as admin at the end of the upgrade.'); + + // Logout and verify that we can login back in with our initial password. + $this->drupalLogout(); + $this->drupalLogin((object) array( + 'uid' => 1, + 'name' => 'admin', + 'pass_raw' => 'admin', + )); + + // The previous login should've triggered a password rehash, so login one + // more time to make sure the new hash is readable. + $this->drupalLogout(); + $this->drupalLogin((object) array( + 'uid' => 1, + 'name' => 'admin', + 'pass_raw' => 'admin', + )); + + // Test that the site name is correctly displayed. + $this->assertText('Drupal', 'The site name is correctly displayed.'); + + // Verify that the main admin sections are available. + $this->drupalGet('admin'); + $this->assertText(t('Content')); + $this->assertText(t('Appearance')); + $this->assertText(t('People')); + $this->assertText(t('Configuration')); + $this->assertText(t('Reports')); + $this->assertText(t('Structure')); + $this->assertText(t('Modules')); + + // Confirm that no {menu_links} entry exists for user/autocomplete. + $result = db_query('SELECT COUNT(*) FROM {menu_links} WHERE link_path = :user_autocomplete', array(':user_autocomplete' => 'user/autocomplete'))->fetchField(); + $this->assertFalse($result, 'No {menu_links} entry exists for user/autocomplete'); + + // Confirm that a date format that just differs in the case can be added. + $admin_date_format = 'j M y'; + $edit = array('date_format' => $admin_date_format); + $this->drupalPost('admin/config/regional/date-time/formats/add', $edit, t('Add format')); + + // Add a new date format which just differs in the case. + $admin_date_format_uppercase = 'j M Y'; + $edit = array('date_format' => $admin_date_format_uppercase); + $this->drupalPost('admin/config/regional/date-time/formats/add', $edit, t('Add format')); + $this->assertText(t('Custom date format added.')); + + // Verify that the unique key on {date_formats}.format still exists. + $this->assertTrue(db_index_exists('date_formats', 'formats'), 'Unique key on {date_formats} exists'); + } +} + +/** + * Performs point release update tests on a 'filled' database. + * + * Loads an installation of Drupal 7.0 and runs the update process on it. + * + * The install contains the standard profile (plus all optional) modules + * with generated content so that an update from any of the modules under this + * profile installation can be wholly tested. + */ +class FilledStandardUpdatePath extends UpdatePathTestCase { + public static function getInfo() { + return array( + 'name' => 'Basic standard + all profile update path, populated database', + 'description' => 'Basic update path tests for a standard profile install with all enabled modules and a populated database.', + 'group' => 'Upgrade path', + ); + } + + public function setUp() { + // Path to the database dump files. + $this->databaseDumpFiles = array( + drupal_get_path('module', 'simpletest') . '/tests/upgrade/drupal-7.filled.standard_all.database.php.gz', + ); + parent::setUp(); + } + + /** + * Tests a successful point release update. + */ + public function testFilledStandardUpdate() { + $this->assertTrue($this->performUpgrade(), 'The upgrade was completed successfully.'); + + // Hit the frontpage. + $this->drupalGet(''); + $this->assertResponse(200); + + // Verify that we are still logged in. + $this->drupalGet('user'); + $this->clickLink(t('Edit')); + $this->assertEqual($this->getUrl(), url('user/1/edit', array('absolute' => TRUE)), 'We are still logged in as admin at the end of the upgrade.'); + + // Logout and verify that we can login back in with our initial password. + $this->drupalLogout(); + $this->drupalLogin((object) array( + 'uid' => 1, + 'name' => 'admin', + 'pass_raw' => 'admin', + )); + + // The previous login should've triggered a password rehash, so login one + // more time to make sure the new hash is readable. + $this->drupalLogout(); + $this->drupalLogin((object) array( + 'uid' => 1, + 'name' => 'admin', + 'pass_raw' => 'admin', + )); + + // Test that the site name is correctly displayed. + $this->assertText('Drupal', 'The site name is correctly displayed.'); + + // Verify that the main admin sections are available. + $this->drupalGet('admin'); + $this->assertText(t('Content')); + $this->assertText(t('Appearance')); + $this->assertText(t('People')); + $this->assertText(t('Configuration')); + $this->assertText(t('Reports')); + $this->assertText(t('Structure')); + $this->assertText(t('Modules')); + + // Confirm that no {menu_links} entry exists for user/autocomplete. + $result = db_query('SELECT COUNT(*) FROM {menu_links} WHERE link_path = :user_autocomplete', array(':user_autocomplete' => 'user/autocomplete'))->fetchField(); + $this->assertFalse($result, 'No {menu_links} entry exists for user/autocomplete'); + } +} + +/** + * Performs point release update tests on a populated database. + * + * Loads an installation of Drupal 7.0 and runs the update process on it. + * + * The install contains the minimal profile modules (along with generated + * content) so that an update from of a site under this profile may be tested. + */ +class FilledMinimalUpdatePath extends UpdatePathTestCase { + public static function getInfo() { + return array( + 'name' => 'Basic minimal profile update path, populated database', + 'description' => 'Basic update path tests for a minimal profile install with a populated database.', + 'group' => 'Upgrade path', + ); + } + + public function setUp() { + // Path to the database dump files. + $this->databaseDumpFiles = array( + drupal_get_path('module', 'simpletest') . '/tests/upgrade/drupal-7.filled.minimal.database.php.gz', + ); + parent::setUp(); + } + + /** + * Tests a successful point release update. + */ + public function testFilledStandardUpdate() { + $this->assertTrue($this->performUpgrade(), 'The upgrade was completed successfully.'); // Hit the frontpage. $this->drupalGet(''); @@ -368,7 +698,7 @@ // Verify that we are still logged in. $this->drupalGet('user'); $this->clickLink(t('Edit')); - $this->assertEqual($this->getUrl(), url('user/1/edit', array('absolute' => TRUE)), t('We are still logged in as admin at the end of the upgrade.')); + $this->assertEqual($this->getUrl(), url('user/1/edit', array('absolute' => TRUE)), 'We are still logged in as admin at the end of the upgrade.'); // Logout and verify that we can login back in with our initial password. $this->drupalLogout(); @@ -388,7 +718,7 @@ )); // Test that the site name is correctly displayed. - $this->assertText('Drupal 6', t('The site name is correctly displayed.')); + $this->assertText('Drupal', 'The site name is correctly displayed.'); // Verify that the main admin sections are available. $this->drupalGet('admin'); @@ -402,6 +732,6 @@ // Confirm that no {menu_links} entry exists for user/autocomplete. $result = db_query('SELECT COUNT(*) FROM {menu_links} WHERE link_path = :user_autocomplete', array(':user_autocomplete' => 'user/autocomplete'))->fetchField(); - $this->assertFalse($result, t('No {menu_links} entry exists for user/autocomplete')); + $this->assertFalse($result, 'No {menu_links} entry exists for user/autocomplete'); } } diff -Naur drupal-7.0/modules/simpletest/tests/upgrade/upgrade.translatable.test drupal-7.66/modules/simpletest/tests/upgrade/upgrade.translatable.test --- drupal-7.0/modules/simpletest/tests/upgrade/upgrade.translatable.test 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/upgrade/upgrade.translatable.test 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,51 @@ + 'Translatable content upgrade path', + 'description' => 'Upgrade path tests for the translatable content types of Node module.', + 'group' => 'Upgrade path', + ); + } + + public function setUp() { + // Path to the database dump files. + $this->databaseDumpFiles = array( + drupal_get_path('module', 'simpletest') . '/tests/upgrade/drupal-6.filled.database.php', + drupal_get_path('module', 'simpletest') . '/tests/upgrade/drupal-6.locale.database.php', + drupal_get_path('module', 'simpletest') . '/tests/upgrade/drupal-6.translatable.database.php', + ); + parent::setUp(); + + $this->uninstallModulesExcept(array('locale')); + } + + /** + * Test a successful upgrade (no negotiation). + */ + public function testTranslatableUpgrade() { + $this->assertTrue($this->performUpgrade(), 'The upgrade was completed successfully.'); + + // The D6 database contains the english node "First translatable page" with + // nid 53. + $nid = 53; + $title = 'First translatable page'; + $teaser = 'Teaser of the first translatable page.'; + $body = 'Body of the first translatable page.'; + + // Check whether the node displays properly. + $this->drupalGet("node/$nid"); + $this->assertText($body, 'Translatable node body displays properly'); + + // Retrieve node object, ensure that both the body and the teaser has + // survived upgrade properly. + $node = $this->drupalGetNodeByTitle($title); + $this->assertTrue($node != NULL, format_string('Node @title was loaded', array('@title' => $title))); + $this->assertEqual($node->body[LANGUAGE_NONE][0]['value'], $body, 'Body of the node survived upgrade properly'); + $this->assertEqual($node->body[LANGUAGE_NONE][0]['summary'], $teaser, 'Teaser of the node survived upgrade properly'); + } +} diff -Naur drupal-7.0/modules/simpletest/tests/upgrade/upgrade.trigger.test drupal-7.66/modules/simpletest/tests/upgrade/upgrade.trigger.test --- drupal-7.0/modules/simpletest/tests/upgrade/upgrade.trigger.test 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/upgrade/upgrade.trigger.test 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,39 @@ + 7 upgrade path. + */ +class UpgradePathTriggerTestCase extends UpgradePathTestCase { + public static function getInfo() { + return array( + 'name' => 'Trigger upgrade path', + 'description' => 'Trigger upgrade path tests for Drupal 6.x.', + 'group' => 'Upgrade path', + ); + } + + public function setUp() { + // Path to the database dump. + $this->databaseDumpFiles = array( + drupal_get_path('module', 'simpletest') . '/tests/upgrade/drupal-6.filled.database.php', + drupal_get_path('module', 'simpletest') . '/tests/upgrade/drupal-6.trigger.database.php', + ); + parent::setUp(); + } + + /** + * Basic tests for the trigger upgrade. + */ + public function testTaxonomyUpgrade() { + $this->assertTrue($this->performUpgrade(), 'The upgrade was completed successfully.'); + $this->drupalGet('admin/structure/trigger/node'); + $this->assertRaw('
    '); + $this->assertRaw(''); + $this->drupalGet('admin/structure/trigger/comment'); + $this->assertRaw(''); + } +} diff -Naur drupal-7.0/modules/simpletest/tests/upgrade/upgrade.upload.test drupal-7.66/modules/simpletest/tests/upgrade/upgrade.upload.test --- drupal-7.0/modules/simpletest/tests/upgrade/upgrade.upload.test 2010-11-21 21:35:10.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/upgrade/upgrade.upload.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ assertTrue($this->performUpgrade(), t('The upgrade was completed successfully.')); + $this->assertTrue($this->performUpgrade(), 'The upgrade was completed successfully.'); $query = new EntityFieldQuery(); $query->entityCondition('entity_type', 'node'); $query->entityCondition('bundle', 'page'); @@ -65,6 +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.0/modules/simpletest/tests/upgrade/upgrade.user.test drupal-7.66/modules/simpletest/tests/upgrade/upgrade.user.test --- drupal-7.0/modules/simpletest/tests/upgrade/upgrade.user.test 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/upgrade/upgrade.user.test 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,92 @@ + 'User upgrade path (password token involved)', + 'description' => 'User upgrade path tests (password token involved).', + 'group' => 'Upgrade path', + ); + } + + public function setUp() { + // Path to the database dump files. + $this->databaseDumpFiles = array( + drupal_get_path('module', 'simpletest') . '/tests/upgrade/drupal-6.bare.database.php', + drupal_get_path('module', 'simpletest') . '/tests/upgrade/drupal-6.user-password-token.database.php', + ); + parent::setUp(); + } + + /** + * Test a successful upgrade. + */ + public function testUserUpgrade() { + $this->assertTrue($this->performUpgrade(), 'The upgrade was completed successfully.'); + $this->assertEqual(variable_get('user_mail_register_no_approval_required_body'), ', [user:name], [site:name], [site:url], [site:url-brief], [user:mail], [date:medium], [site:login-url], [user:edit-url], [user:one-time-login-url].', 'Existing email templates have been modified (password token involved).'); + // Check that a non-md5 hash was untouched. + $pass = db_query('SELECT pass FROM {users} WHERE uid = 3')->fetchField(); + $this->assertEqual('$S$DAK00p3Dkojkf4O/UizYxenguXnjv', $pass, 'Pre-existing non-MD5 password hash was not altered'); + } +} + +/** + * Upgrade test for user.module (password token not involved). + */ +class UserUpgradePathNoPasswordTokenTestCase extends UpgradePathTestCase { + public static function getInfo() { + return array( + 'name' => 'User upgrade path (password token not involved)', + 'description' => 'User upgrade path tests (password token not involved).', + 'group' => 'Upgrade path', + ); + } + + public function setUp() { + // Path to the database dump files. + $this->databaseDumpFiles = array( + drupal_get_path('module', 'simpletest') . '/tests/upgrade/drupal-6.bare.database.php', + drupal_get_path('module', 'simpletest') . '/tests/upgrade/drupal-6.user-no-password-token.database.php', + ); + parent::setUp(); + } + + /** + * Test a successful upgrade. + */ + public function testUserUpgrade() { + $this->assertTrue($this->performUpgrade(), 'The upgrade was completed successfully.'); + $this->assertEqual(variable_get('user_mail_register_no_approval_required_body'), '[user:name], [site:name], [site:url], [site:url-brief], [user:mail], [date:medium], [site:login-url], [user:edit-url], [user:one-time-login-url].', 'Existing email templates have been modified (password token not involved).'); + } +} + +/** + * Upgrade test for user.module (duplicated permission). + */ +class UserUpgradePathDuplicatedPermissionTestCase extends UpgradePathTestCase { + public static function getInfo() { + return array( + 'name' => 'User upgrade path (duplicated permission)', + 'description' => 'User upgrade path tests (duplicated permission).', + 'group' => 'Upgrade path', + ); + } + + public function setUp() { + // Path to the database dump files. + $this->databaseDumpFiles = array( + drupal_get_path('module', 'simpletest') . '/tests/upgrade/drupal-6.bare.database.php', + drupal_get_path('module', 'simpletest') . '/tests/upgrade/drupal-6.duplicate-permission.database.php', + ); + parent::setUp(); + } + + /** + * Test a successful upgrade. + */ + public function testUserUpgrade() { + $this->assertTrue($this->performUpgrade(), 'The upgrade was completed successfully.'); + } +} diff -Naur drupal-7.0/modules/simpletest/tests/url_alter_test.info drupal-7.66/modules/simpletest/tests/url_alter_test.info --- drupal-7.0/modules/simpletest/tests/url_alter_test.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/url_alter_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: url_alter_test.info,v 1.2 2010/12/20 19:59:43 webchick Exp $ name = Url_alter tests description = A support modules for url_alter hook testing. core = 7.x @@ -6,8 +5,7 @@ version = VERSION hidden = TRUE -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/simpletest/tests/url_alter_test.install drupal-7.66/modules/simpletest/tests/url_alter_test.install --- drupal-7.0/modules/simpletest/tests/url_alter_test.install 2009-10-24 07:13:44.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/url_alter_test.install 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ TRUE)) . 'xmlrpc.php'; + $signature = xmlrpc($url, array('system.methodSignature' => array('system.listMethods'))); + $this->assert(is_array($signature) && !empty($signature) && is_array($signature[0]), + 'system.methodSignature returns an array of signature arrays.'); + } + + /** * Ensure that XML-RPC correctly handles invalid messages when parsing. */ protected function testInvalidMessageParsing() { $invalid_messages = array( array( 'message' => xmlrpc_message(''), - 'assertion' => t('Empty message correctly rejected during parsing.'), + 'assertion' => 'Empty message correctly rejected during parsing.', ), array( 'message' => xmlrpc_message(''), - 'assertion' => t('Empty message with XML declaration correctly rejected during parsing.'), + 'assertion' => 'Empty message with XML declaration correctly rejected during parsing.', ), array( 'message' => xmlrpc_message('value'), - 'assertion' => t('Non-empty message without a valid message type is rejected during parsing.'), + 'assertion' => 'Non-empty message without a valid message type is rejected during parsing.', ), array( 'message' => xmlrpc_message('value'), - 'assertion' => t('Non-empty malformed message is rejected during parsing.'), + 'assertion' => 'Non-empty malformed message is rejected during parsing.', ), ); @@ -202,13 +211,18 @@ * 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) { $xml_message_l = xmlrpc_test_message_sized_in_kb($size); $xml_message_r = xmlrpc($xml_url, array('messages.messageSizedInKB' => array($size))); - $this->assertEqual($xml_message_l, $xml_message_r, t('XML-RPC messages.messageSizedInKB of %s Kb size received', array('%s' => $size))); + $this->assertEqual($xml_message_l, $xml_message_r, format_string('XML-RPC messages.messageSizedInKB of %s Kb size received', array('%s' => $size))); } } @@ -227,9 +241,43 @@ $methods2 = xmlrpc($url, array('system.listMethods' => array())); $diff = array_diff($methods1, $methods2); - $this->assertTrue(is_array($diff) && !empty($diff), t('Method list is altered by hook_xmlrpc_alter')); + $this->assertTrue(is_array($diff) && !empty($diff), 'Method list is altered by hook_xmlrpc_alter'); $removed = reset($diff); - $this->assertEqual($removed, 'system.methodSignature', t('Hiding builting system.methodSignature with hook_xmlrpc_alter works')); + $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.0/modules/simpletest/tests/xmlrpc_test.info drupal-7.66/modules/simpletest/tests/xmlrpc_test.info --- drupal-7.0/modules/simpletest/tests/xmlrpc_test.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/simpletest/tests/xmlrpc_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: xmlrpc_test.info,v 1.2 2010/12/20 19:59:43 webchick Exp $ name = "XML-RPC Test" description = "Support module for XML-RPC tests according to the validator1 specification." package = Testing @@ -6,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/simpletest/tests/xmlrpc_test.module drupal-7.66/modules/simpletest/tests/xmlrpc_test.module --- drupal-7.0/modules/simpletest/tests/xmlrpc_test.module 2010-10-02 03:22:41.000000000 +0200 +++ drupal-7.66/modules/simpletest/tests/xmlrpc_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ 'slave'))->extend('PagerDefault')->extend('TableSort'); $query->addExpression('COUNT(path)', 'hits'); - // MAX(title) avoids having empty node titles which otherwise causes duplicates in the top pages list + // MAX(title) avoids having empty node titles which otherwise causes + // duplicates in the top pages list. $query->addExpression('MAX(title)', 'title'); $query->addExpression('AVG(timer)', 'average_time'); $query->addExpression('SUM(timer)', 'total_time'); @@ -91,7 +105,14 @@ } /** - * Menu callback; presents the "top visitors" page. + * Page callback: Displays the "top visitors" page. + * + * This displays the pages with the top number of visitors in a given time + * interval that haven't been flushed yet. The flush interval is set on the + * statistics settings form, but is dependent on cron running. + * + * @return + * A render array containing the top visitors information. */ function statistics_top_visitors() { @@ -116,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')); @@ -144,7 +166,14 @@ } /** - * Menu callback; presents the "referrer" page. + * Page callback: Displays the "top referrers" in the access logs. + * + * This displays the pages with the top referrers in a given time interval that + * haven't been flushed yet. The flush interval is set on the statistics + * settings form, but is dependent on cron running. + * + * @return + * A render array containing the top referrers information. */ function statistics_top_referrers() { drupal_set_title(t('Top referrers in the past %interval', array('%interval' => format_interval(variable_get('statistics_flush_accesslog_timer', 259200)))), PASS_THROUGH); @@ -190,7 +219,14 @@ } /** - * Menu callback; Displays recent page accesses. + * Page callback: Gathers page access statistics suitable for rendering. + * + * @param $aid + * The unique accesslog ID. + * + * @return + * A render array containing page access statistics. If information for the + * page was not found, drupal_not_found() is called. */ function statistics_access_log($aid) { $access = db_query('SELECT a.*, u.name FROM {accesslog} a LEFT JOIN {users} u ON a.uid = u.uid WHERE aid = :aid', array(':aid' => $aid))->fetch(); @@ -228,13 +264,11 @@ ); return $build; } - else { - drupal_not_found(); - } + return MENU_NOT_FOUND; } /** - * Form builder; Configure access logging. + * Form constructor for the statistics administration form. * * @ingroup forms * @see system_settings_form() @@ -270,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.0/modules/statistics/statistics.info drupal-7.66/modules/statistics/statistics.info --- drupal-7.0/modules/statistics/statistics.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/statistics/statistics.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: statistics.info,v 1.12 2010/12/20 19:59:43 webchick Exp $ name = Statistics description = Logs access statistics for your site. package = Core @@ -7,8 +6,7 @@ files[] = statistics.test configure = admin/config/system/statistics -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/statistics/statistics.install drupal-7.66/modules/statistics/statistics.install --- drupal-7.0/modules/statistics/statistics.install 2011-01-02 18:26:39.000000000 +0100 +++ drupal-7.66/modules/statistics/statistics.install 2019-04-17 22:20:46.000000000 +0200 @@ -1,9 +1,8 @@ ' . t('About') . ''; - $output .= '

    ' . t('The Statistics module shows you how often a given page is viewed, who viewed it, the previous page the user visited (referrer URL), and when it was viewed. These statistics are useful in determining how users are visiting and navigating your site. For more information, see the online handbook entry for the Statistics module.', array('@statistics' => url('http://drupal.org/handbook/modules/statistics/'))) . '

    '; + $output .= '

    ' . t('The Statistics module shows you how often a given page is viewed, who viewed it, the previous page the user visited (referrer URL), and when it was viewed. These statistics are useful in determining how users are visiting and navigating your site. For more information, see the online handbook entry for the Statistics module.', array('@statistics' => url('http://drupal.org/documentation/modules/statistics/'))) . '

    '; $output .= '

    ' . t('Uses') . '

    '; $output .= '
    '; $output .= '
    ' . t('Managing logs') . '
    '; @@ -46,7 +45,7 @@ /** * Implements hook_exit(). * - * This is where statistics are gathered on page accesses. + * Gathers statistics for page accesses. */ function statistics_exit() { global $user; @@ -58,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. @@ -76,12 +75,15 @@ } if (variable_get('statistics_enable_access_log', 0)) { drupal_bootstrap(DRUPAL_BOOTSTRAP_SESSION); + + // For anonymous users unicode.inc will not have been loaded. + include_once DRUPAL_ROOT . '/includes/unicode.inc'; // Log this page access. db_insert('accesslog') ->fields(array( - 'title' => strip_tags(drupal_get_title()), - 'path' => $_GET['q'], - 'url' => $_SERVER['HTTP_REFERER'], + 'title' => truncate_utf8(strip_tags(drupal_get_title()), 255), + 'path' => truncate_utf8($_GET['q'], 255), + 'url' => isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '', 'hostname' => ip_address(), 'uid' => $user->uid, 'sid' => session_id(), @@ -113,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); @@ -228,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. @@ -247,20 +264,20 @@ } /** - * Returns all time or today top or last viewed node(s). + * Returns the most viewed content of all time, today, or the last-viewed node. * * @param $dbfield - * one of - * - 'totalcount': top viewed content of all time. - * - 'daycount': top viewed content for today. - * - 'timestamp': last viewed node. - * + * The database field to use, one of: + * - 'totalcount': Integer that shows the top viewed content of all time. + * - 'daycount': Integer that shows the top viewed content for today. + * - 'timestamp': Integer that shows only the last viewed node. * @param $dbrows - * number of rows to be returned. + * The number of rows to be returned. * - * @return - * A query result containing n.nid, n.title, u.uid, u.name of the selected node(s) - * or FALSE if the query could not be executed correctly. + * @return SelectQuery|FALSE + * A query result containing the node ID, title, user ID that owns the node, + * and the username for the selected node(s), or FALSE if the query could not + * be executed correctly. */ function statistics_title_list($dbfield, $dbrows) { if (in_array($dbfield, array('totalcount', 'daycount', 'timestamp'))) { @@ -286,14 +303,15 @@ * Retrieves a node's "view statistics". * * @param $nid - * node ID + * The node ID. * * @return - * An array with three entries: [0]=totalcount, [1]=daycount, [2]=timestamp - * - totalcount: count of the total number of times that node has been viewed. - * - daycount: count of the total number of times that node has been viewed "today". - * For the daycount to be reset, cron must be enabled. - * - timestamp: timestamp of when that node was last viewed. + * An associative array containing: + * - totalcount: Integer for the total number of times the node has been + * viewed. + * - daycount: Integer for the total number of times the node has been viewed + * "today". For the daycount to be reset, cron must be enabled. + * - timestamp: Integer for the timestamp of when the node was last viewed. */ function statistics_get($nid) { @@ -372,8 +390,15 @@ } /** - * It is possible to adjust the width of columns generated by the - * statistics module. + * Generates a link to a path, truncating the displayed text to a given width. + * + * @param $path + * The path to generate the link for. + * @param $width + * The width to set the displayed text of the path. + * + * @return + * A string as a link, truncated to the width, linked to the given $path. */ function _statistics_link($path, $width = 35) { $title = drupal_get_path_alias($path); @@ -381,6 +406,17 @@ return l($title, $path); } +/** + * Formats an item for display, including both the item title and the link. + * + * @param $title + * The text to link to a path; will be truncated to a maximum width of 35. + * @param $path + * The path to link to; will default to '/'. + * + * @return + * An HTML string with $title linked to the $path. + */ function _statistics_format_item($title, $path) { $path = ($path ? $path : '/'); $output = ($title ? "$title
    " : ''); diff -Naur drupal-7.0/modules/statistics/statistics.pages.inc drupal-7.66/modules/statistics/statistics.pages.inc --- drupal-7.0/modules/statistics/statistics.pages.inc 2010-01-09 22:54:01.000000000 +0100 +++ drupal-7.66/modules/statistics/statistics.pages.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,11 +1,17 @@ 'pager'); return $build; } - else { - drupal_not_found(); - } + return MENU_NOT_FOUND; } +/** + * Page callback: Displays statistics for a user. + * + * @return array + * A render array containing user statistics. If information for the user was + * not found, this will deliver a page not found error via drupal_not_found(). + */ function statistics_user_tracker() { if ($account = user_load(arg(1))) { @@ -86,7 +97,5 @@ $build['statistics_pager'] = array('#theme' => 'pager'); return $build; } - else { - drupal_not_found(); - } + return MENU_NOT_FOUND; } diff -Naur drupal-7.0/modules/statistics/statistics.php drupal-7.66/modules/statistics/statistics.php --- drupal-7.0/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.0/modules/statistics/statistics.test drupal-7.66/modules/statistics/statistics.test --- drupal-7.0/modules/statistics/statistics.test 2010-12-08 07:43:07.000000000 +0100 +++ drupal-7.66/modules/statistics/statistics.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,8 +1,12 @@ 'test', 'path' => 'node/1', 'url' => 'http://example.com', - 'hostname' => '192.168.1.1', + 'hostname' => '1.2.3.3', 'uid' => 0, 'sid' => 10, 'timer' => 10, @@ -42,10 +46,10 @@ } /** - * Tests that logging via statistics_exit() works for cached and uncached pages. + * Tests that logging via statistics_exit() works for all pages. * - * Subclass DrupalWebTestCase rather than StatisticsTestCase, because we want - * to test requests from an anonymous user. + * We subclass DrupalWebTestCase rather than StatisticsTestCase, because we + * want to test requests from an anonymous user. */ class StatisticsLoggingTestCase extends DrupalWebTestCase { public static function getInfo() { @@ -59,9 +63,10 @@ function setUp() { parent::setUp('statistics'); + $this->auth_user = $this->drupalCreateUser(array('access content', 'create page content', 'edit own page content')); + // Ensure we have a node page to access. - $this->node = $this->drupalCreateNode(); - $this->auth_user = $this->drupalCreateUser(); + $this->node = $this->drupalCreateNode(array('title' => $this->randomName(255), 'uid' => $this->auth_user->uid)); // Enable page caching. variable_set('cache', TRUE); @@ -87,18 +92,18 @@ // Verify logging of an uncached page. $this->drupalGet($path); - $this->assertIdentical($this->drupalGetHeader('X-Drupal-Cache'), 'MISS', t('Testing an uncached page.')); + $this->assertIdentical($this->drupalGetHeader('X-Drupal-Cache'), 'MISS', 'Testing an uncached page.'); $log = db_query('SELECT * FROM {accesslog}')->fetchAll(PDO::FETCH_ASSOC); - $this->assertTrue(is_array($log) && count($log) == 1, t('Page request was logged.')); + $this->assertTrue(is_array($log) && count($log) == 1, 'Page request was logged.'); $this->assertEqual(array_intersect_key($log[0], $expected), $expected); $node_counter = statistics_get($this->node->nid); $this->assertIdentical($node_counter['totalcount'], '1'); // Verify logging of a cached page. $this->drupalGet($path); - $this->assertIdentical($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', t('Testing a cached page.')); + $this->assertIdentical($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', 'Testing a cached page.'); $log = db_query('SELECT * FROM {accesslog}')->fetchAll(PDO::FETCH_ASSOC); - $this->assertTrue(is_array($log) && count($log) == 2, t('Page request was logged.')); + $this->assertTrue(is_array($log) && count($log) == 2, 'Page request was logged.'); $this->assertEqual(array_intersect_key($log[1], $expected), $expected); $node_counter = statistics_get($this->node->nid); $this->assertIdentical($node_counter['totalcount'], '2'); @@ -108,10 +113,51 @@ $this->drupalGet($path); $log = db_query('SELECT * FROM {accesslog}')->fetchAll(PDO::FETCH_ASSOC); // Check the 6th item since login and account pages are also logged - $this->assertTrue(is_array($log) && count($log) == 6, t('Page request was logged.')); + $this->assertTrue(is_array($log) && count($log) == 6, 'Page request was logged.'); $this->assertEqual(array_intersect_key($log[5], $expected), $expected); $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( + 'title' => truncate_utf8(t('Edit Basic page') . ' ' . $this->node->title, 255), + 'path' => $path, + ); + $this->drupalGet($path); + $log = db_query('SELECT * FROM {accesslog}')->fetchAll(PDO::FETCH_ASSOC); + $this->assertTrue(is_array($log) && count($log) == 7, 'Page request was logged.'); + $this->assertEqual(array_intersect_key($log[6], $expected), $expected); + + // Create a path longer than 255 characters. Drupal's .htaccess file + // instructs Apache to test paths against the file system before routing to + // index.php. Many file systems restrict file names to 255 characters + // (http://en.wikipedia.org/wiki/Comparison_of_file_systems#Limits), and + // Apache returns a 403 when testing longer file names, but the total path + // length is not restricted. + $long_path = $this->randomName(127) . '/' . $this->randomName(128); + + // Test that the long path is properly truncated when logged. + $this->drupalGet($long_path); + $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)); } } @@ -132,9 +178,9 @@ */ function testRecentHits() { $this->drupalGet('admin/reports/hits'); - $this->assertText('test', t('Hit title found.')); - $this->assertText('node/1', t('Hit URL found.')); - $this->assertText('Anonymous', t('Hit user found.')); + $this->assertText('test', 'Hit title found.'); + $this->assertText('node/1', 'Hit URL found.'); + $this->assertText('Anonymous', 'Hit user found.'); } /** @@ -142,8 +188,8 @@ */ function testTopPages() { $this->drupalGet('admin/reports/pages'); - $this->assertText('test', t('Hit title found.')); - $this->assertText('node/1', t('Hit URL found.')); + $this->assertText('test', 'Hit title found.'); + $this->assertText('node/1', 'Hit URL found.'); } /** @@ -151,7 +197,7 @@ */ function testTopReferrers() { $this->drupalGet('admin/reports/referrers'); - $this->assertText('http://example.com', t('Hit referrer found.')); + $this->assertText('http://example.com', 'Hit referrer found.'); } /** @@ -159,9 +205,9 @@ */ function testDetails() { $this->drupalGet('admin/reports/access/1'); - $this->assertText('test', t('Hit title found.')); - $this->assertText('node/1', t('Hit URL found.')); - $this->assertText('Anonymous', t('Hit user found.')); + $this->assertText('test', 'Hit title found.'); + $this->assertText('node/1', 'Hit URL found.'); + $this->assertText('Anonymous', 'Hit user found.'); } /** @@ -170,8 +216,8 @@ function testAccessLogging() { $this->drupalGet('admin/reports/referrers'); $this->drupalGet('admin/reports/hits'); - $this->assertText('Top referrers in the past 3 days', t('Hit title found.')); - $this->assertText('admin/reports/referrers', t('Hit URL found.')); + $this->assertText('Top referrers in the past 3 days', 'Hit title found.'); + $this->assertText('admin/reports/referrers', 'Hit URL found.'); } /** @@ -196,12 +242,12 @@ // Get some page and check if the block is displayed. $this->drupalGet('user'); - $this->assertText('Popular content', t('Found the popular content block.')); - $this->assertText("Today's", t('Found today\'s popular content.')); - $this->assertText('All time', t('Found the alll time popular content.')); - $this->assertText('Last viewed', t('Found the last viewed popular content.')); + $this->assertText('Popular content', 'Found the popular content block.'); + $this->assertText("Today's", 'Found today\'s popular content.'); + $this->assertText('All time', 'Found the alll time popular content.'); + $this->assertText('Last viewed', 'Found the last viewed popular content.'); - $this->assertRaw(l($node->title, 'node/' . $node->nid), t('Found link to visited node.')); + $this->assertRaw(l($node->title, 'node/' . $node->nid), 'Found link to visited node.'); } } @@ -222,44 +268,58 @@ */ 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. $this->drupalLogin($this->blocking_user); $this->drupalGet('admin/reports/visitors'); - $this->assertText($test_ip_address, t('IP address found.')); - $this->assertText(t('block IP address'), t('Block IP link displayed')); + $this->assertText($test_ip_address, 'IP address found.'); + $this->assertText(t('block IP address'), 'Block IP link displayed'); // Block the IP address. $this->clickLink('block IP address'); - $this->assertText(t('IP address blocking'), t('IP blocking page displayed.')); + $this->assertText(t('IP address blocking'), 'IP blocking page displayed.'); $edit = array(); $edit['ip'] = $test_ip_address; $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->assertNotEqual($ip, FALSE, t('IP address found in database')); - $this->assertRaw(t('The IP address %ip has been blocked.', array('%ip' => $edit['ip'])), t('IP address was blocked.')); + $this->assertNotEqual($ip, FALSE, 'IP address found in database'); + $this->assertRaw(t('The IP address %ip has been blocked.', array('%ip' => $edit['ip'])), 'IP address was blocked.'); // Verify that the block/unblock link on the top visitors page has been // altered. $this->drupalGet('admin/reports/visitors'); - $this->assertText(t('unblock IP address'), t('Unblock IP address link displayed')); + $this->assertText(t('unblock IP address'), 'Unblock IP address link displayed'); // Unblock the IP address. $this->clickLink('unblock IP address'); - $this->assertRaw(t('Are you sure you want to delete %ip?', array('%ip' => $test_ip_address)), t('IP address deletion confirmation found.')); + $this->assertRaw(t('Are you sure you want to delete %ip?', array('%ip' => $test_ip_address)), 'IP address deletion confirmation found.'); $edit = array(); $this->drupalPost('admin/config/people/ip-blocking/delete/1', NULL, t('Delete')); - $this->assertRaw(t('The IP address %ip was deleted.', array('%ip' => $test_ip_address)), t('IP address deleted.')); + $this->assertRaw(t('The IP address %ip was deleted.', array('%ip' => $test_ip_address)), 'IP address deleted.'); } } /** - * Test statistics administration screen. + * Tests the statistics administration screen. */ class StatisticsAdminTestCase extends DrupalWebTestCase { + + /** + * A user that has permission to administer and access statistics. + * + * @var object|FALSE + * + * A fully loaded user object, or FALSE if user creation failed. + */ protected $privileged_user; + + /** + * A page node for which to check access statistics. + * + * @var object + */ protected $test_node; public static function getInfo() { @@ -281,32 +341,32 @@ * Verifies that the statistics settings page works. */ function testStatisticsSettings() { - $this->assertFalse(variable_get('statistics_enable_access_log', 0), t('Access log is disabled by default.')); - $this->assertFalse(variable_get('statistics_count_content_views', 0), t('Count content view log is disabled by default.')); + $this->assertFalse(variable_get('statistics_enable_access_log', 0), 'Access log is disabled by default.'); + $this->assertFalse(variable_get('statistics_count_content_views', 0), 'Count content view log is disabled by default.'); $this->drupalGet('admin/reports/pages'); - $this->assertRaw(t('No statistics available.'), t('Verifying text shown when no statistics is available.')); + $this->assertRaw(t('No statistics available.'), 'Verifying text shown when no statistics is available.'); // Enable access log and counter on content view. $edit['statistics_enable_access_log'] = 1; $edit['statistics_count_content_views'] = 1; $this->drupalPost('admin/config/system/statistics', $edit, t('Save configuration')); - $this->assertTrue(variable_get('statistics_enable_access_log'), t('Access log is enabled.')); - $this->assertTrue(variable_get('statistics_count_content_views'), t('Count content view log is enabled.')); + $this->assertTrue(variable_get('statistics_enable_access_log'), 'Access log is enabled.'); + $this->assertTrue(variable_get('statistics_count_content_views'), 'Count content view log is enabled.'); // Hit the node. $this->drupalGet('node/' . $this->test_node->nid); $this->drupalGet('admin/reports/pages'); - $this->assertText('node/1', t('Test node found.')); + $this->assertText('node/1', 'Test node found.'); // Hit the node again (the counter is incremented after the hit, so // "1 read" will actually be shown when the node is hit the second time). $this->drupalGet('node/' . $this->test_node->nid); - $this->assertText('1 read', t('Node is read once.')); + $this->assertText('1 read', 'Node is read once.'); $this->drupalGet('node/' . $this->test_node->nid); - $this->assertText('2 reads', t('Node is read 2 times.')); + $this->assertText('2 reads', 'Node is read 2 times.'); } /** @@ -354,11 +414,11 @@ $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->assertFalse(user_load($account->uid, TRUE), t('User is not found in the database.')); + $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'); - $this->assertNoText($account->name, t('Did not find user in visitor statistics.')); + $this->assertNoText($account->name, 'Did not find user in visitor statistics.'); } /** @@ -372,10 +432,10 @@ $this->drupalGet('node/' . $this->test_node->nid); $this->drupalGet('node/' . $this->test_node->nid); - $this->assertText('1 read', t('Node is read once.')); + $this->assertText('1 read', 'Node is read once.'); $this->drupalGet('admin/reports/pages'); - $this->assertText('node/' . $this->test_node->nid, t('Hit URL found.')); + $this->assertText('node/' . $this->test_node->nid, 'Hit URL found.'); // statistics_cron will subtract the statistics_flush_accesslog_timer // variable from REQUEST_TIME in the delete query, so wait two secs here to @@ -384,19 +444,19 @@ $this->cronRun(); $this->drupalGet('admin/reports/pages'); - $this->assertNoText('node/' . $this->test_node->nid, t('No hit URL found.')); + $this->assertNoText('node/' . $this->test_node->nid, 'No hit URL found.'); $result = db_select('node_counter', 'nc') ->fields('nc', array('daycount')) ->condition('nid', $this->test_node->nid, '=') ->execute() ->fetchField(); - $this->assertFalse($result, t('Daycounter is zero.')); + $this->assertFalse($result, 'Daycounter is zero.'); } } /** - * Test statistics token replacement in strings. + * Tests statistics token replacement in strings. */ class StatisticsTokenReplaceTestCase extends StatisticsTestCase { public static function getInfo() { @@ -430,11 +490,11 @@ $tests['[node:last-view:short]'] = format_date($statistics['timestamp'], 'short'); // Test to make sure that we generated something for each token. - $this->assertFalse(in_array(0, array_map('strlen', $tests)), t('No empty tokens generated.')); + $this->assertFalse(in_array(0, array_map('strlen', $tests)), 'No empty tokens generated.'); foreach ($tests as $input => $expected) { $output = token_replace($input, array('node' => $node), array('language' => $language)); - $this->assertFalse(strcmp($output, $expected), t('Statistics token %token replaced.', array('%token' => $input))); + $this->assertEqual($output, $expected, format_string('Statistics token %token replaced.', array('%token' => $input))); } } } diff -Naur drupal-7.0/modules/statistics/statistics.tokens.inc drupal-7.66/modules/statistics/statistics.tokens.inc --- drupal-7.0/modules/statistics/statistics.tokens.inc 2010-04-20 11:48:06.000000000 +0200 +++ drupal-7.66/modules/statistics/statistics.tokens.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ ' . t('About') . ''; - $output .= '

    ' . t("The Syslog module logs events by sending messages to the logging facility of your web server's operating system. Syslog is an operating system administrative logging tool that provides valuable information for use in system management and security auditing. Most suited to medium and large sites, Syslog provides filtering tools that allow messages to be routed by type and severity. For more information, see the online handbook entry for Syslog module and PHP's openlog and syslog functions.", array('@syslog' => 'http://drupal.org/handbook/modules/syslog', '@php_openlog' => 'http://www.php.net/manual/function.openlog.php', '@php_syslog' => 'http://www.php.net/manual/function.syslog.php')) . '

    '; + $output .= '

    ' . t("The Syslog module logs events by sending messages to the logging facility of your web server's operating system. Syslog is an operating system administrative logging tool that provides valuable information for use in system management and security auditing. Most suited to medium and large sites, Syslog provides filtering tools that allow messages to be routed by type and severity. For more information, see the online handbook entry for Syslog module and PHP's openlog and syslog functions.", array('@syslog' => 'http://drupal.org/documentation/modules/syslog', '@php_openlog' => 'http://www.php.net/manual/function.openlog.php', '@php_syslog' => 'http://www.php.net/manual/function.syslog.php')) . '

    '; $output .= '

    ' . t('Uses') . '

    '; $output .= '
    '; $output .= '
    ' . t('Logging for UNIX, Linux, and Mac OS X') . '
    '; @@ -62,10 +70,11 @@ $form['actions']['#weight'] = 1; } - /** - * List all possible syslog facilities for UNIX/Linux. +/** + * Lists all possible syslog facilities for UNIX/Linux. * * @return array + * An array of syslog facilities for UNIX/Linux. */ function syslog_facility_list() { return array( @@ -101,7 +110,7 @@ '!ip' => $log_entry['ip'], '!request_uri' => $log_entry['request_uri'], '!referer' => $log_entry['referer'], - '!uid' => $log_entry['user']->uid, + '!uid' => $log_entry['uid'], '!link' => strip_tags($log_entry['link']), '!message' => strip_tags(!isset($log_entry['variables']) ? $log_entry['message'] : strtr($log_entry['message'], $log_entry['variables'])), )); diff -Naur drupal-7.0/modules/syslog/syslog.test drupal-7.66/modules/syslog/syslog.test --- drupal-7.0/modules/syslog/syslog.test 2010-08-06 01:53:39.000000000 +0200 +++ drupal-7.66/modules/syslog/syslog.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,6 +1,13 @@ drupalCreateUser(array('administer site configuration')); @@ -30,7 +37,7 @@ $this->drupalGet('admin/config/development/logging'); if ($this->parse()) { $field = $this->xpath('//option[@value=:value]', array(':value' => LOG_LOCAL6)); // Should be one field. - $this->assertTrue($field[0]['selected'] == 'selected', t('Facility value saved.')); + $this->assertTrue($field[0]['selected'] == 'selected', 'Facility value saved.'); } } } diff -Naur drupal-7.0/modules/system/form.api.php drupal-7.66/modules/system/form.api.php --- drupal-7.0/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.0/modules/system/html.tpl.php drupal-7.66/modules/system/html.tpl.php --- drupal-7.0/modules/system/html.tpl.php 2010-11-24 04:30:59.000000000 +0100 +++ drupal-7.66/modules/system/html.tpl.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ diff -Naur drupal-7.0/modules/system/image.gd.inc drupal-7.66/modules/system/image.gd.inc --- drupal-7.0/modules/system/image.gd.inc 2010-10-28 04:27:09.000000000 +0200 +++ drupal-7.66/modules/system/image.gd.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ 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)) { @@ -235,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; } /** @@ -303,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') { @@ -347,7 +396,7 @@ */ function image_gd_get_info(stdClass $image) { $details = FALSE; - $data = getimagesize(drupal_realpath($image->source)); + $data = @getimagesize($image->source); if (isset($data) && is_array($data)) { $extensions = array('1' => 'gif', '2' => 'jpg', '3' => 'png'); @@ -364,5 +413,5 @@ } /** - * @} End of "ingroup image". + * @} End of "addtogroup image". */ diff -Naur drupal-7.0/modules/system/language.api.php drupal-7.66/modules/system/language.api.php --- drupal-7.0/modules/system/language.api.php 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/modules/system/language.api.php 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,186 @@ +language) { + case 'it': + $conf['site_name'] = 'Il mio sito Drupal'; + break; + + case 'fr': + $conf['site_name'] = 'Mon site Drupal'; + break; + } +} + +/** + * Perform alterations on language switcher links. + * + * A language switcher link may need to point to a different path or use a + * translated link text before going through l(), which will just handle the + * path aliases. + * + * @param $links + * Nested array of links keyed by language code. + * @param $type + * The language type the links will switch. + * @param $path + * The current path. + */ +function hook_language_switch_links_alter(array &$links, $type, $path) { + global $language; + + if ($type == LANGUAGE_TYPE_CONTENT && isset($links[$language->language])) { + foreach ($links[$language->language] as $link) { + $link['attributes']['class'][] = 'active-language'; + } + } +} + +/** + * Define language types. + * + * @return + * An associative array of language type definitions. The keys are the + * identifiers, which are also used as names for global variables representing + * the types in the bootstrap phase. The values are associative arrays that + * may contain the following elements: + * - name: The human-readable language type identifier. + * - description: A description of the language type. + * - fixed: A fixed array of language negotiation provider identifiers to use + * to initialize this language. Defining this key makes the language type + * non-configurable, so it will always use the specified providers in the + * given priority order. Omit to make the language type configurable. + * + * @see hook_language_types_info_alter() + * @ingroup language_negotiation + */ +function hook_language_types_info() { + return array( + 'custom_language_type' => array( + 'name' => t('Custom language'), + 'description' => t('A custom language type.'), + ), + 'fixed_custom_language_type' => array( + 'fixed' => array('custom_language_provider'), + ), + ); +} + +/** + * Perform alterations on language types. + * + * @param $language_types + * Array of language type definitions. + * + * @see hook_language_types_info() + * @ingroup language_negotiation + */ +function hook_language_types_info_alter(array &$language_types) { + if (isset($language_types['custom_language_type'])) { + $language_types['custom_language_type_custom']['description'] = t('A far better description.'); + } +} + +/** + * Define language negotiation providers. + * + * @return + * An associative array of language negotiation provider definitions. The keys + * 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: + * - 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 + * included before the callbacks are invoked). + * - weight: The default weight of the provider. + * - name: The translated human-readable name for the provider. + * - description: A translated longer description of the provider. + * - config: An internal path pointing to the provider's configuration page. + * - cache: The value Drupal's page cache should be set to for the current + * provider to be invoked. + * + * @see hook_language_negotiation_info_alter() + * @ingroup language_negotiation + */ +function hook_language_negotiation_info() { + return array( + 'custom_language_provider' => array( + 'callbacks' => array( + 'language' => 'custom_language_provider_callback', + 'switcher' => 'custom_language_switcher_callback', + 'url_rewrite' => 'custom_language_url_rewrite_callback', + ), + 'file' => drupal_get_path('module', 'custom') . '/custom.module', + 'weight' => -4, + 'types' => array('custom_language_type'), + 'name' => t('Custom language negotiation provider'), + 'description' => t('This is a custom language negotiation provider.'), + 'cache' => 0, + ), + ); +} + +/** + * Perform alterations on language negoiation providers. + * + * @param $language_providers + * Array of language negotiation provider definitions. + * + * @see hook_language_negotiation_info() + * @ingroup language_negotiation + */ +function hook_language_negotiation_info_alter(array &$language_providers) { + if (isset($language_providers['custom_language_provider'])) { + $language_providers['custom_language_provider']['config'] = 'admin/config/regional/language/configure/custom-language-provider'; + } +} + +/** + * Perform alterations on the language fallback candidates. + * + * @param $fallback_candidates + * An array of language codes whose order will determine the language fallback + * order. + */ +function hook_language_fallback_candidates_alter(array &$fallback_candidates) { + $fallback_candidates = array_reverse($fallback_candidates); +} + +/** + * @} End of "addtogroup hooks". + */ diff -Naur drupal-7.0/modules/system/maintenance-page.tpl.php drupal-7.66/modules/system/maintenance-page.tpl.php --- drupal-7.0/modules/system/maintenance-page.tpl.php 2010-11-24 04:30:59.000000000 +0100 +++ drupal-7.66/modules/system/maintenance-page.tpl.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ diff -Naur drupal-7.0/modules/system/region.tpl.php drupal-7.66/modules/system/region.tpl.php --- drupal-7.0/modules/system/region.tpl.php 2010-09-23 19:53:09.000000000 +0200 +++ drupal-7.66/modules/system/region.tpl.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ diff -Naur drupal-7.0/modules/system/system.admin-rtl.css drupal-7.66/modules/system/system.admin-rtl.css --- drupal-7.0/modules/system/system.admin-rtl.css 2010-10-09 07:18:53.000000000 +0200 +++ drupal-7.66/modules/system/system.admin-rtl.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -/* $Id: system.admin-rtl.css,v 1.3 2010/10/09 05:18:53 webchick Exp $ */ /** * @file diff -Naur drupal-7.0/modules/system/system.admin.css drupal-7.66/modules/system/system.admin.css --- drupal-7.0/modules/system/system.admin.css 2010-10-15 06:40:41.000000000 +0200 +++ drupal-7.66/modules/system/system.admin.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -/* $Id: system.admin.css,v 1.6 2010/10/15 04:40:41 webchick Exp $ */ /** * @file diff -Naur drupal-7.0/modules/system/system.admin.inc drupal-7.66/modules/system/system.admin.inc --- drupal-7.0/modules/system/system.admin.inc 2011-01-04 05:02:29.000000000 +0100 +++ drupal-7.66/modules/system/system.admin.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ PDO::FETCH_ASSOC)); + WHERE ml.link_path <> 'admin/help' AND menu_name = :menu_name AND ml.plid = :mlid AND hidden = 0", $admin, array('fetch' => PDO::FETCH_ASSOC)); foreach ($result as $item) { _menu_link_translate($item); if (!$item['access']) { @@ -55,6 +54,7 @@ /** * Provide a single block from the administration menu as a page. + * * This function is often a destination for these blocks. * For example, 'admin/structure/types' needs to have a destination to be valid * in the Drupal menu system, but too much information there might be @@ -106,7 +106,15 @@ } /** - * Menu callback; displays a module's settings page. + * Displays the configuration overview page. + * + * This menu callback implementation is a legacy function that used to display + * the configuration overview page at admin/config. It is currently unused and + * will be removed in Drupal 8. The page at admin/config is now generated by + * system_admin_config_page(). + * + * @deprecated + * @see system_admin_config_page() */ function system_settings_overview() { // Check database setup if necessary @@ -158,7 +166,6 @@ 'alt' => t('Screenshot for !theme theme', array('!theme' => $theme->info['name'])), 'title' => t('Screenshot for !theme theme', array('!theme' => $theme->info['name'])), 'attributes' => array('class' => array('screenshot')), - 'getsize' => FALSE, ); break; } @@ -302,7 +309,7 @@ } drupal_goto('admin/appearance'); } - return drupal_access_denied(); + return MENU_ACCESS_DENIED; } /** @@ -330,7 +337,7 @@ } drupal_goto('admin/appearance'); } - return drupal_access_denied(); + return MENU_ACCESS_DENIED; } /** @@ -350,6 +357,14 @@ } // Set the default theme. variable_set('theme_default', $theme); + + // Rebuild the menu. This duplicates the menu_rebuild() in theme_enable(). + // However, modules must know the current default theme in order to use + // this information in hook_menu() or hook_menu_alter() implementations, + // and doing the variable_set() before the theme_enable() could result + // in a race condition where the theme is default but not enabled. + menu_rebuild(); + // The status message depends on whether an admin theme is currently in use: // a value of 0 means the admin theme is set to be the default theme. $admin_theme = variable_get('admin_theme', 0); @@ -368,7 +383,7 @@ } drupal_goto('admin/appearance'); } - return drupal_access_denied(); + return MENU_ACCESS_DENIED; } /** @@ -385,7 +400,7 @@ // Default settings are defined in theme_get_setting() in includes/theme.inc if ($key) { $var = 'theme_' . $key . '_settings'; - $themes = system_rebuild_theme_data(); + $themes = list_themes(); $features = $themes[$key]->info['features']; } else { @@ -463,17 +478,11 @@ ), ), ); - $logo_path = theme_get_setting('logo_path', $key); - // If $logo_path is a public:// URI, display the path relative to the files - // directory; stream wrappers are not end-user friendly. - if (file_uri_scheme($logo_path) == 'public') { - $logo_path = file_uri_target($logo_path); - } $form['logo']['settings']['logo_path'] = array( '#type' => 'textfield', '#title' => t('Path to custom logo'), - '#default_value' => $logo_path, '#description' => t('The path to the file you would like to use as your logo file instead of the default logo.'), + '#default_value' => theme_get_setting('logo_path', $key), ); $form['logo']['settings']['logo_upload'] = array( '#type' => 'file', @@ -504,17 +513,11 @@ ), ), ); - $favicon_path = theme_get_setting('favicon_path', $key); - // If $favicon_path is a public:// URI, display the path relative to the - // files directory; stream wrappers are not end-user friendly. - if (file_uri_scheme($favicon_path) == 'public') { - $favicon_path = file_uri_target($favicon_path); - } $form['favicon']['settings']['favicon_path'] = array( '#type' => 'textfield', '#title' => t('Path to custom icon'), - '#default_value' => $favicon_path, - '#description' => t('The path to the image file you would like to use as your custom shortcut icon.') + '#description' => t('The path to the image file you would like to use as your custom shortcut icon.'), + '#default_value' => theme_get_setting('favicon_path', $key), ); $form['favicon']['settings']['favicon_upload'] = array( '#type' => 'file', @@ -523,6 +526,22 @@ ); } + // Inject human-friendly values for logo and favicon. + foreach (array('logo' => 'logo.png', 'favicon' => 'favicon.ico') as $type => $default) { + if (isset($form[$type]['settings'][$type . '_path'])) { + $element = &$form[$type]['settings'][$type . '_path']; + + // If path is a public:// URI, display the path relative to the files + // directory; stream wrappers are not end-user friendly. + $original_path = $element['#default_value']; + $friendly_path = NULL; + if (file_uri_scheme($original_path) == 'public') { + $friendly_path = file_uri_target($original_path); + $element['#default_value'] = $friendly_path; + } + } + } + if ($key) { // Call engine-specific settings. $function = $themes[$key]->prefix . '_engine_settings'; @@ -553,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. @@ -615,19 +635,19 @@ } else { // File upload failed. - form_set_error('logo_upload', t('The favicon could not be uploaded.')); + form_set_error('favicon_upload', t('The favicon could not be uploaded.')); } } // 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.')); @@ -650,13 +670,20 @@ * the path could not be validated. */ function _system_theme_settings_validate_path($path) { - if (drupal_realpath($path)) { - // The path is relative to the Drupal root, or is a valid URI. + // Absolute local file paths are invalid. + if (drupal_realpath($path) == $path) { + return FALSE; + } + // A path relative to the Drupal root or a fully qualified URI is valid. + if (is_file($path)) { return $path; } - $uri = 'public://' . $path; - if (file_exists($uri)) { - return $uri; + // Prepend 'public://' for relative file paths within public filesystem. + if (file_uri_scheme($path) === FALSE) { + $path = 'public://' . $path; + } + if (is_file($path)) { + return $path; } return FALSE; } @@ -665,18 +692,28 @@ * Process system_theme_settings form submissions. */ function system_theme_settings_submit($form, &$form_state) { + // Exclude unnecessary elements before saving. + form_state_values_clean($form_state); + $values = $form_state['values']; + // Extract the name of the theme from the submitted form values, then remove + // it from the array so that it is not saved as part of the variable. + $key = $values['var']; + unset($values['var']); + // 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; @@ -696,10 +733,7 @@ if (empty($values['default_favicon']) && !empty($values['favicon_path'])) { $values['favicon_mimetype'] = file_get_mimetype($values['favicon_path']); } - $key = $values['var']; - // Exclude unnecessary elements before saving. - unset($values['var'], $values['submit'], $values['reset'], $values['form_id'], $values['op'], $values['form_build_id'], $values['form_token']); variable_set($key, $values); drupal_set_message(t('The configuration options have been saved.')); @@ -782,7 +816,7 @@ // Used when checking if module implements a help page. $help_arg = module_exists('help') ? drupal_help_arg() : FALSE; - // Used when displaying modules that are required by the install profile. + // Used when displaying modules that are required by the installation profile. require_once DRUPAL_ROOT . '/includes/install.inc'; $distribution_name = check_plain(drupal_install_profile_distribution_name()); @@ -792,7 +826,7 @@ $extra['enabled'] = (bool) $module->status; if (!empty($module->info['required'] )) { $extra['disabled'] = TRUE; - $extra['required_by'][] = $distribution_name; + $extra['required_by'][] = $distribution_name . (!empty($module->info['explanation']) ? ' ('. $module->info['explanation'] .')' : ''); } // If this module requires other modules, add them to the array. @@ -804,6 +838,7 @@ // Only display visible modules. elseif (isset($visible_files[$requires])) { $requires_name = $files[$requires]->info['name']; + // Disable this module if it is incompatible with the dependency's version. if ($incompatible_version = drupal_check_incompatibility($v, str_replace(DRUPAL_CORE_COMPATIBILITY . '-', '', $files[$requires]->info['version']))) { $extra['requires'][$requires] = t('@module (incompatible with version @version)', array( '@module' => $requires_name . $incompatible_version, @@ -811,6 +846,14 @@ )); $extra['disabled'] = TRUE; } + // Disable this module if the dependency is incompatible with this + // version of Drupal core. + elseif ($files[$requires]->info['core'] != DRUPAL_CORE_COMPATIBILITY) { + $extra['requires'][$requires] = t('@module (incompatible with this version of Drupal core)', array( + '@module' => $requires_name, + )); + $extra['disabled'] = TRUE; + } elseif ($files[$requires]->status) { $extra['requires'][$requires] = t('@module (enabled)', array('@module' => $requires_name)); } @@ -910,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) { @@ -955,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 @@ -986,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)), ); @@ -1030,7 +1085,7 @@ '@module' => $name, '@depends' => implode(', ', $info['depends']), ); - $items[] = format_plural(count($info['depends']), 'The @module module is missing, so the following module will be disabled: @depends.', 'The @module module is missing, so the following module will be disabled: @depends.', $t_argument); + $items[] = format_plural(count($info['depends']), 'The @module module is missing, so the following module will be disabled: @depends.', 'The @module module is missing, so the following modules will be disabled: @depends.', $t_argument); } $form['text'] = array('#markup' => theme('item_list', array('items' => $items))); @@ -1182,11 +1237,6 @@ } $form_state['redirect'] = 'admin/modules'; - - // Notify locale module about module changes, so translations can be - // imported. This might start a batch, and only return to the redirect - // path after that. - module_invoke('locale', 'system_update', $actions['install']); } /** @@ -1239,8 +1289,8 @@ '#title_display' => 'invisible', ); // All modules which depend on this one must be uninstalled first, before - // we can allow this module to be uninstalled. (The install profile is - // excluded from this list.) + // we can allow this module to be uninstalled. (The installation profile + // is excluded from this list.) foreach (array_keys($module->required_by) as $dependent) { if ($dependent != $profile && drupal_get_installed_schema_version($dependent) != SCHEMA_UNINSTALLED) { $dependent_name = isset($all_modules[$dependent]->info['name']) ? $all_modules[$dependent]->info['name'] : $dependent; @@ -1359,6 +1409,7 @@ '#theme' => 'table', '#header' => $header, '#rows' => $rows, + '#empty' => t('No blocked IP addresses available.'), ); return $build; @@ -1558,8 +1609,9 @@ * @ingroup forms */ function system_cron_settings() { + global $base_url; $form['description'] = array( - '#markup' => '

    '.t('Cron takes care of running periodical tasks like checking for updates and indexing content for search.').'

    ', + '#markup' => '

    ' . t('Cron takes care of running periodic tasks like checking for updates and indexing content for search.') . '

    ', ); $form['run'] = array( '#type' => 'submit', @@ -1570,17 +1622,23 @@ $form['status'] = array( '#markup' => $status, ); + + $form['cron_url'] = array( + '#markup' => '

    ' . t('To run cron from outside the site, go to !cron', array('!cron' => url($base_url . '/cron.php', array('external' => TRUE, 'query' => array('cron_key' => variable_get('cron_key', 'drupal')))))) . '

    ', + ); + $form['cron'] = array( '#type' => 'fieldset', ); $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'), ); - return system_settings_form($form, FALSE); + return system_settings_form($form); } /** @@ -1755,7 +1813,7 @@ '#title' => t('Private file system path'), '#default_value' => variable_get('file_private_path', ''), '#maxlength' => 255, - '#description' => t('A local file system path where private files will be stored. This directory must exist and be writable by Drupal. This directory should not be accessible over the web.'), + '#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'), ); @@ -1799,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; } @@ -2145,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); } @@ -2179,24 +2242,45 @@ * @see system_settings_form() */ function system_clean_url_settings($form, &$form_state) { - global $base_url; - - // When accessing this form using a non-clean URL, allow a re-check to make - // sure clean URLs can be disabled at all times. $available = FALSE; - if (strpos(request_uri(), '?q=') === FALSE || !empty($_SESSION['clean_url'])) { + $conflict = FALSE; + + // If the request URI is a clean URL, clean URLs must be available. + // Otherwise, run a test. + if (strpos(request_uri(), '?q=') === FALSE && strpos(request_uri(), '&q=') === FALSE) { $available = TRUE; } else { - $request = drupal_http_request($base_url . '/admin/config/search/clean-urls/check'); + $request = drupal_http_request($GLOBALS['base_url'] . '/admin/config/search/clean-urls/check'); + // If the request returns HTTP 200, clean URLs are available. if (isset($request->code) && $request->code == 200) { $available = TRUE; + // If the user started the clean URL test, provide explicit feedback. + if (isset($form_state['input']['clean_url_test_execute'])) { + drupal_set_message(t('The clean URL test passed.')); + } + } + else { + // If the test failed while clean URLs are enabled, make sure clean URLs + // can be disabled. + if (variable_get('clean_url', 0)) { + $conflict = TRUE; + // Warn the user of a conflicting situation, unless after processing + // a submitted form. + if (!isset($form_state['input']['op'])) { + drupal_set_message(t('Clean URLs are enabled, but the clean URL test failed. Uncheck the box below to disable clean URLs.'), 'warning'); + } + } + // If the user started the clean URL test, provide explicit feedback. + elseif (isset($form_state['input']['clean_url_test_execute'])) { + drupal_set_message(t('The clean URL test failed.'), 'warning'); + } } } - if ($available) { - $_SESSION['clean_url'] = TRUE; - + // Show the enable/disable form if clean URLs are available or if the user + // must be able to resolve a conflicting setting. + if ($available || $conflict) { $form['clean_url'] = array( '#type' => 'checkbox', '#title' => t('Enable clean URLs'), @@ -2204,18 +2288,37 @@ '#description' => t('Use URLs like example.com/user instead of example.com/?q=user.'), ); $form = system_settings_form($form); + if ($conflict) { + // $form_state['redirect'] needs to be set to the non-clean URL, + // otherwise the setting is not saved. + $form_state['redirect'] = url('', array('query' => array('q' => '/admin/config/search/clean-urls'))); + } } + // Show the clean URLs test form. else { drupal_add_js(drupal_get_path('module', 'system') . '/system.js'); - $form_state['redirect'] = $base_url . '/admin/config/search/clean-urls'; + $form_state['redirect'] = url('admin/config/search/clean-urls'); $form['clean_url_description'] = array( '#type' => 'markup', - '#markup' => '

    ' . t('Use URLs like example.com/user instead of example.com/?q=user.') . ' ' . t('If you are directed to a Page not found (404) error after testing for clean URLs, see the online handbook.', array('@handbook' => 'http://drupal.org/node/15365')) . '

    ', + '#markup' => '

    ' . t('Use URLs like example.com/user instead of example.com/?q=user.'), ); - $form['clean_url_test'] = array( - '#type' => 'submit', - '#value' => t('Run the clean URL test'), + // Explain why the user is seeing this page and what to expect after + // clicking the 'Run the clean URL test' button. + $form['clean_url_test_result'] = array( + '#type' => 'markup', + '#markup' => '

    ' . t('Clean URLs cannot be enabled. If you are directed to this page or to a Page not found (404) error after testing for clean URLs, see the online handbook.', array('@handbook' => 'http://drupal.org/node/15365')) . '

    ', + ); + $form['actions'] = array( + '#type' => 'actions', + 'clean_url_test' => array( + '#type' => 'submit', + '#value' => t('Run the clean URL test'), + ), + ); + $form['clean_url_test_execute'] = array( + '#type' => 'hidden', + '#value' => 1, ); } @@ -2463,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 */ @@ -2493,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'])) { @@ -2549,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; } @@ -2778,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.0/modules/system/system.api.php drupal-7.66/modules/system/system.api.php --- drupal-7.0/modules/system/system.api.php 2011-01-04 01:58:30.000000000 +0100 +++ drupal-7.66/modules/system/system.api.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ subject, then * 'subject' should be specified here. If complex logic is required to * build the label, a 'label callback' should be defined instead (see * the 'label callback' section above for details). + * - language: The name of the property, typically 'language', that contains + * the language code representing the language the entity has been created + * in. This value may be changed when editing the entity and represents + * the language its textual components are supposed to have. If no + * language property is available, the 'language callback' may be used + * instead. This entry can be omitted if the entities of this type are not + * language-aware. * - bundle keys: An array describing how the Field API can extract the - * information it needs from the bundle objects for this type (e.g - * $vocabulary objects for terms; not applicable for nodes). This entry can - * be omitted if this type's bundles do not exist as standalone objects. - * Elements: - * - bundle: The name of the property that contains the name of the bundle - * object. + * information it needs from the bundle objects for this type. This entry + * is required if the 'path' provided in the 'bundles'/'admin' section + * identifies the bundle using a named menu placeholder whose loader + * callback returns an object (e.g., $vocabulary for taxonomy terms, or + * $node_type for nodes). If the path does not include the bundle, or the + * bundle is just a string rather than an automatically loaded object, then + * this can be omitted. Elements: + * - bundle: The name of the property of the bundle object that contains + * the name of the bundle object. * - bundles: An array describing all bundles for this object type. Keys are * bundles machine names, as found in the objects' 'bundle' property - * (defined in the 'entity keys' entry above). Elements: + * (defined in the 'entity keys' entry above). 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. Elements: * - label: The human-readable name of the bundle. * - uri callback: Same as the 'uri callback' key documented above for the * entity type, but for the bundle only. When determining the URI of an @@ -142,9 +166,9 @@ * Elements: * - path: the path of the bundle's main administration page, as defined * in hook_menu(). If the path includes a placeholder for the bundle, - * the 'bundle argument', 'bundle helper' and 'real path' keys below - * are required. - * - bundle argument: The position of the placeholder in 'path', if any. + * the 'bundle argument' and 'real path' keys below are required. + * - bundle argument: The position of the bundle placeholder in 'path', if + * any. * - real path: The actual path (no placeholder) of the bundle's main * administration page. This will be used to generate links. * - access callback: As in hook_menu(). 'user_access' will be assumed if @@ -191,6 +215,7 @@ 'id' => 'nid', 'revision' => 'vid', 'bundle' => 'type', + 'language' => 'language', ), 'bundle keys' => array( 'bundle' => 'type', @@ -222,7 +247,7 @@ 'custom settings' => FALSE, ), 'search_result' => array( - 'label' => t('Search result'), + 'label' => t('Search result highlighting input'), 'custom settings' => FALSE, ), ); @@ -442,6 +467,24 @@ } /** + * Change the view mode of an entity that is being displayed. + * + * @param string $view_mode + * The view_mode that is to be used to display the entity. + * @param array $context + * Array with contextual information, including: + * - entity_type: The type of the entity that is being viewed. + * - entity: The entity object. + * - langcode: The langcode the entity is being viewed in. + */ +function hook_entity_view_mode_alter(&$view_mode, $context) { + // For nodes, change the view mode when it is teaser. + if ($context['entity_type'] == 'node' && $view_mode == 'teaser') { + $view_mode = 'my_custom_view_mode'; + } +} + +/** * Define administrative paths. * * Modules may specify whether or not the paths they define in hook_menu() are @@ -498,8 +541,10 @@ * The entities keyed by entity ID. * @param $type * The type of entities being loaded (i.e. node, user, comment). + * @param $langcode + * The language to display the entity in. */ -function hook_entity_prepare_view($entities, $type) { +function hook_entity_prepare_view($entities, $type, $langcode) { // Load a specific node into the user object for later theming. if ($type == 'user') { $nodes = mymodule_get_user_nodes(array_keys($entities)); @@ -512,8 +557,6 @@ /** * Perform periodic actions. * - * This hook will only be called if cron.php is run (e.g. by crontab). - * * Modules that require some commands to be executed periodically can * implement hook_cron(). The engine will then call the hook whenever a cron * run happens, as defined by the administrator. Typical tasks managed by @@ -540,7 +583,7 @@ // Long-running operation example, leveraging a queue: // Fetch feeds from other sites. - $result = db_query('SELECT * FROM {aggregator_feed} WHERE checked + refresh < :time AND refresh != :never', array( + $result = db_query('SELECT * FROM {aggregator_feed} WHERE checked + refresh < :time AND refresh <> :never', array( ':time' => REQUEST_TIME, ':never' => AGGREGATOR_CLEAR_NEVER, )); @@ -563,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() @@ -599,7 +644,7 @@ } /** - * Allows modules to declare their own Forms API element types and specify their + * Allows modules to declare their own Form API element types and specify their * default values. * * This hook allows modules to declare their own form element types and to @@ -649,7 +694,7 @@ * A module may implement this hook in order to alter the element type defaults * defined by a module. * - * @param &$type + * @param $type * All element type defaults as collected by hook_element_info(). * * @see hook_element_info() @@ -664,12 +709,14 @@ /** * Perform cleanup tasks. * - * This hook is run at the end of each page request. It is often used for - * page logging and specialized cleanup. This hook MUST NOT print anything. + * This hook is run at the end of most regular page requests. It is often + * used for page logging and specialized cleanup. This hook MUST NOT print + * anything because by the time it runs the response is already sent to + * the browser. * * Only use this hook if your code must run even for cached page views. - * If you have code which must run once on all non cached pages, use - * hook_init instead. Thats the usual case. If you implement this hook + * If you have code which must run once on all non-cached pages, use + * hook_init() instead. That is the usual case. If you implement this hook * and see an error like 'Call to undefined function', it is likely that * you are depending on the presence of a module which has not been loaded yet. * It is not loaded because Drupal is still in bootstrap mode. @@ -821,14 +868,14 @@ } /** - * Alter the commands that are sent to the user through the AJAX framework. + * Alter the commands that are sent to the user through the Ajax framework. * * @param $commands * An array of all commands that will be sent to the user. * * @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')); } @@ -871,7 +918,7 @@ * * This hook is invoked by menu_get_item() and allows for run-time alteration of router * information (page_callback, title, and so on) before it is translated and checked for - * access. The passed in $router_item is statically cached for the current request, so this + * access. The passed-in $router_item is statically cached for the current request, so this * hook is only invoked once for any router item that is retrieved via menu_get_item(). * * Usually, modules will only want to inspect the router item and conditionally @@ -912,6 +959,7 @@ * paths and whose values are an associative array of properties for each * path. (The complete list of properties is in the return value section below.) * + * @section sec_callback_funcs Callback Functions * The definition for each path may include a page callback function, which is * invoked when the registered path is requested. If there is no other * registered path that fits the requested path better, any further path @@ -922,6 +970,7 @@ * $items['abc/def'] = array( * 'page callback' => 'mymodule_abc_view', * ); + * return $items; * } * * function mymodule_abc_view($ghi = 0, $jkl = '') { @@ -935,6 +984,7 @@ * $jkl will be 'foo'. Note that this automatic passing of optional path * arguments applies only to page and theme callback functions. * + * @subsection sub_callback_arguments Callback Arguments * In addition to optional path arguments, the page callback and other callback * functions may specify argument lists as arrays. These argument lists may * contain both fixed/hard-coded argument values and integers that correspond @@ -942,22 +992,29 @@ * called, the corresponding path components will be substituted for the * integers. That is, the integer 0 in an argument list will be replaced with * the first path component, integer 1 with the second, and so on (path - * components are numbered starting from zero). This substitution feature allows - * you to re-use a callback function for several different paths. For example: + * components are numbered starting from zero). To pass an integer without it + * being replaced with its respective path component, use the string value of + * the integer (e.g., '1') as the argument value. This substitution feature + * allows you to re-use a callback function for several different paths. For + * example: * @code * function mymodule_menu() { * $items['abc/def'] = array( * 'page callback' => 'mymodule_abc_view', * 'page arguments' => array(1, 'foo'), * ); + * return $items; * } * @endcode * When path 'abc/def' is requested, the page callback function will get 'def' * as the first argument and (always) 'foo' as the second argument. * - * Note that if a page or theme callback function has an argument list array, - * these arguments will be passed first to the function, followed by any - * any arguments generated by optional path arguments as described above. + * If a page callback function uses an argument list array, and its path is + * requested with optional path arguments, then the list array's arguments are + * passed to the callback function first, followed by the optional path + * arguments. Using the above example, when path 'abc/def/bar/baz' is requested, + * mymodule_abc_view() will be called with 'def', 'foo', 'bar' and 'baz' as + * arguments, in that order. * * Special care should be taken for the page callback drupal_get_form(), because * your specific form callback function will always receive $form and @@ -970,6 +1027,8 @@ * @endcode * See @link form_api Form API documentation @endlink for details. * + * @section sec_path_wildcards Wildcards in Paths + * @subsection sub_simple_wildcards Simple Wildcards * Wildcards within paths also work with integer substitution. For example, * your module could register path 'my-module/%/edit': * @code @@ -979,8 +1038,10 @@ * ); * @endcode * When path 'my-module/foo/edit' is requested, integer 1 will be replaced - * with 'foo' and passed to the callback function. + * with 'foo' and passed to the callback function. Note that wildcards may not + * be used as the first component. * + * @subsection sub_autoload_wildcards Auto-Loader Wildcards * Registered paths may also contain special "auto-loader" wildcard components * in the form of '%mymodule_abc', where the '%' part means that this path * component is a wildcard, and the 'mymodule_abc' part defines the prefix for a @@ -1004,9 +1065,40 @@ * return db_query("SELECT * FROM {mymodule_abc} WHERE abc_id = :abc_id", array(':abc_id' => $abc_id))->fetchObject(); * } * @endcode - * This 'abc' object will then be passed into the page callback function - * mymodule_abc_edit() to replace the integer 1 in the page arguments. + * This 'abc' object will then be passed into the callback functions defined + * for the menu item, such as the page callback function mymodule_abc_edit() + * to replace the integer 1 in the argument array. Note that a load function + * should return FALSE when it is unable to provide a loadable object. For + * example, the node_load() function for the 'node/%node/edit' menu item will + * return FALSE for the path 'node/999/edit' if a node with a node ID of 999 + * does not exist. The menu routing system will return a 404 error in this case. + * + * @subsection sub_argument_wildcards Argument Wildcards + * You can also define a %wildcard_to_arg() function (for the example menu + * entry above this would be 'mymodule_abc_to_arg()'). The _to_arg() function + * is invoked to retrieve a value that is used in the path in place of the + * wildcard. A good example is user.module, which defines + * user_uid_optional_to_arg() (corresponding to the menu entry + * 'tracker/%user_uid_optional'). This function returns the user ID of the + * current user. + * + * The _to_arg() function will get called with three arguments: + * - $arg: A string representing whatever argument may have been supplied by + * the caller (this is particularly useful if you want the _to_arg() + * function only supply a (default) value if no other value is specified, + * as in the case of user_uid_optional_to_arg(). + * - $map: An array of all path fragments (e.g. array('node','123','edit') for + * 'node/123/edit'). + * - $index: An integer indicating which element of $map corresponds to $arg. + * + * _load() and _to_arg() functions may seem similar at first glance, but they + * have different purposes and are called at different times. _load() + * functions are called when the menu system is collecting arguments to pass + * to the callback functions defined for the menu item. _to_arg() functions + * are called when the menu system is generating links to related paths, such + * as the tabs for a set of MENU_LOCAL_TASK items. * + * @section sec_render_tabs Rendering Menu Items As Tabs * You can also make groups of menu items to be rendered (by default) as tabs * on a page. To do that, first create one menu item of type MENU_NORMAL_ITEM, * with your chosen path, such as 'foo'. Then duplicate that menu item, using a @@ -1016,24 +1108,24 @@ * MENU_LOCAL_TASK. Example: * @code * // Make "Foo settings" appear on the admin Config page - * $items['admin/config/foo'] = array( + * $items['admin/config/system/foo'] = array( * 'title' => 'Foo settings', * 'type' => MENU_NORMAL_ITEM, * // Page callback, etc. need to be added here. * ); - * // Make "Global settings" the main tab on the "Foo settings" page - * $items['admin/config/foo/global'] = array( - * 'title' => 'Global settings', + * // Make "Tab 1" the main tab on the "Foo settings" page + * $items['admin/config/system/foo/tab1'] = array( + * 'title' => 'Tab 1', * 'type' => MENU_DEFAULT_LOCAL_TASK, * // Access callback, page callback, and theme callback will be inherited - * // from 'admin/config/foo', if not specified here to override. + * // from 'admin/config/system/foo', if not specified here to override. * ); - * // Make an additional tab called "Node settings" on "Foo settings" - * $items['admin/config/foo/node'] = array( - * 'title' => 'Node settings', + * // Make an additional tab called "Tab 2" on "Foo settings" + * $items['admin/config/system/foo/tab2'] = array( + * 'title' => 'Tab 2', * 'type' => MENU_LOCAL_TASK, * // Page callback and theme callback will be inherited from - * // 'admin/config/foo', if not specified here to override. + * // 'admin/config/system/foo', if not specified here to override. * // Need to add access callback or access arguments. * ); * @endcode @@ -1120,6 +1212,10 @@ * same weight are ordered alphabetically. * - "menu_name": Optional. Set this to a custom menu if you don't want your * item to be placed in Navigation. + * - "expanded": Optional. If set to TRUE, and if a menu link is provided for + * this menu item (as a result of other properties), then the menu link is + * always expanded, equivalent to its 'always expanded' checkbox being set + * in the UI. * - "context": (optional) Defines the context a tab may appear in. By * default, all tabs are only displayed as local tasks when being rendered * in a page context. All tabs that should be accessible as contextual links @@ -1158,22 +1254,23 @@ * "default" task, which should display the same page as the parent item. * If the "type" element is omitted, MENU_NORMAL_ITEM is assumed. * - "options": An array of options to be passed to l() when generating a link - * from this menu item. + * from this menu item. Note that the "options" parameter has no effect on + * MENU_LOCAL_TASK, MENU_DEFAULT_LOCAL_TASK, and MENU_LOCAL_ACTION items. * * For a detailed usage example, see page_example.module. * For comprehensive documentation on the menu system, see * http://drupal.org/node/102338. */ function hook_menu() { - $items['blog'] = array( - 'title' => 'blogs', - 'page callback' => 'blog_page', + $items['example'] = array( + 'title' => 'Example Page', + 'page callback' => 'example_page', 'access arguments' => array('access content'), 'type' => MENU_SUGGESTED_ITEM, ); - $items['blog/feed'] = array( - 'title' => 'RSS feed', - 'page callback' => 'blog_feed', + $items['example/feed'] = array( + 'title' => 'Example RSS feed', + 'page callback' => 'example_feed', 'access arguments' => array('access content'), 'type' => MENU_CALLBACK, ); @@ -1289,7 +1386,7 @@ */ function hook_menu_link_update($link) { // If the parent menu has changed, update our record. - $menu_name = db_result(db_query("SELECT mlid, menu_name, status FROM {menu_example} WHERE mlid = :mlid", array(':mlid' => $link['mlid']))); + $menu_name = db_query("SELECT menu_name FROM {menu_example} WHERE mlid = :mlid", array(':mlid' => $link['mlid']))->fetchField(); if ($menu_name != $link['menu_name']) { db_update('menu_example') ->fields(array('menu_name' => $link['menu_name'])) @@ -1330,7 +1427,7 @@ * - #link: An associative array containing: * - title: The localized title of the link. * - href: The system path to link to. - * - localized_options: An array of options to pass to url(). + * - localized_options: An array of options to pass to l(). * - #active: Whether the link should be marked as 'active'. * * @param $data @@ -1541,12 +1638,21 @@ * One popular use of this hook is to add form elements to the node form. When * altering a node form, the node object can be accessed at $form['#node']. * - * Note that instead of hook_form_alter(), which is called for all forms, you - * can also use hook_form_FORM_ID_alter() to alter a specific form. For each - * module (in system weight order) the general form alter hook implementation - * is invoked first, then the form ID specific alter implementation is called. - * After all module hook implementations are invoked, the hook_form_alter() - * implementations from themes are invoked in the same manner. + * In addition to hook_form_alter(), which is called for all forms, there are + * two more specific form hooks available. The first, + * hook_form_BASE_FORM_ID_alter(), allows targeting of a form/forms via a base + * form (if one exists). The second, hook_form_FORM_ID_alter(), can be used to + * target a specific form directly. + * + * The call order is as follows: all existing form alter functions are called + * for module A, then all for module B, etc., followed by all for any base + * theme(s), and finally for the theme itself. The module order is determined + * by system weight, then by module name. + * + * Within each module, form alter hooks are called in the following order: + * first, hook_form_alter(); second, hook_form_BASE_FORM_ID_alter(); third, + * hook_form_FORM_ID_alter(). So, for each module, the more general hooks are + * called first followed by the more specific. * * @param $form * Nested array of form elements that comprise the form. @@ -1558,7 +1664,9 @@ * String representing the name of the form itself. Typically this is the * name of the function that generated the form. * + * @see hook_form_BASE_FORM_ID_alter() * @see hook_form_FORM_ID_alter() + * @see forms_api_reference.html */ function hook_form_alter(&$form, &$form_state, $form_id) { if (isset($form['type']) && $form['type']['#value'] . '_node_settings' == $form_id) { @@ -1578,6 +1686,10 @@ * rather than implementing hook_form_alter() and checking the form ID, or * using long switch statements to alter multiple forms. * + * Form alter hooks are called in the following order: hook_form_alter(), + * hook_form_BASE_FORM_ID_alter(), hook_form_FORM_ID_alter(). See + * hook_form_alter() for more details. + * * @param $form * Nested array of form elements that comprise the form. * @param $form_state @@ -1589,7 +1701,9 @@ * name of the function that generated the form. * * @see hook_form_alter() + * @see hook_form_BASE_FORM_ID_alter() * @see drupal_prepare_form() + * @see forms_api_reference.html */ function hook_form_FORM_ID_alter(&$form, &$form_state, $form_id) { // Modification for the form with the given form ID goes here. For example, if @@ -1605,17 +1719,27 @@ } /** - * Provide a form-specific alteration for shared forms. + * Provide a form-specific alteration for shared ('base') forms. * - * Modules can implement hook_form_BASE_FORM_ID_alter() to modify a specific - * form belonging to multiple form_ids, rather than implementing - * hook_form_alter() and checking for conditions that would identify the - * shared form constructor. + * By default, when drupal_get_form() is called, Drupal looks for a function + * with the same name as the form ID, and uses that function to build the form. + * In contrast, base forms allow multiple form IDs to be mapped to a single base + * (also called 'factory') form function. * - * Examples for such forms are node_form() or comment_form(). + * Modules can implement hook_form_BASE_FORM_ID_alter() to modify a specific + * base form, rather than implementing hook_form_alter() and checking for + * conditions that would identify the shared form constructor. * - * Note that this hook fires after hook_form_FORM_ID_alter() and before - * hook_form_alter(). + * To identify the base form ID for a particular form (or to determine whether + * one exists) check the $form_state. The base form ID is stored under + * $form_state['build_info']['base_form_id']. + * + * See hook_forms() for more information on how to implement base forms in + * Drupal. + * + * Form alter hooks are called in the following order: hook_form_alter(), + * hook_form_BASE_FORM_ID_alter(), hook_form_FORM_ID_alter(). See + * hook_form_alter() for more details. * * @param $form * Nested array of form elements that comprise the form. @@ -1625,8 +1749,10 @@ * String representing the name of the form itself. Typically this is the * name of the function that generated the form. * + * @see hook_form_alter() * @see hook_form_FORM_ID_alter() * @see drupal_prepare_form() + * @see hook_forms() */ function hook_form_BASE_FORM_ID_alter(&$form, &$form_state, $form_id) { // Modification for the form with the given BASE_FORM_ID goes here. For @@ -1646,19 +1772,33 @@ * * By default, when drupal_get_form() is called, the system will look for a * function with the same name as the form ID, and use that function to build - * the form. This hook allows you to override that behavior in two ways. + * the form. If no such function is found, Drupal calls this hook. Modules + * implementing this hook can then provide their own instructions for mapping + * form IDs to constructor functions. As a result, you can easily map multiple + * form IDs to a single form constructor (referred to as a 'base' form). + * + * Using a base form can help to avoid code duplication, by allowing many + * similar forms to use the same code base. Another benefit is that it becomes + * much easier for other modules to apply a general change to the group of + * forms; hook_form_BASE_FORM_ID_alter() can be used to easily alter multiple + * forms at once by directly targeting the shared base form. + * + * Two example use cases where base forms may be useful are given below. * * First, you can use this hook to tell the form system to use a different * function to build certain forms in your module; this is often used to define * a form "factory" function that is used to build several similar forms. In * this case, your hook implementation will likely ignore all of the input - * arguments. See node_forms() for an example of this. + * arguments. See node_forms() for an example of this. Note, node_forms() is the + * hook_forms() implementation; the base form itself is defined in node_form(). * * Second, you could use this hook to define how to build a form with a * dynamically-generated form ID. In this case, you would need to verify that * 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 @@ -1669,17 +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. + * - 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 @@ -1703,36 +1848,50 @@ '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; } /** - * Perform setup tasks. See also, hook_init. + * Perform setup tasks for all page requests. * * This hook is run at the beginning of the page request. It is typically - * used to set up global parameters which are needed later in the request. + * used to set up global parameters that are needed later in the request. + * + * Only use this hook if your code must run even for cached page views. This + * hook is called before the theme, modules, or most include files are loaded + * into memory. It happens while Drupal is still in bootstrap mode. * - * Only use this hook if your code must run even for cached page views.This hook - * is called before modules or most include files are loaded into memory. - * It happens while Drupal is still in bootstrap mode. + * @see hook_init() */ function hook_boot() { - // we need user_access() in the shutdown function. make sure it gets loaded + // We need user_access() in the shutdown function. Make sure it gets loaded. drupal_load('module', 'user'); drupal_register_shutdown_function('devel_shutdown'); } /** - * Perform setup tasks. See also, hook_boot. + * Perform setup tasks for non-cached page requests. * * This hook is run at the beginning of the page request. It is typically - * used to set up global parameters which are needed later in the request. - * when this hook is called, all modules are already loaded in memory. + * used to set up global parameters that are needed later in the request. + * When this hook is called, the theme and all modules are already loaded in + * memory. * * 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() */ function hook_init() { // Since this file should only be loaded on the front page, it cannot be @@ -1745,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: @@ -1799,8 +1957,9 @@ * The drupal_mail() id of the message. Look at module source code or * drupal_mail() for possible id values. * - 'to': - * The address or addresses the message will be sent to. The - * formatting of this string must comply with RFC 2822. + * The address or addresses the message will be sent to. The formatting of + * this string will be validated with the + * @link http://php.net/manual/filter.filters.validate.php PHP e-mail validation filter. @endlink * - 'from': * The address the message will be marked as being from, which is * either a custom address or the site-wide default email address. @@ -1820,12 +1979,20 @@ * - 'language': * The language object used to build the message before hook_mail_alter() * is invoked. + * - 'send': + * Set to FALSE to abort sending this email message. * * @see drupal_mail() */ function hook_mail_alter(&$message) { if ($message['id'] == 'modulename_messagekey') { - $message['body'][] = "--\nMail sent out from " . variable_get('sitename', t('Drupal')); + if (!example_notifications_optin($message['to'], $message['id'])) { + // If the recipient has opted to not receive such messages, cancel + // sending. + $message['send'] = FALSE; + return; + } + $message['body'][] = "--\nMail sent out from " . variable_get('site_name', t('Drupal')); } } @@ -1836,7 +2003,16 @@ * hook in order to reorder the implementing modules, which are otherwise * ordered by the module's system weight. * - * @param &$implementations + * Note that hooks invoked using drupal_alter() can have multiple variations + * (such as hook_form_alter() and hook_form_FORM_ID_alter()). drupal_alter() + * will call all such variants defined by a single module in turn. For the + * purposes of hook_module_implements_alter(), these variants are treated as + * a single hook. Thus, to ensure that your implementation of + * hook_form_FORM_ID_alter() is called at the right time, you will have to + * change the order of hook_form_alter() implementation in + * hook_module_implements_alter(). + * + * @param $implementations * An array keyed by the module's name. The value of each item corresponds * to a $group, which is usually FALSE, unless the implementation is in a * file named $module.$group.inc. @@ -1856,6 +2032,41 @@ } /** + * Return additional themes provided by modules. + * + * Only use this hook for testing purposes. Use a hidden MYMODULE_test.module + * to implement this hook. Testing themes should be hidden, too. + * + * This hook is invoked from _system_rebuild_theme_data() and allows modules to + * register additional themes outside of the regular 'themes' directories of a + * Drupal installation. + * + * @return + * An associative array. Each key is the system name of a theme and each value + * is the corresponding path to the theme's .info file. + */ +function hook_system_theme_info() { + $themes['mymodule_test_theme'] = drupal_get_path('module', 'mymodule') . '/mymodule_test_theme/mymodule_test_theme.info'; + return $themes; +} + +/** + * 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 @@ -1863,7 +2074,7 @@ * add to or alter the data generated by reading the .info file with * drupal_parse_info_file(). * - * @param &$info + * @param $info * The .info file contents, passed by reference so that it can be altered. * @param $file * Full information about the module or theme, including $file->name, and @@ -1904,9 +2115,19 @@ * have inherent security risks across a variety of potential use cases * (for example, the "administer filters" and "bypass node access" * permissions provided by Drupal core). When set to TRUE, a standard - * warning message defined in user_admin_permissions() will be associated - * with the permission and displayed with it on the permission - * administration page. Defaults to FALSE. + * warning message defined in user_admin_permissions() and output via + * theme_user_permission_description() will be associated with the + * permission and displayed with it on the permission administration page. + * Defaults to FALSE. + * - warning: (optional) A translated warning message to display for this + * permission on the permission administration page. This warning overrides + * the automatic warning generated by 'restrict access' being set to TRUE. + * This should rarely be used, since it is important for all permissions to + * have a clear, consistent security warning that is the same across the + * site. Use the 'description' key instead to provide any information that + * is specific to the permission you are defining. + * + * @see theme_user_permission_description() */ function hook_permission() { return array( @@ -1918,8 +2139,71 @@ } /** + * 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 + * specify how a particular render array is to be rendered as HTML (this is + * usually the case if the theme function is assigned to the render array's + * #theme property), or they return the HTML that should be returned by an + * invocation of theme(). See + * @link http://drupal.org/node/933976 Using the theme layer Drupal 7.x @endlink + * for more information on how to implement theme hooks. + * * The following parameters are all optional. * * @param array $existing @@ -1947,40 +2231,48 @@ * @return array * An associative array of theme hook information. The keys on the outer * array are the internal names of the hooks, and the values are arrays - * containing information about the hook. Each array may contain the - * following elements: - * - variables: (required if "render element" not present) An array of - * variables that this theme hook uses. This value allows the theme layer to - * properly utilize templates. Each array key represents the name of the - * variable and the value will be used as the default value if it is not - * given when theme() is called. Template implementations receive these - * arguments as variables in the template file. Function implementations - * are passed this array data in the $variables parameter. - * - render element: (required if "variables" not present) A string that is - * the name of the sole renderable element to pass to the theme function. - * The string represents the name of the "variable" that will hold the - * renderable array inside any optional preprocess or process functions. - * Cannot be used with the "variables" item; only one or the other, not - * both, can be present in a hook's info array. + * containing information about the hook. Each information array must contain + * either a 'variables' element or a 'render element' element, but not both. + * Use 'render element' if you are theming a single element or element tree + * composed of elements, such as a form array, a page array, or a single + * checkbox element. Use 'variables' if your theme implementation is + * intended to be called directly through theme() and has multiple arguments + * for the data and style; in this case, the variables not supplied by the + * calling function will be given default values and passed to the template + * or theme function. The returned theme information array can contain the + * following key/value pairs: + * - variables: (see above) Each array key is the name of the variable, and + * the value given is used as the default value if the function calling + * theme() does not supply it. Template implementations receive each array + * key as a variable in the template file (so they must be legal PHP + * variable names). Function implementations are passed the variables in a + * single $variables function argument. + * - render element: (see above) The name of the renderable element or element + * tree to pass to the theme function. This name is used as the name of the + * variable that holds the renderable element or tree in preprocess and + * process functions. * - file: The file the implementation resides in. This file will be included * prior to the theme being rendered, to make sure that the function or * preprocess function (as needed) is actually loaded; this makes it * possible to split theme functions out into separate files quite easily. * - path: Override the path of the file to be used. Ordinarily the module or - * theme path will be used, but if the file will not be in the default path, - * include it here. This path should be relative to the Drupal root + * theme path will be used, but if the file will not be in the default + * path, include it here. This path should be relative to the Drupal root * directory. - * - template: If specified, this theme implementation is a template, and this - * is the template file without an extension. Do not put .tpl.php on this - * file; that extension will be added automatically by the default rendering - * engine (which is PHPTemplate). If 'path', above, is specified, the - * template should also be in this path. - * - function: If specified, this will be the function name to invoke for this - * implementation. If neither file nor function is specified, a default - * function name will be assumed. For example, if a module registers - * the 'node' theme hook, 'theme_node' will be assigned to its function. - * If the chameleon theme registers the node hook, it will be assigned - * 'chameleon_node' as its function. + * - template: If specified, this theme implementation is a template, and + * this is the template file without an extension. Do not put .tpl.php on + * this file; that extension will be added automatically by the default + * rendering engine (which is PHPTemplate). If 'path', above, is specified, + * the template should also be in this path. + * - function: If specified, this will be the function name to invoke for + * this implementation. If neither 'template' nor 'function' is specified, + * a default function name will be assumed. For example, if a module + * registers the 'node' theme hook, 'theme_node' will be assigned to its + * function. If the chameleon theme registers the node hook, it will be + * assigned 'chameleon_node' as its function. + * - base hook: A string declaring the base theme hook if this theme + * implementation is actually implementing a suggestion for another theme + * hook. * - pattern: A regular expression pattern to be used to allow this theme * implementation to have a dynamic name. The convention is to use __ to * differentiate the dynamic portion of the theme. For example, to allow @@ -1995,17 +2287,19 @@ * a theme this will be filled in as phptemplate_preprocess and * phptemplate_preprocess_HOOK as well as themename_preprocess and * themename_preprocess_HOOK. - * - override preprocess functions: Set to TRUE when a theme does NOT want the - * standard preprocess functions to run. This can be used to give a theme - * FULL control over how variables are set. For example, if a theme wants - * total control over how certain variables in the page.tpl.php are set, - * this can be set to true. Please keep in mind that when this is used + * - override preprocess functions: Set to TRUE when a theme does NOT want + * the standard preprocess functions to run. This can be used to give a + * theme FULL control over how variables are set. For example, if a theme + * wants total control over how certain variables in the page.tpl.php are + * set, this can be set to true. Please keep in mind that when this is used * by a theme, that theme becomes responsible for making sure necessary * variables are set. * - type: (automatically derived) Where the theme hook is defined: * 'module', 'theme_engine', or 'theme'. * - theme path: (automatically derived) The directory path of the theme or * module, so that it doesn't need to be looked up. + * + * @see hook_theme_registry_alter() */ function hook_theme($existing, $type, $theme, $path) { return array( @@ -2072,7 +2366,7 @@ function hook_theme_registry_alter(&$theme_registry) { // Kill the next/previous forum topic navigation links. foreach ($theme_registry['forum_topic_navigation']['preprocess functions'] as $key => $value) { - if ($value = 'template_preprocess_forum_topic_navigation') { + if ($value == 'template_preprocess_forum_topic_navigation') { unset($theme_registry['forum_topic_navigation']['preprocess functions'][$key]); } } @@ -2089,6 +2383,10 @@ * theme set via a theme callback function in hook_menu(); the themes on those * pages can only be overridden using hook_menu_alter(). * + * Note that returning different themes for the same path may not work with page + * caching. This is most likely to be a problem if an anonymous user on a given + * path could have different themes returned under different conditions. + * * Since only one theme can be used at a time, the last (i.e., highest * weighted) module which returns a valid theme name from this hook will * prevail. @@ -2096,7 +2394,8 @@ * @return * The machine-readable name of the theme that should be used for the current * page request. The value returned from this function will only have an - * effect if it corresponds to a currently-active theme on the site. + * effect if it corresponds to a currently-active theme on the site. Do not + * return a value if you do not wish to set a custom theme. */ function hook_custom_theme() { // Allow the user to request a particular theme via a query parameter. @@ -2184,31 +2483,41 @@ } /** - * Log an event message + * Log an event message. * * This hook allows modules to route log events to custom destinations, such as * SMS, Email, pager, syslog, ...etc. * * @param $log_entry * An associative array containing the following keys: - * - type: The type of message for this entry. For contributed modules, this is - * normally the module name. Do not use 'debug', use severity WATCHDOG_DEBUG instead. - * - user: The user object for the user who was logged in when the event happened. - * - request_uri: The Request URI for the page the event happened in. - * - referer: The page that referred the use to the page where the event occurred. + * - type: The type of message for this entry. + * - user: The user object for the user who was logged in when the event + * happened. + * - uid: The user ID for the user who was logged in when the event happened. + * - request_uri: The request URI for the page the event happened in. + * - referer: The page that referred the user to the page where the event + * occurred. * - ip: The IP address where the request for the page came from. - * - timestamp: The UNIX timestamp of the date/time the event occurred - * - severity: One of the following values as defined in RFC 3164 http://www.faqs.org/rfcs/rfc3164.html - * WATCHDOG_EMERGENCY Emergency: system is unusable - * WATCHDOG_ALERT Alert: action must be taken immediately - * WATCHDOG_CRITICAL Critical: critical conditions - * WATCHDOG_ERROR Error: error conditions - * WATCHDOG_WARNING Warning: warning conditions - * WATCHDOG_NOTICE Notice: normal but significant condition - * WATCHDOG_INFO Informational: informational messages - * WATCHDOG_DEBUG Debug: debug-level messages - * - link: an optional link provided by the module that called the watchdog() function. - * - message: The text of the message to be logged. + * - timestamp: The UNIX timestamp of the date/time the event occurred. + * - severity: The severity of the message; one of the following values as + * defined in @link http://www.faqs.org/rfcs/rfc3164.html RFC 3164: @endlink + * - WATCHDOG_EMERGENCY: Emergency, system is unusable. + * - WATCHDOG_ALERT: Alert, action must be taken immediately. + * - WATCHDOG_CRITICAL: Critical conditions. + * - WATCHDOG_ERROR: Error conditions. + * - WATCHDOG_WARNING: Warning conditions. + * - WATCHDOG_NOTICE: Normal but significant conditions. + * - WATCHDOG_INFO: Informational messages. + * - WATCHDOG_DEBUG: Debug-level messages. + * - link: An optional link provided by the module that called the watchdog() + * function. + * - message: The text of the message to be logged. Variables in the message + * are indicated by using placeholder strings alongside the variables + * argument to declare the value of the placeholders. See t() for + * documentation on how the message and variable parameters interact. + * - variables: An array of variables to be inserted into the message on + * display. Will be NULL or missing if a message is already translated or if + * the message is not possible to translate. */ function hook_watchdog(array $log_entry) { global $base_url, $language; @@ -2251,7 +2560,7 @@ '@ip' => $log_entry['ip'], '@request_uri' => $log_entry['request_uri'], '@referer_uri' => $log_entry['referer'], - '@uid' => $log_entry['user']->uid, + '@uid' => $log_entry['uid'], '@name' => $log_entry['user']->name, '@link' => strip_tags($log_entry['link']), '@message' => strip_tags($log_entry['message']), @@ -2272,8 +2581,9 @@ * An array to be filled in. Elements in this array include: * - id: An ID to identify the mail sent. Look at module source code * or drupal_mail() for possible id values. - * - to: The address or addresses the message will be sent to. The - * formatting of this string must comply with RFC 2822. + * - to: The address or addresses the message will be sent to. The formatting + * of this string will be validated with the + * @link http://php.net/manual/filter.filters.validate.php PHP e-mail validation filter. @endlink * - subject: Subject of the e-mail to be sent. This must not contain any * newline characters, or the mail may not be sent properly. drupal_mail() * sets this to an empty string when the hook is invoked. @@ -2352,8 +2662,10 @@ * 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 installed modules. + * An array of the modules that were installed. * * @see module_enable() * @see hook_modules_enabled() @@ -2375,7 +2687,7 @@ * invoked. * * @param $modules - * An array of the enabled modules. + * An array of the modules that were enabled. * * @see hook_enable() * @see hook_modules_installed() @@ -2396,7 +2708,7 @@ * is only called on the module actually being disabled. * * @param $modules - * An array of the disabled modules. + * An array of the modules that were disabled. * * @see hook_disable() * @see hook_modules_uninstalled() @@ -2414,11 +2726,11 @@ * modules a chance to perform actions when a module is uninstalled, whereas * hook_uninstall() is only called on the module actually being uninstalled. * - * It is recommended that you implement this module if your module - * stores data that may have been set by other modules. + * It is recommended that you implement this hook if your module stores + * data that may have been set by other modules. * * @param $modules - * An array of the uninstalled modules. + * An array of the modules that were uninstalled. * * @see hook_uninstall() * @see hook_modules_disabled() @@ -2521,7 +2833,7 @@ * An array of file objects, indexed by fid. * * @see file_load_multiple() - * @see upload_file_load() + * @see file_load() */ function hook_file_load($files) { // Add the upload specific data into the file object. @@ -2547,7 +2859,7 @@ * * @see file_validate() */ -function hook_file_validate(&$file) { +function hook_file_validate($file) { $errors = array(); if (empty($file->filename)) { @@ -2580,17 +2892,21 @@ /** * Respond to a file being added. * - * This hook is called before a file has been added to the database. The hook + * This hook is called after a file has been added to the database. The hook * doesn't distinguish between files created as a result of a copy or those * created by an upload. * * @param $file - * The file that is about to be saved. + * The file that has been added. * * @see file_save() */ function hook_file_insert($file) { - + // Add a message to the log, if the file is a jpg + $validate = file_validate_extensions($file, 'jpg'); + if (empty($validate)) { + watchdog('file', 'A jpg has been added.'); + } } /** @@ -2604,7 +2920,15 @@ * @see file_save() */ function hook_file_update($file) { + $file_user = user_load($file->uid); + // Make sure that the file name starts with the owner's user name. + if (strpos($file->filename, $file_user->name) !== 0) { + $old_filename = $file->filename; + $file->filename = $file_user->name . '_' . $file->filename; + $file->save(); + watchdog('file', t('%source has been renamed to %destination', array('%source' => $old_filename, '%destination' => $file->filename))); + } } /** @@ -2618,7 +2942,14 @@ * @see file_copy() */ function hook_file_copy($file, $source) { + $file_user = user_load($file->uid); + // Make sure that the file name starts with the owner's user name. + if (strpos($file->filename, $file_user->name) !== 0) { + $file->filename = $file_user->name . '_' . $file->filename; + $file->save(); + watchdog('file', t('Copied file %source has been renamed to %destination', array('%source' => $source->filename, '%destination' => $file->filename))); + } } /** @@ -2632,7 +2963,14 @@ * @see file_move() */ function hook_file_move($file, $source) { + $file_user = user_load($file->uid); + // Make sure that the file name starts with the owner's user name. + if (strpos($file->filename, $file_user->name) !== 0) { + $file->filename = $file_user->name . '_' . $file->filename; + $file->save(); + watchdog('file', t('Moved file %source has been renamed to %destination', array('%source' => $source->filename, '%destination' => $file->filename))); + } } /** @@ -2642,7 +2980,6 @@ * The file that has just been deleted. * * @see file_delete() - * @see upload_file_delete() */ function hook_file_delete($file) { // Delete all information associated with the file. @@ -2665,22 +3002,21 @@ * NULL. * * @see file_download() - * @see upload_file_download() */ function hook_file_download($uri) { // Check if the file is controlled by the current module. if (!file_prepare_directory($uri)) { $uri = FALSE; } - $result = db_query("SELECT f.* FROM {file_managed} f INNER JOIN {upload} u ON f.fid = u.fid WHERE uri = :uri", array('uri' => $uri)); - foreach ($result as $file) { - if (!user_access('view uploaded files')) { + if (strpos(file_uri_target($uri), variable_get('user_picture_path', 'pictures') . '/picture-') === 0) { + if (!user_access('access user profiles')) { + // Access to the file is denied. return -1; } - return array( - 'Content-Type' => $file->filemime, - 'Content-Length' => $file->filesize, - ); + else { + $info = image_get_info($uri); + return array('Content-Type' => $info['mime_type']); + } } } @@ -2747,9 +3083,10 @@ /** * Check installation requirements and do status reporting. * - * This hook has two closely related uses, determined by the $phase argument: - * checking installation requirements ($phase == 'install') - * and status reporting ($phase == 'runtime'). + * This hook has three closely related uses, determined by the $phase argument: + * - Checking installation requirements ($phase == 'install'). + * - Checking update requirements ($phase == 'update'). + * - Status reporting ($phase == 'runtime'). * * Note that this hook, like all others dealing with installation and updates, * must reside in a module_name.install file, or it will not properly abort @@ -2775,7 +3112,7 @@ * The returned 'requirements' will be listed on the status report in the * administration section, with indication of the severity level. * Moreover, any requirement with a severity of REQUIREMENT_ERROR severity will - * result in a notice on the the administration overview page. + * result in a notice on the administration configuration page. * * @param $phase * The phase in which requirements are checked: @@ -2785,8 +3122,9 @@ * status report page. * * @return - * A keyed array of requirements. Each requirement is itself an array with - * the following items: + * An associative array where the keys are arbitrary but must be unique (it + * is suggested to use the module short name as a prefix) and the values are + * themselves associative arrays with the following elements: * - title: The name of the requirement. * - value: The current value (e.g., version, time, level, etc). During * install phase, this should only be used for version numbers, do not set @@ -2800,7 +3138,7 @@ */ function hook_requirements($phase) { $requirements = array(); - // Ensure translations don't break at install time + // Ensure translations don't break during installation. $t = get_t(); // Report Drupal version @@ -2815,7 +3153,7 @@ // Test PHP version $requirements['php'] = array( 'title' => $t('PHP'), - 'value' => ($phase == 'runtime') ? l(phpversion(), 'admin/logs/status/php') : phpversion(), + 'value' => ($phase == 'runtime') ? l(phpversion(), 'admin/reports/status/php') : phpversion(), ); if (version_compare(phpversion(), DRUPAL_MINIMUM_PHP) < 0) { $requirements['php']['description'] = $t('Your PHP installation is too old. Drupal requires at least PHP %version.', array('%version' => DRUPAL_MINIMUM_PHP)); @@ -2837,7 +3175,7 @@ ); } - $requirements['cron']['description'] .= ' ' . t('You can run cron manually.', array('@cron' => url('admin/logs/status/run-cron'))); + $requirements['cron']['description'] .= ' ' . $t('You can run cron manually.', array('@cron' => url('admin/reports/status/run-cron'))); $requirements['cron']['title'] = $t('Cron maintenance tasks'); } @@ -2848,73 +3186,91 @@ /** * 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. * - * By implementing hook_schema() and specifying the tables your module - * declares, you can easily create and drop these tables 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. + * 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. 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( 'description' => 'The primary identifier for a node.', 'type' => 'serial', 'unsigned' => TRUE, - 'not null' => TRUE), + 'not null' => TRUE, + ), 'vid' => array( 'description' => 'The current {node_revision}.vid version identifier.', 'type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, - 'default' => 0), + 'default' => 0, + ), 'type' => array( 'description' => 'The {node_type} of this node.', 'type' => 'varchar', 'length' => 32, 'not null' => TRUE, - 'default' => ''), + 'default' => '', + ), 'title' => array( 'description' => 'The title of this node, always treated as non-markup plain text.', 'type' => 'varchar', 'length' => 255, 'not null' => TRUE, - 'default' => ''), + 'default' => '', ), + ), 'indexes' => array( 'node_changed' => array('changed'), 'node_created' => array('created'), - ), + ), 'unique keys' => array( 'nid_vid' => array('nid', 'vid'), - 'vid' => array('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', 'columns' => array('vid' => 'vid'), - ), + ), 'node_author' => array( 'table' => 'users', - 'columns' => array('uid' => 'uid') - ), - ), + 'columns' => array('uid' => 'uid'), + ), + ), 'primary key' => array('nid'), ); return $schema; @@ -2932,6 +3288,8 @@ * * @param $schema * Nested array describing the schemas for all modules. + * + * @ingroup schemaapi */ function hook_schema_alter(&$schema) { // Add field to existing schema. @@ -3013,14 +3371,15 @@ * If the module implements hook_schema(), the database tables will * be created before this hook is fired. * - * This hook will only be called the first time a module is enabled or after it + * Implementations of this hook are by convention declared in the module's + * .install file. The implementation can rely on the .module file being loaded. + * The hook will only be called the first time a module is enabled or after it * is re-enabled after being uninstalled. The module's schema version will be - * set to the module's greatest numbered update hook. Because of this, anytime a - * hook_update_N() is added to the module, this function needs to be updated to - * reflect the current version of the database schema. + * set to the module's greatest numbered update hook. Because of this, any time + * a hook_update_N() is added to the module, this function needs to be updated + * to reflect the current version of the database schema. * - * See the Schema API documentation at - * @link http://drupal.org/node/146843 http://drupal.org/node/146843 @endlink + * See the @link http://drupal.org/node/146843 Schema API documentation @endlink * for details on hook_schema and how database tables are defined. * * Note that since this function is called from a full bootstrap, all functions @@ -3055,69 +3414,87 @@ /** * Perform a single update. * - * For each patch which requires a database change add a new hook_update_N() - * which will be called by update.php. The database updates are numbered - * sequentially according to the version of Drupal you are compatible with. - * - * Schema updates should adhere to the Schema API: - * @link http://drupal.org/node/150215 http://drupal.org/node/150215 @endlink - * - * Database updates consist of 3 parts: - * - 1 digit for Drupal core compatibility - * - 1 digit for your module's major release version (e.g. is this the 5.x-1.* (1) or 5.x-2.* (2) series of your module?) - * - 2 digits for sequential counting starting with 00 - * - * The 2nd digit should be 0 for initial porting of your module to a new Drupal - * core API. + * For each change that requires one or more actions to be performed when + * updating a site, add a new hook_update_N(), which will be called by + * update.php. The documentation block preceding this function is stripped of + * newlines and used as the description for the update on the pending updates + * task list. Schema updates should adhere to the + * @link http://drupal.org/node/150215 Schema API. @endlink + * + * Implementations of hook_update_N() are named (module name)_update_(number). + * The numbers are composed of three parts: + * - 1 digit for Drupal core compatibility. + * - 1 digit for your module's major release version (e.g., is this the 7.x-1.* + * (1) or 7.x-2.* (2) series of your module?). This digit should be 0 for + * initial porting of your module to a new Drupal core API. + * - 2 digits for sequential counting, starting with 00. * * Examples: - * - mymodule_update_5200() - * - This is the first update to get the database ready to run mymodule 5.x-2.*. - * - mymodule_update_6000() - * - This is the required update for mymodule to run with Drupal core API 6.x. - * - mymodule_update_6100() - * - This is the first update to get the database ready to run mymodule 6.x-1.*. - * - mymodule_update_6200() - * - This is the first update to get the database ready to run mymodule 6.x-2.*. - * Users can directly update from 5.x-2.* to 6.x-2.* and they get all 60XX - * and 62XX updates, but not 61XX updates, because those reside in the - * 6.x-1.x branch only. + * - mymodule_update_7000(): This is the required update for mymodule to run + * with Drupal core API 7.x when upgrading from Drupal core API 6.x. + * - mymodule_update_7100(): This is the first update to get the database ready + * to run mymodule 7.x-1.*. + * - mymodule_update_7200(): This is the first update to get the database ready + * to run mymodule 7.x-2.*. Users can directly update from 6.x-2.* to 7.x-2.* + * and they get all 70xx and 72xx updates, but not 71xx updates, because + * those reside in the 7.x-1.x branch only. * * A good rule of thumb is to remove updates older than two major releases of * Drupal. See hook_update_last_removed() to notify Drupal about the removals. + * For further information about releases and release numbers see: + * @link http://drupal.org/node/711070 Maintaining a drupal.org project with Git @endlink * * Never renumber update functions. * - * Further information about releases and release numbers: - * - @link http://drupal.org/handbook/version-info http://drupal.org/handbook/version-info @endlink - * - @link http://drupal.org/node/93999 http://drupal.org/node/93999 @endlink (Overview of contributions branches and tags) - * - @link http://drupal.org/handbook/cvs/releases http://drupal.org/handbook/cvs/releases @endlink - * * Implementations of this hook should be placed in a mymodule.install file in * the same directory as mymodule.module. Drupal core's updates are implemented * using the system module as a name and stored in database/updates.inc. * - * 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. + * Not all module functions are available from within a hook_update_N() function. + * In order to call a function from your mymodule.module or an include file, + * you need to explicitly load that file first. + * + * During database updates the schema of any module could be out of date. For + * this reason, caution is needed when using any API function within an update + * function - particularly CRUD functions, functions that depend on the schema + * (for example by using drupal_write_record()), and any functions that invoke + * hooks. See @link update_api Update versions of API functions @endlink for + * details. + * + * 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 batch API: - * @link http://drupal.org/node/180528 http://drupal.org/node/180528 @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 - * Optionally update hooks may return a translated string that will be displayed - * to the user. If no message is returned, no message will be presented to the - * user. + * @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. + * + * @see batch + * @see schemaapi + * @see update_api + * @see hook_update_last_removed() + * @see update_get_update_list() */ function hook_update_N(&$sandbox) { // For non-multipass updates, the signature can simply be; @@ -3243,9 +3620,18 @@ * The module should not remove its entry from the {system} table. Database * tables defined by hook_schema() will be removed automatically. * - * The uninstall hook will fire when the module gets uninstalled but before the - * module's database tables are removed, allowing your module to query its own - * tables during this routine. + * The uninstall hook must be implemented in the module's .install file. It + * will fire when the module gets uninstalled but before the module's database + * tables are removed, allowing your module to query its own tables during + * this routine. + * + * When hook_uninstall() is called, your module will already be disabled, so + * its .module file will not be automatically included. If you need to call API + * functions from your .module file in this hook, use drupal_load() to make + * them available. (Keep this usage to a minimum, though, especially when + * calling API functions that invoke hooks, or API functions from modules + * listed as dependencies, since these may not be available or work as expected + * when the module is disabled.) * * @see hook_install() * @see hook_schema() @@ -3259,7 +3645,9 @@ /** * Perform necessary actions after module is enabled. * - * The hook is called every time the module is enabled. + * The hook is called every time the module is enabled. It should be + * implemented in the module's .install file. The implementation can + * rely on the .module file being loaded. * * @see module_enable() * @see hook_install() @@ -3272,7 +3660,9 @@ /** * Perform necessary actions before module is disabled. * - * The hook is called every time the module is disabled. + * The hook is called every time the module is disabled. It should be + * implemented in the module's .install file. The implementation can rely + * on the .module file being loaded. * * @see hook_uninstall() * @see hook_modules_disabled() @@ -3339,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 @@ -3381,64 +3772,62 @@ * variable_del() before your last task has completed and control is handed * back to the installer. * - * @return + * @param array $install_state + * An array of information about the current installation state. + * + * @return array * A keyed array of tasks the profile will perform during the final stage of * the installation. Each key represents the name of a function (usually a * function defined by this profile, although that is not strictly required) * that is called when that task is run. The values are associative arrays * containing the following key-value pairs (all of which are optional): - * - 'display_name' - * The human-readable name of the task. This will be displayed to the - * user while the installer is running, along with a list of other tasks - * that are being run. Leave this unset to prevent the task from - * appearing in the list. - * - 'display' - * This is a boolean which can be used to provide finer-grained control - * over whether or not the task will display. This is mostly useful for - * tasks that are intended to display only under certain conditions; for - * these tasks, you can set 'display_name' to the name that you want to - * display, but then use this boolean to hide the task only when certain - * conditions apply. - * - 'type' - * A string representing the type of task. This parameter has three - * possible values: - * - 'normal': This indicates that the task will be treated as a regular - * callback function, which does its processing and optionally returns - * HTML output. This is the default behavior which is used when 'type' is - * not set. - * - 'batch': This indicates that the task function will return a batch - * API definition suitable for batch_set(). The installer will then take - * care of automatically running the task via batch processing. - * - 'form': This indicates that the task function will return a standard + * - display_name: The human-readable name of the task. This will be + * displayed to the user while the installer is running, along with a list + * of other tasks that are being run. Leave this unset to prevent the task + * from appearing in the list. + * - display: This is a boolean which can be used to provide finer-grained + * control over whether or not the task will display. This is mostly useful + * for tasks that are intended to display only under certain conditions; + * for these tasks, you can set 'display_name' to the name that you want to + * display, but then use this boolean to hide the task only when certain + * conditions apply. + * - type: A string representing the type of task. This parameter has three + * possible values: + * - normal: (default) This indicates that the task will be treated as a + * regular callback function, which does its processing and optionally + * returns HTML output. + * - batch: This indicates that the task function will return a batch API + * definition suitable for batch_set(). The installer will then take care + * of automatically running the task via batch processing. + * - form: This indicates that the task function will return a standard * form API definition (and separately define validation and submit * handlers, as appropriate). The installer will then take care of * automatically directing the user through the form submission process. - * - 'run' - * A constant representing the manner in which the task will be run. This - * parameter has three possible values: - * - INSTALL_TASK_RUN_IF_NOT_COMPLETED: This indicates that the task will - * run once during the installation of the profile. This is the default - * behavior which is used when 'run' is not set. - * - INSTALL_TASK_SKIP: This indicates that the task will not run during + * - run: A constant representing the manner in which the task will be run. + * This parameter has three possible values: + * - INSTALL_TASK_RUN_IF_NOT_COMPLETED: (default) This indicates that the + * task will run once during the installation of the profile. + * - INSTALL_TASK_SKIP: This indicates that the task will not run during * the current installation page request. It can be used to skip running * an installation task when certain conditions are met, even though the * task may still show on the list of installation tasks presented to the * user. - * - INSTALL_TASK_RUN_IF_REACHED: This indicates that the task will run - * on each installation page request that reaches it. This is rarely + * - INSTALL_TASK_RUN_IF_REACHED: This indicates that the task will run on + * each installation page request that reaches it. This is rarely * necessary for an installation profile to use; it is primarily used by * the Drupal installer for bootstrap-related tasks. - * - 'function' - * Normally this does not need to be set, but it can be used to force the - * installer to call a different function when the task is run (rather - * than the function whose name is given by the array key). This could be - * used, for example, to allow the same function to be called by two - * different tasks. + * - function: Normally this does not need to be set, but it can be used to + * force the installer to call a different function when the task is run + * (rather than the function whose name is given by the array key). This + * could be used, for example, to allow the same function to be called by + * two different tasks. * * @see install_state_defaults() * @see batch_set() + * @see hook_install_tasks_alter() + * @see install_tasks() */ -function hook_install_tasks() { +function hook_install_tasks(&$install_state) { // Here, we define a variable to allow tasks to indicate that a particular, // processor-intensive batch process needs to be triggered later on in the // installation. @@ -3502,11 +3891,11 @@ /** * Change the page the user is sent to by drupal_goto(). * - * @param &$path + * @param $path * A Drupal path or a full URL. - * @param &$options + * @param $options * An associative array of additional URL options to pass to url(). - * @param &$http_response_code + * @param $http_response_code * The HTTP status code to use for the redirection. See drupal_goto() for more * information. */ @@ -3530,7 +3919,7 @@ function hook_html_head_alter(&$head_elements) { foreach ($head_elements as $key => $element) { if (isset($element['#attributes']['rel']) && $element['#attributes']['rel'] == 'canonical') { - // I want a custom canonical url. + // I want a custom canonical URL. $head_elements[$key]['#attributes']['href'] = mymodule_canonical_url(); } } @@ -3539,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 @@ -3546,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 @@ -3810,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 @@ -3889,7 +4283,7 @@ function hook_page_delivery_callback_alter(&$callback) { // jQuery sets a HTTP_X_REQUESTED_WITH header of 'XMLHttpRequest'. // If a page would normally be delivered as an html page, and it is called - // from jQuery, deliver it instead as an AJAX response. + // from jQuery, deliver it instead as an Ajax response. if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest' && $callback == 'drupal_deliver_html_page') { $callback = 'ajax_deliver'; } @@ -3907,7 +4301,11 @@ foreach ($theme_groups as $state => &$group) { foreach ($theme_groups[$state] as &$theme) { // Add a foo link to each list of theme operations. - $theme->operations[] = l(t('Foo'), 'admin/appearance/foo', array('query' => array('theme' => $theme->name))); + $theme->operations[] = array( + 'title' => t('Foo'), + 'href' => 'admin/appearance/foo', + 'query' => array('theme' => $theme->name) + ); } } } @@ -3941,7 +4339,7 @@ * @param $path * The outbound path to alter, not adjusted for path aliases yet. It won't be * adjusted for path aliases until all modules are finished altering it, thus - * being consistent with hook_url_alter_inbound(), which adjusts for all path + * being consistent with hook_url_inbound_alter(), which adjusts for all path * aliases before allowing modules to alter it. This may have been altered by * other modules before this one. * @param $options @@ -3975,7 +4373,7 @@ * displayed. Can be used to ensure user privacy in situations where * $account->name is too revealing. * - * @param &$name + * @param $name * The string that format_username() will return. * * @param $account @@ -4359,7 +4757,7 @@ /** * Register information about FileTransfer classes provided by a module. * - * The FileTransfer class allows transfering files over a specific type of + * The FileTransfer class allows transferring files over a specific type of * connection. Core provides classes for FTP and SSH. Contributed modules are * free to extend the FileTransfer base class to add other connection types, * and if these classes are registered via hook_filetransfer_info(), those @@ -4422,3 +4820,207 @@ /** * @} End of "addtogroup hooks". */ + +/** + * @addtogroup callbacks + * @{ + */ + +/** + * 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(). + * + * @param $entity + * The entity to return the URI for. + * + * @return + * An associative array with the following elements: + * - 'path': The URL path for the entity. + * - 'options': (optional) An array of options for the url() function. + * The actual entity URI can be constructed by passing these elements to + * url(). + */ +function callback_entity_info_uri($entity) { + return array( + 'path' => 'node/' . $entity->nid, + ); +} + +/** + * Return the label of an entity. + * + * Callback for hook_entity_info(). + * + * @param $entity + * The entity for which to generate the label. + * @param $entity_type + * The entity type; e.g., 'node' or 'user'. + * + * @return + * An unsanitized string with the label of the entity. + * + * @see entity_label() + */ +function callback_entity_info_label($entity, $entity_type) { + return empty($entity->title) ? 'Untitled entity' : $entity->title; +} + +/** + * Return the language code of the entity. + * + * Callback for hook_entity_info(). + * + * The language callback is meant to be used primarily for temporary alterations + * of the property value. + * + * @param $entity + * The entity for which to return the language. + * @param $entity_type + * The entity type; e.g., 'node' or 'user'. + * + * @return + * The language code for the language of the entity. + * + * @see entity_language() + */ +function callback_entity_info_language($entity, $entity_type) { + return $entity->language; +} + +/** + * @} End of "addtogroup callbacks". + */ + +/** + * @defgroup update_api Update versions of API functions + * @{ + * Functions that are similar to normal API functions, but do not invoke hooks. + * + * These simplified versions of core API functions are provided for use by + * update functions (hook_update_N() implementations). + * + * During database updates the schema of any module could be out of date. For + * this reason, caution is needed when using any API function within an update + * function - particularly CRUD functions, functions that depend on the schema + * (for example by using drupal_write_record()), and any functions that invoke + * hooks. + * + * Instead, a simplified utility function should be used. If a utility version + * of the API function you require does not already exist, then you should + * create a new function. The new utility function should be named + * _update_N_mymodule_my_function(). N is the schema version the function acts + * on (the schema version is the number N from the hook_update_N() + * implementation where this schema was introduced, or a number following the + * same numbering scheme), and mymodule_my_function is the name of the original + * API function including the module's name. + * + * Examples: + * - _update_6000_mymodule_save(): This function performs a save operation + * without invoking any hooks using the 6.x schema. + * - _update_7000_mymodule_save(): This function performs the same save + * operation using the 7.x schema. + * + * The utility function should not invoke any hooks, and should perform database + * operations using functions from the + * @link database Database abstraction layer, @endlink + * like db_insert(), db_update(), db_delete(), db_query(), and so on. + * + * If a change to the schema necessitates a change to the utility function, a + * new function should be created with a name based on the version of the schema + * it acts on. See _update_7000_bar_get_types() and _update_7001_bar_get_types() + * in the code examples that follow. + * + * For example, foo.install could contain: + * @code + * function foo_update_dependencies() { + * // foo_update_7010() needs to run after bar_update_7000(). + * $dependencies['foo'][7010] = array( + * 'bar' => 7000, + * ); + * + * // foo_update_7036() needs to run after bar_update_7001(). + * $dependencies['foo'][7036] = array( + * 'bar' => 7001, + * ); + * + * return $dependencies; + * } + * + * function foo_update_7000() { + * // No updates have been run on the {bar_types} table yet, so this needs + * // to work with the 6.x schema. + * foreach (_update_6000_bar_get_types() as $type) { + * // Rename a variable. + * } + * } + * + * function foo_update_7010() { + * // Since foo_update_7010() is going to run after bar_update_7000(), it + * // needs to operate on the new schema, not the old one. + * foreach (_update_7000_bar_get_types() as $type) { + * // Rename a different variable. + * } + * } + * + * function foo_update_7036() { + * // This update will run after bar_update_7001(). + * foreach (_update_7001_bar_get_types() as $type) { + * } + * } + * @endcode + * + * And bar.install could contain: + * @code + * function bar_update_7000() { + * // Type and bundle are confusing, so we renamed the table. + * db_rename_table('bar_types', 'bar_bundles'); + * } + * + * function bar_update_7001() { + * // Database table names should be singular when possible. + * db_rename_table('bar_bundles', 'bar_bundle'); + * } + * + * function _update_6000_bar_get_types() { + * db_query('SELECT * FROM {bar_types}')->fetchAll(); + * } + * + * function _update_7000_bar_get_types() { + * db_query('SELECT * FROM {bar_bundles'})->fetchAll(); + * } + * + * function _update_7001_bar_get_types() { + * db_query('SELECT * FROM {bar_bundle}')->fetchAll(); + * } + * @endcode + * + * @see hook_update_N() + * @see hook_update_dependencies() + */ + +/** + * @} End of "defgroup update_api". + */ diff -Naur drupal-7.0/modules/system/system.archiver.inc drupal-7.66/modules/system/system.archiver.inc --- drupal-7.0/modules/system/system.archiver.inc 2010-12-30 23:33:04.000000000 +0100 +++ drupal-7.66/modules/system/system.archiver.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ -10, ); - // Display the currently active install profile, if the site - // is not running the default install profile. + // Display the currently active installation profile, if the site + // is not running the default installation profile. $profile = drupal_get_profile(); if ($profile != 'standard') { - $info = install_profile_info($profile); + $info = system_get_info('module', $profile); $requirements['install_profile'] = array( 'title' => $t('Install profile'), 'value' => $t('%profile_name (%profile-%version)', array( @@ -166,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 @@ -202,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( @@ -209,7 +209,7 @@ 'value' => $memory_limit == -1 ? t('-1 (Unlimited)') : $memory_limit, ); - if ($memory_limit && $memory_limit != -1 && parse_size($memory_limit) < parse_size(DRUPAL_MINIMUM_PHP_MEMORY_LIMIT)) { + if (!drupal_check_memory_limit(DRUPAL_MINIMUM_PHP_MEMORY_LIMIT, $memory_limit)) { $description = ''; if ($phase == 'install') { $description = $t('Consider increasing your PHP memory limit to %memory_minimum_limit to help prevent errors in the installation process.', array('%memory_minimum_limit' => DRUPAL_MINIMUM_PHP_MEMORY_LIMIT)); @@ -259,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. @@ -309,7 +342,7 @@ variable_get('file_private_path', FALSE), ); - // Do not check for the temporary files directory at install time + // Do not check for the temporary files directory during installation // unless it has been set in settings.php. In this case the user has // no alternative but to fix the directory if it is not writable. if ($phase == 'install') { @@ -413,7 +446,7 @@ $profile = drupal_get_profile(); $files = system_rebuild_module_data(); foreach ($files as $module => $file) { - // Ignore disabled modules and install profiles. + // Ignore disabled modules and installation profiles. if (!$file->status || $module == $profile) { continue; } @@ -466,7 +499,7 @@ $requirements['update status'] = array( 'value' => $t('Not enabled'), 'severity' => REQUIREMENT_WARNING, - 'description' => $t('Update notifications are not enabled. It is highly recommended that you enable the update status module from the module administration page in order to stay up-to-date on new releases. For more information, Update status handbook page.', array('@update' => 'http://drupal.org/handbook/modules/update', '@module' => url('admin/modules'))), + 'description' => $t('Update notifications are not enabled. It is highly recommended that you enable the update manager module from the module administration page in order to stay up-to-date on new releases. For more information, Update status handbook page.', array('@update' => 'http://drupal.org/documentation/modules/update', '@module' => url('admin/modules'))), ); } else { @@ -491,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() { @@ -505,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(); @@ -517,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); } @@ -745,6 +850,7 @@ 'type' => 'varchar', 'length' => 100, 'not null' => TRUE, + 'binary' => TRUE, ), 'type' => array( 'description' => 'The date format type, e.g. medium.', @@ -772,6 +878,7 @@ 'type' => 'varchar', 'length' => 100, 'not null' => TRUE, + 'binary' => TRUE, ), 'type' => array( 'description' => 'The date format type, e.g. medium.', @@ -818,6 +925,7 @@ 'length' => 255, 'not null' => TRUE, 'default' => '', + 'binary' => TRUE, ), 'filemime' => array( 'description' => "The file's MIME type.", @@ -829,6 +937,7 @@ 'filesize' => array( 'description' => 'The size of the file in bytes.', 'type' => 'int', + 'size' => 'big', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0, @@ -953,33 +1062,6 @@ ), ); - $schema['history'] = array( - 'description' => 'A record of which {users} have read which {node}s.', - 'fields' => array( - 'uid' => array( - 'description' => 'The {users}.uid that read the {node} nid.', - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - ), - 'nid' => array( - 'description' => 'The {node}.nid that was read.', - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - ), - 'timestamp' => array( - 'description' => 'The Unix timestamp at which the read occurred.', - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - ), - ), - 'primary key' => array('uid', 'nid'), - 'indexes' => array( - 'nid' => array('nid'), - ), - ); $schema['menu_router'] = array( 'description' => 'Maps paths to various callbacks (access, page and title)', 'fields' => array( @@ -1555,7 +1637,7 @@ 'default' => '', ), 'type' => array( - 'description' => 'The type of the item, either module, theme, theme_engine, or profile.', + 'description' => 'The type of the item, either module, theme, or theme_engine.', 'type' => 'varchar', 'length' => 12, 'not null' => TRUE, @@ -1709,11 +1791,25 @@ * Implements hook_update_dependencies(). */ function system_update_dependencies() { - // Update 7053 adds new blocks, so make sure the block tables are updated. + // system_update_7053() queries the {block} table, so it must run after + // block_update_7002(), which creates that table. $dependencies['system'][7053] = array( 'block' => 7002, ); + // system_update_7061() queries the {node_revision} table, so it must run + // after node_update_7001(), which renames the {node_revisions} table. + $dependencies['system'][7061] = array( + 'node' => 7001, + ); + + // system_update_7067() migrates role permissions and therefore must run + // after the {role} and {role_permission} tables are properly set up, which + // happens in user_update_7007(). + $dependencies['system'][7067] = array( + 'user' => 7007, + ); + return $dependencies; } @@ -1729,17 +1825,18 @@ function system_update_7000() { $result = db_query("SELECT rid, perm FROM {permission} ORDER BY rid"); foreach ($result as $role) { - $renamed_permission = preg_replace('/(?<=^|,\ )create\ blog\ entries(?=,|$)/', 'create blog content', $role->perm); - $renamed_permission = preg_replace('/(?<=^|,\ )edit\ own\ blog\ entries(?=,|$)/', 'edit own blog content', $role->perm); - $renamed_permission = preg_replace('/(?<=^|,\ )edit\ any\ blog\ entry(?=,|$)/', 'edit any blog content', $role->perm); - $renamed_permission = preg_replace('/(?<=^|,\ )delete\ own\ blog\ entries(?=,|$)/', 'delete own blog content', $role->perm); - $renamed_permission = preg_replace('/(?<=^|,\ )delete\ any\ blog\ entry(?=,|$)/', 'delete any blog content', $role->perm); - - $renamed_permission = preg_replace('/(?<=^|,\ )create\ forum\ topics(?=,|$)/', 'create forum content', $role->perm); - $renamed_permission = preg_replace('/(?<=^|,\ )delete\ any\ forum\ topic(?=,|$)/', 'delete any forum content', $role->perm); - $renamed_permission = preg_replace('/(?<=^|,\ )delete\ own\ forum\ topics(?=,|$)/', 'delete own forum content', $role->perm); - $renamed_permission = preg_replace('/(?<=^|,\ )edit\ any\ forum\ topic(?=,|$)/', 'edit any forum content', $role->perm); - $renamed_permission = preg_replace('/(?<=^|,\ )edit\ own\ forum\ topics(?=,|$)/', 'edit own forum content', $role->perm); + $renamed_permission = $role->perm; + $renamed_permission = preg_replace('/(?<=^|,\ )create\ blog\ entries(?=,|$)/', 'create blog content', $renamed_permission); + $renamed_permission = preg_replace('/(?<=^|,\ )edit\ own\ blog\ entries(?=,|$)/', 'edit own blog content', $renamed_permission); + $renamed_permission = preg_replace('/(?<=^|,\ )edit\ any\ blog\ entry(?=,|$)/', 'edit any blog content', $renamed_permission); + $renamed_permission = preg_replace('/(?<=^|,\ )delete\ own\ blog\ entries(?=,|$)/', 'delete own blog content', $renamed_permission); + $renamed_permission = preg_replace('/(?<=^|,\ )delete\ any\ blog\ entry(?=,|$)/', 'delete any blog content', $renamed_permission); + + $renamed_permission = preg_replace('/(?<=^|,\ )create\ forum\ topics(?=,|$)/', 'create forum content', $renamed_permission); + $renamed_permission = preg_replace('/(?<=^|,\ )delete\ any\ forum\ topic(?=,|$)/', 'delete any forum content', $renamed_permission); + $renamed_permission = preg_replace('/(?<=^|,\ )delete\ own\ forum\ topics(?=,|$)/', 'delete own forum content', $renamed_permission); + $renamed_permission = preg_replace('/(?<=^|,\ )edit\ any\ forum\ topic(?=,|$)/', 'edit any forum content', $renamed_permission); + $renamed_permission = preg_replace('/(?<=^|,\ )edit\ own\ forum\ topics(?=,|$)/', 'edit own forum content', $renamed_permission); if ($renamed_permission != $role->perm) { db_update('permission') @@ -1754,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()); } /** @@ -1894,9 +1991,6 @@ /** * Convert to new method of storing permissions. - * - * This update is in system.install rather than user.install so that - * all modules can use the updated permission scheme during their updates. */ function system_update_7007() { // Copy the permissions from the old {permission} table to the new {role_permission} table. @@ -1904,7 +1998,7 @@ $result = db_query("SELECT rid, perm FROM {permission} ORDER BY rid ASC"); $query = db_insert('role_permission')->fields(array('rid', 'permission')); foreach ($result as $role) { - foreach (explode(', ', $role->perm) as $perm) { + foreach (array_unique(explode(', ', $role->perm)) as $perm) { $query->values(array( 'rid' => $role->rid, 'permission' => $perm, @@ -1997,7 +2091,7 @@ $timezone = 'UTC'; } variable_set('date_default_timezone', $timezone); - drupal_set_message('The default time zone has been set to ' . check_plain($timezone) . '. Check the ' . l('date and time configuration page', 'admin/config/regional/settings') . ' to configure it correctly.', 'warning'); + drupal_set_message(format_string('The default time zone has been set to %timezone. Check the date and time configuration page to configure it correctly.', array('%timezone' => $timezone, '@config-url' => url('admin/config/regional/settings'))), 'warning'); // Remove temporary override. variable_del('date_temporary_timezone'); } @@ -2196,6 +2290,7 @@ 'length' => 255, 'not null' => TRUE, 'default' => '', + 'binary' => TRUE, ), 'uri' => array( 'description' => 'URI of file.', @@ -2203,6 +2298,7 @@ 'length' => 255, 'not null' => TRUE, 'default' => '', + 'binary' => TRUE, ), 'filemime' => array( 'description' => "The file's MIME type.", @@ -2265,10 +2361,6 @@ )); } $insert->execute(); - - // Remove obsolete variable 'site_offline_message'. See - // update_fix_d7_requirements(). - variable_del('site_offline_message'); } /** @@ -2291,6 +2383,9 @@ // provided any meaningful unique constraint ('pid' is a primary key). db_add_index('url_alias', 'source_language_pid', array('source', 'language', 'pid')); db_add_index('url_alias', 'alias_language_pid', array('alias', 'language', 'pid')); + + // Now that the URL aliases are correct, we can rebuild the whitelist. + drupal_path_alias_whitelist_rebuild(); } /** @@ -2749,15 +2844,37 @@ } if (!isset($sandbox['progress'])) { + // Delete stale rows from {upload} where the fid is not in the {files} table. + db_delete('upload') + ->notExists( + db_select('files', 'f') + ->fields('f', array('fid')) + ->where('f.fid = {upload}.fid') + ) + ->execute(); + + // Delete stale rows from {upload} where the vid is not in the + // {node_revision} table. The table has already been renamed in + // node_update_7001(). + db_delete('upload') + ->notExists( + db_select('node_revision', 'nr') + ->fields('nr', array('vid')) + ->where('nr.vid = {upload}.vid') + ) + ->execute(); + // Retrieve a list of node revisions that have uploaded files attached. // DISTINCT queries are expensive, especially when paged, so we store the // data in its own table for the duration of the update. - $table = array( - 'description' => t('Stores temporary data for system_update_7061.'), - 'fields' => array('vid' => array('type' => 'int')), - 'primary key' => array('vid'), - ); - db_create_table('system_update_7061', $table); + if (!db_table_exists('system_update_7061')) { + $table = array( + 'description' => t('Stores temporary data for system_update_7061.'), + 'fields' => array('vid' => array('type' => 'int', 'not null' => TRUE)), + 'primary key' => array('vid'), + ); + db_create_table('system_update_7061', $table); + } $query = db_select('upload', 'u'); $query->distinct(); $query->addField('u','vid'); @@ -2765,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; @@ -2794,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; @@ -2813,10 +2950,17 @@ $scheme = file_default_scheme() . '://'; foreach ($node_revisions as $vid => $revision) { foreach ($revision['file'][LANGUAGE_NONE] as $delta => $file) { - // We will convert filepaths to uri using the default scheme + // We will convert filepaths to URI using the default scheme // and stripping off the existing file directory path. - $file['uri'] = $scheme . str_replace($basename, '', $file['filepath']); - $file['uri'] = file_stream_wrapper_uri_normalize($file['uri']); + $file['uri'] = $scheme . preg_replace('!^' . preg_quote($basename) . '!', '', $file['filepath']); + // 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. @@ -2973,6 +3117,182 @@ } /** - * @} End of "defgroup updates-6.x-to-7.x" + * @} End of "defgroup updates-6.x-to-7.x". + * The next series of updates should start at 8000. + */ + +/** + * @defgroup updates-7.x-extra Extra updates for 7.x + * @{ + * Update functions between 7.x versions. + */ + +/** + * Remove the obsolete 'drupal_badge_color' and 'drupal_badge_size' variables. + */ +function system_update_7070() { + variable_del('drupal_badge_color'); + variable_del('drupal_badge_size'); +} + +/** + * Add index missed during upgrade, and fix field default. + */ +function system_update_7071() { + db_drop_index('date_format_type', 'title'); + db_add_index('date_format_type', 'title', array('title')); + db_change_field('registry', 'filename', 'filename', array( + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + )); +} + +/** + * Remove the obsolete 'site_offline_message' variable. + * + * @see update_fix_d7_requirements() + */ +function system_update_7072() { + variable_del('site_offline_message'); +} + +/** + * Add binary to {file_managed}, in case system_update_7034() was run without + * it. + */ +function system_update_7073() { + db_change_field('file_managed', 'filename', 'filename', array( + 'description' => 'Name of the file with no path components. This may differ from the basename of the URI if the file is renamed to avoid overwriting an existing file.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + 'binary' => TRUE, + )); + db_drop_unique_key('file_managed', 'uri'); + db_change_field('file_managed', 'uri', 'uri', array( + 'description' => 'The URI to access the file (either local or remote).', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + 'binary' => TRUE, + )); + db_add_unique_key('file_managed', 'uri', array('uri')); +} + +/** + * This update has been removed and will not run. + */ +function system_update_7074() { + // This update function previously converted menu_links query strings to + // arrays. It has been removed for now due to incompatibility with + // PostgreSQL. +} + +/** + * Convert menu_links query strings into arrays. + */ +function system_update_7076() { + $query = db_select('menu_links', 'ml', array('fetch' => PDO::FETCH_ASSOC)) + ->fields('ml', array('mlid', 'options')); + foreach ($query->execute() as $menu_link) { + if (strpos($menu_link['options'], 'query') !== FALSE) { + $menu_link['options'] = unserialize($menu_link['options']); + if (isset($menu_link['options']['query']) && is_string($menu_link['options']['query'])) { + $menu_link['options']['query'] = drupal_get_query_array($menu_link['options']['query']); + db_update('menu_links') + ->fields(array( + 'options' => serialize($menu_link['options']), + )) + ->condition('mlid', $menu_link['mlid'], '=') + ->execute(); + } + } + } +} + +/** + * Revert {file_managed}.filename changed to a binary column. + */ +function system_update_7077() { + db_change_field('file_managed', 'filename', 'filename', array( + 'description' => 'Name of the file with no path components. This may differ from the basename of the URI if the file is renamed to avoid overwriting an existing file.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + )); +} + + +/** + * Add binary to {date_formats}.format. + */ +function system_update_7078() { + db_drop_unique_key('date_formats', 'formats'); + db_change_field('date_formats', 'format', 'format', array( + 'description' => 'The date format string.', + 'type' => 'varchar', + 'length' => 100, + 'not null' => TRUE, + 'binary' => TRUE, + ), array('unique keys' => array('formats' => array('format', 'type')))); +} + +/** + * 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.0/modules/system/system.js drupal-7.66/modules/system/system.js --- drupal-7.0/modules/system/system.js 2010-10-13 15:43:21.000000000 +0200 +++ drupal-7.66/modules/system/system.js 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -// $Id: system.js,v 1.41 2010/10/13 13:43:21 dries Exp $ (function ($) { /** @@ -97,36 +96,26 @@ */ Drupal.behaviors.dateTime = { attach: function (context, settings) { - for (var value in settings.dateTime) { - var settings = settings.dateTime[value]; - var source = '#edit-' + value; - var suffix = source + '-suffix'; - - // Attach keyup handler to custom format inputs. - $('input' + source, context).once('date-time').keyup(function () { - var input = $(this); - var url = settings.lookup + (settings.lookup.match(/\?q=/) ? '&format=' : '?format=') + encodeURIComponent(input.val()); - $.getJSON(url, function (data) { - $(suffix).empty().append(' ' + settings.text + ': ' + data + ''); - }); - }); + for (var fieldName in settings.dateTime) { + if (settings.dateTime.hasOwnProperty(fieldName)) { + (function (fieldSettings, fieldName) { + var source = '#edit-' + fieldName; + var suffix = source + '-suffix'; + + // Attach keyup handler to custom format inputs. + $('input' + source, context).once('date-time').keyup(function () { + var input = $(this); + var url = fieldSettings.lookup + (/\?/.test(fieldSettings.lookup) ? '&format=' : '?format=') + encodeURIComponent(input.val()); + $.getJSON(url, function (data) { + $(suffix).empty().append(' ' + fieldSettings.text + ': ' + data + ''); + }); + }); + })(settings.dateTime[fieldName], fieldName); + } } } }; -/** - * Show the powered by Drupal image preview - */ -Drupal.behaviors.poweredByPreview = { - attach: function (context, settings) { - $('#edit-color, #edit-size').change(function () { - var path = settings.basePath + 'misc/' + $('#edit-color').val() + '-' + $('#edit-size').val() + '.png'; - $('img.powered-by-preview').attr('src', path); - }); - } -}; - - /** * Show/hide settings for page caching depending on whether page caching is * enabled or not. diff -Naur drupal-7.0/modules/system/system.mail.inc drupal-7.66/modules/system/system.mail.inc --- drupal-7.0/modules/system/system.mail.inc 2010-03-06 13:36:40.000000000 +0100 +++ drupal-7.66/modules/system/system.mail.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ ' . t('About') . ''; - $output .= '

    ' . t('The System module is integral to the site, and provides basic but extensible functionality for use by other modules and themes. Some integral elements of Drupal are contained in and managed by the System module, including caching, enabling and disabling modules and themes, preparing and displaying the administrative page, and configuring fundamental site settings. A number of key system maintenance operations are also part of the System module. For more information, see the online handbook entry for System module.', array('@system' => 'http://drupal.org/handbook/modules/system')) . '

    '; + $output .= '

    ' . t('The System module is integral to the site, and provides basic but extensible functionality for use by other modules and themes. Some integral elements of Drupal are contained in and managed by the System module, including caching, enabling and disabling modules and themes, preparing and displaying the administrative page, and configuring fundamental site settings. A number of key system maintenance operations are also part of the System module. For more information, see the online handbook entry for System module.', array('@system' => 'http://drupal.org/documentation/modules/system')) . '

    '; $output .= '

    ' . t('Uses') . '

    '; $output .= '
    '; $output .= '
    ' . t('Managing modules') . '
    '; @@ -236,6 +242,7 @@ ), 'access site reports' => array( 'title' => t('View site reports'), + 'restrict access' => TRUE, ), 'block IP addresses' => array( 'title' => t('Block IP addresses'), @@ -295,10 +302,10 @@ '#theme' => 'page', '#theme_wrappers' => array('html'), ); - // By default, we don't want AJAX commands being rendered in the context of an + // By default, we don't want Ajax commands being rendered in the context of an // HTML page, so we don't provide defaults for #theme or #theme_wrappers. // However, modules can set these properties (for example, to provide an HTML - // debugging page that displays rather than executes AJAX commands). + // debugging page that displays rather than executes Ajax commands). $types['ajax'] = array( '#header' => TRUE, '#commands' => array(), @@ -352,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'), ); @@ -367,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, @@ -375,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, @@ -650,7 +663,7 @@ // Modules. $items['admin/modules'] = array( 'title' => 'Modules', - 'description' => 'Enable or disable modules.', + 'description' => 'Extend site functionality.', 'page callback' => 'drupal_get_form', 'page arguments' => array('system_modules'), 'access arguments' => array('administer modules'), @@ -970,7 +983,7 @@ ); $items['admin/config/system/site-information'] = array( 'title' => 'Site information', - 'description' => t('Change site name, e-mail address, slogan, default front page, and number of posts per page, error pages.'), + 'description' => 'Change site name, e-mail address, slogan, default front page, and number of posts per page, error pages.', 'page callback' => 'drupal_get_form', 'page arguments' => array('system_site_information_settings'), 'access arguments' => array('administer site configuration'), @@ -978,8 +991,8 @@ 'weight' => -20, ); $items['admin/config/system/cron'] = array( - 'title' => t('Cron'), - 'description' => t('Manage automatic site maintenance tasks.'), + 'title' => 'Cron', + 'description' => 'Manage automatic site maintenance tasks.', 'page callback' => 'drupal_get_form', 'page arguments' => array('system_cron_settings'), 'access arguments' => array('administer site configuration'), @@ -1079,7 +1092,7 @@ * Implements hook_library(). */ function system_library() { - // Drupal's AJAX framework. + // Drupal's Ajax framework. $libraries['drupal.ajax'] = array( 'title' => 'Drupal AJAX', 'website' => 'http://api.drupal.org/api/drupal/includes--ajax.inc/group/ajax/7', @@ -1109,7 +1122,7 @@ 'title' => 'Drupal progress indicator', 'version' => VERSION, 'js' => array( - 'misc/progress.js' => array('group' => JS_DEFAULT, 'cache' => FALSE), + 'misc/progress.js' => array('group' => JS_DEFAULT), ), ); @@ -1169,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), ), ); @@ -1360,7 +1376,7 @@ ), ); $libraries['ui.mouse'] = array( - 'title' => 'jQuery UI: Droppable', + 'title' => 'jQuery UI: Mouse', 'website' => 'http://docs.jquery.com/UI/Mouse', 'version' => '1.8.7', 'js' => array( @@ -1734,16 +1750,27 @@ /** * Setup a given callback to run via authorize.php with elevated privileges. * - * To use authorize.php, certain variables must be stashed into $_SESSION. - * This function sets up all the necessary $_SESSION variables, then returns - * the full path to authorize.php so the caller can redirect to authorize.php. - * That initiates the workflow that will eventually lead to the callback being - * invoked. The callback will be invoked at a low bootstrap level, without all - * modules being invoked, so it needs to be careful not to assume any code - * exists. + * To use authorize.php, certain variables must be stashed into $_SESSION. This + * function sets up all the necessary $_SESSION variables. The calling function + * should then redirect to authorize.php, using the full path returned by + * system_authorized_get_url(). That initiates the workflow that will eventually + * lead to the callback being invoked. The callback will be invoked at a low + * bootstrap level, without all modules being invoked, so it needs to be careful + * not to assume any code exists. Example (system_authorized_run()): + * @code + * system_authorized_init($callback, $file, $arguments, $page_title); + * drupal_goto(system_authorized_get_url()); + * @endcode + * Example (update_manager_install_form_submit()): + * @code + * system_authorized_init('update_authorize_run_install', + * drupal_get_path('module', 'update') . '/update.authorize.inc', + * $arguments, t('Update manager')); + * $form_state['redirect'] = system_authorized_get_url(); + * @endcode * * @param $callback - * The name of the function to invoke one the user authorizes the operation. + * The name of the function to invoke once the user authorizes the operation. * @param $file * The full path to the file where the callback function is implemented. * @param $arguments @@ -1779,11 +1806,13 @@ * @param array $options * Optional array of options to pass to url(). * @return - * The full URL to authorize.php, using https if available. + * The full URL to authorize.php, using HTTPS if available. + * + * @see system_authorized_init() */ function system_authorized_get_url(array $options = array()) { global $base_url; - // Force https if available, regardless of what the caller specifies. + // Force HTTPS if available, regardless of what the caller specifies. $options['https'] = TRUE; // We prefix with $base_url so we get a full path even if clean URLs are // disabled. @@ -1791,6 +1820,13 @@ } /** + * Returns the URL for the authorize.php script when it is processing a batch. + */ +function system_authorized_batch_processing_url() { + return system_authorized_get_url(array('query' => array('batch' => '1'))); +} + +/** * Setup and invoke an operation using authorize.php. * * @see system_authorized_init() @@ -1807,7 +1843,7 @@ */ function system_authorized_batch_process() { $finish_url = system_authorized_get_url(); - $process_url = system_authorized_get_url(array('query' => array('batch' => '1'))); + $process_url = system_authorized_batch_processing_url(); batch_process($finish_url, $process_url); } @@ -1881,17 +1917,18 @@ // Ignore slave database servers for this request. // - // In Drupal's distributed database structure, new data is written to the master - // and then propagated to the slave servers. This means there is a lag - // between when data is written to the master and when it is available on the slave. - // At these times, we will want to avoid using a slave server temporarily. - // For example, if a user posts a new node then we want to disable the slave - // server for that user temporarily to allow the slave server to catch up. - // That way, that user will see their changes immediately while for other - // users we still get the benefits of having a slave server, just with slightly - // stale data. Code that wants to disable the slave server should use the - // db_set_ignore_slave() function to set $_SESSION['ignore_slave_server'] to - // the timestamp after which the slave can be re-enabled. + // In Drupal's distributed database structure, new data is written to the + // master and then propagated to the slave servers. This means there is a + // lag between when data is written to the master and when it is available on + // the slave. At these times, we will want to avoid using a slave server + // temporarily. For example, if a user posts a new node then we want to + // disable the slave server for that user temporarily to allow the slave + // server to catch up. That way, that user will see their changes immediately + // while for other users we still get the benefits of having a slave server, + // just with slightly stale data. Code that wants to disable the slave + // server should use the db_ignore_slave() function to set + // $_SESSION['ignore_slave_server'] to the timestamp after which the slave + // can be re-enabled. if (isset($_SESSION['ignore_slave_server'])) { if ($_SESSION['ignore_slave_server'] >= REQUEST_TIME) { Database::ignoreTarget('default', 'slave'); @@ -1996,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'); } @@ -2010,6 +2046,10 @@ 'info' => t('Main page content'), // Cached elsewhere. 'cache' => DRUPAL_NO_CACHE, + // Auto-enable in 'content' region by default, which always exists. + // @see system_themes_page(), drupal_render_page() + 'status' => 1, + 'region' => 'content', ); $blocks['powered-by'] = array( 'info' => t('Powered by Drupal'), @@ -2019,6 +2059,10 @@ $blocks['help'] = array( 'info' => t('System help'), 'weight' => '5', + 'cache' => DRUPAL_NO_CACHE, + // Auto-enable in 'help' region by default, if the theme defines one. + 'status' => 1, + 'region' => 'help', ); // System-defined menu blocks. foreach (menu_list_system_menus() as $menu_name => $title) { @@ -2327,14 +2371,14 @@ // Find modules $modules = drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.module$/', 'modules', 'name', 0); - // Include the install profile in modules that are loaded. + // Include the installation profile in modules that are loaded. $profile = drupal_get_profile(); $modules[$profile] = new stdClass(); $modules[$profile]->name = $profile; $modules[$profile]->uri = 'profiles/' . $profile . '/' . $profile . '.profile'; $modules[$profile]->filename = $profile . '.profile'; - // Install profile hooks are always executed last. + // Installation profile hooks are always executed last. $modules[$profile]->weight = 1000; // Set defaults for module info. @@ -2363,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'])) { @@ -2375,7 +2427,7 @@ $module->info['scripts'] = _system_info_add_path($module->info['scripts'], $path); } - // Install profiles are hidden by default, unless explicitly specified + // Installation profiles are hidden by default, unless explicitly specified // otherwise in the .info file. if ($key == $profile && !isset($modules[$key]->info['hidden'])) { $modules[$key]->info['hidden'] = TRUE; @@ -2387,9 +2439,14 @@ drupal_alter('system_info', $modules[$key]->info, $modules[$key], $type); } - // The install profile is required, if it's a valid module. if (isset($modules[$profile])) { + // The installation profile is required, if it's a valid module. $modules[$profile]->info['required'] = TRUE; + // Add a default distribution name if the profile did not provide one. This + // matches the default value used in install_profile_info(). + if (!isset($modules[$profile]->info['distribution_name'])) { + $modules[$profile]->info['distribution_name'] = 'Drupal'; + } } return $modules; @@ -2453,8 +2510,30 @@ function _system_rebuild_theme_data() { // Find themes $themes = drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.info$/', 'themes'); + // Allow modules to add further themes. + if ($module_themes = module_invoke_all('system_theme_info')) { + foreach ($module_themes as $name => $uri) { + // @see file_scan_directory() + $themes[$name] = (object) array( + 'uri' => $uri, + 'filename' => pathinfo($uri, PATHINFO_FILENAME), + 'name' => $name, + ); + } + } + // 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( @@ -2484,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'; @@ -2522,7 +2609,7 @@ // Now that we've established all our master themes, go back and fill in data // for subthemes. foreach ($sub_themes as $key) { - $themes[$key]->base_themes = system_find_base_themes($themes, $key); + $themes[$key]->base_themes = drupal_find_base_themes($themes, $key); // Don't proceed if there was a problem with the root base theme. if (!current($themes[$key]->base_themes)) { continue; @@ -2617,42 +2704,10 @@ /** * Find all the base themes for the specified theme. * - * Themes can inherit templates and function implementations from earlier themes. - * - * @param $themes - * An array of available themes. - * @param $key - * The name of the theme whose base we are looking for. - * @param $used_keys - * A recursion parameter preventing endless loops. - * @return - * Returns an array of all of the theme's ancestors; the first element's value - * will be NULL if an error occurred. + * This function has been deprecated in favor of drupal_find_base_themes(). */ function system_find_base_themes($themes, $key, $used_keys = array()) { - $base_key = $themes[$key]->info['base theme']; - // Does the base theme exist? - if (!isset($themes[$base_key])) { - return array($base_key => NULL); - } - - $current_base_theme = array($base_key => $themes[$base_key]->info['name']); - - // Is the base theme itself a child of another theme? - if (isset($themes[$base_key]->info['base theme'])) { - // Do we already know the base themes of this theme? - if (isset($themes[$base_key]->base_themes)) { - return $themes[$base_key]->base_themes + $current_base_theme; - } - // Prevent loops. - if (!empty($used_keys[$base_key])) { - return array($base_key => NULL); - } - $used_keys[$base_key] = TRUE; - return system_find_base_themes($themes, $base_key, $used_keys) + $current_base_theme; - } - // If we get here, then this is our parent theme. - return $current_base_theme; + return drupal_find_base_themes($themes, $key, $used_keys); } /** @@ -2663,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(); @@ -2677,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] = $label; + if ($labels) { + $list[$name] = t($label); + } + else { + $list[$name] = $name; + } } } - return $list; } @@ -2688,8 +2754,8 @@ * Implements hook_system_info_alter(). */ function system_system_info_alter(&$info, $file, $type) { - // Remove page-top from the blocks UI since it is reserved for modules to - // populate from outside the blocks system. + // Remove page-top and page-bottom from the blocks UI since they are reserved for + // modules to populate from outside the blocks system. if ($type == 'theme') { $info['regions_hidden'][] = 'page_top'; $info['regions_hidden'][] = 'page_bottom'; @@ -2701,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. @@ -2719,6 +2796,7 @@ * The form structure. * * @see system_settings_form_submit() + * * @ingroup forms */ function system_settings_form($form) { @@ -2737,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. @@ -2762,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']; } @@ -2818,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; @@ -2852,7 +2930,18 @@ } /** - * Determines if the current user is in compact mode. + * Determines whether the current user is in compact mode. + * + * Compact mode shows certain administration pages with less description text, + * such as the configuration page and the permissions page. + * + * Whether the user is in compact mode is determined by a cookie, which is set + * for the user by system_admin_compact_page(). + * + * If the user does not have the cookie, the default value is given by the + * system variable 'admin_compact_mode', which itself defaults to FALSE. This + * does not have a user interface to set it: it is a hidden variable which can + * be set in the settings.php file. * * @return * TRUE when in compact mode, FALSE when in expanded mode. @@ -2900,7 +2989,7 @@ ->condition('ml.hidden', 0, '>=') ->condition('ml.module', 'system') ->condition('m.number_parts', 1, '>') - ->condition('m.page_callback', 'system_admin_menu_block_page', '!='); + ->condition('m.page_callback', 'system_admin_menu_block_page', '<>'); foreach ($query->execute() as $link) { _menu_link_translate($link); if ($link['access']) { @@ -2992,8 +3081,20 @@ } } - $core = array('cache', '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); } @@ -3013,6 +3114,7 @@ ->fields(array( 'expire' => 0, )) + ->condition('expire', 0, '<>') ->condition('expire', REQUEST_TIME, '<') ->execute(); } @@ -3240,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, ); @@ -3277,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)); @@ -3342,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')) . ''; } /** @@ -3379,40 +3482,42 @@ /** * 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); if (!isset($destination)) { - $path = file_build_uri(basename($parsed_url['path'])); + $path = file_build_uri(drupal_basename($parsed_url['path'])); } else { if (is_dir(drupal_realpath($destination))) { // Prevent URIs with triple slashes when glueing parts together. - $path = str_replace('///', '//', "$destination/") . basename($parsed_url['path']); + $path = str_replace('///', '//', "$destination/") . drupal_basename($parsed_url['path']); } else { $path = $destination; @@ -3437,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; @@ -3451,7 +3555,7 @@ */ function system_run_automated_cron() { // If the site is not fully installed, suppress the automated cron run. - // Otherwise it could be triggered prematurely by AJAX requests during + // Otherwise it could be triggered prematurely by Ajax requests during // installation. if (($threshold = variable_get('cron_safe_threshold', DRUPAL_CRON_DEFAULT_THRESHOLD)) > 0 && variable_get('install_task') == 'done') { $cron_last = variable_get('cron_last', NULL); @@ -3741,7 +3845,7 @@ } // Get custom formats added to the database by the end user. - $result = db_query('SELECT df.dfid, df.format, df.type, df.locked, dfl.language FROM {date_formats} df LEFT JOIN {date_format_type} dft ON df.type = dft.type LEFT JOIN {date_format_locale} dfl ON df.format = dfl.format AND df.type = dfl.type ORDER BY df.type, df.format'); + $result = db_query('SELECT df.dfid, df.format, df.type, df.locked, dfl.language FROM {date_formats} df LEFT JOIN {date_format_locale} dfl ON df.format = dfl.format AND df.type = dfl.type ORDER BY df.type, df.format'); foreach ($result as $record) { // If this date type isn't set, initialise the array. if (!isset($date_formats[$record->type])) { @@ -3847,7 +3951,10 @@ drupal_write_record('date_formats', $info, $keys); } + // Retrieve an array of language objects for enabled languages. $languages = language_list('enabled'); + // This list is keyed off the value of $language->enabled; we want the ones + // that are enabled (value of 1). $languages = $languages[1]; $locale_format = array(); @@ -3858,7 +3965,7 @@ if (!empty($date_format['locales'])) { foreach ($date_format['locales'] as $langcode) { // Only proceed if language is enabled. - if (in_array($langcode, $languages)) { + if (isset($languages[$langcode])) { $is_existing = (bool) db_query_range('SELECT 1 FROM {date_format_locale} WHERE type = :type AND language = :language', 0, 1, array(':type' => $date_format['type'], ':language' => $langcode))->fetchField(); if (!$is_existing) { $locale_format['language'] = $langcode; diff -Naur drupal-7.0/modules/system/system.queue.inc drupal-7.66/modules/system/system.queue.inc --- drupal-7.0/modules/system/system.queue.inc 2011-01-03 19:03:54.000000000 +0100 +++ drupal-7.66/modules/system/system.queue.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ $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 @@ -316,6 +308,12 @@ */ protected $id_sequence; + /** + * Start working with a queue. + * + * @param $name + * Arbitrary string. The name of the queue to work with. + */ public function __construct($name) { $this->queue = array(); $this->id_sequence = 0; @@ -328,6 +326,7 @@ $item->created = time(); $item->expire = 0; $this->queue[$item->item_id] = $item; + return TRUE; } public function numberOfItems() { diff -Naur drupal-7.0/modules/system/system.tar.inc drupal-7.66/modules/system/system.tar.inc --- drupal-7.0/modules/system/system.tar.inc 2010-08-18 00:05:22.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 independant PHP extension load. Remember to take care + * 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.0/modules/system/system.test drupal-7.66/modules/system/system.test --- drupal-7.0/modules/system/system.test 2010-12-01 01:23:36.000000000 +0100 +++ drupal-7.66/modules/system/system.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,9 @@ prefixTables('{' . $base_table . '}') . '%'); if ($count) { - return $this->assertTrue($tables, t('Tables matching "@base_table" found.', array('@base_table' => $base_table))); + return $this->assertTrue($tables, format_string('Tables matching "@base_table" found.', array('@base_table' => $base_table))); + } + return $this->assertFalse($tables, format_string('Tables matching "@base_table" not found.', array('@base_table' => $base_table))); + } + + /** + * Assert that all tables defined in a module's hook_schema() exist. + * + * @param $module + * The name of the module. + */ + function assertModuleTablesExist($module) { + $tables = array_keys(drupal_get_schema_unprocessed($module)); + $tables_exist = TRUE; + foreach ($tables as $table) { + if (!db_table_exists($table)) { + $tables_exist = FALSE; + } + } + return $this->assertTrue($tables_exist, format_string('All database tables defined by the @module module exist.', array('@module' => $module))); + } + + /** + * Assert that none of the tables defined in a module's hook_schema() exist. + * + * @param $module + * The name of the module. + */ + function assertModuleTablesDoNotExist($module) { + $tables = array_keys(drupal_get_schema_unprocessed($module)); + $tables_exist = FALSE; + foreach ($tables as $table) { + if (db_table_exists($table)) { + $tables_exist = TRUE; + } } - return $this->assertFalse($tables, t('Tables matching "@base_table" not found.', array('@base_table' => $base_table))); + return $this->assertFalse($tables_exist, format_string('None of the database tables defined by the @module module exist.', array('@module' => $module))); } /** @@ -49,7 +87,7 @@ else { $message = 'Module "@module" is not enabled.'; } - $this->assertEqual(module_exists($module), $enabled, t($message, array('@module' => $module))); + $this->assertEqual(module_exists($module), $enabled, format_string($message, array('@module' => $module))); } } @@ -84,7 +122,7 @@ ->countQuery() ->execute() ->fetchField(); - $this->assertTrue($count > 0, t('watchdog table contains @count rows for @message', array('@count' => $count, '@message' => $message))); + $this->assertTrue($count > 0, format_string('watchdog table contains @count rows for @message', array('@count' => $count, '@message' => $message))); } } @@ -92,6 +130,8 @@ * Test module enabling/disabling functionality. */ class EnableDisableTestCase extends ModuleTestCase { + protected $profile = 'testing'; + public static function getInfo() { return array( 'name' => 'Enable/disable modules', @@ -101,75 +141,211 @@ } /** - * Enable a module, check the database for related tables, disable module, - * check for related tables, uninstall module, check for related tables. - * Also check for invocation of the hook_module_action hook. + * Test that all core modules can be enabled, disabled and uninstalled. */ function testEnableDisable() { - // Enable aggregator, and check tables. - $this->assertModules(array('aggregator'), FALSE); - $this->assertTableCount('aggregator', FALSE); + // Try to enable, disable and uninstall all core modules, unless they are + // hidden or required. + $modules = system_rebuild_module_data(); + foreach ($modules as $name => $module) { + if ($module->info['package'] != 'Core' || !empty($module->info['hidden']) || !empty($module->info['required'])) { + unset($modules[$name]); + } + } + $this->assertTrue(count($modules), format_string('Found @count core modules that we can try to enable in this test.', array('@count' => count($modules)))); - // Install (and enable) aggregator module. - $edit = array(); - $edit['modules[Core][aggregator][enable]'] = 'aggregator'; - $edit['modules[Core][forum][enable]'] = 'forum'; - $this->drupalPost('admin/modules', $edit, t('Save configuration')); - $this->assertText(t('The configuration options have been saved.'), t('Modules status has been updated.')); + // Enable the dblog module first, since we will be asserting the presence + // of log messages throughout the test. + if (isset($modules['dblog'])) { + $modules = array('dblog' => $modules['dblog']) + $modules; + } + + // Set a variable so that the hook implementations in system_test.module + // will display messages via drupal_set_message(). + variable_set('test_verbose_module_hooks', TRUE); + + // Throughout this test, some modules may be automatically enabled (due to + // dependencies). We'll keep track of them in an array, so we can handle + // them separately. + $automatically_enabled = array(); + + // Go through each module in the list and try to enable it (unless it was + // already enabled automatically due to a dependency). + foreach ($modules as $name => $module) { + if (empty($automatically_enabled[$name])) { + // Start a list of modules that we expect to be enabled this time. + $modules_to_enable = array($name); + + // Find out if the module has any dependencies that aren't enabled yet; + // if so, add them to the list of modules we expect to be automatically + // enabled. + foreach (array_keys($module->requires) as $dependency) { + if (isset($modules[$dependency]) && empty($automatically_enabled[$dependency])) { + $modules_to_enable[] = $dependency; + $automatically_enabled[$dependency] = TRUE; + } + } - // Check that hook_modules_installed and hook_modules_enabled hooks were invoked and check tables. - $this->assertText(t('hook_modules_installed fired for aggregator'), t('hook_modules_installed fired.')); - $this->assertText(t('hook_modules_enabled fired for aggregator'), t('hook_modules_enabled fired.')); - $this->assertModules(array('aggregator'), TRUE); - $this->assertTableCount('aggregator', TRUE); - $this->assertLogMessage('system', "%module module installed.", array('%module' => 'aggregator'), WATCHDOG_INFO); - $this->assertLogMessage('system', "%module module enabled.", array('%module' => 'aggregator'), WATCHDOG_INFO); + // Check that each module is not yet enabled and does not have any + // database tables yet. + foreach ($modules_to_enable as $module_to_enable) { + $this->assertModules(array($module_to_enable), FALSE); + $this->assertModuleTablesDoNotExist($module_to_enable); + } - // Disable aggregator, check tables, uninstall aggregator, check tables. - $edit = array(); - $edit['modules[Core][aggregator][enable]'] = FALSE; - $this->drupalPost('admin/modules', $edit, t('Save configuration')); - $this->assertText(t('The configuration options have been saved.'), t('Modules status has been updated.')); + // Install and enable the module. + $edit = array(); + $edit['modules[Core][' . $name . '][enable]'] = $name; + $this->drupalPost('admin/modules', $edit, t('Save configuration')); + // Handle the case where modules were installed along with this one and + // where we therefore hit a confirmation screen. + if (count($modules_to_enable) > 1) { + $this->drupalPost(NULL, array(), t('Continue')); + } + $this->assertText(t('The configuration options have been saved.'), 'Modules status has been updated.'); - // Check that hook_modules_disabled hook was invoked and check tables. - $this->assertText(t('hook_modules_disabled fired for aggregator'), t('hook_modules_disabled fired.')); - $this->assertModules(array('aggregator'), FALSE); - $this->assertTableCount('aggregator', TRUE); - $this->assertLogMessage('system', "%module module disabled.", array('%module' => 'aggregator'), WATCHDOG_INFO); + // Check that hook_modules_installed() and hook_modules_enabled() were + // invoked with the expected list of modules, that each module's + // database tables now exist, and that appropriate messages appear in + // the logs. + foreach ($modules_to_enable as $module_to_enable) { + $this->assertText(t('hook_modules_installed fired for @module', array('@module' => $module_to_enable))); + $this->assertText(t('hook_modules_enabled fired for @module', array('@module' => $module_to_enable))); + $this->assertModules(array($module_to_enable), TRUE); + $this->assertModuleTablesExist($module_to_enable); + $this->assertLogMessage('system', "%module module installed.", array('%module' => $module_to_enable), WATCHDOG_INFO); + $this->assertLogMessage('system', "%module module enabled.", array('%module' => $module_to_enable), WATCHDOG_INFO); + } - // Uninstall the module. - $edit = array(); - $edit['uninstall[aggregator]'] = 'aggregator'; - $this->drupalPost('admin/modules/uninstall', $edit, t('Uninstall')); + // Disable and uninstall the original module, and check appropriate + // hooks, tables, and log messages. (Later, we'll go back and do the + // same thing for modules that were enabled automatically.) Skip this + // for the dblog module, because that is needed for the test; we'll go + // back and do that one at the end also. + if ($name != 'dblog') { + $this->assertSuccessfulDisableAndUninstall($name); + } + } + } - $this->drupalPost(NULL, NULL, t('Uninstall')); - $this->assertText(t('The selected modules have been uninstalled.'), t('Modules status has been updated.')); + // Go through all modules that were automatically enabled, and try to + // disable and uninstall them one by one. + while (!empty($automatically_enabled)) { + $initial_count = count($automatically_enabled); + foreach (array_keys($automatically_enabled) as $name) { + // If the module can't be disabled due to dependencies, skip it and try + // again the next time. Otherwise, try to disable it. + $this->drupalGet('admin/modules'); + $disabled_checkbox = $this->xpath('//input[@type="checkbox" and @disabled="disabled" and @name="modules[Core][' . $name . '][enable]"]'); + if (empty($disabled_checkbox) && $name != 'dblog') { + unset($automatically_enabled[$name]); + $this->assertSuccessfulDisableAndUninstall($name); + } + } + $final_count = count($automatically_enabled); + // If all checkboxes were disabled, something is really wrong with the + // test. Throw a failure and avoid an infinite loop. + if ($initial_count == $final_count) { + $this->fail(t('Remaining modules could not be disabled.')); + break; + } + } - // Check that hook_modules_uninstalled hook was invoked and check tables. - $this->assertText(t('hook_modules_uninstalled fired for aggregator'), t('hook_modules_uninstalled fired.')); - $this->assertModules(array('aggregator'), FALSE); - $this->assertTableCount('aggregator', FALSE); - $this->assertLogMessage('system', "%module module uninstalled.", array('%module' => 'aggregator'), WATCHDOG_INFO); + // Disable and uninstall the dblog module last, since we needed it for + // assertions in all the above tests. + if (isset($modules['dblog'])) { + $this->assertSuccessfulDisableAndUninstall('dblog'); + } - // Reinstall (and enable) aggregator module. + // Now that all modules have been tested, go back and try to enable them + // all again at once. This tests two things: + // - That each module can be successfully enabled again after being + // uninstalled. + // - That enabling more than one module at the same time does not lead to + // any errors. $edit = array(); - $edit['modules[Core][aggregator][enable]'] = 'aggregator'; + foreach (array_keys($modules) as $name) { + $edit['modules[Core][' . $name . '][enable]'] = $name; + } $this->drupalPost('admin/modules', $edit, t('Save configuration')); - $this->assertText(t('The configuration options have been saved.'), t('Modules status has been updated.')); + $this->assertText(t('The configuration options have been saved.'), 'Modules status has been updated.'); + } + + /** + * Ensures entity info cache is updated after changes. + */ + function testEntityInfoChanges() { + module_enable(array('entity_cache_test')); + $entity_info = entity_get_info(); + $this->assertTrue(isset($entity_info['entity_cache_test']), 'Test entity type found.'); + + // Change the label of the test entity type and make sure changes appear + // after flushing caches. + variable_set('entity_cache_test_label', 'New label.'); + drupal_flush_all_caches(); + $info = entity_get_info('entity_cache_test'); + $this->assertEqual($info['label'], 'New label.', 'New label appears in entity info.'); + + // Disable the providing module and make sure the entity type is gone. + module_disable(array('entity_cache_test', 'entity_cache_test_dependency')); + $entity_info = entity_get_info(); + $this->assertFalse(isset($entity_info['entity_cache_test']), 'Entity type of the providing module is gone.'); } /** - * Tests entity cache after enabling a module with a dependency on an enitity - * providing module. + * Tests entity info cache after enabling a module with a dependency on an entity providing module. * * @see entity_cache_test_watchdog() */ - function testEntityCache() { + function testEntityInfoCacheWatchdog() { module_enable(array('entity_cache_test')); $info = variable_get('entity_cache_test'); $this->assertEqual($info['label'], 'Entity Cache Test', 'Entity info label is correct.'); $this->assertEqual($info['controller class'], 'DrupalDefaultEntityController', 'Entity controller class info is correct.'); } + + /** + * Disables and uninstalls a module and asserts that it was done correctly. + * + * @param $module + * The name of the module to disable and uninstall. + */ + function assertSuccessfulDisableAndUninstall($module) { + // Disable the module. + $edit = array(); + $edit['modules[Core][' . $module . '][enable]'] = FALSE; + $this->drupalPost('admin/modules', $edit, t('Save configuration')); + $this->assertText(t('The configuration options have been saved.'), 'Modules status has been updated.'); + $this->assertModules(array($module), FALSE); + + // Check that the appropriate hook was fired and the appropriate log + // message appears. + $this->assertText(t('hook_modules_disabled fired for @module', array('@module' => $module))); + $this->assertLogMessage('system', "%module module disabled.", array('%module' => $module), WATCHDOG_INFO); + + // Check that the module's database tables still exist. + $this->assertModuleTablesExist($module); + + // Uninstall the module. + $edit = array(); + $edit['uninstall[' . $module . ']'] = $module; + $this->drupalPost('admin/modules/uninstall', $edit, t('Uninstall')); + $this->drupalPost(NULL, NULL, t('Uninstall')); + $this->assertText(t('The selected modules have been uninstalled.'), 'Modules status has been updated.'); + $this->assertModules(array($module), FALSE); + + // Check that the appropriate hook was fired and the appropriate log + // message appears. (But don't check for the log message if the dblog + // module was just uninstalled, since the {watchdog} table won't be there + // anymore.) + $this->assertText(t('hook_modules_uninstalled fired for @module', array('@module' => $module))); + if ($module != 'dblog') { + $this->assertLogMessage('system', "%module module uninstalled.", array('%module' => $module), WATCHDOG_INFO); + } + + // Check that the module's database tables no longer exist. + $this->assertModuleTablesDoNotExist($module); + } } /** @@ -192,11 +368,11 @@ // Attempt to install the requirements1_test module. $edit = array(); - $edit['modules[Core][requirements1_test][enable]'] = 'requirements1_test'; + $edit['modules[Testing][requirements1_test][enable]'] = 'requirements1_test'; $this->drupalPost('admin/modules', $edit, t('Save configuration')); // Makes sure the module was NOT installed. - $this->assertText(t('Requirements 1 Test failed requirements'), t('Modules status has been updated.')); + $this->assertText(t('Requirements 1 Test failed requirements'), 'Modules status has been updated.'); $this->assertModules(array('requirements1_test'), FALSE); } } @@ -214,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() { @@ -221,7 +409,7 @@ $edit = array(); $edit['modules[Core][translation][enable]'] = 'translation'; $this->drupalPost('admin/modules', $edit, t('Save configuration')); - $this->assertText(t('Some required modules must be enabled'), t('Dependency required.')); + $this->assertText(t('Some required modules must be enabled'), 'Dependency required.'); $this->assertModules(array('translation', 'locale'), FALSE); @@ -230,7 +418,7 @@ $this->assertTableCount('locale', FALSE); $this->drupalPost(NULL, NULL, t('Continue')); - $this->assertText(t('The configuration options have been saved.'), t('Modules status has been updated.')); + $this->assertText(t('The configuration options have been saved.'), 'Modules status has been updated.'); $this->assertModules(array('translation', 'locale'), TRUE); @@ -246,9 +434,9 @@ // Test that the system_dependencies_test module is marked // as missing a dependency. $this->drupalGet('admin/modules'); - $this->assertRaw(t('@module (missing)', array('@module' => drupal_ucfirst('_missing_dependency'))), t('A module with missing dependencies is marked as such.')); + $this->assertRaw(t('@module (missing)', array('@module' => drupal_ucfirst('_missing_dependency'))), 'A module with missing dependencies is marked as such.'); $checkbox = $this->xpath('//input[@type="checkbox" and @disabled="disabled" and @name="modules[Testing][system_dependencies_test][enable]"]'); - $this->assert(count($checkbox) == 1, t('Checkbox for the module is disabled.')); + $this->assert(count($checkbox) == 1, 'Checkbox for the module is disabled.'); // Force enable the system_dependencies_test module. module_enable(array('system_dependencies_test'), FALSE); @@ -256,7 +444,7 @@ // Verify that the module is forced to be disabled when submitting // the module page. $this->drupalPost('admin/modules', array(), t('Save configuration')); - $this->assertText(t('The @module module is missing, so the following module will be disabled: @depends.', array('@module' => '_missing_dependency', '@depends' => 'system_dependencies_test')), t('The module missing dependencies will be disabled.')); + $this->assertText(t('The @module module is missing, so the following module will be disabled: @depends.', array('@module' => '_missing_dependency', '@depends' => 'system_dependencies_test')), 'The module missing dependencies will be disabled.'); // Confirm. $this->drupalPost(NULL, NULL, t('Continue')); @@ -266,6 +454,35 @@ } /** + * Tests enabling a module that depends on an incompatible version of a module. + */ + function testIncompatibleModuleVersionDependency() { + // Test that the system_incompatible_module_version_dependencies_test is + // marked as having an incompatible dependency. + $this->drupalGet('admin/modules'); + $this->assertRaw(t('@module (incompatible with version @version)', array( + '@module' => 'System incompatible module version test (>2.0)', + '@version' => '1.0', + )), 'A module that depends on an incompatible version of a module is marked as such.'); + $checkbox = $this->xpath('//input[@type="checkbox" and @disabled="disabled" and @name="modules[Testing][system_incompatible_module_version_dependencies_test][enable]"]'); + $this->assert(count($checkbox) == 1, 'Checkbox for the module is disabled.'); + } + + /** + * Tests enabling a module that depends on a module with an incompatible core version. + */ + function testIncompatibleCoreVersionDependency() { + // Test that the system_incompatible_core_version_dependencies_test is + // marked as having an incompatible dependency. + $this->drupalGet('admin/modules'); + $this->assertRaw(t('@module (incompatible with this version of Drupal core)', array( + '@module' => 'System incompatible core version test', + )), 'A module that depends on a module with an incompatible core version is marked as such.'); + $checkbox = $this->xpath('//input[@type="checkbox" and @disabled="disabled" and @name="modules[Testing][system_incompatible_core_version_dependencies_test][enable]"]'); + $this->assert(count($checkbox) == 1, 'Checkbox for the module is disabled.'); + } + + /** * Tests enabling a module that depends on a module which fails hook_requirements(). */ function testEnableRequirementsFailureDependency() { @@ -274,12 +491,12 @@ // Attempt to install both modules at the same time. $edit = array(); - $edit['modules[Core][requirements1_test][enable]'] = 'requirements1_test'; - $edit['modules[Core][requirements2_test][enable]'] = 'requirements2_test'; + $edit['modules[Testing][requirements1_test][enable]'] = 'requirements1_test'; + $edit['modules[Testing][requirements2_test][enable]'] = 'requirements2_test'; $this->drupalPost('admin/modules', $edit, t('Save configuration')); // Makes sure the modules were NOT installed. - $this->assertText(t('Requirements 1 Test failed requirements'), t('Modules status has been updated.')); + $this->assertText(t('Requirements 1 Test failed requirements'), 'Modules status has been updated.'); $this->assertModules(array('requirements1_test'), FALSE); $this->assertModules(array('requirements2_test'), FALSE); @@ -290,39 +507,6 @@ } /** - * Tests re-enabling forum with taxonomy disabled. - */ - function testEnableForumTaxonomyFieldDependency() { - // Enable the forum module. - $edit = array(); - $edit['modules[Core][forum][enable]'] = 'forum'; - $this->drupalPost('admin/modules', $edit, t('Save configuration')); - $this->assertModules(array('forum'), TRUE); - - // Disable the forum module. - $edit = array(); - $edit['modules[Core][forum][enable]'] = FALSE; - $this->drupalPost('admin/modules', $edit, t('Save configuration')); - $this->assertModules(array('forum'), FALSE); - - // Disable the taxonomy module. - $edit = array(); - $edit['modules[Core][taxonomy][enable]'] = FALSE; - $this->drupalPost('admin/modules', $edit, t('Save configuration')); - $this->assertModules(array('taxonomy'), FALSE); - - // Attempt to re-enable the forum module with taxonomy disabled and ensure - // forum does not try to recreate the taxonomy_forums field. - $edit = array(); - $edit['modules[Core][forum][enable]'] = 'forum'; - $this->drupalPost('admin/modules', $edit, t('Save configuration')); - $this->assertText(t('Some required modules must be enabled'), t('Dependency required.')); - $this->drupalPost(NULL, NULL, t('Continue')); - $this->assertText(t('The configuration options have been saved.'), t('Modules status has been updated.')); - $this->assertModules(array('taxonomy', 'forum'), TRUE); - } - - /** * Tests that module dependencies are enabled in the correct order via the * UI. Dependencies should be enabled before their dependents. */ @@ -360,29 +544,57 @@ $this->drupalPost('admin/modules', $edit, t('Save configuration')); $this->assertModules(array('forum'), TRUE); - // Disable forum and taxonomy. Both should now be installed but disabled. + // Disable forum and comment. Both should now be installed but disabled. $edit = array('modules[Core][forum][enable]' => FALSE); $this->drupalPost('admin/modules', $edit, t('Save configuration')); $this->assertModules(array('forum'), FALSE); - $edit = array('modules[Core][taxonomy][enable]' => FALSE); + $edit = array('modules[Core][comment][enable]' => FALSE); $this->drupalPost('admin/modules', $edit, t('Save configuration')); - $this->assertModules(array('taxonomy'), FALSE); + $this->assertModules(array('comment'), FALSE); // Check that the taxonomy module cannot be uninstalled. $this->drupalGet('admin/modules/uninstall'); - $checkbox = $this->xpath('//input[@type="checkbox" and @disabled="disabled" and @name="uninstall[taxonomy]"]'); - $this->assert(count($checkbox) == 1, t('Checkbox for uninstalling the taxonomy module is disabled.')); + $checkbox = $this->xpath('//input[@type="checkbox" and @disabled="disabled" and @name="uninstall[comment]"]'); + $this->assert(count($checkbox) == 1, 'Checkbox for uninstalling the comment module is disabled.'); // Uninstall the forum module, and check that taxonomy now can also be // uninstalled. $edit = array('uninstall[forum]' => 'forum'); $this->drupalPost('admin/modules/uninstall', $edit, t('Uninstall')); $this->drupalPost(NULL, NULL, t('Uninstall')); - $this->assertText(t('The selected modules have been uninstalled.'), t('Modules status has been updated.')); - $edit = array('uninstall[taxonomy]' => 'taxonomy'); + $this->assertText(t('The selected modules have been uninstalled.'), 'Modules status has been updated.'); + $edit = array('uninstall[comment]' => 'comment'); $this->drupalPost('admin/modules/uninstall', $edit, t('Uninstall')); $this->drupalPost(NULL, NULL, t('Uninstall')); - $this->assertText(t('The selected modules have been uninstalled.'), t('Modules status has been updated.')); + $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.'); } } @@ -471,7 +683,7 @@ if (!empty($info['required'])) { $field_name = "modules[{$info['package']}][$module][enable]"; if (empty($info['hidden'])) { - $this->assertFieldByXPath("//input[@name='$field_name' and @disabled='disabled' and @checked='checked']", '', t('Field @name was disabled and checked.', array('@name' => $field_name))); + $this->assertFieldByXPath("//input[@name='$field_name' and @disabled='disabled' and @checked='checked']", '', format_string('Field @name was disabled and checked.', array('@name' => $field_name))); } else { $this->assertNoFieldByName($field_name); @@ -514,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.')); @@ -522,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.')); @@ -558,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 { @@ -572,6 +803,10 @@ ); } + function setUp() { + parent::setUp(array('common_test', 'common_test_cron_helper')); + } + /** * Test cron runs. */ @@ -607,14 +842,14 @@ variable_set('cron_last', $cron_last); variable_set('cron_safe_threshold', $cron_safe_threshold); $this->drupalGet(''); - $this->assertTrue($cron_last == variable_get('cron_last', NULL), t('Cron does not run when the cron threshold is not passed.')); + $this->assertTrue($cron_last == variable_get('cron_last', NULL), 'Cron does not run when the cron threshold is not passed.'); // Test if cron runs when the cron threshold was passed. $cron_last = time() - 200; variable_set('cron_last', $cron_last); $this->drupalGet(''); sleep(1); - $this->assertTrue($cron_last < variable_get('cron_last', NULL), t('Cron runs when the cron threshold is passed.')); + $this->assertTrue($cron_last < variable_get('cron_last', NULL), 'Cron runs when the cron threshold is passed.'); // Disable the cron threshold through the interface. $admin_user = $this->drupalCreateUser(array('administer site configuration')); @@ -627,7 +862,7 @@ $cron_last = time() - 200; variable_set('cron_last', $cron_last); $this->drupalGet(''); - $this->assertTrue($cron_last == variable_get('cron_last', NULL), t('Cron does not run when the cron threshold is disabled.')); + $this->assertTrue($cron_last == variable_get('cron_last', NULL), 'Cron does not run when the cron threshold is disabled.'); } /** @@ -647,7 +882,7 @@ )) ->condition('fid', $temp_old->fid) ->execute(); - $this->assertTrue(file_exists($temp_old->uri), t('Old temp file was created correctly.')); + $this->assertTrue(file_exists($temp_old->uri), 'Old temp file was created correctly.'); // Temporary file that is less than DRUPAL_MAXIMUM_TEMP_FILE_AGE. $temp_new = file_save_data(''); @@ -655,7 +890,7 @@ ->fields(array('status' => 0)) ->condition('fid', $temp_new->fid) ->execute(); - $this->assertTrue(file_exists($temp_new->uri), t('New temp file was created correctly.')); + $this->assertTrue(file_exists($temp_new->uri), 'New temp file was created correctly.'); // Permanent file that is older than DRUPAL_MAXIMUM_TEMP_FILE_AGE. $perm_old = file_save_data(''); @@ -663,19 +898,110 @@ ->fields(array('timestamp' => 1)) ->condition('fid', $temp_old->fid) ->execute(); - $this->assertTrue(file_exists($perm_old->uri), t('Old permanent file was created correctly.')); + $this->assertTrue(file_exists($perm_old->uri), 'Old permanent file was created correctly.'); // Permanent file that is newer than DRUPAL_MAXIMUM_TEMP_FILE_AGE. $perm_new = file_save_data(''); - $this->assertTrue(file_exists($perm_new->uri), t('New permanent file was created correctly.')); + $this->assertTrue(file_exists($perm_new->uri), 'New permanent file was created correctly.'); // Run cron and then ensure that only the old, temp file was deleted. $this->cronRun(); - $this->assertFalse(file_exists($temp_old->uri), t('Old temp file was correctly removed.')); - $this->assertTrue(file_exists($temp_new->uri), t('New temp file was correctly ignored.')); - $this->assertTrue(file_exists($perm_old->uri), t('Old permanent file was correctly ignored.')); - $this->assertTrue(file_exists($perm_new->uri), t('New permanent file was correctly ignored.')); + $this->assertFalse(file_exists($temp_old->uri), 'Old temp file was correctly removed.'); + $this->assertTrue(file_exists($temp_new->uri), 'New temp file was correctly ignored.'); + $this->assertTrue(file_exists($perm_old->uri), 'Old permanent file was correctly ignored.'); + $this->assertTrue(file_exists($perm_new->uri), 'New permanent file was correctly ignored.'); } + + /** + * Make sure exceptions thrown on hook_cron() don't affect other modules. + */ + function testCronExceptions() { + variable_del('common_test_cron'); + // The common_test module throws an exception. If it isn't caught, the tests + // won't finish successfully. + // The common_test_cron_helper module sets the 'common_test_cron' variable. + $this->cronRun(); + $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 { @@ -697,7 +1023,7 @@ list($version, ) = explode('.', VERSION); $string = ''; $this->drupalGet('node'); - $this->assertRaw($string, t('Fingerprinting meta tag generated correctly.'), t('System')); + $this->assertRaw($string, 'Fingerprinting meta tag generated correctly.', 'System'); } } @@ -724,7 +1050,7 @@ function testAccessDenied() { $this->drupalGet('admin'); - $this->assertText(t('Access denied'), t('Found the default 403 page')); + $this->assertText(t('Access denied'), 'Found the default 403 page'); $this->assertResponse(403); $this->drupalLogin($this->admin_user); @@ -739,14 +1065,14 @@ $this->drupalLogout(); $this->drupalGet('admin'); - $this->assertText($node->title, t('Found the custom 403 page')); + $this->assertText($node->title, 'Found the custom 403 page'); // Logout and check that the user login block is shown on custom 403 pages. $this->drupalLogout(); $this->drupalGet('admin'); - $this->assertText($node->title, t('Found the custom 403 page')); - $this->assertText(t('User login'), t('Blocks are shown on the custom 403 page')); + $this->assertText($node->title, 'Found the custom 403 page'); + $this->assertText(t('User login'), 'Blocks are shown on the custom 403 page'); // Log back in and remove the custom 403 page. $this->drupalLogin($this->admin_user); @@ -756,9 +1082,9 @@ $this->drupalLogout(); $this->drupalGet('admin'); - $this->assertText(t('Access denied'), t('Found the default 403 page')); + $this->assertText(t('Access denied'), 'Found the default 403 page'); $this->assertResponse(403); - $this->assertText(t('User login'), t('Blocks are shown on the default 403 page')); + $this->assertText(t('User login'), 'Blocks are shown on the default 403 page'); // Log back in, set the custom 403 page to /user and remove the block $this->drupalLogin($this->admin_user); @@ -805,7 +1131,7 @@ function testPageNotFound() { $this->drupalGet($this->randomName(10)); - $this->assertText(t('Page not found'), t('Found the default 404 page')); + $this->assertText(t('Page not found'), 'Found the default 404 page'); $edit = array( 'title' => $this->randomName(10), @@ -813,11 +1139,16 @@ ); $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')); $this->drupalGet($this->randomName(10)); - $this->assertText($node->title, t('Found the custom 404 page')); + $this->assertText($node->title, 'Found the custom 404 page'); } } @@ -860,7 +1191,7 @@ $offline_message = t('@site is currently under maintenance. We should be back shortly. Thank you for your patience.', array('@site' => variable_get('site_name', 'Drupal'))); $this->drupalGet(''); - $this->assertRaw($admin_message, t('Found the site maintenance mode message.')); + $this->assertRaw($admin_message, 'Found the site maintenance mode message.'); // Logout and verify that offline message is displayed. $this->drupalLogout(); @@ -890,7 +1221,7 @@ $this->drupalLogout(); $this->drupalLogin($this->admin_user); $this->drupalGet('admin/config/development/maintenance'); - $this->assertNoRaw($admin_message, t('Site maintenance mode message not displayed.')); + $this->assertNoRaw($admin_message, 'Site maintenance mode message not displayed.'); $offline_message = 'Sorry, not online.'; $edit = array( @@ -901,11 +1232,11 @@ // Logout and verify that custom site offline message is displayed. $this->drupalLogout(); $this->drupalGet(''); - $this->assertRaw($offline_message, t('Found the site offline message.')); + $this->assertRaw($offline_message, 'Found the site offline message.'); // Verify that custom site offline message is not displayed on user/password. $this->drupalGet('user/password'); - $this->assertText(t('Username or e-mail address'), t('Anonymous users can access user/password')); + $this->assertText(t('Username or e-mail address'), 'Anonymous users can access user/password'); // Submit password reset form. $edit = array( @@ -935,7 +1266,7 @@ } function setUp() { - parent::setUp(); + parent::setUp(array('locale')); // Create admin user and log in admin user. $this->admin_user = $this->drupalCreateUser(array('administer site configuration')); @@ -960,18 +1291,18 @@ // Confirm date format and time zone. $this->drupalGet("node/$node1->nid"); - $this->assertText('2007-01-31 21:00:00 -1000', t('Date should be identical, with GMT offset of -10 hours.')); + $this->assertText('2007-01-31 21:00:00 -1000', 'Date should be identical, with GMT offset of -10 hours.'); $this->drupalGet("node/$node2->nid"); - $this->assertText('2007-07-31 21:00:00 -1000', t('Date should be identical, with GMT offset of -10 hours.')); + $this->assertText('2007-07-31 21:00:00 -1000', 'Date should be identical, with GMT offset of -10 hours.'); // Set time zone to Los Angeles time. variable_set('date_default_timezone', 'America/Los_Angeles'); // Confirm date format and time zone. $this->drupalGet("node/$node1->nid"); - $this->assertText('2007-01-31 23:00:00 -0800', t('Date should be two hours ahead, with GMT offset of -8 hours.')); + $this->assertText('2007-01-31 23:00:00 -0800', 'Date should be two hours ahead, with GMT offset of -8 hours.'); $this->drupalGet("node/$node2->nid"); - $this->assertText('2007-08-01 00:00:00 -0700', t('Date should be three hours ahead, with GMT offset of -7 hours.')); + $this->assertText('2007-08-01 00:00:00 -0700', 'Date should be three hours ahead, with GMT offset of -7 hours.'); } /** @@ -994,7 +1325,7 @@ 'date_format' => $date_format, ); $this->drupalPost('admin/config/regional/date-time/types/add', $edit, t('Add date type')); - $this->assertEqual($this->getUrl(), url('admin/config/regional/date-time', array('absolute' => TRUE)), t('Correct page redirection.')); + $this->assertEqual($this->getUrl(), url('admin/config/regional/date-time', array('absolute' => TRUE)), 'Correct page redirection.'); $this->assertText(t('New date type added successfully.'), 'Date type added confirmation message appears.'); $this->assertText($date_type, 'Custom date type appears in the date type list.'); $this->assertText(t('delete'), 'Delete link for custom date type appears.'); @@ -1002,7 +1333,7 @@ // Delete custom date type. $this->clickLink(t('delete')); $this->drupalPost('admin/config/regional/date-time/types/' . $machine_name . '/delete', array(), t('Remove')); - $this->assertEqual($this->getUrl(), url('admin/config/regional/date-time', array('absolute' => TRUE)), t('Correct page redirection.')); + $this->assertEqual($this->getUrl(), url('admin/config/regional/date-time', array('absolute' => TRUE)), 'Correct page redirection.'); $this->assertText(t('Removed date type ' . $date_type), 'Custom date type removed.'); } @@ -1020,7 +1351,7 @@ 'date_format' => 'Y', ); $this->drupalPost('admin/config/regional/date-time/formats/add', $edit, t('Add format')); - $this->assertEqual($this->getUrl(), url('admin/config/regional/date-time/formats', array('absolute' => TRUE)), t('Correct page redirection.')); + $this->assertEqual($this->getUrl(), url('admin/config/regional/date-time/formats', array('absolute' => TRUE)), 'Correct page redirection.'); $this->assertNoText(t('No custom date formats available.'), 'No custom date formats message does not appear.'); $this->assertText(t('Custom date format added.'), 'Custom date format added.'); @@ -1035,15 +1366,153 @@ 'date_format' => 'Y m', ); $this->drupalPost($this->getUrl(), $edit, t('Save format')); - $this->assertEqual($this->getUrl(), url('admin/config/regional/date-time/formats', array('absolute' => TRUE)), t('Correct page redirection.')); + $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)), t('Correct page redirection.')); + $this->assertEqual($this->getUrl(), url('admin/config/regional/date-time/formats', array('absolute' => TRUE)), 'Correct page redirection.'); $this->assertText(t('Removed date format'), 'Custom date format removed successfully.'); } + + /** + * Test if the date formats are stored properly. + */ + function testDateFormatStorage() { + $date_format = array( + 'type' => 'short', + 'format' => 'dmYHis', + 'locked' => 0, + 'is_new' => 1, + ); + system_date_format_save($date_format); + + $format = db_select('date_formats', 'df') + ->fields('df', array('format')) + ->condition('type', 'short') + ->condition('format', 'dmYHis') + ->execute() + ->fetchField(); + $this->verbose($format); + $this->assertEqual('dmYHis', $format, 'Unlocalized date format resides in general table.'); + + $format = db_select('date_format_locale', 'dfl') + ->fields('dfl', array('format')) + ->condition('type', 'short') + ->condition('format', 'dmYHis') + ->execute() + ->fetchField(); + $this->assertFalse($format, 'Unlocalized date format resides not in localized table.'); + + // Enable German language + locale_add_language('de', NULL, NULL, LANGUAGE_LTR, '', '', TRUE, 'en'); + + $date_format = array( + 'type' => 'short', + 'format' => 'YMDHis', + 'locales' => array('de', 'tr'), + 'locked' => 0, + 'is_new' => 1, + ); + system_date_format_save($date_format); + + $format = db_select('date_format_locale', 'dfl') + ->fields('dfl', array('format')) + ->condition('type', 'short') + ->condition('format', 'YMDHis') + ->condition('language', 'de') + ->execute() + ->fetchField(); + $this->assertEqual('YMDHis', $format, 'Localized date format resides in localized table.'); + + $format = db_select('date_formats', 'df') + ->fields('df', array('format')) + ->condition('type', 'short') + ->condition('format', 'YMDHis') + ->execute() + ->fetchField(); + $this->assertEqual('YMDHis', $format, 'Localized date format resides in general table too.'); + + $format = db_select('date_format_locale', 'dfl') + ->fields('dfl', array('format')) + ->condition('type', 'short') + ->condition('format', 'YMDHis') + ->condition('language', 'tr') + ->execute() + ->fetchColumn(); + $this->assertFalse($format, 'Localized date format for disabled language is ignored.'); + } +} + +/** + * 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 { @@ -1090,11 +1559,11 @@ // drupal_set_title's $filter is CHECK_PLAIN by default, so the title should be // returned with check_plain(). drupal_set_title($title, CHECK_PLAIN); - $this->assertTrue(strpos(drupal_get_title(), '') === FALSE, t('Tags in title converted to entities when $output is CHECK_PLAIN.')); + $this->assertTrue(strpos(drupal_get_title(), '') === FALSE, 'Tags in title converted to entities when $output is CHECK_PLAIN.'); // drupal_set_title's $filter is passed as PASS_THROUGH, so the title should be // returned with HTML. drupal_set_title($title, PASS_THROUGH); - $this->assertTrue(strpos(drupal_get_title(), '') !== FALSE, t('Tags in title are not converted to entities when $output is PASS_THROUGH.')); + $this->assertTrue(strpos(drupal_get_title(), '') !== FALSE, 'Tags in title are not converted to entities when $output is PASS_THROUGH.'); // Generate node content. $langcode = LANGUAGE_NONE; $edit = array( @@ -1180,11 +1649,11 @@ */ function testDrupalIsFrontPage() { $this->drupalGet(''); - $this->assertText(t('On front page.'), t('Path is the front page.')); + $this->assertText(t('On front page.'), 'Path is the front page.'); $this->drupalGet('node'); - $this->assertText(t('On front page.'), t('Path is the front page.')); + $this->assertText(t('On front page.'), 'Path is the front page.'); $this->drupalGet($this->node_path); - $this->assertNoText(t('On front page.'), t('Path is not the front page.')); + $this->assertNoText(t('On front page.'), 'Path is not the front page.'); // Change the front page to an invalid path. $edit = array('site_frontpage' => 'kittens'); @@ -1194,18 +1663,20 @@ // Change the front page to a valid path. $edit['site_frontpage'] = $this->node_path; $this->drupalPost('admin/config/system/site-information', $edit, t('Save configuration')); - $this->assertText(t('The configuration options have been saved.'), t('The front page path has been saved.')); + $this->assertText(t('The configuration options have been saved.'), 'The front page path has been saved.'); $this->drupalGet(''); - $this->assertText(t('On front page.'), t('Path is the front page.')); + $this->assertText(t('On front page.'), 'Path is the front page.'); $this->drupalGet('node'); - $this->assertNoText(t('On front page.'), t('Path is not the front page.')); + $this->assertNoText(t('On front page.'), 'Path is not the front page.'); $this->drupalGet($this->node_path); - $this->assertText(t('On front page.'), t('Path is the front page.')); + $this->assertText(t('On front page.'), 'Path is the front page.'); } } class SystemBlockTestCase extends DrupalWebTestCase { + protected $profile = 'testing'; + public static function getInfo() { return array( 'name' => 'Block functionality', @@ -1215,17 +1686,17 @@ } function setUp() { - parent::setUp(); + parent::setUp('block'); // Create and login user - $admin_user = $this->drupalCreateUser(array('administer blocks')); + $admin_user = $this->drupalCreateUser(array('administer blocks', 'access administration pages')); $this->drupalLogin($admin_user); } /** - * Test displaying and hiding the powered-by block. + * Test displaying and hiding the powered-by and help blocks. */ - function testPoweredByBlock() { + function testSystemBlocks() { // Set block title and some settings to confirm that the interface is available. $this->drupalPost('admin/structure/block/manage/system/powered-by/configure', array('title' => $this->randomName(8)), t('Save block')); $this->assertText(t('The block configuration has been saved.'), t('Block configuration set.')); @@ -1233,6 +1704,7 @@ // Set the powered-by block to the footer region. $edit = array(); $edit['blocks[system_powered-by][region]'] = 'footer'; + $edit['blocks[system_main][region]'] = 'content'; $this->drupalPost('admin/structure/block', $edit, t('Save blocks')); $this->assertText(t('The block settings have been updated.'), t('Block successfully moved to footer region.')); @@ -1253,6 +1725,18 @@ $edit['blocks[system_powered-by][region]'] = 'footer'; $this->drupalPost('admin/structure/block', $edit, t('Save blocks')); $this->drupalPost('admin/structure/block/manage/system/powered-by/configure', array('title' => ''), t('Save block')); + + // Set the help block to the help region. + $edit = array(); + $edit['blocks[system_help][region]'] = 'help'; + $this->drupalPost('admin/structure/block', $edit, t('Save blocks')); + + // Test displaying the help block with block caching enabled. + variable_set('block_cache', TRUE); + $this->drupalGet('admin/structure/block/add'); + $this->assertRaw(t('Use this page to create a new custom block.')); + $this->drupalGet('admin/index'); + $this->assertRaw(t('This page shows you all available administration tasks for each module.')); } } @@ -1296,47 +1780,47 @@ // Disable the dashboard module, which depends on the block module. $edit['modules[Core][dashboard][enable]'] = FALSE; $this->drupalPost('admin/modules', $edit, t('Save configuration')); - $this->assertText(t('The configuration options have been saved.'), t('Modules status has been updated.')); + $this->assertText(t('The configuration options have been saved.'), 'Modules status has been updated.'); // Disable the block module. $edit['modules[Core][block][enable]'] = FALSE; $this->drupalPost('admin/modules', $edit, t('Save configuration')); - $this->assertText(t('The configuration options have been saved.'), t('Modules status has been updated.')); + $this->assertText(t('The configuration options have been saved.'), 'Modules status has been updated.'); module_list(TRUE); - $this->assertFalse(module_exists('block'), t('Block module disabled.')); + $this->assertFalse(module_exists('block'), 'Block module disabled.'); // At this point, no region is filled and fallback should be triggered. $this->drupalGet('admin/config/system/site-information'); - $this->assertField('site_name', t('Admin interface still available.')); + $this->assertField('site_name', 'Admin interface still available.'); // Fallback should not trigger when another module is handling content. $this->drupalGet('system-test/main-content-handling'); - $this->assertRaw('id="system-test-content"', t('Content handled by another module')); - $this->assertText(t('Content to test main content fallback'), t('Main content still displayed.')); + $this->assertRaw('id="system-test-content"', 'Content handled by another module'); + $this->assertText(t('Content to test main content fallback'), 'Main content still displayed.'); // Fallback should trigger when another module // indicates that it is not handling the content. $this->drupalGet('system-test/main-content-fallback'); - $this->assertText(t('Content to test main content fallback'), t('Main content fallback properly triggers.')); + $this->assertText(t('Content to test main content fallback'), 'Main content fallback properly triggers.'); // Fallback should not trigger when another module is handling content. // Note that this test ensures that no duplicate // content gets created by the fallback. $this->drupalGet('system-test/main-content-duplication'); - $this->assertNoText(t('Content to test main content fallback'), t('Main content not duplicated.')); + $this->assertNoText(t('Content to test main content fallback'), 'Main content not duplicated.'); // Request a user* page and see if it is displayed. $this->drupalLogin($this->web_user); $this->drupalGet('user/' . $this->web_user->uid . '/edit'); - $this->assertField('mail', t('User interface still available.')); + $this->assertField('mail', 'User interface still available.'); // Enable the block module again. $this->drupalLogin($this->admin_user); $edit = array(); $edit['modules[Core][block][enable]'] = 'block'; $this->drupalPost('admin/modules', $edit, t('Save configuration')); - $this->assertText(t('The configuration options have been saved.'), t('Modules status has been updated.')); + $this->assertText(t('The configuration options have been saved.'), 'Modules status has been updated.'); module_list(TRUE); - $this->assertTrue(module_exists('block'), t('Block module re-enabled.')); + $this->assertTrue(module_exists('block'), 'Block module re-enabled.'); } } @@ -1355,7 +1839,7 @@ function setUp() { parent::setUp(); - $this->admin_user = $this->drupalCreateUser(array('access administration pages', 'view the administration theme', 'administer themes', 'bypass node access')); + $this->admin_user = $this->drupalCreateUser(array('access administration pages', 'view the administration theme', 'administer themes', 'bypass node access', 'administer blocks')); $this->drupalLogin($this->admin_user); $this->node = $this->drupalCreateNode(); } @@ -1366,26 +1850,122 @@ function testThemeSettings() { // Specify a filesystem path to be used for the logo. $file = current($this->drupalGetTestFiles('image')); - $fullpath = drupal_realpath($file->uri); - $edit = array( - 'default_logo' => FALSE, - 'logo_path' => $fullpath, + $file_relative = strtr($file->uri, array('public:/' => variable_get('file_public_path', conf_path() . '/files'))); + $default_theme_path = 'themes/stark'; + + $supported_paths = array( + // Raw stream wrapper URI. + $file->uri => array( + 'form' => file_uri_target($file->uri), + 'src' => file_create_url($file->uri), + ), + // Relative path within the public filesystem. + file_uri_target($file->uri) => array( + 'form' => file_uri_target($file->uri), + 'src' => file_create_url($file->uri), + ), + // Relative path to a public file. + $file_relative => array( + 'form' => $file_relative, + 'src' => file_create_url($file->uri), + ), + // Relative path to an arbitrary file. + 'misc/druplicon.png' => array( + 'form' => 'misc/druplicon.png', + 'src' => $GLOBALS['base_url'] . '/' . 'misc/druplicon.png', + ), + // Relative path to a file in a theme. + $default_theme_path . '/logo.png' => array( + 'form' => $default_theme_path . '/logo.png', + 'src' => $GLOBALS['base_url'] . '/' . $default_theme_path . '/logo.png', + ), ); - $this->drupalPost('admin/appearance/settings', $edit, t('Save configuration')); - $this->drupalGet('node'); - $this->assertRaw($fullpath, t('Logo path successfully changed.')); + foreach ($supported_paths as $input => $expected) { + $edit = array( + 'default_logo' => FALSE, + 'logo_path' => $input, + ); + $this->drupalPost('admin/appearance/settings', $edit, t('Save configuration')); + $this->assertNoText('The custom logo path is invalid.'); + $this->assertFieldByName('logo_path', $expected['form']); + + // Verify the actual 'src' attribute of the logo being output. + $this->drupalGet(''); + $elements = $this->xpath('//*[@id=:id]/img', array(':id' => 'logo')); + $this->assertEqual((string) $elements[0]['src'], $expected['src']); + } + + $unsupported_paths = array( + // Stream wrapper URI to non-existing file. + 'public://whatever.png', + 'private://whatever.png', + 'temporary://whatever.png', + // Bogus stream wrapper URIs. + 'public:/whatever.png', + '://whatever.png', + ':whatever.png', + 'public://', + // Relative path within the public filesystem to non-existing file. + 'whatever.png', + // Relative path to non-existing file in public filesystem. + variable_get('file_public_path', conf_path() . '/files') . '/whatever.png', + // Semi-absolute path to non-existing file in public filesystem. + '/' . variable_get('file_public_path', conf_path() . '/files') . '/whatever.png', + // Relative path to arbitrary non-existing file. + 'misc/whatever.png', + // Semi-absolute path to arbitrary non-existing file. + '/misc/whatever.png', + // Absolute paths to any local file (even if it exists). + drupal_realpath($file->uri), + ); + $this->drupalGet('admin/appearance/settings'); + foreach ($unsupported_paths as $path) { + $edit = array( + 'default_logo' => FALSE, + 'logo_path' => $path, + ); + $this->drupalPost(NULL, $edit, t('Save configuration')); + $this->assertText('The custom logo path is invalid.'); + } // Upload a file to use for the logo. - $file = current($this->drupalGetTestFiles('image')); $edit = array( 'default_logo' => FALSE, 'logo_path' => '', 'files[logo_upload]' => drupal_realpath($file->uri), ); - $options = array(); - $this->drupalPost('admin/appearance/settings', $edit, t('Save configuration'), $options); - $this->drupalGet('node'); - $this->assertRaw($file->name, t('Logo file successfully uploaded.')); + $this->drupalPost('admin/appearance/settings', $edit, t('Save configuration')); + + $fields = $this->xpath($this->constructFieldXpath('name', 'logo_path')); + $uploaded_filename = 'public://' . $fields[0]['value']; + + $this->drupalGet(''); + $elements = $this->xpath('//*[@id=:id]/img', array(':id' => 'logo')); + $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.'); } /** @@ -1402,16 +1982,16 @@ $this->drupalPost('admin/appearance', $edit, t('Save configuration')); $this->drupalGet('admin/config'); - $this->assertRaw('themes/seven', t('Administration theme used on an administration page.')); + $this->assertRaw('themes/seven', 'Administration theme used on an administration page.'); $this->drupalGet('node/' . $this->node->nid); - $this->assertRaw('themes/stark', t('Site default theme used on node page.')); + $this->assertRaw('themes/stark', 'Site default theme used on node page.'); $this->drupalGet('node/add'); - $this->assertRaw('themes/seven', t('Administration theme used on the add content page.')); + $this->assertRaw('themes/seven', 'Administration theme used on the add content page.'); $this->drupalGet('node/' . $this->node->nid . '/edit'); - $this->assertRaw('themes/seven', t('Administration theme used on the edit content page.')); + $this->assertRaw('themes/seven', 'Administration theme used on the edit content page.'); // Disable the admin theme on the node admin pages. $edit = array( @@ -1420,10 +2000,10 @@ $this->drupalPost('admin/appearance', $edit, t('Save configuration')); $this->drupalGet('admin/config'); - $this->assertRaw('themes/seven', t('Administration theme used on an administration page.')); + $this->assertRaw('themes/seven', 'Administration theme used on an administration page.'); $this->drupalGet('node/add'); - $this->assertRaw('themes/stark', t('Site default theme used on the add content page.')); + $this->assertRaw('themes/stark', 'Site default theme used on the add content page.'); // Reset to the default theme settings. variable_set('theme_default', 'bartik'); @@ -1434,10 +2014,30 @@ $this->drupalPost('admin/appearance', $edit, t('Save configuration')); $this->drupalGet('admin'); - $this->assertRaw('themes/bartik', t('Site default theme used on administration page.')); + $this->assertRaw('themes/bartik', 'Site default theme used on administration page.'); $this->drupalGet('node/add'); - $this->assertRaw('themes/bartik', t('Site default theme used on the add content page.')); + $this->assertRaw('themes/bartik', 'Site default theme used on the add content page.'); + } + + /** + * Test switching the default theme. + */ + function testSwitchDefaultTheme() { + // Enable "stark" and set it as the default theme. + theme_enable(array('stark')); + $this->drupalGet('admin/appearance'); + $this->clickLink(t('Set default'), 1); + $this->assertTrue(variable_get('theme_default', '') == 'stark', 'Site default theme switched successfully.'); + + // Test the default theme on the secondary links (blocks admin page). + $this->drupalGet('admin/structure/block'); + $this->assertText('Stark(' . t('active tab') . ')', 'Default local task on blocks admin page is the default theme.'); + // Switch back to Bartik and test again to test that the menu cache is cleared. + $this->drupalGet('admin/appearance'); + $this->clickLink(t('Set default'), 0); + $this->drupalGet('admin/structure/block'); + $this->assertText('Bartik(' . t('active tab') . ')', 'Default local task on blocks admin page has changed.'); } } @@ -1485,14 +2085,14 @@ $new_items[] = $item->data; // First two dequeued items should match the first two items we queued. - $this->assertEqual($this->queueScore($data, $new_items), 2, t('Two items matched')); + $this->assertEqual($this->queueScore($data, $new_items), 2, 'Two items matched'); // Add two more items. $queue1->createItem($data[2]); $queue1->createItem($data[3]); - $this->assertTrue($queue1->numberOfItems(), t('Queue 1 is not empty after adding items.')); - $this->assertFalse($queue2->numberOfItems(), t('Queue 2 is empty while Queue 1 has items')); + $this->assertTrue($queue1->numberOfItems(), 'Queue 1 is not empty after adding items.'); + $this->assertFalse($queue2->numberOfItems(), 'Queue 2 is empty while Queue 1 has items'); $items[] = $item = $queue1->claimItem(); $new_items[] = $item->data; @@ -1502,10 +2102,10 @@ // All dequeued items should match the items we queued exactly once, // therefore the score must be exactly 4. - $this->assertEqual($this->queueScore($data, $new_items), 4, t('Four items matched')); + $this->assertEqual($this->queueScore($data, $new_items), 4, 'Four items matched'); // There should be no duplicate items. - $this->assertEqual($this->queueScore($new_items, $new_items), 4, t('Four items matched')); + $this->assertEqual($this->queueScore($new_items, $new_items), 4, 'Four items matched'); // Delete all items from queue1. foreach ($items as $item) { @@ -1513,8 +2113,8 @@ } // Check that both queues are empty. - $this->assertFalse($queue1->numberOfItems(), t('Queue 1 is empty')); - $this->assertFalse($queue2->numberOfItems(), t('Queue 2 is empty')); + $this->assertFalse($queue1->numberOfItems(), 'Queue 1 is empty'); + $this->assertFalse($queue2->numberOfItems(), 'Queue 2 is empty'); } /** @@ -1571,13 +2171,13 @@ // Test that the clear parameter cleans out non-existent tokens. $result = token_replace($source, array('node' => $node), array('language' => $language, 'clear' => TRUE)); - $result = $this->assertFalse(strcmp($target, $result), 'Valid tokens replaced while invalid tokens cleared out.'); + $result = $this->assertEqual($target, $result, 'Valid tokens replaced while invalid tokens cleared out.'); - // Test without using the clear parameter (non-existant token untouched). + // Test without using the clear parameter (non-existent token untouched). $target .= '[user:name]'; $target .= '[bogus:token]'; $result = token_replace($source, array('node' => $node), array('language' => $language)); - $this->assertFalse(strcmp($target, $result), 'Valid tokens replaced while invalid tokens ignored.'); + $this->assertEqual($target, $result, 'Valid tokens replaced while invalid tokens ignored.'); // Check that the results of token_generate are sanitized properly. This does NOT // test the cleanliness of every token -- just that the $sanitize flag is being @@ -1585,10 +2185,42 @@ // token, [node:title]. $raw_tokens = array('title' => '[node:title]'); $generated = token_generate('node', $raw_tokens, array('node' => $node)); - $this->assertFalse(strcmp($generated['[node:title]'], check_plain($node->title)), t('Token sanitized.')); + $this->assertEqual($generated['[node:title]'], check_plain($node->title), 'Token sanitized.'); $generated = token_generate('node', $raw_tokens, array('node' => $node), array('sanitize' => FALSE)); - $this->assertFalse(strcmp($generated['[node:title]'], $node->title), t('Unsanitized token generated properly.')); + $this->assertEqual($generated['[node:title]'], $node->title, 'Unsanitized token generated properly.'); + + // Test token replacement when the string contains no tokens. + $this->assertEqual(token_replace('No tokens here.'), 'No tokens here.'); + } + + /** + * Test whether token-replacement works in various contexts. + */ + function testSystemTokenRecognition() { + global $language; + + // Generate prefixes and suffixes for the token context. + $tests = array( + array('prefix' => 'this is the ', 'suffix' => ' site'), + array('prefix' => 'this is the', 'suffix' => 'site'), + array('prefix' => '[', 'suffix' => ']'), + array('prefix' => '', 'suffix' => ']]]'), + array('prefix' => '[[[', 'suffix' => ''), + array('prefix' => ':[:', 'suffix' => '--]'), + array('prefix' => '-[-', 'suffix' => ':]:'), + array('prefix' => '[:', 'suffix' => ']'), + array('prefix' => '[site:', 'suffix' => ':name]'), + array('prefix' => '[site:', 'suffix' => ']'), + ); + + // Check if the token is recognized in each of the contexts. + foreach ($tests as $test) { + $input = $test['prefix'] . '[site:name]' . $test['suffix']; + $expected = $test['prefix'] . 'Drupal' . $test['suffix']; + $output = token_replace($input, array(), array('language' => $language)); + $this->assertTrue($output == $expected, format_string('Token recognized in string %string', array('%string' => $input))); + } } /** @@ -1604,7 +2236,6 @@ // Set a few site variables. variable_set('site_name', 'Drupal'); variable_set('site_slogan', 'Slogan'); - variable_set('site_mission', 'Mission'); // Generate and test sanitized tokens. $tests = array(); @@ -1616,11 +2247,11 @@ $tests['[site:login-url]'] = url('user', $url_options); // Test to make sure that we generated something for each token. - $this->assertFalse(in_array(0, array_map('strlen', $tests)), t('No empty tokens generated.')); + $this->assertFalse(in_array(0, array_map('strlen', $tests)), 'No empty tokens generated.'); foreach ($tests as $input => $expected) { $output = token_replace($input, array(), array('language' => $language)); - $this->assertFalse(strcmp($output, $expected), t('Sanitized system site information token %token replaced.', array('%token' => $input))); + $this->assertEqual($output, $expected, format_string('Sanitized system site information token %token replaced.', array('%token' => $input))); } // Generate and test unsanitized tokens. @@ -1629,7 +2260,7 @@ foreach ($tests as $input => $expected) { $output = token_replace($input, array(), array('language' => $language, 'sanitize' => FALSE)); - $this->assertFalse(strcmp($output, $expected), t('Unsanitized system site information token %token replaced.', array('%token' => $input))); + $this->assertEqual($output, $expected, format_string('Unsanitized system site information token %token replaced.', array('%token' => $input))); } } @@ -1652,11 +2283,11 @@ $tests['[date:raw]'] = filter_xss($date); // Test to make sure that we generated something for each token. - $this->assertFalse(in_array(0, array_map('strlen', $tests)), t('No empty tokens generated.')); + $this->assertFalse(in_array(0, array_map('strlen', $tests)), 'No empty tokens generated.'); foreach ($tests as $input => $expected) { $output = token_replace($input, array('date' => $date), array('language' => $language)); - $this->assertFalse(strcmp($output, $expected), t('Date token %token replaced.', array('%token' => $input))); + $this->assertEqual($output, $expected, format_string('Date token %token replaced.', array('%token' => $input))); } } } @@ -1720,15 +2351,15 @@ $parsed = drupal_parse_info_format($config); - $this->assertEqual($parsed['simple'], $expected['simple'], t('Set a simple value.')); - $this->assertEqual($parsed['quoted'], $expected['quoted'], t('Set a simple value in quotes.')); - $this->assertEqual($parsed['multiline'], $expected['multiline'], t('Set a multiline value.')); - $this->assertEqual($parsed['array'], $expected['array'], t('Set a simple array.')); - $this->assertEqual($parsed['array_assoc'], $expected['array_assoc'], t('Set an associative array.')); - $this->assertEqual($parsed['array_deep'], $expected['array_deep'], t('Set a nested array.')); - $this->assertEqual($parsed['array_deep_assoc'], $expected['array_deep_assoc'], t('Set a nested associative array.')); - $this->assertEqual($parsed['array_space'], $expected['array_space'], t('Set an array with a whitespace in the key.')); - $this->assertEqual($parsed, $expected, t('Entire parsed .info string and expected array are identical.')); + $this->assertEqual($parsed['simple'], $expected['simple'], 'Set a simple value.'); + $this->assertEqual($parsed['quoted'], $expected['quoted'], 'Set a simple value in quotes.'); + $this->assertEqual($parsed['multiline'], $expected['multiline'], 'Set a multiline value.'); + $this->assertEqual($parsed['array'], $expected['array'], 'Set a simple array.'); + $this->assertEqual($parsed['array_assoc'], $expected['array_assoc'], 'Set an associative array.'); + $this->assertEqual($parsed['array_deep'], $expected['array_deep'], 'Set a nested array.'); + $this->assertEqual($parsed['array_deep_assoc'], $expected['array_deep_assoc'], 'Set a nested associative array.'); + $this->assertEqual($parsed['array_space'], $expected['array_space'], 'Set an array with a whitespace in the key.'); + $this->assertEqual($parsed, $expected, 'Entire parsed .info string and expected array are identical.'); } } @@ -1754,32 +2385,32 @@ // thing necessary to use the rebuilt {system}.info. module_enable(array('module_test'), FALSE); drupal_flush_all_caches(); - $this->assertTrue(module_exists('module_test'), t('Test module is enabled.')); + $this->assertTrue(module_exists('module_test'), 'Test module is enabled.'); $info = $this->getSystemInfo('seven', 'theme'); - $this->assertTrue(isset($info['regions']['test_region']), t('Altered theme info was added to {system}.info.')); + $this->assertTrue(isset($info['regions']['test_region']), 'Altered theme info was added to {system}.info.'); $seven_regions = system_region_list('seven'); - $this->assertTrue(isset($seven_regions['test_region']), t('Altered theme info was returned by system_region_list().')); + $this->assertTrue(isset($seven_regions['test_region']), 'Altered theme info was returned by system_region_list().'); $system_list_themes = system_list('theme'); $info = $system_list_themes['seven']->info; - $this->assertTrue(isset($info['regions']['test_region']), t('Altered theme info was returned by system_list().')); + $this->assertTrue(isset($info['regions']['test_region']), 'Altered theme info was returned by system_list().'); $list_themes = list_themes(); - $this->assertTrue(isset($list_themes['seven']->info['regions']['test_region']), t('Altered theme info was returned by list_themes().')); + $this->assertTrue(isset($list_themes['seven']->info['regions']['test_region']), 'Altered theme info was returned by list_themes().'); // Disable the module and verify that {system}.info is rebuilt without it. module_disable(array('module_test'), FALSE); drupal_flush_all_caches(); - $this->assertFalse(module_exists('module_test'), t('Test module is disabled.')); + $this->assertFalse(module_exists('module_test'), 'Test module is disabled.'); $info = $this->getSystemInfo('seven', 'theme'); - $this->assertFalse(isset($info['regions']['test_region']), t('Altered theme info was removed from {system}.info.')); + $this->assertFalse(isset($info['regions']['test_region']), 'Altered theme info was removed from {system}.info.'); $seven_regions = system_region_list('seven'); - $this->assertFalse(isset($seven_regions['test_region']), t('Altered theme info was not returned by system_region_list().')); + $this->assertFalse(isset($seven_regions['test_region']), 'Altered theme info was not returned by system_region_list().'); $system_list_themes = system_list('theme'); $info = $system_list_themes['seven']->info; - $this->assertFalse(isset($info['regions']['test_region']), t('Altered theme info was not returned by system_list().')); + $this->assertFalse(isset($info['regions']['test_region']), 'Altered theme info was not returned by system_list().'); $list_themes = list_themes(); - $this->assertFalse(isset($list_themes['seven']->info['regions']['test_region']), t('Altered theme info was not returned by list_themes().')); + $this->assertFalse(isset($list_themes['seven']->info['regions']['test_region']), 'Altered theme info was not returned by list_themes().'); } /** @@ -1815,12 +2446,26 @@ } function setUp() { - parent::setUp(); + parent::setUp('update_script_test'); $this->update_url = $GLOBALS['base_url'] . '/update.php'; $this->update_user = $this->drupalCreateUser(array('administer software updates')); } /** + * 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() { @@ -1852,6 +2497,64 @@ } /** + * Tests that requirements warnings and errors are correctly displayed. + */ + function testRequirements() { + $this->drupalLogin($this->update_user); + + // If there are no requirements warnings or errors, we expect to be able to + // go through the update process uninterrupted. + $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.'); + // Confirm that all caches were cleared. + $this->assertText(t('hook_flush_caches() invoked for update_script_test.module.'), 'Caches were cleared when there were no requirements warnings or errors.'); + + // If there is a requirements warning, we expect it to be initially + // displayed, but clicking the link to proceed should allow us to go + // through the rest of the update process uninterrupted. + + // First, run this test with pending updates to make sure they can be run + // successfully. + variable_set('update_script_test_requirement_type', REQUIREMENT_WARNING); + drupal_set_installed_schema_version('update_script_test', drupal_get_installed_schema_version('update_script_test') - 1); + $this->drupalGet($this->update_url, array('external' => TRUE)); + $this->assertText('This is a requirements warning provided by the update_script_test module.'); + $this->clickLink('try again'); + $this->assertNoText('This is a requirements warning provided by the update_script_test module.'); + $this->drupalPost(NULL, array(), t('Continue')); + $this->drupalPost(NULL, array(), t('Apply pending updates')); + $this->assertText(t('The update_script_test_update_7000() update was executed successfully.'), 'End of update process was reached.'); + // Confirm that all caches were cleared. + $this->assertText(t('hook_flush_caches() invoked for update_script_test.module.'), 'Caches were cleared after resolving a requirements warning and applying updates.'); + + // Now try again without pending updates to make sure that works too. + $this->drupalGet($this->update_url, array('external' => TRUE)); + $this->assertText('This is a requirements warning provided by the update_script_test module.'); + $this->clickLink('try again'); + $this->assertNoText('This is a requirements warning provided by the update_script_test module.'); + $this->drupalPost(NULL, array(), t('Continue')); + $this->assertText(t('No pending updates.'), 'End of update process was reached.'); + // Confirm that all caches were cleared. + $this->assertText(t('hook_flush_caches() invoked for update_script_test.module.'), 'Caches were cleared after applying updates and re-running the script.'); + + // If there is a requirements error, it should be displayed even after + // clicking the link to proceed (since the problem that triggered the error + // has not been fixed). + variable_set('update_script_test_requirement_type', REQUIREMENT_ERROR); + $this->drupalGet($this->update_url, array('external' => TRUE)); + $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()'); + } + + /** * Tests the effect of using the update script on the theme system. */ function testThemeSystem() { @@ -1862,7 +2565,57 @@ $this->drupalLogin($this->update_user); $this->drupalGet($this->update_url, array('external' => TRUE)); $final_theme_data = db_query("SELECT * FROM {system} WHERE type = 'theme' ORDER BY name")->fetchAll(); - $this->assertEqual($original_theme_data, $final_theme_data, t('Visiting update.php does not alter the information about themes stored in the database.')); + $this->assertEqual($original_theme_data, $final_theme_data, 'Visiting update.php does not alter the information about themes stored in the database.'); + } + + /** + * Tests update.php when there are no updates to apply. + */ + function testNoUpdateFunctionality() { + // Click through update.php with 'administer software updates' permission. + $this->drupalLogin($this->update_user); + $this->drupalPost($this->update_url, array(), t('Continue'), array('external' => TRUE)); + $this->assertText(t('No pending updates.')); + $this->assertNoLink('Administration pages'); + $this->clickLink('Front page'); + $this->assertResponse(200); + + // Click through update.php with 'access administration pages' permission. + $admin_user = $this->drupalCreateUser(array('administer software updates', 'access administration pages')); + $this->drupalLogin($admin_user); + $this->drupalPost($this->update_url, array(), t('Continue'), array('external' => TRUE)); + $this->assertText(t('No pending updates.')); + $this->clickLink('Administration pages'); + $this->assertResponse(200); + } + + /** + * Tests update.php after performing a successful update. + */ + function testSuccessfulUpdateFunctionality() { + drupal_set_installed_schema_version('update_script_test', drupal_get_installed_schema_version('update_script_test') - 1); + // Click through update.php with 'administer software updates' permission. + $this->drupalLogin($this->update_user); + $this->drupalPost($this->update_url, array(), t('Continue'), array('external' => TRUE)); + $this->drupalPost(NULL, array(), t('Apply pending updates')); + $this->assertText('Updates were attempted.'); + $this->assertLink('site'); + $this->assertNoLink('Administration pages'); + $this->assertNoLink('logged'); + $this->clickLink('Front page'); + $this->assertResponse(200); + + drupal_set_installed_schema_version('update_script_test', drupal_get_installed_schema_version('update_script_test') - 1); + // Click through update.php with 'access administration pages' and + // 'access site reports' permissions. + $admin_user = $this->drupalCreateUser(array('administer software updates', 'access administration pages', 'access site reports')); + $this->drupalLogin($admin_user); + $this->drupalPost($this->update_url, array(), t('Continue'), array('external' => TRUE)); + $this->drupalPost(NULL, array(), t('Apply pending updates')); + $this->assertText('Updates were attempted.'); + $this->assertLink('logged'); + $this->clickLink('Administration pages'); + $this->assertResponse(200); } } @@ -1922,25 +2675,30 @@ function testFileRetrieving() { // Test 404 handling by trying to fetch a randomly named file. drupal_mkdir($sourcedir = 'public://' . $this->randomName()); - $filename = $this->randomName(); + $filename = 'Файл для тестирования ' . $this->randomName(); $url = file_create_url($sourcedir . '/' . $filename); $retrieved_file = system_retrieve_file($url); - $this->assertFalse($retrieved_file, t('Non-existent file not fetched.')); + $this->assertFalse($retrieved_file, 'Non-existent file not fetched.'); // Actually create that file, download it via HTTP and test the returned path. file_put_contents($sourcedir . '/' . $filename, 'testing'); $retrieved_file = system_retrieve_file($url); - $this->assertEqual($retrieved_file, 'public://' . $filename, t('Sane path for downloaded file returned (public:// scheme).')); - $this->assertTrue(is_file($retrieved_file), t('Downloaded file does exist (public:// scheme).')); - $this->assertEqual(filesize($retrieved_file), 7, t('File size of downloaded file is correct (public:// scheme).')); + + // URLs could not contains characters outside the ASCII set so $filename + // has to be encoded. + $encoded_filename = rawurlencode($filename); + + $this->assertEqual($retrieved_file, 'public://' . $encoded_filename, 'Sane path for downloaded file returned (public:// scheme).'); + $this->assertTrue(is_file($retrieved_file), 'Downloaded file does exist (public:// scheme).'); + $this->assertEqual(filesize($retrieved_file), 7, 'File size of downloaded file is correct (public:// scheme).'); file_unmanaged_delete($retrieved_file); // Test downloading file to a different location. drupal_mkdir($targetdir = 'temporary://' . $this->randomName()); $retrieved_file = system_retrieve_file($url, $targetdir); - $this->assertEqual($retrieved_file, "$targetdir/$filename", t('Sane path for downloaded file returned (temporary:// scheme).')); - $this->assertTrue(is_file($retrieved_file), t('Downloaded file does exist (temporary:// scheme).')); - $this->assertEqual(filesize($retrieved_file), 7, t('File size of downloaded file is correct (temporary:// scheme).')); + $this->assertEqual($retrieved_file, "$targetdir/$encoded_filename", 'Sane path for downloaded file returned (temporary:// scheme).'); + $this->assertTrue(is_file($retrieved_file), 'Downloaded file does exist (temporary:// scheme).'); + $this->assertEqual(filesize($retrieved_file), 7, 'File size of downloaded file is correct (temporary:// scheme).'); file_unmanaged_delete($retrieved_file); file_unmanaged_delete_recursive($sourcedir); @@ -2078,18 +2836,18 @@ */ function testCompactMode() { $this->drupalGet('admin/compact/on'); - $this->assertTrue($this->cookies['Drupal.visitor.admin_compact_mode']['value'], t('Compact mode turns on.')); + $this->assertTrue($this->cookies['Drupal.visitor.admin_compact_mode']['value'], 'Compact mode turns on.'); $this->drupalGet('admin/compact/on'); - $this->assertTrue($this->cookies['Drupal.visitor.admin_compact_mode']['value'], t('Compact mode remains on after a repeat call.')); + $this->assertTrue($this->cookies['Drupal.visitor.admin_compact_mode']['value'], 'Compact mode remains on after a repeat call.'); $this->drupalGet(''); - $this->assertTrue($this->cookies['Drupal.visitor.admin_compact_mode']['value'], t('Compact mode persists on new requests.')); + $this->assertTrue($this->cookies['Drupal.visitor.admin_compact_mode']['value'], 'Compact mode persists on new requests.'); $this->drupalGet('admin/compact/off'); - $this->assertEqual($this->cookies['Drupal.visitor.admin_compact_mode']['value'], 'deleted', t('Compact mode turns off.')); + $this->assertEqual($this->cookies['Drupal.visitor.admin_compact_mode']['value'], 'deleted', 'Compact mode turns off.'); $this->drupalGet('admin/compact/off'); - $this->assertEqual($this->cookies['Drupal.visitor.admin_compact_mode']['value'], 'deleted', t('Compact mode remains off after a repeat call.')); + $this->assertEqual($this->cookies['Drupal.visitor.admin_compact_mode']['value'], 'deleted', 'Compact mode remains off after a repeat call.'); $this->drupalGet(''); - $this->assertTrue($this->cookies['Drupal.visitor.admin_compact_mode']['value'], t('Compact mode persists on new requests.')); + $this->assertTrue($this->cookies['Drupal.visitor.admin_compact_mode']['value'], 'Compact mode persists on new requests.'); } } @@ -2145,3 +2903,187 @@ $this->assertText('System Test Username'); } } + +/** + * Test the handling of requests containing 'index.php'. + */ +class SystemIndexPhpTest extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'Index.php handling', + 'description' => "Test the handling of requests containing 'index.php'.", + 'group' => 'System', + ); + } + + function setUp() { + parent::setUp(); + } + + /** + * Test index.php handling. + */ + function testIndexPhpHandling() { + $index_php = $GLOBALS['base_url'] . '/index.php'; + + $this->drupalGet($index_php, array('external' => TRUE)); + $this->assertResponse(200, 'Make sure index.php returns a valid page.'); + + $this->drupalGet($index_php, array('external' => TRUE, 'query' => array('q' => 'user'))); + $this->assertResponse(200, 'Make sure index.php?q=user returns a valid page.'); + + $this->drupalGet($index_php .'/user', array('external' => TRUE)); + $this->assertResponse(404, "Make sure index.php/user returns a 'page not found'."); + } +} + +/** + * Test token replacement in strings. + */ +class TokenScanTest extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Token scanning', + 'description' => 'Scan token-like patterns in a dummy text to check token scanning.', + 'group' => 'System', + ); + } + + /** + * Scans dummy text, then tests the output. + */ + function testTokenScan() { + // Define text with valid and not valid, fake and existing token-like + // strings. + $text = 'First a [valid:simple], but dummy token, and a dummy [valid:token with: spaces].'; + $text .= 'Then a [not valid:token].'; + $text .= 'Last an existing token: [node:author:name].'; + $token_wannabes = token_scan($text); + + $this->assertTrue(isset($token_wannabes['valid']['simple']), 'A simple valid token has been matched.'); + $this->assertTrue(isset($token_wannabes['valid']['token with: spaces']), 'A valid token with space characters in the token name has been matched.'); + $this->assertFalse(isset($token_wannabes['not valid']), 'An invalid token with spaces in the token type has not been matched.'); + $this->assertTrue(isset($token_wannabes['node']), 'An existing valid token has been matched.'); + } +} + +/** + * 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.0/modules/system/system.theme-rtl.css drupal-7.66/modules/system/system.theme-rtl.css --- drupal-7.0/modules/system/system.theme-rtl.css 2010-09-25 04:28:14.000000000 +0200 +++ drupal-7.66/modules/system/system.theme-rtl.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -/* $Id: system.theme-rtl.css,v 1.2 2010/09/25 02:28:14 dries Exp $ */ /** * @file diff -Naur drupal-7.0/modules/system/system.theme.css drupal-7.66/modules/system/system.theme.css --- drupal-7.0/modules/system/system.theme.css 2010-09-27 03:12:45.000000000 +0200 +++ drupal-7.66/modules/system/system.theme.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -/* $Id: system.theme.css,v 1.3 2010/09/27 01:12:45 dries Exp $ */ /** * @file diff -Naur drupal-7.0/modules/system/system.tokens.inc drupal-7.66/modules/system/system.tokens.inc --- drupal-7.0/modules/system/system.tokens.inc 2010-10-28 03:33:41.000000000 +0200 +++ drupal-7.66/modules/system/system.tokens.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ t("File name"), 'description' => t("The name of the file on disk."), ); - $file['description'] = array( - 'name' => t("Description"), - 'description' => t("An optional human-readable description of the file."), - ); $file['path'] = array( 'name' => t("Path"), 'description' => t("The location of the file relative to Drupal root."), @@ -103,7 +98,7 @@ ); $file['size'] = array( 'name' => t("File size"), - 'description' => t("The size of the file, in kilobytes."), + 'description' => t("The size of the file."), ); $file['url'] = array( 'name' => t("URL"), @@ -135,10 +130,13 @@ */ function system_tokens($type, $tokens, array $data = array(), array $options = array()) { $url_options = array('absolute' => TRUE); - if (isset($language)) { - $url_options['language'] = $language; + if (isset($options['language'])) { + $url_options['language'] = $options['language']; + $language_code = $options['language']->language; + } + else { + $language_code = NULL; } - $langcode = (isset($language) ? $language->language : NULL); $sanitize = !empty($options['sanitize']); $replacements = array(); @@ -186,19 +184,19 @@ foreach ($tokens as $name => $original) { switch ($name) { case 'short': - $replacements[$original] = format_date($date, 'short', '', NULL, $langcode); + $replacements[$original] = format_date($date, 'short', '', NULL, $language_code); break; case 'medium': - $replacements[$original] = format_date($date, 'medium', '', NULL, $langcode); + $replacements[$original] = format_date($date, 'medium', '', NULL, $language_code); break; case 'long': - $replacements[$original] = format_date($date, 'long', '', NULL, $langcode); + $replacements[$original] = format_date($date, 'long', '', NULL, $language_code); break; case 'since': - $replacements[$original] = format_interval((REQUEST_TIME - $date), 2, $langcode); + $replacements[$original] = format_interval((REQUEST_TIME - $date), 2, $language_code); break; case 'raw': @@ -209,7 +207,7 @@ if ($created_tokens = token_find_with_prefix($tokens, 'custom')) { foreach ($created_tokens as $name => $original) { - $replacements[$original] = format_date($date, 'custom', $name, NULL, $langcode); + $replacements[$original] = format_date($date, 'custom', $name, NULL, $language_code); } } } @@ -229,10 +227,6 @@ $replacements[$original] = $sanitize ? check_plain($file->filename) : $file->filename; break; - case 'description': - $replacements[$original] = $sanitize ? check_plain($file->description) : $file->description; - break; - case 'path': $replacements[$original] = $sanitize ? check_plain($file->uri) : $file->uri; break; @@ -251,12 +245,13 @@ // These tokens are default variations on the chained tokens handled below. case 'timestamp': - $replacements[$original] = format_date($file->timestamp, 'medium', '', NULL, $langcode); + $replacements[$original] = format_date($file->timestamp, 'medium', '', NULL, $language_code); break; case 'owner': $account = user_load($file->uid); - $replacements[$original] = $sanitize ? check_plain($account->name) : $account->name; + $name = format_username($account); + $replacements[$original] = $sanitize ? check_plain($name) : $name; break; } } diff -Naur drupal-7.0/modules/system/system.updater.inc drupal-7.66/modules/system/system.updater.inc --- drupal-7.0/modules/system/system.updater.inc 2011-01-04 04:06:24.000000000 +0100 +++ drupal-7.66/modules/system/system.updater.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ name)) { + if ($this->isInstalled() && ($relative_path = drupal_get_path('module', $this->name))) { $relative_path = dirname($relative_path); } else { @@ -35,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) { @@ -74,8 +73,12 @@ return array(); } + /** + * Returns a list of post install actions. + */ public function postInstallTasks() { return array( + l(t('Install another module'), 'admin/modules/install'), l(t('Enable newly added modules'), 'admin/modules'), l(t('Administration pages'), 'admin'), ); @@ -106,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 { @@ -116,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.0/modules/system/tests/cron_queue_test.info drupal-7.66/modules/system/tests/cron_queue_test.info --- drupal-7.0/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.0/modules/system/tests/cron_queue_test.module drupal-7.66/modules/system/tests/cron_queue_test.module --- drupal-7.0/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.0/modules/system/tests/system_cron_test.info drupal-7.66/modules/system/tests/system_cron_test.info --- drupal-7.0/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.0/modules/system/tests/system_cron_test.module drupal-7.66/modules/system/tests/system_cron_test.module --- drupal-7.0/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 @@ + -
    +

    diff -Naur drupal-7.0/modules/taxonomy/taxonomy.admin.inc drupal-7.66/modules/taxonomy/taxonomy.admin.inc --- drupal-7.0/modules/taxonomy/taxonomy.admin.inc 2010-11-20 07:49:47.000000000 +0100 +++ drupal-7.66/modules/taxonomy/taxonomy.admin.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ 'machine_name', '#default_value' => $vocabulary->machine_name, - '#maxlength' => 21, + '#maxlength' => 255, '#machine_name' => array( 'exists' => 'taxonomy_vocabulary_machine_name_load', ), @@ -175,14 +175,41 @@ $form['vid'] = array('#type' => 'value', '#value' => $vocabulary->vid); $form['module'] = array('#type' => 'value', '#value' => $vocabulary->module); } + $form['#validate'][] = 'taxonomy_form_vocabulary_validate'; + return $form; } /** - * Accept the form submission for a vocabulary and save the results. + * Form validation handler for taxonomy_form_vocabulary(). + * + * Makes sure that the machine name of the vocabulary is not in the + * disallowed list (names that conflict with menu items, such as 'list' + * and 'add'). + * + * @see taxonomy_form_vocabulary() + * @see taxonomy_form_vocabulary_submit() + */ +function taxonomy_form_vocabulary_validate($form, &$form_state) { + // During the deletion there is no 'machine_name' key + if (isset($form_state['values']['machine_name'])) { + // Do not allow machine names to conflict with taxonomy path arguments. + $machine_name = $form_state['values']['machine_name']; + $disallowed = array('add', 'list'); + if (in_array($machine_name, $disallowed)) { + form_set_error('machine_name', t('The machine-readable name cannot be "add" or "list".')); + } + } +} + +/** + * Form submission handler for taxonomy_form_vocabulary(). + * + * @see taxonomy_form_vocabulary() + * @see taxonomy_form_vocabulary_validate() */ function taxonomy_form_vocabulary_submit($form, &$form_state) { - if ($form_state['clicked_button']['#value'] == t('Delete')) { + if ($form_state['triggering_element']['#value'] == t('Delete')) { // Rebuild the form to confirm vocabulary deletion. $form_state['rebuild'] = TRUE; $form_state['confirm_delete'] = TRUE; @@ -407,7 +434,7 @@ * @see taxonomy_overview_terms() */ function taxonomy_overview_terms_submit($form, &$form_state) { - if ($form_state['clicked_button']['#value'] == t('Reset to alphabetical')) { + if ($form_state['triggering_element']['#value'] == t('Reset to alphabetical')) { // Execute the reset action. if ($form_state['values']['reset_alphabetical'] === TRUE) { return taxonomy_vocabulary_confirm_reset_alphabetical_submit($form, $form_state); @@ -647,9 +674,6 @@ if (isset($form_state['confirm_delete'])) { return array_merge($form, taxonomy_term_confirm_delete($form, $form_state, $term->tid)); } - elseif (isset($form_state['confirm_parents'])) { - return array_merge($form, taxonomy_term_confirm_parents($form, $form_state, $vocabulary)); - } $form['name'] = array( '#type' => 'textfield', @@ -672,7 +696,8 @@ '#value' => isset($term->vocabulary_machine_name) ? $term->vocabulary_machine_name : $vocabulary->name, ); - field_attach_form('taxonomy_term', $term, $form, $form_state); + $langcode = entity_language('taxonomy_term', $term); + field_attach_form('taxonomy_term', $term, $form, $form_state, $langcode); $form['relations'] = array( '#type' => 'fieldset', @@ -774,7 +799,7 @@ * @see taxonomy_form_term() */ function taxonomy_form_term_submit($form, &$form_state) { - if ($form_state['clicked_button']['#value'] == t('Delete')) { + if ($form_state['triggering_element']['#value'] == t('Delete')) { // Execute the term deletion. if ($form_state['values']['delete'] === TRUE) { return taxonomy_term_confirm_delete_submit($form, $form_state); @@ -784,12 +809,6 @@ $form_state['confirm_delete'] = TRUE; return; } - // Rebuild the form to confirm enabling multiple parents. - elseif ($form_state['clicked_button']['#value'] == t('Save') && count($form_state['values']['parent']) > 1 && $form['#vocabulary']->hierarchy < 2) { - $form_state['rebuild'] = TRUE; - $form_state['confirm_parents'] = TRUE; - return; - } $term = taxonomy_form_term_submit_build_taxonomy_term($form, $form_state); @@ -846,25 +865,6 @@ } /** - * Form builder for the confirmation of multiple term parents. - * - * @ingroup forms - * @see taxonomy_form_term() - */ -function taxonomy_term_confirm_parents($form, &$form_state, $vocabulary) { - foreach (element_children($form_state['values']) as $key) { - $form[$key] = array( - '#type' => 'value', - '#value' => $form_state['values'][$key], - ); - } - $question = t('Set multiple term parents?'); - $description = '

    ' . t("Adding multiple parents to a term will cause the %vocabulary vocabulary to look for multiple parents on every term. Because multiple parents are not supported when using the drag and drop outline interface, drag and drop will be disabled if you enable this option. If you choose to have multiple parents, you will only be able to set parents by using the term edit form.", array('%vocabulary' => $vocabulary->name)) . '

    '; - $description .= '

    ' . t("You may re-enable the drag and drop interface at any time by reducing multiple parents to a single parent for the terms in this vocabulary.") . '

    '; - return confirm_form($form, $question, drupal_get_destination(), $description, t('Set multiple parents')); -} - -/** * Form builder for the term delete form. * * @ingroup forms diff -Naur drupal-7.0/modules/taxonomy/taxonomy.api.php drupal-7.66/modules/taxonomy/taxonomy.api.php --- drupal-7.0/modules/taxonomy/taxonomy.api.php 2010-10-23 17:30:34.000000000 +0200 +++ drupal-7.66/modules/taxonomy/taxonomy.api.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ synonyms = variable_get('taxonomy_' . $vocabulary->vid . '_synonyms', FALSE); + $result = db_select('mytable', 'm') + ->fields('m', array('vid', 'foo')) + ->condition('m.vid', array_keys($vocabularies), 'IN') + ->execute(); + foreach ($result as $record) { + $vocabularies[$record->vid]->foo = $record->foo; } } - /** * Act on taxonomy vocabularies before they are saved. * @@ -50,8 +52,8 @@ * A taxonomy vocabulary object. */ function hook_taxonomy_vocabulary_insert($vocabulary) { - if ($vocabulary->synonyms) { - variable_set('taxonomy_' . $vocabulary->vid . '_synonyms', TRUE); + if ($vocabulary->machine_name == 'my_vocabulary') { + $vocabulary->weight = 100; } } @@ -64,10 +66,10 @@ * A taxonomy vocabulary object. */ function hook_taxonomy_vocabulary_update($vocabulary) { - $status = $vocabulary->synonyms ? TRUE : FALSE; - if ($vocabulary->synonyms) { - variable_set('taxonomy_' . $vocabulary->vid . '_synonyms', $status); - } + db_update('mytable') + ->fields(array('foo' => $vocabulary->foo)) + ->condition('vid', $vocabulary->vid) + ->execute(); } /** @@ -80,9 +82,9 @@ * A taxonomy vocabulary object. */ function hook_taxonomy_vocabulary_delete($vocabulary) { - if (variable_get('taxonomy_' . $vocabulary->vid . '_synonyms', FALSE)) { - variable_del('taxonomy_' . $vocabulary->vid . '_synonyms'); - } + db_delete('mytable') + ->condition('vid', $vocabulary->vid) + ->execute(); } /** @@ -102,7 +104,10 @@ * An array of term objects, indexed by tid. */ function hook_taxonomy_term_load($terms) { - $result = db_query('SELECT tid, foo FROM {mytable} WHERE tid IN (:tids)', array(':tids' => array_keys($terms))); + $result = db_select('mytable', 'm') + ->fields('m', array('tid', 'foo')) + ->condition('m.tid', array_keys($terms), 'IN') + ->execute(); foreach ($result as $record) { $terms[$record->tid]->foo = $record->foo; } @@ -131,18 +136,12 @@ * A taxonomy term object. */ function hook_taxonomy_term_insert($term) { - if (!empty($term->synonyms)) { - foreach (explode ("\n", str_replace("\r", '', $term->synonyms)) as $synonym) { - if ($synonym) { - db_insert('taxonomy_term_synonym') - ->fields(array( - 'tid' => $term->tid, - 'name' => rtrim($synonym), - )) - ->execute(); - } - } - } + db_insert('mytable') + ->fields(array( + 'tid' => $term->tid, + 'foo' => $term->foo, + )) + ->execute(); } /** @@ -154,19 +153,10 @@ * A taxonomy term object. */ function hook_taxonomy_term_update($term) { - hook_taxonomy_term_delete($term); - if (!empty($term->synonyms)) { - foreach (explode ("\n", str_replace("\r", '', $term->synonyms)) as $synonym) { - if ($synonym) { - db_insert('taxonomy_term_synonym') - ->fields(array( - 'tid' => $term->tid, - 'name' => rtrim($synonym), - )) - ->execute(); - } - } - } + db_update('mytable') + ->fields(array('foo' => $term->foo)) + ->condition('tid', $term->tid) + ->execute(); } /** @@ -179,7 +169,33 @@ * A taxonomy term object. */ function hook_taxonomy_term_delete($term) { - db_delete('term_synoynm')->condition('tid', $term->tid)->execute(); + db_delete('mytable') + ->condition('tid', $term->tid) + ->execute(); +} + +/** + * Act on a taxonomy term that is being assembled before rendering. + * + * The module may add elements to $term->content prior to rendering. The + * structure of $term->content is a renderable array as expected by + * drupal_render(). + * + * @param $term + * The term that is being assembled for rendering. + * @param $view_mode + * The $view_mode parameter from taxonomy_term_view(). + * @param $langcode + * The language code used for rendering. + * + * @see hook_entity_view() + */ +function hook_taxonomy_term_view($term, $view_mode, $langcode) { + $term->content['my_additional_field'] = array( + '#markup' => $additional_field, + '#weight' => 10, + '#theme' => 'mymodule_my_additional_field', + ); } /** @@ -196,7 +212,7 @@ * documentation respectively for details. * * @param $build - * A renderable array representing the node content. + * A renderable array representing the term. * * @see hook_entity_view_alter() */ diff -Naur drupal-7.0/modules/taxonomy/taxonomy.css drupal-7.66/modules/taxonomy/taxonomy.css --- drupal-7.0/modules/taxonomy/taxonomy.css 2008-01-25 22:20:26.000000000 +0100 +++ drupal-7.66/modules/taxonomy/taxonomy.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -/* $Id: taxonomy.css,v 1.5 2008/01/25 21:20:26 goba Exp $ */ tr.taxonomy-term-preview { background-color: #EEE; diff -Naur drupal-7.0/modules/taxonomy/taxonomy.info drupal-7.66/modules/taxonomy/taxonomy.info --- drupal-7.0/modules/taxonomy/taxonomy.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/taxonomy/taxonomy.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: taxonomy.info,v 1.13 2010/12/20 19:59:43 webchick Exp $ name = Taxonomy description = Enables the categorization of content. package = Core @@ -9,8 +8,7 @@ files[] = taxonomy.test configure = admin/structure/taxonomy -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/taxonomy/taxonomy.install drupal-7.66/modules/taxonomy/taxonomy.install --- drupal-7.0/modules/taxonomy/taxonomy.install 2010-12-14 20:50:05.000000000 +0100 +++ drupal-7.66/modules/taxonomy/taxonomy.install 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ fetchCol(); + foreach ($vocabularies as $vocabulary) { + field_attach_delete_bundle('taxonomy_term', $vocabulary); + } } /** @@ -247,26 +251,11 @@ * Implements hook_update_dependencies(). */ function taxonomy_update_dependencies() { - // Taxonomy update 7002 creates comment Field API bundles and therefore must - // run after the Field module has been enabled, but before upgrading field - // data. - $dependencies['taxonomy'][7002] = array( - 'system' => 7049, - ); - $dependencies['user'][7006] = array( - 'taxonomy' => 7002, - ); - $dependencies['system'][7050] = array( - 'taxonomy' => 7002, - ); - // It also must run before nodes are upgraded to use the Field API. - $dependencies['node'][7006] = array( - 'taxonomy' => 7002, - ); - // Ensure that format columns are only changed after Filter module has changed - // the primary records. - $dependencies['taxonomy'][7009] = array( - 'filter' => 7010, + // taxonomy_update_7004() migrates taxonomy term data to fields and therefore + // must run after all Field modules have been enabled, which happens in + // system_update_7027(). + $dependencies['taxonomy'][7004] = array( + 'system' => 7027, ); return $dependencies; @@ -277,7 +266,7 @@ * * This function is valid for a database schema version 7002. * - * @ingroup update-api-6.x-to-7.x + * @ingroup update_api */ function _update_7002_taxonomy_get_vocabularies() { return db_query('SELECT v.* FROM {taxonomy_vocabulary} v ORDER BY v.weight, v.name')->fetchAllAssoc('vid', PDO::FETCH_OBJ); @@ -434,6 +423,7 @@ 'entity_type' => 'node', 'settings' => array(), 'description' => $vocabulary->help, + 'required' => $vocabulary->required, 'widget' => array(), 'display' => array( 'default' => array( @@ -502,6 +492,7 @@ 'bundle' => $bundle->type, 'settings' => array(), 'description' => 'Debris left over after upgrade from Drupal 6', + 'required' => FALSE, 'widget' => array( 'type' => 'taxonomy_autocomplete', 'module' => 'taxonomy', @@ -568,7 +559,11 @@ // provides the delta value for each term reference data insert. The // deltas are reset for each new revision. - $field_info = _update_7000_field_read_fields(); + $conditions = array( + 'type' => 'taxonomy_term_reference', + 'deleted' => 0, + ); + $field_info = _update_7000_field_read_fields($conditions, 'field_name'); // This is a multi-pass update. On the first call we need to initialize some // variables. @@ -598,36 +593,118 @@ if (!empty($vocabularies)) { $sandbox['vocabularies'] = $vocabularies; } + + db_create_table('taxonomy_update_7005', array( + 'description' => 'Stores temporary data for taxonomy_update_7005.', + 'fields' => array( + 'n' => array( + 'description' => 'Preserve order.', + 'type' => 'serial', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'vocab_id' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'tid' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'nid' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'vid' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => FALSE, + 'default' => NULL, + ), + 'type' => array( + 'type' => 'varchar', + 'length' => 32, + 'not null' => TRUE, + 'default' => '', + ), + 'created' => array( + 'type' => 'int', + 'not null' => FALSE, + ), + 'sticky' => array( + 'type' => 'int', + 'not null' => FALSE, + ), + 'status' => array( + 'type' => 'int', + 'not null' => FALSE, + ), + 'is_current' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => FALSE, + ), + ), + 'primary key' => array('n'), + )); + + // Query selects all revisions at once and processes them in revision and + // term weight order. + $query = db_select('taxonomy_term_data', 'td'); + // We are migrating term-node relationships. If there are none for a + // term, we do not need the term_data row. + $query->join('taxonomy_term_node', 'tn', 'td.tid = tn.tid'); + // If a term-node relationship exists for a nid that does not exist, we + // cannot migrate it as we have no node to relate it to; thus we do not + // need that row from term_node. + $query->join('node', 'n', 'tn.nid = n.nid'); + // If the current term-node relationship is for the current revision of + // the node, this left join will match and is_current will be non-NULL + // (we also get the current sticky and created in this case). This + // tells us whether to insert into the current data tables in addition + // to the revision data tables. + $query->leftJoin('node', 'n2', 'tn.vid = n2.vid'); + $query->addField('td', 'vid', 'vocab_id'); + $query->addField('td', 'tid'); + $query->addField('tn', 'nid'); + $query->addField('tn', 'vid'); + $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 + // to reset the delta counters) and by term weight so they appear + // within each node in weight order. However, tn.vid,td.weight is not + // guaranteed to be unique, so we add tn.tid as an additional sort key + // because tn.tid,tn.vid is the primary key of the D6 term_node table + // and so is guaranteed unique. Unfortunately it also happens to be in + // the wrong order which is less efficient, but c'est la vie. + $query->orderBy('tn.vid'); + $query->orderBy('td.weight'); + $query->orderBy('tn.tid'); + + // Work around a bug in the PostgreSQL driver that would result in fatal + // errors when this subquery is used in the insert query below. See + // https://drupal.org/node/2057693. + $fields = &$query->getFields(); + unset($fields['td.weight']); + unset($fields['tn.tid']); + + db_insert('taxonomy_update_7005') + ->from($query) + ->execute(); } else { // We do each pass in batches of 1000. $batch = 1000; - // Query selects all revisions at once and processes them in revision and - // term weight order. Join types: - // - // - INNER JOIN term_node ON tn.tid: We are migrating term-node - // relationships. If there are none for a term, we do not need the - // term_data row. - // - INNER JOIN {node} n ON n.nid: If a term-node relationship exists for a - // nid that does not exist, we cannot migrate it as we have no node to - // relate it to; thus we do not need that row from term_node. - // - LEFT JOIN {node} n2 ON n2.vid: If the current term-node relationship - // is for the current revision of the node, this left join will match and - // is_current will be non-NULL (we also get the current sticky and - // created in this case). This tells us whether to insert into the - // current data tables in addition to the revision data tables. - // - // This query must return a consistent ordering across multiple calls. We - // need them ordered by node vid (since we use that to decide when to reset - // the delta counters) and by term weight so they appear within each node - // in weight order. However, tn.vid,td.weight is not guaranteed to be - // unique, so we add tn.tid as an additional sort key because tn.tid,tn.vid - // is the primary key of the D6 term_node table and so is guaranteed - // unique. Unfortunately it also happens to be in the wrong order which is - // less efficient, but c'est la vie. - $query = 'SELECT td.vid AS vocab_id, td.tid, tn.nid, tn.vid, n.type, n2.created, n2.sticky, n2.nid AS is_current FROM {taxonomy_term_data} td INNER JOIN {taxonomy_term_node} tn ON td.tid = tn.tid INNER JOIN {node} n ON tn.nid = n.nid LEFT JOIN {node} n2 ON tn.vid = n2.vid ORDER BY tn.vid, td.weight ASC, tn.tid'; - $result = db_query_range($query, $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']; @@ -694,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(); + } } } @@ -720,6 +799,7 @@ db_drop_table('taxonomy_term_node'); // If there are no vocabs, we're done. + db_drop_table('taxonomy_update_7005'); $sandbox['#finished'] = TRUE; // Determine necessity of taxonomyextras field. @@ -816,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.0/modules/taxonomy/taxonomy.js drupal-7.66/modules/taxonomy/taxonomy.js --- drupal-7.0/modules/taxonomy/taxonomy.js 2010-01-04 13:04:07.000000000 +0100 +++ drupal-7.66/modules/taxonomy/taxonomy.js 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -// $Id: taxonomy.js,v 1.7 2010/01/04 12:04:07 dries Exp $ (function ($) { /** @@ -11,7 +10,7 @@ attach: function (context, settings) { var table = $('#taxonomy', context); var tableDrag = Drupal.tableDrag.taxonomy; // Get the blocks tableDrag object. - var rows = $('tr', table).size(); + var rows = $('tr', table).length; // When a row is swapped, keep previous and next page classes set. tableDrag.row.prototype.onSwap = function (swappedRow) { diff -Naur drupal-7.0/modules/taxonomy/taxonomy.module drupal-7.66/modules/taxonomy/taxonomy.module --- drupal-7.0/modules/taxonomy/taxonomy.module 2011-01-03 19:03:54.000000000 +0100 +++ drupal-7.66/modules/taxonomy/taxonomy.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ ' . t('About') . ''; - $output .= '

    ' . t('The Taxonomy module allows you to classify the content of your website. To classify content, you define vocabularies that contain related terms, and then assign the vocabularies to content types. For more information, see the online handbook entry for the Taxonomy module.', array('@taxonomy' => 'http://drupal.org/handbook/modules/taxonomy/')) . '

    '; + $output .= '

    ' . t('The Taxonomy module allows you to classify the content of your website. To classify content, you define vocabularies that contain related terms, and then assign the vocabularies to content types. For more information, see the online handbook entry for the Taxonomy module.', array('@taxonomy' => 'http://drupal.org/documentation/modules/taxonomy/')) . '

    '; $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') . '
        • '; @@ -141,7 +140,7 @@ } /** - * Entity uri callback. + * Implements callback_entity_info_uri(). */ function taxonomy_term_uri($term) { return array( @@ -196,7 +195,7 @@ * @param $limit * Integer. The maximum number of nodes to find. * Set to FALSE for no limit. - * @order + * @param $order * An array of fields and directions. * * @return @@ -284,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'), @@ -296,7 +297,9 @@ $items['taxonomy/term/%taxonomy_term/edit'] = array( 'title' => 'Edit', 'page callback' => 'drupal_get_form', - 'page arguments' => array('taxonomy_form_term', 2), + // Pass a NULL argument to ensure that additional path components are not + // passed to taxonomy_form_term() as the vocabulary machine name argument. + 'page arguments' => array('taxonomy_form_term', 2, NULL), 'access callback' => 'taxonomy_term_edit_access', 'access arguments' => array(2), 'type' => MENU_LOCAL_TASK, @@ -322,8 +325,8 @@ ); $items['admin/structure/taxonomy/%taxonomy_vocabulary_machine_name'] = array( - 'title callback' => 'taxonomy_admin_vocabulary_title_callback', - 'title arguments' => array(3), + 'title callback' => 'entity_label', + 'title arguments' => array('taxonomy_vocabulary', 3), 'page callback' => 'drupal_get_form', 'page arguments' => array('taxonomy_overview_terms', 3), 'access arguments' => array('administer taxonomy'), @@ -374,14 +377,40 @@ } /** - * Return the vocabulary name given the vocabulary object. + * Returns the sanitized name of a vocabulary. + * + * Deprecated. This function was previously used as a menu item title callback + * but has been replaced by using entity_label() (which does not + * sanitize the title, since the menu system does that automatically). In + * Drupal 7, use that function for title callbacks, and call check_plain() + * directly if you need a sanitized title. */ function taxonomy_admin_vocabulary_title_callback($vocabulary) { return check_plain($vocabulary->name); } /** - * Save a vocabulary given a vocabulary object. + * Saves a vocabulary. + * + * @param $vocabulary + * A vocabulary object with the following properties: + * - vid: (optional) The ID of the vocabulary (omit if creating a new + * vocabulary; only use to update an existing vocabulary). + * - name: The human-readable name of the vocabulary. + * - machine_name: The machine name of the vocabulary. + * - description: (optional) The vocabulary's description. + * - hierarchy: The hierarchy level of the vocabulary. + * - module: (optional) The module altering the vocabulary. + * - weight: (optional) The weight of this vocabulary in relation to other + * vocabularies. + * - original: (optional) The original vocabulary object before any changes + * are applied. + * - old_machine_name: (optional) The original machine name of the + * vocabulary. + * + * @return + * Status constant indicating whether the vocabulary was inserted (SAVED_NEW) + * or updated (SAVED_UPDATED). */ function taxonomy_vocabulary_save($vocabulary) { // Prevent leading and trailing spaces in vocabulary names. @@ -408,6 +437,7 @@ if (!empty($vocabulary->vid) && !empty($vocabulary->name)) { $status = drupal_write_record('taxonomy_vocabulary', $vocabulary, 'vid'); + taxonomy_vocabulary_static_reset(array($vocabulary->vid)); if ($vocabulary->old_machine_name != $vocabulary->machine_name) { field_attach_rename_bundle('taxonomy_term', $vocabulary->old_machine_name, $vocabulary->machine_name); } @@ -416,6 +446,7 @@ } elseif (empty($vocabulary->vid)) { $status = drupal_write_record('taxonomy_vocabulary', $vocabulary); + taxonomy_vocabulary_static_reset(); field_attach_create_bundle('taxonomy_term', $vocabulary->machine_name); module_invoke_all('taxonomy_vocabulary_insert', $vocabulary); module_invoke_all('entity_insert', $vocabulary, 'taxonomy_vocabulary'); @@ -423,13 +454,17 @@ unset($vocabulary->original); cache_clear_all(); - entity_get_controller('taxonomy_vocabulary')->resetCache(array($vocabulary->vid)); return $status; } /** - * 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. @@ -454,8 +489,32 @@ module_invoke_all('taxonomy_vocabulary_delete', $vocabulary); module_invoke_all('entity_delete', $vocabulary, 'taxonomy_vocabulary'); + // Load all Taxonomy module fields and delete those which use only this + // vocabulary. + $taxonomy_fields = field_read_fields(array('module' => 'taxonomy')); + foreach ($taxonomy_fields as $field_name => $taxonomy_field) { + $modified_field = FALSE; + // Term reference fields may reference terms from more than one + // vocabulary. + foreach ($taxonomy_field['settings']['allowed_values'] as $key => $allowed_value) { + if ($allowed_value['vocabulary'] == $vocabulary->machine_name) { + unset($taxonomy_field['settings']['allowed_values'][$key]); + $modified_field = TRUE; + } + } + if ($modified_field) { + if (empty($taxonomy_field['settings']['allowed_values'])) { + field_delete_field($field_name); + } + else { + // Update the field definition with the new allowed values. + field_update_field($taxonomy_field); + } + } + } + cache_clear_all(); - entity_get_controller('taxonomy_vocabulary')->resetCache(); + taxonomy_vocabulary_static_reset(); return SAVED_DELETED; } @@ -492,19 +551,22 @@ } /** - * Dynamically check and update the hierarchy flag of a vocabulary. + * Checks and updates the hierarchy flag of a vocabulary. * * Checks the current parents of all terms in a vocabulary and updates the - * vocabularies hierarchy setting to the lowest possible level. A hierarchy with - * no parents in any of its terms will be given a hierarchy of 0. If terms - * contain at most a single parent, the vocabulary will be given a hierarchy of - * 1. If any term contain multiple parents, the vocabulary will be given a - * hierarchy of 2. + * vocabulary's hierarchy setting to the lowest possible level. If no term + * has parent terms then the vocabulary will be given a hierarchy of 0. + * If any term has a single parent then the vocabulary will be given a + * hierarchy of 1. If any term has multiple parents then the vocabulary + * will be given a hierarchy of 2. * * @param $vocabulary * A vocabulary object. * @param $changed_term * An array of the term structure that was updated. + * + * @return + * An integer that represents the level of the vocabulary's hierarchy. */ function taxonomy_check_vocabulary_hierarchy($vocabulary, $changed_term) { $tree = taxonomy_get_tree($vocabulary->vid); @@ -520,7 +582,7 @@ $hierarchy = 2; break; } - elseif (count($term->parents) == 1 && 0 !== array_shift($term->parents)) { + elseif (count($term->parents) == 1 && !isset($term->parents[0])) { $hierarchy = 1; } } @@ -533,12 +595,35 @@ } /** - * Save a term object to the database. + * Saves a term object to the database. * * @param $term - * A term object. + * The taxonomy term object with the following properties: + * - vid: The ID of the vocabulary the term is assigned to. + * - name: The name of the term. + * - tid: (optional) The unique ID for the term being saved. If $term->tid is + * empty or omitted, a new term will be inserted. + * - description: (optional) The term's description. + * - format: (optional) The text format for the term's description. + * - weight: (optional) The weight of this term in relation to other terms + * within the same vocabulary. + * - parent: (optional) The parent term(s) for this term. This can be a single + * term ID or an array of term IDs. A value of 0 means this term does not + * have any parents. When omitting this variable during an update, the + * existing hierarchy for the term remains unchanged. + * - vocabulary_machine_name: (optional) The machine name of the vocabulary + * the term is assigned to. If not given, this value will be set + * automatically by loading the vocabulary based on $term->vid. + * - original: (optional) The original taxonomy term object before any changes + * were applied. When omitted, the unchanged taxonomy term object is + * loaded from the database and stored in this property. + * Since a taxonomy term is an entity, any fields contained in the term object + * are saved alongside the term object. + * * @return - * Status constant indicating if term was inserted or updated. + * Status constant indicating whether term was inserted (SAVED_NEW) or updated + * (SAVED_UPDATED). When inserting a new term, $term->tid will contain the + * term ID of the newly created term. */ function taxonomy_term_save($term) { // Prevent leading and trailing spaces in term names. @@ -665,6 +750,120 @@ } /** + * Generates an array which displays a term detail page. + * + * @param term + * A taxonomy term object. + * @return + * A $page element suitable for use by drupal_render(). + */ +function taxonomy_term_show($term) { + return taxonomy_term_view_multiple(array($term->tid => $term), 'full'); +} + +/** + * Constructs a drupal_render() style array from an array of loaded terms. + * + * @param $terms + * An array of taxonomy terms as returned by taxonomy_term_load_multiple(). + * @param $view_mode + * View mode, e.g. 'full', 'teaser'... + * @param $weight + * An integer representing the weight of the first taxonomy term in the list. + * @param $langcode + * (optional) A language code to use for rendering. Defaults to the global + * content language of the current request. + * + * @return + * An array in the format expected by drupal_render(). + */ +function taxonomy_term_view_multiple($terms, $view_mode = 'teaser', $weight = 0, $langcode = NULL) { + $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]['#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; +} + +/** + * Builds a structured array representing the term's content. + * + * The content built for the taxonomy term (field values, file attachments or + * other term components) will vary depending on the $view_mode parameter. + * + * Drupal core defines the following view modes for terms, with the following + * default use cases: + * - full (default): term is displayed on its own page (taxonomy/term/123) + * Contributed modules might define additional view modes, or use existing + * view modes in additional contexts. + * + * @param $term + * A taxonomy term object. + * @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. + */ +function taxonomy_term_build_content($term, $view_mode = 'full', $langcode = NULL) { + if (!isset($langcode)) { + $langcode = $GLOBALS['language_content']->language; + } + + // Remove previously built content, if exists. + $term->content = array(); + + // Allow modules to change the view mode. + $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'; + $entity_ids = entity_extract_ids($type, $term); + $settings = field_view_mode_settings($type, $entity_ids[2]); + $fields = field_extra_fields_get_display($type, $entity_ids[2], $view_mode); + if (!empty($term->description) && isset($fields['description']) && $fields['description']['visible']) { + $term->content['description'] = array( + '#markup' => check_markup($term->description, $term->format, '', TRUE), + '#weight' => $fields['description']['weight'], + '#prefix' => '
          ', + '#suffix' => '
          ', + ); + } + + // Build fields content. + // In case of a multiple view, taxonomy_term_view_multiple() already ran the + // 'prepare_view' step. An internal flag prevents the operation from running + // twice. + field_attach_prepare_view('taxonomy_term', array($term->tid => $term), $view_mode, $langcode); + entity_prepare_view('taxonomy_term', array($term->tid => $term), $langcode); + $term->content += field_attach_view('taxonomy_term', $term, $view_mode, $langcode); + + // Allow modules to make their own additions to the taxonomy term. + module_invoke_all('taxonomy_term_view', $term, $view_mode, $langcode); + module_invoke_all('entity_view', $term, 'taxonomy_term', $view_mode, $langcode); + + // Make sure the current view mode is stored if no module has already + // populated the related key. + $term->content += array('#view_mode' => $view_mode); +} + +/** * Generate an array for rendering the given term. * * @param $term @@ -683,28 +882,23 @@ $langcode = $GLOBALS['language_content']->language; } - field_attach_prepare_view('taxonomy_term', array($term->tid => $term), $view_mode); - entity_prepare_view('taxonomy_term', array($term->tid => $term)); + // Populate $term->content with a render() array. + taxonomy_term_build_content($term, $view_mode, $langcode); + $build = $term->content; - $build = array( + // We don't need duplicate rendering info in $term->content. + unset($term->content); + + $build += array( '#theme' => 'taxonomy_term', '#term' => $term, '#view_mode' => $view_mode, '#language' => $langcode, ); - $build += field_attach_view('taxonomy_term', $term, $view_mode, $langcode); - - $build['description'] = array( - '#markup' => check_markup($term->description, $term->format, '', TRUE), - '#weight' => 0, - '#prefix' => '
          ', - '#suffix' => '
          ', - ); - $build['#attached']['css'][] = drupal_get_path('module', 'taxonomy') . '/taxonomy.css'; - // Allow modules to modify the structured term. + // Allow modules to modify the structured taxonomy term. $type = 'taxonomy_term'; drupal_alter(array('taxonomy_term_view', 'entity_view'), $build, $type); @@ -728,6 +922,7 @@ $variables = array_merge((array) $term, $variables); // Helpful $content variable for templates. + $variables['content'] = array(); foreach (element_children($variables['elements']) as $key) { $variables['content'][$key] = $variables['elements'][$key]; } @@ -747,7 +942,7 @@ } /** - * Returns whether the current page is the page of the passed in term. + * Returns whether the current page is the page of the passed-in term. * * @param $term * A term object. @@ -758,7 +953,7 @@ } /** - * Clear all static cache variables for terms.. + * Clear all static cache variables for terms. */ function taxonomy_terms_static_reset() { drupal_static_reset('taxonomy_term_count_nodes'); @@ -772,6 +967,17 @@ } /** + * Clear all static cache variables for vocabularies. + * + * @param $ids + * An array of ids to reset in entity controller cache. + */ +function taxonomy_vocabulary_static_reset($ids = NULL) { + drupal_static_reset('taxonomy_vocabulary_get_names'); + entity_get_controller('taxonomy_vocabulary')->resetCache($ids); +} + +/** * Return an array of all vocabulary objects. * * @return @@ -785,10 +991,19 @@ * Get names for all taxonomy vocabularies. * * @return - * An array of vocabulary ids, names, machine names, keyed by machine name. + * An associative array of objects keyed by vocabulary machine name with + * information about taxonomy vocabularies. Each object has properties: + * - name: The vocabulary name. + * - machine_name: The machine name. + * - vid: The vocabulary ID. */ function taxonomy_vocabulary_get_names() { - $names = db_query('SELECT name, machine_name, vid FROM {taxonomy_vocabulary}')->fetchAllAssoc('machine_name'); + $names = &drupal_static(__FUNCTION__); + + if (!isset($names)) { + $names = db_query('SELECT name, machine_name, vid FROM {taxonomy_vocabulary}')->fetchAllAssoc('machine_name'); + } + return $names; } @@ -799,7 +1014,8 @@ * A taxonomy term ID. * * @return - * An array of term objects which are the parents of the term $tid. + * An array of term objects which are the parents of the term $tid, or an + * empty array if parents are not found. */ function taxonomy_get_parents($tid) { $parents = &drupal_static(__FUNCTION__, array()); @@ -809,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(); @@ -853,7 +1069,8 @@ * An optional vocabulary ID to restrict the child search. * * @return - * An array of term objects which are the children of the term $tid. + * An array of term objects that are the children of the term $tid, or an + * empty array when no children exist. */ function taxonomy_get_children($tid, $vid = 0) { $children = &drupal_static(__FUNCTION__, array()); @@ -866,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(); @@ -903,8 +1120,8 @@ $parents = &drupal_static(__FUNCTION__ . ':parents', array()); $terms = &drupal_static(__FUNCTION__ . ':terms', array()); - // We cache trees, so it's not CPU-intensive to call get_tree() on a term - // and its children, too. + // We cache trees, so it's not CPU-intensive to call taxonomy_get_tree() on a + // term and its children, too. if (!isset($children[$vid])) { $children[$vid] = array(); $parents[$vid] = array(); @@ -914,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) @@ -957,9 +1174,9 @@ break; } $term = $load_entities ? $term_entities[$child] : $terms[$vid][$child]; - if (count($parents[$vid][$term->tid]) > 1) { - // We have a term with multi parents here. Clone the term, - // so that the depth attribute remains correct. + if (isset($parents[$vid][$term->tid])) { + // Clone the term so that the depth attribute remains correct + // in the event of multiple parents. $term = clone $term; } $term->depth = $depth; @@ -1000,14 +1217,27 @@ * Provides a case-insensitive and trimmed mapping, to maximize the * likelihood of a successful match. * - * @param name + * @param $name * Name of the term to search for. + * @param $vocabulary + * (optional) Vocabulary machine name to limit the search. Defaults to NULL. * * @return * An array of matching term objects. */ -function taxonomy_get_term_by_name($name) { - return taxonomy_term_load_multiple(array(), array('name' => trim($name))); +function taxonomy_get_term_by_name($name, $vocabulary = NULL) { + $conditions = array('name' => trim($name)); + if (isset($vocabulary)) { + $vocabularies = taxonomy_vocabulary_get_names(); + if (isset($vocabularies[$vocabulary])) { + $conditions['vid'] = $vocabularies[$vocabulary]->vid; + } + else { + // Return an empty array when filtering by a non-existing vocabulary. + return array(); + } + } + return taxonomy_term_load_multiple(array(), $conditions); } /** @@ -1021,12 +1251,12 @@ 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(); foreach ($query_conditions as $key => $condition) { - if ($condition['field'] == 'base.name') { + if (is_array($condition) && $condition['field'] == 'base.name') { $query_conditions[$key]['operator'] = 'LIKE'; $query_conditions[$key]['value'] = db_like($query_conditions[$key]['value']); } @@ -1089,7 +1319,8 @@ * this function. * * @return - * An array of term objects, indexed by tid. + * An array of term objects, indexed by tid. When no results are found, an + * empty array is returned. * * @todo Remove $conditions in Drupal 8. */ @@ -1127,6 +1358,8 @@ * @return * The vocabulary object with all of its metadata, if exists, FALSE otherwise. * Results are statically cached. + * + * @see taxonomy_vocabulary_machine_name_load() */ function taxonomy_vocabulary_load($vid) { $vocabularies = taxonomy_vocabulary_load_multiple(array($vid)); @@ -1142,6 +1375,8 @@ * @return * The vocabulary object with all of its metadata, if exists, FALSE otherwise. * Results are statically cached. + * + * @see taxonomy_vocabulary_load() */ function taxonomy_vocabulary_machine_name_load($name) { $vocabularies = taxonomy_vocabulary_load_multiple(NULL, array('machine_name' => $name)); @@ -1155,7 +1390,8 @@ * A term's ID * * @return - * A term object. Results are statically cached. + * A taxonomy term object, or FALSE if the term was not found. Results are + * statically cached. */ function taxonomy_term_load($tid) { if (!is_numeric($tid)) { @@ -1186,10 +1422,11 @@ if (isset($tag->name)) { // Commas and quotes in tag names are special cases, so encode 'em. if (strpos($tag->name, ',') !== FALSE || strpos($tag->name, '"') !== FALSE) { - $tag->name = '"' . str_replace('"', '""', $tag->name) . '"'; + $typed_tags[] = '"' . str_replace('"', '""', $tag->name) . '"'; + } + else { + $typed_tags[] = $tag->name; } - - $typed_tags[] = $tag->name; } } } @@ -1256,7 +1493,7 @@ /** * Implements hook_options_list(). */ -function taxonomy_options_list($field) { +function taxonomy_options_list($field, $instance, $entity_type, $entity) { $function = !empty($field['settings']['options_list_callback']) ? $field['settings']['options_list_callback'] : 'taxonomy_allowed_values'; return $function($field); } @@ -1313,7 +1550,7 @@ if (!$validate) { $errors[$field['field_name']][$langcode][$delta][] = array( 'error' => 'taxonomy_term_reference_illegal_value', - 'message' => t('%name: illegal value.', array('%name' => t($instance['label']))), + 'message' => t('%name: illegal value.', array('%name' => $instance['label'])), ); } } @@ -1343,6 +1580,10 @@ 'label' => t('Plain text'), 'field types' => array('taxonomy_term_reference'), ), + 'taxonomy_term_reference_rss_category' => array( + 'label' => t('RSS category'), + 'field types' => array('taxonomy_term_reference'), + ), ); } @@ -1385,6 +1626,18 @@ ); } break; + + case 'taxonomy_term_reference_rss_category': + foreach ($items as $delta => $item) { + $entity->rss_elements[] = array( + 'key' => 'category', + 'value' => $item['tid'] != 'autocreate' ? $item['taxonomy_term']->name : $item['name'], + 'attributes' => array( + 'domain' => $item['tid'] != 'autocreate' ? url('taxonomy/term/' . $item['tid'], array('absolute' => TRUE)) : '', + ), + ); + } + break; } return $element; @@ -1444,7 +1697,7 @@ $items[$id][$delta]['taxonomy_term'] = $terms[$item['tid']]; } // Terms to be created are not in $terms, but are still legitimate. - else if ($item['tid'] == 'autocreate') { + elseif ($item['tid'] == 'autocreate') { // Leave the item in place. } // Otherwise, unset the instance value, since the term does not exist. @@ -1463,15 +1716,18 @@ } /** - * 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 check_plain($term->name); + return $term->name; } /** @@ -1655,48 +1911,75 @@ } /** - * Implements hook_field_insert(). + * Implements hook_node_insert(). */ -function taxonomy_field_insert($entity_type, $entity, $field, $instance, $langcode, &$items) { - // We maintain a denormalized table of term/node relationships, containing - // only data for current, published nodes. - if (variable_get('taxonomy_maintain_index_table', TRUE) && $field['storage']['type'] == 'field_sql_storage' && $entity_type == 'node' && $entity->status) { - $query = db_insert('taxonomy_index')->fields(array('nid', 'tid', 'sticky', 'created', )); - foreach ($items as $item) { - $query->values(array( - 'nid' => $entity->nid, - 'tid' => $item['tid'], - 'sticky' => $entity->sticky, - 'created' => $entity->created, - )); - } - $query->execute(); - } +function taxonomy_node_insert($node) { + // Add taxonomy index entries for the node. + taxonomy_build_node_index($node); } /** - * Implements hook_field_update(). + * Builds and inserts taxonomy index entries for a given node. + * + * The index lists all terms that are related to a given node entity, and is + * therefore maintained at the entity level. + * + * @param $node + * The node object. */ -function taxonomy_field_update($entity_type, $entity, $field, $instance, $langcode, &$items) { - if (variable_get('taxonomy_maintain_index_table', TRUE) && $field['storage']['type'] == 'field_sql_storage' && $entity_type == 'node') { - $first_call = &drupal_static(__FUNCTION__, array()); - - // We don't maintain data for old revisions, so clear all previous values - // from the table. Since this hook runs once per field, per object, make - // sure we only wipe values once. - if (!isset($first_call[$entity->nid])) { - $first_call[$entity->nid] = FALSE; - db_delete('taxonomy_index')->condition('nid', $entity->nid)->execute(); +function taxonomy_build_node_index($node) { + // We maintain a denormalized table of term/node relationships, containing + // only data for current, published nodes. + $status = NULL; + if (variable_get('taxonomy_maintain_index_table', TRUE)) { + // If a node property is not set in the node object when node_save() is + // called, the old value from $node->original is used. + if (!empty($node->original)) { + $status = (int)(!empty($node->status) || (!isset($node->status) && !empty($node->original->status))); + $sticky = (int)(!empty($node->sticky) || (!isset($node->sticky) && !empty($node->original->sticky))); + } + else { + $status = (int)(!empty($node->status)); + $sticky = (int)(!empty($node->sticky)); + } + } + // We only maintain the taxonomy index for published nodes. + if ($status) { + // Collect a unique list of all the term IDs from all node fields. + $tid_all = array(); + foreach (field_info_instances('node', $node->type) as $instance) { + $field_name = $instance['field_name']; + $field = field_info_field($field_name); + if ($field['module'] == 'taxonomy' && $field['storage']['type'] == 'field_sql_storage') { + // If a field value is not set in the node object when node_save() is + // called, the old value from $node->original is used. + if (isset($node->{$field_name})) { + $items = $node->{$field_name}; + } + elseif (isset($node->original->{$field_name})) { + $items = $node->original->{$field_name}; + } + else { + continue; + } + foreach (field_available_languages('node', $field) as $langcode) { + if (!empty($items[$langcode])) { + foreach ($items[$langcode] as $item) { + $tid_all[$item['tid']] = $item['tid']; + } + } + } + } } - // Only save data to the table if the node is published. - if ($entity->status) { + // Insert index entries for all the node's terms. + if (!empty($tid_all)) { $query = db_insert('taxonomy_index')->fields(array('nid', 'tid', 'sticky', 'created')); - foreach ($items as $item) { + foreach ($tid_all as $tid) { $query->values(array( - 'nid' => $entity->nid, - 'tid' => $item['tid'], - 'sticky' => $entity->sticky, - 'created' => $entity->created, + 'nid' => $node->nid, + 'tid' => $tid, + 'sticky' => $sticky, + 'created' => $node->created, )); } $query->execute(); @@ -1705,11 +1988,30 @@ } /** + * Implements hook_node_update(). + */ +function taxonomy_node_update($node) { + // Always rebuild the node's taxonomy index entries on node save. + taxonomy_delete_node_index($node); + taxonomy_build_node_index($node); +} + +/** * Implements hook_node_delete(). */ function taxonomy_node_delete($node) { + // Clean up the {taxonomy_index} table when nodes are deleted. + taxonomy_delete_node_index($node); +} + +/** + * Deletes taxonomy index entries for a given node. + * + * @param $node + * The node object. + */ +function taxonomy_delete_node_index($node) { if (variable_get('taxonomy_maintain_index_table', TRUE)) { - // Clean up the {taxonomy_index} table when nodes are deleted. db_delete('taxonomy_index')->condition('nid', $node->nid)->execute(); } } @@ -1725,5 +2027,37 @@ } /** - * @} End of "defgroup taxonomy_index" + * @} End of "defgroup taxonomy_index". + */ + +/** + * Implements hook_entity_query_alter(). + * + * Converts EntityFieldQuery instances on taxonomy terms that have an entity + * condition on term bundles (vocabulary machine names). Since the vocabulary + * machine name is not present in the {taxonomy_term_data} table itself, we have + * to convert the bundle condition into a property condition of vocabulary IDs + * to match against {taxonomy_term_data}.vid. */ +function taxonomy_entity_query_alter($query) { + $conditions = &$query->entityConditions; + + // Alter only taxonomy term queries with bundle conditions. + if (isset($conditions['entity_type']) && $conditions['entity_type']['value'] == 'taxonomy_term' && isset($conditions['bundle'])) { + // Convert vocabulary machine names to vocabulary IDs. + $vocabulary_data = taxonomy_vocabulary_get_names(); + $vids = array(); + if (is_array($conditions['bundle']['value'])) { + foreach ($conditions['bundle']['value'] as $vocabulary_machine_name) { + $vids[] = $vocabulary_data[$vocabulary_machine_name]->vid; + } + } + else { + $vocabulary_machine_name = $conditions['bundle']['value']; + $vids = $vocabulary_data[$vocabulary_machine_name]->vid; + } + + $query->propertyCondition('vid', $vids, $conditions['bundle']['operator']); + unset($conditions['bundle']); + } +} diff -Naur drupal-7.0/modules/taxonomy/taxonomy.pages.inc drupal-7.66/modules/taxonomy/taxonomy.pages.inc --- drupal-7.0/modules/taxonomy/taxonomy.pages.inc 2010-11-23 03:56:10.000000000 +0100 +++ drupal-7.66/modules/taxonomy/taxonomy.pages.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ name); + // Build breadcrumb based on the hierarchy of the term. $current = (object) array( 'tid' => $term->tid, @@ -31,15 +35,22 @@ drupal_set_breadcrumb($breadcrumb); drupal_add_feed('taxonomy/term/' . $term->tid . '/feed', 'RSS - ' . $term->name); - $build = array(); - // Add term heading if the term has a description - if (!empty($term->description)) { - $build['term_heading'] = array( - '#prefix' => '
          ', - '#suffix' => '
          ', - 'term' => taxonomy_term_view($term, 'full'), - ); - } + // Set the term path as the canonical URL to prevent duplicate content. + $uri = entity_uri('taxonomy_term', $term); + drupal_add_html_head_link(array('rel' => 'canonical', 'href' => url($uri['path'], $uri['options'])), TRUE); + // Set the non-aliased path as a default shortlink. + drupal_add_html_head_link(array('rel' => 'shortlink', 'href' => url($uri['path'], array_merge($uri['options'], array('alias' => TRUE)))), TRUE); + + // Normally we would call taxonomy_term_show() here, but for backwards + // compatibility in Drupal 7 we do not want to do that (it produces different + // data structures and HTML markup than what Drupal 7 released with). Calling + // taxonomy_term_view() directly provides essentially the same thing, but + // allows us to wrap the rendered term in our desired array structure. + $build['term_heading'] = array( + '#prefix' => '
          ', + '#suffix' => '
          ', + 'term' => taxonomy_term_view($term, 'full'), + ); if ($nids = taxonomy_select_nodes($term->tid, TRUE, variable_get('default_nodes_main', 10))) { $nodes = node_load_multiple($nids); @@ -47,7 +58,7 @@ $build['pager'] = array( '#theme' => 'pager', '#weight' => 5, - ); + ); } else { $build['no_content'] = array( @@ -77,16 +88,57 @@ } /** - * Helper function for autocompletion + * Page callback: Outputs JSON for taxonomy autocomplete suggestions. + * + * Path: taxonomy/autocomplete + * + * This callback outputs term name suggestions in response to Ajax requests + * made by the taxonomy autocomplete widget for taxonomy term reference + * fields. The output is a JSON object of plain-text term suggestions, keyed by + * the user-entered value with the completed term name appended. Term names + * containing commas are wrapped in quotes. + * + * For example, suppose the user has entered the string 'red fish, blue' in the + * field, and there are two taxonomy terms, 'blue fish' and 'blue moon'. The + * JSON output would have the following structure: + * @code + * { + * "red fish, blue fish": "blue fish", + * "red fish, blue moon": "blue moon", + * }; + * @endcode + * + * @param $field_name + * The name of the term reference field. + * @param $tags_typed + * (optional) A comma-separated list of term names entered in the + * autocomplete form element. Only the last term is used for autocompletion. + * Defaults to '' (an empty string). + * + * @see taxonomy_menu() + * @see taxonomy_field_widget_info() */ -function taxonomy_autocomplete($field_name, $tags_typed = '') { - $field = field_info_field($field_name); +function taxonomy_autocomplete($field_name = '', $tags_typed = '') { + // If the request has a '/' in the search text, then the menu system will have + // split it into multiple arguments, recover the intended $tags_typed. + $args = func_get_args(); + // Shift off the $field_name argument. + array_shift($args); + $tags_typed = implode('/', $args); + + // Make sure the field exists and is a taxonomy field. + if (!($field = field_info_field($field_name)) || $field['type'] !== 'taxonomy_term_reference') { + // Error string. The JavaScript handler will realize this is not JSON and + // will display it as debugging information. + print t('Taxonomy field @field_name not found.', array('@field_name' => $field_name)); + exit; + } // The user enters a comma-separated list of tags. We only autocomplete the last tag. $tags_typed = drupal_explode_tags($tags_typed); $tag_last = drupal_strtolower(array_pop($tags_typed)); - $matches = array(); + $term_matches = array(); if ($tag_last != '') { // Part of the criteria for the query come from the field's own settings. @@ -98,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)) { @@ -113,18 +165,15 @@ ->execute() ->fetchAllKeyed(); - $prefix = count($tags_typed) ? implode(', ', $tags_typed) . ', ' : ''; + $prefix = count($tags_typed) ? drupal_implode_tags($tags_typed) . ', ' : ''; - $term_matches = array(); foreach ($tags_return as $tid => $name) { $n = $name; // Term names containing commas or quotes must be wrapped in quotes. if (strpos($name, ',') !== FALSE || strpos($name, '"') !== FALSE) { $n = '"' . str_replace('"', '""', $name) . '"'; } - else { - $term_matches[$prefix . $n] = check_plain($name); - } + $term_matches[$prefix . $n] = check_plain($name); } } diff -Naur drupal-7.0/modules/taxonomy/taxonomy.test drupal-7.66/modules/taxonomy/taxonomy.test --- drupal-7.0/modules/taxonomy/taxonomy.test 2010-12-07 06:20:08.000000000 +0100 +++ drupal-7.66/modules/taxonomy/taxonomy.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,14 +1,13 @@ randomName(); $edit['machine_name'] = $machine_name; $this->drupalPost(NULL, $edit, t('Save')); - $this->assertRaw(t('Created new vocabulary %name.', array('%name' => $edit['name'])), t('Vocabulary created successfully')); + $this->assertRaw(t('Created new vocabulary %name.', array('%name' => $edit['name'])), 'Vocabulary created successfully.'); // Edit the vocabulary. $this->drupalGet('admin/structure/taxonomy'); - $this->assertText($edit['name'], t('Vocabulary found in the vocabulary overview listing.')); + $this->assertText($edit['name'], 'Vocabulary found in the vocabulary overview listing.'); $this->clickLink(t('edit vocabulary')); $edit = array(); $edit['name'] = $this->randomName(); $this->drupalPost(NULL, $edit, t('Save')); $this->drupalGet('admin/structure/taxonomy'); - $this->assertText($edit['name'], t('Vocabulary found in the vocabulary overview listing.')); + $this->assertText($edit['name'], 'Vocabulary found in the vocabulary overview listing.'); // Try to submit a vocabulary with a duplicate machine name. $edit['machine_name'] = $machine_name; @@ -98,6 +98,18 @@ $edit['machine_name'] = '!&^%'; $this->drupalPost('admin/structure/taxonomy/add', $edit, t('Save')); $this->assertText(t('The machine-readable name must contain only lowercase letters, numbers, and underscores.')); + + // Ensure that vocabulary titles are escaped properly. + $edit = array(); + $edit['name'] = 'Don\'t Panic'; + $edit['description'] = $this->randomName(); + $edit['machine_name'] = 'don_t_panic'; + $this->drupalPost('admin/structure/taxonomy/add', $edit, t('Save')); + + $site_name = variable_get('site_name', 'Drupal'); + $this->drupalGet('admin/structure/taxonomy/don_t_panic'); + $this->assertTitle(t('Don\'t Panic | @site-name', array('@site-name' => $site_name))); + $this->assertNoTitle(t('Don't Panic | @site-name', array('@site-name' => $site_name))); } /** @@ -124,7 +136,7 @@ // Check that the weights are saved in the database correctly. foreach ($vocabularies as $key => $vocabulary) { - $this->assertEqual($new_vocabularies[$key]->weight, $vocabularies[$key]->weight, t('The vocabulary weight was changed.')); + $this->assertEqual($new_vocabularies[$key]->weight, $vocabularies[$key]->weight, 'The vocabulary weight was changed.'); } } @@ -138,10 +150,10 @@ taxonomy_vocabulary_delete($key); } // Confirm that no vocabularies are found in the database. - $this->assertFalse(taxonomy_get_vocabularies(), t('No vocabularies found in the database')); + $this->assertFalse(taxonomy_get_vocabularies(), 'No vocabularies found in the database.'); $this->drupalGet('admin/structure/taxonomy'); // Check the default message for no vocabularies. - $this->assertText(t('No vocabularies available.'), t('No vocabularies were found.')); + $this->assertText(t('No vocabularies available.'), 'No vocabularies were found.'); } /** @@ -154,34 +166,35 @@ 'machine_name' => drupal_strtolower($this->randomName()), ); $this->drupalPost('admin/structure/taxonomy/add', $edit, t('Save')); - $this->assertText(t('Created new vocabulary'), t('New vocabulary was created.')); + $this->assertText(t('Created new vocabulary'), 'New vocabulary was created.'); // Check the created vocabulary. $vocabularies = taxonomy_get_vocabularies(); - $vid = $vocabularies[count($vocabularies)-1]->vid; + $vocabularies_keys = array_keys($vocabularies); + $vid = $vocabularies[end($vocabularies_keys)]->vid; entity_get_controller('taxonomy_vocabulary')->resetCache(); $vocabulary = taxonomy_vocabulary_load($vid); - $this->assertTrue($vocabulary, t('Vocabulary found in database')); + $this->assertTrue($vocabulary, 'Vocabulary found in database.'); // Delete the vocabulary. $edit = array(); $this->drupalPost('admin/structure/taxonomy/' . $vocabulary->machine_name . '/edit', $edit, t('Delete')); - $this->assertRaw(t('Are you sure you want to delete the vocabulary %name?', array('%name' => $vocabulary->name)), t('[confirm deletion] Asks for confirmation.')); - $this->assertText(t('Deleting a vocabulary will delete all the terms in it. This action cannot be undone.'), t('[confirm deletion] Inform that all terms will be deleted.')); + $this->assertRaw(t('Are you sure you want to delete the vocabulary %name?', array('%name' => $vocabulary->name)), '[confirm deletion] Asks for confirmation.'); + $this->assertText(t('Deleting a vocabulary will delete all the terms in it. This action cannot be undone.'), '[confirm deletion] Inform that all terms will be deleted.'); // Confirm deletion. $this->drupalPost(NULL, NULL, t('Delete')); - $this->assertRaw(t('Deleted vocabulary %name.', array('%name' => $vocabulary->name)), t('Vocabulary deleted')); + $this->assertRaw(t('Deleted vocabulary %name.', array('%name' => $vocabulary->name)), 'Vocabulary deleted.'); entity_get_controller('taxonomy_vocabulary')->resetCache(); - $this->assertFalse(taxonomy_vocabulary_load($vid), t('Vocabulary is not found in the database')); + $this->assertFalse(taxonomy_vocabulary_load($vid), 'Vocabulary is not found in the database.'); } -} +} /** * Tests for taxonomy vocabulary functions. */ -class TaxonomyVocabularyUnitTest extends TaxonomyWebTestCase { +class TaxonomyVocabularyTestCase extends TaxonomyWebTestCase { public static function getInfo() { return array( @@ -208,15 +221,15 @@ $vid = count($vocabularies) + 1; $vocabulary = taxonomy_vocabulary_load($vid); // This should not return an object because no such vocabulary exists. - $this->assertTrue(empty($vocabulary), t('No object loaded.')); + $this->assertTrue(empty($vocabulary), 'No object loaded.'); // Create a new vocabulary. $this->createVocabulary(); // Load the vocabulary with the same $vid from earlier. // This should return a vocabulary object since it now matches a real vid. $vocabulary = taxonomy_vocabulary_load($vid); - $this->assertTrue(!empty($vocabulary) && is_object($vocabulary), t('Vocabulary is an object')); - $this->assertTrue($vocabulary->vid == $vid, t('Valid vocabulary vid is the same as our previously invalid one.')); + $this->assertTrue(!empty($vocabulary) && is_object($vocabulary), 'Vocabulary is an object.'); + $this->assertEqual($vocabulary->vid, $vid, 'Valid vocabulary vid is the same as our previously invalid one.'); } /** @@ -258,8 +271,8 @@ */ function testTaxonomyVocabularyLoadStaticReset() { $original_vocabulary = taxonomy_vocabulary_load($this->vocabulary->vid); - $this->assertTrue(is_object($original_vocabulary), t('Vocabulary loaded successfully')); - $this->assertEqual($this->vocabulary->name, $original_vocabulary->name, t('Vocabulary loaded successfully')); + $this->assertTrue(is_object($original_vocabulary), 'Vocabulary loaded successfully.'); + $this->assertEqual($this->vocabulary->name, $original_vocabulary->name, 'Vocabulary loaded successfully.'); // Change the name and description. $vocabulary = $original_vocabulary; @@ -275,7 +288,7 @@ // Delete the vocabulary. taxonomy_vocabulary_delete($this->vocabulary->vid); $vocabularies = taxonomy_get_vocabularies(); - $this->assertTrue(!isset($vocabularies[$this->vocabulary->vid]), t('The vocabulary was deleted')); + $this->assertTrue(!isset($vocabularies[$this->vocabulary->vid]), 'The vocabulary was deleted.'); } /** @@ -302,28 +315,28 @@ // Fetch the names for all vocabularies, confirm that they are keyed by // machine name. $names = taxonomy_vocabulary_get_names(); - $this->assertEqual($names[$vocabulary1->machine_name]->name, $vocabulary1->name, t('Vocabulary 1 name found.')); + $this->assertEqual($names[$vocabulary1->machine_name]->name, $vocabulary1->name, 'Vocabulary 1 name found.'); // Fetch all of the vocabularies using taxonomy_get_vocabularies(). // Confirm that the vocabularies are ordered by weight. $vocabularies = taxonomy_get_vocabularies(); - $this->assertEqual(array_shift($vocabularies)->vid, $vocabulary1->vid, t('Vocabulary was found in the vocabularies array.')); - $this->assertEqual(array_shift($vocabularies)->vid, $vocabulary2->vid, t('Vocabulary was found in the vocabularies array.')); - $this->assertEqual(array_shift($vocabularies)->vid, $vocabulary3->vid, t('Vocabulary was found in the vocabularies array.')); + $this->assertEqual(array_shift($vocabularies)->vid, $vocabulary1->vid, 'Vocabulary was found in the vocabularies array.'); + $this->assertEqual(array_shift($vocabularies)->vid, $vocabulary2->vid, 'Vocabulary was found in the vocabularies array.'); + $this->assertEqual(array_shift($vocabularies)->vid, $vocabulary3->vid, 'Vocabulary was found in the vocabularies array.'); // Fetch the vocabularies with taxonomy_vocabulary_load_multiple(), specifying IDs. // Ensure they are returned in the same order as the original array. $vocabularies = taxonomy_vocabulary_load_multiple(array($vocabulary3->vid, $vocabulary2->vid, $vocabulary1->vid)); - $this->assertEqual(array_shift($vocabularies)->vid, $vocabulary3->vid, t('Vocabulary loaded successfully by ID.')); - $this->assertEqual(array_shift($vocabularies)->vid, $vocabulary2->vid, t('Vocabulary loaded successfully by ID.')); - $this->assertEqual(array_shift($vocabularies)->vid, $vocabulary1->vid, t('Vocabulary loaded successfully by ID.')); + $this->assertEqual(array_shift($vocabularies)->vid, $vocabulary3->vid, 'Vocabulary loaded successfully by ID.'); + $this->assertEqual(array_shift($vocabularies)->vid, $vocabulary2->vid, 'Vocabulary loaded successfully by ID.'); + $this->assertEqual(array_shift($vocabularies)->vid, $vocabulary1->vid, 'Vocabulary loaded successfully by ID.'); // Fetch vocabulary 1 by name. $vocabulary = current(taxonomy_vocabulary_load_multiple(array(), array('name' => $vocabulary1->name))); - $this->assertTrue($vocabulary->vid == $vocabulary1->vid, t('Vocabulary loaded successfully by name.')); + $this->assertEqual($vocabulary->vid, $vocabulary1->vid, 'Vocabulary loaded successfully by name.'); // Fetch vocabulary 1 by name and ID. - $this->assertTrue(current(taxonomy_vocabulary_load_multiple(array($vocabulary1->vid), array('name' => $vocabulary1->name)))->vid == $vocabulary1->vid, t('Vocabulary loaded successfully by name and ID.')); + $this->assertEqual(current(taxonomy_vocabulary_load_multiple(array($vocabulary1->vid), array('name' => $vocabulary1->name)))->vid, $vocabulary1->vid, 'Vocabulary loaded successfully by name and ID.'); } /** @@ -349,14 +362,48 @@ taxonomy_vocabulary_save($this->vocabulary); // Check that the field instance is still attached to the vocabulary. - $this->assertTrue(field_info_instance('taxonomy_term', 'field_test', $new_name), t('The bundle name was updated correctly.')); + $this->assertTrue(field_info_instance('taxonomy_term', 'field_test', $new_name), 'The bundle name was updated correctly.'); } + + /** + * Test uninstall and reinstall of the taxonomy module. + */ + function testUninstallReinstall() { + // Fields and field instances attached to taxonomy term bundles should be + // removed when the module is uninstalled. + $this->field_name = drupal_strtolower($this->randomName() . '_field_name'); + $this->field = array('field_name' => $this->field_name, 'type' => 'text', 'cardinality' => 4); + $this->field = field_create_field($this->field); + $this->instance = array( + 'field_name' => $this->field_name, + 'entity_type' => 'taxonomy_term', + 'bundle' => $this->vocabulary->machine_name, + 'label' => $this->randomName() . '_label', + ); + field_create_instance($this->instance); + + module_disable(array('taxonomy')); + require_once DRUPAL_ROOT . '/includes/install.inc'; + drupal_uninstall_modules(array('taxonomy')); + module_enable(array('taxonomy')); + + // Now create a vocabulary with the same name. All field instances + // connected to this vocabulary name should have been removed when the + // module was uninstalled. Creating a new field with the same name and + // an instance of this field on the same bundle name should be successful. + unset($this->vocabulary->vid); + taxonomy_vocabulary_save($this->vocabulary); + unset($this->field['id']); + field_create_field($this->field); + field_create_instance($this->instance); + } + } /** * Unit tests for taxonomy term functions. */ -class TaxonomyTermUnitTest extends TaxonomyWebTestCase { +class TaxonomyTermFunctionTestCase extends TaxonomyWebTestCase { public static function getInfo() { return array( @@ -372,11 +419,65 @@ // Delete a valid term. taxonomy_term_delete($valid_term->tid); $terms = taxonomy_term_load_multiple(array(), array('vid' => $vocabulary->vid)); - $this->assertTrue(empty($terms), 'Vocabulary is empty after deletion'); + $this->assertTrue(empty($terms), 'Vocabulary is empty after deletion.'); // Delete an invalid term. Should not throw any notices. taxonomy_term_delete(42); } + + /** + * Test a taxonomy with terms that have multiple parents of different depths. + */ + function testTaxonomyVocabularyTree() { + // Create a new vocabulary with 6 terms. + $vocabulary = $this->createVocabulary(); + $term = array(); + for ($i = 0; $i < 6; $i++) { + $term[$i] = $this->createTerm($vocabulary); + } + + // $term[2] is a child of 1 and 5. + $term[2]->parent = array($term[1]->tid, $term[5]->tid); + taxonomy_term_save($term[2]); + // $term[3] is a child of 2. + $term[3]->parent = array($term[2]->tid); + taxonomy_term_save($term[3]); + // $term[5] is a child of 4. + $term[5]->parent = array($term[4]->tid); + taxonomy_term_save($term[5]); + + /** + * Expected tree: + * term[0] | depth: 0 + * term[1] | depth: 0 + * -- term[2] | depth: 1 + * ---- term[3] | depth: 2 + * term[4] | depth: 0 + * -- term[5] | depth: 1 + * ---- term[2] | depth: 2 + * ------ term[3] | depth: 3 + */ + // Count $term[1] parents with $max_depth = 1. + $tree = taxonomy_get_tree($vocabulary->vid, $term[1]->tid, 1); + $this->assertEqual(1, count($tree), 'We have one parent with depth 1.'); + + // Count all vocabulary tree elements. + $tree = taxonomy_get_tree($vocabulary->vid); + $this->assertEqual(8, count($tree), 'We have all vocabulary tree elements.'); + + // Count elements in every tree depth. + foreach ($tree as $element) { + if (!isset($depth_count[$element->depth])) { + $depth_count[$element->depth] = 0; + } + $depth_count[$element->depth]++; + } + $this->assertEqual(3, $depth_count[0], 'Three elements in taxonomy tree depth 0.'); + $this->assertEqual(2, $depth_count[1], 'Two elements in taxonomy tree depth 1.'); + $this->assertEqual(2, $depth_count[2], 'Two elements in taxonomy tree depth 2.'); + $this->assertEqual(1, $depth_count[3], 'One element in taxonomy tree depth 3.'); + } + } /** @@ -412,8 +513,9 @@ $this->drupalPost('node/add/article', $edit, t('Save')); // Checks that the node has been saved. $node = $this->drupalGetNodeByTitle($edit['title']); - $this->assertEqual($node->created, strtotime($edit['date']), t('Legacy node was saved with the right date.')); + $this->assertEqual($node->created, strtotime($edit['date']), 'Legacy node was saved with the right date.'); } + } /** @@ -474,6 +576,10 @@ $term1 = $this->createTerm($this->vocabulary); $term2 = $this->createTerm($this->vocabulary); + // Check that hierarchy is flat. + $vocabulary = taxonomy_vocabulary_load($this->vocabulary->vid); + $this->assertEqual(0, $vocabulary->hierarchy, 'Vocabulary is flat.'); + // Edit $term2, setting $term1 as parent. $edit = array(); $edit['parent[]'] = array($term1->tid); @@ -482,21 +588,21 @@ // Check the hierarchy. $children = taxonomy_get_children($term1->tid); $parents = taxonomy_get_parents($term2->tid); - $this->assertTrue(isset($children[$term2->tid]), t('Child found correctly.')); - $this->assertTrue(isset($parents[$term1->tid]), t('Parent found correctly.')); + $this->assertTrue(isset($children[$term2->tid]), 'Child found correctly.'); + $this->assertTrue(isset($parents[$term1->tid]), 'Parent found correctly.'); // Load and save a term, confirming that parents are still set. $term = taxonomy_term_load($term2->tid); taxonomy_term_save($term); $parents = taxonomy_get_parents($term2->tid); - $this->assertTrue(isset($parents[$term1->tid]), t('Parent found correctly.')); + $this->assertTrue(isset($parents[$term1->tid]), 'Parent found correctly.'); // Create a third term and save this as a parent of term2. $term3 = $this->createTerm($this->vocabulary); $term2->parent = array($term1->tid, $term3->tid); taxonomy_term_save($term2); $parents = taxonomy_get_parents($term2->tid); - $this->assertTrue(isset($parents[$term1->tid]) && isset($parents[$term3->tid]), t('Both parents found successfully.')); + $this->assertTrue(isset($parents[$term1->tid]) && isset($parents[$term3->tid]), 'Both parents found successfully.'); } /** @@ -514,26 +620,26 @@ $langcode = LANGUAGE_NONE; $edit["title"] = $this->randomName(); $edit["body[$langcode][0][value]"] = $this->randomName(); - $edit[$this->instance['field_name'] . '[' . $langcode .'][]'] = $term1->tid; + $edit[$this->instance['field_name'] . '[' . $langcode . '][]'] = $term1->tid; $this->drupalPost('node/add/article', $edit, t('Save')); // Check that the term is displayed when the node is viewed. $node = $this->drupalGetNodeByTitle($edit["title"]); $this->drupalGet('node/' . $node->nid); - $this->assertText($term1->name, t('Term is displayed when viewing the node.')); + $this->assertText($term1->name, 'Term is displayed when viewing the node.'); // Edit the node with a different term. $edit[$this->instance['field_name'] . '[' . $langcode . '][]'] = $term2->tid; $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save')); $this->drupalGet('node/' . $node->nid); - $this->assertText($term2->name, t('Term is displayed when viewing the node.')); + $this->assertText($term2->name, 'Term is displayed when viewing the node.'); // Preview the node. $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Preview')); - $this->assertNoUniqueText($term2->name, t('Term is displayed when previewing the node.')); + $this->assertNoUniqueText($term2->name, 'Term is displayed when previewing the node.'); $this->drupalPost(NULL, NULL, t('Preview')); - $this->assertNoUniqueText($term2->name, t('Term is displayed when previewing the node again.')); + $this->assertNoUniqueText($term2->name, 'Term is displayed when previewing the node again.'); } /** @@ -546,9 +652,9 @@ $instance['bundle'] = 'page'; field_create_instance($instance); $terms = array( - $this->randomName(), - $this->randomName(), - $this->randomName(), + 'term1' => $this->randomName(), + 'term2' => $this->randomName() . ', ' . $this->randomName(), + 'term3' => $this->randomName(), ); $edit = array(); @@ -557,47 +663,130 @@ $edit["body[$langcode][0][value]"] = $this->randomName(); // Insert the terms in a comma separated list. Vocabulary 1 is a // free-tagging field created by the default profile. - $edit[$instance['field_name'] . "[$langcode]"] = implode(', ', $terms); + $edit[$instance['field_name'] . "[$langcode]"] = drupal_implode_tags($terms); // Preview and verify the terms appear but are not created. $this->drupalPost('node/add/page', $edit, t('Preview')); foreach ($terms as $term) { - $this->assertText($term, t('The term appears on the node preview')); + $this->assertText($term, 'The term appears on the node preview.'); } $tree = taxonomy_get_tree($this->vocabulary->vid); - $this->assertTrue(empty($tree), t('The terms are not created on preview.')); + $this->assertTrue(empty($tree), 'The terms are not created on preview.'); // taxonomy.module does not maintain its static caches. drupal_static_reset(); // Save, creating the terms. $this->drupalPost('node/add/page', $edit, t('Save')); - $this->assertRaw(t('@type %title has been created.', array('@type' => t('Basic page'), '%title' => $edit["title"])), t('The node was created successfully')); + $this->assertRaw(t('@type %title has been created.', array('@type' => t('Basic page'), '%title' => $edit["title"])), 'The node was created successfully.'); foreach ($terms as $term) { - $this->assertText($term, t('The term was saved and appears on the node page')); + $this->assertText($term, 'The term was saved and appears on the node page.'); } // Get the created terms. - list($term1, $term2, $term3) = taxonomy_get_tree($this->vocabulary->vid); + $term_objects = array(); + foreach ($terms as $key => $term) { + $term_objects[$key] = taxonomy_get_term_by_name($term); + $term_objects[$key] = reset($term_objects[$key]); + } - // Delete one term. - $this->drupalPost('taxonomy/term/' . $term1->tid . '/edit', array(), t('Delete')); + // 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($term2->name, $term3->name); + $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, t('The term %name appears on the node page after one term %deleted was deleted', array('%name' => $term_name, '%deleted' => $term1->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))); } - $this->assertNoText($term1->name, t('The deleted term %name does not appear on the node page.', array('%name' => $term1->name))); + $this->assertNoText($term_objects['term1']->name, format_string('The deleted term %name does not appear on the node page.', array('%name' => $term_objects['term1']->name))); + + // Test autocomplete on term 2, which contains a comma. + // The term will be quoted, and the " will be encoded in unicode (\u0022). + $input = substr($term_objects['term2']->name, 0, 3); + $this->drupalGet('taxonomy/autocomplete/taxonomy_' . $this->vocabulary->machine_name . '/' . $input); + $this->assertRaw('{"\u0022' . $term_objects['term2']->name . '\u0022":"' . $term_objects['term2']->name . '"}', format_string('Autocomplete returns term %term_name after typing the first 3 letters.', array('%term_name' => $term_objects['term2']->name))); - // Test autocomplete on term 2. - $input = substr($term2->name, 0, 3); + // Test autocomplete on term 3 - it is alphanumeric only, so no extra + // quoting. + $input = substr($term_objects['term3']->name, 0, 3); $this->drupalGet('taxonomy/autocomplete/taxonomy_' . $this->vocabulary->machine_name . '/' . $input); - $this->assertRaw('{"' . $term2->name . '":"' . $term2->name . '"}', t('Autocomplete returns term %term_name after typing the first 3 letters.', array('%term_name' => $term2->name))); + $this->assertRaw('{"' . $term_objects['term3']->name . '":"' . $term_objects['term3']->name . '"}', format_string('Autocomplete returns term %term_name after typing the first 3 letters.', array('%term_name' => $term_objects['term3']->name))); + + // Test taxonomy autocomplete with a nonexistent field. + $field_name = $this->randomName(); + $tag = $this->randomName(); + $message = t("Taxonomy field @field_name not found.", array('@field_name' => $field_name)); + $this->assertFalse(field_info_field($field_name), format_string('Field %field_name does not exist.', array('%field_name' => $field_name))); + $this->drupalGet('taxonomy/autocomplete/' . $field_name . '/' . $tag); + $this->assertRaw($message, 'Autocomplete returns correct error message when the taxonomy field does not exist.'); + + // Test the autocomplete path without passing a field_name and terms. + // This should not trigger a PHP notice. + $field_name = ''; + $message = t("Taxonomy field @field_name not found.", array('@field_name' => $field_name)); + $this->drupalGet('taxonomy/autocomplete'); + $this->assertRaw($message, 'Autocomplete returns correct error message when no taxonomy field is given.'); + } + + /** + * Tests term autocompletion edge cases with slashes in the names. + */ + function testTermAutocompletion() { + // Add a term with a slash in the name. + $first_term = $this->createTerm($this->vocabulary); + $first_term->name = '10/16/2011'; + taxonomy_term_save($first_term); + // Add another term that differs after the slash character. + $second_term = $this->createTerm($this->vocabulary); + $second_term->name = '10/17/2011'; + taxonomy_term_save($second_term); + // Add another term that has both a comma and a slash character. + $third_term = $this->createTerm($this->vocabulary); + $third_term->name = 'term with, a comma and / a slash'; + taxonomy_term_save($third_term); + + // Try to autocomplete a term name that matches both terms. + // We should get both term in a json encoded string. + $input = '10/'; + $path = 'taxonomy/autocomplete/taxonomy_'; + $path .= $this->vocabulary->machine_name . '/' . $input; + // The result order is not guaranteed, so check each term separately. + $url = url($path, array('absolute' => TRUE)); + $result = drupal_http_request($url); + $data = drupal_json_decode($result->data); + $this->assertEqual($data[$first_term->name], check_plain($first_term->name), 'Autocomplete returned the first matching term.'); + $this->assertEqual($data[$second_term->name], check_plain($second_term->name), 'Autocomplete returned the second matching term.'); + + // Try to autocomplete a term name that matches first term. + // We should only get the first term in a json encoded string. + $input = '10/16'; + $url = 'taxonomy/autocomplete/taxonomy_'; + $url .= $this->vocabulary->machine_name . '/' . $input; + $this->drupalGet($url); + $target = array($first_term->name => check_plain($first_term->name)); + $this->assertRaw(drupal_json_encode($target), 'Autocomplete returns only the expected matching term.'); + + // Try to autocomplete a term name with both a comma and a slash. + $input = '"term with, comma and / a'; + $url = 'taxonomy/autocomplete/taxonomy_'; + $url .= $this->vocabulary->machine_name . '/' . $input; + $this->drupalGet($url); + $n = $third_term->name; + // Term names containing commas or quotes must be wrapped in quotes. + if (strpos($third_term->name, ',') !== FALSE || strpos($third_term->name, '"') !== FALSE) { + $n = '"' . str_replace('"', '""', $third_term->name) . '"'; + } + $target = array($n => check_plain($third_term->name)); + $this->assertRaw(drupal_json_encode($target), 'Autocomplete returns a term containing a comma and a slash.'); } /** @@ -617,7 +806,7 @@ $terms = taxonomy_get_term_by_name($edit['name']); $term = reset($terms); - $this->assertNotNull($term, t('Term found in database')); + $this->assertNotNull($term, 'Term found in database.'); // Submitting a term takes us to the add page; we need the List page. $this->drupalGet('admin/structure/taxonomy/' . $this->vocabulary->machine_name); @@ -627,8 +816,8 @@ // the first edit link found on the listing page is to our term. $this->clickLink(t('edit')); - $this->assertRaw($edit['name'], t('The randomly generated term name is present.')); - $this->assertText($edit['description[value]'], t('The randomly generated term description is present.')); + $this->assertRaw($edit['name'], 'The randomly generated term name is present.'); + $this->assertText($edit['description[value]'], 'The randomly generated term description is present.'); $edit = array( 'name' => $this->randomName(14), @@ -640,32 +829,36 @@ // Check that the term is still present at admin UI after edit. $this->drupalGet('admin/structure/taxonomy/' . $this->vocabulary->machine_name); - $this->assertText($edit['name'], t('The randomly generated term name is present.')); + $this->assertText($edit['name'], 'The randomly generated term name is present.'); $this->assertLink(t('edit')); // View the term and check that it is correct. $this->drupalGet('taxonomy/term/' . $term->tid); - $this->assertText($edit['name'], t('The randomly generated term name is present.')); - $this->assertText($edit['description[value]'], t('The randomly generated term description is present.')); + $this->assertText($edit['name'], 'The randomly generated term name is present.'); + $this->assertText($edit['description[value]'], 'The randomly generated term description is present.'); // Did this page request display a 'term-listing-heading'? - $this->assertPattern('|class="term-listing-heading"|', 'Term page displayed the term description element.'); + $this->assertPattern('|class="taxonomy-term-description"|', 'Term page displayed the term description element.'); // Check that it does NOT show a description when description is blank. $term->description = ''; taxonomy_term_save($term); $this->drupalGet('taxonomy/term/' . $term->tid); - $this->assertNoPattern('|class="term-listing-heading"|', 'Term page did not display the term description when description was blank.'); + $this->assertNoPattern('|class="taxonomy-term-description"|', 'Term page did not display the term description when description was blank.'); // Check that the term feed page is working. $this->drupalGet('taxonomy/term/' . $term->tid . '/feed'); + // Check that the term edit page does not try to interpret additional path + // components as arguments for taxonomy_form_term(). + $this->drupalGet('taxonomy/term/' . $term->tid . '/edit/' . $this->randomName()); + // Delete the term. $this->drupalPost('taxonomy/term/' . $term->tid . '/edit', array(), t('Delete')); $this->drupalPost(NULL, NULL, t('Delete')); // Assert that the term no longer exists. $this->drupalGet('taxonomy/term/' . $term->tid); - $this->assertResponse(404, t('The taxonomy term page was not found')); + $this->assertResponse(404, 'The taxonomy term page was not found.'); } /** @@ -709,9 +902,9 @@ drupal_static_reset('taxonomy_get_treeparent'); drupal_static_reset('taxonomy_get_treeterms'); $terms = taxonomy_get_tree($this->vocabulary->vid); - $this->assertEqual($terms[0]->tid, $term2->tid, t('Term 2 was moved above term 1.')); - $this->assertEqual($terms[1]->parents, array($term2->tid), t('Term 3 was made a child of term 2.')); - $this->assertEqual($terms[2]->tid, $term1->tid, t('Term 1 was moved below term 2.')); + $this->assertEqual($terms[0]->tid, $term2->tid, 'Term 2 was moved above term 1.'); + $this->assertEqual($terms[1]->parents, array($term2->tid), 'Term 3 was made a child of term 2.'); + $this->assertEqual($terms[2]->tid, $term1->tid, 'Term 1 was moved below term 2.'); $this->drupalPost('admin/structure/taxonomy/' . $this->vocabulary->machine_name, array(), t('Reset to alphabetical')); // Submit confirmation form. @@ -721,10 +914,39 @@ drupal_static_reset('taxonomy_get_treeparent'); drupal_static_reset('taxonomy_get_treeterms'); $terms = taxonomy_get_tree($this->vocabulary->vid); - $this->assertEqual($terms[0]->tid, $term1->tid, t('Term 1 was moved to back above term 2.')); - $this->assertEqual($terms[1]->tid, $term2->tid, t('Term 2 was moved to back below term 1.')); - $this->assertEqual($terms[2]->tid, $term3->tid, t('Term 3 is still below term 2.')); - $this->assertEqual($terms[2]->parents, array($term2->tid), t('Term 3 is still a child of term 2.').var_export($terms[1]->tid,1)); + $this->assertEqual($terms[0]->tid, $term1->tid, 'Term 1 was moved to back above term 2.'); + $this->assertEqual($terms[1]->tid, $term2->tid, 'Term 2 was moved to back below term 1.'); + $this->assertEqual($terms[2]->tid, $term3->tid, 'Term 3 is still below term 2.'); + $this->assertEqual($terms[2]->parents, array($term2->tid), 'Term 3 is still a child of term 2.' . var_export($terms[1]->tid, 1)); + } + + /** + * Test saving a term with multiple parents through the UI. + */ + function testTermMultipleParentsInterface() { + // Add a new term to the vocabulary so that we can have multiple parents. + $parent = $this->createTerm($this->vocabulary); + + // Add a new term with multiple parents. + $edit = array( + 'name' => $this->randomName(12), + 'description[value]' => $this->randomName(100), + 'parent[]' => array(0, $parent->tid), + ); + // Save the new term. + $this->drupalPost('admin/structure/taxonomy/' . $this->vocabulary->machine_name . '/add', $edit, t('Save')); + + // Check that the term was successfully created. + $terms = taxonomy_get_term_by_name($edit['name']); + $term = reset($terms); + $this->assertNotNull($term, 'Term found in database.'); + $this->assertEqual($edit['name'], $term->name, 'Term name was successfully saved.'); + $this->assertEqual($edit['description[value]'], $term->description, 'Term description was successfully saved.'); + // Check that the parent tid is still there. The other parent () is + // not added by taxonomy_get_parents(). + $parents = taxonomy_get_parents($term->tid); + $parent = reset($parents); + $this->assertEqual($edit['parent[]'][1], $parent->tid, 'Term parents were successfully saved.'); } /** @@ -735,19 +957,19 @@ // Load the term with the exact name. $terms = taxonomy_get_term_by_name($term->name); - $this->assertTrue(isset($terms[$term->tid]), t('Term loaded using exact name.')); + $this->assertTrue(isset($terms[$term->tid]), 'Term loaded using exact name.'); // Load the term with space concatenated. $terms = taxonomy_get_term_by_name(' ' . $term->name . ' '); - $this->assertTrue(isset($terms[$term->tid]), t('Term loaded with extra whitespace.')); + $this->assertTrue(isset($terms[$term->tid]), 'Term loaded with extra whitespace.'); // Load the term with name uppercased. $terms = taxonomy_get_term_by_name(strtoupper($term->name)); - $this->assertTrue(isset($terms[$term->tid]), t('Term loaded with uppercased name.')); + $this->assertTrue(isset($terms[$term->tid]), 'Term loaded with uppercased name.'); // Load the term with name lowercased. $terms = taxonomy_get_term_by_name(strtolower($term->name)); - $this->assertTrue(isset($terms[$term->tid]), t('Term loaded with lowercased name.')); + $this->assertTrue(isset($terms[$term->tid]), 'Term loaded with lowercased name.'); // Try to load an invalid term name. $terms = taxonomy_get_term_by_name('Banana'); @@ -756,13 +978,354 @@ // Try to load the term using a substring of the name. $terms = taxonomy_get_term_by_name(drupal_substr($term->name, 2)); $this->assertFalse($terms); + + // Create a new term in a different vocabulary with the same name. + $new_vocabulary = $this->createVocabulary(); + $new_term = new stdClass(); + $new_term->name = $term->name; + $new_term->vid = $new_vocabulary->vid; + taxonomy_term_save($new_term); + + // Load multiple terms with the same name. + $terms = taxonomy_get_term_by_name($term->name); + $this->assertEqual(count($terms), 2, 'Two terms loaded with the same name.'); + + // Load single term when restricted to one vocabulary. + $terms = taxonomy_get_term_by_name($term->name, $this->vocabulary->machine_name); + $this->assertEqual(count($terms), 1, 'One term loaded when restricted by vocabulary.'); + $this->assertTrue(isset($terms[$term->tid]), 'Term loaded using exact name and vocabulary machine name.'); + + // Create a new term with another name. + $term2 = $this->createTerm($this->vocabulary); + + // Try to load a term by name that doesn't exist in this vocabulary but + // exists in another vocabulary. + $terms = taxonomy_get_term_by_name($term2->name, $new_vocabulary->machine_name); + $this->assertFalse($terms, 'Invalid term name restricted by vocabulary machine name not loaded.'); + + // Try to load terms filtering by a non-existing vocabulary. + $terms = taxonomy_get_term_by_name($term2->name, 'non_existing_vocabulary'); + $this->assertEqual(count($terms), 0, 'No terms loaded when restricted by a non-existing vocabulary.'); + } + +} + +/** + * Tests the rendering of term reference fields in RSS feeds. + */ +class TaxonomyRSSTestCase extends TaxonomyWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Taxonomy RSS Content.', + 'description' => 'Ensure that data added as terms appears in RSS feeds if "RSS Category" format is selected.', + 'group' => 'Taxonomy', + ); + } + + function setUp() { + parent::setUp('taxonomy'); + $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(); + + $field = array( + 'field_name' => 'taxonomy_' . $this->vocabulary->machine_name, + 'type' => 'taxonomy_term_reference', + 'cardinality' => FIELD_CARDINALITY_UNLIMITED, + 'settings' => array( + 'allowed_values' => array( + array( + 'vocabulary' => $this->vocabulary->machine_name, + 'parent' => 0, + ), + ), + ), + ); + field_create_field($field); + + $this->instance = array( + 'field_name' => 'taxonomy_' . $this->vocabulary->machine_name, + 'bundle' => 'article', + 'entity_type' => 'node', + 'widget' => array( + 'type' => 'options_select', + ), + 'display' => array( + 'default' => array( + 'type' => 'taxonomy_term_reference_link', + ), + ), + ); + field_create_instance($this->instance); + } + + /** + * Tests that terms added to nodes are displayed in core RSS feed. + * + * Create a node and assert that taxonomy terms appear in rss.xml. + */ + function testTaxonomyRSS() { + // Create two taxonomy terms. + $term1 = $this->createTerm($this->vocabulary); + + // RSS display must be added manually. + $this->drupalGet("admin/structure/types/manage/article/display"); + $edit = array( + "view_modes_custom[rss]" => '1', + ); + $this->drupalPost(NULL, $edit, t('Save')); + + // Change the format to 'RSS category'. + $this->drupalGet("admin/structure/types/manage/article/display/rss"); + $edit = array( + "fields[taxonomy_" . $this->vocabulary->machine_name . "][type]" => 'taxonomy_term_reference_rss_category', + ); + $this->drupalPost(NULL, $edit, t('Save')); + + // Post an article. + $edit = array(); + $langcode = LANGUAGE_NONE; + $edit["title"] = $this->randomName(); + $edit[$this->instance['field_name'] . '[' . $langcode . '][]'] = $term1->tid; + $this->drupalPost('node/add/article', $edit, t('Save')); + + // Check that the term is displayed when the RSS feed is viewed. + $this->drupalGet('rss.xml'); + $test_element = array( + 'key' => 'category', + 'value' => $term1->name, + 'attributes' => array( + 'domain' => url('taxonomy/term/' . $term1->tid, array('absolute' => TRUE)), + ), + ); + $this->assertRaw(format_xml_elements(array($test_element)), 'Term is displayed when viewing the rss feed.'); + } + +} + +/** + * Tests the hook implementations that maintain the taxonomy index. + */ +class TaxonomyTermIndexTestCase extends TaxonomyWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Taxonomy term index', + 'description' => 'Tests the hook implementations that maintain the taxonomy index.', + 'group' => 'Taxonomy', + ); + } + + function setUp() { + parent::setUp('taxonomy'); + + // Create an administrative user. + $this->admin_user = $this->drupalCreateUser(array('administer taxonomy', 'bypass node access')); + $this->drupalLogin($this->admin_user); + + // Create a vocabulary and add two term reference fields to article nodes. + $this->vocabulary = $this->createVocabulary(); + + $this->field_name_1 = drupal_strtolower($this->randomName()); + $this->field_1 = array( + 'field_name' => $this->field_name_1, + 'type' => 'taxonomy_term_reference', + 'cardinality' => FIELD_CARDINALITY_UNLIMITED, + 'settings' => array( + 'allowed_values' => array( + array( + 'vocabulary' => $this->vocabulary->machine_name, + 'parent' => 0, + ), + ), + ), + ); + field_create_field($this->field_1); + $this->instance_1 = array( + 'field_name' => $this->field_name_1, + 'bundle' => 'article', + 'entity_type' => 'node', + 'widget' => array( + 'type' => 'options_select', + ), + 'display' => array( + 'default' => array( + 'type' => 'taxonomy_term_reference_link', + ), + ), + ); + field_create_instance($this->instance_1); + + $this->field_name_2 = drupal_strtolower($this->randomName()); + $this->field_2 = array( + 'field_name' => $this->field_name_2, + 'type' => 'taxonomy_term_reference', + 'cardinality' => FIELD_CARDINALITY_UNLIMITED, + 'settings' => array( + 'allowed_values' => array( + array( + 'vocabulary' => $this->vocabulary->machine_name, + 'parent' => 0, + ), + ), + ), + ); + field_create_field($this->field_2); + $this->instance_2 = array( + 'field_name' => $this->field_name_2, + 'bundle' => 'article', + 'entity_type' => 'node', + 'widget' => array( + 'type' => 'options_select', + ), + 'display' => array( + 'default' => array( + 'type' => 'taxonomy_term_reference_link', + ), + ), + ); + field_create_instance($this->instance_2); + } + + /** + * Tests that the taxonomy index is maintained properly. + */ + function testTaxonomyIndex() { + // Create terms in the vocabulary. + $term_1 = $this->createTerm($this->vocabulary); + $term_2 = $this->createTerm($this->vocabulary); + + // Post an article. + $edit = array(); + $langcode = LANGUAGE_NONE; + $edit["title"] = $this->randomName(); + $edit["body[$langcode][0][value]"] = $this->randomName(); + $edit["{$this->field_name_1}[$langcode][]"] = $term_1->tid; + $edit["{$this->field_name_2}[$langcode][]"] = $term_1->tid; + $this->drupalPost('node/add/article', $edit, t('Save')); + + // Check that the term is indexed, and only once. + $node = $this->drupalGetNodeByTitle($edit["title"]); + $index_count = db_query('SELECT COUNT(*) FROM {taxonomy_index} WHERE nid = :nid AND tid = :tid', array( + ':nid' => $node->nid, + ':tid' => $term_1->tid, + ))->fetchField(); + $this->assertEqual(1, $index_count, 'Term 1 is indexed once.'); + + // Update the article to change one term. + $edit["{$this->field_name_1}[$langcode][]"] = $term_2->tid; + $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save')); + + // Check that both terms are indexed. + $index_count = db_query('SELECT COUNT(*) FROM {taxonomy_index} WHERE nid = :nid AND tid = :tid', array( + ':nid' => $node->nid, + ':tid' => $term_1->tid, + ))->fetchField(); + $this->assertEqual(1, $index_count, 'Term 1 is indexed.'); + $index_count = db_query('SELECT COUNT(*) FROM {taxonomy_index} WHERE nid = :nid AND tid = :tid', array( + ':nid' => $node->nid, + ':tid' => $term_2->tid, + ))->fetchField(); + $this->assertEqual(1, $index_count, 'Term 2 is indexed.'); + + // Update the article to change another term. + $edit["{$this->field_name_2}[$langcode][]"] = $term_2->tid; + $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save')); + + // Check that only one term is indexed. + $index_count = db_query('SELECT COUNT(*) FROM {taxonomy_index} WHERE nid = :nid AND tid = :tid', array( + ':nid' => $node->nid, + ':tid' => $term_1->tid, + ))->fetchField(); + $this->assertEqual(0, $index_count, 'Term 1 is not indexed.'); + $index_count = db_query('SELECT COUNT(*) FROM {taxonomy_index} WHERE nid = :nid AND tid = :tid', array( + ':nid' => $node->nid, + ':tid' => $term_2->tid, + ))->fetchField(); + $this->assertEqual(1, $index_count, 'Term 2 is indexed once.'); + + // Redo the above tests without interface. + $update_node = array( + 'nid' => $node->nid, + 'vid' => $node->vid, + 'uid' => $node->uid, + 'type' => $node->type, + 'title' => $this->randomName(), + ); + + // Update the article with no term changed. + $updated_node = (object) $update_node; + node_save($updated_node); + + // Check that the index was not changed. + $index_count = db_query('SELECT COUNT(*) FROM {taxonomy_index} WHERE nid = :nid AND tid = :tid', array( + ':nid' => $node->nid, + ':tid' => $term_1->tid, + ))->fetchField(); + $this->assertEqual(0, $index_count, 'Term 1 is not indexed.'); + $index_count = db_query('SELECT COUNT(*) FROM {taxonomy_index} WHERE nid = :nid AND tid = :tid', array( + ':nid' => $node->nid, + ':tid' => $term_2->tid, + ))->fetchField(); + $this->assertEqual(1, $index_count, 'Term 2 is indexed once.'); + + // Update the article to change one term. + $update_node[$this->field_name_1][$langcode] = array(array('tid' => $term_1->tid)); + $updated_node = (object) $update_node; + node_save($updated_node); + + // Check that both terms are indexed. + $index_count = db_query('SELECT COUNT(*) FROM {taxonomy_index} WHERE nid = :nid AND tid = :tid', array( + ':nid' => $node->nid, + ':tid' => $term_1->tid, + ))->fetchField(); + $this->assertEqual(1, $index_count, 'Term 1 is indexed.'); + $index_count = db_query('SELECT COUNT(*) FROM {taxonomy_index} WHERE nid = :nid AND tid = :tid', array( + ':nid' => $node->nid, + ':tid' => $term_2->tid, + ))->fetchField(); + $this->assertEqual(1, $index_count, 'Term 2 is indexed.'); + + // Update the article to change another term. + $update_node[$this->field_name_2][$langcode] = array(array('tid' => $term_1->tid)); + $updated_node = (object) $update_node; + node_save($updated_node); + + // Check that only one term is indexed. + $index_count = db_query('SELECT COUNT(*) FROM {taxonomy_index} WHERE nid = :nid AND tid = :tid', array( + ':nid' => $node->nid, + ':tid' => $term_1->tid, + ))->fetchField(); + $this->assertEqual(1, $index_count, 'Term 1 is indexed once.'); + $index_count = db_query('SELECT COUNT(*) FROM {taxonomy_index} WHERE nid = :nid AND tid = :tid', array( + ':nid' => $node->nid, + ':tid' => $term_2->tid, + ))->fetchField(); + $this->assertEqual(0, $index_count, 'Term 2 is not indexed.'); } + + /** + * Tests that there is a link to the parent term on the child term page. + */ + function testTaxonomyTermHierarchyBreadcrumbs() { + // Create two taxonomy terms and set term2 as the parent of term1. + $term1 = $this->createTerm($this->vocabulary); + $term2 = $this->createTerm($this->vocabulary); + $term1->parent = array($term2->tid); + taxonomy_term_save($term1); + + // Verify that the page breadcrumbs include a link to the parent term. + $this->drupalGet('taxonomy/term/' . $term1->tid); + $this->assertRaw(l($term2->name, 'taxonomy/term/' . $term2->tid), 'Parent term link is displayed when viewing the node.'); + } + } /** * Test the taxonomy_term_load_multiple() function. */ -class TaxonomyLoadMultipleUnitTest extends TaxonomyWebTestCase { +class TaxonomyLoadMultipleTestCase extends TaxonomyWebTestCase { public static function getInfo() { return array( @@ -795,12 +1358,12 @@ // Load the terms from the vocabulary. $terms = taxonomy_term_load_multiple(NULL, array('vid' => $vocabulary->vid)); $count = count($terms); - $this->assertTrue($count == 5, t('Correct number of terms were loaded. !count terms.', array('!count' => $count))); + $this->assertEqual($count, 5, format_string('Correct number of terms were loaded. !count terms.', array('!count' => $count))); // Load the same terms again by tid. $terms2 = taxonomy_term_load_multiple(array_keys($terms)); - $this->assertTrue($count == count($terms2), t('Five terms were loaded by tid')); - $this->assertEqual($terms, $terms2, t('Both arrays contain the same terms')); + $this->assertEqual($count, count($terms2), 'Five terms were loaded by tid.'); + $this->assertEqual($terms, $terms2, 'Both arrays contain the same terms.'); // Load the terms by tid, with a condition on vid. $terms3 = taxonomy_term_load_multiple(array_keys($terms2), array('vid' => $vocabulary->vid)); @@ -814,15 +1377,15 @@ // Load terms from the vocabulary by vid. $terms4 = taxonomy_term_load_multiple(NULL, array('vid' => $vocabulary->vid)); - $this->assertTrue(count($terms4 == 4), t('Correct number of terms were loaded.')); + $this->assertEqual(count($terms4), 4, 'Correct number of terms were loaded.'); $this->assertFalse(isset($terms4[$deleted->tid])); // Create a single term and load it by name. $term = $this->createTerm($vocabulary); $loaded_terms = taxonomy_term_load_multiple(array(), array('name' => $term->name)); - $this->assertEqual(count($loaded_terms), 1, t('One term was loaded')); + $this->assertEqual(count($loaded_terms), 1, 'One term was loaded.'); $loaded_term = reset($loaded_terms); - $this->assertEqual($term->tid, $loaded_term->tid, t('Term loaded by name successfully.')); + $this->assertEqual($term->tid, $loaded_term->tid, 'Term loaded by name successfully.'); } } @@ -840,12 +1403,16 @@ function setUp() { parent::setUp('taxonomy', 'taxonomy_test'); + module_load_include('inc', 'taxonomy', 'taxonomy.pages'); $taxonomy_admin = $this->drupalCreateUser(array('administer taxonomy')); $this->drupalLogin($taxonomy_admin); } /** - * Test that hooks are run correctly on creating, editing and deleting a term. + * Test that hooks are run correctly on creating, editing, viewing, + * and deleting a term. + * + * @see taxonomy_test.module */ function testTaxonomyTermHooks() { $vocabulary = $this->createVocabulary(); @@ -858,7 +1425,7 @@ $this->drupalPost('admin/structure/taxonomy/' . $vocabulary->machine_name . '/add', $edit, t('Save')); $terms = taxonomy_get_term_by_name($edit['name']); $term = reset($terms); - $this->assertEqual($term->antonym, $edit['antonym'], t('Antonym was loaded into the term object')); + $this->assertEqual($term->antonym, $edit['antonym'], 'Antonym was loaded into the term object.'); // Update the term with a different antonym. $edit = array( @@ -868,26 +1435,35 @@ $this->drupalPost('taxonomy/term/' . $term->tid . '/edit', $edit, t('Save')); taxonomy_terms_static_reset(); $term = taxonomy_term_load($term->tid); - $this->assertEqual($edit['antonym'], $term->antonym, t('Antonym was successfully edited')); + $this->assertEqual($edit['antonym'], $term->antonym, 'Antonym was successfully edited.'); + + // View the term and ensure that hook_taxonomy_term_view() and + // hook_entity_view() are invoked. + $term = taxonomy_term_load($term->tid); + $term_build = taxonomy_term_page($term); + $this->assertFalse(empty($term_build['term_heading']['term']['taxonomy_test_term_view_check']), 'hook_taxonomy_term_view() was invoked when viewing the term.'); + $this->assertFalse(empty($term_build['term_heading']['term']['taxonomy_test_entity_view_check']), 'hook_entity_view() was invoked when viewing the term.'); // Delete the term. taxonomy_term_delete($term->tid); $antonym = db_query('SELECT tid FROM {taxonomy_term_antonym} WHERE tid = :tid', array(':tid' => $term->tid))->fetchField(); - $this->assertFalse($antonym, t('The antonym were deleted from the database.')); + $this->assertFalse($antonym, 'The antonym were deleted from the database.'); } + } /** * Tests for taxonomy term field and formatter. */ class TaxonomyTermFieldTestCase extends TaxonomyWebTestCase { + protected $instance; protected $vocabulary; public static function getInfo() { return array( - 'name' => 'Taxonomy term reference field', - 'description' => 'Test the creation of term fields.', + 'name' => 'Taxonomy term reference field', + 'description' => 'Test the creation of term fields.', 'group' => 'Taxonomy', ); } @@ -941,10 +1517,10 @@ $entity->{$this->field_name}[$langcode][0]['tid'] = $term->tid; try { field_attach_validate('test_entity', $entity); - $this->pass(t('Correct term does not cause validation error')); + $this->pass('Correct term does not cause validation error.'); } catch (FieldValidationException $e) { - $this->fail(t('Correct term does not cause validation error')); + $this->fail('Correct term does not cause validation error.'); } $entity = field_test_create_stub_entity(); @@ -952,10 +1528,10 @@ $entity->{$this->field_name}[$langcode][0]['tid'] = $bad_term->tid; try { field_attach_validate('test_entity', $entity); - $this->fail(t('Wrong term causes validation error')); + $this->fail('Wrong term causes validation error.'); } catch (FieldValidationException $e) { - $this->pass(t('Wrong term causes validation error')); + $this->pass('Wrong term causes validation error.'); } } @@ -969,7 +1545,7 @@ // Display creation form. $langcode = LANGUAGE_NONE; $this->drupalGet('test-entity/add/test-bundle'); - $this->assertFieldByName("{$this->field_name}[$langcode]", '', t('Widget is displayed')); + $this->assertFieldByName("{$this->field_name}[$langcode]", '', 'Widget is displayed.'); // Submit with some value. $edit = array( @@ -978,7 +1554,7 @@ $this->drupalPost(NULL, $edit, t('Save')); 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)), t('Entity was created')); + $this->assertRaw(t('test_entity @id has been created.', array('@id' => $id)), 'Entity was created.'); // Display the object. $entity = field_test_entity_test_load($id); @@ -986,7 +1562,12 @@ field_attach_prepare_view('test_entity', $entities, 'full'); $entity->content = field_attach_view('test_entity', $entity, 'full'); $this->content = drupal_render($entity->content); - $this->assertText($term->name, t('Term name is displayed')); + $this->assertText($term->name, 'Term name is displayed.'); + + // Delete the vocabulary and verify that the widget is gone. + taxonomy_vocabulary_delete($this->vocabulary->vid); + $this->drupalGet('test-entity/add/test-bundle'); + $this->assertNoFieldByName("{$this->field_name}[$langcode]", '', 'Widget is not displayed.'); } /** @@ -1011,23 +1592,153 @@ ); field_update_field($this->field); // Change the machine name. + $old_name = $this->vocabulary->machine_name; $new_name = drupal_strtolower($this->randomName()); $this->vocabulary->machine_name = $new_name; taxonomy_vocabulary_save($this->vocabulary); + // Check that entity bundles are properly updated. + $info = entity_get_info('taxonomy_term'); + $this->assertFalse(isset($info['bundles'][$old_name]), 'The old bundle name does not appear in entity_get_info().'); + $this->assertTrue(isset($info['bundles'][$new_name]), 'The new bundle name appears in entity_get_info().'); + // Check that the field instance is still attached to the vocabulary. $field = field_info_field($this->field_name); $allowed_values = $field['settings']['allowed_values']; - $this->assertEqual($allowed_values[0]['vocabulary'], $new_name, t('Index 0: Machine name was updated correctly.')); - $this->assertEqual($allowed_values[1]['vocabulary'], $new_name, t('Index 1: Machine name was updated correctly.')); - $this->assertEqual($allowed_values[2]['vocabulary'], 'foo', t('Index 2: Machine name was left untouched.')); + $this->assertEqual($allowed_values[0]['vocabulary'], $new_name, 'Index 0: Machine name was updated correctly.'); + $this->assertEqual($allowed_values[1]['vocabulary'], $new_name, 'Index 1: Machine name was updated correctly.'); + $this->assertEqual($allowed_values[2]['vocabulary'], 'foo', 'Index 2: Machine name was left untouched.'); + } + +} + +/** + * Tests a taxonomy term reference field that allows multiple vocabularies. + */ +class TaxonomyTermFieldMultipleVocabularyTestCase extends TaxonomyWebTestCase { + + protected $instance; + protected $vocabulary1; + protected $vocabulary2; + + public static function getInfo() { + return array( + 'name' => 'Multiple vocabulary term reference field', + 'description' => 'Tests term reference fields that allow multiple vocabularies.', + 'group' => 'Taxonomy', + ); + } + + function setUp() { + parent::setUp('field_test'); + + $web_user = $this->drupalCreateUser(array('access field_test content', 'administer field_test content', 'administer taxonomy')); + $this->drupalLogin($web_user); + $this->vocabulary1 = $this->createVocabulary(); + $this->vocabulary2 = $this->createVocabulary(); + + // 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', + 'cardinality' => FIELD_CARDINALITY_UNLIMITED, + 'settings' => array( + 'allowed_values' => array( + array( + 'vocabulary' => $this->vocabulary1->machine_name, + 'parent' => '0', + ), + array( + 'vocabulary' => $this->vocabulary2->machine_name, + 'parent' => '0', + ), + ), + ) + ); + field_create_field($this->field); + $this->instance = array( + 'field_name' => $this->field_name, + 'entity_type' => 'test_entity', + 'bundle' => 'test_bundle', + 'widget' => array( + 'type' => 'options_select', + ), + 'display' => array( + 'full' => array( + 'type' => 'taxonomy_term_reference_link', + ), + ), + ); + field_create_instance($this->instance); + } + + /** + * Tests term reference field and widget with multiple vocabularies. + */ + function testTaxonomyTermFieldMultipleVocabularies() { + // Create a term in each vocabulary. + $term1 = $this->createTerm($this->vocabulary1); + $term2 = $this->createTerm($this->vocabulary2); + + // Submit an entity with both terms. + $langcode = LANGUAGE_NONE; + $this->drupalGet('test-entity/add/test-bundle'); + $this->assertFieldByName("{$this->field_name}[$langcode][]", '', 'Widget is displayed.'); + $edit = array( + "{$this->field_name}[$langcode][]" => array($term1->tid, $term2->tid), + ); + $this->drupalPost(NULL, $edit, t('Save')); + 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.'); + + // Render the entity. + $entity = field_test_entity_test_load($id); + $entities = array($id => $entity); + field_attach_prepare_view('test_entity', $entities, 'full'); + $entity->content = field_attach_view('test_entity', $entity, 'full'); + $this->content = drupal_render($entity->content); + $this->assertText($term1->name, 'Term 1 name is displayed.'); + $this->assertText($term2->name, 'Term 2 name is displayed.'); + + // Delete vocabulary 2. + taxonomy_vocabulary_delete($this->vocabulary2->vid); + + // Re-render the content. + $entity = field_test_entity_test_load($id); + $entities = array($id => $entity); + field_attach_prepare_view('test_entity', $entities, 'full'); + $entity->content = field_attach_view('test_entity', $entity, 'full'); + $this->plainTextContent = FALSE; + $this->content = drupal_render($entity->content); + + // Term 1 should still be displayed; term 2 should not be. + $this->assertText($term1->name, 'Term 1 name is displayed.'); + $this->assertNoText($term2->name, 'Term 2 name is not displayed.'); + + // Verify that field and instance settings are correct. + $field_info = field_info_field($this->field_name); + $this->assertEqual(sizeof($field_info['settings']['allowed_values']), 1, 'Only one vocabulary is allowed for the field.'); + + // The widget should still be displayed. + $this->drupalGet('test-entity/add/test-bundle'); + $this->assertFieldByName("{$this->field_name}[$langcode][]", '', 'Widget is still displayed.'); + + // Term 1 should still pass validation. + $edit = array( + "{$this->field_name}[$langcode][]" => array($term1->tid), + ); + $this->drupalPost(NULL, $edit, t('Save')); } + } /** * Test taxonomy token replacement in strings. */ class TaxonomyTokenReplaceTestCase extends TaxonomyWebTestCase { + public static function getInfo() { return array( 'name' => 'Taxonomy token replacement', @@ -1108,7 +1819,7 @@ foreach ($tests as $input => $expected) { $output = token_replace($input, array('term' => $term1), array('language' => $language)); - $this->assertFalse(strcmp($output, $expected), t('Sanitized taxonomy term token %token replaced.', array('%token' => $input))); + $this->assertEqual($output, $expected, format_string('Sanitized taxonomy term token %token replaced.', array('%token' => $input))); } // Generate and test sanitized tokens for term2. @@ -1124,11 +1835,11 @@ $tests['[term:vocabulary:name]'] = check_plain($this->vocabulary->name); // Test to make sure that we generated something for each token. - $this->assertFalse(in_array(0, array_map('strlen', $tests)), t('No empty tokens generated.')); + $this->assertFalse(in_array(0, array_map('strlen', $tests)), 'No empty tokens generated.'); foreach ($tests as $input => $expected) { $output = token_replace($input, array('term' => $term2), array('language' => $language)); - $this->assertFalse(strcmp($output, $expected), t('Sanitized taxonomy term token %token replaced.', array('%token' => $input))); + $this->assertEqual($output, $expected, format_string('Sanitized taxonomy term token %token replaced.', array('%token' => $input))); } // Generate and test unsanitized tokens. @@ -1139,7 +1850,7 @@ foreach ($tests as $input => $expected) { $output = token_replace($input, array('term' => $term2), array('language' => $language, 'sanitize' => FALSE)); - $this->assertFalse(strcmp($output, $expected), t('Unsanitized taxonomy term token %token replaced.', array('%token' => $input))); + $this->assertEqual($output, $expected, format_string('Unsanitized taxonomy term token %token replaced.', array('%token' => $input))); } // Generate and test sanitized tokens. @@ -1151,11 +1862,11 @@ $tests['[vocabulary:term-count]'] = 2; // Test to make sure that we generated something for each token. - $this->assertFalse(in_array(0, array_map('strlen', $tests)), t('No empty tokens generated.')); + $this->assertFalse(in_array(0, array_map('strlen', $tests)), 'No empty tokens generated.'); foreach ($tests as $input => $expected) { $output = token_replace($input, array('vocabulary' => $this->vocabulary), array('language' => $language)); - $this->assertFalse(strcmp($output, $expected), t('Sanitized taxonomy vocabulary token %token replaced.', array('%token' => $input))); + $this->assertEqual($output, $expected, format_string('Sanitized taxonomy vocabulary token %token replaced.', array('%token' => $input))); } // Generate and test unsanitized tokens. @@ -1164,15 +1875,17 @@ foreach ($tests as $input => $expected) { $output = token_replace($input, array('vocabulary' => $this->vocabulary), array('language' => $language, 'sanitize' => FALSE)); - $this->assertFalse(strcmp($output, $expected), t('Unsanitized taxonomy vocabulary token %token replaced.', array('%token' => $input))); + $this->assertEqual($output, $expected, format_string('Unsanitized taxonomy vocabulary token %token replaced.', array('%token' => $input))); } } + } /** * Tests for verifying that taxonomy pages use the correct theme. */ class TaxonomyThemeTestCase extends TaxonomyWebTestCase { + public static function getInfo() { return array( 'name' => 'Taxonomy theme switching', @@ -1203,15 +1916,180 @@ // should use the administrative theme. $vocabulary = $this->createVocabulary(); $this->drupalGet('admin/structure/taxonomy/' . $vocabulary->machine_name . '/add'); - $this->assertRaw('seven/style.css', t("The administrative theme's CSS appears on the page for adding a taxonomy term.")); + $this->assertRaw('seven/style.css', "The administrative theme's CSS appears on the page for adding a taxonomy term."); // Viewing a taxonomy term should use the default theme. $term = $this->createTerm($vocabulary); $this->drupalGet('taxonomy/term/' . $term->tid); - $this->assertRaw('bartik/css/style.css', t("The default theme's CSS appears on the page for viewing a taxonomy term.")); + $this->assertRaw('bartik/css/style.css', "The default theme's CSS appears on the page for viewing a taxonomy term."); // Editing a taxonomy term should use the same theme as adding one. $this->drupalGet('taxonomy/term/' . $term->tid . '/edit'); - $this->assertRaw('seven/style.css', t("The administrative theme's CSS appears on the page for editing a taxonomy term.")); + $this->assertRaw('seven/style.css', "The administrative theme's CSS appears on the page for editing a taxonomy term."); } + +} + +/** + * Tests the functionality of EntityFieldQuery for taxonomy entities. + */ +class TaxonomyEFQTestCase extends TaxonomyWebTestCase { + public static function getInfo() { + return array( + 'name' => 'Taxonomy EntityFieldQuery', + 'description' => 'Verifies operation of a taxonomy-based EntityFieldQuery.', + 'group' => 'Taxonomy', + ); + } + + function setUp() { + parent::setUp(); + $this->admin_user = $this->drupalCreateUser(array('administer taxonomy')); + $this->drupalLogin($this->admin_user); + $this->vocabulary = $this->createVocabulary(); + } + + /** + * Tests that a basic taxonomy EntityFieldQuery works. + */ + function testTaxonomyEFQ() { + $terms = array(); + for ($i = 0; $i < 5; $i++) { + $term = $this->createTerm($this->vocabulary); + $terms[$term->tid] = $term; + } + $query = new EntityFieldQuery(); + $query->entityCondition('entity_type', 'taxonomy_term'); + $result = $query->execute(); + $result = $result['taxonomy_term']; + asort($result); + $this->assertEqual(array_keys($terms), array_keys($result), 'Taxonomy terms were retrieved by EntityFieldQuery.'); + + // Create a second vocabulary and five more terms. + $vocabulary2 = $this->createVocabulary(); + $terms2 = array(); + for ($i = 0; $i < 5; $i++) { + $term = $this->createTerm($vocabulary2); + $terms2[$term->tid] = $term; + } + + $query = new EntityFieldQuery(); + $query->entityCondition('entity_type', 'taxonomy_term'); + $query->entityCondition('bundle', $vocabulary2->machine_name); + $result = $query->execute(); + $result = $result['taxonomy_term']; + asort($result); + $this->assertEqual(array_keys($terms2), array_keys($result), format_string('Taxonomy terms from the %name vocabulary were retrieved by EntityFieldQuery.', array('%name' => $vocabulary2->name))); + } + +} + +/** + * 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.0/modules/taxonomy/taxonomy.tokens.inc drupal-7.66/modules/taxonomy/taxonomy.tokens.inc --- drupal-7.0/modules/taxonomy/taxonomy.tokens.inc 2010-11-14 01:25:44.000000000 +0100 +++ drupal-7.66/modules/taxonomy/taxonomy.tokens.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ ' . t('About') . ''; - $output .= '

          ' . t('The Toolbar module displays links to top-level administration menu items and links from other modules at the top of the screen. For more information, see the online handbook entry for Toolbar module.', array('@toolbar' => 'http://drupal.org/handbook/modules/toolbar/')) . '

          '; + $output .= '

          ' . t('The Toolbar module displays links to top-level administration menu items and links from other modules at the top of the screen. For more information, see the online handbook entry for Toolbar module.', array('@toolbar' => 'http://drupal.org/documentation/modules/toolbar/')) . '

          '; $output .= '

          ' . t('Uses') . '

          '; $output .= '
          '; $output .= '
          ' . t('Displaying administrative links') . '
          '; @@ -83,6 +82,7 @@ * An associative array containing: * - collapsed: A boolean value representing the toolbar drawer's visibility. * - attributes: An associative array of HTML attributes. + * * @return * An HTML string representing the element for toggling. * @@ -96,7 +96,7 @@ $toggle_text = t('Hide shortcuts'); $variables['attributes']['class'][] = 'toggle-active'; } - return '' . $toggle_text . ''; + return l($toggle_text, 'toolbar/toggle', array('query' => drupal_get_destination(), 'attributes' => array('title' => $toggle_text) + $variables['attributes'])); } /** @@ -176,7 +176,10 @@ } /** - * Build the admin menu as a structured array ready for drupal_render(). + * Builds the admin menu as a structured array ready for drupal_render(). + * + * @return + * Array of links and settings relating to the admin menu. */ function toolbar_view() { global $user; @@ -273,7 +276,10 @@ } /** - * Get only the top level items below the 'admin' path. + * Gets only the top level items below the 'admin' path. + * + * @return + * An array containing a menu tree of top level items below the 'admin' path. */ function toolbar_get_menu_tree() { $tree = array(); @@ -290,10 +296,13 @@ } /** - * Generate a links array from a menu tree array. + * Generates a links array from a menu tree array. * * Based on menu_navigation_links(). Adds path based IDs and icon placeholders * to the links. + * + * @return + * An array of links as defined above. */ function toolbar_menu_navigation_links($tree) { $links = array(); @@ -331,6 +340,9 @@ * Useful when using a menu generated by menu_tree_all_data() which does * not set the 'in_active_trail' flag on items. * + * @return + * TRUE when path is in the active trail, FALSE if not. + * * @todo * Look at migrating to a menu system level function. */ diff -Naur drupal-7.0/modules/toolbar/toolbar.tpl.php drupal-7.66/modules/toolbar/toolbar.tpl.php --- drupal-7.0/modules/toolbar/toolbar.tpl.php 2010-05-23 20:23:32.000000000 +0200 +++ drupal-7.66/modules/toolbar/toolbar.tpl.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@
          diff -Naur drupal-7.0/modules/tracker/tracker.css drupal-7.66/modules/tracker/tracker.css --- drupal-7.0/modules/tracker/tracker.css 2009-07-02 06:27:23.000000000 +0200 +++ drupal-7.66/modules/tracker/tracker.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -/* $Id: tracker.css,v 1.2 2009/07/02 04:27:23 webchick Exp $ */ .page-tracker td.replies { text-align: center; diff -Naur drupal-7.0/modules/tracker/tracker.info drupal-7.66/modules/tracker/tracker.info --- drupal-7.0/modules/tracker/tracker.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/tracker/tracker.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: tracker.info,v 1.11 2010/12/20 19:59:43 webchick Exp $ name = Tracker description = Enables tracking of recent content for users. dependencies[] = comment @@ -7,8 +6,7 @@ core = 7.x files[] = tracker.test -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/tracker/tracker.install drupal-7.66/modules/tracker/tracker.install --- drupal-7.0/modules/tracker/tracker.install 2011-01-02 18:26:39.000000000 +0100 +++ drupal-7.66/modules/tracker/tracker.install 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,9 @@ ' . t('About') . ''; - $output .= '

          ' . t('The Tracker module displays the most recently added and updated content on your site, and allows you to follow new content created by each user. This module has no configuration options. For more information, see the online handbook entry for Tracker module.', array('@tracker' => 'http://drupal.org/handbook/modules/tracker/')) . '

          '; + $output .= '

          ' . t('The Tracker module displays the most recently added and updated content on your site, and allows you to follow new content created by each user. This module has no configuration options. For more information, see the online handbook entry for Tracker module.', array('@tracker' => 'http://drupal.org/documentation/modules/tracker/')) . '

          '; $output .= '

          ' . t('Uses') . '

          '; $output .= '
          '; $output .= '
          ' . t('Navigation') . '
          '; @@ -71,6 +70,11 @@ /** * Implements hook_cron(). + * + * Updates tracking information for any items still to be tracked. The variable + * 'tracker_index_nid' is set to ((the last node ID that was indexed) - 1) and + * used to select the nodes to be processed. If there are no remaining nodes to + * process, 'tracker_index_nid' will be 0. */ function tracker_cron() { $max_nid = variable_get('tracker_index_nid', 0); @@ -165,6 +169,8 @@ /** * Implements hook_node_insert(). + * + * Adds new tracking information for this node since it's new. */ function tracker_node_insert($node, $arg = 0) { _tracker_add($node->nid, $node->uid, $node->changed); @@ -172,6 +178,8 @@ /** * Implements hook_node_update(). + * + * Adds tracking information for this node since it's been updated. */ function tracker_node_update($node, $arg = 0) { _tracker_add($node->nid, $node->uid, $node->changed); @@ -179,6 +187,8 @@ /** * Implements hook_node_delete(). + * + * Deletes tracking information for a node. */ function tracker_node_delete($node, $arg = 0) { db_delete('tracker_node') @@ -197,7 +207,7 @@ */ function tracker_comment_update($comment) { // comment_save() calls hook_comment_publish() for all published comments - // so we to handle all other values here. + // so we need to handle all other values here. if ($comment->status != COMMENT_PUBLISHED) { _tracker_remove($comment->nid, $comment->uid, $comment->changed); } @@ -228,7 +238,7 @@ } /** - * Update indexing tables when a node is added, updated or commented on. + * Updates indexing tables when a node is added, updated, or commented on. * * @param $nid * A node ID. @@ -253,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, @@ -264,10 +274,18 @@ '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(); } /** - * Determine the max timestamp between $node->changed and the last comment. + * Determines the max timestamp between $node->changed and the last comment. * * @param $nid * A node ID. @@ -289,7 +307,7 @@ } /** - * Clean up indexed data when nodes or comments are removed. + * Cleans up indexed data when nodes or comments are removed. * * @param $nid * The node ID. @@ -302,8 +320,8 @@ $node = db_query('SELECT nid, status, uid, changed FROM {node} WHERE nid = :nid', array(':nid' => $nid))->fetchObject(); // The user only keeps his or her subscription if both of the following are true: - // (1) The node exists. - // (2) The user is either the node author or has commented on the node. + // (1) The node exists. + // (2) The user is either the node author or has commented on the node. $keep_subscription = FALSE; if ($node) { @@ -312,7 +330,7 @@ // Comments are a second reason to keep the user's subscription. if (!$keep_subscription) { - // Check if the user has commented at least once on the given nid + // Check if the user has commented at least once on the given nid. $keep_subscription = db_query_range('SELECT COUNT(*) FROM {comment} WHERE nid = :nid AND uid = :uid AND status = :status', 0, 1, array( ':nid' => $nid, ':uid' => $uid, @@ -330,9 +348,8 @@ // Now we need to update the (possibly) changed timestamps for other users // and the node itself. - // We only need to do this if the removed item has a timestamp that equals - // or exceeds the listed changed timestamp for the node + // or exceeds the listed changed timestamp for the node. $tracker_node = db_query('SELECT nid, changed FROM {tracker_node} WHERE nid = :nid', array(':nid' => $nid))->fetchObject(); if ($tracker_node && $changed >= $tracker_node->changed) { // If we're here, the item being removed is *possibly* the item that @@ -357,7 +374,7 @@ )) ->condition('nid', $nid) ->execute(); - } + } } else { // If the node doesn't exist, remove everything. diff -Naur drupal-7.0/modules/tracker/tracker.pages.inc drupal-7.66/modules/tracker/tracker.pages.inc --- drupal-7.0/modules/tracker/tracker.pages.inc 2010-07-30 04:47:28.000000000 +0200 +++ drupal-7.66/modules/tracker/tracker.pages.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,14 +1,16 @@ array_keys($nodes)), array('target' => 'slave')); foreach ($result as $node) { $node->last_activity = $nodes[$node->nid]->changed; $nodes[$node->nid] = $node; } - // Finally display the data + // Display the data. foreach ($nodes as $node) { - // Determine the number of comments: + // Determine the number of comments. $comments = 0; if ($node->comment_count) { $comments = $node->comment_count; if ($new = comment_num_new($node->nid)) { $comments .= '
          '; - $comments .= l(format_plural($new, '1 new', '@count new'), 'node/'. $node->nid, array('fragment' => 'new')); + $comments .= l(format_plural($new, '1 new', '@count new'), 'node/' . $node->nid, array('fragment' => 'new')); } } @@ -98,7 +100,7 @@ $row['last updated'] += $mapping_last_activity; // We need to add the about attribute on the tr tag to specify which - // node the RDFa annoatations above apply to. We move the content of + // node the RDFa annotations above apply to. We move the content of // $row to a 'data' sub array so we can specify attributes for the row. $row = array('data' => $row); $row['about'] = url('node/' . $node->nid); @@ -118,7 +120,6 @@ ); $page['pager'] = array( '#theme' => 'pager', - '#quantity' => 25, '#weight' => 10, ); $page['#sorted'] = TRUE; diff -Naur drupal-7.0/modules/tracker/tracker.test drupal-7.66/modules/tracker/tracker.test --- drupal-7.0/modules/tracker/tracker.test 2010-12-28 19:39:23.000000000 +0100 +++ drupal-7.66/modules/tracker/tracker.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,10 +1,28 @@ drupalLogin($this->user); $unpublished = $this->drupalCreateNode(array( - 'title' =>$this->randomName(8), + 'title' => $this->randomName(8), 'status' => 0, )); $published = $this->drupalCreateNode(array( @@ -41,18 +59,18 @@ )); $this->drupalGet('tracker'); - $this->assertNoText($unpublished->title, t('Unpublished node do not show up in the tracker listing.')); - $this->assertText($published->title, t('Published node show up in the tracker listing.')); - $this->assertLink(t('My recent content'), 0, t('User tab shows up on the global tracker page.')); + $this->assertNoText($unpublished->title, 'Unpublished node do not show up in the tracker listing.'); + $this->assertText($published->title, 'Published node show up in the tracker listing.'); + $this->assertLink(t('My recent content'), 0, 'User tab shows up on the global tracker page.'); // Delete a node and ensure it no longer appears on the tracker. node_delete($published->nid); $this->drupalGet('tracker'); - $this->assertNoText($published->title, t('Deleted node do not show up in the tracker listing.')); + $this->assertNoText($published->title, 'Deleted node do not show up in the tracker listing.'); } /** - * Test the presence of nodes on a user's tracker listing. + * Tests for the presence of nodes on a user's tracker listing. */ function testTrackerUser() { $this->drupalLogin($this->user); @@ -84,10 +102,10 @@ $this->drupalPost('comment/reply/' . $other_published_my_comment->nid, $comment, t('Save')); $this->drupalGet('user/' . $this->user->uid . '/track'); - $this->assertNoText($unpublished->title, t("Unpublished nodes do not show up in the users's tracker listing.")); - $this->assertText($my_published->title, t("Published nodes show up in the user's tracker listing.")); - $this->assertNoText($other_published_no_comment->title, t("Other user's nodes do not show up in the user's tracker listing.")); - $this->assertText($other_published_my_comment->title, t("Nodes that the user has commented on appear in the user's tracker listing.")); + $this->assertNoText($unpublished->title, "Unpublished nodes do not show up in the users's tracker listing."); + $this->assertText($my_published->title, "Published nodes show up in the user's tracker listing."); + $this->assertNoText($other_published_no_comment->title, "Other user's nodes do not show up in the user's tracker listing."); + $this->assertText($other_published_my_comment->title, "Nodes that the user has commented on appear in the user's tracker listing."); // Verify that unpublished comments are removed from the tracker. $admin_user = $this->drupalCreateUser(array('administer comments', 'access user profiles')); @@ -98,7 +116,7 @@ } /** - * Test the presence of the "new" flag for nodes. + * Tests for the presence of the "new" flag for nodes. */ function testTrackerNewNodes() { $this->drupalLogin($this->user); @@ -110,30 +128,29 @@ $node = $this->drupalCreateNode($edit); $title = $edit['title']; $this->drupalGet('tracker'); - $this->assertPattern('/' . $title . '.*new/', t('New nodes are flagged as such in the tracker listing.')); + $this->assertPattern('/' . $title . '.*new/', 'New nodes are flagged as such in the tracker listing.'); $this->drupalGet('node/' . $node->nid); $this->drupalGet('tracker'); - $this->assertNoPattern('/' . $title . '.*new/', t('Visited nodes are not flagged as new.')); + $this->assertNoPattern('/' . $title . '.*new/', 'Visited nodes are not flagged as new.'); $this->drupalLogin($this->other_user); $this->drupalGet('tracker'); - $this->assertPattern('/' . $title . '.*new/', t('For another user, new nodes are flagged as such in the tracker listing.')); + $this->assertPattern('/' . $title . '.*new/', 'For another user, new nodes are flagged as such in the tracker listing.'); $this->drupalGet('node/' . $node->nid); $this->drupalGet('tracker'); - $this->assertNoPattern('/' . $title . '.*new/', t('For another user, visited nodes are not flagged as new.')); + $this->assertNoPattern('/' . $title . '.*new/', 'For another user, visited nodes are not flagged as new.'); } /** - * Test comment counters on the tracker listing. + * Tests for comment counters on the tracker listing. */ function testTrackerNewComments() { $this->drupalLogin($this->user); $node = $this->drupalCreateNode(array( 'comment' => 2, - 'title' => array(LANGUAGE_NONE => array(array('value' => $this->randomName(8)))), )); // Add a comment to the page. @@ -141,11 +158,12 @@ 'subject' => $this->randomName(), 'comment_body[' . LANGUAGE_NONE . '][0][value]' => $this->randomName(20), ); - $this->drupalPost('comment/reply/' . $node->nid, $comment, t('Save')); // The new comment is automatically viewed by the current user. + // The new comment is automatically viewed by the current user. + $this->drupalPost('comment/reply/' . $node->nid, $comment, t('Save')); $this->drupalLogin($this->other_user); $this->drupalGet('tracker'); - $this->assertText('1 new', t('New comments are counted on the tracker listing pages.')); + $this->assertText('1 new', 'New comments are counted on the tracker listing pages.'); $this->drupalGet('node/' . $node->nid); // Add another comment as other_user. @@ -154,17 +172,83 @@ 'comment_body[' . LANGUAGE_NONE . '][0][value]' => $this->randomName(20), ); // If the comment is posted in the same second as the last one then Drupal - // can't tell a difference, so wait one second here. + // can't tell the difference, so we wait one second here. sleep(1); $this->drupalPost('comment/reply/' . $node->nid, $comment, t('Save')); $this->drupalLogin($this->user); $this->drupalGet('tracker'); - $this->assertText('1 new', t('New comments are counted on the tracker listing pages.')); + $this->assertText('1 new', 'New comments are counted on the tracker listing pages.'); + } + + /** + * Tests for ordering on a users tracker listing when comments are posted. + */ + function testTrackerOrderingNewComments() { + $this->drupalLogin($this->user); + + $node_one = $this->drupalCreateNode(array( + 'title' => $this->randomName(8), + )); + + $node_two = $this->drupalCreateNode(array( + 'title' => $this->randomName(8), + )); + + // Now get other_user to track these pieces of content. + $this->drupalLogin($this->other_user); + + // Add a comment to the first page. + $comment = array( + 'subject' => $this->randomName(), + 'comment_body[' . LANGUAGE_NONE . '][0][value]' => $this->randomName(20), + ); + $this->drupalPost('comment/reply/' . $node_one->nid, $comment, t('Save')); + + // If the comment is posted in the same second as the last one then Drupal + // can't tell the difference, so we wait one second here. + sleep(1); + + // Add a comment to the second page. + $comment = array( + 'subject' => $this->randomName(), + 'comment_body[' . LANGUAGE_NONE . '][0][value]' => $this->randomName(20), + ); + $this->drupalPost('comment/reply/' . $node_two->nid, $comment, t('Save')); + + // We should at this point have in our tracker for other_user: + // 1. node_two + // 2. node_one + // Because that's the reverse order of the posted comments. + + // Now we're going to post a comment to node_one which should jump it to the + // top of the list. + + $this->drupalLogin($this->user); + // If the comment is posted in the same second as the last one then Drupal + // can't tell the difference, so we wait one second here. + sleep(1); + + // Add a comment to the second page. + $comment = array( + 'subject' => $this->randomName(), + 'comment_body[' . LANGUAGE_NONE . '][0][value]' => $this->randomName(20), + ); + $this->drupalPost('comment/reply/' . $node_one->nid, $comment, t('Save')); + + // Switch back to the other_user and assert that the order has swapped. + $this->drupalLogin($this->other_user); + $this->drupalGet('user/' . $this->other_user->uid . '/track'); + // This is a cheeky way of asserting that the nodes are in the right order + // on the tracker page. + // It's almost certainly too brittle. + $pattern = '/' . preg_quote($node_one->title) . '.+' . preg_quote($node_two->title) . '/s'; + $this->verbose($pattern); + $this->assertPattern($pattern, 'Most recently commented on node appears at the top of tracker'); } /** - * Test that existing nodes are indexed by cron. + * Tests that existing nodes are indexed by cron. */ function testTrackerCronIndexing() { $this->drupalLogin($this->user); @@ -205,24 +289,23 @@ // Assert that all node titles are displayed. foreach ($nodes as $i => $node) { - $this->assertText($node->title, t('Node @i is displayed on the tracker listing pages.', array('@i' => $i))); + $this->assertText($node->title, format_string('Node @i is displayed on the tracker listing pages.', array('@i' => $i))); } - $this->assertText('1 new', t('New comment is counted on the tracker listing pages.')); - $this->assertText('updated', t('Node is listed as updated')); - + $this->assertText('1 new', 'New comment is counted on the tracker listing pages.'); + $this->assertText('updated', 'Node is listed as updated'); // Fetch the site-wide tracker. $this->drupalGet('tracker'); // Assert that all node titles are displayed. foreach ($nodes as $i => $node) { - $this->assertText($node->title, t('Node @i is displayed on the tracker listing pages.', array('@i' => $i))); + $this->assertText($node->title, format_string('Node @i is displayed on the tracker listing pages.', array('@i' => $i))); } - $this->assertText('1 new', t('New comment is counted on the tracker listing pages.')); + $this->assertText('1 new', 'New comment is counted on the tracker listing pages.'); } /** - * Test that publish/unpublish works at admin/content/node + * Tests that publish/unpublish works at admin/content/node. */ function testTrackerAdminUnpublish() { $admin_user = $this->drupalCreateUser(array('access content overview', 'administer nodes', 'bypass node access')); @@ -235,7 +318,7 @@ // Assert that the node is displayed. $this->drupalGet('tracker'); - $this->assertText($node->title, t('Node is displayed on the tracker listing pages.')); + $this->assertText($node->title, 'Node is displayed on the tracker listing pages.'); // Unpublish the node and ensure that it's no longer displayed. $edit = array( @@ -245,6 +328,6 @@ $this->drupalPost('admin/content', $edit, t('Update')); $this->drupalGet('tracker'); - $this->assertText(t('No content available.'), t('Node is displayed on the tracker listing pages.')); + $this->assertText(t('No content available.'), 'Node is displayed on the tracker listing pages.'); } } diff -Naur drupal-7.0/modules/translation/tests/translation_test.info drupal-7.66/modules/translation/tests/translation_test.info --- drupal-7.0/modules/translation/tests/translation_test.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/translation/tests/translation_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: translation_test.info,v 1.2 2010/12/20 19:59:43 webchick Exp $ name = "Content Translation Test" description = "Support module for the content translation tests." core = 7.x @@ -6,8 +5,7 @@ version = VERSION hidden = TRUE -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/translation/tests/translation_test.module drupal-7.66/modules/translation/tests/translation_test.module --- drupal-7.0/modules/translation/tests/translation_test.module 2010-10-09 05:22:30.000000000 +0200 +++ drupal-7.66/modules/translation/tests/translation_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ ' . t('About') . ''; - $output .= '

          ' . t('The Content translation module allows content to be translated into different languages. Working with the Locale module (which manages enabled languages and provides translation for the site interface), the Content translation module is key to creating and maintaining translated site content. For more information, see the online handbook entry for Content translation module.', array('@locale' => url('admin/help/locale'), '@translation' => 'http://drupal.org/handbook/modules/translation/')) . '

          '; + $output .= '

          ' . t('The Content translation module allows content to be translated into different languages. Working with the Locale module (which manages enabled languages and provides translation for the site interface), the Content translation module is key to creating and maintaining translated site content. For more information, see the online handbook entry for Content translation module.', array('@locale' => url('admin/help/locale'), '@translation' => 'http://drupal.org/documentation/modules/translation/')) . '

          '; $output .= '

          ' . t('Uses') . '

          '; $output .= '
          '; $output .= '
          ' . t('Configuring content types for translation') . '
          '; - $output .= '
          ' . t('To configure a particular content type for translation, visit the Content types page, and click the edit link for the content type. In the Publishing options section, select Enabled, with translation under Multilingual support.', array('@content-types' => url('admin/structure/types'))) . '
        '; + $output .= '
        ' . t('To configure a particular content type for translation, visit the Content types page, and click the edit link for the content type. In the Publishing options section, select Enabled, with translation under Multilingual support.', array('@content-types' => url('admin/structure/types'))) . '
        '; $output .= '
        ' . t('Assigning a language to content') . '
        '; $output .= '
        ' . t('Use the Language drop down to select the appropriate language when creating or editing content.') . '
        '; $output .= '
        ' . t('Translating content') . '
        '; @@ -71,14 +70,21 @@ } /** - * Menu access callback. + * Access callback: Checks that the user has permission to 'translate content'. + * + * Only displays the translation tab for nodes that are not language-neutral + * of types that have translation enabled. + * + * @param $node + * A node object. + * + * @return + * TRUE if the translation tab should be displayed, FALSE otherwise. * - * Only display translation tab for node types, which have translation enabled - * and where the current node is not language neutral (which should span - * all languages). + * @see translation_menu() */ function _translation_tab_access($node) { - if ($node->language != LANGUAGE_NONE && translation_supported_type($node->type) && node_access('view', $node)) { + if (entity_language('node', $node) != LANGUAGE_NONE && translation_supported_type($node->type) && node_access('view', $node)) { return user_access('translate content'); } return FALSE; @@ -108,21 +114,22 @@ } /** - * Implements hook_form_FORM_ID_alter(). + * Implements hook_form_FORM_ID_alter() for node_type_form(). */ function translation_form_node_type_form_alter(&$form, &$form_state) { // Add translation option to content type form. $form['workflow']['language_content_type']['#options'][TRANSLATION_ENABLED] = t('Enabled, with translation'); // Description based on text from locale.module. - $form['workflow']['language_content_type']['#description'] = t('Enable multilingual support for this content type. If enabled, a language selection field will be added to the editing form, allowing you to select from one of the enabled languages. You can also turn on translation for this content type, which lets you have content translated to any of the enabled languages. If disabled, new posts are saved with the default language. Existing content will not be affected by changing this option.', array('!languages' => url('admin/config/regional/language'))); + $form['workflow']['language_content_type']['#description'] = t('Enable multilingual support for this content type. If enabled, a language selection field will be added to the editing form, allowing you to select from one of the enabled languages. You can also turn on translation for this content type, which lets you have content translated to any of the installed languages. If disabled, new posts are saved with the default language. Existing content will not be affected by changing this option.', array('!languages' => url('admin/config/regional/language'))); } /** - * Implements hook_form_alter(). + * Implements hook_form_BASE_FORM_ID_alter() for node_form(). + * + * Alters language fields on node edit forms when a translation is about to be + * created. * - * - Add translation option to content type form. - * - Alters language fields on node forms when a translation - * is about to be created. + * @see node_form() */ function translation_form_node_form_alter(&$form, &$form_state) { if (translation_supported_type($form['#node']->type)) { @@ -135,7 +142,7 @@ // might need to distinguish between enabled and disabled languages, hence // we divide them in two option groups. if ($translator_widget) { - $options = array(); + $options = array($groups[1] => array(LANGUAGE_NONE => t('Language neutral'))); $language_list = locale_language_list('name', TRUE); foreach (array(1, 0) as $status) { $group = $groups[$status]; @@ -202,9 +209,9 @@ /** * Implements hook_node_view(). * - * Display translation links with native language names, if this node is part of - * a translation set. If no language provider is enabled "fall back" to the - * simple links built through the result of translation_node_get_translations(). + * Displays translation links with language names if this node is part of a + * translation set. If no language provider is enabled, "fall back" to simple + * links built through the result of translation_node_get_translations(). */ function translation_node_view($node, $view_mode) { // If the site has no translations or is not multilingual we have no content @@ -226,7 +233,7 @@ foreach ($translations as $langcode => $translation) { // Do not show links to the same node, to unpublished translations or to // translations in disabled languages. - if ($translation->status && isset($languages[$langcode]) && $langcode != $node->language) { + if ($translation->status && isset($languages[$langcode]) && $langcode != entity_language('node', $node)) { $language = $languages[$langcode]; $key = "translation_$langcode"; @@ -306,7 +313,7 @@ // Add field translations and let other modules module add custom translated // fields. - field_attach_prepare_translation('node', $node, $node->language, $source_node, $source_node->language); + field_attach_prepare_translation('node', $node, $langcode, $source_node, $source_node->language); } } @@ -329,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, @@ -351,7 +362,8 @@ function translation_node_update($node) { // Only act if we are dealing with a content type supporting translations. if (translation_supported_type($node->type)) { - if (isset($node->translation) && $node->translation && !empty($node->language) && $node->tnid) { + $langcode = entity_language('node', $node); + if (isset($node->translation) && $node->translation && !empty($langcode) && $node->tnid) { // Update translation information. db_update('node') ->fields(array( @@ -360,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); } } } @@ -375,14 +397,15 @@ /** * Implements hook_node_validate(). * - * Ensure that duplicate translations can not be created for the same source. + * Ensures that duplicate translations can't be created for the same source. */ function translation_node_validate($node, $form) { // Only act on translatable nodes with a tnid or translation_source. if (translation_supported_type($node->type) && (!empty($node->tnid) || !empty($form['#node']->translation_source->nid))) { $tnid = !empty($node->tnid) ? $node->tnid : $form['#node']->translation_source->nid; $translations = translation_node_get_translations($tnid); - if (isset($translations[$node->language]) && $translations[$node->language]->nid != $node->nid ) { + $langcode = entity_language('node', $node); + if (isset($translations[$langcode]) && $translations[$langcode]->nid != $node->nid ) { form_set_error('language', t('There is already a translation in this language.')); } } @@ -399,21 +422,28 @@ } /** - * Remove a node from its translation set (if any) - * and update the set accordingly. + * Removes a node from its translation set and updates accordingly. + * + * @param $node + * 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 @@ -428,23 +458,30 @@ ->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); } } /** - * Get all nodes in a translation set, represented by $tnid. + * Gets all nodes in a given translation set. * * @param $tnid - * The translation source nid of the translation set, the identifier - * of the node used to derive all translations in the set. + * The translation source nid of the translation set, the identifier of the + * node used to derive all translations in the set. + * * @return - * Array of partial node objects (nid, title, language) representing - * all nodes in the translation set, in effect all translations - * of node $tnid, including node $tnid itself. Because these are - * partial nodes, you need to node_load() the full node, if you - * need more properties. The array is indexed by language code. + * Array of partial node objects (nid, title, language) representing all + * nodes in the translation set, in effect all translations of node $tnid, + * including node $tnid itself. Because these are partial nodes, you need to + * node_load() the full node, if you need more properties. The array is + * indexed by language code. */ function translation_node_get_translations($tnid) { if (is_numeric($tnid) && $tnid) { @@ -459,7 +496,8 @@ ->execute(); foreach ($result as $node) { - $translations[$tnid][$node->language] = $node; + $langcode = entity_language('node', $node); + $translations[$tnid][$langcode] = $node; } } return $translations[$tnid]; @@ -470,21 +508,21 @@ * Returns whether the given content type has support for translations. * * @return - * Boolean value. + * TRUE if translation is supported, and FALSE if not. */ function translation_supported_type($type) { return variable_get('language_content_type_' . $type, 0) == TRANSLATION_ENABLED; } /** - * Return paths of all translations of a node, based on - * its Drupal path. + * Returns the paths of all translations of a node, based on its Drupal path. * * @param $path * A Drupal path, for example node/432. + * * @return - * An array of paths of translations of the node accessible - * to the current user keyed with language codes. + * An array of paths of translations of the node accessible to the current + * user, keyed with language codes. */ function translation_path_get_translations($path) { $paths = array(); @@ -504,8 +542,24 @@ */ function translation_language_switch_links_alter(array &$links, $type, $path) { $language_type = variable_get('translation_language_type', LANGUAGE_TYPE_INTERFACE); - if ($type == $language_type && preg_match("!^node/(\d+)(/.+|)!", $path, $matches) && ($node = node_load((int) $matches[1]))) { - $translations = $node->tnid ? translation_node_get_translations($node->tnid) : array($node->language => $node); + + if ($type == $language_type && preg_match("!^node/(\d+)(/.+|)!", $path, $matches)) { + $node = node_load((int) $matches[1]); + + if (empty($node->tnid)) { + // If the node cannot be found nothing needs to be done. If it does not + // have translations it might be a language neutral node, in which case we + // must leave the language switch links unaltered. This is true also for + // nodes not having translation support enabled. + if (empty($node) || entity_language('node', $node) == LANGUAGE_NONE || !translation_supported_type($node->type)) { + return; + } + $langcode = entity_language('node', $node); + $translations = array($langcode => $node); + } + else { + $translations = translation_node_get_translations($node->tnid); + } foreach ($links as $langcode => $link) { if (isset($translations[$langcode]) && $translations[$langcode]->status) { @@ -515,7 +569,7 @@ else { // No translation in this language, or no permission to view. unset($links[$langcode]['href']); - $links[$langcode]['attributes']['class'] = 'locale-untranslated'; + $links[$langcode]['attributes']['class'][] = 'locale-untranslated'; } } } diff -Naur drupal-7.0/modules/translation/translation.pages.inc drupal-7.66/modules/translation/translation.pages.inc --- drupal-7.0/modules/translation/translation.pages.inc 2010-12-31 21:45:25.000000000 +0100 +++ drupal-7.66/modules/translation/translation.pages.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,16 +1,20 @@ nid; - $translations = array($node->language => $node); + $translations = array(entity_language('node', $node) => $node); } $type = variable_get('translation_language_type', LANGUAGE_TYPE_INTERFACE); @@ -38,12 +42,12 @@ $translation_node = node_load($translations[$langcode]->nid); $path = 'node/' . $translation_node->nid; $links = language_negotiation_get_switch_links($type, $path); - $title = empty($links->links[$langcode]) ? l($translation_node->title, $path) : l($translation_node->title, $links->links[$langcode]['href'], $links->links[$langcode]); + $title = empty($links->links[$langcode]['href']) ? l($translation_node->title, $path) : l($translation_node->title, $links->links[$langcode]['href'], $links->links[$langcode]); if (node_access('update', $translation_node)) { $text = t('edit'); $path = 'node/' . $translation_node->nid . '/edit'; $links = language_negotiation_get_switch_links($type, $path); - $options[] = empty($links->links[$langcode]) ? l($text, $path) : l($text, $links->links[$langcode]['href'], $links->links[$langcode]); + $options[] = empty($links->links[$langcode]['href']) ? l($text, $path) : l($text, $links->links[$langcode]['href'], $links->links[$langcode]); } $status = $translation_node->status ? t('Published') : t('Not published'); $status .= $translation_node->translate ? ' - ' . t('outdated') . '' : ''; @@ -59,7 +63,7 @@ $path = 'node/add/' . str_replace('_', '-', $node->type); $links = language_negotiation_get_switch_links($type, $path); $query = array('query' => array('translation' => $node->nid, 'target' => $langcode)); - $options[] = empty($links->links[$langcode]) ? l($text, $path, $query) : l($text, $links->links[$langcode]['href'], array_merge_recursive($links->links[$langcode], $query)); + $options[] = empty($links->links[$langcode]['href']) ? l($text, $path, $query) : l($text, $links->links[$langcode]['href'], array_merge_recursive($links->links[$langcode], $query)); } $status = t('Not translated'); } diff -Naur drupal-7.0/modules/translation/translation.test drupal-7.66/modules/translation/translation.test --- drupal-7.0/modules/translation/translation.test 2010-12-31 21:45:25.000000000 +0100 +++ drupal-7.66/modules/translation/translation.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,6 +1,13 @@ admin_user = $this->drupalCreateUser(array('bypass node access', 'administer nodes', 'administer languages', 'administer content types', 'administer blocks', 'access administration pages')); + $this->admin_user = $this->drupalCreateUser(array('bypass node access', 'administer nodes', 'administer languages', 'administer content types', 'administer blocks', 'access administration pages', 'translate content')); $this->translator = $this->drupalCreateUser(array('create page content', 'edit own page content', 'translate content')); $this->drupalLogin($this->admin_user); @@ -36,7 +43,7 @@ $edit = array(); $edit['language_content_type'] = 2; $this->drupalPost('admin/structure/types/manage/page', $edit, t('Save content type')); - $this->assertRaw(t('The content type %type has been updated.', array('%type' => 'Basic page')), t('Basic page content type has been updated.')); + $this->assertRaw(t('The content type %type has been updated.', array('%type' => 'Basic page')), 'Basic page content type has been updated.'); // Enable the language switcher block. $language_type = LANGUAGE_TYPE_INTERFACE; @@ -47,15 +54,14 @@ // block appear. $edit = array('language[enabled][locale-url]' => TRUE); $this->drupalPost('admin/config/regional/language/configure', $edit, t('Save settings')); - $this->assertRaw(t('Language negotiation configuration saved.'), t('URL language detection enabled.')); + $this->assertRaw(t('Language negotiation configuration saved.'), 'URL language detection enabled.'); $this->resetCaches(); $this->drupalLogin($this->translator); } /** - * Create a basic page with translation, modify the basic page outdating - * translation, and update translation. + * Creates, modifies, and updates a basic page with a translation. */ function testContentTranslation() { // Create Basic page in English. @@ -63,10 +69,18 @@ $node_body = $this->randomName(); $node = $this->createPage($node_title, $node_body, 'en'); + // Unpublish the original node to check that this has no impact on the + // translation overview page, publish it again afterwards. + $this->drupalLogin($this->admin_user); + $this->drupalPost('node/' . $node->nid . '/edit', array('status' => FALSE), t('Save')); + $this->drupalGet('node/' . $node->nid . '/translate'); + $this->drupalPost('node/' . $node->nid . '/edit', array('status' => NODE_PUBLISHED), t('Save')); + $this->drupalLogin($this->translator); + // Check that the "add translation" link uses a localized path. $languages = language_list(); $this->drupalGet('node/' . $node->nid . '/translate'); - $this->assertLinkByHref($languages['es']->prefix . '/node/add/' . str_replace('_', '-', $node->type), 0, t('The "add translation" link for %language points to the localized path of the target language.', array('%language' => $languages['es']->name))); + $this->assertLinkByHref($languages['es']->prefix . '/node/add/' . str_replace('_', '-', $node->type), 0, format_string('The "add translation" link for %language points to the localized path of the target language.', array('%language' => $languages['es']->name))); // Submit translation in Spanish. $node_translation_title = $this->randomName(); @@ -76,13 +90,13 @@ // Check that the "edit translation" and "view node" links use localized // paths. $this->drupalGet('node/' . $node->nid . '/translate'); - $this->assertLinkByHref($languages['es']->prefix . '/node/' . $node_translation->nid . '/edit', 0, t('The "edit" link for the translation in %language points to the localized path of the translation language.', array('%language' => $languages['es']->name))); - $this->assertLinkByHref($languages['es']->prefix . '/node/' . $node_translation->nid, 0, t('The "view" link for the translation in %language points to the localized path of the translation language.', array('%language' => $languages['es']->name))); + $this->assertLinkByHref($languages['es']->prefix . '/node/' . $node_translation->nid . '/edit', 0, format_string('The "edit" link for the translation in %language points to the localized path of the translation language.', array('%language' => $languages['es']->name))); + $this->assertLinkByHref($languages['es']->prefix . '/node/' . $node_translation->nid, 0, format_string('The "view" link for the translation in %language points to the localized path of the translation language.', array('%language' => $languages['es']->name))); // Attempt to submit a duplicate translation by visiting the node/add page // with identical query string. $this->drupalGet('node/add/page', array('query' => array('translation' => $node->nid, 'target' => 'es'))); - $this->assertRaw(t('A translation of %title in %language already exists', array('%title' => $node_title, '%language' => $languages['es']->name)), t('Message regarding attempted duplicate translation is displayed.')); + $this->assertRaw(t('A translation of %title in %language already exists', array('%title' => $node_title, '%language' => $languages['es']->name)), 'Message regarding attempted duplicate translation is displayed.'); // Attempt a resubmission of the form - this emulates using the back button // to return to the page then resubmitting the form without a refresh. @@ -92,34 +106,41 @@ $edit["body[$langcode][0][value]"] = $this->randomName(); $this->drupalPost('node/add/page', $edit, t('Save'), array('query' => array('translation' => $node->nid, 'language' => 'es'))); $duplicate = $this->drupalGetNodeByTitle($edit["title"]); - $this->assertEqual($duplicate->tnid, 0, t('The node does not have a tnid.')); + $this->assertEqual($duplicate->tnid, 0, 'The node does not have a tnid.'); // Update original and mark translation as outdated. $node_body = $this->randomName(); - $node->body[$node->language][0]['value'] = $node_body; + $node->body[LANGUAGE_NONE][0]['value'] = $node_body; $edit = array(); - $edit["body[$node->language][0][value]"] = $node_body; + $edit["body[$langcode][0][value]"] = $node_body; $edit['translation[retranslate]'] = TRUE; $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save')); - $this->assertRaw(t('Basic page %title has been updated.', array('%title' => $node_title)), t('Original node updated.')); + $this->assertRaw(t('Basic page %title has been updated.', array('%title' => $node_title)), 'Original node updated.'); // Check to make sure that interface shows translation as outdated. $this->drupalGet('node/' . $node->nid . '/translate'); - $this->assertRaw('' . t('outdated') . '', t('Translation marked as outdated.')); + $this->assertRaw('' . t('outdated') . '', 'Translation marked as outdated.'); // Update translation and mark as updated. $edit = array(); - $edit["body[$node_translation->language][0][value]"] = $this->randomName(); + $edit["body[$langcode][0][value]"] = $this->randomName(); $edit['translation[status]'] = FALSE; $this->drupalPost('node/' . $node_translation->nid . '/edit', $edit, t('Save')); - $this->assertRaw(t('Basic page %title has been updated.', array('%title' => $node_translation_title)), t('Translated node updated.')); + $this->assertRaw(t('Basic page %title has been updated.', array('%title' => $node_translation_title)), 'Translated node updated.'); // Confirm that disabled languages are an option for translators when // creating nodes. $this->drupalGet('node/add/page'); - $this->assertFieldByXPath('//select[@name="language"]//option', 'it', t('Italian (disabled) is available in language selection.')); + $this->assertFieldByXPath('//select[@name="language"]//option', 'it', 'Italian (disabled) is available in language selection.'); $translation_it = $this->createTranslation($node, $this->randomName(), $this->randomName(), 'it'); - $this->assertRaw($translation_it->body['it'][0]['value'], t('Content created in Italian (disabled).')); + $this->assertRaw($translation_it->body[LANGUAGE_NONE][0]['value'], 'Content created in Italian (disabled).'); + + // Confirm that language neutral is an option for translators when there are + // disabled languages. + $this->drupalGet('node/add/page'); + $this->assertFieldByXPath('//select[@name="language"]//option', LANGUAGE_NONE, 'Language neutral is available in language selection with disabled languages.'); + $node2 = $this->createPage($this->randomName(), $this->randomName(), LANGUAGE_NONE); + $this->assertRaw($node2->body[LANGUAGE_NONE][0]['value'], 'Language neutral content created with disabled languages available.'); // Leave just one language enabled and check that the translation overview // page is still accessible. @@ -128,11 +149,11 @@ $this->drupalPost('admin/config/regional/language', $edit, t('Save configuration')); $this->drupalLogin($this->translator); $this->drupalGet('node/' . $node->nid . '/translate'); - $this->assertRaw(t('Translations of %title', array('%title' => $node->title)), t('Translation overview page available with only one language enabled.')); + $this->assertRaw(t('Translations of %title', array('%title' => $node->title)), 'Translation overview page available with only one language enabled.'); } /** - * Check that language switch links behave properly. + * Checks that the language switch links behave properly. */ function testLanguageSwitchLinks() { // Create a Basic page in English and its translations in Spanish and @@ -173,7 +194,7 @@ } /** - * Test that the language switcher block alterations work as intended. + * Tests that the language switcher block alterations work as intended. */ function testLanguageSwitcherBlockIntegration() { // Enable Italian to have three items in the language switcher block. @@ -200,28 +221,65 @@ $this->assertLanguageSwitchLinks($node, $node, TRUE, $type); $this->assertLanguageSwitchLinks($node, $translation_es, TRUE, $type); $this->assertLanguageSwitchLinks($node, $translation_it, TRUE, $type); + + // Create a language neutral node and check that the language switcher is + // left untouched. + $node2 = $this->createPage($this->randomName(), $this->randomName(), LANGUAGE_NONE); + $node2_en = (object) array('nid' => $node2->nid, 'language' => 'en'); + $node2_es = (object) array('nid' => $node2->nid, 'language' => 'es'); + $node2_it = (object) array('nid' => $node2->nid, 'language' => 'it'); + $this->assertLanguageSwitchLinks($node2_en, $node2_en, TRUE, $type); + $this->assertLanguageSwitchLinks($node2_en, $node2_es, TRUE, $type); + $this->assertLanguageSwitchLinks($node2_en, $node2_it, TRUE, $type); + + // Disable translation support to check that the language switcher is left + // untouched only for new nodes. + $this->drupalLogin($this->admin_user); + $edit = array('language_content_type' => 0); + $this->drupalPost('admin/structure/types/manage/page', $edit, t('Save content type')); + $this->drupalLogin($this->translator); + + // Existing translations trigger alterations even if translation support is + // disabled. + $this->assertLanguageSwitchLinks($node, $node, TRUE, $type); + $this->assertLanguageSwitchLinks($node, $translation_es, TRUE, $type); + $this->assertLanguageSwitchLinks($node, $translation_it, TRUE, $type); + + // Check that new nodes with a language assigned do not trigger language + // switcher alterations when translation support is disabled. + $node = $this->createPage($this->randomName(), $this->randomName()); + $node_es = (object) array('nid' => $node->nid, 'language' => 'es'); + $node_it = (object) array('nid' => $node->nid, 'language' => 'it'); + $this->assertLanguageSwitchLinks($node, $node, TRUE, $type); + $this->assertLanguageSwitchLinks($node, $node_es, TRUE, $type); + $this->assertLanguageSwitchLinks($node, $node_it, TRUE, $type); } /** - * Reset static caches to make the test code match the client site behavior. + * Resets static caches to make the test code match the client-side behavior. */ function resetCaches() { drupal_static_reset('locale_url_outbound_alter'); } /** - * Return an empty node data structure. + * Returns an empty node data structure. + * + * @param $langcode + * The language code. + * + * @return + * An empty node data structure. */ function emptyNode($langcode) { return (object) array('nid' => NULL, 'language' => $langcode); } /** - * Install a the specified language if it has not been already. Otherwise make sure that - * the language is enabled. + * Installs the specified language, or enables it if it is already installed. * * @param $language_code - * The language code the check. + * The language code to check. */ function addLanguage($language_code) { // Check to make sure that language has not already been installed. @@ -236,10 +294,10 @@ // Make sure we are not using a stale list. drupal_static_reset('language_list'); $languages = language_list('language'); - $this->assertTrue(array_key_exists($language_code, $languages), t('Language was installed successfully.')); + $this->assertTrue(array_key_exists($language_code, $languages), 'Language was installed successfully.'); if (array_key_exists($language_code, $languages)) { - $this->assertRaw(t('The language %language has been created and can now be used. More information is available on the help screen.', array('%language' => $languages[$language_code]->name, '@locale-help' => url('admin/help/locale'))), t('Language has been created.')); + $this->assertRaw(t('The language %language has been created and can now be used. More information is available on the help screen.', array('%language' => $languages[$language_code]->name, '@locale-help' => url('admin/help/locale'))), 'Language has been created.'); } } elseif ($this->xpath('//input[@type="checkbox" and @name=:name and @checked="checked"]', array(':name' => 'enabled[' . $language_code . ']'))) { @@ -250,75 +308,83 @@ // It's installed but not enabled. Enable it. $this->assertTrue(true, 'Language [' . $language_code . '] already installed.'); $this->drupalPost(NULL, array('enabled[' . $language_code . ']' => TRUE), t('Save configuration')); - $this->assertRaw(t('Configuration saved.'), t('Language successfully enabled.')); + $this->assertRaw(t('Configuration saved.'), 'Language successfully enabled.'); } } /** - * Create a "Basic page" in the specified language. + * Creates a "Basic page" in the specified language. * * @param $title - * Title of basic page in specified language. + * The title of a basic page in the specified language. * @param $body - * Body of basic page in specified language. - * @param - * $language Language code. + * The body of a basic page in the specified language. + * @param $language + * (optional) Language code. + * + * @return + * A node object. */ - function createPage($title, $body, $language) { + function createPage($title, $body, $language = NULL) { $edit = array(); $langcode = LANGUAGE_NONE; $edit["title"] = $title; $edit["body[$langcode][0][value]"] = $body; - $edit['language'] = $language; + if (!empty($language)) { + $edit['language'] = $language; + } $this->drupalPost('node/add/page', $edit, t('Save')); - $this->assertRaw(t('Basic page %title has been created.', array('%title' => $title)), t('Basic page created.')); + $this->assertRaw(t('Basic page %title has been created.', array('%title' => $title)), 'Basic page created.'); // Check to make sure the node was created. $node = $this->drupalGetNodeByTitle($title); - $this->assertTrue($node, t('Node found in database.')); + $this->assertTrue($node, 'Node found in database.'); return $node; } /** - * Create a translation for the specified basic page in the specified - * language. + * Creates a translation for a basic page in the specified language. * * @param $node - * The basic page to create translation for. + * The basic page to create the translation for. * @param $title - * Title of basic page in specified language. + * The title of a basic page in the specified language. * @param $body - * Body of basic page in specified language. + * The body of a basic page in the specified language. * @param $language * Language code. + * + * @return + * Translation object. */ function createTranslation($node, $title, $body, $language) { $this->drupalGet('node/add/page', array('query' => array('translation' => $node->nid, 'target' => $language))); - $body_key = "body[$language][0][value]"; + $langcode = LANGUAGE_NONE; + $body_key = "body[$langcode][0][value]"; $this->assertFieldByXPath('//input[@id="edit-title"]', $node->title, "Original title value correctly populated."); - $this->assertFieldByXPath("//textarea[@name='$body_key']", $node->body[$node->language][0]['value'], "Original body value correctly populated."); + $this->assertFieldByXPath("//textarea[@name='$body_key']", $node->body[LANGUAGE_NONE][0]['value'], "Original body value correctly populated."); $edit = array(); $edit["title"] = $title; $edit[$body_key] = $body; $this->drupalPost(NULL, $edit, t('Save')); - $this->assertRaw(t('Basic page %title has been created.', array('%title' => $title)), t('Translation created.')); + $this->assertRaw(t('Basic page %title has been created.', array('%title' => $title)), 'Translation created.'); // Check to make sure that translation was successful. $translation = $this->drupalGetNodeByTitle($title); - $this->assertTrue($translation, t('Node found in database.')); - $this->assertTrue($translation->tnid == $node->nid, t('Translation set id correctly stored.')); + $this->assertTrue($translation, 'Node found in database.'); + $this->assertTrue($translation->tnid == $node->nid, 'Translation set id correctly stored.'); return $translation; } /** - * Assert that an element identified by the given XPath has the given content. + * Asserts an element identified by the given XPath has the given content. * * @param $xpath - * XPath used to find the element. + * The XPath used to find the element. * @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 @@ -326,7 +392,7 @@ * @param $value * The text content of the matched element to assert. * @param $message - * Message to display. + * The message to display. * @param $group * The group this message belongs to. * @@ -339,7 +405,7 @@ } /** - * Check that the specified language switch links are found/not found. + * Tests whether the specified language switch links are found. * * @param $node * The node to display. @@ -351,7 +417,7 @@ * The page areas to be checked. * * @return - * TRUE if the language switch links are found/not found. + * TRUE if the language switch links are found, FALSE if not. */ function assertLanguageSwitchLinks($node, $translation, $find = TRUE, $types = NULL) { if (empty($types)) { @@ -363,7 +429,7 @@ $result = TRUE; $languages = language_list(); - $page_language = $languages[$node->language]; + $page_language = $languages[entity_language('node', $node)]; $translation_language = $languages[$translation->language]; $url = url("node/$translation->nid", array('language' => $translation_language)); @@ -372,17 +438,17 @@ foreach ($types as $type) { $args = array('%translation_language' => $translation_language->native, '%page_language' => $page_language->native, '%type' => $type); if ($find) { - $message = t('[%page_language] Language switch item found for %translation_language language in the %type page area.', $args); + $message = format_string('[%page_language] Language switch item found for %translation_language language in the %type page area.', $args); } else { - $message = t('[%page_language] Language switch item not found for %translation_language language in the %type page area.', $args); + $message = format_string('[%page_language] Language switch item not found for %translation_language language in the %type page area.', $args); } if (!empty($translation->nid)) { $xpath = '//div[contains(@class, :type)]//a[@href=:url]'; } else { - $xpath = '//div[contains(@class, :type)]//span[@class="locale-untranslated"]'; + $xpath = '//div[contains(@class, :type)]//span[contains(@class, "locale-untranslated")]'; } $found = $this->findContentByXPath($xpath, array(':type' => $type, ':url' => $url), $translation_language->native); @@ -393,7 +459,19 @@ } /** - * Search for elements matching the given xpath and value. + * Searches for elements matching the given xpath and value. + * + * @param $xpath + * The XPath used to find the element. + * @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. + * @param $value + * The text content of the matched element to assert. + * + * @return + * TRUE if found, otherwise FALSE. */ function findContentByXPath($xpath, array $arguments = array(), $value = NULL) { $elements = $this->xpath($xpath, $arguments); diff -Naur drupal-7.0/modules/trigger/tests/trigger_test.info drupal-7.66/modules/trigger/tests/trigger_test.info --- drupal-7.0/modules/trigger/tests/trigger_test.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/trigger/tests/trigger_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,12 +1,10 @@ -; $Id: trigger_test.info,v 1.2 2010/12/20 19:59:43 webchick Exp $ name = "Trigger Test" description = "Support module for Trigger tests." package = Testing core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/trigger/tests/trigger_test.module drupal-7.66/modules/trigger/tests/trigger_test.module --- drupal-7.0/modules/trigger/tests/trigger_test.module 2010-06-29 20:24:10.000000000 +0200 +++ drupal-7.66/modules/trigger/tests/trigger_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ array( 'node_triggertest' => array( @@ -69,6 +68,9 @@ 'trigger_test_triggertest' => array( 'label' => t('Another test trigger is fired'), ), + 'trigger_test_we_sweat_it_out_in_the_streets_of_a_runaway_american_dream' => array( + 'label' => t('A test trigger with a name over 64 characters'), + ), ), ); } diff -Naur drupal-7.0/modules/trigger/trigger.admin.inc drupal-7.66/modules/trigger/trigger.admin.inc --- drupal-7.0/modules/trigger/trigger.admin.inc 2010-10-20 18:19:24.000000000 +0200 +++ drupal-7.66/modules/trigger/trigger.admin.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ condition('hook', $form_state['values']['hook']) ->condition('aid', $aid) ->execute(); + drupal_static_reset('trigger_get_assigned_actions'); $actions = actions_get_all_actions(); watchdog('actions', 'Action %action has been unassigned.', array('%action' => $actions[$aid]['label'])); drupal_set_message(t('Action %action has been unassigned.', array('%action' => $actions[$aid]['label']))); @@ -109,9 +114,9 @@ * @param $label * A plain English description of what this trigger does. * - * @ingoup forms * @see trigger_assign_form_validate() * @see trigger_assign_form_submit() + * @ingroup forms */ function trigger_assign_form($form, $form_state, $module, $hook, $label) { $form['module'] = array( @@ -198,9 +203,11 @@ } /** - * Validation function for trigger_assign_form(). + * Form validation handler for trigger_assign_form(). * * Makes sure that the user is not re-assigning an action to an event. + * + * @see trigger_assign_form_submit() */ function trigger_assign_form_validate($form, $form_state) { $form_values = $form_state['values']; @@ -217,7 +224,9 @@ } /** - * Submit function for trigger_assign_form(). + * Form submission handler for trigger_assign_form(). + * + * @see trigger_assign_form_validate() */ function trigger_assign_form_submit($form, &$form_state) { if (!empty($form_state['values']['aid'])) { @@ -272,6 +281,7 @@ } } } + drupal_static_reset('trigger_get_assigned_actions'); } /** @@ -307,4 +317,3 @@ } return $output; } - diff -Naur drupal-7.0/modules/trigger/trigger.api.php drupal-7.66/modules/trigger/trigger.api.php --- drupal-7.0/modules/trigger/trigger.api.php 2010-04-22 08:33:06.000000000 +0200 +++ drupal-7.66/modules/trigger/trigger.api.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ 'Maps trigger to hook and operation assignments from trigger.module.', 'fields' => array( 'hook' => array( 'type' => 'varchar', - 'length' => 32, + 'length' => 78, 'not null' => TRUE, 'default' => '', 'description' => 'Primary Key: The name of the internal Drupal hook; for example, node_insert.', @@ -54,9 +55,15 @@ } /** - * Adds operation names to the hook names and drops the "op" field. + * Alter the "hook" field and drop the "op field" of {trigger_assignments}. + * + * Increase the length of the "hook" field to 78 characters and adds operation + * names to the hook names, and drops the "op" field. */ function trigger_update_7000() { + db_drop_primary_key('trigger_assignments'); + db_change_field('trigger_assignments', 'hook', 'hook', array('type' => 'varchar', 'length' => 78, 'not null' => TRUE, 'default' => '', 'description' => 'Primary Key: The name of the internal Drupal hook; for example, node_insert.')); + $result = db_query("SELECT hook, op, aid FROM {trigger_assignments} WHERE op <> ''"); foreach ($result as $record) { @@ -68,4 +75,42 @@ ->execute(); } db_drop_field('trigger_assignments', 'op'); + + db_add_primary_key('trigger_assignments', array('hook', 'aid')); +} + +/** + * @addtogroup updates-7.x-extra + * @{ + */ + +/** + * Increase the length of the "hook" field to 78 characters. + * + * This is a separate function for those who ran an older version of + * trigger_update_7000() that did not do this. + */ +function trigger_update_7001() { + db_drop_primary_key('trigger_assignments'); + db_change_field('trigger_assignments', 'hook', 'hook', array('type' => 'varchar', 'length' => 78, 'not null' => TRUE, 'default' => '', 'description' => 'Primary Key: The name of the internal Drupal hook; for example, node_insert.', ), array('primary key' => array('hook', 'aid'))); +} + +/** + * Renames nodeapi to node. + */ +function trigger_update_7002() { + $result = db_query("SELECT hook, aid FROM {trigger_assignments}"); + + foreach($result as $record) { + $new_hook = str_replace('nodeapi', 'node', $record->hook); + db_update('trigger_assignments') + ->fields(array('hook' => $new_hook)) + ->condition('hook', $record->hook) + ->condition('aid', $record->aid) + ->execute(); + } } + +/** + * @} End of "addtogroup updates-7.x-extra". + */ diff -Naur drupal-7.0/modules/trigger/trigger.module drupal-7.66/modules/trigger/trigger.module --- drupal-7.0/modules/trigger/trigger.module 2010-12-01 08:41:03.000000000 +0100 +++ drupal-7.66/modules/trigger/trigger.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,10 +1,8 @@ ' . t('About') . ''; - $output .= '

        ' . t('The Trigger module provides the ability to cause actions to run when certain triggers take place on your site. Triggers are events, such as new content being added to your site or a user logging in, and actions are tasks, such as unpublishing content or e-mailing an administrator. For more information, see the online handbook entry for Trigger module.', array('@trigger' => 'http://drupal.org/handbook/modules/trigger/')) . '

        '; + $output .= '

        ' . t('The Trigger module provides the ability to cause actions to run when certain triggers take place on your site. Triggers are events, such as new content being added to your site or a user logging in, and actions are tasks, such as unpublishing content or e-mailing an administrator. For more information, see the online handbook entry for Trigger module.', array('@trigger' => 'http://drupal.org/documentation/modules/trigger/')) . '

        '; $output .= '

        ' . t('Uses') . '

        '; $output .= '
        '; $output .= '
        ' . t('Configuring triggers and actions') . '
        '; @@ -67,7 +65,10 @@ 'description' => 'Unassign an action from a trigger.', 'page callback' => 'drupal_get_form', 'page arguments' => array('trigger_unassign'), - 'access arguments' => array('administer actions'), + // Only accessible if there are any actions that can be unassigned. + 'access callback' => 'trigger_menu_unassign_access', + // Only output in the breadcrumb, not in menu trees. + 'type' => MENU_VISIBLE_IN_BREADCRUMB, 'file' => 'trigger.admin.inc', ); @@ -75,6 +76,25 @@ } /** + * Access callback: Determines if triggers can be unassigned. + * + * @return bool + * TRUE if there are triggers that the user can unassign, FALSE otherwise. + * + * @see trigger_menu() + */ +function trigger_menu_unassign_access() { + if (!user_access('administer actions')) { + return FALSE; + } + $count = db_select('trigger_assignments') + ->countQuery() + ->execute() + ->fetchField(); + return $count > 0; +} + +/** * Implements hook_trigger_info(). * * Defines all the triggers that this module implements triggers for. @@ -159,15 +179,20 @@ * * @param $hook * The name of the hook being fired. + * * @return * An array whose keys are action IDs that the user has associated with * this trigger, and whose values are arrays containing the action type and * label. */ function trigger_get_assigned_actions($hook) { - return db_query("SELECT ta.aid, a.type, a.label FROM {trigger_assignments} ta LEFT JOIN {actions} a ON ta.aid = a.aid WHERE ta.hook = :hook ORDER BY ta.weight", array( - ':hook' => $hook, - ))->fetchAllAssoc( 'aid', PDO::FETCH_ASSOC); + $actions = &drupal_static(__FUNCTION__, array()); + if (!isset($actions[$hook])) { + $actions[$hook] = db_query("SELECT ta.aid, a.type, a.label FROM {trigger_assignments} ta LEFT JOIN {actions} a ON ta.aid = a.aid WHERE ta.hook = :hook ORDER BY ta.weight", array( + ':hook' => $hook, + ))->fetchAllAssoc('aid', PDO::FETCH_ASSOC); + } + return $actions[$hook]; } /** @@ -232,8 +257,8 @@ * * @param $node * Node object. - * @param $op - * Operation to trigger. + * @param $hook + * Hook to trigger. * @param $a3 * Additional argument to action function. * @param $a4 @@ -385,8 +410,8 @@ * * @param $a1 * Comment object or array of form values. - * @param $op - * Operation to trigger. + * @param $hook + * Hook to trigger. */ function _trigger_comment($a1, $hook) { // Keep objects for reuse so that changes actions make to objects can persist. @@ -443,6 +468,7 @@ * The type of action that is about to be called. * @param $account * The account object that was passed via the user hook. + * * @return * The object expected by the action that is about to be called. */ @@ -519,6 +545,15 @@ /** * Calls action functions for user triggers. + * + * @param $hook + * The hook that called this function. + * @param $edit + * Edit variable passed in to the hook or empty array if not needed. + * @param $account + * Account variable passed in to the hook. + * @param $method + * Method variable passed in to the hook or NULL if not needed. */ function _trigger_user($hook, &$edit, $account, $category = NULL) { // Keep objects for reuse so that changes actions make to objects can persist. @@ -593,10 +628,14 @@ db_delete('trigger_assignments') ->condition('aid', $aid) ->execute(); + drupal_static_reset('trigger_get_assigned_actions'); } /** * Retrieves and caches information from hook_trigger_info() implementations. + * + * @return + * Array of all triggers. */ function _trigger_get_all_info() { $triggers = &drupal_static(__FUNCTION__); diff -Naur drupal-7.0/modules/trigger/trigger.test drupal-7.66/modules/trigger/trigger.test --- drupal-7.0/modules/trigger/trigger.test 2010-10-05 08:17:29.000000000 +0200 +++ drupal-7.66/modules/trigger/trigger.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,9 @@ 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 @@ -90,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.'); } } @@ -110,10 +114,10 @@ // Assign an action to the node save/update trigger. $test_user = $this->drupalCreateUser(array('administer actions', 'administer nodes', 'create page content', 'access administration pages', 'access content overview')); $this->drupalLogin($test_user); + $nodes = array(); for ($index = 0; $index < 3; $index++) { - $edit = array('title' => $this->randomName()); - $this->drupalPost('node/add/page', $edit, t('Save')); + $nodes[] = $this->drupalCreateNode(array('type' => 'page')); } $action_id = 'trigger_test_generic_any_action'; @@ -123,12 +127,12 @@ $edit = array( 'operation' => 'unpublish', - 'nodes[1]' => TRUE, - 'nodes[2]' => TRUE, + 'nodes[' . $nodes[0]->nid . ']' => TRUE, + 'nodes[' . $nodes[1]->nid . ']' => TRUE, ); $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))); } /** @@ -238,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.'); } } @@ -317,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))); } /** @@ -340,6 +344,7 @@ $edit = array('aid' => drupal_hash_base64($aid)); $this->drupalPost('admin/structure/trigger/user', $edit, t('Assign'), array(), array(), $form_html_id); + drupal_static_reset('trigger_get_asssigned_actions'); } @@ -368,6 +373,7 @@ $edit = array('aid' => drupal_hash_base64($aid)); $this->drupalPost('admin/structure/trigger/user', $edit, t('Assign'), array(), array(), $form_html_id); + drupal_static_reset('trigger_get_assigned_actions'); } /** @@ -396,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 )) ); } @@ -415,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'); } } @@ -511,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. @@ -523,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')); @@ -583,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); @@ -602,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'));; @@ -637,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.'); } /** @@ -673,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.'); } } @@ -717,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. @@ -731,6 +737,35 @@ // 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.'); + } +} + +/** + * Tests the unassigning of triggers. + */ +class TriggerUnassignTestCase extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Trigger unassigning', + 'description' => 'Tests the unassigning of triggers.', + 'group' => 'Trigger', + ); + } + + function setUp() { + parent::setUp('trigger', 'trigger_test'); + $web_user = $this->drupalCreateUser(array('administer actions')); + $this->drupalLogin($web_user); + } + + /** + * Tests an attempt to unassign triggers when none are assigned. + */ + function testUnassignAccessDenied() { + $this->drupalGet('admin/structure/trigger/unassign'); + $this->assertResponse(403, 'If there are no actions available, return access denied.'); } + } diff -Naur drupal-7.0/modules/update/tests/aaa_update_test.info drupal-7.66/modules/update/tests/aaa_update_test.info --- drupal-7.0/modules/update/tests/aaa_update_test.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/update/tests/aaa_update_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,12 +1,10 @@ -; $Id: aaa_update_test.info,v 1.2 2010/12/20 19:59:44 webchick Exp $ name = AAA Update test description = Support module for update module testing. package = Testing core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/update/tests/aaa_update_test.module drupal-7.66/modules/update/tests/aaa_update_test.module --- drupal-7.0/modules/update/tests/aaa_update_test.module 2009-10-01 21:23:21.000000000 +0200 +++ drupal-7.66/modules/update/tests/aaa_update_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ TRUE, 'type' => MENU_CALLBACK, ); + $items['503-error'] = array( + 'title' => t('503 Service unavailable'), + 'page callback' => 'update_callback_service_unavailable', + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); return $items; } @@ -20,13 +40,13 @@ /** * Implements hook_system_info_alter(). * - * This checks the 'update_test_system_info' variable and sees if we need to - * alter the system info for the given $file based on the setting. The setting - * is expected to be a nested associative array. If the key '#all' is defined, - * its subarray will include .info keys and values for all modules and themes - * on the system. Otherwise, the settings array is keyed by the module or - * theme short name ($file->name) and the subarrays contain settings just for - * that module or theme. + * Checks the 'update_test_system_info' variable and sees if we need to alter + * the system info for the given $file based on the setting. The setting is + * expected to be a nested associative array. If the key '#all' is defined, its + * subarray will include .info keys and values for all modules and themes on the + * system. Otherwise, the settings array is keyed by the module or theme short + * name ($file->name) and the subarrays contain settings just for that module or + * theme. */ function update_test_system_info_alter(&$info, $file) { $setting = variable_get('update_test_system_info', array()); @@ -42,13 +62,12 @@ /** * Implements hook_update_status_alter(). * - * This checks the 'update_test_update_status' variable and sees if we need to - * alter the update status for the given project based on the setting. The - * setting is expected to be a nested associative array. If the key '#all' is - * defined, its subarray will include .info keys and values for all modules - * and themes on the system. Otherwise, the settings array is keyed by the - * module or theme short name and the subarrays contain settings just for that - * module or theme. + * Checks the 'update_test_update_status' variable and sees if we need to alter + * the update status for the given project based on the setting. The setting is + * expected to be a nested associative array. If the key '#all' is defined, its + * subarray will include .info keys and values for all modules and themes on the + * system. Otherwise, the settings array is keyed by the module or theme short + * name and the subarrays contain settings just for that module or theme. */ function update_test_update_status_alter(&$projects) { $setting = variable_get('update_test_update_status', array()); @@ -66,18 +85,20 @@ } /** - * Page callback, prints mock XML for the update module. + * Page callback: Prints mock XML for the Update Manager module. * * The specific XML file to print depends on two things: the project we're * trying to fetch data for, and the desired "availability scenario" for that - * project which we're trying to test. Before attempting to fetch this data - * (by checking for updates on the available updates report), callers need to - * define the 'update_test_xml_map' variable as an array, keyed by project - * name, indicating which availability scenario to use for that project. + * project which we're trying to test. Before attempting to fetch this data (by + * checking for updates on the available updates report), callers need to define + * the 'update_test_xml_map' variable as an array, keyed by project name, + * indicating which availability scenario to use for that project. * * @param $project_name - * The project short name update.module is trying to fetch data for (the + * The project short name the update manager is trying to fetch data for (the * fetch URLs are of the form: [base_url]/[project_name]/[core_version]). + * + * @see update_test_menu() */ function update_test_mock_page($project_name) { $xml_map = variable_get('update_test_xml_map', FALSE); @@ -101,7 +122,7 @@ } /** - * Implement hook_archiver_info(). + * Implements hook_archiver_info(). */ function update_test_archiver_info() { return array( @@ -133,13 +154,23 @@ } /** - * Mock FileTransfer object to test the settings form functionality. + * Mocks a FileTransfer object to test the settings form functionality. */ class UpdateTestFileTransfer { + + /** + * Returns an UpdateTestFileTransfer object. + * + * @return + * A new UpdateTestFileTransfer object. + */ public static function factory() { return new UpdateTestFileTransfer; } + /** + * Returns a settings form with a text field to input a username. + */ public function getSettingsForm() { $form = array(); $form['udpate_test_username'] = array( @@ -149,3 +180,13 @@ return $form; } } + +/** + * Page callback: Displays an Error 503 (Service unavailable) page. + * + * @see update_test_menu() + */ +function update_callback_service_unavailable() { + drupal_add_http_header('Status', '503 Service unavailable'); + print "503 Service Temporarily Unavailable"; +} diff -Naur drupal-7.0/modules/update/update-rtl.css drupal-7.66/modules/update/update-rtl.css --- drupal-7.0/modules/update/update-rtl.css 2007-11-27 17:22:22.000000000 +0100 +++ drupal-7.66/modules/update/update-rtl.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,7 @@ -/* $Id: update-rtl.css,v 1.3 2007/11/27 16:22:22 goba Exp $ */ +/** + * @file + * RTL styles used by the Update Manager module. + */ .update .project { padding-right: .25em; diff -Naur drupal-7.0/modules/update/update.api.php drupal-7.66/modules/update/update.api.php --- drupal-7.0/modules/update/update.api.php 2010-12-06 07:50:08.000000000 +0100 +++ drupal-7.66/modules/update/update.api.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,9 +1,8 @@ connection); if (!empty($context['results']['log'][$project]['#abort'])) { @@ -164,11 +170,18 @@ } /** - * Batch callback for when the authorized update batch is finished. + * 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 * back online and clearing the update status cache after a successful update. + * + * @param $success + * TRUE if the batch operation was successful; FALSE if there were errors. + * @param $results + * An associative array of results from the batch operation. */ function update_authorize_update_batch_finished($success, $results) { foreach ($results['log'] as $project => $messages) { @@ -226,11 +239,18 @@ } /** - * Batch callback for when the authorized install batch is finished. + * 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 * back online after a successful install if necessary. + * + * @param $success + * TRUE if the batch operation was a success; FALSE if there were errors. + * @param $results + * An associative array of results from the batch operation. */ function update_authorize_install_batch_finished($success, $results) { foreach ($results['log'] as $project => $messages) { @@ -280,26 +300,30 @@ } /** - * Helper function to create a structure of log messages. + * Creates a structure of log messages. * * @param array $project_results + * An associative array of results from the batch operation. * @param string $message + * A string containing a log message. * @param bool $success + * (optional) TRUE if the operation the message is about was a success, FALSE + * if there were errors. Defaults to TRUE. */ function _update_batch_create_message(&$project_results, $message, $success = TRUE) { $project_results[] = array('message' => $message, 'success' => $success); } /** - * Private helper function to clear cached available update status data. + * Clears cached available update status data. * - * Since this function is run at such a low bootstrap level, update.module is - * not loaded. So, we can't just call _update_cache_clear(). However, the - * database is bootstrapped, so we can do a query ourselves to clear out what - * we want to clear. + * Since this function is run at such a low bootstrap level, the Update Manager + * module is not loaded. So, we can't just call _update_cache_clear(). However, + * the database is bootstrapped, so we can do a query ourselves to clear out + * what we want to clear. * - * Note that we do not want to just truncate the table, since that would - * remove items related to currently pending fetch attempts. + * Note that we do not want to just truncate the table, since that would remove + * items related to currently pending fetch attempts. * * @see update_authorize_update_batch_finished() * @see _update_cache_clear() diff -Naur drupal-7.0/modules/update/update.compare.inc drupal-7.66/modules/update/update.compare.inc --- drupal-7.0/modules/update/update.compare.inc 2010-10-04 09:26:25.000000000 +0200 +++ drupal-7.66/modules/update/update.compare.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ 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)) { @@ -212,8 +242,13 @@ } /** - * Given a $file object (as returned by system_get_files_database()), figure - * out what project it belongs to. + * Determines what project a given file object belongs to. + * + * @param $file + * A file object as returned by system_get_files_database(). + * + * @return + * The canonical project short name. * * @see system_get_files_database() */ @@ -229,7 +264,9 @@ } /** - * Process the list of projects on the system to figure out the currently + * Determines version and type information for currently installed projects. + * + * Processes the list of projects on the system to figure out the currently * installed versions, and other information that is required before we can * compare against the available releases to produce the status report. * @@ -278,7 +315,7 @@ } /** - * Calculate the current update status of all projects on the site. + * Calculates the current update status of all projects on the site. * * The results of this function are expensive to compute, especially on sites * with lots of modules or themes, since it involves a lot of comparisons and @@ -286,13 +323,16 @@ * table using the 'update_project_data' cache ID. However, since this is not * the data about available updates fetched from the network, it is ok to * invalidate it somewhat quickly. If we keep this data for very long, site - * administrators are more likely to see incorrect results if they upgrade to - * a newer version of a module or theme but do not visit certain pages that + * administrators are more likely to see incorrect results if they upgrade to a + * newer version of a module or theme but do not visit certain pages that * automatically clear this cache. * * @param array $available * Data about available project releases. * + * @return + * An array of installed projects with current update status information. + * * @see update_get_available() * @see update_get_projects() * @see update_process_project_info() @@ -328,52 +368,50 @@ } /** - * Calculate the current update status of a specific project. + * Calculates the current update status of a specific project. * - * This function is the heart of the update status feature. For each project - * it is invoked with, it first checks if the project has been flagged with a + * This function is the heart of the update status feature. For each project it + * is invoked with, it first checks if the project has been flagged with a * special status like "unsupported" or "insecure", or if the project node * itself has been unpublished. In any of those cases, the project is marked * with an error and the next project is considered. * * If the project itself is valid, the function decides what major release * series to consider. The project defines what the currently supported major - * versions are for each version of core, so the first step is to make sure - * the current version is still supported. If so, that's the target version. - * If the current version is unsupported, the project maintainer's recommended - * major version is used. There's also a check to make sure that this function - * never recommends an earlier release than the currently installed major - * version. + * versions are for each version of core, so the first step is to make sure the + * current version is still supported. If so, that's the target version. If the + * current version is unsupported, the project maintainer's recommended major + * version is used. There's also a check to make sure that this function never + * recommends an earlier release than the currently installed major version. * - * Given a target major version, it scans the available releases looking for + * Given a target major version, the available releases are scanned looking for * the specific release to recommend (avoiding beta releases and development - * snapshots if possible). This is complicated to describe, but an example - * will help clarify. For the target major version, find the highest patch - * level. If there is a release at that patch level with no extra ("beta", - * etc), then we recommend the release at that patch level with the most - * recent release date. If every release at that patch level has extra (only - * betas), then recommend the latest release from the previous patch - * level. For example: + * snapshots if possible). For the target major version, the highest patch level + * is found. If there is a release at that patch level with no extra ("beta", + * etc.), then the release at that patch level with the most recent release date + * is recommended. If every release at that patch level has extra (only betas), + * then the latest release from the previous patch level is recommended. For + * example: * - * 1.6-bugfix <-- recommended version because 1.6 already exists. - * 1.6 + * - 1.6-bugfix <-- recommended version because 1.6 already exists. + * - 1.6 * * or * - * 1.6-beta - * 1.5 <-- recommended version because no 1.6 exists. - * 1.4 - * - * It also looks for the latest release from the same major version, even a - * beta release, to display to the user as the "Latest version" option. - * Additionally, it finds the latest official release from any higher major - * versions that have been released to provide a set of "Also available" + * - 1.6-beta + * - 1.5 <-- recommended version because no 1.6 exists. + * - 1.4 + * + * Also, the latest release from the same major version is looked for, even beta + * releases, to display to the user as the "Latest version" option. + * Additionally, the latest official release from any higher major versions that + * have been released is searched for to provide a set of "Also available" * options. * - * Finally, and most importantly, it keeps scanning the release history until - * it gets to the currently installed release, searching for anything marked - * as a security update. If any security updates have been found between the - * recommended release and the installed version, all of the releases that + * Finally, and most importantly, the release history continues to be scanned + * until the currently installed release is reached, searching for anything + * marked as a security update. If any security updates have been found between + * the recommended release and the installed version, all of the releases that * included a security fix are recorded so that the site administrator can be * warned their site is insecure, and links pointing to the release notes for * each security update can be included (which, in turn, will link to the @@ -382,11 +420,18 @@ * This function relies on the fact that the .xml release history data comes * sorted based on major version and patch level, then finally by release date * if there are multiple releases such as betas from the same major.patch - * version (e.g. 5.x-1.5-beta1, 5.x-1.5-beta2, and 5.x-1.5). Development + * 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 $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]; @@ -554,7 +599,10 @@ // See if this is a higher major version than our target and yet still // supported. If so, record it as an "Also available" release. - if ($release['version_major'] > $target_major) { + // Note: some projects have a HEAD release from CVS days, which could + // be one of those being compared. They would not have version_major + // set, so we must call isset first. + if (isset($release['version_major']) && $release['version_major'] > $target_major) { if (in_array($release['version_major'], $supported_majors)) { if (!isset($project_data['also'])) { $project_data['also'] = array(); @@ -708,16 +756,16 @@ } /** - * Retrieve data from {cache_update} or empty the cache when necessary. + * Retrieves data from {cache_update} or empties the cache when necessary. * * Two very expensive arrays computed by this module are the list of all - * installed modules and themes (and .info data, project associations, etc), - * and the current status of the site relative to the currently available - * releases. These two arrays are cached in the {cache_update} table and used - * whenever possible. The cache is cleared whenever the administrator visits - * the status report, available updates report, or the module or theme - * administration pages, since we should always recompute the most current - * values on any of those pages. + * installed modules and themes (and .info data, project associations, etc), and + * the current status of the site relative to the currently available releases. + * These two arrays are cached in the {cache_update} table and used whenever + * possible. The cache is cleared whenever the administrator visits the status + * report, available updates report, or the module or theme administration + * pages, since we should always recompute the most current values on any of + * those pages. * * Note: while both of these arrays are expensive to compute (in terms of disk * I/O and some fairly heavy CPU processing), neither of these is the actual @@ -727,13 +775,13 @@ * hour and never get invalidated just by visiting a page on the site. * * @param $cid - * The cache id of data to return from the cache. Valid options are + * The cache ID of data to return from the cache. Valid options are * 'update_project_data' and 'update_project_projects'. * * @return * The cached value of the $projects array generated by - * update_calculate_project_data() or update_get_projects(), or an empty - * array when the cache is cleared. + * update_calculate_project_data() or update_get_projects(), or an empty array + * when the cache is cleared. */ function update_project_cache($cid) { $projects = array(); @@ -765,13 +813,13 @@ } /** - * Filter the project .info data to only save attributes we need. + * Filters the project .info data to only save attributes we need. * * @param array $info * Array of .info file data as returned by drupal_parse_info_file(). * * @return - * Array of .info file data we need for the Update manager. + * Array of .info file data we need for the update manager. * * @see _update_process_info_list() */ diff -Naur drupal-7.0/modules/update/update.css drupal-7.66/modules/update/update.css --- drupal-7.0/modules/update/update.css 2010-10-08 02:24:09.000000000 +0200 +++ drupal-7.66/modules/update/update.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,7 @@ -/* $Id: update.css,v 1.8 2010/10/08 00:24:09 dries Exp $ */ +/** + * @file + * Styles used by the Update Manager module. + */ .update .project { font-weight: bold; diff -Naur drupal-7.0/modules/update/update.fetch.inc drupal-7.66/modules/update/update.fetch.inc --- drupal-7.0/modules/update/update.fetch.inc 2010-05-06 07:59:31.000000000 +0200 +++ drupal-7.66/modules/update/update.fetch.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ data)) { + if (!isset($xml->error) && isset($xml->data)) { $data = $xml->data; } } @@ -185,7 +196,7 @@ } /** - * Clear out all the cached available update data and initiate re-fetching. + * Clears out all the cached available update data and initiates re-fetching. */ function _update_refresh() { module_load_include('inc', 'update', 'update.compare'); @@ -212,7 +223,7 @@ } /** - * Add a task to the queue for fetching release history data for a project. + * Adds a task to the queue for fetching release history data for a project. * * We only create a new fetch task if there's no task already in the queue for * this particular project (based on 'fetch_task::' entries in the @@ -220,8 +231,8 @@ * * @param $project * Associative array of information about a project as created by - * update_get_projects(), including keys such as 'name' (short name), - * and the 'info' array with data from a .info file for the project. + * update_get_projects(), including keys such as 'name' (short name), and the + * 'info' array with data from a .info file for the project. * * @see update_get_projects() * @see update_get_available() @@ -238,12 +249,22 @@ if (empty($fetch_tasks[$cid])) { $queue = DrupalQueue::get('update_fetch_tasks'); $queue->createItem($project); - db_insert('cache_update') - ->fields(array( - 'cid' => $cid, - 'created' => REQUEST_TIME, - )) - ->execute(); + // Due to race conditions, it is possible that another process already + // inserted a row into the {cache_update} table and the following query will + // throw an exception. + // @todo: Remove the need for the manual check by relying on a queue that + // enforces unique items. + try { + db_insert('cache_update') + ->fields(array( + 'cid' => $cid, + 'created' => REQUEST_TIME, + )) + ->execute(); + } + catch (Exception $e) { + // The exception can be ignored safely. + } $fetch_tasks[$cid] = REQUEST_TIME; } } @@ -251,14 +272,17 @@ /** * Generates the URL to fetch information about project updates. * - * This figures out the right URL to use, based on the project's .info file - * and the global defaults. Appends optional query arguments when the site is + * This figures out the right URL to use, based on the project's .info file and + * the global defaults. Appends optional query arguments when the site is * configured to report usage stats. * * @param $project * The array of project information from update_get_projects(). * @param $site_key - * The anonymous site key hash (optional). + * (optional) The anonymous site key hash. Defaults to an empty string. + * + * @return + * The URL for fetching information about updates to the specified project. * * @see update_fetch_data() * @see _update_process_fetch_task() @@ -268,26 +292,35 @@ $name = $project['name']; $url = _update_get_fetch_url_base($project); $url .= '/' . $name . '/' . DRUPAL_CORE_COMPATIBILITY; - // Only append a site_key and the version information if we have a site_key - // in the first place, and if this is not a disabled module or theme. We do - // not want to record usage statistics for disabled code. + + // 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)) { - $url .= (strpos($url, '?') === TRUE) ? '&' : '?'; + // Append the site key. + $url .= (strpos($url, '?') !== FALSE) ? '&' : '?'; $url .= 'site_key='; $url .= rawurlencode($site_key); + + // Append the version. if (!empty($project['info']['version'])) { $url .= '&version='; $url .= rawurlencode($project['info']['version']); } + + // Append the list of modules or themes enabled. + $list = array_keys($project['includes']); + $url .= '&list='; + $url .= rawurlencode(implode(',', $list)); } return $url; } /** - * Return the base of the URL to fetch available update data for a project. + * Returns the base of the URL to fetch available update data for a project. * * @param $project * The array of project information from update_get_projects(). + * * @return * The base of the URL used for fetching available update data. This does * not include the path elements to specify a particular project, version, @@ -300,10 +333,10 @@ } /** - * Perform any notifications that should be done once cron fetches new data. + * Performs any notifications that should be done once cron fetches new data. * - * This method checks the status of the site using the new data and depending - * on the configuration of the site, notifies administrators via email if there + * This method checks the status of the site using the new data and, depending + * on the configuration of the site, notifies administrators via e-mail if there * are new releases or missing security updates. * * @see update_requirements() @@ -331,14 +364,19 @@ else { $target_language = $default_language; } - drupal_mail('update', 'status_notify', $target, $target_language, $params); + $message = drupal_mail('update', 'status_notify', $target, $target_language, $params); + // Track when the last mail was successfully sent to avoid sending + // too many e-mails. + if ($message['result']) { + variable_set('update_last_email_notification', REQUEST_TIME); + } } } } } /** - * Parse the XML of the Drupal release history info files. + * Parses the XML of the Drupal release history info files. * * @param $raw_xml * A raw XML string of available release data for a given project. diff -Naur drupal-7.0/modules/update/update.info drupal-7.66/modules/update/update.info --- drupal-7.0/modules/update/update.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/update/update.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: update.info,v 1.10 2010/12/20 19:59:43 webchick Exp $ name = Update manager description = Checks for available updates, and can securely install or update modules and themes via a web interface. version = VERSION @@ -7,8 +6,7 @@ files[] = update.test configure = admin/reports/updates/settings -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/update/update.install drupal-7.66/modules/update/update.install --- drupal-7.0/modules/update/update.install 2011-01-02 18:26:39.000000000 +0100 +++ drupal-7.66/modules/update/update.install 2019-04-17 22:20:46.000000000 +0200 @@ -1,28 +1,26 @@ $entry, + ) + $attributes; } else { $form['project_downloads'][$name] = array( @@ -258,7 +265,7 @@ } /** - * Returns HTML for the first page in the update manager wizard to select projects. + * Returns HTML for the first page in the process of updating projects. * * @param $variables * An associative array containing: @@ -275,7 +282,11 @@ } /** - * Validation callback to ensure that at least one project is selected. + * Form validation handler for update_manager_update_form(). + * + * Ensures that at least one project is selected. + * + * @see update_manager_update_form_submit() */ function update_manager_update_form_validate($form, &$form_state) { if (!empty($form_state['values']['projects'])) { @@ -290,11 +301,11 @@ } /** - * Submit function for the main update form. + * Form submission handler for update_manager_update_form(). * - * This sets up a batch to download, extract and verify the selected releases + * Sets up a batch that downloads, extracts, and verifies the selected releases. * - * @see update_manager_update_form() + * @see update_manager_update_form_validate() */ function update_manager_update_form_submit($form, &$form_state) { $projects = array(); @@ -324,7 +335,14 @@ } /** - * Batch callback invoked when the download batch is completed. + * Implements callback_batch_finished(). + * + * Batch callback: Performs actions when the download batch is completed. + * + * @param $success + * TRUE if the batch operation was successful, FALSE if there were errors. + * @param $results + * An associative array of results from the batch operation. */ function update_manager_download_batch_finished($success, $results) { if (!empty($results['errors'])) { @@ -347,15 +365,21 @@ } /** + * Form constructor for the update ready form. + * * Build the form when the site is ready to update (after downloading). * * This form is an intermediary step in the automated update workflow. It is - * presented to the site administrator after all the required updates have - * been downloaded and verified. The point of this page is to encourage the - * user to backup their site, gives them the opportunity to put the site - * offline, and then asks them to confirm that the update should continue. - * After this step, the user is redirected to authorize.php to enter their - * file transfer credentials and attempt to complete the update. + * presented to the site administrator after all the required updates have been + * downloaded and verified. The point of this page is to encourage the user to + * backup their site, give them the opportunity to put the site offline, and + * then ask them to confirm that the update should continue. After this step, + * the user is redirected to authorize.php to enter their file transfer + * credentials and attempt to complete the update. + * + * @see update_manager_update_ready_form_submit() + * @see update_menu() + * @ingroup forms */ function update_manager_update_ready_form($form, &$form_state) { if (!_update_manager_check_backends($form, 'update')) { @@ -384,13 +408,13 @@ } /** - * Submit handler for the form to confirm that an update should continue. + * Form submission handler for update_manager_update_ready_form(). * * If the site administrator requested that the site is put offline during the - * update, do so now. Otherwise, pull information about all the required - * updates out of the SESSION, figure out what Updater class is needed for - * each one, generate an array of update operations to perform, and hand it - * all off to system_authorized_init(), then redirect to authorize.php. + * update, do so now. Otherwise, pull information about all the required updates + * out of the SESSION, figure out what Drupal\Core\Updater\Updater class is + * needed for each one, generate an array of update operations to perform, and + * hand it all off to system_authorized_init(), then redirect to authorize.php. * * @see update_authorize_run_update() * @see system_authorized_init() @@ -449,26 +473,27 @@ */ /** - * @defgroup update_manager_install Update manager: install + * @defgroup update_manager_install Update Manager module: install * @{ - * Update manager for installing new code. + * Update Manager module functionality for installing new code. * * Provides a user interface to install new code. */ /** - * Build the form for the update manager page to install new projects. + * Form constructor for the install form of the Update Manager module. * * This presents a place to enter a URL or upload an archive file to use to * install a new module or theme. * - * @param $form - * @param $form_state * @param $context - * String representing the context from which we're trying to install, can - * be: 'module', 'theme' or 'report'. - * @return - * The form array for selecting which project to install. + * String representing the context from which we're trying to install. + * Allowed values are 'module', 'theme', and 'report'. + * + * @see update_manager_install_form_validate() + * @see update_manager_install_form_submit() + * @see update_menu() + * @ingroup forms */ function update_manager_install_form($form, &$form_state, $context) { if (!_update_manager_check_backends($form, 'install')) { @@ -519,11 +544,11 @@ * @param array $form * Reference to the form array we're building. * @param string $operation - * The Update manager operation we're in the middle of. Can be either - * 'update' or 'install'. Use to provide operation-specific interface text. + * The update manager operation we're in the middle of. Can be either 'update' + * or 'install'. Use to provide operation-specific interface text. * * @return - * TRUE if the Update manager should continue to the next step in the + * TRUE if the update manager should continue to the next step in the * workflow, or FALSE if we've hit a fatal configuration and must halt the * workflow. */ @@ -581,7 +606,9 @@ } /** - * Validate the form for installing a new project via the update manager. + * Form validation handler for update_manager_install_form(). + * + * @see update_manager_install_form_submit() */ function update_manager_install_form_validate($form, &$form_state) { if (!($form_state['values']['project_url'] XOR !empty($_FILES['files']['name']['project_upload']))) { @@ -596,7 +623,7 @@ } /** - * Handle form submission when installing new projects via the update manager. + * Form submission handler for update_manager_install_form(). * * Either downloads the file specified in the URL to a temporary cache, or * uploads the file attached to the form, then attempts to extract the archive @@ -606,6 +633,7 @@ * via authorize.php which will copy the extracted files from the temporary * location into the live site. * + * @see update_manager_install_form_validate() * @see update_authorize_run_install() * @see system_authorized_init() * @see system_authorized_get_url() @@ -644,8 +672,11 @@ form_set_error($field, t('Provided archive contains no files.')); return; } - // Unfortunately, we can only use the directory name for this. :( - $project = drupal_substr($files[0], 0, -1); + + // Unfortunately, we can only use the directory name to determine the project + // name. Some archivers list the first file as the directory (i.e., MODULE/) + // and others list an actual file (i.e., MODULE/README.TXT). + $project = strtok($files[0], '/\\'); $archive_errors = update_manager_archive_verify($project, $local_cache, $directory); if (!empty($archive_errors)) { @@ -720,46 +751,26 @@ */ /** - * @defgroup update_manager_file Update manager: file management + * @defgroup update_manager_file Update Manager module: file management * @{ - * Update manager file management functions. + * Update Manager module file management functions. * - * These functions are used by the update manager to copy, extract - * and verify archive files. + * These functions are used by the update manager to copy, extract, and verify + * archive files. */ /** - * Return the directory where update archive files should be extracted. + * Unpacks a downloaded archive file. * - * If the directory does not already exist, attempt to create it. - * - * @return - * The full path to the temporary directory where update file archives - * should be extracted. - */ -function _update_manager_extract_directory() { - $directory = &drupal_static(__FUNCTION__, ''); - if (empty($directory)) { - $directory = 'temporary://update-extraction'; - if (!file_exists($directory)) { - mkdir($directory); - } - } - return $directory; -} - -/** - * Unpack a downloaded archive file. - * - * @param string $project - * The short name of the project to download. * @param string $file * The filename of the archive you wish to extract. * @param string $directory * The directory you wish to extract the archive into. + * * @return Archiver * The Archiver object used to extract the archive. - * @throws Exception on failure. + * + * @throws Exception */ function update_manager_archive_extract($file, $directory) { $archiver = archiver_get_archiver($file); @@ -771,8 +782,12 @@ // old files mixed with the new files (e.g. in cases where files were removed // from a later release). $files = $archiver->listContents(); - // Unfortunately, we can only use the directory name for this. :( - $project = drupal_substr($files[0], 0, -1); + + // Unfortunately, we can only use the directory name to determine the project + // name. Some archivers list the first file as the directory (i.e., MODULE/) + // and others list an actual file (i.e., MODULE/README.TXT). + $project = strtok($files[0], '/\\'); + $extract_location = $directory . '/' . $project; if (file_exists($extract_location)) { file_unmanaged_delete_recursive($extract_location); @@ -783,7 +798,7 @@ } /** - * Verify an archive after it has been downloaded and extracted. + * Verifies an archive after it has been downloaded and extracted. * * This function is responsible for invoking hook_verify_update_archive(). * @@ -795,18 +810,17 @@ * The directory that the archive was extracted into. * * @return array - * An array of error messages to display if the archive was invalid. If - * there are no errors, it will be an empty array. - * + * An array of error messages to display if the archive was invalid. If there + * are no errors, it will be an empty array. */ function update_manager_archive_verify($project, $archive_file, $directory) { return module_invoke_all('verify_update_archive', $project, $archive_file, $directory); } /** - * Copies a file from $url to the temporary directory for updates. + * Copies a file from the specified URL to the temporary directory for updates. * - * If the file has already been downloaded, returns the the local path. + * Returns the local path if the file has already been downloaded. * * @param $url * The URL of the file on the server. @@ -823,12 +837,8 @@ } // Check the cache and download the file if needed. - $cache_directory = 'temporary://update-cache'; - $local = $cache_directory . '/' . basename($parsed_url['path']); - - if (!file_exists($cache_directory)) { - mkdir($cache_directory); - } + $cache_directory = _update_manager_cache_directory(); + $local = $cache_directory . '/' . drupal_basename($parsed_url['path']); if (!file_exists($local) || update_delete_file_if_stale($local)) { return system_retrieve_file($url, $local, FALSE, FILE_EXISTS_REPLACE); @@ -839,18 +849,20 @@ } /** - * Batch operation: download, unpack, and verify 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 support. The file is downloaded to a local cache. + * 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 + * support. The file is downloaded to a local cache. * * @param string $project * The short name of the project to download. * @param string $url * The URL to download a specific project release archive file. - * @param array &$context - * Reference to an array used for BatchAPI storage. + * @param array $context + * Reference to an array used for Batch API storage. * * @see update_manager_download_page() */ @@ -899,8 +911,8 @@ * Determines if file transfers will be performed locally. * * If the server is configured such that webserver-created files have the same - * owner as the configuration directory (e.g. sites/default) where new code - * will eventually be installed, the Update manager can transfer files entirely + * owner as the configuration directory (e.g., sites/default) where new code + * will eventually be installed, the update manager can transfer files entirely * locally, without changing their ownership (in other words, without prompting * the user for FTP, SSH or other credentials). * diff -Naur drupal-7.0/modules/update/update.module drupal-7.66/modules/update/update.module --- drupal-7.0/modules/update/update.module 2011-01-03 03:17:34.000000000 +0100 +++ drupal-7.66/modules/update/update.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,12 +1,14 @@ ' . t('About') . ''; - $output .= '

        ' . t("The Update manager module periodically checks for new versions of your site's software (including contributed modules and themes), and alerts administrators to available updates. In order to provide update information, anonymous usage statistics are sent to Drupal.org. If desired, you may disable the Update manager module from the Module administration page. For more information, see the online handbook entry for Update manager module.", array('@update' => 'http://drupal.org/handbook/modules/update', '@modules' => url('admin/modules'))) . '

        '; + $output .= '

        ' . t("The Update manager module periodically checks for new versions of your site's software (including contributed modules and themes), and alerts administrators to available updates. In order to provide update information, anonymous usage statistics are sent to Drupal.org. If desired, you may disable the Update manager module from the Module administration page. For more information, see the online handbook entry for Update manager module.", array('@update' => 'http://drupal.org/documentation/modules/update', '@modules' => url('admin/modules'))) . '

        '; // Only explain the Update manager if it has not been disabled. if (update_manager_access()) { $output .= '

        ' . t('The Update manager also allows administrators to update and install modules and themes through the administration interface.') . '

        '; @@ -94,7 +96,7 @@ // Only explain the Update manager if it has not been disabled. if (update_manager_access()) { $output .= '
        ' . t('Performing updates through the user interface') . '
        '; - $output .= '
        ' . t('The Update manager module allows administrators to perform updates directly through the administration interface. At the top of the modules and themes pages you will see a link to update to new releases. This will direct you to the update page where you see a listing of all the missing updates and confirm which ones you want to upgrade. From there, you are prompted for your FTP/SSH password, which then transfers the files into your Drupal installation, overwriting your old files. More detailed instructions can be found in the online handbook.', array('@modules_page' => url('admin/modules'), '@themes_page' => url('admin/appearance'), '@update-page' => url('admin/reports/updates/update'), '@update' => 'http://drupal.org/handbook/modules/update')) . '
        '; + $output .= '
        ' . t('The Update manager module allows administrators to perform updates directly through the administration interface. At the top of the modules and themes pages you will see a link to update to new releases. This will direct you to the update page where you see a listing of all the missing updates and confirm which ones you want to upgrade. From there, you are prompted for your FTP/SSH password, which then transfers the files into your Drupal installation, overwriting your old files. More detailed instructions can be found in the online handbook.', array('@modules_page' => url('admin/modules'), '@themes_page' => url('admin/appearance'), '@update-page' => url('admin/reports/updates/update'), '@update' => 'http://drupal.org/documentation/modules/update')) . '
        '; $output .= '
        ' . t('Installing new modules and themes through the user interface') . '
        '; $output .= '
        ' . t('You can also install new modules and themes in the same fashion, through the install page, or by clicking the Install new module/theme link at the top of the modules and themes pages. In this case, you are prompted to provide either the URL to the download, or to upload a packaged release file from your local computer.', array('@modules_page' => url('admin/modules'), '@themes_page' => url('admin/appearance'), '@install' => url('admin/reports/updates/install'))) . '
        '; } @@ -137,10 +139,10 @@ if (!empty($verbose)) { if (isset($status[$type]['severity'])) { if ($status[$type]['severity'] == REQUIREMENT_ERROR) { - drupal_set_message($status[$type]['description'], 'error'); + drupal_set_message($status[$type]['description'], 'error', FALSE); } elseif ($status[$type]['severity'] == REQUIREMENT_WARNING) { - drupal_set_message($status[$type]['description'], 'warning'); + drupal_set_message($status[$type]['description'], 'warning', FALSE); } } } @@ -150,7 +152,7 @@ if (isset($status[$type]) && isset($status[$type]['reason']) && $status[$type]['reason'] === UPDATE_NOT_SECURE) { - drupal_set_message($status[$type]['description'], 'error'); + drupal_set_message($status[$type]['description'], 'error', FALSE); } } } @@ -247,11 +249,14 @@ } /** - * Determine if the current user can access the updater menu items. + * Access callback: Resolves if the current user can access updater menu items. + * + * It both enforces the 'administer software updates' permission and the global + * kill switch for the authorize.php script. * - * This is used as a menu system access callback. It both enforces the - * 'administer software updates' permission and the global killswitch for the - * authorize.php script. + * @return + * TRUE if the current user can access the updater menu items; FALSE + * otherwise. * * @see update_menu() */ @@ -273,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', ), ); } @@ -294,13 +302,19 @@ // the cached data for all projects, attempt to re-fetch, and trigger any // configured notifications about the new status. update_refresh(); - _update_cron_notify(); + update_fetch_data(); } else { // Otherwise, see if any individual projects are now stale or still // missing data, and if so, try to fetch the data. update_get_available(TRUE); } + if ((REQUEST_TIME - variable_get('update_last_email_notification', 0)) > $interval) { + // If configured time between notifications elapsed, send email about + // updates possibly available. + module_load_include('inc', 'update', 'update.fetch'); + _update_cron_notify(); + } // Clear garbage from disk. update_clear_update_disk_cache(); @@ -327,10 +341,10 @@ } /** - * Implements hook_form_FORM_ID_alter(). + * Implements hook_form_FORM_ID_alter() for system_modules(). * - * Adds a submit handler to the system modules form, so that if a site admin - * saves the form, we invalidate the cache of available updates. + * Adds a form submission handler to the system modules form, so that if a site + * admin saves the form, we invalidate the cache of available updates. * * @see _update_cache_clear() */ @@ -339,7 +353,9 @@ } /** - * Helper function for use as a form submit callback. + * Form submission handler for system_modules(). + * + * @see update_form_system_modules_alter() */ function update_cache_clear_submit($form, &$form_state) { // Clear all update module caches. @@ -347,7 +363,7 @@ } /** - * Prints a warning message when there is no data about available updates. + * Returns a warning message when there is no data about available updates. */ function _update_no_data() { $destination = drupal_get_destination(); @@ -358,20 +374,22 @@ } /** - * Internal helper to try to get the update information from the cache - * if possible, and to refresh the cache when necessary. + * Tries to get update information from cache and refreshes it when necessary. * * In addition to checking the cache lifetime, this function also ensures that * there are no .info files for enabled modules or themes that have a newer * modification timestamp than the last time we checked for available update - * data. If any .info file was modified, it almost certainly means a new - * version of something was installed. Without fresh available update data, - * the logic in update_calculate_project_data() will be wrong and produce - * confusing, bogus results. + * data. If any .info file was modified, it almost certainly means a new version + * of something was installed. Without fresh available update data, the logic in + * update_calculate_project_data() will be wrong and produce confusing, bogus + * results. * * @param $refresh - * Boolean to indicate if this method should refresh the cache automatically - * if there's no data. + * (optional) Boolean to indicate if this method should refresh the cache + * automatically if there's no data. Defaults to FALSE. + * + * @return + * Array of data about available releases, keyed by project shortname. * * @see update_refresh() * @see update_get_projects() @@ -428,7 +446,11 @@ } /** - * Wrapper to load the include file and then create a new fetch task. + * Creates a new fetch task after loading the necessary include file. + * + * @param $project + * Associative array of information about a project. See update_get_projects() + * for the keys used. * * @see _update_create_fetch_task() */ @@ -438,7 +460,7 @@ } /** - * Wrapper to load the include file and then refresh the release data. + * Refreshes the release data after loading the necessary include file. * * @see _update_refresh() */ @@ -448,7 +470,9 @@ } /** - * Wrapper to load the include file and then attempt to fetch update data. + * Attempts to fetch update data after loading the necessary include file. + * + * @see _update_fetch_data() */ function update_fetch_data() { module_load_include('inc', 'update', 'update.fetch'); @@ -456,7 +480,7 @@ } /** - * Return all currently cached data about available releases for all projects. + * Returns all currently cached data about available releases for all projects. * * @return * Array of data about available releases, keyed by project shortname. @@ -481,17 +505,17 @@ /** * Implements hook_mail(). * - * Constructs the email notification message when the site is out of date. + * Constructs the e-mail notification message when the site is out of date. * * @param $key * Unique key to indicate what message to build, always 'status_notify'. * @param $message * Reference to the message array being built. * @param $params - * Array of parameters to indicate what kind of text to include in the - * message body. This is a keyed array of message type ('core' or 'contrib') - * as the keys, and the status reason constant (UPDATE_NOT_SECURE, etc) for - * the values. + * Array of parameters to indicate what kind of text to include in the message + * body. This is a keyed array of message type ('core' or 'contrib') as the + * keys, and the status reason constant (UPDATE_NOT_SECURE, etc) for the + * values. * * @see drupal_mail() * @see _update_cron_notify() @@ -518,22 +542,23 @@ } /** - * Helper function to return the appropriate message text when the site is out - * of date or missing a security update. + * Returns the appropriate message text when site is out of date or not secure. * * These error messages are shared by both update_requirements() for the * site-wide status report at admin/reports/status and in the body of the - * notification emails generated by update_cron(). + * notification e-mail messages generated by update_cron(). * * @param $msg_type - * String to indicate what kind of message to generate. Can be either - * 'core' or 'contrib'. + * String to indicate what kind of message to generate. Can be either 'core' + * or 'contrib'. * @param $msg_reason * Integer constant specifying why message is generated. * @param $report_link - * Boolean that controls if a link to the updates report should be added. + * (optional) Boolean that controls if a link to the updates report should be + * added. Defaults to FALSE. * @param $language - * An optional language object to use. + * (optional) A language object to use. Defaults to NULL. + * * @return * The properly translated error message for the given key. */ @@ -603,10 +628,9 @@ } /** - * Private sort function to order projects based on their status. + * Orders projects based on their status. * - * @see update_requirements() - * @see uasort() + * Callback for uasort() within update_requirements(). */ function _update_project_status_sort($a, $b) { // The status constants are numerically in the right order, so we can @@ -621,17 +645,16 @@ /** * Returns HTML for the last time we checked for update data. * - * In addition to properly formating the given timestamp, this function also + * In addition to properly formatting the given timestamp, this function also * provides a "Check manually" link that refreshes the available update and * redirects back to the same page. * * @param $variables * An associative array containing: - * - 'last': The timestamp when the site last checked for available updates. + * - last: The timestamp when the site last checked for available updates. * * @see theme_update_report() * @see theme_update_available_updates_form() - * * @ingroup themeable */ function theme_update_last_check($variables) { @@ -647,7 +670,7 @@ * Implements hook_verify_update_archive(). * * First, we ensure that the archive isn't a copy of Drupal core, which the - * Update manager does not yet support. @see http://drupal.org/node/606592 + * update manager does not yet support. See http://drupal.org/node/606592 * * Then, we make sure that at least one module included in the archive file has * an .info file which claims that the code is compatible with the current @@ -695,14 +718,14 @@ } if (empty($files)) { - $errors[] = t('%archive_file does not contain any .info files.', array('%archive_file' => basename($archive_file))); + $errors[] = t('%archive_file does not contain any .info files.', array('%archive_file' => drupal_basename($archive_file))); } elseif (!$compatible_project) { $errors[] = format_plural( count($incompatible), '%archive_file contains a version of %names that is not compatible with Drupal !version.', '%archive_file contains versions of modules or themes that are not compatible with Drupal !version: %names', - array('!version' => DRUPAL_CORE_COMPATIBILITY, '%archive_file' => basename($archive_file), '%names' => implode(', ', $incompatible)) + array('!version' => DRUPAL_CORE_COMPATIBILITY, '%archive_file' => drupal_basename($archive_file), '%names' => implode(', ', $incompatible)) ); } @@ -719,19 +742,19 @@ * cleared when we're populating it after successfully fetching new available * update data. Usage of the core cache API results in all sorts of potential * problems that would result in attempting to fetch available update data all - * the time, including if a site has a "minimum cache lifetime" (which is both - * a minimum and a maximum) defined, or if a site uses memcache or another - * plug-able cache system that assumes volatile caches. - * - * Update module still uses the {cache_update} table, but instead of using - * cache_set(), cache_get(), and cache_clear_all(), there are private helper - * functions that implement these same basic tasks but ensure that the cache - * is not prematurely cleared, and that the data is always stored in the + * the time, including if a site has a "minimum cache lifetime" (which is both a + * minimum and a maximum) defined, or if a site uses memcache or another + * pluggable cache system that assumes volatile caches. + * + * The Update Manager module still uses the {cache_update} table, but instead of + * using cache_set(), cache_get(), and cache_clear_all(), there are private + * helper functions that implement these same basic tasks but ensure that the + * cache is not prematurely cleared, and that the data is always stored in the * database, even if memcache or another cache backend is in use. */ /** - * Store data in the private update status cache table. + * Stores data in the private update status cache table. * * @param $cid * The cache ID to save the data with. @@ -743,6 +766,8 @@ * by explicitly using _update_cache_clear(). * - A Unix timestamp: Indicates that the item should be kept at least until * the given time, after which it will be invalidated. + * + * @see _update_cache_get() */ function _update_cache_set($cid, $data, $expire) { $fields = array( @@ -764,12 +789,15 @@ } /** - * Retrieve data from the private update status cache table. + * Retrieves data from the private update status cache table. * * @param $cid * The cache ID to retrieve. + * * @return - * The data for the given cache ID, or NULL if the ID was not found. + * An array of data for the given cache ID, or NULL if the ID was not found. + * + * @see _update_cache_set() */ function _update_cache_get($cid) { $cache = db_query("SELECT data, created, expire, serialized FROM {cache_update} WHERE cid = :cid", array(':cid' => $cid))->fetchObject(); @@ -782,7 +810,10 @@ } /** - * Return an array of cache items with a given cache ID prefix. + * Returns an array of cache items with a given cache ID prefix. + * + * @param string $cid_prefix + * The cache ID prefix. * * @return * Associative array of cache items, keyed by cache ID. @@ -808,15 +839,20 @@ * Invalidates cached data relating to update status. * * @param $cid - * Optional cache ID of the record to clear from the private update module - * cache. If empty, all records will be cleared from the table. + * (optional) Cache ID of the record to clear from the private update module + * cache. If empty, all records will be cleared from the table except fetch + * tasks. Defaults to NULL. * @param $wildcard - * If $wildcard is TRUE, cache IDs starting with $cid are deleted in - * addition to the exact cache ID specified by $cid. + * (optional) If TRUE, cache IDs starting with $cid are deleted in addition to + * the exact cache ID specified by $cid. Defaults to FALSE. */ function _update_cache_clear($cid = NULL, $wildcard = FALSE) { if (empty($cid)) { - db_truncate('cache_update')->execute(); + db_delete('cache_update') + // Clear everything except fetch task information because these are used + // to ensure that the fetch task queue items are not added multiple times. + ->condition('cid', 'fetch_task::%', 'NOT LIKE') + ->execute(); } else { $query = db_delete('cache_update'); @@ -833,18 +869,18 @@ /** * Implements hook_flush_caches(). * - * Called from update.php (among others) to flush the caches. - * Since we're running update.php, we are likely to install a new version of - * something, in which case, we want to check for available update data again. - * However, because we have our own caching system, we need to directly clear - * the database table ourselves at this point and return nothing, for example, - * on sites that use memcache where cache_clear_all() won't know how to purge - * this data. - * - * However, we only want to do this from update.php, since otherwise, we'd - * lose all the available update data on every cron run. So, we specifically - * check if the site is in MAINTENANCE_MODE == 'update' (which indicates - * update.php is running, not update module... alas for overloaded names). + * Called from update.php (among others) to flush the caches. Since we're + * running update.php, we are likely to install a new version of something, in + * which case, we want to check for available update data again. However, + * because we have our own caching system, we need to directly clear the + * database table ourselves at this point and return nothing, for example, on + * sites that use memcache where cache_clear_all() won't know how to purge this + * data. + * + * However, we only want to do this from update.php, since otherwise, we'd lose + * all the available update data on every cron run. So, we specifically check if + * the site is in MAINTENANCE_MODE == 'update' (which indicates update.php is + * running, not update module... alas for overloaded names). */ function update_flush_caches() { if (defined('MAINTENANCE_MODE') && MAINTENANCE_MODE == 'update') { @@ -858,13 +894,72 @@ */ /** - * Clear the temporary files and directories based on file age from disk. + * Returns a short unique identifier for this Drupal installation. + * + * @return + * An eight character string uniquely identifying this Drupal installation. + */ +function _update_manager_unique_identifier() { + $id = &drupal_static(__FUNCTION__, ''); + if (empty($id)) { + $id = substr(hash('sha256', drupal_get_hash_salt()), 0, 8); + } + return $id; +} + +/** + * Returns the directory where update archive files should be extracted. + * + * @param $create + * (optional) Whether to attempt to create the directory if it does not + * already exist. Defaults to TRUE. + * + * @return + * The full path to the temporary directory where update file archives should + * be extracted. + */ +function _update_manager_extract_directory($create = TRUE) { + $directory = &drupal_static(__FUNCTION__, ''); + if (empty($directory)) { + $directory = 'temporary://update-extraction-' . _update_manager_unique_identifier(); + if ($create && !file_exists($directory)) { + mkdir($directory); + } + } + return $directory; +} + +/** + * Returns the directory where update archive files should be cached. + * + * @param $create + * (optional) Whether to attempt to create the directory if it does not + * already exist. Defaults to TRUE. + * + * @return + * The full path to the temporary directory where update file archives should + * be cached. + */ +function _update_manager_cache_directory($create = TRUE) { + $directory = &drupal_static(__FUNCTION__, ''); + if (empty($directory)) { + $directory = 'temporary://update-cache-' . _update_manager_unique_identifier(); + if ($create && !file_exists($directory)) { + mkdir($directory); + } + } + return $directory; +} + +/** + * Clears the temporary files and directories based on file age from disk. */ function update_clear_update_disk_cache() { - // List of update module cache directories. + // List of update module cache directories. Do not create the directories if + // they do not exist. $directories = array( - 'temporary://update-cache', - 'temporary://update-extraction', + _update_manager_cache_directory(FALSE), + _update_manager_extract_directory(FALSE), ); // Search for files and directories in base folder only without recursion. @@ -874,19 +969,19 @@ } /** - * Delete stale files and directories from the Update manager disk cache. + * Deletes stale files and directories from the update manager disk cache. * - * Files and directories older than 6 hours and development snapshots older - * than 5 minutes are considered stale. We only cache development snapshots - * for 5 minutes since otherwise updated snapshots might not be downloaded as + * Files and directories older than 6 hours and development snapshots older than + * 5 minutes are considered stale. We only cache development snapshots for 5 + * minutes since otherwise updated snapshots might not be downloaded as * expected. * * When checking file ages, we need to use the ctime, not the mtime - * (modification time) since many (all?) tar implementations go out of their - * way to set the mtime on the files they create to the timestamps recorded - * in the tarball. We want to see the last time the file was changed on disk, - * which is left alone by tar and correctly set to the time the archive file - * was unpacked. + * (modification time) since many (all?) tar implementations go out of their way + * to set the mtime on the files they create to the timestamps recorded in the + * tarball. We want to see the last time the file was changed on disk, which is + * left alone by tar and correctly set to the time the archive file was + * unpacked. * * @param $path * A string containing a file path or (streamwrapper) URI. diff -Naur drupal-7.0/modules/update/update.report.inc drupal-7.66/modules/update/update.report.inc --- drupal-7.0/modules/update/update.report.inc 2010-10-04 00:43:16.000000000 +0200 +++ drupal-7.66/modules/update/update.report.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ 'misc/watchdog-ok.png', 'alt' => t('ok'), 'title' => t('ok'))); + $icon = theme('image', array('path' => 'misc/watchdog-ok.png', 'width' => 18, 'height' => 18, 'alt' => t('ok'), 'title' => t('ok'))); break; case UPDATE_UNKNOWN: case UPDATE_FETCH_PENDING: case UPDATE_NOT_FETCHED: $class = 'unknown'; - $icon = theme('image', array('path' => 'misc/watchdog-warning.png', 'alt' => t('warning'), 'title' => t('warning'))); + $icon = theme('image', array('path' => 'misc/watchdog-warning.png', 'width' => 18, 'height' => 18, 'alt' => t('warning'), 'title' => t('warning'))); break; case UPDATE_NOT_SECURE: case UPDATE_REVOKED: case UPDATE_NOT_SUPPORTED: $class = 'error'; - $icon = theme('image', array('path' => 'misc/watchdog-error.png', 'alt' => t('error'), 'title' => t('error'))); + $icon = theme('image', array('path' => 'misc/watchdog-error.png', 'width' => 18, 'height' => 18, 'alt' => t('error'), 'title' => t('error'))); break; case UPDATE_NOT_CHECKED: case UPDATE_NOT_CURRENT: default: $class = 'warning'; - $icon = theme('image', array('path' => 'misc/watchdog-warning.png', 'alt' => t('warning'), 'title' => t('warning'))); + $icon = theme('image', array('path' => 'misc/watchdog-warning.png', 'width' => 18, 'height' => 18, 'alt' => t('warning'), 'title' => t('warning'))); break; } @@ -258,6 +259,7 @@ * - status: The integer code for a project's current update status. * * @see update_calculate_project_data() + * @ingroup themeable */ function theme_update_status_label($variables) { switch ($variables['status']) { diff -Naur drupal-7.0/modules/update/update.settings.inc drupal-7.66/modules/update/update.settings.inc --- drupal-7.0/modules/update/update.settings.inc 2010-06-26 23:32:20.000000000 +0200 +++ drupal-7.66/modules/update/update.settings.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ '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), ); @@ -58,9 +61,11 @@ } /** - * Validation callback for the settings form. + * Form validation handler for update_settings(). + * + * Validates the e-mail addresses and ensures the field is formatted correctly. * - * Validates the email addresses and ensures the field is formatted correctly. + * @see update_settings_submit() */ function update_settings_validate($form, &$form_state) { if (!empty($form_state['values']['update_notify_emails'])) { @@ -90,13 +95,16 @@ } /** - * Submit handler for the settings tab. + * Form submission handler for update_settings(). + * + * Also invalidates the cache of available updates if the "Check for updates of + * 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). * - * 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 need 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() */ function update_settings_submit($form, $form_state) { $op = $form_state['values']['op']; diff -Naur drupal-7.0/modules/update/update.test drupal-7.66/modules/update/update.test --- drupal-7.0/modules/update/update.test 2011-01-03 03:41:33.000000000 +0100 +++ drupal-7.66/modules/update/update.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,39 +1,45 @@ TRUE))); + protected function refreshUpdateStatus($xml_map, $url = 'update-test') { + // Tell the Update Manager module to fetch from the URL provided by + // update_test module. + variable_set('update_fetch_url', url($url, array('absolute' => TRUE))); // Save the map for update_test_mock_page() to use. variable_set('update_test_xml_map', $xml_map); // Manually check the update status. @@ -41,22 +47,25 @@ } /** - * Run a series of assertions that are applicable for all update statuses. + * Runs a series of assertions that are applicable to all update statuses. */ protected function standardTests() { $this->assertRaw('

        ' . t('Drupal core') . '

        '); - $this->assertRaw(l(t('Drupal'), 'http://example.com/project/drupal'), t('Link to the Drupal project appears.')); + $this->assertRaw(l(t('Drupal'), 'http://example.com/project/drupal'), 'Link to the Drupal project appears.'); $this->assertNoText(t('No available releases found')); } } +/** + * Tests behavior related to discovering and listing updates to Drupal core. + */ class UpdateCoreTestCase extends UpdateTestHelper { public static function getInfo() { return array( 'name' => 'Update core functionality', - 'description' => 'Tests the update module through a series of functional tests using mock XML data.', + 'description' => 'Tests the Update Manager module through a series of functional tests using mock XML data.', 'group' => 'Update', ); } @@ -68,7 +77,7 @@ } /** - * Tests the update module when no updates are available. + * Tests the Update Manager module when no updates are available. */ function testNoUpdatesAvailable() { $this->setSystemInfo7_0(); @@ -80,7 +89,7 @@ } /** - * Tests the update module when one normal update ("7.1") is available. + * Tests the Update Manager module when one normal update is available. */ function testNormalUpdateAvailable() { $this->setSystemInfo7_0(); @@ -89,13 +98,13 @@ $this->assertNoText(t('Up to date')); $this->assertText(t('Update available')); $this->assertNoText(t('Security update required!')); - $this->assertRaw(l('7.1', 'http://example.com/drupal-7-1-release'), t('Link to release appears.')); - $this->assertRaw(l(t('Download'), 'http://example.com/drupal-7-1.tar.gz'), t('Link to download appears.')); - $this->assertRaw(l(t('Release notes'), 'http://example.com/drupal-7-1-release'), t('Link to release notes appears.')); + $this->assertRaw(l('7.1', 'http://example.com/drupal-7-1-release'), 'Link to release appears.'); + $this->assertRaw(l(t('Download'), 'http://example.com/drupal-7-1.tar.gz'), 'Link to download appears.'); + $this->assertRaw(l(t('Release notes'), 'http://example.com/drupal-7-1-release'), 'Link to release notes appears.'); } /** - * Tests the update module when a security update ("7.2") is available. + * Tests the Update Manager module when a security update is available. */ function testSecurityUpdateAvailable() { $this->setSystemInfo7_0(); @@ -104,13 +113,13 @@ $this->assertNoText(t('Up to date')); $this->assertNoText(t('Update available')); $this->assertText(t('Security update required!')); - $this->assertRaw(l('7.2', 'http://example.com/drupal-7-2-release'), t('Link to release appears.')); - $this->assertRaw(l(t('Download'), 'http://example.com/drupal-7-2.tar.gz'), t('Link to download appears.')); - $this->assertRaw(l(t('Release notes'), 'http://example.com/drupal-7-2-release'), t('Link to release notes appears.')); + $this->assertRaw(l('7.2', 'http://example.com/drupal-7-2-release'), 'Link to release appears.'); + $this->assertRaw(l(t('Download'), 'http://example.com/drupal-7-2.tar.gz'), 'Link to download appears.'); + $this->assertRaw(l(t('Release notes'), 'http://example.com/drupal-7-2-release'), 'Link to release notes appears.'); } /** - * Ensure proper results where there are date mismatches among modules. + * Ensures proper results where there are date mismatches among modules. */ function testDatestampMismatch() { $system_info = array( @@ -133,7 +142,20 @@ } /** - * Check the messages at admin/modules when the site is up to date. + * Checks that running cron updates the list of available updates. + */ + function testModulePageRunCron() { + $this->setSystemInfo7_0(); + variable_set('update_fetch_url', url('update-test', array('absolute' => TRUE))); + variable_set('update_test_xml_map', array('drupal' => '0')); + + $this->cronRun(); + $this->drupalGet('admin/modules'); + $this->assertNoText(t('No update information available.')); + } + + /** + * Checks the messages at admin/modules when the site is up to date. */ function testModulePageUpToDate() { $this->setSystemInfo7_0(); @@ -141,16 +163,16 @@ variable_set('update_fetch_url', url('update-test', array('absolute' => TRUE))); variable_set('update_test_xml_map', array('drupal' => '0')); - $this->drupalGet('admin/modules'); - $this->assertText(t('No update information available.')); - $this->clickLink(t('check manually')); + $this->drupalGet('admin/reports/updates'); + $this->clickLink(t('Check manually')); $this->assertText(t('Checked available update data for one project.')); + $this->drupalGet('admin/modules'); $this->assertNoText(t('There are updates available for your version of Drupal.')); $this->assertNoText(t('There is a security update available for your version of Drupal.')); } /** - * Check the messages at admin/modules when missing an update. + * Checks the messages at admin/modules when an update is missing. */ function testModulePageRegularUpdate() { $this->setSystemInfo7_0(); @@ -158,16 +180,16 @@ variable_set('update_fetch_url', url('update-test', array('absolute' => TRUE))); variable_set('update_test_xml_map', array('drupal' => '1')); - $this->drupalGet('admin/modules'); - $this->assertText(t('No update information available.')); - $this->clickLink(t('check manually')); + $this->drupalGet('admin/reports/updates'); + $this->clickLink(t('Check manually')); $this->assertText(t('Checked available update data for one project.')); + $this->drupalGet('admin/modules'); $this->assertText(t('There are updates available for your version of Drupal.')); $this->assertNoText(t('There is a security update available for your version of Drupal.')); } /** - * Check the messages at admin/modules when missing a security update. + * Checks the messages at admin/modules when a security update is missing. */ function testModulePageSecurityUpdate() { $this->setSystemInfo7_0(); @@ -175,10 +197,10 @@ variable_set('update_fetch_url', url('update-test', array('absolute' => TRUE))); variable_set('update_test_xml_map', array('drupal' => '2-sec')); - $this->drupalGet('admin/modules'); - $this->assertText(t('No update information available.')); - $this->clickLink(t('check manually')); + $this->drupalGet('admin/reports/updates'); + $this->clickLink(t('Check manually')); $this->assertText(t('Checked available update data for one project.')); + $this->drupalGet('admin/modules'); $this->assertNoText(t('There are updates available for your version of Drupal.')); $this->assertText(t('There is a security update available for your version of Drupal.')); @@ -201,6 +223,46 @@ $this->assertNoText(t('There is a security update available for your version of Drupal.')); } + /** + * Tests the Update Manager module when the update server returns 503 errors. + */ + function testServiceUnavailable() { + $this->refreshUpdateStatus(array(), '503-error'); + // Ensure that no "Warning: SimpleXMLElement..." parse errors are found. + $this->assertNoText('SimpleXMLElement'); + $this->assertUniqueText(t('Failed to get available update data for one project.')); + } + + /** + * Tests that exactly one fetch task per project is created and not more. + */ + function testFetchTasks() { + $projecta = array( + 'name' => 'aaa_update_test', + ); + $projectb = array( + 'name' => 'bbb_update_test', + ); + $queue = DrupalQueue::get('update_fetch_tasks'); + $this->assertEqual($queue->numberOfItems(), 0, 'Queue is empty'); + update_create_fetch_task($projecta); + $this->assertEqual($queue->numberOfItems(), 1, 'Queue contains one item'); + update_create_fetch_task($projectb); + $this->assertEqual($queue->numberOfItems(), 2, 'Queue contains two items'); + // Try to add project a again. + update_create_fetch_task($projecta); + $this->assertEqual($queue->numberOfItems(), 2, 'Queue still contains two items'); + + // Clear cache and try again. + _update_cache_clear(); + drupal_static_reset('_update_create_fetch_task'); + update_create_fetch_task($projecta); + $this->assertEqual($queue->numberOfItems(), 2, 'Queue contains two items'); + } + + /** + * Sets the version to 7.0 when no project-specific mapping is defined. + */ protected function setSystemInfo7_0() { $setting = array( '#all' => array( @@ -212,12 +274,15 @@ } +/** + * Tests behavior related to handling updates to contributed modules and themes. + */ class UpdateTestContribCase extends UpdateTestHelper { public static function getInfo() { return array( 'name' => 'Update contrib functionality', - 'description' => 'Tests how the update module handles contributed modules and themes in a series of functional tests using mock XML data.', + 'description' => 'Tests how the Update Manager module handles contributed modules and themes in a series of functional tests using mock XML data.', 'group' => 'Update', ); } @@ -257,7 +322,7 @@ } /** - * Test the basic functionality of a contrib module on the status report. + * Tests the basic functionality of a contrib module on the status report. */ function testUpdateContribBasic() { $system_info = array( @@ -281,21 +346,21 @@ $this->assertText(t('Up to date')); $this->assertRaw('

        ' . t('Modules') . '

        '); $this->assertNoText(t('Update available')); - $this->assertRaw(l(t('AAA Update test'), 'http://example.com/project/aaa_update_test'), t('Link to aaa_update_test project appears.')); + $this->assertRaw(l(t('AAA Update test'), 'http://example.com/project/aaa_update_test'), 'Link to aaa_update_test project appears.'); } /** - * Test that contrib projects are ordered by project name. + * Tests that contrib projects are ordered by project name. * * If a project contains multiple modules, we want to make sure that the - * available updates report is sorted by the parent project names, not by - * the names of the modules included in each project. In this test case, we - * have 2 contrib projects, "BBB Update test" and "CCC Update test". - * However, we have a module called "aaa_update_test" that's part of the - * "CCC Update test" project. We need to make sure that we see the "BBB" - * project before the "CCC" project, even though "CCC" includes a module - * that's processed first if you sort alphabetically by module name (which - * is the order we see things inside system_rebuild_module_data() for example). + * available updates report is sorted by the parent project names, not by the + * names of the modules included in each project. In this test case, we have + * two contrib projects, "BBB Update test" and "CCC Update test". However, we + * have a module called "aaa_update_test" that's part of the "CCC Update test" + * project. We need to make sure that we see the "BBB" project before the + * "CCC" project, even though "CCC" includes a module that's processed first + * if you sort alphabetically by module name (which is the order we see things + * inside system_rebuild_module_data() for example). */ function testUpdateContribOrder() { // We want core to be version 7.0. @@ -342,10 +407,10 @@ $this->assertText(t('CCC Update test')); // We want aaa_update_test included in the ccc_update_test project, not as // its own project on the report. - $this->assertNoRaw(l(t('AAA Update test'), 'http://example.com/project/aaa_update_test'), t('Link to aaa_update_test project does not appear.')); + $this->assertNoRaw(l(t('AAA Update test'), 'http://example.com/project/aaa_update_test'), 'Link to aaa_update_test project does not appear.'); // The other two should be listed as projects. - $this->assertRaw(l(t('BBB Update test'), 'http://example.com/project/bbb_update_test'), t('Link to bbb_update_test project appears.')); - $this->assertRaw(l(t('CCC Update test'), 'http://example.com/project/ccc_update_test'), t('Link to bbb_update_test project appears.')); + $this->assertRaw(l(t('BBB Update test'), 'http://example.com/project/bbb_update_test'), 'Link to bbb_update_test project appears.'); + $this->assertRaw(l(t('CCC Update test'), 'http://example.com/project/ccc_update_test'), 'Link to bbb_update_test project appears.'); // We want to make sure we see the BBB project before the CCC project. // Instead of just searching for 'BBB Update test' or something, we want @@ -357,7 +422,7 @@ } /** - * Test that subthemes are notified about security updates for base themes. + * Tests that subthemes are notified about security updates for base themes. */ function testUpdateBaseThemeSecurityUpdate() { // Only enable the subtheme, not the base theme. @@ -394,11 +459,60 @@ ); $this->refreshUpdateStatus($xml_mapping); $this->assertText(t('Security update required!')); - $this->assertRaw(l(t('Update test base theme'), 'http://example.com/project/update_test_basetheme'), t('Link to the Update test base theme project appears.')); + $this->assertRaw(l(t('Update test base theme'), 'http://example.com/project/update_test_basetheme'), 'Link to the Update test base theme project appears.'); } /** - * Test that disabled themes are only shown when desired. + * 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() { // Make sure all the update_test_* themes are disabled. @@ -427,6 +541,11 @@ 'hidden' => FALSE, ), ); + // When there are contributed modules in the site's file system, the + // total number of attempts made in the test may exceed the default value + // of update_max_fetch_attempts. Therefore this variable is set very high + // to avoid test failures in those cases. + variable_set('update_max_fetch_attempts', 99999); variable_set('update_test_system_info', $system_info); $xml_mapping = array( 'drupal' => '0', @@ -442,19 +561,19 @@ $this->assertNoText(t('Themes')); if ($check_disabled) { $this->assertText(t('Disabled themes')); - $this->assertRaw($base_theme_project_link, t('Link to the Update test base theme project appears.')); - $this->assertRaw($sub_theme_project_link, t('Link to the Update test subtheme project appears.')); + $this->assertRaw($base_theme_project_link, 'Link to the Update test base theme project appears.'); + $this->assertRaw($sub_theme_project_link, 'Link to the Update test subtheme project appears.'); } else { $this->assertNoText(t('Disabled themes')); - $this->assertNoRaw($base_theme_project_link, t('Link to the Update test base theme project does not appear.')); - $this->assertNoRaw($sub_theme_project_link, t('Link to the Update test subtheme project does not appear.')); + $this->assertNoRaw($base_theme_project_link, 'Link to the Update test base theme project does not appear.'); + $this->assertNoRaw($sub_theme_project_link, 'Link to the Update test subtheme project does not appear.'); } } } /** - * Make sure that if we fetch from a broken URL, sane things happen. + * Makes sure that if we fetch from a broken URL, sane things happen. */ function testUpdateBrokenFetchURL() { $system_info = array( @@ -504,19 +623,18 @@ $this->assertUniqueText(t('Failed to get available update data for one project.')); // The other two should be listed as projects. - $this->assertRaw(l(t('AAA Update test'), 'http://example.com/project/aaa_update_test'), t('Link to aaa_update_test project appears.')); - $this->assertNoRaw(l(t('BBB Update test'), 'http://example.com/project/bbb_update_test'), t('Link to bbb_update_test project does not appear.')); - $this->assertRaw(l(t('CCC Update test'), 'http://example.com/project/ccc_update_test'), t('Link to bbb_update_test project appears.')); + $this->assertRaw(l(t('AAA Update test'), 'http://example.com/project/aaa_update_test'), 'Link to aaa_update_test project appears.'); + $this->assertNoRaw(l(t('BBB Update test'), 'http://example.com/project/bbb_update_test'), 'Link to bbb_update_test project does not appear.'); + $this->assertRaw(l(t('CCC Update test'), 'http://example.com/project/ccc_update_test'), 'Link to bbb_update_test project appears.'); } /** - * Check that hook_update_status_alter() works to change a status. + * Checks that hook_update_status_alter() works to change a status. * * We provide the same external data as if aaa_update_test 7.x-1.0 were * installed and that was the latest release. Then we use * hook_update_status_alter() to try to mark this as missing a security - * update, then assert if we see the appropriate warnings on the right - * pages. + * update, then assert if we see the appropriate warnings on the right pages. */ function testHookUpdateStatusAlter() { variable_set('allow_authorize_operations', TRUE); @@ -549,7 +667,7 @@ $this->drupalGet('admin/reports/updates'); $this->assertRaw('

        ' . t('Modules') . '

        '); $this->assertText(t('Security update required!')); - $this->assertRaw(l(t('AAA Update test'), 'http://example.com/project/aaa_update_test'), t('Link to aaa_update_test project appears.')); + $this->assertRaw(l(t('AAA Update test'), 'http://example.com/project/aaa_update_test'), 'Link to aaa_update_test project appears.'); // Visit the reports page again without the altering and make sure the // status is back to normal. @@ -557,7 +675,7 @@ $this->drupalGet('admin/reports/updates'); $this->assertRaw('

        ' . t('Modules') . '

        '); $this->assertNoText(t('Security update required!')); - $this->assertRaw(l(t('AAA Update test'), 'http://example.com/project/aaa_update_test'), t('Link to aaa_update_test project appears.')); + $this->assertRaw(l(t('AAA Update test'), 'http://example.com/project/aaa_update_test'), 'Link to aaa_update_test project appears.'); // Turn the altering back on and visit the Update manager UI. variable_set('update_test_update_status', $update_status); @@ -572,11 +690,15 @@ } +/** + * Tests project upload and extract functionality. + */ class UpdateTestUploadCase extends UpdateTestHelper { + public static function getInfo() { return array( 'name' => 'Upload and extract module functionality', - 'description' => 'Tests the update module\'s upload and extraction functionality.', + 'description' => 'Tests the Update Manager module\'s upload and extraction functionality.', 'group' => 'Update', ); } @@ -617,18 +739,18 @@ } /** - * Ensure that archiver extensions are properly merged in the UI. + * Ensures that archiver extensions are properly merged in the UI. */ function testFileNameExtensionMerging() { - $this->drupalGet('admin/modules/install'); + $this->drupalGet('admin/modules/install'); // Make sure the bogus extension supported by update_test.module is there. - $this->assertPattern('/file extensions are supported:.*update-test-extension/', t("Found 'update-test-extension' extension")); + $this->assertPattern('/file extensions are supported:.*update-test-extension/', "Found 'update-test-extension' extension"); // Make sure it didn't clobber the first option from core. - $this->assertPattern('/file extensions are supported:.*tar/', t("Found 'tar' extension")); + $this->assertPattern('/file extensions are supported:.*tar/', "Found 'tar' extension"); } /** - * Check the messages on Update manager pages when missing a security update. + * Checks the messages on update manager pages when missing a security update. */ function testUpdateManagerCoreSecurityUpdateMessages() { $setting = array( @@ -668,3 +790,63 @@ } } + +/** + * Tests update functionality unrelated to the database. + */ +class UpdateCoreUnitTestCase extends DrupalUnitTestCase { + + public static function getInfo() { + return array( + 'name' => "Unit tests", + 'description' => 'Test update funcionality unrelated to the database.', + 'group' => 'Update', + ); + } + + function setUp() { + parent::setUp('update'); + module_load_include('inc', 'update', 'update.fetch'); + } + + /** + * Tests that _update_build_fetch_url() builds the URL correctly. + */ + function testUpdateBuildFetchUrl() { + //first test that we didn't break the trivial case + $project['name'] = 'update_test'; + $project['project_type'] = ''; + $project['info']['version'] = ''; + $project['info']['project status url'] = 'http://www.example.com'; + $project['includes'] = array('module1' => 'Module 1', 'module2' => 'Module 2'); + $site_key = ''; + $expected = 'http://www.example.com/' . $project['name'] . '/' . DRUPAL_CORE_COMPATIBILITY; + $url = _update_build_fetch_url($project, $site_key); + $this->assertEqual($url, $expected, "'$url' when no site_key provided should be '$expected'."); + + //For disabled projects it shouldn't add the site key either. + $site_key = 'site_key'; + $project['project_type'] = 'disabled'; + $expected = 'http://www.example.com/' . $project['name'] . '/' . DRUPAL_CORE_COMPATIBILITY; + $url = _update_build_fetch_url($project, $site_key); + $this->assertEqual($url, $expected, "'$url' should be '$expected' for disabled projects."); + + //for enabled projects, adding the site key + $project['project_type'] = ''; + $expected = 'http://www.example.com/' . $project['name'] . '/' . DRUPAL_CORE_COMPATIBILITY; + $expected .= '?site_key=site_key'; + $expected .= '&list=' . rawurlencode('module1,module2'); + $url = _update_build_fetch_url($project, $site_key); + $this->assertEqual($url, $expected, "When site_key provided, '$url' should be '$expected'."); + + // http://drupal.org/node/1481156 test incorrect logic when URL contains + // a question mark. + $project['info']['project status url'] = 'http://www.example.com/?project='; + $expected = 'http://www.example.com/?project=/' . $project['name'] . '/' . DRUPAL_CORE_COMPATIBILITY; + $expected .= '&site_key=site_key'; + $expected .= '&list=' . rawurlencode('module1,module2'); + $url = _update_build_fetch_url($project, $site_key); + $this->assertEqual($url, $expected, "When ? is present, '$url' should be '$expected'."); + + } +} diff -Naur drupal-7.0/modules/user/tests/user_form_test.info drupal-7.66/modules/user/tests/user_form_test.info --- drupal-7.0/modules/user/tests/user_form_test.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/user/tests/user_form_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: user_form_test.info,v 1.2 2010/12/21 16:31:47 webchick Exp $ name = "User module form tests" description = "Support module for user form testing." package = Testing @@ -6,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/user/tests/user_form_test.module drupal-7.66/modules/user/tests/user_form_test.module --- drupal-7.0/modules/user/tests/user_form_test.module 2010-12-11 07:22:33.000000000 +0100 +++ drupal-7.66/modules/user/tests/user_form_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ -
        +
        diff -Naur drupal-7.0/modules/user/user-profile-category.tpl.php drupal-7.66/modules/user/user-profile-category.tpl.php --- drupal-7.0/modules/user/user-profile-category.tpl.php 2008-10-13 14:31:43.000000000 +0200 +++ drupal-7.66/modules/user/user-profile-category.tpl.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ - +

        diff -Naur drupal-7.0/modules/user/user-profile-item.tpl.php drupal-7.66/modules/user/user-profile-item.tpl.php --- drupal-7.0/modules/user/user-profile-item.tpl.php 2008-10-13 14:31:43.000000000 +0200 +++ drupal-7.66/modules/user/user-profile-item.tpl.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ field_example has a + * corresponding variable is defined; e.g., $account->field_example has a * variable $field_example defined. When needing to access a field's raw * values, developers/themers are strongly encouraged to use these * variables. Otherwise they will have to explicitly specify the desired - * field language, e.g. $user->field_example['en'], thus overriding any + * field language, e.g. $account->field_example['en'], thus overriding any * language negotiation rule that was previously applied. * * @see user-profile-category.tpl.php @@ -31,6 +30,8 @@ * @see user-profile-item.tpl.php * Where the html is handled for each item in the group. * @see template_preprocess_user_profile() + * + * @ingroup themeable */ ?>
        > diff -Naur drupal-7.0/modules/user/user-rtl.css drupal-7.66/modules/user/user-rtl.css --- drupal-7.0/modules/user/user-rtl.css 2010-09-19 20:10:42.000000000 +0200 +++ drupal-7.66/modules/user/user-rtl.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,26 +1,30 @@ -/* $Id: user-rtl.css,v 1.9 2010/09/19 18:10:42 dries Exp $ */ #permissions td.permission { padding-left: 0; padding-right: 1.5em; } +#user-admin-roles .form-item-name { + float: right; + margin-left: 1em; + margin-right: 0; +} + /** * Password strength indicator. */ -input.password-field { - margin-left: 10px; - margin-right: 0; +.password-strength { + float: left; } -input.password-confirm { - margin-left: 10px; - margin-right: 0; +.password-strength-text { + float: left; } -.password-strength-title { - float: right; +div.password-confirm { + float: left; } +.confirm-parent, .password-parent { - float: right; + clear: right; } /* Generated by user.module but used by profile.module: */ diff -Naur drupal-7.0/modules/user/user.admin.inc drupal-7.66/modules/user/user.admin.inc --- drupal-7.0/modules/user/user.admin.inc 2010-12-15 03:59:01.000000000 +0100 +++ drupal-7.66/modules/user/user.admin.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,11 +1,25 @@ 'select', '#title' => t('Picture display style'), - '#options' => image_style_options(TRUE), + '#options' => image_style_options(TRUE, PASS_THROUGH), '#default_value' => variable_get('user_picture_style', ''), '#description' => t('The style selected will be used on display, while the original image is retained. Styles may be configured in the Image styles administration area.', array('!url' => url('admin/config/media/image-styles'))), ); @@ -414,6 +428,7 @@ '#maxlength' => 10, '#field_suffix' => ' ' . t('KB'), '#description' => t('Maximum allowed file size for uploaded pictures. Upload size is normally limited only by the PHP maximum post and file upload settings, and images are automatically scaled down to the dimensions specified above.'), + '#element_validate' => array('element_validate_integer_positive'), ); $form['personalization']['pictures']['user_picture_guidelines'] = array( '#type' => 'textarea', @@ -713,7 +728,12 @@ // Have to build checkboxes here after checkbox arrays are built foreach ($role_names as $rid => $name) { - $form['checkboxes'][$rid] = array('#type' => 'checkboxes', '#options' => $options, '#default_value' => isset($status[$rid]) ? $status[$rid] : array()); + $form['checkboxes'][$rid] = array( + '#type' => 'checkboxes', + '#options' => $options, + '#default_value' => isset($status[$rid]) ? $status[$rid] : array(), + '#attributes' => array('class' => array('rid-' . $rid)), + ); $form['role_names'][$rid] = array('#markup' => check_plain($name), '#tree' => TRUE); } @@ -837,7 +857,7 @@ $form['roles'][$rid]['#weight'] = $order; $form['roles'][$rid]['weight'] = array( '#type' => 'textfield', - '#title' => t('Weight for @title', array('@title' => $name['label'])), + '#title' => t('Weight for @title', array('@title' => $name)), '#title_display' => 'invisible', '#size' => 4, '#default_value' => $order, diff -Naur drupal-7.0/modules/user/user.api.php drupal-7.66/modules/user/user.api.php --- drupal-7.0/modules/user/user.api.php 2010-12-11 20:16:42.000000000 +0100 +++ drupal-7.66/modules/user/user.api.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ array_keys($users))); + $result = db_query('SELECT uid, foo FROM {my_table} WHERE uid IN (:uids)', array(':uids' => array_keys($users))); foreach ($result as $record) { - $users[$record->uid]->foo = $result->foo; + $users[$record->uid]->foo = $record->foo; } } /** * Respond to user deletion. * - * This hook is invoked from user_delete_multiple() after the account has been - * removed from the user tables in the database, and before - * field_attach_delete() is called. + * This hook is invoked from user_delete_multiple() before field_attach_delete() + * is called and before users are actually removed from the database. * * Modules should additionally implement hook_user_cancel() to process stored * user data for other account cancellation methods. @@ -125,10 +123,10 @@ * 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 + * @param $methods * An array containing user account cancellation methods, keyed by method id. * * @see user_cancel_methods() @@ -185,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: @@ -215,10 +229,12 @@ * user account object is loaded, modules may add to $edit['data'] in order * to have their data serialized on save. * - * @param &$edit - * The array of form values submitted by the user. + * @param $edit + * The array of form values submitted by the user. Assign values to this + * array to save changes in the database. * @param $account - * The user object on which the operation is performed. + * The user object on which the operation is performed. Values assigned in + * this object will not be saved in the database. * @param $category * The active category of user information being edited. * @@ -226,9 +242,10 @@ * @see hook_user_update() */ function hook_user_presave(&$edit, $account, $category) { - // Make sure that our form value 'mymodule_foo' is stored as 'mymodule_bar'. + // Make sure that our form value 'mymodule_foo' is stored as + // 'mymodule_bar' in the 'data' (serialized) column. if (isset($edit['mymodule_foo'])) { - $edit['data']['my_module_foo'] = $edit['my_module_foo']; + $edit['data']['mymodule_bar'] = $edit['mymodule_foo']; } } @@ -238,7 +255,7 @@ * The module should save its custom additions to the user object into the * database. * - * @param &$edit + * @param $edit * The array of form values submitted by the user. * @param $account * The user object on which the operation is being performed. @@ -263,7 +280,7 @@ * Modules may use this hook to update their user data in a custom storage * after a user account has been updated. * - * @param &$edit + * @param $edit * The array of form values submitted by the user. * @param $account * The user object on which the operation is performed. @@ -285,7 +302,7 @@ /** * The user just logged in. * - * @param &$edit + * @param $edit * The array of form values submitted by the user. * @param $account * The user object on which the operation was just performed. @@ -300,6 +317,14 @@ /** * The user just logged out. * + * 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 was just performed. */ @@ -369,7 +394,26 @@ } /** - * Inform other modules that a user role has been added. + * 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. + * + * @param $role + * A user role object. + * + * @see hook_user_role_insert() + * @see hook_user_role_update() + */ +function hook_user_role_presave($role) { + // Set a UUID for the user role if it doesn't already exist + if (empty($role->uuid)) { + $role->uuid = uuid_uuid(); + } +} + +/** + * 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 @@ -390,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 @@ -411,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.0/modules/user/user.css drupal-7.66/modules/user/user.css --- drupal-7.0/modules/user/user.css 2010-09-19 20:10:42.000000000 +0200 +++ drupal-7.66/modules/user/user.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -/* $Id: user.css,v 1.23 2010/09/19 18:10:42 dries Exp $ */ #permissions td.module { font-weight: bold; @@ -23,8 +22,8 @@ clear: both; } #user-admin-roles .form-item-name { - float: left; - margin-right: 1em; + float: left; /* LTR */ + margin-right: 1em; /* LTR */ } /** @@ -58,8 +57,10 @@ margin-bottom: 0.4em; } div.password-confirm { - display: inline; - padding-left: 1em; + float: right; /* LTR */ + margin-top: 1.5em; + visibility: hidden; + width: 17em; } div.form-item div.password-suggestions { padding: 0.2em 0.5em; @@ -70,19 +71,11 @@ div.password-suggestions ul { margin-bottom: 0; } +.confirm-parent, .password-parent { + clear: left; /* LTR */ margin: 0; - width: 34.3em; -} - -/** - * Password confirmation checker. - */ -.confirm-parent { - margin: 0; -} -div.password-confirm { - visibility: hidden; + width: 36.3em; } /* Generated by user.module but used by profile.module: */ diff -Naur drupal-7.0/modules/user/user.info drupal-7.66/modules/user/user.info --- drupal-7.0/modules/user/user.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/modules/user/user.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: user.info,v 1.15 2010/12/20 19:59:44 webchick Exp $ name = User description = Manages the user registration and login system. package = Core @@ -10,8 +9,7 @@ configure = admin/config/people stylesheets[all][] = user.css -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/modules/user/user.install drupal-7.66/modules/user/user.install --- drupal-7.0/modules/user/user.install 2011-01-02 18:26:40.000000000 +0100 +++ drupal-7.66/modules/user/user.install 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ array('uid' => 'uid'), ), ), + 'indexes' => array( + 'uid_module' => array('uid', 'module'), + ), ); $schema['role_permission'] = array( @@ -82,7 +84,7 @@ ), 'foreign keys' => array( 'role' => array( - 'table' => 'roles', + 'table' => 'role', 'columns' => array('rid' => 'rid'), ), ), @@ -121,6 +123,8 @@ ), ); + // The table name here is plural, despite Drupal table naming standards, + // because "user" is a reserved word in many databases. $schema['users'] = array( 'description' => 'Stores user data.', 'fields' => array( @@ -235,6 +239,7 @@ 'access' => array('access'), 'created' => array('created'), 'mail' => array('mail'), + 'picture' => array('picture'), ), 'unique keys' => array( 'name' => array('name'), @@ -276,7 +281,7 @@ 'columns' => array('uid' => 'uid'), ), 'role' => array( - 'table' => 'roles', + 'table' => 'role', 'columns' => array('rid' => 'rid'), ), ), @@ -341,25 +346,34 @@ * Implements hook_update_dependencies(). */ function user_update_dependencies() { - // Run all the critical user upgrades before everything. - $dependencies['system'][7000] = array( - 'user' => 7008, - ); - // Both user_update_7006() and user_update_7010() need to query the list of - // existing text formats and therefore must run after filter_update_7003(). - // @todo: move user_update_7006 down below in the upgrade process. + // user_update_7006() updates data in the {role_permission} table, so it must + // run after system_update_7007(), which populates that table. $dependencies['user'][7006] = array( - 'filter' => 7003, + 'system' => 7007, + ); + + // user_update_7010() needs to query the {filter_format} table to get a list + // of existing text formats, so it must run after filter_update_7000(), which + // creates that table. + $dependencies['user'][7010] = array( + 'filter' => 7000, + ); + + // 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' => 7061, ); - // user_update_7013 relies on system_update_7060. + + // user_update_7013() uses the file usage API, which relies on the + // {file_usage} table, so it must run after system_update_7059(), which + // creates that table. $dependencies['user'][7013] = array( 'system' => 7059, ); - // Ensure that format columns are only changed after Filter module has changed - // the primary records. - $dependencies['user'][7015] = array( - 'filter' => 7010, - ); return $dependencies; } @@ -375,7 +389,7 @@ * An array of permissions names. * @param $module * The name of the module defining the permissions. - * @ingroup update-api-6.x-to-7.x + * @ingroup update_api */ function _update_7000_user_role_grant_permissions($rid, array $permissions, $module) { // Grant new permissions for the role. @@ -422,6 +436,13 @@ $result = db_query_range("SELECT uid, pass FROM {users} WHERE uid > 0 ORDER BY uid", $sandbox['user_from'], $count); foreach ($result as $account) { $has_rows = TRUE; + + // If the $account->pass value is not a MD5 hash (a 32 character + // hexadecimal string) then skip it. + if (!preg_match('/^[0-9a-f]{32}$/', $account->pass)) { + continue; + } + $new_hash = user_hash_password($account->pass, $hash_count_log2); if ($new_hash) { // Indicate an updated password. @@ -519,7 +540,7 @@ if ($sandbox['user_from'] == $sandbox['user_count']) { if ($sandbox['user_not_migrated'] > 0) { variable_set('empty_timezone_message', 1); - drupal_set_message('Some user time zones have been emptied and need to be set to the correct values. Use the new ' . l('time zone options', 'admin/config/regional/settings') . ' to choose whether to remind users at login to set the correct time zone.', 'warning'); + drupal_set_message(format_string('Some user time zones have been emptied and need to be set to the correct values. Use the new time zone options to choose whether to remind users at login to set the correct time zone.', array('@config-url' => url('admin/config/regional/settings'))), 'warning'); } return t('Migrated user time zones'); } @@ -592,13 +613,6 @@ // Add a new field for the fid. db_add_field('role_permission', 'module', $module_field); } - $permissions = user_permission_get_modules(); - foreach ($permissions as $key => $value) { - db_update('role_permission') - ->fields(array('module' => $value)) - ->condition('permission', $key) - ->execute(); - } } /** @@ -684,46 +698,13 @@ } /** - * Updates email templates to use new tokens. + * Placeholder function. * - * This function upgrades customized email templates from the old !token format - * to the new core tokens format. Additionally, in Drupal 7 we no longer e-mail - * plain text passwords to users, and there is no token for a plain text - * password in the new token system. Therefore, it also modifies any saved - * templates using the old '!password' token such that the token is removed, and - * displays a warning to users that they may need to go and modify the wording - * of their templates. + * As a fix for user_update_7011() not updating email templates to use the new + * tokens, user_update_7017() now targets email templates of Drupal 6 sites and + * already upgraded sites. */ function user_update_7011() { - $message = ''; - - $tokens = array( - '!site-name-token' => '[site:name]', - '!site-url-token' => '[site:url]', - '!user-name-token' => '[user:name]', - '!user-mail-token' => '[user:mail]', - '!site-login-url-token' => '[site:login-url]', - '!site-url-brief-token' => '[site:url-brief]', - '!user-edit-url-token' => '[user:edit-url]', - '!user-one-time-login-url-token' => '[user:one-time-login-url]', - '!user-cancel-url-token' => '[user:cancel-url]', - '!password' => '', - ); - - $result = db_select('variable', 'v') - ->fields('v', array('name', 'value')) - ->condition('value', db_like('user_mail_') . '%', 'LIKE') - ->execute(); - - foreach ($result as $row) { - if (empty($message) && (strpos($row->value, '!password') !== FALSE)) { - $message = t('The ability to send users their passwords in plain text has been removed in Drupal 7. Your existing email templates have been modified to remove it. You should review these templates to make sure they read properly.', array('@template-url' => url('admin/config/people/accounts'))); - } - - variable_set($row->name, str_replace(array_keys($tokens), $tokens, $row->value)); - } - - return $message; } /** @@ -770,7 +751,7 @@ // Create a file object. $file = new stdClass(); $file->uri = $user->picture; - $file->filename = basename($file->uri); + $file->filename = drupal_basename($file->uri); $file->filemime = file_get_mimetype($file->uri); $file->uid = $user->uid; $file->status = FILE_STATUS_PERMANENT; @@ -855,5 +836,92 @@ } /** - * @} End of "addtogroup updates-6.x-to-7.x" + * @} End of "addtogroup updates-6.x-to-7.x". + */ + +/** + * @addtogroup updates-7.x-extra + * @{ + */ + +/** + * Update the database to match the schema. + */ +function user_update_7016() { + // Add field default. + db_change_field('users', 'uid', 'uid', array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + )); +} + +/** + * Update email templates to use new tokens. + * + * This function upgrades customized email templates from the old !token format + * to the new core tokens format. Additionally, in Drupal 7 we no longer e-mail + * plain text passwords to users, and there is no token for a plain text + * password in the new token system. Therefore, it also modifies any saved + * templates using the old '!password' token such that the token is removed, and + * displays a warning to users that they may need to go and modify the wording + * of their templates. + */ +function user_update_7017() { + $message = ''; + + $tokens = array( + '!site' => '[site:name]', + '!username' => '[user:name]', + '!mailto' => '[user:mail]', + '!login_uri' => '[site:login-url]', + '!uri_brief' => '[site:url-brief]', + '!edit_uri' => '[user:edit-url]', + '!login_url' => '[user:one-time-login-url]', + '!uri' => '[site:url]', + '!date' => '[date:medium]', + '!password' => '', + ); + + $result = db_select('variable', 'v') + ->fields('v', array('name')) + ->condition('name', db_like('user_mail_') . '%', 'LIKE') + ->execute(); + + foreach ($result as $row) { + // Use variable_get() to get the unserialized value for free. + if ($value = variable_get($row->name, FALSE)) { + + if (empty($message) && (strpos($value, '!password') !== FALSE)) { + $message = t('The ability to send users their passwords in plain text has been removed in Drupal 7. Your existing email templates have been modified to remove it. You should review these templates to make sure they read properly.', array('@template-url' => url('admin/config/people/accounts'))); + } + + variable_set($row->name, str_replace(array_keys($tokens), $tokens, $value)); + } + } + + return $message; +} + +/** + * Ensure there is an index on {users}.picture. + */ +function user_update_7018() { + if (!db_index_exists('users', 'picture')) { + db_add_index('users', 'picture', array('picture')); + } +} + +/** + * 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.0/modules/user/user.js drupal-7.66/modules/user/user.js --- drupal-7.0/modules/user/user.js 2010-12-01 00:55:11.000000000 +0100 +++ drupal-7.66/modules/user/user.js 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -// $Id: user.js,v 1.25 2010/11/30 23:55:11 webchick Exp $ (function ($) { /** @@ -17,7 +16,7 @@ innerWrapper.addClass('password-parent'); // Add the password confirmation layer. - $('input.password-confirm', outerWrapper).after('
        ' + translate['confirmTitle'] + '
        ').parent().addClass('confirm-parent'); + $('input.password-confirm', outerWrapper).parent().prepend('
        ' + translate['confirmTitle'] + '
        ').addClass('confirm-parent'); var confirmInput = $('input.password-confirm', outerWrapper); var confirmResult = $('div.password-confirm', outerWrapper); var confirmChild = $('span', confirmResult); @@ -94,12 +93,14 @@ * 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 = password.match(/[a-z]+/); - var hasUppercase = password.match(/[A-Z]+/); - var hasNumbers = password.match(/[0-9]+/); - var hasPunctuation = password.match(/[^a-zA-Z0-9]+/); + var hasLowercase = /[a-z]+/.test(password); + var hasUppercase = /[A-Z]+/.test(password); + var hasNumbers = /[0-9]+/.test(password); + var hasPunctuation = /[^a-zA-Z0-9]+/.test(password); // If there is a username edit box on the page, compare password to that, otherwise // use value from the database. @@ -169,7 +170,7 @@ // Assemble the final message. msg = translate.hasWeaknesses + '
        • ' + msg.join('
        • ') + '
        '; - return { strength: strength, message: msg, indicatorText: indicatorText } + return { strength: strength, message: msg, indicatorText: indicatorText }; }; @@ -181,7 +182,7 @@ attach: function (context, settings) { var $checkbox = $('form#field-ui-field-edit-form input#edit-instance-settings-user-register-form'); - if ($checkbox.size()) { + if ($checkbox.length) { $('input#edit-instance-required', context).once('user-register-form-checkbox', function () { $(this).bind('change', function (e) { if ($(this).attr('checked')) { diff -Naur drupal-7.0/modules/user/user.module drupal-7.66/modules/user/user.module --- drupal-7.0/modules/user/user.module 2010-12-28 22:46:23.000000000 +0100 +++ drupal-7.66/modules/user/user.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ ' . t('About') . ''; - $output .= '

        ' . t('The User module allows users to register, log in, and log out. It also allows users with proper permissions to manage user roles (used to classify users) and permissions associated with those roles. For more information, see the online handbook entry for User module.', array('@user' => 'http://drupal.org/handbook/modules/user')) . '

        '; + $output .= '

        ' . t('The User module allows users to register, log in, and log out. It also allows users with proper permissions to manage user roles (used to classify users) and permissions associated with those roles. For more information, see the online handbook entry for User module.', array('@user' => 'http://drupal.org/documentation/modules/user')) . '

        '; $output .= '

        ' . t('Uses') . '

        '; $output .= '
        '; $output .= '
        ' . t('Creating and managing users') . '
        '; @@ -75,10 +74,26 @@ } /** - * Invokes hook_user() in every module. + * Invokes a user hook in every module. * * We cannot use module_invoke() for this, because the arguments need to * be passed by reference. + * + * @param $type + * A text string that controls which user hook to invoke. Valid choices are: + * - cancel: Invokes hook_user_cancel(). + * - insert: Invokes hook_user_insert(). + * - login: Invokes hook_user_login(). + * - presave: Invokes hook_user_presave(). + * - update: Invokes hook_user_update(). + * @param $edit + * An associative array variable containing form values to be passed + * as the first parameter of the hook function. + * @param $account + * The user account object to be passed as the second parameter of the hook + * function. + * @param $category + * The category of user information being acted upon. */ function user_module_invoke($type, &$edit, $account, $category = NULL) { foreach (module_implements('user_' . $type) as $module) { @@ -144,6 +159,10 @@ 'uri callback' => 'user_uri', 'label callback' => 'format_username', 'fieldable' => TRUE, + // $user->language is only the preferred user language for the user + // interface textual elements. As it is not necessarily related to the + // language assigned to fields, we do not define it as the entity language + // key. 'entity keys' => array( 'id' => 'uid', ), @@ -168,7 +187,7 @@ } /** - * Entity uri callback. + * Implements callback_entity_info_uri(). */ function user_uri($user) { return array( @@ -196,19 +215,19 @@ $return['user']['user'] = array( 'form' => array( 'account' => array( - 'label' => 'User name and password', - 'description' => t('User module account form elements'), + 'label' => t('User name and password'), + 'description' => t('User module account form elements.'), 'weight' => -10, ), 'timezone' => array( - 'label' => 'Timezone', + 'label' => t('Timezone'), 'description' => t('User module timezone form element.'), 'weight' => 6, ), ), 'display' => array( 'summary' => array( - 'label' => 'History', + 'label' => t('History'), 'description' => t('User module history view element.'), 'weight' => 5, ), @@ -302,7 +321,7 @@ } // Add the full file objects for user pictures if enabled. - if (!empty($picture_fids) && variable_get('user_pictures', 1) == 1) { + if (!empty($picture_fids) && variable_get('user_pictures', 0)) { $pictures = file_load_multiple($picture_fids); foreach ($queried_users as $account) { if (!empty($account->picture) && isset($pictures[$account->picture])) { @@ -326,9 +345,10 @@ * user. So to avoid confusion and to avoid clobbering the global $user object, * it is a good idea to assign the result of this function to a different local * variable, generally $account. If you actually do want to act as the user you - * are loading, it is essential to call @code session_save_session(FALSE); - * @endcode first. See @link http://drupal.org/node/218104 Safely impersonating - * another user @endlink for more information. + * are loading, it is essential to call drupal_save_session(FALSE); first. + * See + * @link http://drupal.org/node/218104 Safely impersonating another user @endlink + * for more information. * * @param $uid * Integer specifying the user ID to load. @@ -398,15 +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 { - $table = drupal_get_schema('users'); - - 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'])); @@ -419,6 +435,9 @@ // Avoid overwriting an existing password with a blank password. unset($edit['pass']); } + if (isset($edit['mail'])) { + $edit['mail'] = trim($edit['mail']); + } // Load the stored entity, if any. if (!empty($account->uid) && !isset($account->original)) { @@ -441,27 +460,18 @@ user_module_invoke('presave', $edit, $account, $category); // Invoke presave operations of Field Attach API and Entity API. Those APIs - // require a fully-fledged (and updated) entity object, so $edit is not - // necessarily sufficient, as it technically contains submitted form values - // only. Therefore, we need to clone $account into a new object and copy any - // new property values of $edit into it. - $account_updated = clone $account; + // require a fully-fledged and updated entity object. Therefore, we need to + // copy any new property values of $edit into it. foreach ($edit as $key => $value) { - $account_updated->$key = $value; - } - field_attach_presave('user', $account_updated); - module_invoke_all('entity_presave', $account_updated, 'user'); - // Update $edit with any changes modules might have applied to the account. - foreach ($account_updated as $key => $value) { - if (!property_exists($account, $key) || $value !== $account->$key) { - $edit[$key] = $value; - } + $account->$key = $value; } + field_attach_presave('user', $account); + module_invoke_all('entity_presave', $account, 'user'); if (is_object($account) && !$account->is_new) { // Process picture uploads. - if (!$delete_previous_picture = empty($edit['picture']->fid)) { - $picture = $edit['picture']; + if (!empty($account->picture->fid) && (!isset($account->original->picture->fid) || $account->picture->fid != $account->original->picture->fid)) { + $picture = $account->picture; // If the picture is a temporary file move it to its final location and // make it permanent. if (!$picture->status) { @@ -474,26 +484,32 @@ // Move the temporary file into the final location. if ($picture = file_move($picture, $destination, FILE_EXISTS_RENAME)) { - $delete_previous_picture = TRUE; $picture->status = FILE_STATUS_PERMANENT; - $edit['picture'] = file_save($picture); + $account->picture = file_save($picture); file_usage_add($picture, 'user', 'user', $account->uid); } } + // Delete the previous picture if it was deleted or replaced. + if (!empty($account->original->picture->fid)) { + file_usage_delete($account->original->picture, 'user', 'user', $account->uid); + file_delete($account->original->picture); + } } - - // Delete the previous picture if it was deleted or replaced. - if ($delete_previous_picture && !empty($account->picture->fid)) { - file_usage_delete($account->picture, 'user', 'user', $account->uid); - file_delete($account->picture); - } - - $edit['picture'] = empty($edit['picture']->fid) ? 0 : $edit['picture']->fid; + elseif (isset($edit['picture_delete']) && $edit['picture_delete']) { + 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. - $edit['uid'] = $account->uid; + $account->uid = $account->original->uid; // Save changes to the user table. - $success = drupal_write_record('users', $edit, 'uid'); + $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. @@ -501,13 +517,13 @@ } // Reload user roles if provided. - if (isset($edit['roles']) && is_array($edit['roles'])) { + if ($account->roles != $account->original->roles) { db_delete('users_roles') ->condition('uid', $account->uid) ->execute(); $query = db_insert('users_roles')->fields(array('uid', 'rid')); - foreach (array_keys($edit['roles']) as $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, @@ -519,13 +535,13 @@ } // Delete a blocked user's sessions to kick them if they are online. - if (isset($edit['status']) && $edit['status'] == 0) { + if ($account->original->status != $account->status && $account->status == 0) { drupal_session_destroy_uid($account->uid); } // If the password changed, delete all open sessions and recreate // the current one. - if (!empty($edit['pass'])) { + if ($account->pass != $account->original->pass) { drupal_session_destroy_uid($account->uid); if ($account->uid == $GLOBALS['user']->uid) { drupal_session_regenerate(); @@ -533,70 +549,70 @@ } // Save Field data. - $entity = (object) $edit; - field_attach_update('user', $entity); - - // Refresh user object. - $user = user_load($account->uid, TRUE); - // Make the original, unchanged user account available to update hooks. - if (isset($account->original)) { - $user->original = $account->original; - } + field_attach_update('user', $account); // Send emails after we have the new user object. - if (isset($edit['status']) && $edit['status'] != $account->status) { + if ($account->status != $account->original->status) { // The user's status is changing; conditionally send notification email. - $op = $edit['status'] == 1 ? 'status_activated' : 'status_blocked'; - _user_mail_notify($op, $user); + $op = $account->status == 1 ? 'status_activated' : 'status_blocked'; + _user_mail_notify($op, $account); } - user_module_invoke('update', $edit, $user, $category); - module_invoke_all('entity_update', $user, 'user'); - unset($user->original); + // Update $edit with any interim changes to $account. + foreach ($account as $key => $value) { + if (!property_exists($account->original, $key) || $value !== $account->original->$key) { + $edit[$key] = $value; + } + } + user_module_invoke('update', $edit, $account, $category); + module_invoke_all('entity_update', $account, 'user'); } else { // Allow 'uid' to be set by the caller. There is no danger of writing an // existing user as drupal_write_record will do an INSERT. - if (empty($edit['uid'])) { - $edit['uid'] = db_next_id(db_query('SELECT MAX(uid) FROM {users}')->fetchField()); + if (empty($account->uid)) { + $account->uid = db_next_id(db_query('SELECT MAX(uid) FROM {users}')->fetchField()); } // Allow 'created' to be set by the caller. - if (!isset($edit['created'])) { - $edit['created'] = REQUEST_TIME; + if (!isset($account->created)) { + $account->created = REQUEST_TIME; } - $edit['mail'] = trim($edit['mail']); - $success = drupal_write_record('users', $edit); + $success = drupal_write_record('users', $account); if ($success === FALSE) { // On a failed INSERT some other existing user's uid may be returned. // We must abort to avoid overwriting their account. return FALSE; } - // Build a stub user object. - $user = (object) $edit; - $user->roles[DRUPAL_AUTHENTICATED_RID] = 'authenticated user'; - - field_attach_insert('user', $user); - - user_module_invoke('insert', $edit, $user, $category); - module_invoke_all('entity_insert', $user, 'user'); + // Make sure $account is properly initialized. + $account->roles[DRUPAL_AUTHENTICATED_RID] = 'authenticated user'; - // Save user roles. - if (isset($edit['roles']) && is_array($edit['roles'])) { + field_attach_insert('user', $account); + $edit = (array) $account; + user_module_invoke('insert', $edit, $account, $category); + module_invoke_all('entity_insert', $account, 'user'); + + // 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($edit['roles']) as $rid) { - if (!in_array($rid, array(DRUPAL_ANONYMOUS_RID, DRUPAL_AUTHENTICATED_RID))) { - $query->values(array( - 'uid' => $edit['uid'], - 'rid' => $rid, - )); - } + foreach ($rids_to_save as $rid) { + $query->values(array( + 'uid' => $account->uid, + 'rid' => $rid, + )); } $query->execute(); } } + // Clear internal properties. + unset($account->is_new); + unset($account->original); + // Clear the static loading cache. + entity_get_controller('user')->resetCache(array($account->uid)); - return $user; + return $account; } catch (Exception $e) { $transaction->rollback(); @@ -621,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 @@ -655,7 +671,6 @@ * If the address is valid, nothing is returned. */ function user_validate_mail($mail) { - $mail = trim($mail); if (!$mail) { return t('You must enter an e-mail address.'); } @@ -674,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. @@ -705,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; @@ -721,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()); @@ -769,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 @@ -811,7 +831,12 @@ /** * Checks for usernames blocked by user administration. * - * @return boolean TRUE for blocked users, FALSE for active. + * @param $name + * A string containing a name of the user. + * + * @return + * Object with property 'name' (the user name), if the user is blocked; + * FALSE if the user is not blocked. */ function user_is_blocked($name) { return db_select('users') @@ -822,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() { @@ -911,19 +956,25 @@ */ 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'); $query->fields('users', array('uid')); if (user_access('administer users')) { - // Administrators can also search in the otherwise private email field. + // Administrators can also search in the otherwise private email field, + // 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 { - $query->condition('name', '%' . db_like($keys) . '%', 'LIKE'); + // Regular users can only search via usernames, and we do not show them + // blocked accounts. + $query->condition('name', '%' . $keys . '%', 'LIKE') + ->condition('status', 1); } $uids = $query ->limit(15) @@ -1003,6 +1054,7 @@ // Account information. $form['account'] = array( + '#type' => 'container', '#weight' => -10, ); // Only show name field on registration form or user can change own username. @@ -1036,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.')))); @@ -1061,6 +1116,10 @@ '#access' => !empty($protected_values), '#description' => $current_pass_description, '#weight' => -5, + // Do not let web browsers remember this password, since we are trying + // to confirm that the person submitting the form actually knows the + // current one. + '#attributes' => array('autocomplete' => 'off'), ); $form['#validate'][] = 'user_validate_current_pass'; } @@ -1104,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, @@ -1174,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); @@ -1203,6 +1262,11 @@ } } + // Trim whitespace from mail, to prevent confusing 'e-mail not valid' + // warnings often caused by cutting and pasting. + $mail = trim($form_state['values']['mail']); + form_set_value($form['account']['mail'], $mail, $form_state); + // Validate the e-mail address, and check if it is taken by an existing user. if ($error = user_validate_mail($form_state['values']['mail'])) { form_set_error('mail', $error); @@ -1245,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. @@ -1271,7 +1337,7 @@ } function user_login_block($form) { - $form['#action'] = url($_GET['q'], array('query' => drupal_get_destination())); + $form['#action'] = url(current_path(), array('query' => drupal_get_destination(), 'external' => FALSE)); $form['#id'] = 'user-login-form'; $form['#validate'] = user_login_default_validators(); $form['#submit'][] = 'user_login_submit'; @@ -1283,7 +1349,6 @@ ); $form['pass'] = array('#type' => 'password', '#title' => t('Password'), - '#maxlength' => 60, '#size' => 15, '#required' => TRUE, ); @@ -1480,6 +1545,7 @@ function theme_user_list($variables) { $users = $variables['users']; $title = $variables['title']; + $items = array(); if (!empty($users)) { foreach ($users as $user) { @@ -1489,15 +1555,33 @@ return theme('item_list', array('items' => $items, 'title' => $title)); } +/** + * Determines if the current user is anonymous. + * + * @return bool + * TRUE if the user is anonymous, FALSE if the user is authenticated. + */ function user_is_anonymous() { // Menu administrators can see items for anonymous when administering. return !$GLOBALS['user']->uid || !empty($GLOBALS['menu_admin']); } +/** + * Determines if the current user is logged in. + * + * @return bool + * TRUE if the user is logged in, FALSE if the user is anonymous. + */ function user_is_logged_in() { return (bool) $GLOBALS['user']->uid; } +/** + * Determines if the current user has access to the user registration page. + * + * @return bool + * TRUE if the user is not already logged in and can register for an account. + */ function user_register_access() { return user_is_anonymous() && variable_get('user_register', USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL); } @@ -1672,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', @@ -1832,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; } } @@ -1848,7 +1934,7 @@ */ function user_translated_menu_link_alter(&$link) { // Hide the "User account" link for anonymous users. - if ($link['link_path'] == 'user' && $link['module'] == 'system' && user_is_anonymous()) { + if ($link['link_path'] == 'user' && $link['module'] == 'system' && !$GLOBALS['user']->uid) { $link['hidden'] = 1; } } @@ -2069,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']))); } @@ -2082,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 @@ -2146,7 +2232,11 @@ } } else { - form_set_error('name', t('Sorry, unrecognized username or password. Have you forgotten your password?', array('@password' => url('user/password')))); + // 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'])); } } @@ -2169,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. @@ -2192,7 +2282,12 @@ * Finalize the login process. Must be called when logging in a user. * * The function records a watchdog message about the new session, saves the - * login timestamp, calls hook_user op 'login' and generates a new session. * + * login timestamp, calls hook_user_login(), and generates a new session. + * + * @param array $edit + * The array of form values submitted by the user. + * + * @see hook_user_login() */ function user_login_finalize(&$edit = array()) { global $user; @@ -2260,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 @@ -2268,22 +2366,75 @@ */ 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)); } /** - * Generate a URL to confirm an account cancellation request. + * Generates a URL to confirm an account cancellation request. + * + * @param object $account + * The user account object, which must contain at least the following + * properties: + * - uid: The user ID number. + * - pass: The hashed user password string. + * - 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 + * account. * * @see user_mail_tokens() * @see user_cancel_confirm() */ 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)); } -function user_pass_rehash($password, $timestamp, $login) { - return drupal_hmac_base64($timestamp . $login, drupal_get_hash_salt() . $password); +/** + * Creates a unique hash value for use in time-dependent per-user URLs. + * + * This hash is normally used to build a unique and secure URL that is sent to + * the user by email for purposes such as resetting the user's password. In + * 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 + * timestamp and hashed password are retrieved from the database as necessary. + * For a usage example, see user_cancel_url() and user_cancel_confirm(). + * + * @param string $password + * The hashed user account password value. + * @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, $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); } /** @@ -2333,6 +2484,14 @@ array('_user_cancel', array($edit, $account, $method)), ), ); + + // After cancelling account, ensure that user is logged out. + if ($account->uid == $user->uid) { + // Batch API stores data in the session, so use the finished operation to + // manipulate the current user's session id. + $batch['finished'] = '_user_cancel_session_regenerate'; + } + batch_set($batch); // Batch processing is either handled via Form API or has to be invoked @@ -2340,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. @@ -2375,10 +2536,12 @@ break; } - // After cancelling account, ensure that user is logged out. + // After cancelling account, ensure that user is logged out. We can't destroy + // their session though, as we might have information in it, and we can't + // regenerate it because batch API uses the session ID, we will regenerate it + // in _user_cancel_session_regenerate(). if ($account->uid == $user->uid) { - // Destroy the current session, and reset $user to the anonymous user. - session_destroy(); + $user = drupal_anonymous_user(); } // Clear the cache for anonymous users. @@ -2386,6 +2549,19 @@ } /** + * Implements callback_batch_finished(). + * + * Finished batch processing callback for cancelling a user account. + * + * @see user_cancel() + */ +function _user_cancel_session_regenerate() { + // Regenerate the users session instead of calling session_destroy() as we + // want to preserve any messages that might have been set. + drupal_session_regenerate(); +} + +/** * Delete a user. * * @param $uid @@ -2517,14 +2693,21 @@ // Remove previously built content, if exists. $account->content = array(); + // Allow modules to change the view mode. + $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); - entity_prepare_view('user', array($account->uid => $account)); + field_attach_prepare_view('user', array($account->uid => $account), $view_mode, $langcode); + entity_prepare_view('user', array($account->uid => $account), $langcode); $account->content += field_attach_view('user', $account, $view_mode, $langcode); // Populate $account->content with a render() array. module_invoke_all('user_view', $account, $view_mode, $langcode); module_invoke_all('entity_view', $account, 'user', $view_mode, $langcode); + + // Make sure the current view mode is stored if no module has already + // populated the related key. + $account->content += array('#view_mode' => $view_mode); } /** @@ -2695,7 +2878,7 @@ if ($replace) { // We do not sanitize the token replacement, since the output of this // replacement is intended for an e-mail message, not a web browser. - return token_replace($text, $variables, array('language' => $language, 'callback' => 'user_mail_tokens', 'sanitize' => FALSE)); + return token_replace($text, $variables, array('language' => $language, 'callback' => 'user_mail_tokens', 'sanitize' => FALSE, 'clear' => TRUE)); } return $text; @@ -2704,7 +2887,21 @@ /** * Token callback to add unsafe tokens for user mails. * - * @see user_mail() + * This function is used by the token_replace() call at the end of + * _user_mail_text() to set up some additional tokens that can be + * used in email messages generated by user_mail(). + * + * @param $replacements + * An associative array variable containing mappings from token names to + * values (for use with strtr()). + * @param $data + * 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 UNIX timestamp of the user's last login. + * - pass: The hashed account login password. + * @param $options + * Unused parameter required by the token_replace() function. */ function user_mail_tokens(&$replacements, $data, $options) { if (isset($data['user'])) { @@ -2823,6 +3020,10 @@ $query->addExpression('MAX(weight)'); $role->weight = $query->execute()->fetchField() + 1; } + + // Let modules modify the user role before it is saved to the database. + module_invoke_all('user_role_presave', $role); + if (!empty($role->rid) && $role->name) { $status = drupal_write_record('role', $role, 'rid'); module_invoke_all('user_role_update', $role); @@ -2853,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(); @@ -3263,7 +3469,7 @@ $options = array(); foreach (module_implements('permission') as $module) { $function = $module . '_permission'; - if ($permissions = $function('permission')) { + if ($permissions = $function()) { asort($permissions); foreach ($permissions as $permission => $description) { $options[t('@module module', array('@module' => $module))][$permission] = t($permission); @@ -3326,15 +3532,6 @@ } /** - * Implements hook_forms(). - */ -function user_forms() { - $forms['user_admin_access_add_form']['callback'] = 'user_admin_access_form'; - $forms['user_admin_access_edit_form']['callback'] = 'user_admin_access_form'; - return $forms; -} - -/** * Implements hook_comment_view(). */ function user_comment_view($comment) { @@ -3404,23 +3601,27 @@ * @see drupal_mail() * * @param $op - * The operation being performed on the account. Possible values: - * 'register_admin_created': Welcome message for user created by the admin - * 'register_no_approval_required': Welcome message when user self-registers - * 'register_pending_approval': Welcome message, user pending admin approval - * 'password_reset': Password recovery request - * 'status_activated': Account activated - * 'status_blocked': Account blocked - * 'cancel_confirm': Account cancellation request - * 'status_canceled': Account canceled + * The operation being performed on the account. Possible values: + * - 'register_admin_created': Welcome message for user created by the admin. + * - 'register_no_approval_required': Welcome message when user + * self-registers. + * - 'register_pending_approval': Welcome message, user pending admin + * approval. + * - 'password_reset': Password recovery request. + * - 'status_activated': Account activated. + * - 'status_blocked': Account blocked. + * - 'cancel_confirm': Account cancellation request. + * - 'status_canceled': Account canceled. * * @param $account - * The user object of the account being notified. Must contain at - * least the fields 'uid', 'name', and 'mail'. + * The user object of the account being notified. Must contain at + * least the fields 'uid', 'name', and 'mail'. * @param $language - * Optional language to use for the notification, overriding account language. + * Optional language to use for the notification, overriding account language. + * * @return - * The return value from drupal_mail_system()->mail(), if ends up being called. + * The return value from drupal_mail_system()->mail(), if ends up being + * called. */ function _user_mail_notify($op, $account, $language = NULL) { // By default, we always notify except for canceled and blocked. @@ -3473,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; } @@ -3538,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 */ @@ -3569,7 +3772,7 @@ function user_form_field_ui_field_edit_form_alter(&$form, &$form_state, $form_id) { $instance = $form['#instance']; - if ($instance['entity_type'] == 'user') { + if ($instance['entity_type'] == 'user' && !$form['#field']['locked']) { $form['instance']['settings']['user_register_form'] = array( '#type' => 'checkbox', '#title' => t('Display on user registration form.'), @@ -3622,6 +3825,14 @@ $admin = user_access('administer users'); + // Pass access information to the submit handler. Running an access check + // inside the submit function interferes with form processing and breaks + // hook_form_alter(). + $form['administer_users'] = array( + '#type' => 'value', + '#value' => $admin, + ); + // If we aren't admin but already logged on, go to the user page instead. if (!$admin && $user->uid) { drupal_goto('user/' . $user->uid); @@ -3638,7 +3849,8 @@ // Attach field widgets, and hide the ones where the 'user_register_form' // setting is not on. - field_attach_form('user', $form['#user'], $form, $form_state); + $langcode = entity_language('user', $form['#user']); + field_attach_form('user', $form['#user'], $form, $form_state, $langcode); foreach (field_info_instances('user', 'user') as $field_name => $instance) { if (empty($instance['settings']['user_register_form'])) { $form[$field_name]['#access'] = FALSE; @@ -3680,7 +3892,7 @@ * @see user_register_form() */ function user_register_submit($form, &$form_state) { - $admin = user_access('administer users'); + $admin = $form_state['values']['administer_users']; if (!variable_get('user_email_verification', TRUE) || $admin) { $pass = $form_state['values']['pass']; diff -Naur drupal-7.0/modules/user/user.pages.inc drupal-7.66/modules/user/user.pages.inc --- drupal-7.0/modules/user/user.pages.inc 2010-12-01 01:29:41.000000000 +0100 +++ drupal-7.66/modules/user/user.pages.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ 60, '#maxlength' => max(USERNAME_MAX_LENGTH, EMAIL_MAX_LENGTH), '#required' => TRUE, + '#default_value' => isset($_GET['name']) ? $_GET['name'] : '', ); // Allow logged in users to request this also. if ($user->uid > 0) { @@ -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,14 +83,21 @@ } } +/** + * Form submission handler for user_pass(). + * + * @see user_pass_validate() + */ function user_pass_submit($form, &$form_state) { global $language; $account = $form_state['values']['account']; // Mail one time login URL and instructions using current language. - _user_mail_notify('password_reset', $account, $language); - watchdog('user', 'Password reset instructions mailed to %name at %email.', array('%name' => $account->name, '%email' => $account->mail)); - drupal_set_message(t('Further instructions have been sent to your e-mail address.')); + $mail = _user_mail_notify('password_reset', $account, $language); + if (!empty($mail)) { + watchdog('user', 'Password reset instructions mailed to %name at %email.', array('%name' => $account->name, '%email' => $account->mail)); + drupal_set_message(t('Further instructions have been sent to your e-mail address.')); + } $form_state['redirect'] = 'user'; return; @@ -94,47 +112,59 @@ // 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. 24 hours = 86400 seconds. - $timeout = 86400; + // Time out, in seconds, until login URL expires. Defaults to 24 hours = + // 86400 seconds. + $timeout = variable_get('user_password_reset_timeout', 86400); $current = REQUEST_TIME; // Some redundant checks for extra security ? $users = user_load_multiple(array($uid), array('status' => '1')); 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') { - watchdog('user', 'User %name used one-time login link at time %timestamp.', array('%name' => $account->name, '%timestamp' => $timestamp)); // Set the new user. $user = $account; // user_login_finalize() also updates the login timestamp of the // user, which invalidates further use of the one-time login link. user_login_finalize(); + 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))); } @@ -148,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'); } } @@ -156,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(); } } } @@ -164,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)); @@ -172,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 */ @@ -261,7 +301,8 @@ if ($category == 'account') { user_account_form($form, $form_state); // Attach field widgets. - field_attach_form('user', $account, $form, $form_state); + $langcode = entity_language('user', $account); + field_attach_form('user', $account, $form, $form_state, $langcode); } $form['actions'] = array('#type' => 'actions'); @@ -286,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']; @@ -351,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( @@ -515,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), ); @@ -526,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.0/modules/user/user.permissions.js drupal-7.66/modules/user/user.permissions.js --- drupal-7.0/modules/user/user.permissions.js 2010-04-19 23:17:16.000000000 +0200 +++ drupal-7.66/modules/user/user.permissions.js 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -// $Id: user.permissions.js,v 1.2 2010/04/19 21:17:16 webchick Exp $ (function ($) { /** @@ -6,34 +5,64 @@ */ Drupal.behaviors.permissions = { attach: function (context) { - $('table#permissions:not(.permissions-processed)').each(function () { + var self = this; + $('table#permissions').once('permissions', function () { + // On a site with many roles and permissions, this behavior initially has + // to perform thousands of DOM manipulations to inject checkboxes and hide + // them. By detaching the table from the DOM, all operations can be + // performed without triggering internal layout and re-rendering processes + // in the browser. + var $table = $(this); + if ($table.prev().length) { + var $ancestor = $table.prev(), method = 'after'; + } + else { + var $ancestor = $table.parent(), method = 'append'; + } + $table.detach(); + // Create dummy checkboxes. We use dummy checkboxes instead of reusing // the existing checkboxes here because new checkboxes don't alter the // submitted form. If we'd automatically check existing checkboxes, the // permission table would be polluted with redundant entries. This // is deliberate, but desirable when we automatically check them. - $(':checkbox', this).not('[name^="2["]').not('[name^="1["]').each(function () { - $(this).addClass('real-checkbox'); - $('') - .attr('title', Drupal.t("This permission is inherited from the authenticated user role.")) - .insertAfter(this) - .hide(); - }); + var $dummy = $('') + .attr('title', Drupal.t("This permission is inherited from the authenticated user role.")) + .hide(); - // Helper function toggles all dummy checkboxes based on the checkboxes' - // state. If the "authenticated user" checkbox is checked, the checked - // and disabled checkboxes are shown, the real checkboxes otherwise. - var toggle = function () { - $(this).closest('tr') - .find('.real-checkbox')[this.checked ? 'hide' : 'show']().end() - .find('.dummy-checkbox')[this.checked ? 'show' : 'hide'](); - }; + $('input[type=checkbox]', this).not('.rid-2, .rid-1').addClass('real-checkbox').each(function () { + $dummy.clone().insertAfter(this); + }); // Initialize the authenticated user checkbox. - $(':checkbox[name^="2["]', this) - .click(toggle) - .each(function () { toggle.call(this); }); - }).addClass('permissions-processed'); + $('input[type=checkbox].rid-2', this) + .bind('click.permissions', self.toggle) + // .triggerHandler() cannot be used here, as it only affects the first + // element. + .each(self.toggle); + + // Re-insert the table into the DOM. + $ancestor[method]($table); + }); + }, + + /** + * Toggles all dummy checkboxes based on the checkboxes' state. + * + * If the "authenticated user" checkbox is checked, the checked and disabled + * checkboxes are shown, the real checkboxes otherwise. + */ + toggle: function () { + var authCheckbox = this, $row = $(this).closest('tr'); + // jQuery performs too many layout calculations for .hide() and .show(), + // leading to a major page rendering lag on sites with many roles and + // permissions. Therefore, we toggle visibility directly. + $row.find('.real-checkbox').each(function () { + this.style.display = (authCheckbox.checked ? 'none' : ''); + }); + $row.find('.dummy-checkbox').each(function () { + this.style.display = (authCheckbox.checked ? '' : 'none'); + }); } }; diff -Naur drupal-7.0/modules/user/user.test drupal-7.66/modules/user/user.test --- drupal-7.0/modules/user/user.test 2010-12-18 01:56:18.000000000 +0100 +++ drupal-7.66/modules/user/user.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,9 @@ drupalGet('user/register'); - $this->assertResponse(403, t('Registration page is inaccessible when only administrators can create accounts.')); + $this->assertResponse(403, 'Registration page is inaccessible when only administrators can create accounts.'); // Allow registration by site visitors without administrator approval. variable_set('user_register', USER_REGISTER_VISITORS); @@ -29,10 +33,10 @@ $edit['name'] = $name = $this->randomName(); $edit['mail'] = $mail = $edit['name'] . '@example.com'; $this->drupalPost('user/register', $edit, t('Create new account')); - $this->assertText(t('A welcome message with further instructions has been sent to your e-mail address.'), t('User registered successfully.')); + $this->assertText(t('A welcome message with further instructions has been sent to your e-mail address.'), 'User registered successfully.'); $accounts = user_load_multiple(array(), array('name' => $name, 'mail' => $mail)); $new_user = reset($accounts); - $this->assertTrue($new_user->status, t('New account is active after registration.')); + $this->assertTrue($new_user->status, 'New account is active after registration.'); // Allow registration by site visitors, but require administrator approval. variable_set('user_register', USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL); @@ -42,7 +46,7 @@ $this->drupalPost('user/register', $edit, t('Create new account')); $accounts = user_load_multiple(array(), array('name' => $name, 'mail' => $mail)); $new_user = reset($accounts); - $this->assertFalse($new_user->status, t('New account is blocked until approved by an administrator.')); + $this->assertFalse($new_user->status, 'New account is blocked until approved by an administrator.'); } function testRegistrationWithoutEmailVerification() { @@ -59,7 +63,7 @@ $edit['pass[pass1]'] = '99999.0'; $edit['pass[pass2]'] = '99999'; $this->drupalPost('user/register', $edit, t('Create new account')); - $this->assertText(t('The specified passwords do not match.'), t('Typing mismatched passwords displays an error message.')); + $this->assertText(t('The specified passwords do not match.'), 'Typing mismatched passwords displays an error message.'); // Enter a correct password. $edit['pass[pass1]'] = $new_pass = $this->randomName(); @@ -67,7 +71,7 @@ $this->drupalPost('user/register', $edit, t('Create new account')); $accounts = user_load_multiple(array(), array('name' => $name, 'mail' => $mail)); $new_user = reset($accounts); - $this->assertText(t('Registration successful. You are now logged in.'), t('Users are logged in after registering.')); + $this->assertText(t('Registration successful. You are now logged in.'), 'Users are logged in after registering.'); $this->drupalLogout(); // Allow registration by site visitors, but require administrator approval. @@ -78,7 +82,7 @@ $edit['pass[pass1]'] = $pass = $this->randomName(); $edit['pass[pass2]'] = $pass; $this->drupalPost('user/register', $edit, t('Create new account')); - $this->assertText(t('Thank you for applying for an account. Your account is currently pending approval by the site administrator.'), t('Users are notified of pending approval')); + $this->assertText(t('Thank you for applying for an account. Your account is currently pending approval by the site administrator.'), 'Users are notified of pending approval'); // Try to login before administrator approval. $auth = array( @@ -86,7 +90,7 @@ 'pass' => $pass, ); $this->drupalPost('user/login', $auth, t('Log in')); - $this->assertText(t('The username @name has not been activated or is blocked.', array('@name' => $name)), t('User cannot login yet.')); + $this->assertText(t('The username @name has not been activated or is blocked.', array('@name' => $name)), 'User cannot login yet.'); // Activate the new account. $accounts = user_load_multiple(array(), array('name' => $name, 'mail' => $mail)); @@ -101,7 +105,32 @@ // Login after administrator approval. $this->drupalPost('user/login', $auth, t('Log in')); - $this->assertText(t('Member for'), t('User can log in after administrator approval.')); + $this->assertText(t('Member for'), 'User can log in after administrator approval.'); + } + + function testRegistrationEmailDuplicates() { + // Don't require e-mail verification. + variable_set('user_email_verification', FALSE); + + // Allow registration by site visitors without administrator approval. + variable_set('user_register', USER_REGISTER_VISITORS); + + // Set up a user to check for duplicates. + $duplicate_user = $this->drupalCreateUser(); + + $edit = array(); + $edit['name'] = $this->randomName(); + $edit['mail'] = $duplicate_user->mail; + + // Attempt to create a new account using an existing e-mail address. + $this->drupalPost('user/register', $edit, t('Create new account')); + $this->assertText(t('The e-mail address @email is already registered.', array('@email' => $duplicate_user->mail)), 'Supplying an exact duplicate email address displays an error message'); + + // Attempt to bypass duplicate email registration validation by adding spaces. + $edit['mail'] = ' ' . $duplicate_user->mail . ' '; + + $this->drupalPost('user/register', $edit, t('Create new account')); + $this->assertText(t('The e-mail address @email is already registered.', array('@email' => $duplicate_user->mail)), 'Supplying a duplicate email address with added whitespace displays an error message'); } function testRegistrationDefaultValues() { @@ -118,7 +147,7 @@ // Check that the account information fieldset's options are not displayed // is a fieldset if there is not more than one fieldset in the form. $this->drupalGet('user/register'); - $this->assertNoRaw('
        Account information', t('Account settings fieldset was hidden.')); + $this->assertNoRaw('
        Account information', 'Account settings fieldset was hidden.'); $edit = array(); $edit['name'] = $name = $this->randomName(); @@ -130,16 +159,16 @@ // Check user fields. $accounts = user_load_multiple(array(), array('name' => $name, 'mail' => $mail)); $new_user = reset($accounts); - $this->assertEqual($new_user->name, $name, t('Username matches.')); - $this->assertEqual($new_user->mail, $mail, t('E-mail address matches.')); - $this->assertEqual($new_user->theme, '', t('Correct theme field.')); - $this->assertEqual($new_user->signature, '', t('Correct signature field.')); - $this->assertTrue(($new_user->created > REQUEST_TIME - 20 ), t('Correct creation time.')); - $this->assertEqual($new_user->status, variable_get('user_register', USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL) == USER_REGISTER_VISITORS ? 1 : 0, t('Correct status field.')); - $this->assertEqual($new_user->timezone, variable_get('date_default_timezone'), t('Correct time zone field.')); - $this->assertEqual($new_user->language, '', t('Correct language field.')); - $this->assertEqual($new_user->picture, '', t('Correct picture field.')); - $this->assertEqual($new_user->init, $mail, t('Correct init field.')); + $this->assertEqual($new_user->name, $name, 'Username matches.'); + $this->assertEqual($new_user->mail, $mail, 'E-mail address matches.'); + $this->assertEqual($new_user->theme, '', 'Correct theme field.'); + $this->assertEqual($new_user->signature, '', 'Correct signature field.'); + $this->assertTrue(($new_user->created > REQUEST_TIME - 20 ), 'Correct creation time.'); + $this->assertEqual($new_user->status, variable_get('user_register', USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL) == USER_REGISTER_VISITORS ? 1 : 0, 'Correct status field.'); + $this->assertEqual($new_user->timezone, variable_get('date_default_timezone'), 'Correct time zone field.'); + $this->assertEqual($new_user->language, '', 'Correct language field.'); + $this->assertEqual($new_user->picture, '', 'Correct picture field.'); + $this->assertEqual($new_user->init, $mail, 'Correct init field.'); } /** @@ -165,13 +194,13 @@ // Check that the field does not appear on the registration form. $this->drupalGet('user/register'); - $this->assertNoText($instance['label'], t('The field does not appear on user registration form')); + $this->assertNoText($instance['label'], 'The field does not appear on user registration form'); // Have the field appear on the registration form. $instance['settings']['user_register_form'] = TRUE; field_update_instance($instance); $this->drupalGet('user/register'); - $this->assertText($instance['label'], t('The field appears on user registration form')); + $this->assertText($instance['label'], 'The field appears on user registration form'); // Check that validation errors are correctly reported. $edit = array(); @@ -180,11 +209,11 @@ // Missing input in required field. $edit['test_user_field[und][0][value]'] = ''; $this->drupalPost(NULL, $edit, t('Create new account')); - $this->assertRaw(t('@name field is required.', array('@name' => $instance['label'])), t('Field validation error was correctly reported.')); + $this->assertRaw(t('@name field is required.', array('@name' => $instance['label'])), 'Field validation error was correctly reported.'); // Invalid input. $edit['test_user_field[und][0][value]'] = '-1'; $this->drupalPost(NULL, $edit, t('Create new account')); - $this->assertRaw(t('%name does not accept the value -1.', array('%name' => $instance['label'])), t('Field validation error was correctly reported.')); + $this->assertRaw(t('%name does not accept the value -1.', array('%name' => $instance['label'])), 'Field validation error was correctly reported.'); // Submit with valid data. $value = rand(1, 255); @@ -193,7 +222,7 @@ // Check user fields. $accounts = user_load_multiple(array(), array('name' => $name, 'mail' => $mail)); $new_user = reset($accounts); - $this->assertEqual($new_user->test_user_field[LANGUAGE_NONE][0]['value'], $value, t('The field value was correclty saved.')); + $this->assertEqual($new_user->test_user_field[LANGUAGE_NONE][0]['value'], $value, 'The field value was correctly saved.'); // Check that the 'add more' button works. $field['cardinality'] = FIELD_CARDINALITY_UNLIMITED; @@ -221,9 +250,9 @@ // Check user fields. $accounts = user_load_multiple(array(), array('name' => $name, 'mail' => $mail)); $new_user = reset($accounts); - $this->assertEqual($new_user->test_user_field[LANGUAGE_NONE][0]['value'], $value, t('@js : The field value was correclty saved.', array('@js' => $js))); - $this->assertEqual($new_user->test_user_field[LANGUAGE_NONE][1]['value'], $value + 1, t('@js : The field value was correclty saved.', array('@js' => $js))); - $this->assertEqual($new_user->test_user_field[LANGUAGE_NONE][2]['value'], $value + 2, t('@js : The field value was correclty saved.', array('@js' => $js))); + $this->assertEqual($new_user->test_user_field[LANGUAGE_NONE][0]['value'], $value, format_string('@js : The field value was correclty saved.', array('@js' => $js))); + $this->assertEqual($new_user->test_user_field[LANGUAGE_NONE][1]['value'], $value + 1, format_string('@js : The field value was correclty saved.', array('@js' => $js))); + $this->assertEqual($new_user->test_user_field[LANGUAGE_NONE][2]['value'], $value + 2, format_string('@js : The field value was correclty saved.', array('@js' => $js))); } } } @@ -247,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'), @@ -406,6 +436,7 @@ 'pass' => $account->pass_raw, ); $this->drupalPost('user', $edit, t('Log in')); + $this->assertNoFieldByXPath("//input[@name='pass' and @value!='']", NULL, 'Password value attribute is blank.'); if (isset($flood_trigger)) { if ($flood_trigger == 'user') { $this->assertRaw(format_plural(variable_get('user_failed_login_user_limit', 5), 'Sorry, there has been more than one failed login attempt for this account. It is temporarily blocked. Try again later or request a new password.', 'Sorry, there have been more than @count failed login attempts for this account. It is temporarily blocked. Try again later or request a new password.', array('@url' => url('user/password')))); @@ -422,6 +453,228 @@ } /** + * Tests resetting a user password. + */ +class UserPasswordResetTestCase extends DrupalWebTestCase { + protected $profile = 'standard'; + + public static function getInfo() { + return array( + 'name' => 'Reset password', + 'description' => 'Ensure that password reset methods work as expected.', + 'group' => 'User', + ); + } + + /** + * 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() { + // Create a user. + $account = $this->drupalCreateUser(); + $this->drupalLogin($account); + $this->drupalLogout(); + // Attempt to reset password. + $edit = array('name' => $account->name); + $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.'); + } + + /** + * Attempts login using an expired password reset link. + */ + function testUserPasswordResetExpired() { + // Set password reset timeout variable to 43200 seconds = 12 hours. + $timeout = 43200; + variable_set('user_password_reset_timeout', $timeout); + + // Create a user. + $account = $this->drupalCreateUser(); + $this->drupalLogin($account); + // Load real user object. + $account = user_load($account->uid, TRUE); + $this->drupalLogout(); + + // 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, $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.'); + } + + /** + * Prefill the text box on incorrect login via link to password reset page. + */ + function testUserPasswordTextboxFilled() { + $this->drupalGet('user/login'); + $edit = array( + 'name' => $this->randomName(), + 'pass' => $this->randomName(), + ); + $this->drupalPost('user', $edit, t('Log in')); + $this->assertRaw(t('Sorry, unrecognized username or password. Have you forgotten your password?', + array('@password' => url('user/password', array('query' => array('name' => $edit['name'])))))); + unset($edit['pass']); + $this->drupalGet('user/password', array('query' => array('name' => $edit['name']))); + $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.'); + } + +} + +/** * Test cancelling a user. */ class UserCancelTestCase extends DrupalWebTestCase { @@ -454,18 +707,18 @@ // Attempt to cancel account. $this->drupalGet('user/' . $account->uid . '/edit'); - $this->assertNoRaw(t('Cancel account'), t('No cancel account button displayed.')); + $this->assertNoRaw(t('Cancel account'), 'No cancel account button displayed.'); // 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->assertResponse(403, t('Bogus cancelling request rejected.')); + $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, t('User account was not canceled.')); + $this->assertTrue($account->status == 1, 'User account was not canceled.'); // Confirm user's content has not been altered. $test_node = node_load($node->nid, NULL, TRUE); - $this->assertTrue(($test_node->uid == $account->uid && $test_node->status == 1), t('Node of the user has not been altered.')); + $this->assertTrue(($test_node->uid == $account->uid && $test_node->status == 1), 'Node of the user has not been altered.'); } /** @@ -503,7 +756,7 @@ // Verify that uid 1's account was not cancelled. $user1 = user_load(1, TRUE); - $this->assertEqual($user1->status, 1, t('User #1 still exists and is not blocked.')); + $this->assertEqual($user1->status, 1, 'User #1 still exists and is not blocked.'); } /** @@ -527,25 +780,25 @@ // Confirm account cancellation. $timestamp = time(); $this->drupalPost(NULL, NULL, t('Cancel account')); - $this->assertText(t('A confirmation request to cancel your account has been sent to your e-mail address.'), t('Account cancellation request mailed message displayed.')); + $this->assertText(t('A confirmation request to cancel your account has been sent to your e-mail address.'), 'Account cancellation request mailed message displayed.'); // 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->assertText(t('You have tried to use an account cancellation link that has expired. Please request a new one using the form below.'), t('Bogus cancelling request rejected.')); + $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, t('User account was not canceled.')); + $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->assertText(t('You have tried to use an account cancellation link that has expired. Please request a new one using the form below.'), t('Expired cancel account request rejected.')); + $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), t('User account was not canceled.')); + $this->assertTrue(reset($accounts), 'User account was not canceled.'); // Confirm user's content has not been altered. $test_node = node_load($node->nid, NULL, TRUE); - $this->assertTrue(($test_node->uid == $account->uid && $test_node->status == 1), t('Node of the user has not been altered.')); + $this->assertTrue(($test_node->uid == $account->uid && $test_node->status == 1), 'Node of the user has not been altered.'); } /** @@ -564,23 +817,23 @@ // Attempt to cancel account. $this->drupalGet('user/' . $account->uid . '/edit'); $this->drupalPost(NULL, NULL, t('Cancel account')); - $this->assertText(t('Are you sure you want to cancel your account?'), t('Confirmation form to cancel account displayed.')); - $this->assertText(t('Your account will be blocked and you will no longer be able to log in. All of your content will remain attributed to your user name.'), t('Informs that all content will be remain as is.')); - $this->assertNoText(t('Select the method to cancel the account above.'), t('Does not allow user to select account cancellation method.')); + $this->assertText(t('Are you sure you want to cancel your account?'), 'Confirmation form to cancel account displayed.'); + $this->assertText(t('Your account will be blocked and you will no longer be able to log in. All of your content will remain attributed to your user name.'), 'Informs that all content will be remain as is.'); + $this->assertNoText(t('Select the method to cancel the account above.'), 'Does not allow user to select account cancellation method.'); // Confirm account cancellation. $timestamp = time(); $this->drupalPost(NULL, NULL, t('Cancel account')); - $this->assertText(t('A confirmation request to cancel your account has been sent to your e-mail address.'), t('Account cancellation request mailed message displayed.')); + $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, t('User has been blocked.')); + $this->assertTrue($account->status == 0, 'User has been blocked.'); - // Confirm user is logged out. - $this->assertNoText($account->name, t('Logged out.')); + // Confirm that the confirmation message made it through to the end user. + $this->assertRaw(t('%name has been disabled.', array('%name' => $account->name)), "Confirmation message displayed to user."); } /** @@ -604,27 +857,27 @@ // Attempt to cancel account. $this->drupalGet('user/' . $account->uid . '/edit'); $this->drupalPost(NULL, NULL, t('Cancel account')); - $this->assertText(t('Are you sure you want to cancel your account?'), t('Confirmation form to cancel account displayed.')); - $this->assertText(t('Your account will be blocked and you will no longer be able to log in. All of your content will be hidden from everyone but administrators.'), t('Informs that all content will be unpublished.')); + $this->assertText(t('Are you sure you want to cancel your account?'), 'Confirmation form to cancel account displayed.'); + $this->assertText(t('Your account will be blocked and you will no longer be able to log in. All of your content will be hidden from everyone but administrators.'), 'Informs that all content will be unpublished.'); // Confirm account cancellation. $timestamp = time(); $this->drupalPost(NULL, NULL, t('Cancel account')); - $this->assertText(t('A confirmation request to cancel your account has been sent to your e-mail address.'), t('Account cancellation request mailed message displayed.')); + $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, t('User has been blocked.')); + $this->assertTrue($account->status == 0, 'User has been blocked.'); // Confirm user's content has been unpublished. $test_node = node_load($node->nid, NULL, TRUE); - $this->assertTrue($test_node->status == 0, t('Node of the user has been unpublished.')); + $this->assertTrue($test_node->status == 0, 'Node of the user has been unpublished.'); $test_node = node_load($node->nid, $node->vid, TRUE); - $this->assertTrue($test_node->status == 0, t('Node revision of the user has been unpublished.')); + $this->assertTrue($test_node->status == 0, 'Node revision of the user has been unpublished.'); - // Confirm user is logged out. - $this->assertNoText($account->name, t('Logged out.')); + // Confirm that the confirmation message made it through to the end user. + $this->assertRaw(t('%name has been disabled.', array('%name' => $account->name)), "Confirmation message displayed to user."); } /** @@ -654,28 +907,28 @@ // Attempt to cancel account. $this->drupalGet('user/' . $account->uid . '/edit'); $this->drupalPost(NULL, NULL, t('Cancel account')); - $this->assertText(t('Are you sure you want to cancel your account?'), t('Confirmation form to cancel account displayed.')); - $this->assertRaw(t('Your account will be removed and all account information deleted. All of your content will be assigned to the %anonymous-name user.', array('%anonymous-name' => variable_get('anonymous', t('Anonymous')))), t('Informs that all content will be attributed to anonymous account.')); + $this->assertText(t('Are you sure you want to cancel your account?'), 'Confirmation form to cancel account displayed.'); + $this->assertRaw(t('Your account will be removed and all account information deleted. All of your content will be assigned to the %anonymous-name user.', array('%anonymous-name' => variable_get('anonymous', t('Anonymous')))), 'Informs that all content will be attributed to anonymous account.'); // Confirm account cancellation. $timestamp = time(); $this->drupalPost(NULL, NULL, t('Cancel account')); - $this->assertText(t('A confirmation request to cancel your account has been sent to your e-mail address.'), t('Account cancellation request mailed message displayed.')); + $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->assertFalse(user_load($account->uid, TRUE), t('User is not found in the database.')); + $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. $test_node = node_load($node->nid, NULL, TRUE); - $this->assertTrue(($test_node->uid == 0 && $test_node->status == 1), t('Node of the user has been attributed to anonymous user.')); + $this->assertTrue(($test_node->uid == 0 && $test_node->status == 1), 'Node of the user has been attributed to anonymous user.'); $test_node = node_load($revision_node->nid, $revision, TRUE); - $this->assertTrue(($test_node->revision_uid == 0 && $test_node->status == 1), t('Node revision of the user has been attributed to anonymous user.')); + $this->assertTrue(($test_node->revision_uid == 0 && $test_node->status == 1), 'Node revision of the user has been attributed to anonymous user.'); $test_node = node_load($revision_node->nid, NULL, TRUE); - $this->assertTrue(($test_node->uid != 0 && $test_node->status == 1), t("Current revision of the user's node was not attributed to anonymous user.")); + $this->assertTrue(($test_node->uid != 0 && $test_node->status == 1), "Current revision of the user's node was not attributed to anonymous user."); - // Confirm that user is logged out. - $this->assertNoText($account->name, t('Logged out.')); + // Confirm that the confirmation message made it through to the end user. + $this->assertRaw(t('%name has been deleted.', array('%name' => $account->name)), "Confirmation message displayed to user."); } /** @@ -704,7 +957,7 @@ $this->assertText(t('Your comment has been posted.')); $comments = comment_load_multiple(array(), array('subject' => $edit['subject'])); $comment = reset($comments); - $this->assertTrue($comment->cid, t('Comment found.')); + $this->assertTrue($comment->cid, 'Comment found.'); // Create a node with two revisions, the initial one belonging to the // cancelling user. @@ -718,26 +971,26 @@ // Attempt to cancel account. $this->drupalGet('user/' . $account->uid . '/edit'); $this->drupalPost(NULL, NULL, t('Cancel account')); - $this->assertText(t('Are you sure you want to cancel your account?'), t('Confirmation form to cancel account displayed.')); - $this->assertText(t('Your account will be removed and all account information deleted. All of your content will also be deleted.'), t('Informs that all content will be deleted.')); + $this->assertText(t('Are you sure you want to cancel your account?'), 'Confirmation form to cancel account displayed.'); + $this->assertText(t('Your account will be removed and all account information deleted. All of your content will also be deleted.'), 'Informs that all content will be deleted.'); // Confirm account cancellation. $timestamp = time(); $this->drupalPost(NULL, NULL, t('Cancel account')); - $this->assertText(t('A confirmation request to cancel your account has been sent to your e-mail address.'), t('Account cancellation request mailed message displayed.')); + $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->assertFalse(user_load($account->uid, TRUE), t('User is not found in the database.')); + $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. - $this->assertFalse(node_load($node->nid, NULL, TRUE), t('Node of the user has been deleted.')); - $this->assertFalse(node_load($node->nid, $revision, TRUE), t('Node revision of the user has been deleted.')); - $this->assertTrue(node_load($revision_node->nid, NULL, TRUE), t("Current revision of the user's node was not deleted.")); - $this->assertFalse(comment_load($comment->cid), t('Comment of the user has been deleted.')); + $this->assertFalse(node_load($node->nid, NULL, TRUE), 'Node of the user has been deleted.'); + $this->assertFalse(node_load($node->nid, $revision, TRUE), 'Node revision of the user has been deleted.'); + $this->assertTrue(node_load($revision_node->nid, NULL, TRUE), "Current revision of the user's node was not deleted."); + $this->assertFalse(comment_load($comment->cid), 'Comment of the user has been deleted.'); - // Confirm that user is logged out. - $this->assertNoText($account->name, t('Logged out.')); + // Confirm that the confirmation message made it through to the end user. + $this->assertRaw(t('%name has been deleted.', array('%name' => $account->name)), "Confirmation message displayed to user."); } /** @@ -756,13 +1009,13 @@ // Delete regular user. $this->drupalGet('user/' . $account->uid . '/edit'); $this->drupalPost(NULL, NULL, t('Cancel account')); - $this->assertRaw(t('Are you sure you want to cancel the account %name?', array('%name' => $account->name)), t('Confirmation form to cancel account displayed.')); - $this->assertText(t('Select the method to cancel the account above.'), t('Allows to select account cancellation method.')); + $this->assertRaw(t('Are you sure you want to cancel the account %name?', array('%name' => $account->name)), 'Confirmation form to cancel account displayed.'); + $this->assertText(t('Select the method to cancel the account above.'), 'Allows to select account cancellation method.'); // Confirm deletion. $this->drupalPost(NULL, NULL, t('Cancel account')); - $this->assertRaw(t('%name has been deleted.', array('%name' => $account->name)), t('User deleted.')); - $this->assertFalse(user_load($account->uid), t('User is not found in the database.')); + $this->assertRaw(t('%name has been deleted.', array('%name' => $account->name)), 'User deleted.'); + $this->assertFalse(user_load($account->uid), 'User is not found in the database.'); } /** @@ -794,10 +1047,10 @@ // Also try to cancel uid 1. $edit['accounts[1]'] = TRUE; $this->drupalPost('admin/people', $edit, t('Update')); - $this->assertText(t('Are you sure you want to cancel these user accounts?'), t('Confirmation form to cancel accounts displayed.')); - $this->assertText(t('When cancelling these accounts'), t('Allows to select account cancellation method.')); - $this->assertText(t('Require e-mail confirmation to cancel account.'), t('Allows to send confirmation mail.')); - $this->assertText(t('Notify user when account is canceled.'), t('Allows to send notification mail.')); + $this->assertText(t('Are you sure you want to cancel these user accounts?'), 'Confirmation form to cancel accounts displayed.'); + $this->assertText(t('When cancelling these accounts'), 'Allows to select account cancellation method.'); + $this->assertText(t('Require e-mail confirmation to cancel account.'), 'Allows to send confirmation mail.'); + $this->assertText(t('Notify user when account is canceled.'), 'Allows to send notification mail.'); // Confirm deletion. $this->drupalPost(NULL, NULL, t('Cancel accounts')); @@ -806,16 +1059,16 @@ $status = $status && (strpos($this->content, t('%name has been deleted.', array('%name' => $account->name))) !== FALSE); $status = $status && !user_load($account->uid, TRUE); } - $this->assertTrue($status, t('Users deleted and not found in the database.')); + $this->assertTrue($status, 'Users deleted and not found in the database.'); // Ensure that admin account was not cancelled. - $this->assertText(t('A confirmation request to cancel your account has been sent to your e-mail address.'), t('Account cancellation request mailed message displayed.')); + $this->assertText(t('A confirmation request to cancel your account has been sent to your e-mail address.'), 'Account cancellation request mailed message displayed.'); $admin_user = user_load($admin_user->uid); - $this->assertTrue($admin_user->status == 1, t('Administrative user is found in the database and enabled.')); + $this->assertTrue($admin_user->status == 1, 'Administrative user is found in the database and enabled.'); // Verify that uid 1's account was not cancelled. $user1 = user_load(1, TRUE); - $this->assertEqual($user1->status, 1, t('User #1 still exists and is not blocked.')); + $this->assertEqual($user1->status, 1, 'User #1 still exists and is not blocked.'); } } @@ -857,7 +1110,7 @@ // Try to upload a file that is not an image for the user picture. $not_an_image = current($this->drupalGetTestFiles('html')); $this->saveUserPicture($not_an_image); - $this->assertRaw(t('Only JPEG, PNG and GIF images are allowed.'), t('Non-image files are not accepted.')); + $this->assertRaw(t('Only JPEG, PNG and GIF images are allowed.'), 'Non-image files are not accepted.'); } /** @@ -883,13 +1136,13 @@ // Check that the image was resized and is being displayed on the // user's profile page. $text = t('The image was resized to fit within the maximum allowed dimensions of %dimensions pixels.', array('%dimensions' => $test_dim)); - $this->assertRaw($text, t('Image was resized.')); + $this->assertRaw($text, 'Image was resized.'); $alt = t("@user's picture", array('@user' => format_username($this->user))); $style = variable_get('user_picture_style', ''); - $this->assertRaw(image_style_url($style, $pic_path), t("Image is displayed in user's edit page")); + $this->assertRaw(check_plain(image_style_url($style, $pic_path)), "Image is displayed in user's edit page"); // Check if file is located in proper directory. - $this->assertTrue(is_file($pic_path), t("File is located in proper directory")); + $this->assertTrue(is_file($pic_path), "File is located in proper directory"); } } @@ -920,12 +1173,12 @@ // Test that the upload failed and that the correct reason was cited. $text = t('The specified file %filename could not be uploaded.', array('%filename' => $image->filename)); - $this->assertRaw($text, t('Upload failed.')); + $this->assertRaw($text, 'Upload failed.'); $text = t('The file is %filesize exceeding the maximum file size of %maxsize.', array('%filesize' => format_size(filesize($image->uri)), '%maxsize' => format_size($test_size * 1024))); - $this->assertRaw($text, t('File size cited as reason for failure.')); + $this->assertRaw($text, 'File size cited as reason for failure.'); // Check if file is not uploaded. - $this->assertFalse(is_file($pic_path), t('File was not uploaded.')); + $this->assertFalse(is_file($pic_path), 'File was not uploaded.'); } } @@ -952,12 +1205,12 @@ // Test that the upload failed and that the correct reason was cited. $text = t('The specified file %filename could not be uploaded.', array('%filename' => $image->filename)); - $this->assertRaw($text, t('Upload failed.')); + $this->assertRaw($text, 'Upload failed.'); $text = t('The image is too large; the maximum dimensions are %dimensions pixels.', array('%dimensions' => $test_dim)); - $this->assertRaw($text, t('Checking response on invalid image (dimensions).')); + $this->assertRaw($text, 'Checking response on invalid image (dimensions).'); // Check if file is not uploaded. - $this->assertFalse(is_file($pic_path), t('File was not uploaded.')); + $this->assertFalse(is_file($pic_path), 'File was not uploaded.'); } } @@ -985,12 +1238,12 @@ // Test that the upload failed and that the correct reason was cited. $text = t('The specified file %filename could not be uploaded.', array('%filename' => $image->filename)); - $this->assertRaw($text, t('Upload failed.')); + $this->assertRaw($text, 'Upload failed.'); $text = t('The file is %filesize exceeding the maximum file size of %maxsize.', array('%filesize' => format_size(filesize($image->uri)), '%maxsize' => format_size($test_size * 1024))); - $this->assertRaw($text, t('File size cited as reason for failure.')); + $this->assertRaw($text, 'File size cited as reason for failure.'); // Check if file is not uploaded. - $this->assertFalse(is_file($pic_path), t('File was not uploaded.')); + $this->assertFalse(is_file($pic_path), 'File was not uploaded.'); } } @@ -1016,17 +1269,28 @@ // Check if image is displayed in user's profile page. $this->drupalGet('user'); - $this->assertRaw(file_uri_target($pic_path), t("Image is displayed in user's profile page")); + $this->assertRaw(file_uri_target($pic_path), "Image is displayed in user's profile page"); // Check if file is located in proper directory. - $this->assertTrue(is_file($pic_path), t('File is located in proper directory')); + $this->assertTrue(is_file($pic_path), 'File is located in proper directory'); // Set new picture dimensions. $test_dim = ($info['width'] + 5) . 'x' . ($info['height'] + 5); variable_set('user_picture_dimensions', $test_dim); $pic_path2 = $this->saveUserPicture($image); - $this->assertNotEqual($pic_path, $pic_path2, t('Filename of second picture is different.')); + $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.'); } } @@ -1046,8 +1310,52 @@ // Get the user picture image via xpath. $elements = $this->xpath('//div[@class="user-picture"]/img'); - $this->assertEqual(count($elements), 1, t("There is exactly one user picture on the user's profile page")); - $this->assertEqual($pic_path, (string) $elements[0]['src'], t("User picture source is correct.")); + $this->assertEqual(count($elements), 1, "There is exactly one user picture on the user's profile page"); + $this->assertEqual($pic_path, (string) $elements[0]['src'], "User picture source is correct."); + } + + /** + * Tests deletion of user pictures. + */ + function testDeletePicture() { + $this->drupalLogin($this->user); + + $image = current($this->drupalGetTestFiles('image')); + $info = image_get_info($image->uri); + + // Set new variables: valid dimensions, valid filesize (0 = no limit). + $test_dim = ($info['width'] + 10) . 'x' . ($info['height'] + 10); + variable_set('user_picture_dimensions', $test_dim); + variable_set('user_picture_file_size', 0); + + // Save a new picture. + $edit = array('files[picture_upload]' => drupal_realpath($image->uri)); + $this->drupalPost('user/' . $this->user->uid . '/edit', $edit, t('Save')); + + // Load actual user data from database. + $account = user_load($this->user->uid, TRUE); + $pic_path = isset($account->picture) ? $account->picture->uri : NULL; + + // Check if image is displayed in user's profile page. + $this->drupalGet('user'); + $this->assertRaw(file_uri_target($pic_path), "Image is displayed in user's profile page"); + + // Check if file is located in proper directory. + $this->assertTrue(is_file($pic_path), 'File is located in proper directory'); + + $edit = array('picture_delete' => 1); + $this->drupalPost('user/' . $this->user->uid . '/edit', $edit, t('Save')); + + // Load actual user data from database. + $account1 = user_load($this->user->uid, TRUE); + $this->assertNull($account1->picture, 'User object has no picture'); + + $file = file_load($account->picture->fid); + $this->assertFalse($file, 'File is removed from database'); + + // Clear out PHP's file stat cache so we see the current value. + clearstatcache(); + $this->assertFalse(is_file($pic_path), 'File is removed from file system'); } function saveUserPicture($image) { @@ -1058,6 +1366,24 @@ $account = user_load($this->user->uid, TRUE); return isset($account->picture) ? $account->picture->uri : NULL; } + + /** + * Tests the admin form validates user picture settings. + */ + function testUserPictureAdminFormValidation() { + $this->drupalLogin($this->drupalCreateUser(array('administer users'))); + + // The default values are valid. + $this->drupalPost('admin/config/people/accounts', array(), t('Save configuration')); + $this->assertText(t('The configuration options have been saved.'), 'The default values are valid.'); + + // The form does not save with an invalid file size. + $edit = array( + 'user_picture_file_size' => $this->randomName(), + ); + $this->drupalPost('admin/config/people/accounts', $edit, t('Save configuration')); + $this->assertNoText(t('The configuration options have been saved.'), 'The form does not save with an invalid file size.'); + } } @@ -1093,24 +1419,24 @@ $account = $this->admin_user; // Add a permission. - $this->assertFalse(user_access('administer nodes', $account), t('User does not have "administer nodes" permission.')); + $this->assertFalse(user_access('administer nodes', $account), 'User does not have "administer nodes" permission.'); $edit = array(); $edit[$rid . '[administer nodes]'] = TRUE; $this->drupalPost('admin/people/permissions', $edit, t('Save permissions')); - $this->assertText(t('The changes have been saved.'), t('Successful save message displayed.')); + $this->assertText(t('The changes have been saved.'), 'Successful save message displayed.'); drupal_static_reset('user_access'); drupal_static_reset('user_role_permissions'); - $this->assertTrue(user_access('administer nodes', $account), t('User now has "administer nodes" permission.')); + $this->assertTrue(user_access('administer nodes', $account), 'User now has "administer nodes" permission.'); // Remove a permission. - $this->assertTrue(user_access('access user profiles', $account), t('User has "access user profiles" permission.')); + $this->assertTrue(user_access('access user profiles', $account), 'User has "access user profiles" permission.'); $edit = array(); $edit[$rid . '[access user profiles]'] = FALSE; $this->drupalPost('admin/people/permissions', $edit, t('Save permissions')); - $this->assertText(t('The changes have been saved.'), t('Successful save message displayed.')); + $this->assertText(t('The changes have been saved.'), 'Successful save message displayed.'); drupal_static_reset('user_access'); drupal_static_reset('user_role_permissions'); - $this->assertFalse(user_access('access user profiles', $account), t('User no longer has "access user profiles" permission.')); + $this->assertFalse(user_access('access user profiles', $account), 'User no longer has "access user profiles" permission.'); } /** @@ -1130,7 +1456,7 @@ $edit = array(); $edit['modules[Core][aggregator][enable]'] = TRUE; $this->drupalPost('admin/modules', $edit, t('Save configuration')); - $this->assertTrue(user_access('administer news feeds', $this->admin_user), t('The permission was automatically assigned to the administrator role')); + $this->assertTrue(user_access('administer news feeds', $this->admin_user), 'The permission was automatically assigned to the administrator role'); } /** @@ -1141,9 +1467,9 @@ $account = $this->admin_user; // Verify current permissions. - $this->assertFalse(user_access('administer nodes', $account), t('User does not have "administer nodes" permission.')); - $this->assertTrue(user_access('access user profiles', $account), t('User has "access user profiles" permission.')); - $this->assertTrue(user_access('administer site configuration', $account), t('User has "administer site configuration" permission.')); + $this->assertFalse(user_access('administer nodes', $account), 'User does not have "administer nodes" permission.'); + $this->assertTrue(user_access('access user profiles', $account), 'User has "access user profiles" permission.'); + $this->assertTrue(user_access('administer site configuration', $account), 'User has "administer site configuration" permission.'); // Change permissions. $permissions = array( @@ -1153,9 +1479,9 @@ user_role_change_permissions($rid, $permissions); // Verify proper permission changes. - $this->assertTrue(user_access('administer nodes', $account), t('User now has "administer nodes" permission.')); - $this->assertFalse(user_access('access user profiles', $account), t('User no longer has "access user profiles" permission.')); - $this->assertTrue(user_access('administer site configuration', $account), t('User still has "administer site configuration" permission.')); + $this->assertTrue(user_access('administer nodes', $account), 'User now has "administer nodes" permission.'); + $this->assertFalse(user_access('access user profiles', $account), 'User no longer has "access user profiles" permission.'); + $this->assertTrue(user_access('administer site configuration', $account), 'User still has "administer site configuration" permission.'); } } @@ -1181,14 +1507,14 @@ $admin_user = $this->drupalCreateUser(array('administer users')); $this->drupalLogin($admin_user); $this->drupalGet('admin/people'); - $this->assertText($user_a->name, t('Found user A on admin users page')); - $this->assertText($user_b->name, t('Found user B on admin users page')); - $this->assertText($user_c->name, t('Found user C on admin users page')); - $this->assertText($admin_user->name, t('Found Admin user on admin users page')); + $this->assertText($user_a->name, 'Found user A on admin users page'); + $this->assertText($user_b->name, 'Found user B on admin users page'); + $this->assertText($user_c->name, 'Found user C on admin users page'); + $this->assertText($admin_user->name, 'Found Admin user on admin users page'); // Test for existence of edit link in table. $link = l(t('edit'), "user/$user_a->uid/edit", array('query' => array('destination' => 'admin/people'))); - $this->assertRaw($link, t('Found user A edit link on admin users page')); + $this->assertRaw($link, 'Found user A edit link on admin users page'); // Filter the users by permission 'administer taxonomy'. $edit = array(); @@ -1196,18 +1522,18 @@ $this->drupalPost('admin/people', $edit, t('Filter')); // Check if the correct users show up. - $this->assertNoText($user_a->name, t('User A not on filtered by perm admin users page')); - $this->assertText($user_b->name, t('Found user B on filtered by perm admin users page')); - $this->assertText($user_c->name, t('Found user C on filtered by perm admin users page')); + $this->assertNoText($user_a->name, 'User A not on filtered by perm admin users page'); + $this->assertText($user_b->name, 'Found user B on filtered by perm admin users page'); + $this->assertText($user_c->name, 'Found user C on filtered by perm admin users page'); // Filter the users by role. Grab the system-generated role name for User C. $edit['role'] = max(array_flip($user_c->roles)); $this->drupalPost('admin/people', $edit, t('Refine')); // Check if the correct users show up when filtered by role. - $this->assertNoText($user_a->name, t('User A not on filtered by role on admin users page')); - $this->assertNoText($user_b->name, t('User B not on filtered by role on admin users page')); - $this->assertText($user_c->name, t('User C on filtered by role on admin users page')); + $this->assertNoText($user_a->name, 'User A not on filtered by role on admin users page'); + $this->assertNoText($user_b->name, 'User B not on filtered by role on admin users page'); + $this->assertText($user_c->name, 'User C on filtered by role on admin users page'); // Test blocking of a user. $account = user_load($user_c->uid); @@ -1260,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(); @@ -1278,26 +1610,26 @@ // Confirm date format and time zone. $this->drupalGet("node/$node1->nid"); - $this->assertText('2007-03-09 21:00 PST', t('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', t('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', t('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(); $edit['mail'] = $web_user->mail; $edit['timezone'] = 'America/Santiago'; $this->drupalPost("user/$web_user->uid/edit", $edit, t('Save')); - $this->assertText(t('The changes have been saved.'), t('Time zone changed to Santiago time.')); + $this->assertText(t('The changes have been saved.'), 'Time zone changed to Santiago time.'); // Confirm date format and time zone. $this->drupalGet("node/$node1->nid"); - $this->assertText('2007-03-10 02:00 CLST', t('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', t('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', t('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.'); } } @@ -1328,22 +1660,22 @@ // Check access from unprivileged user, should be denied. $this->drupalLogin($this->unprivileged_user); $this->drupalGet('user/autocomplete/' . $this->unprivileged_user->name[0]); - $this->assertResponse(403, t('Autocompletion access denied to user without permission.')); + $this->assertResponse(403, 'Autocompletion access denied to user without permission.'); // Check access from privileged user. $this->drupalLogout(); $this->drupalLogin($this->privileged_user); $this->drupalGet('user/autocomplete/' . $this->unprivileged_user->name[0]); - $this->assertResponse(200, t('Autocompletion access allowed.')); + $this->assertResponse(200, 'Autocompletion access allowed.'); // Using first letter of the user's name, make sure the user's full name is in the results. - $this->assertRaw($this->unprivileged_user->name, t('User name found in autocompletion results.')); + $this->assertRaw($this->unprivileged_user->name, 'User name found in autocompletion results.'); } } /** - * Test user-links in secondary menu. + * Tests user links in the secondary menu. */ class UserAccountLinksUnitTests extends DrupalWebTestCase { public static function getInfo() { @@ -1354,8 +1686,12 @@ ); } + function setUp() { + parent::setUp('menu'); + } + /** - * Test the user login block. + * Tests the secondary menu. */ function testSecondaryMenu() { // Create a regular user. @@ -1389,6 +1725,38 @@ $element = $this->xpath('//ul[@id=:menu_id]', array(':menu_id' => 'secondary-menu-links')); $this->assertEqual(count($element), 0, 'No secondary-menu for logged-out users.'); } + + /** + * Tests disabling the 'My account' link. + */ + function testDisabledAccountLink() { + // Create an admin user and log in. + $this->drupalLogin($this->drupalCreateUser(array('access administration pages', 'administer menu'))); + + // Verify that the 'My account' link is enabled. + $this->drupalGet('admin/structure/menu/manage/user-menu'); + $label = $this->xpath('//label[contains(.,:text)]/@for', array(':text' => 'Enable My account menu link')); + $this->assertFieldChecked((string) $label[0], "The 'My account' link is enabled by default."); + + // Disable the 'My account' link. + $input = $this->xpath('//input[@id=:field_id]/@name', array(':field_id' => (string)$label[0])); + $edit = array( + (string) $input[0] => FALSE, + ); + $this->drupalPost('admin/structure/menu/manage/user-menu', $edit, t('Save configuration')); + + // Get the homepage. + $this->drupalGet(''); + + // Verify that the 'My account' link does not appear when disabled. + $link = $this->xpath('//ul[@id=:menu_id]/li/a[contains(@href, :href) and text()=:text]', array( + ':menu_id' => 'secondary-menu-links', + ':href' => 'user', + ':text' => 'My account', + )); + $this->assertEqual(count($link), 0, 'My account link is not in the secondary menu.'); + } + } /** @@ -1415,16 +1783,23 @@ $edit['name'] = $user->name; $edit['pass'] = $user->pass_raw; $this->drupalPost('admin/people/permissions', $edit, t('Log in')); - $this->assertNoText(t('User login'), t('Logged in.')); + $this->assertNoText(t('User login'), 'Logged in.'); // Check that we are still on the same page. - $this->assertEqual(url('admin/people/permissions', array('absolute' => TRUE)), $this->getUrl(), t('Still on the same page after login for access denied page')); + $this->assertEqual(url('admin/people/permissions', array('absolute' => TRUE)), $this->getUrl(), 'Still on the same page after login for access denied page'); // Now, log out and repeat with a non-403 page. $this->drupalLogout(); $this->drupalPost('filter/tips', $edit, t('Log in')); - $this->assertNoText(t('User login'), t('Logged in.')); - $this->assertPattern('!!', t('Still on the same page after login for allowed page')); + $this->assertNoText(t('User login'), 'Logged in.'); + $this->assertPattern('!!', 'Still on the same page after login for allowed page'); + + // Check that the user login block is not vulnerable to information + // disclosure to third party sites. + $this->drupalLogout(); + $this->drupalPost('http://example.com/', $edit, t('Log in'), array('external' => FALSE)); + // Check that we remain on the site after login. + $this->assertEqual(url('user/' . $user->uid, array('absolute' => TRUE)), $this->getUrl(), 'Redirected to user profile page after login from the frontpage'); } /** @@ -1435,12 +1810,12 @@ $user1 = $this->drupalCreateUser(array()); $user2 = $this->drupalCreateUser(array()); $user3 = $this->drupalCreateUser(array()); - $this->assertEqual(db_query("SELECT COUNT(*) FROM {sessions}")->fetchField(), 0, t('Sessions table is empty.')); + $this->assertEqual(db_query("SELECT COUNT(*) FROM {sessions}")->fetchField(), 0, 'Sessions table is empty.'); // Insert a user with two sessions. $this->insertSession(array('uid' => $user1->uid)); $this->insertSession(array('uid' => $user1->uid)); - $this->assertEqual(db_query("SELECT COUNT(*) FROM {sessions} WHERE uid = :uid", array(':uid' => $user1->uid))->fetchField(), 2, t('Duplicate user session has been inserted.')); + $this->assertEqual(db_query("SELECT COUNT(*) FROM {sessions} WHERE uid = :uid", array(':uid' => $user1->uid))->fetchField(), 2, 'Duplicate user session has been inserted.'); // Insert a user with only one session. $this->insertSession(array('uid' => $user2->uid, 'timestamp' => REQUEST_TIME + 1)); @@ -1455,11 +1830,11 @@ // Test block output. $block = user_block_view('online'); $this->drupalSetContent($block['content']); - $this->assertRaw(t('2 users'), t('Correct number of online users (2 users).')); - $this->assertText($user1->name, t('Active user 1 found in online list.')); - $this->assertText($user2->name, t('Active user 2 found in online list.')); - $this->assertNoText($user3->name, t("Inactive user not found in online list.")); - $this->assertTrue(strpos($this->drupalGetContent(), $user1->name) > strpos($this->drupalGetContent(), $user2->name), t('Online users are ordered correctly.')); + $this->assertRaw(t('2 users'), 'Correct number of online users (2 users).'); + $this->assertText($user1->name, 'Active user 1 found in online list.'); + $this->assertText($user2->name, 'Active user 2 found in online list.'); + $this->assertNoText($user3->name, "Inactive user not found in online list."); + $this->assertTrue(strpos($this->drupalGetContent(), $user1->name) > strpos($this->drupalGetContent(), $user2->name), 'Online users are ordered correctly.'); } /** @@ -1475,12 +1850,12 @@ db_insert('sessions') ->fields($fields) ->execute(); - $this->assertEqual(db_query("SELECT COUNT(*) FROM {sessions} WHERE uid = :uid AND sid = :sid AND timestamp = :timestamp", array(':uid' => $fields['uid'], ':sid' => $fields['sid'], ':timestamp' => $fields['timestamp']))->fetchField(), 1, t('Session record inserted.')); + $this->assertEqual(db_query("SELECT COUNT(*) FROM {sessions} WHERE uid = :uid AND sid = :sid AND timestamp = :timestamp", array(':uid' => $fields['uid'], ':sid' => $fields['sid'], ':timestamp' => $fields['timestamp']))->fetchField(), 1, 'Session record inserted.'); } } /** - * Test case to test user_save() behaviour. + * Tests saving a user account. */ class UserSaveTestCase extends DrupalWebTestCase { @@ -1511,14 +1886,14 @@ 'status' => 1, ); $user_by_return = user_save(drupal_anonymous_user(), $user); - $this->assertTrue($user_by_return, t('Loading user by return of user_save().')); + $this->assertTrue($user_by_return, 'Loading user by return of user_save().'); // Test if created user exists. $user_by_uid = user_load($test_uid); - $this->assertTrue($user_by_uid, t('Loading user by uid.')); + $this->assertTrue($user_by_uid, 'Loading user by uid.'); $user_by_name = user_load_by_name($test_name); - $this->assertTrue($user_by_name, t('Loading user by name.')); + $this->assertTrue($user_by_name, 'Loading user by name.'); } } @@ -1565,11 +1940,24 @@ $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'); } } /** - * Test case to test user_save() behaviour. + * Tests editing a user account. */ class UserEditTestCase extends DrupalWebTestCase { @@ -1606,12 +1994,12 @@ $edit['pass[pass1]'] = ''; $edit['pass[pass2]'] = $this->randomName(); $this->drupalPost("user/$user1->uid/edit", $edit, t('Save')); - $this->assertText(t("The specified passwords do not match."), t('Typing mismatched passwords displays an error message.')); + $this->assertText(t("The specified passwords do not match."), 'Typing mismatched passwords displays an error message.'); $edit['pass[pass1]'] = $this->randomName(); $edit['pass[pass2]'] = ''; $this->drupalPost("user/$user1->uid/edit", $edit, t('Save')); - $this->assertText(t("The specified passwords do not match."), t('Typing mismatched passwords displays an error message.')); + $this->assertText(t("The specified passwords do not match."), 'Typing mismatched passwords displays an error message.'); // Test that the error message appears when attempting to change the mail or // pass without the current password. @@ -1642,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)))); + } } /** @@ -1794,36 +2250,40 @@ $role_name = '123'; $edit = array('name' => $role_name); $this->drupalPost('admin/people/permissions/roles', $edit, t('Add role')); - $this->assertText(t('The role has been added.'), t('The role has been added.')); + $this->assertText(t('The role has been added.'), 'The role has been added.'); $role = user_role_load_by_name($role_name); - $this->assertTrue(is_object($role), t('The role was successfully retrieved from the database.')); + $this->assertTrue(is_object($role), 'The role was successfully retrieved from the database.'); // Try adding a duplicate role. $this->drupalPost(NULL, $edit, t('Add role')); - $this->assertRaw(t('The role name %name already exists. Choose another role name.', array('%name' => $role_name)), t('Duplicate role warning displayed.')); + $this->assertRaw(t('The role name %name already exists. Choose another role name.', array('%name' => $role_name)), 'Duplicate role warning displayed.'); // Test renaming a role. $old_name = $role_name; $role_name = '456'; $edit = array('name' => $role_name); $this->drupalPost("admin/people/permissions/roles/edit/{$role->rid}", $edit, t('Save role')); - $this->assertText(t('The role has been renamed.'), t('The role has been renamed.')); - $this->assertFalse(user_role_load_by_name($old_name), t('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)), t('The role can be retrieved from the database using its new name.')); + $this->assertText(t('The role has been renamed.'), 'The role has been renamed.'); + $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.'), t('The role has been deleted')); - $this->assertNoLinkByHref("admin/people/permissions/roles/edit/{$role->rid}", t('Role edit link removed.')); - $this->assertFalse(user_role_load_by_name($role_name), t('A deleted role can no longer be loaded.')); + $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. $this->drupalGet('admin/people/permissions/roles/edit/' . DRUPAL_ANONYMOUS_RID); - $this->assertResponse(403, t('Access denied when trying to edit the built-in anonymous role.')); + $this->assertResponse(403, 'Access denied when trying to edit the built-in anonymous role.'); $this->drupalGet('admin/people/permissions/roles/edit/' . DRUPAL_AUTHENTICATED_RID); - $this->assertResponse(403, t('Access denied when trying to edit the built-in authenticated role.')); + $this->assertResponse(403, 'Access denied when trying to edit the built-in authenticated role.'); } /** @@ -1840,12 +2300,12 @@ // Change the role weight and submit the form. $edit = array('roles['. $rid .'][weight]' => $old_weight + 1); $this->drupalPost('admin/people/permissions/roles', $edit, t('Save order')); - $this->assertText(t('The role settings have been updated.'), t('The role settings form submitted successfully.')); + $this->assertText(t('The role settings have been updated.'), 'The role settings form submitted successfully.'); // Retrieve the saved role and compare its weight. $role = user_role_load($rid); $new_weight = $role->weight; - $this->assertTrue(($old_weight + 1) == $new_weight, t('Role weight updated successfully.')); + $this->assertTrue(($old_weight + 1) == $new_weight, 'Role weight updated successfully.'); } } @@ -1895,11 +2355,11 @@ $tests['[current-user:name]'] = check_plain(format_username($global_account)); // Test to make sure that we generated something for each token. - $this->assertFalse(in_array(0, array_map('strlen', $tests)), t('No empty tokens generated.')); + $this->assertFalse(in_array(0, array_map('strlen', $tests)), 'No empty tokens generated.'); foreach ($tests as $input => $expected) { $output = token_replace($input, array('user' => $account), array('language' => $language)); - $this->assertFalse(strcmp($output, $expected), t('Sanitized user token %token replaced.', array('%token' => $input))); + $this->assertEqual($output, $expected, format_string('Sanitized user token %token replaced.', array('%token' => $input))); } // Generate and test unsanitized tokens. @@ -1909,7 +2369,7 @@ foreach ($tests as $input => $expected) { $output = token_replace($input, array('user' => $account), array('language' => $language, 'sanitize' => FALSE)); - $this->assertFalse(strcmp($output, $expected), t('Unsanitized user token %token replaced.', array('%token' => $input))); + $this->assertEqual($output, $expected, format_string('Unsanitized user token %token replaced.', array('%token' => $input))); } } } @@ -1921,13 +2381,19 @@ public static function getInfo() { return array( 'name' => 'User search', - 'description' => 'Testing that only user with the right permission can see the email address in the user search.', + 'description' => 'Tests the user search page and verifies that sensitive information is hidden from unauthorized users.', 'group' => 'User', ); } 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); @@ -1941,11 +2407,43 @@ $edit = array('keys' => $keys); $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); + $blocked_user = user_save($blocked_user, $edit); + + // Verify that users with "administer users" permissions can see blocked + // accounts in search results. + $edit = array('keys' => $blocked_user->name); + $this->drupalPost('search/user/', $edit, t('Search')); + $this->assertText($blocked_user->name, 'Blocked users are listed on the user search results for users with the "administer users" permission.'); + + // Verify that users without "administer users" permissions do not see + // blocked accounts in search results. + $this->drupalLogin($user1); + $edit = array('keys' => $blocked_user->name); + $this->drupalPost('search/user/', $edit, t('Search')); + $this->assertNoText($blocked_user->name, 'Blocked users are hidden from the user search results.'); + $this->drupalLogout(); } } - /** * Test role assignment. */ @@ -1954,9 +2452,9 @@ public static function getInfo() { return array( - 'name' => t('Role assignment'), - 'description' => t('Tests that users can be assigned and unassigned roles.'), - 'group' => t('User') + 'name' => 'Role assignment', + 'description' => 'Tests that users can be assigned and unassigned roles.', + 'group' => 'User' ); } @@ -1977,13 +2475,13 @@ // Assign the role to the user. $this->drupalPost('user/' . $account->uid . '/edit', array("roles[$rid]" => $rid), t('Save')); $this->assertText(t('The changes have been saved.')); - $this->assertFieldChecked('edit-roles-' . $rid, t('Role is assigned.')); + $this->assertFieldChecked('edit-roles-' . $rid, 'Role is assigned.'); $this->userLoadAndCheckRoleAssigned($account, $rid); // Remove the role from the user. $this->drupalPost('user/' . $account->uid . '/edit', array("roles[$rid]" => FALSE), t('Save')); $this->assertText(t('The changes have been saved.')); - $this->assertNoFieldChecked('edit-roles-' . $rid, t('Role is removed from user.')); + $this->assertNoFieldChecked('edit-roles-' . $rid, 'Role is removed from user.'); $this->userLoadAndCheckRoleAssigned($account, $rid, FALSE); } @@ -2007,30 +2505,34 @@ $account = user_load_by_name($edit['name']); $this->drupalGet('user/' . $account->uid . '/edit'); - $this->assertFieldChecked('edit-roles-' . $rid, t('Role is assigned.')); + $this->assertFieldChecked('edit-roles-' . $rid, 'Role is assigned.'); $this->userLoadAndCheckRoleAssigned($account, $rid); // Remove the role again. $this->drupalPost('user/' . $account->uid . '/edit', array("roles[$rid]" => FALSE), t('Save')); $this->assertText(t('The changes have been saved.')); - $this->assertNoFieldChecked('edit-roles-' . $rid, t('Role is removed from user.')); + $this->assertNoFieldChecked('edit-roles-' . $rid, 'Role is removed from user.'); $this->userLoadAndCheckRoleAssigned($account, $rid, FALSE); } /** * Check role on user object. * - * @param object $account User. - * @param integer $rid Role id. - * @param bool $is_assigned True if the role should present on the account. + * @param object $account + * The user account to check. + * @param string $rid + * The role ID to search for. + * @param bool $is_assigned + * (optional) Whether to assert that $rid exists (TRUE) or not (FALSE). + * Defaults to TRUE. */ private function userLoadAndCheckRoleAssigned($account, $rid, $is_assigned = TRUE) { $account = user_load($account->uid, TRUE); if ($is_assigned) { - $this->assertTrue(array_key_exists($rid, $account->roles), t('The role is present in the user object.')); + $this->assertTrue(array_key_exists($rid, $account->roles), 'The role is present in the user object.'); } else { - $this->assertFalse(array_key_exists($rid, $account->roles), t('The role is not present in the user object.')); + $this->assertFalse(array_key_exists($rid, $account->roles), 'The role is not present in the user object.'); } } } @@ -2042,9 +2544,9 @@ class UserAuthmapAssignmentTestCase extends DrupalWebTestCase { public static function getInfo() { return array( - 'name' => t('Authmap assignment'), - 'description' => t('Tests that users can be assigned and unassigned authmaps.'), - 'group' => t('User') + 'name' => 'Authmap assignment', + 'description' => 'Tests that users can be assigned and unassigned authmaps.', + 'group' => 'User' ); } @@ -2071,7 +2573,7 @@ ), ); foreach ($expected_authmaps as $authname => $expected_output) { - $this->assertIdentical(user_get_authmaps($authname), $expected_output, t('Authmap for authname %authname was set correctly.', array('%authname' => $authname))); + $this->assertIdentical(user_get_authmaps($authname), $expected_output, format_string('Authmap for authname %authname was set correctly.', array('%authname' => $authname))); } // Remove authmap for module poll, add authmap for module blog. @@ -2084,13 +2586,13 @@ // Assert that external username one does not have authmaps. $remove_username = 'external username one'; unset($expected_authmaps[$remove_username]); - $this->assertFalse(user_get_authmaps($remove_username), t('Authmap for %authname was removed.', array('%authname' => $remove_username))); + $this->assertFalse(user_get_authmaps($remove_username), format_string('Authmap for %authname was removed.', array('%authname' => $remove_username))); // Assert that a new authmap was created for external username three, and // existing authmaps for external username two were unchanged. $expected_authmaps['external username three'] = array('blog' => 'external username three'); foreach ($expected_authmaps as $authname => $expected_output) { - $this->assertIdentical(user_get_authmaps($authname), $expected_output, t('Authmap for authname %authname was set correctly.', array('%authname' => $authname))); + $this->assertIdentical(user_get_authmaps($authname), $expected_output, format_string('Authmap for authname %authname was set correctly.', array('%authname' => $authname))); } } } diff -Naur drupal-7.0/modules/user/user.tokens.inc drupal-7.66/modules/user/user.tokens.inc --- drupal-7.0/modules/user/user.tokens.inc 2010-12-09 09:01:56.000000000 +0100 +++ drupal-7.66/modules/user/user.tokens.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ 'system', 'delta' => 'main', @@ -189,8 +194,8 @@ ), ); $query = db_insert('block')->fields(array('module', 'delta', 'theme', 'status', 'weight', 'region', 'pages', 'cache')); - foreach ($values as $record) { - $query->values($record); + foreach ($blocks as $block) { + $query->values($block); } $query->execute(); @@ -270,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' => 'Tags', + 'name' => st('Tags'), 'description' => $description, 'machine_name' => 'tags', - 'help' => $help, - ); taxonomy_vocabulary_save($vocabulary); @@ -296,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' => $vocabulary->name, + 'label' => 'Tags', 'bundle' => 'article', - 'description' => $vocabulary->help, + 'description' => $help, 'widget' => array( 'type' => 'taxonomy_autocomplete', 'weight' => -4, @@ -328,7 +331,6 @@ 'field_name' => 'field_image', 'type' => 'image', 'cardinality' => 1, - 'translatable' => TRUE, 'locked' => FALSE, 'indexes' => array('fid' => array('fid')), 'settings' => array( diff -Naur drupal-7.0/profiles/standard/standard.profile drupal-7.66/profiles/standard/standard.profile --- drupal-7.0/profiles/standard/standard.profile 2010-07-22 18:16:42.000000000 +0200 +++ drupal-7.66/profiles/standard/standard.profile 2019-04-17 22:20:46.000000000 +0200 @@ -1,8 +1,11 @@ 'Installation profile module tests helper', + 'description' => 'Verifies that tests in installation profile modules are found and may use another profile for running tests.', + 'group' => 'Installation profile', + ); + } + + function setUp() { + // Attempt to install a module in Testing profile, while this test runs with + // a different profile. + parent::setUp(array('drupal_system_listing_compatible_test')); + } + + /** + * Non-empty test* method required to executed the test case class. + */ + function testDrupalSystemListing() { + $this->pass(__CLASS__ . ' test executed.'); + } +} diff -Naur drupal-7.0/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.0/profiles/testing/modules/drupal_system_listing_incompatible_test/drupal_system_listing_incompatible_test.info 2011-01-05 07:25:56.000000000 +0100 +++ 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 @@ -1,4 +1,3 @@ -; $Id: drupal_system_listing_incompatible_test.info,v 1.2 2010/12/21 16:31:47 webchick Exp $ name = "Drupal system listing incompatible test" description = "Support module for testing the drupal_system_listing function." package = Testing @@ -9,8 +8,7 @@ core = 6.x hidden = TRUE -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/profiles/testing/modules/drupal_system_listing_incompatible_test/drupal_system_listing_incompatible_test.module drupal-7.66/profiles/testing/modules/drupal_system_listing_incompatible_test/drupal_system_listing_incompatible_test.module --- drupal-7.0/profiles/testing/modules/drupal_system_listing_incompatible_test/drupal_system_listing_incompatible_test.module 2010-11-15 01:37:08.000000000 +0100 +++ drupal-7.66/profiles/testing/modules/drupal_system_listing_incompatible_test/drupal_system_listing_incompatible_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,2 +1 @@ /dev/null 2>&1 diff -Naur drupal-7.0/scripts/drupal.sh drupal-7.66/scripts/drupal.sh --- drupal-7.0/scripts/drupal.sh 2009-09-19 12:38:47.000000000 +0200 +++ drupal-7.66/scripts/drupal.sh 2019-04-17 22:20:46.000000000 +0200 @@ -1,6 +1,5 @@ #!/usr/bin/env php $data) { + // Remove descriptions to save time and code. + unset($data['description']); + foreach ($data['fields'] as &$field) { + unset($field['description']); + } + + // Dump the table structure. + $output .= "db_create_table('" . $table . "', " . drupal_var_export($data) . ");\n"; + + // Don't output values for those tables. + if (substr($table, 0, 5) == 'cache' || $table == 'sessions' || $table == 'watchdog') { + $output .= "\n"; + continue; + } + + // Prepare the export of values. + $result = db_query('SELECT * FROM {'. $table .'}', array(), array('fetch' => PDO::FETCH_ASSOC)); + $insert = ''; + foreach ($result as $record) { + $insert .= '->values('. drupal_var_export($record) .")\n"; + } + + // Dump the values if there are some. + if ($insert) { + $output .= "db_insert('". $table . "')->fields(". drupal_var_export(array_keys($data['fields'])) .")\n"; + $output .= $insert; + $output .= "->execute();\n"; + } + + $output .= "\n"; +} + +print $output; diff -Naur drupal-7.0/scripts/generate-d6-content.sh drupal-7.66/scripts/generate-d6-content.sh --- drupal-7.0/scripts/generate-d6-content.sh 2010-09-11 02:39:49.000000000 +0200 +++ drupal-7.66/scripts/generate-d6-content.sh 2019-04-17 22:20:46.000000000 +0200 @@ -1,6 +1,5 @@ #!/usr/bin/env php 11 ? array('page' => TRUE) : array(); $vocabulary['multiple'] = $multiple[$i % 12]; $vocabulary['required'] = $required[$i % 12]; @@ -76,7 +76,7 @@ $vocabulary['weight'] = $i; taxonomy_save_vocabulary($vocabulary); $parents = array(); - // Vocabularies without hierarcy get one term, single parent vocabularies get + // Vocabularies without hierarchy get one term, single parent vocabularies get // one parent and one child term. Multiple parent vocabularies get three // terms: t0, t1, t2 where t0 is a parent of both t1 and t2. for ($j = 0; $j < $vocabulary['hierarchy'] + 1; $j++) { @@ -101,7 +101,7 @@ for ($i = 0; $i < 24; $i++) { $uid = intval($i / 8) + 3; $user = user_load($uid); - $node = new stdClass; + $node = new stdClass(); $node->uid = $uid; $node->type = $i < 12 ? 'page' : 'story'; $node->sticky = 0; @@ -149,7 +149,7 @@ for ($i = 0; $i < 12; $i++) { $uid = intval($i / 4) + 3; $user = user_load($uid); - $node = new stdClass; + $node = new stdClass(); $node->uid = $uid; $node->type = 'poll'; $node->sticky = 0; @@ -188,7 +188,7 @@ $uid = 6; $user = user_load($uid); -$node = new stdClass; +$node = new stdClass(); $node->uid = $uid; $node->type = 'broken'; $node->sticky = 0; diff -Naur drupal-7.0/scripts/generate-d7-content.sh drupal-7.66/scripts/generate-d7-content.sh --- drupal-7.0/scripts/generate-d7-content.sh 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/scripts/generate-d7-content.sh 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,320 @@ +#!/usr/bin/env php +fields(array('uid', 'name', 'pass', 'mail', 'status', 'created', 'access')); +for ($i = 0; $i < 6; $i++) { + $name = "test user $i"; + $pass = md5("test PassW0rd $i !(.)"); + $mail = "test$i@example.com"; + $now = mktime(0, 0, 0, 1, $i + 1, 2010); + $query->values(array(db_next_id(), $name, user_hash_password($pass), $mail, 1, $now, $now)); +} +$query->execute(); + +// Create vocabularies and terms. + +if (module_exists('taxonomy')) { + $terms = array(); + + // All possible combinations of these vocabulary properties. + $hierarchy = array(0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2); + $multiple = array(0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1); + $required = array(0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1); + + $voc_id = 0; + $term_id = 0; + for ($i = 0; $i < 24; $i++) { + $vocabulary = new stdClass; + ++$voc_id; + $vocabulary->name = "vocabulary $voc_id (i=$i)"; + $vocabulary->machine_name = 'vocabulary_' . $voc_id . '_' . $i; + $vocabulary->description = "description of ". $vocabulary->name; + $vocabulary->multiple = $multiple[$i % 12]; + $vocabulary->required = $required[$i % 12]; + $vocabulary->relations = 1; + $vocabulary->hierarchy = $hierarchy[$i % 12]; + $vocabulary->weight = $i; + taxonomy_vocabulary_save($vocabulary); + $field = array( + 'field_name' => 'taxonomy_'. $vocabulary->machine_name, + 'module' => 'taxonomy', + 'type' => 'taxonomy_term_reference', + 'cardinality' => $vocabulary->multiple || $vocabulary->tags ? FIELD_CARDINALITY_UNLIMITED : 1, + 'settings' => array( + 'required' => $vocabulary->required ? TRUE : FALSE, + 'allowed_values' => array( + array( + 'vocabulary' => $vocabulary->machine_name, + 'parent' => 0, + ), + ), + ), + ); + field_create_field($field); + $node_types = $i > 11 ? array('page') : array_keys(node_type_get_types()); + foreach ($node_types as $bundle) { + $instance = array( + 'label' => $vocabulary->name, + 'field_name' => $field['field_name'], + 'bundle' => $bundle, + 'entity_type' => 'node', + 'settings' => array(), + 'description' => $vocabulary->help, + 'required' => $vocabulary->required, + 'widget' => array(), + 'display' => array( + 'default' => array( + 'type' => 'taxonomy_term_reference_link', + 'weight' => 10, + ), + 'teaser' => array( + 'type' => 'taxonomy_term_reference_link', + 'weight' => 10, + ), + ), + ); + if ($vocabulary->tags) { + $instance['widget'] = array( + 'type' => 'taxonomy_autocomplete', + 'module' => 'taxonomy', + 'settings' => array( + 'size' => 60, + 'autocomplete_path' => 'taxonomy/autocomplete', + ), + ); + } + else { + $instance['widget'] = array( + 'type' => 'options_select', + 'settings' => array(), + ); + } + field_create_instance($instance); + } + $parents = array(); + // Vocabularies without hierarchy get one term; single parent vocabularies + // get one parent and one child term. Multiple parent vocabularies get + // three terms: t0, t1, t2 where t0 is a parent of both t1 and t2. + for ($j = 0; $j < $vocabulary->hierarchy + 1; $j++) { + $term = new stdClass; + $term->vocabulary_machine_name = $vocabulary->machine_name; + // For multiple parent vocabularies, omit the t0-t1 relation, otherwise + // every parent in the vocabulary is a parent. + $term->parent = $vocabulary->hierarchy == 2 && i == 1 ? array() : $parents; + ++$term_id; + $term->name = "term $term_id of vocabulary $voc_id (j=$j)"; + $term->description = 'description of ' . $term->name; + $term->format = 'filtered_html'; + $term->weight = $i * 3 + $j; + taxonomy_term_save($term); + $terms[] = $term->tid; + $term_vocabs[$term->tid] = 'taxonomy_' . $vocabulary->machine_name; + $parents[] = $term->tid; + } + } +} + +$node_id = 0; +$revision_id = 0; +module_load_include('inc', 'node', 'node.pages'); +for ($i = 0; $i < 24; $i++) { + $uid = intval($i / 8) + 3; + $user = user_load($uid); + $node = new stdClass(); + $node->uid = $uid; + $node->type = $i < 12 ? 'page' : 'story'; + $node->sticky = 0; + ++$node_id; + ++$revision_id; + $node->title = "node title $node_id rev $revision_id (i=$i)"; + $node->language = LANGUAGE_NONE; + $body_text = str_repeat("node body ($node->type) - $i", 100); + $node->body[$node->language][0]['value'] = $body_text; + $node->body[$node->language][0]['summary'] = text_summary($body_text); + $node->body[$node->language][0]['format'] = 'filtered_html'; + $node->status = intval($i / 4) % 2; + $node->revision = $i < 12; + $node->promote = $i % 2; + $node->created = $now + $i * 86400; + $node->log = "added $i node"; + // Make every term association different a little. For nodes with revisions, + // make the initial revision have a different set of terms than the + // newest revision. + $items = array(); + if (module_exists('taxonomy')) { + if ($node->revision) { + $node_terms = array($terms[$i], $terms[47-$i]); + } + else { + $node_terms = $terms; + unset($node_terms[$i], $node_terms[47 - $i]); + } + foreach ($node_terms as $tid) { + $field_name = $term_vocabs[$tid]; + $node->{$field_name}[LANGUAGE_NONE][] = array('tid' => $tid); + } + } + $node->path = array('alias' => "content/$node->created"); + node_save($node); + if ($node->revision) { + $user = user_load($uid + 3); + ++$revision_id; + $node->title .= " rev2 $revision_id"; + $body_text = str_repeat("node revision body ($node->type) - $i", 100); + $node->body[$node->language][0]['value'] = $body_text; + $node->body[$node->language][0]['summary'] = text_summary($body_text); + $node->body[$node->language][0]['format'] = 'filtered_html'; + $node->log = "added $i revision"; + $node_terms = $terms; + unset($node_terms[$i], $node_terms[47 - $i]); + foreach ($node_terms as $tid) { + $field_name = $term_vocabs[$tid]; + $node->{$field_name}[LANGUAGE_NONE][] = array('tid' => $tid); + } + node_save($node); + } +} + +if (module_exists('poll')) { + // Create poll content. + for ($i = 0; $i < 12; $i++) { + $uid = intval($i / 4) + 3; + $user = user_load($uid); + $node = new stdClass(); + $node->uid = $uid; + $node->type = 'poll'; + $node->sticky = 0; + $node->title = "poll title $i"; + $node->language = LANGUAGE_NONE; + $node->status = intval($i / 2) % 2; + $node->revision = 1; + $node->promote = $i % 2; + $node->created = REQUEST_TIME + $i * 43200; + $node->runtime = 0; + $node->active = 1; + $node->log = "added $i poll"; + $node->path = array('alias' => "content/poll/$i"); + + $nbchoices = ($i % 4) + 2; + for ($c = 0; $c < $nbchoices; $c++) { + $node->choice[] = array('chtext' => "Choice $c for poll $i", 'chvotes' => 0, 'weight' => 0); + } + node_save($node); + $path = array( + 'alias' => "content/poll/$i/results", + 'source' => "node/$node->nid/results", + ); + path_save($path); + + // Add some votes. + $node = node_load($node->nid); + $choices = array_keys($node->choice); + $original_user = $GLOBALS['user']; + for ($v = 0; $v < ($i % 4); $v++) { + drupal_static_reset('ip_address'); + $_SERVER['REMOTE_ADDR'] = "127.0.$v.1"; + $GLOBALS['user'] = drupal_anonymous_user();// We should have already allowed anon to vote. + $c = $v % $nbchoices; + $form_state = array(); + $form_state['values']['choice'] = $choices[$c]; + $form_state['values']['op'] = t('Vote'); + drupal_form_submit('poll_view_voting', $form_state, $node); + } + } +} + +// Test that upgrade works even on a bundle whose parent module was disabled. +// This is simulated by creating an existing content type and changing the +// bundle to another type through direct database update queries. +$node_type = 'broken'; +$uid = 6; +$user = user_load($uid); +$node = new stdClass(); +$node->uid = $uid; +$node->type = 'article'; +$body_text = str_repeat("node body ($node_type) - 37", 100); +$node->sticky = 0; +$node->title = "node title 24"; +$node->language = LANGUAGE_NONE; +$node->body[$node->language][0]['value'] = $body_text; +$node->body[$node->language][0]['summary'] = text_summary($body_text); +$node->body[$node->language][0]['format'] = 'filtered_html'; +$node->status = 1; +$node->revision = 0; +$node->promote = 0; +$node->created = 1263769200; +$node->log = "added a broken node"; +$node->path = array('alias' => "content/1263769200"); +node_save($node); +db_update('node') + ->fields(array( + 'type' => $node_type, + )) + ->condition('nid', $node->nid) + ->execute(); +if (db_table_exists('field_data_body')) { + db_update('field_data_body') + ->fields(array( + 'bundle' => $node_type, + )) + ->condition('entity_id', $node->nid) + ->condition('entity_type', 'node') + ->execute(); + db_update('field_revision_body') + ->fields(array( + 'bundle' => $node_type, + )) + ->condition('entity_id', $node->nid) + ->condition('entity_type', 'node') + ->execute(); +} +db_update('field_config_instance') + ->fields(array( + 'bundle' => $node_type, + )) + ->condition('bundle', 'article') + ->execute(); diff -Naur drupal-7.0/scripts/password-hash.sh drupal-7.66/scripts/password-hash.sh --- drupal-7.0/scripts/password-hash.sh 2010-05-01 10:12:23.000000000 +0200 +++ drupal-7.66/scripts/password-hash.sh 2019-04-17 22:20:46.000000000 +0200 @@ -1,6 +1,5 @@ -#!/usr/bin/php +#!/usr/bin/env php useDefaults(array('test_id'))->execute(); // Execute tests. -simpletest_script_command($args['concurrency'], $test_id, implode(",", $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,6 +103,9 @@ // Cleanup our test results. simpletest_clean_results_table($test_id); +// Test complete, exit. +exit($status); + /** * Print help text. */ @@ -122,7 +128,7 @@ --clean Cleans up database tables or directories from previous, failed, tests and then exits (no tests are run). - --url Immediately preceeds a URL to set the host and path. You will + --url Immediately precedes a URL to set the host and path. You will need this parameter if Drupal is in a subdirectory on your localhost and you have not set \$base_url in settings.php. Tests can be run under SSL by including https:// in the URL. @@ -131,9 +137,7 @@ --concurrency [num] - Run tests in parallel, up to [num] tests at a time. This requires - the Process Control Extension (PCNTL) to be compiled in PHP, not - supported under Windows. + Run tests in parallel, up to [num] tests at a time. --all Run all available tests. @@ -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. @@ -167,7 +173,7 @@ sudo -u [wwwrun|www-data|etc] php ./scripts/{$args['script']} --url http://example.com/ --all sudo -u [wwwrun|www-data|etc] php ./scripts/{$args['script']} - --url http://example.com/ --class UploadTestCase + --url http://example.com/ --class BlockTestCase \n EOF; } @@ -190,12 +196,13 @@ 'all' => FALSE, 'class' => FALSE, 'file' => FALSE, + 'directory' => '', 'color' => FALSE, 'verbose' => FALSE, 'test_names' => array(), // Used internally. - 'test-id' => NULL, - 'execute-batch' => FALSE, + 'test-id' => 0, + 'execute-test' => '', 'xml' => '', ); @@ -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,11 +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; - } - elseif ($args['concurrency'] > 1 && !function_exists('pcntl_fork')) { - simpletest_script_print_error("Parallel test execution requires the Process Control extension to be compiled in PHP. See http://php.net/manual/en/intro.pcntl.php for more information."); - exit; + exit(SIMPLETEST_SCRIPT_EXIT_FAILURE); } return array($args, $count); @@ -257,29 +260,29 @@ if (!empty($args['php'])) { $php = $args['php']; } - elseif (!empty($_ENV['_'])) { + elseif ($php_env = getenv('_')) { // '_' is an environment variable set by the shell. It contains the command that was executed. - $php = $_ENV['_']; + $php = $php_env; } - elseif (!empty($_ENV['SUDO_COMMAND'])) { + elseif ($sudo = getenv('SUDO_COMMAND')) { // 'SUDO_COMMAND' is an environment variable set by the sudo program. // Extract only the PHP interpreter, not the rest of the command. - list($php, ) = explode(' ', $_ENV['SUDO_COMMAND'], 2); + list($php, ) = explode(' ', $sudo, 2); } 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. + // Get URL from arguments. if (!empty($args['url'])) { $parsed_url = parse_url($args['url']); $host = $parsed_url['host'] . (isset($parsed_url['port']) ? ':' . $parsed_url['port'] : ''); $path = isset($parsed_url['path']) ? $parsed_url['path'] : ''; // If the passed URL schema is 'https' then setup the $_SERVER variables - // properly so that testing will run under https. + // properly so that testing will run under HTTPS. if ($parsed_url['scheme'] == 'https') { $_SERVER['HTTPS'] = 'on'; } @@ -311,93 +314,112 @@ /** * Execute a batch of tests. */ -function simpletest_script_execute_batch() { +function simpletest_script_execute_batch($test_id, $test_classes) { global $args; - if (!isset($args['test-id'])) { - simpletest_script_print_error("--execute-batch should not be called interactively."); - exit; - } - if ($args['concurrency'] == 1) { - // Fallback to mono-threaded execution. - if (count($args['test_names']) > 1) { - foreach ($args['test_names'] as $test_class) { - // Execute each test in its separate Drupal environment. - simpletest_script_command(1, $args['test-id'], $test_class); - } - exit; - } - else { - // Execute an individual test. - $test_class = array_shift($args['test_names']); - drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL); - simpletest_script_run_one_test($args['test-id'], $test_class); - exit; - } - } - else { - // Multi-threaded execution. - $children = array(); - while (!empty($args['test_names']) || !empty($children)) { - // Fork children safely since Drupal is not bootstrapped yet. - while (count($children) < $args['concurrency']) { - if (empty($args['test_names'])) break; - - $child = array(); - $child['test_class'] = $test_class = array_shift($args['test_names']); - $child['pid'] = pcntl_fork(); - if (!$child['pid']) { - // This is the child process, bootstrap and execute the test. - drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL); - simpletest_script_run_one_test($args['test-id'], $test_class); - exit; + $total_status = SIMPLETEST_SCRIPT_EXIT_SUCCESS; + + // Multi-process execution. + $children = array(); + while (!empty($test_classes) || !empty($children)) { + while (count($children) < $args['concurrency']) { + if (empty($test_classes)) { + break; + } + + // Fork a child process. + $test_class = array_shift($test_classes); + $command = simpletest_script_command($test_id, $test_class); + $process = proc_open($command, array(), $pipes, NULL, NULL, array('bypass_shell' => TRUE)); + + if (!is_resource($process)) { + echo "Unable to fork test process. Aborting.\n"; + exit(SIMPLETEST_SCRIPT_EXIT_FAILURE); + } + + // Register our new child. + $children[] = array( + 'process' => $process, + 'class' => $test_class, + 'pipes' => $pipes, + ); + } + + // Wait for children every 200ms. + usleep(200000); + + // Check if some children finished. + foreach ($children as $cid => $child) { + $status = proc_get_status($child['process']); + if (empty($status['running'])) { + // The child exited, unregister it. + proc_close($child['process']); + if ($status['exitcode'] == SIMPLETEST_SCRIPT_EXIT_FAILURE) { + if ($status['exitcode'] > $total_status) { + $total_status = $status['exitcode']; + } } - else { - // Register our new child. - $children[] = $child; + elseif ($status['exitcode']) { + $total_status = $status['exitcode']; + echo 'FATAL ' . $test_class . ': test runner returned a non-zero error code (' . $status['exitcode'] . ').' . "\n"; } - } - // Wait for children every 200ms. - usleep(200000); - - // Check if some children finished. - foreach ($children as $cid => $child) { - if (pcntl_waitpid($child['pid'], $status, WUNTRACED | WNOHANG)) { - // This particular child exited. - unset($children[$cid]); - } + // Remove this child. + unset($children[$cid]); } } - exit; } + return $total_status; } /** - * Run a single test (assume a Drupal bootstrapped environment). + * Bootstrap Drupal and run a single test. */ function simpletest_script_run_one_test($test_id, $test_class) { - $test = new $test_class($test_id); - $test->run(); - $info = $test->getInfo(); + try { + // Bootstrap Drupal. + drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL); + + simpletest_classloader_register(); + + $test = new $test_class($test_id); + $test->run(); + $info = $test->getInfo(); - $status = ((isset($test->results['#fail']) && $test->results['#fail'] > 0) - || (isset($test->results['#exception']) && $test->results['#exception'] > 0) ? 'fail' : 'pass'); - simpletest_script_print($info['name'] . ' ' . _simpletest_format_summary_line($test->results) . "\n", simpletest_script_color_code($status)); + $had_fails = (isset($test->results['#fail']) && $test->results['#fail'] > 0); + $had_exceptions = (isset($test->results['#exception']) && $test->results['#exception'] > 0); + $status = ($had_fails || $had_exceptions ? 'fail' : 'pass'); + simpletest_script_print($info['name'] . ' ' . _simpletest_format_summary_line($test->results) . "\n", simpletest_script_color_code($status)); + + // Finished, kill this runner. + if ($had_fails || $had_exceptions) { + exit(SIMPLETEST_SCRIPT_EXIT_FAILURE); + } + exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS); + } + catch (Exception $e) { + echo (string) $e; + exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION); + } } /** - * Execute a command to run batch of tests in separate process. + * Return a command used to run a test in a separate process. + * + * @param $test_id + * The current test ID. + * @param $test_class + * The name of the test class to run. */ -function simpletest_script_command($concurrency, $test_id, $tests) { +function simpletest_script_command($test_id, $test_class) { global $args, $php; - $command = "$php ./scripts/{$args['script']} --url {$args['url']}"; + $command = escapeshellarg($php) . ' ' . escapeshellarg('./scripts/' . $args['script']) . ' --url ' . escapeshellarg($args['url']); if ($args['color']) { $command .= ' --color'; } - $command .= " --php " . escapeshellarg($php) . " --concurrency $concurrency --test-id $test_id --execute-batch $tests"; - passthru($command); + $command .= " --php " . escapeshellarg($php) . " --test-id $test_id --execute-test " . escapeshellarg($test_class); + return $command; } /** @@ -418,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); } } } @@ -439,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); } } } @@ -453,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; } @@ -488,12 +569,13 @@ echo "\n"; } - echo "Test run started: " . format_date($_SERVER['REQUEST_TIME'], 'long') . "\n"; + echo "Test run started:\n"; + echo " " . format_date($_SERVER['REQUEST_TIME'], 'long') . "\n"; timer_start('run-tests'); echo "\n"; - echo "Test summary:\n"; - echo "-------------\n"; + echo "Test summary\n"; + echo "------------\n"; echo "\n"; } @@ -574,7 +656,7 @@ echo "\n"; $end = timer_stop('run-tests'); echo "Test run duration: " . format_interval($end['time'] / 1000); - echo "\n"; + echo "\n\n"; } /** @@ -585,9 +667,8 @@ if ($args['verbose']) { // Report results. - echo "Detailed test results:\n"; - echo "----------------------\n"; - echo "\n"; + echo "Detailed test results\n"; + echo "---------------------\n"; $results = db_query("SELECT * FROM {simpletest} WHERE test_id = :test_id ORDER BY test_class, message_id", array(':test_id' => $test_id)); $test_class = ''; @@ -597,6 +678,10 @@ // Display test class every time results are for new test class. echo "\n\n---- $result->test_class ----\n\n\n"; $test_class = $result->test_class; + + // Print table header. + echo "Status Group Filename Line Function \n"; + echo "--------------------------------------------------------------------------------\n"; } simpletest_script_format_result($result); @@ -614,8 +699,8 @@ function simpletest_script_format_result($result) { global $results_map, $color; - $summary = sprintf("%-10.10s %-10.10s %-30.30s %-5.5s %-20.20s\n", - $results_map[$result->status], $result->message_group, basename($result->file), $result->line, $result->caller); + $summary = sprintf("%-9.9s %-10.10s %-17.17s %4.4s %-35.35s\n", + $results_map[$result->status], $result->message_group, basename($result->file), $result->line, $result->function); simpletest_script_print($summary, simpletest_script_color_code($result->status)); @@ -669,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.0/scripts/test.script drupal-7.66/scripts/test.script --- drupal-7.0/scripts/test.script 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/scripts/test.script 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,4 @@ +This file is for testing purposes only. + +It is used to test the functionality of drupal_get_filename(). See +BootstrapGetFilenameTestCase::testDrupalGetFilename() for more information. diff -Naur drupal-7.0/sites/README.txt drupal-7.66/sites/README.txt --- drupal-7.0/sites/README.txt 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/sites/README.txt 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,19 @@ +This directory structure contains the settings and configuration files specific +to your site or sites and is an integral part of multisite configuration. + +The sites/all/ subdirectory structure should be used to place your custom and +downloaded extensions including modules, themes, and third party libraries. + +Downloaded installation profiles should be placed in the /profiles directory +in the Drupal root. + +In multisite configuration, extensions found in the sites/all directory +structure are available to all sites. Alternatively, the sites/your_site_name/ +subdirectory pattern may be used to restrict extensions to a specific +site instance. + +See the respective README.txt files in sites/all/themes and sites/all/modules +for additional information about obtaining and organizing extensions. + +See INSTALL.txt in the Drupal root for information about single-site +installation or multisite configuration. diff -Naur drupal-7.0/sites/all/README.txt drupal-7.66/sites/all/README.txt --- drupal-7.0/sites/all/README.txt 2009-03-18 10:53:04.000000000 +0100 +++ drupal-7.66/sites/all/README.txt 1970-01-01 01:00:00.000000000 +0100 @@ -1,8 +0,0 @@ -// $Id: README.txt,v 1.4 2009/03/18 09:53:04 dries Exp $ - -This directory should be used to place downloaded and custom modules -and themes which are common to all sites. Keeping contributed and -custom modules and themes in the sites directory will aid in upgrading -Drupal core files. Place contributed and custom modules and themes in -the sites/all/modules and sites/all/themes directories respectively. - diff -Naur drupal-7.0/sites/all/libraries/README.txt drupal-7.66/sites/all/libraries/README.txt --- drupal-7.0/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.0/sites/all/modules/README.txt drupal-7.66/sites/all/modules/README.txt --- drupal-7.0/sites/all/modules/README.txt 2009-01-22 05:33:38.000000000 +0100 +++ drupal-7.66/sites/all/modules/README.txt 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,37 @@ -// $Id: README.txt,v 1.1 2009/01/22 04:33:38 webchick Exp $ +Modules extend your site functionality beyond Drupal core. -This directory should be used to place downloaded and custom modules -which are common to all sites. This will allow you to more easily -update Drupal core files. +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 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. + +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.0/sites/all/themes/README.txt drupal-7.66/sites/all/themes/README.txt --- drupal-7.0/sites/all/themes/README.txt 2009-01-22 05:33:38.000000000 +0100 +++ drupal-7.66/sites/all/themes/README.txt 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,29 @@ -// $Id: README.txt,v 1.1 2009/01/22 04:33:38 webchick Exp $ +Themes allow you to change the look and feel of your Drupal site. You can use +themes contributed by others or create your own. -This directory should be used to place downloaded and custom themes -which are common to all sites. This will allow you to more easily -update Drupal core files. +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.0/sites/default/default.settings.php drupal-7.66/sites/default/default.settings.php --- drupal-7.0/sites/default/default.settings.php 2010-10-12 01:49:48.000000000 +0200 +++ drupal-7.66/sites/default/default.settings.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,46 +1,55 @@ '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 @@ -138,7 +181,7 @@ * 'authmap' => 'shared_', * ), * @endcode - * You can also use a reference to a schema/database as a prefix. This maybe + * You can also use a reference to a schema/database as a prefix. This may be * useful if your Drupal installation exists in a schema that is not the default * or you want to access several databases from the same code base at the same * time. @@ -154,6 +197,29 @@ * @endcode * NOTE: MySQL and SQLite's definition of a schema is a database. * + * Advanced users can add or override initial commands to execute when + * connecting to the database server, as well as PDO connection settings. For + * example, to enable MySQL SELECT queries to exceed the max_join_size system + * variable, and to reduce the database connection timeout to 5 seconds: + * + * @code + * $databases['default']['default'] = array( + * 'init_commands' => array( + * 'big_selects' => 'SET SQL_BIG_SELECTS=1', + * ), + * 'pdo' => array( + * PDO::ATTR_TIMEOUT => 5, + * ), + * ); + * @endcode + * + * WARNING: These defaults are designed for database portability. Changing them + * may cause unexpected behavior, including potential data loss. + * + * @see DatabaseConnection_mysql::__construct + * @see DatabaseConnection_pgsql::__construct + * @see DatabaseConnection_sqlite::__construct + * * Database configuration format: * @code * $databases['default']['default'] = array( @@ -197,10 +263,10 @@ * Salt for one-time login links and cancel links, form tokens, etc. * * This variable will be set to a random value by the installer. All one-time - * login links will be invalidated if the value is changed. Note that this - * variable must have the same value on every web server. If this variable is - * empty, a hash of the serialized database credentials will be used as a - * fallback salt. + * login links will be invalidated if the value is changed. Note that if your + * site is deployed on a cluster of web servers, you must ensure that this + * variable has the same value on each server. If this variable is empty, a hash + * of the serialized database credentials will be used as a fallback salt. * * For enhanced security, you may set this variable to a value using the * contents of a file outside your docroot that is never saved together @@ -239,8 +305,8 @@ * * 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 - * See drupal_initialize_variables() in includes/bootstrap.inc for required + * 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. */ @@ -275,20 +341,21 @@ * 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); /** * Drupal automatically generates a unique session cookie name for each site - * based on on its full domain name. If you have multiple domains pointing at - * the same Drupal site, you can either redirect them all to a single domain - * (see comment in .htaccess), or uncomment the line below and specify their - * shared base domain. Doing so assures that users remain logged in as they - * cross between your various domains. + * based on its full domain name. If you have multiple domains pointing at the + * same Drupal site, you can either redirect them all to a single domain (see + * comment in .htaccess), or uncomment the line below and specify their shared + * base domain. Doing so assures that users remain logged in as they cross + * between your various domains. Make sure to always start the $cookie_domain + * with a leading dot, as per RFC 2109. */ -# $cookie_domain = 'example.com'; +# $cookie_domain = '.example.com'; /** * Variable overrides: @@ -322,41 +389,49 @@ # $conf['maintenance_theme'] = 'bartik'; /** - * Enable this setting to determine the correct IP address of the remote - * client by examining information stored in the X-Forwarded-For headers. - * X-Forwarded-For headers are a standard mechanism for identifying client - * systems connecting through a reverse proxy server, such as Squid or - * Pound. Reverse proxy servers are often used to enhance the performance + * Reverse Proxy Configuration: + * + * Reverse proxy servers are often used to enhance the performance * of heavily visited sites and may also provide other site caching, - * security or encryption benefits. If this Drupal installation operates - * behind a reverse proxy, this setting should be enabled so that correct - * IP address information is captured in Drupal's session management, - * logging, statistics and access management systems; if you are unsure - * about this setting, do not have a reverse proxy, or Drupal operates in - * a shared hosting environment, this setting should remain commented out. + * security, or encryption benefits. In an environment where Drupal + * is behind a reverse proxy, the real IP address of the client should + * be determined such that the correct client IP address is available + * to Drupal's logging, statistics, and access management systems. In + * the most simple scenario, the proxy server will add an + * X-Forwarded-For header to the request that contains the client IP + * address. However, HTTP headers are vulnerable to spoofing, where a + * malicious client could bypass restrictions by setting the + * X-Forwarded-For header directly. Therefore, Drupal's proxy + * configuration requires the IP addresses of all remote proxies to be + * specified in $conf['reverse_proxy_addresses'] to work correctly. + * + * Enable this setting to get Drupal to determine the client IP from + * the X-Forwarded-For header (or $conf['reverse_proxy_header'] if set). + * If you are unsure about this setting, do not have a reverse proxy, + * or Drupal operates in a shared hosting environment, this setting + * should remain commented out. + * + * In order for this setting to be used you must specify every possible + * reverse proxy IP address in $conf['reverse_proxy_addresses']. + * If a complete list of reverse proxies is not available in your + * environment (for example, if you use a CDN) you may set the + * $_SERVER['REMOTE_ADDR'] variable directly in settings.php. + * Be aware, however, that it is likely that this would allow IP + * address spoofing unless more advanced precautions are taken. */ # $conf['reverse_proxy'] = TRUE; /** - * Set this value if your proxy server sends the client IP in a header other - * than X-Forwarded-For. - * - * The "X-Forwarded-For" header is a comma+space separated list of IP addresses, - * only the last one (the left-most) will be used. + * Specify every reverse proxy IP address in your environment. + * This setting is required if $conf['reverse_proxy'] is TRUE. */ -# $conf['reverse_proxy_header'] = 'HTTP_X_CLUSTER_CLIENT_IP'; +# $conf['reverse_proxy_addresses'] = array('a.b.c.d', ...); /** - * reverse_proxy accepts an array of IP addresses. - * - * Each element of this array is the IP address of any of your reverse - * proxies. Filling this array Drupal will trust the information stored - * in the X-Forwarded-For headers only if Remote IP address is one of - * these, that is the request reaches the web server from one of your - * reverse proxies. Otherwise, the client could directly connect to - * your web server spoofing the X-Forwarded-For headers. + * Set this value if your proxy server sends the client IP in a header + * other than X-Forwarded-For. */ -# $conf['reverse_proxy_addresses'] = array('a.b.c.d', ...); +# $conf['reverse_proxy_header'] = 'HTTP_X_CLUSTER_CLIENT_IP'; /** * Page caching: @@ -369,8 +444,7 @@ * the cache. If the site has mostly anonymous users except a few known * editors/administrators, the Vary header can be omitted. This allows for * better caching in HTTP proxies (including reverse proxies), i.e. even if - * clients send different cookies, they still get content served from the cache - * if aggressive caching is enabled and the minimum cache time is non-zero. + * clients send different cookies, they still get content served from the cache. * However, authenticated users should access the site directly (i.e. not use an * HTTP proxy, and bypass the reverse proxy if one is used) in order to avoid * getting cached pages from the proxy. @@ -393,9 +467,38 @@ # $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 locale + * To override specific strings on your site with or without enabling the Locale * module, add an entry to this list. This functionality allows you to change * a small number of your site's default English language interface strings. * @@ -430,17 +533,129 @@ # ); /** + * Fast 404 pages: + * + * Drupal can generate fully themed 404 pages. However, some of these responses + * are for images or other resource files that are not displayed to the user. + * This can waste bandwidth, and also generate server load. + * + * The options below return a simple, fast 404 page for URLs matching a + * 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 + * any aliases ending in htm or html you can add '|s?html?' to the expression. + * - 404_fast_html: The html to return for simple 404 pages. + * + * Add leading hash signs if you would like to disable this functionality. + */ +$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.

        '; + +/** + * By default the page request process will return a fast 404 page for missing + * files if they match the regular expression set in '404_fast_paths' and not + * '404_fast_paths_exclude' above. 404 errors will simultaneously be logged in + * the Drupal system log. + * + * You can choose to return a fast 404 page earlier for missing pages (as soon + * as settings.php is loaded) by uncommenting the line below. This speeds up + * 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_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(); + +/** + * External access proxy settings: + * + * If your site must access the Internet via a web proxy then you can enter + * the proxy settings here. Currently only basic authentication is supported + * by using the username and password variables. The proxy_user_agent variable + * can be set to NULL for proxies that require no User-Agent header or to a + * non-empty string for proxies that limit requests to a specific agent. The + * proxy_exceptions variable is an array of host names to be accessed directly, + * not via proxy. + */ +# $conf['proxy_server'] = ''; +# $conf['proxy_port'] = 8080; +# $conf['proxy_username'] = ''; +# $conf['proxy_password'] = ''; +# $conf['proxy_user_agent'] = ''; +# $conf['proxy_exceptions'] = array('127.0.0.1', 'localhost'); + +/** * Authorized file system operations: * * The Update manager module included with Drupal provides a mechanism for * site administrators to securely install missing updates for the site - * directly through the web user interface by providing either SSH or FTP - * credentials. This allows the site to update the new files as the user who - * owns all the Drupal files, instead of as the user the webserver is running - * as. However, some sites might wish to disable this functionality, and only - * update the code directly via SSH or FTP themselves. This setting completely + * directly through the web user interface. On securely-configured servers, + * the Update manager will require the administrator to provide SSH or FTP + * credentials before allowing the installation to proceed; this allows the + * site to update the new files as the user who owns all the Drupal files, + * instead of as the user the webserver is running as. On servers where the + * webserver user is itself the owner of the Drupal files, the administrator + * will not be prompted for SSH or FTP credentials (note that these server + * setups are common on shared hosting, but are inherently insecure). + * + * Some sites might wish to disable the above functionality, and only update + * the code directly via SSH or FTP themselves. This setting completely * disables all functionality related to these authorized file operations. * + * @see http://drupal.org/node/244924 + * * 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.0/sites/example.sites.php drupal-7.66/sites/example.sites.php --- drupal-7.0/sites/example.sites.php 2010-04-15 14:01:28.000000000 +0200 +++ drupal-7.66/sites/example.sites.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,44 +1,55 @@ 'example.com', - * 'localhost.example' => 'example.com', - * ); - * - * The above array will cause Drupal to look for a directory named - * "example.com" in the sites directory whenever a request comes from - * "example.com", "devexample.com", or "localhost/example". That is useful - * on development servers, where the domain name may not be the same as the - * domain of the live server. Since Drupal stores file paths into the database - * (files, system table, etc.) this will ensure the paths are correct while - * accessed on development servers. + * This file allows you to define a set of aliases that map hostnames, ports, and + * pathnames to configuration directories in the sites directory. These aliases + * are loaded prior to scanning for directories, and they are exempt from the + * normal discovery rules. See default.settings.php to view how Drupal discovers + * the configuration directory when no alias is found. + * + * Aliases are useful on development servers, where the domain name may not be + * the same as the domain of the live server. Since Drupal stores file paths in + * the database (files, system table, etc.) this will ensure the paths are + * correct when the site is deployed to a live server. * * To use this file, copy and rename it such that its path plus filename is * 'sites/sites.php'. If you don't need to use multi-site directory aliasing, * then you can safely ignore this file, and Drupal will ignore it too. - */ - -/** - * Multi-site directory aliasing: * - * Edit the lines below to define directory aliases. Remove the leading hash - * signs to enable. + * Aliases are defined in an associative array named $sites. The array is + * written in the format: '..' => 'directory'. As an + * example, to map http://www.drupal.org:8080/mysite/test to the configuration + * directory sites/example.com, the array should be defined as: + * @code + * $sites = array( + * '8080.www.drupal.org.mysite.test' => 'example.com', + * ); + * @endcode + * The URL, http://www.drupal.org:8080/mysite/test/, could be a symbolic link or + * an Apache Alias directive that points to the Drupal root containing + * index.php. An alias could also be created for a subdomain. See the + * @link http://drupal.org/documentation/install online Drupal installation guide @endlink + * for more information on setting up domains, subdomains, and subdirectories. + * + * The following examples look for a site configuration in sites/example.com: + * @code + * URL: http://dev.drupal.org + * $sites['dev.drupal.org'] = 'example.com'; + * + * URL: http://localhost/example + * $sites['localhost.example'] = 'example.com'; + * + * URL: http://localhost:8080/example + * $sites['8080.localhost.example'] = 'example.com'; + * + * URL: http://www.drupal.org:8080/mysite/test/ + * $sites['8080.www.drupal.org.mysite.test'] = 'example.com'; + * @endcode + * + * @see default.settings.php + * @see conf_path() + * @see http://drupal.org/documentation/install/multi-site */ -# $sites['devexample.com'] = 'example.com'; -# $sites['localhost.example'] = 'example.com'; diff -Naur drupal-7.0/themes/README.txt drupal-7.66/themes/README.txt --- drupal-7.0/themes/README.txt 2009-10-08 03:44:22.000000000 +0200 +++ drupal-7.66/themes/README.txt 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -// $Id: README.txt,v 1.3 2009/10/08 01:44:22 dries Exp $ This directory is reserved for core theme files. Custom or contributed themes should be placed in their own subdirectory of the sites/all/themes directory. diff -Naur drupal-7.0/themes/bartik/bartik.info drupal-7.66/themes/bartik/bartik.info --- drupal-7.0/themes/bartik/bartik.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/themes/bartik/bartik.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: bartik.info,v 1.5 2010/11/07 00:27:20 dries Exp $ name = Bartik description = A flexible, recolorable theme with many regions. @@ -35,8 +34,7 @@ settings[shortcut_module_link] = 0 -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/themes/bartik/color/color.inc drupal-7.66/themes/bartik/color/color.inc --- drupal-7.0/themes/bartik/color/color.inc 2010-12-14 20:53:14.000000000 +0100 +++ drupal-7.66/themes/bartik/color/color.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ array('logo' => theme_get_setting('logo', 'bartik'))), 'setting'); @@ -7,86 +6,86 @@ $info = array( // Available colors and color labels used in theme. 'fields' => array( - 'bg' => t('Main background'), - 'link' => t('Link color'), 'top' => t('Header top'), 'bottom' => t('Header bottom'), - 'text' => t('Text color'), + 'bg' => t('Main background'), 'sidebar' => t('Sidebar background'), 'sidebarborders' => t('Sidebar borders'), 'footer' => t('Footer background'), 'titleslogan' => t('Title and slogan'), + 'text' => t('Text color'), + 'link' => t('Link color'), ), // Pre-defined color schemes. 'schemes' => array( 'default' => array( 'title' => t('Blue Lagoon (default)'), 'colors' => array( - 'bg' => '#ffffff', - 'link' => '#0071B3', 'top' => '#0779bf', 'bottom' => '#48a9e4', - 'text' => '#3b3b3b', + 'bg' => '#ffffff', 'sidebar' => '#f6f6f2', 'sidebarborders' => '#f9f9f9', 'footer' => '#292929', 'titleslogan' => '#fffeff', + 'text' => '#3b3b3b', + 'link' => '#0071B3', ), ), 'firehouse' => array( 'title' => t('Firehouse'), 'colors' => array( - 'bg' => '#ffffff', - 'link' => '#d6121f', 'top' => '#cd2d2d', 'bottom' => '#cf3535', - 'text' => '#3b3b3b', + 'bg' => '#ffffff', 'sidebar' => '#f1f4f0', 'sidebarborders' => '#ededed', 'footer' => '#1f1d1c', 'titleslogan' => '#fffeff', + 'text' => '#3b3b3b', + 'link' => '#d6121f', ), ), 'ice' => array( 'title' => t('Ice'), 'colors' => array( - 'bg' => '#ffffff', - 'link' => '#019dbf', 'top' => '#d0d0d0', 'bottom' => '#c2c4c5', - 'text' => '#4a4a4a', + 'bg' => '#ffffff', 'sidebar' => '#ffffff', 'sidebarborders' => '#cccccc', 'footer' => '#24272c', 'titleslogan' => '#000000', + 'text' => '#4a4a4a', + 'link' => '#019dbf', ), ), 'plum' => array( 'title' => t('Plum'), 'colors' => array( - 'bg' => '#fffdf7', - 'link' => '#9d408d', 'top' => '#4c1c58', 'bottom' => '#593662', - 'text' => '#301313', + 'bg' => '#fffdf7', 'sidebar' => '#edede7', 'sidebarborders' => '#e7e7e7', 'footer' => '#2c2c28', 'titleslogan' => '#ffffff', + 'text' => '#301313', + 'link' => '#9d408d', ), ), 'slate' => array( 'title' => t('Slate'), 'colors' => array( - 'bg' => '#ffffff', - 'link' => '#0073b6', 'top' => '#4a4a4a', 'bottom' => '#4e4e4e', - 'text' => '#3b3b3b', + 'bg' => '#ffffff', 'sidebar' => '#ffffff', 'sidebarborders' => '#d0d0d0', 'footer' => '#161617', 'titleslogan' => '#ffffff', + 'text' => '#3b3b3b', + 'link' => '#0073b6', ), ), ), diff -Naur drupal-7.0/themes/bartik/color/preview.css drupal-7.66/themes/bartik/color/preview.css --- drupal-7.0/themes/bartik/color/preview.css 2010-12-14 03:50:24.000000000 +0100 +++ drupal-7.66/themes/bartik/color/preview.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -/* $Id: preview.css,v 1.5 2010/12/14 02:50:24 dries Exp $ */ /* ---------- Color form ----------- */ #color_scheme_form #palette .form-item { diff -Naur drupal-7.0/themes/bartik/color/preview.js drupal-7.66/themes/bartik/color/preview.js --- drupal-7.0/themes/bartik/color/preview.js 2010-12-11 22:37:41.000000000 +0100 +++ drupal-7.66/themes/bartik/color/preview.js 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -// $Id: preview.js,v 1.5 2010/12/11 21:37:41 webchick Exp $ (function ($) { Drupal.color = { diff -Naur drupal-7.0/themes/bartik/css/colors.css drupal-7.66/themes/bartik/css/colors.css --- drupal-7.0/themes/bartik/css/colors.css 2011-01-04 06:24:13.000000000 +0100 +++ drupal-7.66/themes/bartik/css/colors.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -/* $Id: colors.css,v 1.10 2011/01/04 05:24:13 webchick Exp $ */ /* ---------- Color Module Styles ----------- */ @@ -24,8 +23,12 @@ } #header { background-color: #48a9e4; - background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#0779bf), to(#48a9e4)); - background-image: -moz-linear-gradient(-90deg, #0779bf, #48a9e4); + background-image: -moz-linear-gradient(top, #0779bf 0%, #48a9e4 100%); + background-image: -ms-linear-gradient(top, #0779bf 0%, #48a9e4 100%); + background-image: -o-linear-gradient(top, #0779bf 0%, #48a9e4 100%); + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #0779bf), color-stop(1, #48a9e4)); + background-image: -webkit-linear-gradient(top, #0779bf 0%, #48a9e4 100%); + background-image: linear-gradient(top, #0779bf 0%, #48a9e4 100%); } a { color: #0071B3; diff -Naur drupal-7.0/themes/bartik/css/ie-rtl.css drupal-7.66/themes/bartik/css/ie-rtl.css --- drupal-7.0/themes/bartik/css/ie-rtl.css 2011-01-04 03:59:37.000000000 +0100 +++ drupal-7.66/themes/bartik/css/ie-rtl.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -/* $Id: ie-rtl.css,v 1.7 2011/01/04 02:59:37 webchick Exp $ */ fieldset legend { left: 6px; diff -Naur drupal-7.0/themes/bartik/css/ie.css drupal-7.66/themes/bartik/css/ie.css --- drupal-7.0/themes/bartik/css/ie.css 2011-01-04 03:59:37.000000000 +0100 +++ drupal-7.66/themes/bartik/css/ie.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -/* $Id: ie.css,v 1.12 2011/01/04 02:59:37 webchick Exp $ */ .block { zoom: 1; diff -Naur drupal-7.0/themes/bartik/css/ie6.css drupal-7.66/themes/bartik/css/ie6.css --- drupal-7.0/themes/bartik/css/ie6.css 2010-12-03 00:54:56.000000000 +0100 +++ drupal-7.66/themes/bartik/css/ie6.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -/* $Id: ie6.css,v 1.5 2010/12/02 23:54:56 dries Exp $ */ #content { overflow: hidden; diff -Naur drupal-7.0/themes/bartik/css/layout-rtl.css drupal-7.66/themes/bartik/css/layout-rtl.css --- drupal-7.0/themes/bartik/css/layout-rtl.css 2010-11-30 18:43:52.000000000 +0100 +++ drupal-7.66/themes/bartik/css/layout-rtl.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -/* $Id: layout-rtl.css,v 1.5 2010/11/30 17:43:52 dries Exp $ */ /* ---------- Basic Layout RTL Styles ----------- */ diff -Naur drupal-7.0/themes/bartik/css/layout.css drupal-7.66/themes/bartik/css/layout.css --- drupal-7.0/themes/bartik/css/layout.css 2011-01-04 06:24:13.000000000 +0100 +++ drupal-7.66/themes/bartik/css/layout.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -/* $Id: layout.css,v 1.8 2011/01/04 05:24:13 webchick Exp $ */ /* ---------- Basic Layout Styles ----------- */ diff -Naur drupal-7.0/themes/bartik/css/maintenance-page.css drupal-7.66/themes/bartik/css/maintenance-page.css --- drupal-7.0/themes/bartik/css/maintenance-page.css 2010-11-26 12:00:37.000000000 +0100 +++ drupal-7.66/themes/bartik/css/maintenance-page.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,13 +1,14 @@ -/* $Id: maintenance-page.css,v 1.6 2010/11/26 11:00:37 webchick Exp $ */ body.maintenance-page { background-color: #fff; color: #000; } .maintenance-page #page-wrapper { + background: #fff; margin-left: auto; margin-right: auto; min-width: 0; + min-height: 0; width: 800px; border: 1px solid #ddd; margin-top: 40px; @@ -24,7 +25,6 @@ width: auto; } .maintenance-page #header div.section, -.maintenance-page #messages, .maintenance-page #main { width: 700px; } @@ -58,6 +58,10 @@ padding: 0; margin-top: 30px; } +.maintenance-page #messages div.messages { + margin: 0; +} .maintenance-page #messages div.section { padding: 0; + width: auto; } diff -Naur drupal-7.0/themes/bartik/css/print.css drupal-7.66/themes/bartik/css/print.css --- drupal-7.0/themes/bartik/css/print.css 2010-09-09 17:27:08.000000000 +0200 +++ drupal-7.66/themes/bartik/css/print.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -/* $Id: print.css,v 1.2 2010/09/09 15:27:08 webchick Exp $ */ /* ---------- General Layout ---------- */ diff -Naur drupal-7.0/themes/bartik/css/style-rtl.css drupal-7.66/themes/bartik/css/style-rtl.css --- drupal-7.0/themes/bartik/css/style-rtl.css 2011-01-04 07:23:29.000000000 +0100 +++ drupal-7.66/themes/bartik/css/style-rtl.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -/* $Id: style-rtl.css,v 1.19 2011/01/04 06:23:29 dries Exp $ */ /* ------------------ Reset Styles ------------------ */ @@ -17,6 +16,10 @@ blockquote:after { content: "\201C"; } +tr td, +tr th { + text-align: right; +} /* ------------------ List Styles ------------------ */ @@ -97,6 +100,7 @@ float: right; } .link-wrapper { + text-align: left; margin-right: 236px; margin-left: 0; } @@ -221,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.0/themes/bartik/css/style.css drupal-7.66/themes/bartik/css/style.css --- drupal-7.0/themes/bartik/css/style.css 2011-01-04 07:23:29.000000000 +0100 +++ drupal-7.66/themes/bartik/css/style.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -/* $Id: style.css,v 1.53 2011/01/04 06:23:29 dries Exp $ */ /* ---------- Overall Specifications ---------- */ @@ -54,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 { @@ -159,7 +158,7 @@ line-height: 0.1em; vertical-align: -.45em; } -blockquote :first-child { +blockquote > p:first-child { display: inline; } a.feed-icon { @@ -460,10 +459,6 @@ #main-menu { clear: both; } -#main-menu-links a { - color: #d9d9d9; - padding: 0.6em 1em 0.4em; -} #main-menu-links { font-size: 0.929em; margin: 0; @@ -709,7 +704,6 @@ } .comment .submitted .comment-permalink { font-size: 0.786em; - text-transform: lowercase; } .comment .content { font-size: 0.929em; @@ -1067,25 +1061,26 @@ /* -------------- Password Meter ------------- */ +.confirm-parent, +.password-parent { + width: 34em; +} .password-parent, div.form-item div.password-suggestions { position: relative; - width: auto; } -#password-strength { - float: none; - left: 16em; - position: absolute; - width: 11.5em; -} -#password-strength-text, +.password-strength-text, .password-strength-title, div.password-confirm { font-size: 0.82em; } -#password-strength-text { +.password-strength-text { margin-top: 0.2em; } +div.password-confirm { + margin-top: 2.2em; + width: 20.73em; +} /* ---------------- Buttons ---------------- */ @@ -1136,7 +1131,7 @@ .fieldset-wrapper { margin-top: 25px; } -.node-form .fieldset-wrapper { +.node-form .vertical-tabs .fieldset-wrapper { margin-top: 0; } .filter-wrapper { @@ -1213,6 +1208,13 @@ fieldset .fieldset-wrapper { padding: 0 10px; } +fieldset .fieldset-description { + margin-top: 5px; + margin-bottom: 1em; + line-height: 1.4; + color: #3c3c3c; + font-style: italic; +} input { margin: 2px 0; padding: 4px; @@ -1247,12 +1249,6 @@ .form-item label { font-size: 0.929em; } -fieldset .description { - margin-top: 5px; - line-height: 1.4; - color: #3c3c3c; - font-style: italic; -} .form-type-radio label, .form-type-checkbox label { margin-left: 4px; @@ -1329,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 */ @@ -1434,9 +1422,6 @@ div.vertical-tabs .vertical-tabs-panes fieldset.vertical-tabs-pane { padding: 1em; } -#forum tr td.forum { - padding-left: 35px; -} #forum .name { font-size: 1.083em; } @@ -1494,7 +1479,7 @@ } .search-results li:last-child { border-bottom: none; - padding-bottom: none; + padding-bottom: 0; margin-bottom: 1em; } .search-results .search-snippet-info { @@ -1587,7 +1572,6 @@ .overlay #page { padding: 0 2em; } -.overlay #skip-link, .overlay .region-page-top, .overlay #header, .overlay #page-title, diff -Naur drupal-7.0/themes/bartik/template.php drupal-7.66/themes/bartik/template.php --- drupal-7.0/themes/bartik/template.php 2010-12-14 02:04:27.000000000 +0100 +++ drupal-7.66/themes/bartik/template.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ $variables['name'], '!datetime' => $variables['date'])); if ($variables['view_mode'] == 'full' && node_is_page($variables['node'])) { $variables['classes_array'][] = 'node-full'; } @@ -148,7 +150,7 @@ $output .= '
      '; // Render the top-level DIV. - $output = '
      ' . $output . '
      '; + $output = '
      ' . $output . '
      '; return $output; } diff -Naur drupal-7.0/themes/bartik/templates/comment-wrapper.tpl.php drupal-7.66/themes/bartik/templates/comment-wrapper.tpl.php --- drupal-7.0/themes/bartik/templates/comment-wrapper.tpl.php 2010-09-25 04:05:51.000000000 +0200 +++ drupal-7.66/themes/bartik/templates/comment-wrapper.tpl.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@
      > diff -Naur drupal-7.0/themes/bartik/templates/comment.tpl.php drupal-7.66/themes/bartik/templates/comment.tpl.php --- drupal-7.0/themes/bartik/templates/comment.tpl.php 2010-12-01 01:18:15.000000000 +0100 +++ drupal-7.66/themes/bartik/templates/comment.tpl.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ body becomes $body. When needing to access - * a field's raw values, developers/themers are strongly encouraged to use these - * variables. Otherwise they will have to explicitly specify the desired field - * language, e.g. $node->body['en'], thus overriding any language negotiation - * rule that was previously applied. + * variable is defined; for example, $node->body becomes $body. When needing to + * access a field's raw values, developers/themers are strongly encouraged to + * use these variables. Otherwise they will have to explicitly specify the + * desired field language; for example, $node->body['en'], thus overriding any + * language negotiation rule that was previously applied. * * @see template_preprocess() * @see template_preprocess_node() diff -Naur drupal-7.0/themes/bartik/templates/page.tpl.php drupal-7.66/themes/bartik/templates/page.tpl.php --- drupal-7.0/themes/bartik/templates/page.tpl.php 2010-11-07 22:48:56.000000000 +0100 +++ drupal-7.66/themes/bartik/templates/page.tpl.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@
      diff -Naur drupal-7.0/themes/engines/phptemplate/phptemplate.engine drupal-7.66/themes/engines/phptemplate/phptemplate.engine --- drupal-7.0/themes/engines/phptemplate/phptemplate.engine 2010-02-23 20:03:37.000000000 +0100 +++ drupal-7.66/themes/engines/phptemplate/phptemplate.engine 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@
      > @@ -7,7 +6,7 @@ - + diff -Naur drupal-7.0/themes/garland/fix-ie-rtl.css drupal-7.66/themes/garland/fix-ie-rtl.css --- drupal-7.0/themes/garland/fix-ie-rtl.css 2010-04-28 22:08:39.000000000 +0200 +++ drupal-7.66/themes/garland/fix-ie-rtl.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -/* $Id: fix-ie-rtl.css,v 1.6 2010/04/28 20:08:39 dries Exp $ */ body { /* Center layout */ diff -Naur drupal-7.0/themes/garland/fix-ie.css drupal-7.66/themes/garland/fix-ie.css --- drupal-7.0/themes/garland/fix-ie.css 2010-04-28 22:08:39.000000000 +0200 +++ drupal-7.66/themes/garland/fix-ie.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -/* $Id: fix-ie.css,v 1.13 2010/04/28 20:08:39 dries Exp $ */ body { /* Center layout */ diff -Naur drupal-7.0/themes/garland/garland.info drupal-7.66/themes/garland/garland.info --- drupal-7.0/themes/garland/garland.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/themes/garland/garland.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: garland.info,v 1.10 2010/11/07 00:27:20 dries Exp $ name = Garland description = A multi-column theme which can be configured to modify colors and switch between fixed and fluid width layouts. package = Core @@ -8,8 +7,7 @@ stylesheets[print][] = print.css settings[garland_width] = fluid -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/themes/garland/maintenance-page.tpl.php drupal-7.66/themes/garland/maintenance-page.tpl.php --- drupal-7.0/themes/garland/maintenance-page.tpl.php 2010-03-04 10:03:08.000000000 +0100 +++ drupal-7.66/themes/garland/maintenance-page.tpl.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@
      > diff -Naur drupal-7.0/themes/garland/page.tpl.php drupal-7.66/themes/garland/page.tpl.php --- drupal-7.0/themes/garland/page.tpl.php 2010-11-20 05:03:51.000000000 +0100 +++ drupal-7.66/themes/garland/page.tpl.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ diff -Naur drupal-7.0/themes/garland/print.css drupal-7.66/themes/garland/print.css --- drupal-7.0/themes/garland/print.css 2010-04-28 22:08:39.000000000 +0200 +++ drupal-7.66/themes/garland/print.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -/* $Id: print.css,v 1.9 2010/04/28 20:08:39 dries Exp $ */ body, input, diff -Naur drupal-7.0/themes/garland/style-rtl.css drupal-7.66/themes/garland/style-rtl.css --- drupal-7.0/themes/garland/style-rtl.css 2010-09-27 03:12:45.000000000 +0200 +++ drupal-7.66/themes/garland/style-rtl.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -/* $Id: style-rtl.css,v 1.23 2010/09/27 01:12:45 dries Exp $ */ html { direction: rtl; diff -Naur drupal-7.0/themes/garland/style.css drupal-7.66/themes/garland/style.css --- drupal-7.0/themes/garland/style.css 2011-01-03 08:04:48.000000000 +0100 +++ drupal-7.66/themes/garland/style.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -/* $Id: style.css,v 1.88 2011/01/03 07:04:48 webchick Exp $ */ /** * Generic elements diff -Naur drupal-7.0/themes/garland/template.php drupal-7.66/themes/garland/template.php --- drupal-7.0/themes/garland/template.php 2010-12-01 01:18:15.000000000 +0100 +++ drupal-7.66/themes/garland/template.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,12 +1,7 @@ CSS_THEME, 'browsers' => array('IE' => 'lt IE 7', '!IE' => FALSE), 'preprocess' => FALSE)); @@ -48,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'), ), @@ -80,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'), ), @@ -96,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.0/themes/garland/theme-settings.php drupal-7.66/themes/garland/theme-settings.php --- drupal-7.0/themes/garland/theme-settings.php 2010-09-04 17:21:09.000000000 +0200 +++ drupal-7.66/themes/garland/theme-settings.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ gAMA!PLTEptRNS:yx8WIDATc`uZLإVUP `c.fЂ11 œg3gt˜+;faa")@ֆd +$H64w8QKIENDB` \ No newline at end of file diff -Naur drupal-7.0/themes/seven/images/task-item-rtl.png drupal-7.66/themes/seven/images/task-item-rtl.png --- drupal-7.0/themes/seven/images/task-item-rtl.png 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/themes/seven/images/task-item-rtl.png 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,3 @@ +PNG + + IHDR  v+gAMA0PLTEeee555bbb222?-IDATc?~@bۻ ? w@3S$/FC6IENDB` \ No newline at end of file diff -Naur drupal-7.0/themes/seven/maintenance-page.tpl.php drupal-7.66/themes/seven/maintenance-page.tpl.php --- drupal-7.0/themes/seven/maintenance-page.tpl.php 2009-11-23 00:44:09.000000000 +0100 +++ drupal-7.66/themes/seven/maintenance-page.tpl.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,6 +1,4 @@ - diff -Naur drupal-7.0/themes/seven/page.tpl.php drupal-7.66/themes/seven/page.tpl.php --- drupal-7.0/themes/seven/page.tpl.php 2010-11-20 05:03:51.000000000 +0100 +++ drupal-7.66/themes/seven/page.tpl.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,6 +1,4 @@ - +
      @@ -12,7 +10,9 @@
      - + +
      +
      diff -Naur drupal-7.0/themes/seven/reset.css drupal-7.66/themes/seven/reset.css --- drupal-7.0/themes/seven/reset.css 2010-12-19 05:58:13.000000000 +0100 +++ drupal-7.66/themes/seven/reset.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -/* $Id: reset.css,v 1.13 2010/12/19 04:58:13 webchick Exp $ */ /** * Reset CSS styles. @@ -111,11 +110,6 @@ .item-list .pager li, .pager-current, .tips, -dl.multiselect dd, -dl.multiselect dd .form-item, -dl.multiselect dd select, -dl.multiselect dt, -dl.multiselect .form-item, ul.primary, ul.primary li, ul.primary li a, diff -Naur drupal-7.0/themes/seven/seven.info drupal-7.66/themes/seven/seven.info --- drupal-7.0/themes/seven/seven.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/themes/seven/seven.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: seven.info,v 1.6 2010/11/07 00:27:20 dries Exp $ name = Seven description = A simple one-column, tableless, fluid width administration theme. package = Core @@ -14,8 +13,7 @@ regions[sidebar_first] = First sidebar regions_hidden[] = sidebar_first -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/themes/seven/style-rtl.css drupal-7.66/themes/seven/style-rtl.css --- drupal-7.0/themes/seven/style-rtl.css 2010-10-15 06:37:15.000000000 +0200 +++ drupal-7.66/themes/seven/style-rtl.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,25 +1,241 @@ -/* $Id: style-rtl.css,v 1.2 2010/10/15 04:37:15 webchick Exp $ */ /** * Generic elements. */ +dl dd, +dl dl { + margin-right: 20px; +} ul, .block ul, -.item-list ul, .item-list ul { margin: 0.25em 1.5em 0.25em 0; } +ol { + margin: 0.25em 2em 0.25em 0; +} -/* User login block */ -#user-login-form .openid-links { +/** + * Skip link. + */ +#skip-link { + right: 50%; + margin-right: -5.25em; +} +#skip-link a, +#skip-link a:link, +#skip-link a:visited { + padding: 1px 10px 2px 10px; +} + +/** + * Branding. + */ +#branding { + padding: 20px 20px 0 20px; +} + +#branding div.block { + float: left; + padding-left: 0; + padding-right: 10px; +} +#branding div.block form div.form-item { + float: right; +} +#branding div.block form input.form-text { + margin-left: 10px; margin-right: 0; } -#user-login-form .openid-links .user-link { + +/** + * Help. + */ +#help div.more-help-link { + text-align: left; +} + +/** + * Page title. + */ +#branding h1.page-title { + float: right; +} + +/** + * Tabs. + */ +ul.primary li, +ul.primary li a:link, +ul.primary li a.active { + float: right; +} +ul.primary, +ul.secondary { + float: left; +} +ul.secondary li { + float: none; +} +ul.primary { + padding-top: 0; +} + +/** + * Page layout. + */ +#page { + padding: 20px 0 40px 0; + margin-left: 40px; + margin-right: 40px; +} +#secondary-links ul.links li { + padding: 0 0 10px 10px; +} +ul.links li, +ul.inline li { + padding-left: 1em; + padding-right: 0; +} +ul.admin-list li { + padding: 9px 30px 0 0; + margin-right: 0; + background: url(images/list-item-rtl.png) no-repeat right 11px; +} +ul.admin-list li a { + margin-right: -30px; margin-left: 0; - margin-right: 1.5em; + padding: 0 30px 4px 0; +} +ul.admin-list.compact li a { + margin-right: 0; +} +ul.admin-list li div.description a { + margin-right: 0; } -/* Sortable tables */ +/** + * Tables. + */ table th.active a { padding: 0 0 0 25px; } +table th.active img { + left: 3px; + right: auto; +} +/** + * Exception for webkit bug with the right border of the last cell + * in some tables, since it's webkit only, we can use :last-child + */ +tr td:last-child { + border-left: 1px solid #bebfb9; + border-right: none; +} + +/** + * Fieldsets. + */ +fieldset { + padding: 2.5em 0 0 0; +} +fieldset .fieldset-legend { + padding-right: 15px; + right: 0; +} +fieldset .fieldset-wrapper { + padding: 0 15px 13px 13px; +} + +/* Filter */ +.filter-wrapper .form-item, +.filter-wrapper .filter-guidelines, +.filter-wrapper .filter-help { + padding: 2px 0 0 0; +} +ul.tips li { + margin: 0.25em 1.5em 0.25em 0; +} +body div.form-type-radio div.description, +body div.form-type-checkbox div.description { + margin-left: 0; + margin-right: 1.5em; +} +input.form-submit, +a.button { + margin-left: 1em; + margin-right: 0; +} +ul.action-links li { + float: right; + margin: 0 0 0 1em; +} +ul.action-links a { + padding-left: 0; + padding-right: 15px; + background-position: right center; +} + +/* Update options. */ +div.admin-options label, +div.admin-options div.form-item { + margin-left: 10px; + margin-right: 0; + float: right; +} + +/* Maintenance theming */ +body.in-maintenance #sidebar-first { + float: right; +} +body.in-maintenance #content { + float: left; + padding-left: 20px; + padding-right: 0; +} +ol.task-list { + margin-right: 0; +} +ol.task-list li { + padding: 0.5em 20px 0.5em 1em; +} +ol.task-list li.active { + background: transparent url(images/task-item-rtl.png) no-repeat right 50%; + padding: 0.5em 20px 0.5em 1em; +} + +/* Overlay theming */ +.overlay #branding div.breadcrumb { + float: right; +} +.overlay ul.secondary { + margin: -1.4em 0 0.3em 0; +} + +/* Shortcut theming */ +div.add-or-remove-shortcuts { + float: none; + padding-left: 0; + padding-right: 6px; +} + +/* Dashboard */ +#dashboard div.block div.content { + padding: 10px 5px 5px 5px; +} +#dashboard div.block div.content ul.menu { + margin-right: 20px; +} + +/* Recent content block */ +#block-node-recent .more-link { + padding: 0 0 5px 5px; +} + +/* User login block */ +#user-login-form .openid-links { + margin-right: 0; +} +#user-login-form .openid-links .user-link { + margin-right: 1.5em; +} diff -Naur drupal-7.0/themes/seven/style.css drupal-7.66/themes/seven/style.css --- drupal-7.0/themes/seven/style.css 2011-01-03 08:04:48.000000000 +0100 +++ drupal-7.66/themes/seven/style.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -/* $Id: style.css,v 1.86 2011/01/03 07:04:48 webchick Exp $ */ /** * Generic elements. @@ -20,7 +19,7 @@ padding: 0; border: none; height: 1px; - background: #CCCCCC; + background: #cccccc; } legend { font-weight: bold; @@ -58,7 +57,7 @@ } dl dd, dl dl { - margin-left: 20px; + margin-left: 20px; /* LTR */ margin-bottom: 10px; } blockquote { @@ -101,12 +100,14 @@ } ul, .block ul, -.item-list ul, .item-list ul { list-style-type: disc; list-style-image: none; margin: 0.25em 0 0.25em 1.5em; /* LTR */ } +.item-list .pager li { + padding: 0.5em; +} .item-list ul li, li.leaf, ul.menu li { @@ -118,7 +119,7 @@ } ol { list-style-type: decimal; - margin: 0.25em 0 0.25em 2em; + margin: 0.25em 0 0.25em 2em; /* LTR */ } .item-list ul li.collapsed, ul.menu li.collapsed { @@ -150,8 +151,8 @@ #skip-link { margin-top: 0; position: absolute; - left: 50%; - margin-left: -5.25em; + left: 50%; /* LTR */ + margin-left: -5.25em; /* LTR */ width: auto; z-index: 50; } @@ -162,7 +163,7 @@ background: #444; color: #fff; font-size: 0.94em; - padding: 1px 10px 2px 10px; + padding: 1px 10px 2px 10px; /* LTR */ text-decoration: none; -moz-border-radius: 0 0 10px 10px; -webkit-border-top-left-radius: 0; @@ -182,7 +183,7 @@ */ #branding { overflow: hidden; - padding: 20px 20px 0 20px; + padding: 20px 20px 0 20px; /* LTR */ position: relative; background-color: #e0e0d8; } @@ -192,23 +193,23 @@ } #branding div.block { position: relative; - float: right; + float: right; /* LTR */ width: 240px; - padding-left: 10px; + padding-left: 10px; /* LTR */ background: #333; } #branding div.block form label { display: none; } #branding div.block form div.form-item { - float: left; + float: left; /* LTR */ border: 0; margin: 0; padding: 0; } #branding div.block form input.form-text { width: 140px; - margin-right: 10px; + margin-right: 10px; /* LTR */ } #branding div.block form input.form-submit { text-align: center; @@ -226,7 +227,7 @@ margin: 0 0 10px; } #help div.more-help-link { - text-align: right; + text-align: right; /* LTR */ } /** @@ -242,7 +243,7 @@ padding-bottom: 10px; font-size: 1.385em; font-weight: normal; - float: left; + float: left; /* LTR */ } /** @@ -256,26 +257,33 @@ * Tabs. */ ul.primary { - float: right; + float: right; /* LTR */ border-bottom: none; - padding: 0.769em 0 5px 8px; text-transform: uppercase; font-size: 0.923em; + height: 2.60em; + margin: 0; + padding-top: 0; } ul.primary li { - display: inline; + float: left; /* LTR */ list-style: none; + margin: 0 2px; } -ul.primary li a, +ul.primary li a:link, ul.primary li a.active, ul.primary li a:active, ul.primary li a:visited, ul.primary li a:hover, ul.primary li.active a { + display: block; + float: left; /* LTR */ + height: 2.60em; + line-height: 2.60em; + padding: 0 18px 8px; background-color: #a6a7a2; color: #000; font-weight: bold; - padding: 6px 20px; border-width: 1px 1px 0 1px; border-style: solid; border-color: #a6a7a2; @@ -297,22 +305,25 @@ ul.primary li.active a:hover { color: #000; } -ul.secondary { - float: none; +.tabs-secondary { clear: both; +} +ul.secondary { + float: right; /* LTR */ font-size: 0.923em; - text-align: right; - padding: 4px 10px 10px; + padding: 0 3px 5px; line-height: 1.385em; overflow: hidden; background-color: #fff; } ul.secondary li { - padding-left: 10px; + margin: 0 5px; + float: none; /* LTR */ } ul.secondary li a { background-color: #ddd; color: #000; + display: inline-block; } ul.secondary li a, ul.secondary li a:hover, @@ -329,20 +340,23 @@ color: #fff; background: #666; } +#content { + clear: left; +} /** * Page layout. */ #page { - padding: 20px 0 40px 0; - margin-right: 40px; - margin-left: 40px; + padding: 20px 0 40px 0; /* LTR */ + margin-right: 40px; /* LTR */ + margin-left: 40px; /* LTR */ background: #fff; position: relative; color: #333; } #secondary-links ul.links li { - padding: 0 10px 10px 0; + padding: 0 10px 10px 0; /* LTR */ } #secondary-links ul.links li a { font-size: 0.923em; @@ -362,7 +376,7 @@ } ul.links li, ul.inline li { - padding-right: 1em; + padding-right: 1em; /* LTR */ } ul.inline li { display: inline; @@ -373,12 +387,12 @@ } ul.admin-list li { position: relative; - padding-left: 30px; + padding-left: 30px; /* LTR */ padding-top: 9px; border-top: 1px solid #ccc; - margin-left: 0; + margin-left: 0; /* LTR */ margin-bottom: 10px; - background: url(images/list-item.png) no-repeat 0 11px; + background: url(images/list-item.png) no-repeat 0 11px; /* LTR */ list-style-type: none; list-style-image: none; } @@ -403,17 +417,17 @@ border-bottom: none; } ul.admin-list li a { - margin-left: -30px; - padding: 0px 0 4px 30px; + margin-left: -30px; /* LTR */ + padding: 0 0 4px 30px; /* LTR */ min-height: 0; } ul.admin-list.compact li a { - margin-left: 0; + margin-left: 0; /* LTR */ padding: 0; } ul.admin-list li div.description a { - margin-left: 0px; - padding: 0px; + margin-left: 0; /* LTR */ + padding: 0; min-height: inherit; } div.submitted { @@ -461,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; } @@ -474,7 +494,7 @@ table th.active img { position: absolute; top: 3px; - right: 3px; + right: 3px; /* LTR */ } table td.active { background: #e9e9dd; @@ -511,7 +531,7 @@ * in some tables, since it's webkit only, we can use :last-child */ tr td:last-child { - border-right: 1px solid #BEBFB9; + border-right: 1px solid #bebfb9; /* LTR */ } @@ -535,18 +555,18 @@ */ fieldset { border: 1px solid #ccc; - padding: 2.5em 0 0 0; + padding: 2.5em 0 0 0; /* LTR */ position: relative; margin: 1em 0; } fieldset .fieldset-legend { margin-top: 0.5em; - padding-left: 15px; + padding-left: 15px; /* LTR */ position: absolute; text-transform: uppercase; } fieldset .fieldset-wrapper { - padding: 0 13px 13px 15px; + padding: 0 13px 13px 15px; /* LTR */ } fieldset.collapsed { background-color: transparent; @@ -587,10 +607,8 @@ padding: 0; } .form-item label.option { - text-transform: none; -} -.form-item label.option { font-size: 0.923em; + text-transform: none; } .form-item label.option input { vertical-align: middle; @@ -616,7 +634,7 @@ .filter-wrapper .filter-guidelines, .filter-wrapper .filter-help { font-size: 0.923em; - padding: 2px 0 0 0; + padding: 2px 0 0 0; /* LTR */ } ul.tips, div.description, @@ -627,18 +645,18 @@ color: #666; } ul.tips li { - margin: 0.25em 0 0.25em 1.5em; + margin: 0.25em 0 0.25em 1.5em; /* LTR */ } body div.form-type-radio div.description, body div.form-type-checkbox div.description { - margin-left: 1.5em; + margin-left: 1.5em; /* LTR */ } input.form-submit, a.button { cursor: pointer; padding: 4px 17px; margin-bottom: 1em; - margin-right: 1em; + margin-right: 1em; /* LTR */ color: #5a5a5a; text-align: center; font-weight: normal; @@ -646,8 +664,8 @@ font-family: "Lucida Grande", Verdana, sans-serif; border: 1px solid #e4e4e4; border-bottom: 1px solid #b4b4b4; - border-left-color: #D2D2D2; - border-right-color: #D2D2D2; + border-left-color: #d2d2d2; + border-right-color: #d2d2d2; background: url(images/buttons.png) 0 0 repeat-x; -moz-border-radius: 20px; -webkit-border-radius: 20px; @@ -660,20 +678,11 @@ text-decoration: none; color: #5a5a5a; } -div.node-form input#edit-submit, -div.node-form input#edit-submit-1 { - border: 1px solid #8eB7cd; - border-left-color: #8eB7cd; - border-right-color: #8eB7cd; - border-bottom-color: #7691a2; - background: url(images/buttons.png) 0px -40px repeat-x; - color: #133B54; -} input.form-submit:active { background: #666; color: #fff; border-color: #555; - text-shadow: #222 0px -1px 0px; + text-shadow: #222 0 -1px 0; } input.form-button-disabled, input.form-button-disabled:active { @@ -700,24 +709,19 @@ 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; + padding: 0 20px 0 20px; /* LTR */ list-style-type: none; overflow: hidden; } ul.action-links li { - float: left; - margin: 0 1em 0 0; + float: left; /* LTR */ + margin: 0 1em 0 0; /* LTR */ } ul.action-links a { - padding-left: 15px; + padding-left: 15px; /* LTR */ background: transparent url(images/add.png) no-repeat 0 center; line-height: 30px; } @@ -769,19 +773,6 @@ margin-top: 0; } -/* admin/content and admin/people */ -dl.multiselect, -dl.multiselect dt, -dl.multiselect dd { - margin: 0 10px 0 0; -} -dl.multiselect select, -dl.multiselect dd select { - font-size: 0.923em; - background: #fff; - border: 1px solid #ccc; -} - /* Update options. */ div.admin-options { background: #f8f8f8; @@ -797,8 +788,8 @@ } div.admin-options label, div.admin-options div.form-item { - margin-right: 10px; - float: left; + margin-right: 10px; /* LTR */ + float: left; /* LTR */ } div.admin-options div.form-item { padding: 0; @@ -812,13 +803,14 @@ /* Maintenance theming */ body.in-maintenance #sidebar-first { - float: left; + float: left; /* LTR */ width: 200px; } body.in-maintenance #content { - float: right; + float: right; /* LTR */ width: 550px; - padding-right: 20px; + padding-right: 20px; /* LTR */ + clear: none; } body.in-maintenance #page { overflow: auto; @@ -844,6 +836,7 @@ } body.in-maintenance #logo { margin-bottom: 1.5em; + max-width: 180px; } ol.task-list { margin-left: 0; /* LTR */ @@ -860,8 +853,7 @@ color: #000; } ol.task-list li.done { - color: #393; - background: transparent url(images/task-check.png) no-repeat 0px 50%; /* LTR */ + background: transparent url(images/task-check.png) no-repeat 0 50%; color: green; } @@ -880,7 +872,7 @@ padding: 0 20px; } .overlay #branding div.breadcrumb { - float: left; + float: left; /* LTR */ position: relative; z-index: 10; } @@ -895,7 +887,8 @@ } .overlay ul.secondary { background: transparent none; - margin: -2.4em 0 0.3em 0; + margin: -1.4em 0 0.3em 0; /* LTR */ + overflow: visible; } .overlay #content { padding: 0; @@ -906,9 +899,9 @@ /* Shortcut theming */ div.add-or-remove-shortcuts { - float: left; + float: left; /* LTR */ padding-top: 6px; - padding-left: 6px; + padding-left: 6px; /* LTR */ } /* Dashboard */ @@ -918,13 +911,13 @@ #dashboard div.block h2 { margin: 0; font-size: 1em; - padding: 3px 9px 3px 19px; + padding: 3px 10px; } #dashboard div.block div.content { - padding: 10px 5px 5px 5px; + padding: 10px 5px 5px 5px; /* LTR */ } #dashboard div.block div.content ul.menu { - margin-left: 20px; + margin-left: 20px; /* LTR */ } #dashboard .dashboard-region .block { border: #ccc 1px solid; @@ -961,7 +954,7 @@ border: none; } #block-node-recent .more-link { - padding: 0 5px 5px 0; + padding: 0 5px 5px 0; /* LTR */ } /* User login block */ @@ -989,7 +982,7 @@ padding: 0.4em 0.6em; } .overlay-disable-message-focused #overlay-dismiss-message { - background-color: #59A0D8; + background-color: #59a0d8; color: #fff; -moz-border-radius: 8px; -webkit-border-radius: 8px; diff -Naur drupal-7.0/themes/seven/template.php drupal-7.66/themes/seven/template.php --- drupal-7.0/themes/seven/template.php 2010-11-20 05:03:51.000000000 +0100 +++ drupal-7.66/themes/seven/template.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@ CSS_THEME, 'browsers' => array('IE' => 'lte IE 8', '!IE' => FALSE), 'preprocess' => FALSE)); + drupal_add_css(path_to_theme() . '/ie.css', array('group' => CSS_THEME, 'browsers' => array('IE' => 'lte IE 8', '!IE' => FALSE), 'weight' => 999, 'preprocess' => FALSE)); + // Add conditional CSS for IE7 and below. + drupal_add_css(path_to_theme() . '/ie7.css', array('group' => CSS_THEME, 'browsers' => array('IE' => 'lte IE 7', '!IE' => FALSE), 'weight' => 999, 'preprocess' => FALSE)); // Add conditional CSS for IE6. - drupal_add_css(path_to_theme() . '/ie6.css', array('group' => CSS_THEME, 'browsers' => array('IE' => 'lt IE 7', '!IE' => FALSE), 'preprocess' => FALSE)); + drupal_add_css(path_to_theme() . '/ie6.css', array('group' => CSS_THEME, 'browsers' => array('IE' => 'lte IE 6', '!IE' => FALSE), 'weight' => 999, 'preprocess' => FALSE)); } /** @@ -89,10 +90,10 @@ $style = $variables['style']; $theme_path = drupal_get_path('theme', 'seven'); if ($style == 'asc') { - return theme('image', array('path' => $theme_path . '/images/arrow-asc.png', 'alt' => t('sort ascending'), 'title' => t('sort ascending'))); + return theme('image', array('path' => $theme_path . '/images/arrow-asc.png', 'alt' => t('sort ascending'), 'width' => 13, 'height' => 13, 'title' => t('sort ascending'))); } else { - return theme('image', array('path' => $theme_path . '/images/arrow-desc.png', 'alt' => t('sort descending'), 'title' => t('sort descending'))); + return theme('image', array('path' => $theme_path . '/images/arrow-desc.png', 'alt' => t('sort descending'), 'width' => 13, 'height' => 13, 'title' => t('sort descending'))); } } @@ -103,9 +104,15 @@ // Use Seven's vertical tabs style instead of the default one. if (isset($css['misc/vertical-tabs.css'])) { $css['misc/vertical-tabs.css']['data'] = drupal_get_path('theme', 'seven') . '/vertical-tabs.css'; + $css['misc/vertical-tabs.css']['type'] = 'file'; + } + if (isset($css['misc/vertical-tabs-rtl.css'])) { + $css['misc/vertical-tabs-rtl.css']['data'] = drupal_get_path('theme', 'seven') . '/vertical-tabs-rtl.css'; + $css['misc/vertical-tabs-rtl.css']['type'] = 'file'; } // Use Seven's jQuery UI theme style instead of the default one. if (isset($css['misc/ui/jquery.ui.theme.css'])) { $css['misc/ui/jquery.ui.theme.css']['data'] = drupal_get_path('theme', 'seven') . '/jquery.ui.theme.css'; + $css['misc/ui/jquery.ui.theme.css']['type'] = 'file'; } } diff -Naur drupal-7.0/themes/seven/vertical-tabs-rtl.css drupal-7.66/themes/seven/vertical-tabs-rtl.css --- drupal-7.0/themes/seven/vertical-tabs-rtl.css 1970-01-01 01:00:00.000000000 +0100 +++ drupal-7.66/themes/seven/vertical-tabs-rtl.css 2019-04-17 22:20:46.000000000 +0200 @@ -0,0 +1,21 @@ + +/** + * Override of misc/vertical-tabs-rtl.css. + */ +div.vertical-tabs { + background: #fff url(images/fc-rtl.png) repeat-y right 0; +} +div.vertical-tabs .vertical-tabs-list { + float: right; + margin: 0 0 -1px -100%; +} +div.vertical-tabs ul li.selected a, +div.vertical-tabs ul li.selected a:hover, +div.vertical-tabs ul li.selected a:focus, +div.vertical-tabs ul li.selected a:active { + border-left-color: #fff; +} +div.vertical-tabs .vertical-tabs-panes { + margin: 0 265px 0 0; + padding: 10px 0 10px 15px; +} diff -Naur drupal-7.0/themes/seven/vertical-tabs.css drupal-7.66/themes/seven/vertical-tabs.css --- drupal-7.0/themes/seven/vertical-tabs.css 2010-11-19 21:45:04.000000000 +0100 +++ drupal-7.66/themes/seven/vertical-tabs.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,25 +1,24 @@ -/* $Id: vertical-tabs.css,v 1.10 2010/11/19 20:45:04 dries Exp $ */ /** * Override of misc/vertical-tabs.css. */ div.vertical-tabs { - background: #fff url(images/fc.png) repeat-y 0 0; + background: #fff url(images/fc.png) repeat-y 0 0; /* LTR */ border: 1px solid #ccc; margin: 10px 0; position: relative; } -div.vertical-tabs fieldset { +fieldset.vertical-tabs-pane { border: 0; padding: 0; margin: 0; } div.vertical-tabs .vertical-tabs-list { border-bottom: 1px solid #ccc; - float: left; + float: left; /* LTR */ font-size: 1em; line-height: 1; - margin: 0 -100% -1px 0; + margin: 0 -100% -1px 0; /* LTR */ padding: 0; width: 240px; } @@ -56,7 +55,7 @@ div.vertical-tabs ul li.selected a:focus, div.vertical-tabs ul li.selected a:active { background: #fff; - border-right-color: #fff; + border-right-color: #fff; /* LTR */ border-top: 1px solid #ccc; } div.vertical-tabs ul li.first.selected a, @@ -67,12 +66,31 @@ text-decoration: underline; } div.vertical-tabs .vertical-tabs-panes { - margin: 0 0 0 265px; - padding: 10px 15px 10px 0; + margin: 0 0 0 265px; /* LTR */ + padding: 10px 15px 10px 0; /* LTR */ } -div.vertical-tabs .vertical-tabs-panes legend { +fieldset.vertical-tabs-pane legend { display: none; } +fieldset.vertical-tabs-pane fieldset legend { + display: block; +} .vertical-tabs-pane .fieldset-wrapper > div:first-child { padding-top: 5px; } + +/** + * Prevent text inputs from overflowing when container is too narrow. "width" is + * applied to override hardcoded cols or size attributes and used in conjunction + * with "box-sizing" to prevent box model issues from occurring in most browsers. +*/ +.vertical-tabs .form-type-textfield input { + width: 100%; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} +* html .vertical-tabs .form-type-textfield, +* html .vertical-tabs .form-textarea-wrapper { + width: 95%; /* IE6 */ +} diff -Naur drupal-7.0/themes/stark/README.txt drupal-7.66/themes/stark/README.txt --- drupal-7.0/themes/stark/README.txt 2009-12-20 20:43:10.000000000 +0100 +++ drupal-7.66/themes/stark/README.txt 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -// $Id: README.txt,v 1.2 2009/12/20 19:43:10 dries Exp $ ABOUT STARK ----------- diff -Naur drupal-7.0/themes/stark/layout.css drupal-7.66/themes/stark/layout.css --- drupal-7.0/themes/stark/layout.css 2009-08-03 05:04:34.000000000 +0200 +++ drupal-7.66/themes/stark/layout.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ -/* $Id: layout.css,v 1.3 2009/08/03 03:04:34 webchick Exp $ */ /** * @file diff -Naur drupal-7.0/themes/stark/stark.info drupal-7.66/themes/stark/stark.info --- drupal-7.0/themes/stark/stark.info 2011-01-05 07:25:56.000000000 +0100 +++ drupal-7.66/themes/stark/stark.info 2019-04-17 22:39:36.000000000 +0200 @@ -1,4 +1,3 @@ -; $Id: stark.info,v 1.4 2010/11/07 00:27:20 dries Exp $ name = Stark description = This theme demonstrates Drupal's default HTML markup and CSS styles. To learn how to build your own theme and override Drupal's default code, see the Theming Guide. package = Core @@ -6,8 +5,7 @@ core = 7.x stylesheets[all][] = layout.css -; Information added by drupal.org packaging script on 2011-01-05 -version = "7.0" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1294208756" - +datestamp = "1555533576" diff -Naur drupal-7.0/themes/tests/README.txt drupal-7.66/themes/tests/README.txt --- drupal-7.0/themes/tests/README.txt 2009-10-06 03:38:55.000000000 +0200 +++ drupal-7.66/themes/tests/README.txt 1970-01-01 01:00:00.000000000 +0100 @@ -1,5 +0,0 @@ -// $Id: README.txt,v 1.1 2009/10/06 01:38:55 webchick Exp $ - -The themes in this subdirectory are all used by the Drupal core testing -framework. They are not functioning themes that could be used on a real site -and are hidden in the administrative user interface. diff -Naur drupal-7.0/themes/tests/test_theme/template.php drupal-7.66/themes/tests/test_theme/template.php --- drupal-7.0/themes/tests/test_theme/template.php 2010-08-22 14:46:21.000000000 +0200 +++ drupal-7.66/themes/tests/test_theme/template.php 1970-01-01 01:00:00.000000000 +0100 @@ -1,22 +0,0 @@ - $update) { if (!isset($update['start'])) { $form['start'][$module] = array( - '#title' => $module, - '#item' => $update['warning'], - '#prefix' => '
      ', + '#type' => 'item', + '#title' => $module . ' module', + '#markup' => $update['warning'], + '#prefix' => '
      ', '#suffix' => '
      ', ); $incompatible_updates_exist = TRUE; @@ -108,6 +114,9 @@ $form['links'] = array( '#markup' => theme('item_list', array('items' => update_helpful_links())), ); + + // No updates to run, so caches won't get flushed later. Clear them now. + drupal_flush_all_caches(); } else { $form['help'] = array( @@ -138,21 +147,27 @@ return $form; } +/** + * Provides links to the homepage and administration pages. + */ function update_helpful_links() { - // NOTE: we can't use l() here because the URL would point to - // 'update.php?q=admin'. $links[] = 'Front page'; - $links[] = 'Administration pages'; + if (user_access('access administration pages')) { + $links[] = 'Administration pages'; + } return $links; } +/** + * Displays results of the update script with any accompanying errors. + */ function update_results_page() { drupal_set_title('Drupal database update'); $links = update_helpful_links(); update_task_list(); // Report end result. - if (module_exists('dblog')) { + if (module_exists('dblog') && user_access('access site reports')) { $log_message = ' All errors have been logged.'; } else { @@ -160,10 +175,11 @@ } if ($_SESSION['update_success']) { - $output = '

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

      '; + $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.'; @@ -226,6 +242,15 @@ return $output; } +/** + * Provides an overview of the Drupal database update. + * + * This page provides cautionary suggestions that should happen before + * proceeding with the update to ensure data integrity. + * + * @return + * Rendered HTML form. + */ function update_info_page() { // Change query-strings on css/js files to enforce reload for all users. _drupal_flush_css_js(); @@ -245,11 +270,18 @@ $output .= "

    • Install your new files in the appropriate location, as described in the handbook.
    • \n"; $output .= "\n"; $output .= "

      When you have performed the steps above, you may proceed.

      \n"; - $output .= '

      '; + $form_action = check_url(drupal_current_script_url(array('op' => 'selection', 'token' => $token))); + $output .= '

      '; $output .= "\n"; return $output; } +/** + * Renders a 403 access denied page for update.php. + * + * @return + * Rendered HTML warning with 403 status. + */ function update_access_denied_page() { drupal_add_http_header('Status', '403 Forbidden'); watchdog('access denied', 'update.php', NULL, WATCHDOG_WARNING); @@ -288,7 +320,7 @@ } /** - * Add the update task list to the current page. + * Adds the update task list to the current page. */ function update_task_list($active = NULL) { // Default list of tasks. @@ -304,8 +336,7 @@ } /** - * Returns (and optionally stores) extra requirements that only apply during - * particular parts of the update.php process. + * Returns and stores extra requirements that apply during the update process. */ function update_extra_requirements($requirements = NULL) { static $extra_requirements = array(); @@ -316,20 +347,26 @@ } /** - * Check update requirements and report any errors. + * Checks update requirements and reports errors and (optionally) warnings. + * + * @param $skip_warnings + * (optional) If set to TRUE, requirement warnings will be ignored, and a + * report will only be issued if there are requirement errors. Defaults to + * FALSE. */ -function update_check_requirements() { +function update_check_requirements($skip_warnings = FALSE) { // Check requirements of all loaded modules. $requirements = module_invoke_all('requirements', 'update'); $requirements += update_extra_requirements(); $severity = drupal_requirements_severity($requirements); - // If there are issues, report them. - if ($severity == REQUIREMENT_ERROR) { + // If there are errors, always display them. If there are only warnings, skip + // them if the caller has indicated they should be skipped. + if ($severity == REQUIREMENT_ERROR || ($severity == REQUIREMENT_WARNING && !$skip_warnings)) { update_task_list('requirements'); drupal_set_title('Requirements problem'); $status_report = theme('status_report', array('requirements' => $requirements)); - $status_report .= 'Check the error messages and try again.'; + $status_report .= 'Check the error messages and try again.'; print theme('update_page', array('content' => $status_report)); exit(); } @@ -349,9 +386,18 @@ require_once DRUPAL_ROOT . '/includes/unicode.inc'; update_prepare_d7_bootstrap(); +// Temporarily disable configurable timezones so the upgrade process uses the +// site-wide timezone. This prevents a PHP notice during session initlization +// and before offsets have been converted in user_update_7002(). +$configurable_timezones = variable_get('configurable_timezones', 1); +$conf['configurable_timezones'] = 0; + // Determine if the current user has access to run update.php. drupal_bootstrap(DRUPAL_BOOTSTRAP_SESSION); +// Reset configurable timezones. +$conf['configurable_timezones'] = $configurable_timezones; + // Only allow the requirements check to proceed if the current user has access // to run updates (since it may expose sensitive information about the site's // configuration). @@ -376,8 +422,9 @@ // Set up theme system for the maintenance page. drupal_maintenance_theme(); - // Check the update requirements for Drupal. - update_check_requirements(); + // Check the update requirements for Drupal. Only report on errors at this + // stage, since the real requirements check happens further down. + update_check_requirements(TRUE); // Redirect to the update information page if all requirements were met. install_goto('update.php?op=info'); @@ -409,22 +456,31 @@ update_fix_compatibility(); - // Check the update requirements for all modules. - update_check_requirements(); + // Check the update requirements for all modules. If there are warnings, but + // no errors, skip reporting them if the user has provided a URL parameter + // acknowledging the warnings and indicating a desire to continue anyway. See + // drupal_requirements_url(). + $skip_warnings = !empty($_GET['continue']); + update_check_requirements($skip_warnings); $op = isset($_REQUEST['op']) ? $_REQUEST['op'] : ''; switch ($op) { // 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')) { - update_batch($_POST['start'], $base_url . '/update.php?op=results', $base_url . '/update.php'); + 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. + $batch_url = $base_root . drupal_current_script_url(); + $redirect_url = $base_root . drupal_current_script_url(array('op' => 'results')); + update_batch($_POST['start'], $redirect_url, $batch_url); break; } @@ -447,7 +503,7 @@ $output = update_access_denied_page(); } if (isset($output) && $output) { - // Explictly start a session so that the update.php token will be accepted. + // Explicitly start a session so that the update.php token will be accepted. drupal_session_start(); // We defer the display of messages until all updates are done. $progress_page = ($batch = batch_get()) && isset($batch['running']); diff -Naur drupal-7.0/web.config drupal-7.66/web.config --- drupal-7.0/web.config 2010-07-28 04:28:03.000000000 +0200 +++ drupal-7.66/web.config 2019-04-17 22:20:46.000000000 +0200 @@ -6,12 +6,15 @@ - + + + + diff -Naur drupal-7.0/xmlrpc.php drupal-7.66/xmlrpc.php --- drupal-7.0/xmlrpc.php 2010-10-02 03:22:41.000000000 +0200 +++ drupal-7.66/xmlrpc.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,5 +1,4 @@
    '. t('Make post sticky') .''. t('Publish post') .''. t('Publish comment') .'