diff -Naur drupal-7.5/.editorconfig drupal-7.66/.editorconfig --- drupal-7.5/.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.5/.htaccess drupal-7.66/.htaccess --- drupal-7.5/.htaccess 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/.htaccess 2019-04-17 22:20:46.000000000 +0200 @@ -3,8 +3,13 @@ # # Protect files and directories from prying eyes. - - Order allow,deny + + + Require all denied + + + Order allow,deny + # Don't show directory listings for URLs which map to a directory. @@ -16,17 +21,11 @@ # 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. @@ -62,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 @@ -75,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 @@ -84,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. @@ -129,9 +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 + +# 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.5/CHANGELOG.txt drupal-7.66/CHANGELOG.txt --- drupal-7.5/CHANGELOG.txt 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/CHANGELOG.txt 2019-04-17 22:20:46.000000000 +0200 @@ -1,3 +1,1044 @@ +Drupal 7.xx, xxxx-xx-xx (development version) +----------------------- + +Drupal 7.66, 2019-04-17 +----------------------- +- Fixed security issues: + - SA-CORE-2019-006 + +Drupal 7.65, 2019-03-20 +----------------------- +- Fixed security issues: + - SA-CORE-2019-004 + +Drupal 7.64, 2019-02-06 +----------------------- +- [regression] Unset the 'host' header in drupal_http_request() during redirect +- Fixed: 7.x does not have Phar protection and Phar tests are failing on Drupal 7 +- Fixed: Notice: Undefined index: display_field in file_field_widget_value() (line 582 of /module/file/file.field.inc) +- Performance improvement: Registry rebuild should not parse the same file twice in the same request +- Fixed _registry_update() to clear caches after transaction is committed + +Drupal 7.63, 2019-01-16 +----------------------- +- Fixed a fatal error for some Drush users introduced by SA-CORE-2019-002. + +Drupal 7.62, 2019-01-15 +----------------------- +- Fixed security issues: + - SA-CORE-2019-001 + - SA-CORE-2019-002 + +Drupal 7.61, 2018-11-07 +----------------------- +- File upload validation functions and hook_file_validate() implementations are + now always passed the correct file URI. +- The default form cache expiration of 6 hours is now configurable (API + addition: https://www.drupal.org/node/2857751). +- Allowed callers of drupal_http_request() to optionally specify an explicit + Host header. +- Allowed the + character to appear in usernames. +- PHP 7.2: Fixed Archive_Tar incompatibility. +- PHP 7.2: Removed deprecated function each(). +- PHP 7.2: Avoid count() calls on uncountable variables. +- PHP 7.2: Removed deprecated create_function() call. +- PHP 7.2: Make sure variables are arrays in theme_links(). +- Fixed theme-settings.php not being loaded on cached forms +- Fixed problem with IE11 & Chrome(PointerEvents enabled) & some Firefox scroll to the top of the page after dragging the bottom item with jquery 1.5 <-> 1.11 + +Drupal 7.60, 2018-10-18 +------------------------ +- Fixed security issues. See SA-CORE-2018-006. + +Drupal 7.59, 2018-04-25 +----------------------- +- Fixed security issues (remote code execution). See SA-CORE-2018-004. + +Drupal 7.58, 2018-03-28 +----------------------- +- Fixed security issues (remote code execution). See SA-CORE-2018-002. + +Drupal 7.57, 2018-02-21 +----------------------- +- Fixed security issues (multiple vulnerabilities). See SA-CORE-2018-001. + +Drupal 7.56, 2017-06-21 +----------------------- +- Fixed security issues (access bypass). See SA-CORE-2017-003. + +Drupal 7.55, 2017-06-07 +----------------------- +- Fixed incompatibility with PHP versions 7.0.19 and 7.1.5 due to duplicate + DATE_RFC7231 definition. +- Made Drupal core pass all automated tests on PHP 7.1. +- Allowed services such as Let's Encrypt to work with Drupal on Apache, by + making Drupal's .htaccess file allow access to the .well-known directory + defined by RFC 5785. +- Made new Drupal sites work correctly on Apache 2.4 when the mod_access_compat + Apache module is disabled. +- Fixed Drupal's URL-generating functions to always encode '[' and ']' so that + the URLs will pass HTML5 validation. +- Various additional bug fixes. +- Various API documentation improvements. +- Additional automated test coverage. + +Drupal 7.54, 2017-02-01 +----------------------- +- Modules are now able to define theme engines (API addition: + https://www.drupal.org/node/2826480). +- Logging of searches can now be disabled (new option in the administrative + interface). +- Added menu tree render structure to (pre-)process hooks for theme_menu_tree() + (API addition: https://www.drupal.org/node/2827134). +- Added new function for determining whether an HTTPS request is being served + (API addition: https://www.drupal.org/node/2824590). +- Fixed incorrect default value for short and medium date formats on the date + type configuration page. +- File validation error message is now removed after subsequent upload of valid + file. +- Numerous bug fixes. +- Numerous API documentation improvements. +- Additional performance improvements. +- Additional automated test coverage. + +Drupal 7.53, 2016-12-07 +----------------------- +- Fixed drag and drop support on newer Chrome/IE 11+ versions after 7.51 update + when jQuery is updated to 1.7-1.11.0. + +Drupal 7.52, 2016-11-16 +----------------------- +- Fixed security issues (multiple vulnerabilities). See SA-CORE-2016-005. + +Drupal 7.51, 2016-10-05 +----------------------- +- The Update module now also checks for updates to a disabled theme that is + used as an admin theme. +- Exceptions thrown in dblog_watchdog() are now caught and ignored. +- Clarified the warning that appears when modules are missing or have moved. +- Log messages are now XSS filtered on display. +- Draggable tables now work on touch screen devices. +- Added a setting for allowing double underscores in CSS identifiers + (https://www.drupal.org/node/2810369). +- If a user navigates away from a page while an Ajax request is running they + will no longer get an error message saying "An Ajax HTTP request terminated + abnormally". +- The system_region_list() API function now takes an optional third parameter + which allows region name translations to be skipped when they are not needed + (API addition: https://www.drupal.org/node/2810365). +- Numerous performance improvements. +- Numerous bug fixes. +- Numerous API documentation improvements. +- Additional automated test coverage. + +Drupal 7.50, 2016-07-07 +----------------------- +- Added a new "administer fields" permission for trusted users, which is + required in addition to other permissions to use the field UI + (https://www.drupal.org/node/2483307). +- Added clickjacking protection to Drupal core by setting the X-Frame-Options + header to SAMEORIGIN by default (https://www.drupal.org/node/2735873). +- Added support for full UTF-8 (emojis, Asian symbols, mathematical symbols) on + MySQL and other database drivers when the site and database are configured to + allow it (https://www.drupal.org/node/2761183). +- Improved performance by avoiding a re-scan of directories when a file is + missing; instead, trigger a PHP warning (minor API change: + https://www.drupal.org/node/2581445). +- Made it possible to use any PHP callable in Ajax form callbacks, form API + form-building functions, and form API wrapper callbacks (API addition: + https://www.drupal.org/node/2761169). +- Fixed that following a password reset link while logged in leaves users unable + to change their password (minor user interface change: + https://www.drupal.org/node/2759023). +- Implemented various fixes for automated test failures on PHP 5.4+ and PHP 7. + Drupal core automated tests now pass in these environments. +- Improved support for PHP 7 by fixing various problems. +- Fixed various bugs with PHP 5.5+ imagerotate(), including when incorrect + color indices are passed in. +- Fixed a regression introduced in Drupal 7.43 that allowed files uploaded by + anonymous users to be lost after form validation errors, and that also caused + regressions with certain contributed modules. +- Fixed a regression introduced in Drupal 7.36 which caused the default value + of hidden textarea fields to be ignored. +- Fixed robots.txt to allow search engines to access CSS, JavaScript and image + files. +- Changed wording on the Update Manager settings page to clarify that the + option to check for disabled module updates also applies to uninstalled + modules (administrative-facing translatable string change). +- Changed the help text when editing menu links and configuring URL redirect + actions so that it does not reference "Drupal" or the drupal.org website + (administrative-facing translatable string change). +- Fixed the locale safety check that is used to ensure that translations are + safe to allow for tokens in the href/src attributes of translated strings. +- Fixed that URL generation only works on port 80 when using domain based + language negotation. +- Made method="get" forms work inside the administrative overlay. The fix adds + a new hidden field to these forms when they appear inside the overlay (minor + data structure change). +- Increased maxlength of menu link title input fields in the node form and + menu link form from 128 to 255 characters. +- Removed meaningless post-check=0 and pre-check=0 cache control headers from + Drupal HTTP responses. +- Added a .editorconfig file to auto-configure editors that support it. +- Added --directory option to run-tests.sh for easier test discovery of all + tests within a project. +- Made run-tests.sh exit with a failure code when there are test fails or + problems running the script. +- Fixed that cookies from previous tests are still present when a new test + starts in DrupalWebTestCase. +- Improved performance of queries on the {authmap} database table. +- Fixed handling of missing files and functions inside the registry. +- Fixed Ajax handling for tableselect form elements that use checkboxes. +- Fixed a bug which caused ip_address() to return nothing when the client IP + address and proxy IP address are the same. +- Added a new option to format_xml_elements() to allow for already encoded + values. +- Changed the {history} table's node ID field to be an unsigned integer, to + match the same field in the {node} table and to prevent errors with very + large node IDs. +- Added an explicit page callback to the "admin/people/create" menu item in the + User module (minor data structure change). Previously this automatically + inherited the page callback from the parent "admin/people" menu item, which + broke contributed modules that override the "admin/people" page. +- Numerous small bug fixes. +- Numerous API documentation improvements. +- Additional automated test coverage. + +Drupal 7.44, 2016-06-15 +----------------------- +- Fixed security issues (privilege escalation). See SA-CORE-2016-002. + +Drupal 7.43, 2016-02-24 +----------------------- +- Fixed security issues (multiple vulnerabilities). See SA-CORE-2016-001. + +Drupal 7.42, 2016-02-03 +----------------------- +- Stopped invoking hook_flush_caches() on every cron run, since some modules + use that hook for expensive operations that are only needed on cache clears. +- Changed the default .htaccess and web.config to block Composer-related files. +- Added static caching to module_load_include() to improve performance. +- Fixed double-encoding bugs in select field widgets provided by the Options + module. The fix deprecates the 'strip_tags' property on option widgets and + replaces it with a new 'strip_tags_and_unescape' property (minor data + structure change). +- Improved MySQL 5.7 support by changing the MySQL database driver to stop + using the ANSI SQL mode alias, which has different meanings for different + MySQL versions. +- Fixed a regression introduced in Drupal 7.39 which prevented autocomplete + functionality from working on servers that are not configured to + automatically recognize index.php. +- Updated the Archive_Tar PEAR package to the latest 1.4.0 release, to fix bugs + with tar file handling on various operating systems. +- Fixed fatal errors on node preview when a field is displayed in the node + teaser but hidden in the full node view. The fix removes a + field_attach_prepare_view() call from the node_preview() function since it is + redundant with one in the node preview theme layer. +- Improved the description of the "Trimmed" format option on text fields + (translatable string change, and minor UI and data structure change). +- Numerous small bug fixes. +- Numerous API documentation improvements. +- Additional automated test coverage. + +Drupal 7.41, 2015-10-21 +----------------------- +- Fixed security issues (open redirect). See SA-CORE-2015-004. + +Drupal 7.40, 2015-10-14 +----------------------- +- Made Drupal's code for parsing .info files run much faster and use much less + memory. +- Prevented drupal_http_request() from returning an error when it receives a + 201 through 206 HTTP status code. +- Added support for autoloading traits via the registry on sites running PHP + 5.4 or higher. +- Allowed the user-picture.tpl.php theme template to have HTML classes besides + the default "user-picture" class printed in it (markup change). +- Fixed the URL text filter to convert e-mail addresses with plus signs into + mailto: links. +- Added alternate text to file icons displayed by the File module, to improve + accessibility (string change, and minor API addition to theme_file_icon()). +- Changed one-time login link failure messages to be displayed as errors or + warnings as appropriate, rather than as regular status messages (minor UI + change and data structure change). +- Changed the default settings.php configuration to exclude private files from + the "404_fast_paths" behavior. +- Changed the page that displays filter tips for a particular text format, for + example filter/tips/full_html, to return "page not found" or "access denied" + if the format does not exist or the user does not have access to it. This + change adds a new menu item to the Filter module's hook_menu() entry (minor + data structure change). +- Added a new hook, hook_block_cid_parts_alter(), to allow modules to alter the + cache keys used for caching a particular block. +- Made drupal_set_message() display and return messages when "0" is passed in + as the message to set. +- Fixed non-functional "Files displayed by default" setting on file fields. +- The "worker callback" provided in hook_cron_queue_info() and the "finished" + callback specified during batch processing can now be any PHP callable + instead of just functions. +- Prevented drupal_set_time_limit() from decreasing the time limit in the case + where the PHP maximum execution time is already unlimited. +- Changed the default thousand marker for numeric fields from a space ("1 000") + to nothing ("1000") (minor UI change: https://www.drupal.org/node/1388376). +- Prevented malformed theme .info files (without a "name" key) from causing + exceptions during menu rebuilds. If an .info file without a "name" key is + found in a module or theme directory, Drupal will now use the module or + theme's machine name as the display name instead. +- Made the format column in the {date_format_locale} database table + case-sensitive, to match the equivalent column in the {date_formats} table. +- Fixed a bug in the Statistics module that caused JavaScript files attached to + a node while it is being viewed to be omitted from the page. +- Added an optional 'project:' prefix that can be added to dependencies in a + module's .info file to indicate which project the dependency resides in (API + addition: https://www.drupal.org/node/2299747). +- Fixed various bugs that occurred after hooks were invoked early in the Drupal + bootstrap and that caused module_implements() and drupal_alter() to cache an + incomplete set of hook implementations for later use. +- Set the X-Content-Type-Options header to "nosniff" when possible, to prevent + certain web browsers from picking an unsafe MIME type. +- Prevented the database API from executing multiple queries at once on MySQL, + if the site's PHP version is new enough to do so. This is a secondary defense + against SQL injection (API change: https://www.drupal.org/node/2463973). +- Fixed a bug in the Drupal 6 to Drupal 7 upgrade path which caused the upgrade + to fail when there were multiple file records pointing to the same file. +- Numerous small bug fixes. +- Numerous API documentation improvements. +- Additional automated test coverage. + +Drupal 7.39, 2015-08-19 +----------------------- +- Fixed security issues (multiple vulnerabilities). See SA-CORE-2015-003. + +Drupal 7.38, 2015-06-17 +----------------------- +- Fixed security issues (multiple vulnerabilities). See SA-CORE-2015-002. + +Drupal 7.37, 2015-05-07 +----------------------- +- Fixed a regression in Drupal 7.36 which caused certain kinds of content types + to become disabled if they were defined by a no-longer-enabled module. +- Removed a confusing description regarding automatic time zone detection from + the user account form (minor UI and data structure change). +- Allowed custom HTML tags with a dash in the name to pass through filter_xss() + when specified in the list of allowed tags. +- Allowed hook_field_schema() implementations to specify indexes for fields + based on a fixed-length column prefix (rather than the entire column), as was + already allowed in hook_schema() implementations. +- Fixed PDO exceptions on PostgreSQL when accessing invalid entity URLs. +- Added a sites/all/libraries folder to the codebase, with instructions for + using it. +- Added a description to the "Administer text formats and filters" permission + on the Permissions page (string change). +- Numerous small bug fixes. +- Numerous API documentation improvements. +- Additional automated test coverage. + +Drupal 7.36, 2015-04-01 +----------------------- +- Added a 'file_public_schema' variable which allows modules that define + publicly-accessible streams in hook_stream_wrappers() to bypass file download + access checks when processing managed file upload fields. +- Fixed a bug that caused database query tags not to be added to search-related + database queries under many circumstances, and which prevented the + corresponding hook_query_TAG_alter() implementations from being called. +- Fixed the "for" attribute on managed file upload field labels to improve + accessibility (minor markup change). +- Added a 'javascript_always_use_jquery' variable which can be set to FALSE by + sites that may not need jQuery loaded on all pages, and a 'requires_jquery' + option to drupal_add_js() which modules can set to FALSE when adding + JavaScript files that have no dependency on jQuery (API addition: + https://www.drupal.org/node/2462717). +- Fixed incorrect foreign keys in the User module's role_permission and + users_roles database tables. +- Changed permission descriptions throughout Drupal core to consistently link + to relevant administrative pages, regardless of whether the user viewing the + Permissions page can view the page being linked to (minor UI change). +- Fixed the drupal_add_region_content() function so that it actually adds + content to the page. +- Added an 'image_suppress_itok_output' variable to allow sites already using + the existing 'image_allow_insecure_derivatives' variable to also prevent + security tokens from appearing in image derivative URLs. +- Fixed double-escaping of theme names in the Block module administrative + interface (minor string change). +- Added basic support for Xdebug when running automated tests. +- Fixed a bug which caused previewing a node to remove elements from the node + being edited. With this fix, calling node_preview() will no longer modify the + passed-in node object (minor API change). +- Added a user_has_role() function to check whether a user has a particular + role (API addition: https://www.drupal.org/node/2462411). +- Fixed installation failures when an opcode cache is enabled. +- Fixed a bug in the Drupal 6 to Drupal 7 upgrade path which caused private + files to be inaccessible. +- Fixed a bug in the Drupal 6 to Drupal 7 upgrade path which caused user + pictures to be lost. +- Fixed missing language code in hook_field_attach_view_alter() when it is + invoked from field_view_field(). +- Stopped sending ETag and Last-Modified headers for uncached page requests, + since they break caching for certain Varnish and Nginx configurations. +- Changed the Simpletest module to allow PSR-4 test classes to be used in + Drupal 7. +- Fixed a fatal error that occurred when using the Comment module's "Unpublish + comment containing keyword(s)" action. +- Changed the "lang" attribute on language links to "xml:lang" so it validates + as XHTML (minor markup change). +- Prevented the form API from allowing arrays to be submitted for various form + elements, such as textfields, textareas, and password fields (API change: + https://www.drupal.org/node/2462723). +- Fixed a bug in the Contact module which caused the global user object to have + the incorrect name and e-mail address during the remainder of the page + request after the contact form is submitted. +- Numerous small bug fixes. +- Numerous API documentation improvements. +- Additional automated test coverage. + +Drupal 7.35, 2015-03-18 +----------------------- +- Fixed security issues (multiple vulnerabilities). See SA-CORE-2015-001. + +Drupal 7.34, 2014-11-19 +----------------------- +- Fixed security issues (multiple vulnerabilities). See SA-CORE-2014-006. + +Drupal 7.33, 2014-11-07 +----------------------- +- Began storing the file modification time of each module and theme in the + {system} database table so that contributed modules can use it to identify + recently changed modules and themes (minor data structure change to the + return value of system_get_info() and other related functions). +- Added a "Did you mean?" feature to the run-tests.sh script for running + automated tests from the command line, to help developers who are attempting + to run a particular test class or group. +- Changed the date format used in various HTTP headers output by Drupal core + from RFC 1123 format to RFC 7231 format. +- Added a "block_cache_bypass_node_grants" variable to allow sites which have + node access modules enabled to use the block cache if desired (API addition). +- Made image derivative generation HTTP requests return a 404 error (rather + than a 500 error) when the source image does not exist. +- Fixed a bug which caused user pictures to be removed from the user object + after saving, and resulted in data loss if the user account was subsequently + re-saved. +- Fixed a bug in which field_has_data() did not return TRUE for fields that + only had data in older entity revisions, leading to loss of the field's data + when the field configuration was edited. +- Fixed a bug which caused the Ajax progress throbber to appear misaligned in + many situatons (minor styling change). +- Prevented the Bartik theme from lower-casing the "Permalink" link on + comments, for improved multilingual support (minor UI change). +- Added a "preferred_menu_links" tag to the database query that is used by + menu_link_get_preferred() to find the preferred menu link for a given path, + to make it easier to alter. +- Increased the maximum allowed length of block titles to 255 characters + (database schema change to the {block} table). +- Removed the Field module's field_modules_uninstalled() function, since it did + not do anything when it was invoked. +- Added a "theme_hook_original" variable to templates and theme functions and + an optional sitewide theme debug mode, to provide contextual information in + the page's HTML to theme developers. The theme debug mode is based on the one + used with Twig in Drupal 8 and can be accessed by setting the "theme_debug" + variable to TRUE (API addition). +- Added an entity_view_mode_prepare() API function to allow entity-defining + modules to properly invoke hook_entity_view_mode_alter(), and used it + throughout Drupal core to fix bugs with the invocation of that hook (API + change: https://www.drupal.org/node/2369141). +- Security improvement: Made the database API's orderBy() method sanitize the + sort direction ("ASC" or "DESC") for queries built with db_select(), so that + calling code does not have to. +- Changed the RDF module to consistently output RDF metadata for nodes and + comments near where the node is rendered in the HTML (minor markup and data + structure change). +- Added an HTML class to RDFa metatags throughout Drupal to prevent them from + accidentally affecting the site appearance (minor markup change). +- Fixed a bug in the Unicode requirements check which prevented installing + Drupal on PHP 5.6. +- Fixed a bug which caused drupal_get_bootstrap_phase() to abort the bootstrap + when called early in the page request. +- Renamed the "Search result" view mode to "Search result highlighting input" + to better reflect how it is used (UI change). +- Improved database queries generated by EntityFieldQuery in the case where + delta or language condition groups are used, to reduce the number of INNER + JOINs (this is a minor data structure change affecting code which implements + hook_query_alter() on these queries). +- Removed special-case behavior for file uploads which allowed user #1 to + bypass maximum file size and user quota limits. +- Numerous small bug fixes. +- Numerous API documentation improvements. +- Additional automated test coverage. + +Drupal 7.32, 2014-10-15 +----------------------- +- Fixed security issues (SQL injection). See SA-CORE-2014-005. + +Drupal 7.31, 2014-08-06 +----------------------- +- Fixed security issues (denial of service). See SA-CORE-2014-004. + +Drupal 7.30, 2014-07-24 +----------------------- +- Fixed a regression introduced in Drupal 7.29 that caused files or images + attached to taxonomy terms to be deleted when the taxonomy term was edited + and resaved (and other related bugs with contributed and custom modules). +- Added a warning on the permissions page to recommend restricting access to + the "View site reports" permission to trusted administrators. See + DRUPAL-PSA-2014-002. +- Numerous API documentation improvements. +- Additional automated test coverage. + +Drupal 7.29, 2014-07-16 +----------------------- +- Fixed security issues (multiple vulnerabilities). See SA-CORE-2014-003. + +Drupal 7.28, 2014-05-08 +----------------------- +- Fixed a regression introduced in Drupal 7.27 that caused JavaScript to break + on older browsers (such as Internet Explorer 8 and earlier) when Ajax was + used. +- Increased the timeout used by the Update Manager module when it fetches data + from drupal.org (from 5 seconds to 30 seconds), to work around a problem + which causes incomplete information about security updates to be presented to + site administrators. This fix may lead to a performance slowdown on the + Update Manager administration pages, when installing Drupal distributions, + and (for sites that use the automated cron feature) on occasional page loads + by site visitors. +- Fixed the behavior of the token system's "[node:summary]" token when the body + field does not have a manual summary. +- Changed the behavior of db_query_temporary() so that it works on SELECT + queries even when they have leading comments/whitespace. A side effect of + this fix is that db_query_temporary() will now fail with an error if it is + ever used on non-SELECT queries. +- Added a "node_admin_filter" tag to the database query used to build the list + of nodes on the content administration page, to make it easier to alter. +- Made the cron queue system log any exceptions that are thrown while an item + in the queue is being processed, rather than stopping the entire PHP request. +- Improved screen reader support by adding an aria-live HTML attribute to file + upload fields when there is an error uploading the file (minor markup + change). +- Made the pager on the Tracker module listing pages show the same number of + items as other pagers throughout Drupal core (minor UI change). +- Fixed a bug which caused caches not to be properly cleared when a file entity + was saved or deleted. +- Added several missing countries to the default list returned by + country_get_list() (string change). +- Replaced the term "weight" with "influence" in the content ranking settings + for search, and added help text for administrators (string change). +- Fixed untranslatable text strings in the administrative interface for the + "Crop" effect provided by the Image module (minor string change). +- Fixed a bug in the Taxonomy module update function introduced in Drupal 7.26 + that caused memory and CPU problems on sites with very large numbers of + unpublished nodes. +- Numerous small bug fixes. +- Numerous API documentation improvements. +- Additional automated test coverage. + +Drupal 7.27, 2014-04-16 +----------------------- +- Fixed security issues (information disclosure). See SA-CORE-2014-002. + +Drupal 7.26, 2014-01-15 +----------------------- +- Fixed security issues (multiple vulnerabilities). See SA-CORE-2014-001. + +Drupal 7.25, 2014-01-02 +----------------------- +- Fixed a bug in node_save() which prevented the saved node from being updated + in hook_node_insert() and other similar hooks. +- Added a meta tag to install.php to prevent it from being indexed by search + engines even when Drupal is installed in a subfolder (minor markup change). +- Fixed a bug in the database API that caused frequent deadlock errors when + running merge queries on some servers. +- Performance improvement: Prevented block rehashing from writing blocks to the + database on every cache clear and cron run when the blocks have not changed. + This fix results in an extra 'saved' key which is added and set to TRUE for + each block returned by _block_rehash() that actually is saved to the database + (data structure change). +- Added an optional 'skip on cron' parameter to hook_cron_queue_info() to allow + queues to avoid being automatically processed on cron runs (API addition). +- Fixed a bug which caused hook_block_view_MODULE_DELTA_alter() to never be + invoked if the block delta had a hyphen in it. To implement the hook when the + block delta has a hyphen, modules should now replace hyphens with underscores + when constructing the function name for the hook implementation. +- Fixed a bug which caused cached pages to sometimes be sent to the browser + with incorrect compression. The fix adds a new 'page_compressed' key to the + $cache->data array returned by drupal_page_get_cache() (minor data structure + change). +- Fixed broken tests on PHP 5.5. +- Made the File and Image modules more robust when saving entities that have + deleted files attached. The code in file_field_presave() will now remove the + record of the deleted file from the entity before saving (minor data + structure change). +- Standardized menu callback functions throughout Drupal core to return + MENU_NOT_FOUND and MENU_ACCESS_DENIED rather than printing their own "page + not found" or "access denied" pages (minor API change in the return value of + these functions under some circumstances). +- Fixed a bug in which caches were not properly cleared when a node was deleted + via the administrative interface. +- Changed the Bartik theme to render content contained in
,  and
+  similar tags in a larger font size, so it is easier to read.
+- Fixed a bug in the Search module that caused exceptions to be thrown during
+  searches if the server was not configured to represent decimal points as a
+  period.
+- Fixed a regression in the Image module that made image_style_url() not work
+  when a relative path (rather than a complete file URI) was passed to it.
+- Added an optional feature to the Statistics module to allow node views to be
+  tracked by Ajax requests rather than during the server-side generation of the
+  page. This allows the node counter to work on sites that use external page
+  caches (string change and new administrative option:
+  https://drupal.org/node/2164069).
+- Added a link to the drupal.org documentation page for cron to the Cron
+  settings page (string change).
+- Added a 'drupal_anonymous_user_object' variable to allow the anonymous user
+  object returned by drupal_anonymous_user() to be overridden with a classed
+  object (API addition).
+- Changed the database API to allow inserts based on a SELECT * query to work
+  correctly.
+- Changed the database schema of the {file_managed} table to allow Drupal to
+  manage files larger than 4 GB.
+- Changed the File module's hook_field_load() implementation to prevent file
+  entity properties which have the same name as file or image field properties
+  from overwriting the field properties (minor API change).
+- Numerous small bug fixes.
+- Numerous API documentation improvements.
+- Additional automated test coverage.
+
+Drupal 7.24, 2013-11-20
+-----------------------
+- Fixed security issues (multiple vulnerabilities), see SA-CORE-2013-003.
+
+Drupal 7.23, 2013-08-07
+-----------------------
+- 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
 ----------------------
@@ -73,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
@@ -258,7 +1299,7 @@
       requests.
 
 Drupal 6.23-dev, xxxx-xx-xx (development release)
------------------------
+---------------------------
 
 Drupal 6.22, 2011-05-25
 -----------------------
@@ -268,25 +1309,25 @@
 - Fixed a variety of other bugs.
 
 Drupal 6.21, 2011-05-25
-----------------------
+-----------------------
 - Fixed security issues (Cross site scripting), see SA-CORE-2011-001.
 
 Drupal 6.20, 2010-12-15
-----------------------
+-----------------------
 - Fixed a variety of small bugs, improved code documentation.
 
 Drupal 6.19, 2010-08-11
-----------------------
+-----------------------
 - Fixed a variety of small bugs, improved code documentation.
 
 Drupal 6.18, 2010-08-11
-----------------------
+-----------------------
 - Fixed security issues (OpenID authentication bypass, File download access
   bypass, Comment unpublishing bypass, Actions cross site scripting),
   see SA-CORE-2010-002.
 
 Drupal 6.17, 2010-06-02
-----------------------
+-----------------------
 - Improved PostgreSQL compatibility
 - Better PHP 5.3 and PHP 4 compatibility
 - Better browser compatibility of CSS and JS aggregation
@@ -295,24 +1336,24 @@
 - 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 install profiles and
+- 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
@@ -321,18 +1362,18 @@
 - Fixed a variety of small bugs.
 
 Drupal 6.13, 2009-07-01
-----------------------
+-----------------------
 - Fixed security issues (Cross site scripting, Input format access bypass and
   Password leakage in URL), see SA-CORE-2009-007.
 - Fixed a variety of small bugs.
 
 Drupal 6.12, 2009-05-13
-----------------------
+-----------------------
 - Fixed security issues (Cross site scripting), see SA-CORE-2009-006.
 - Fixed a variety of small bugs.
 
 Drupal 6.11, 2009-04-29
-----------------------
+-----------------------
 - Fixed security issues (Cross site scripting and limited information
   disclosure), see SA-CORE-2009-005
 - Fixed performance issues with the menu router cache, the update
@@ -340,7 +1381,7 @@
 - Fixed a variety of small bugs.
 
 Drupal 6.10, 2009-02-25
-----------------------
+-----------------------
 - Fixed a security issue, (Local file inclusion on Windows),
   see SA-CORE-2009-003
 - Fixed node_feed() so custom fields can show up in RSS feeds.
@@ -419,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
@@ -474,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.
@@ -653,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
@@ -713,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:
@@ -736,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
@@ -849,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
@@ -1017,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.5/COPYRIGHT.txt drupal-7.66/COPYRIGHT.txt
--- drupal-7.5/COPYRIGHT.txt	2011-07-27 22:17:40.000000000 +0200
+++ drupal-7.66/COPYRIGHT.txt	2019-04-17 22:20:46.000000000 +0200
@@ -1,5 +1,4 @@
-
-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
@@ -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.5/INSTALL.mysql.txt drupal-7.66/INSTALL.mysql.txt
--- drupal-7.5/INSTALL.mysql.txt	2011-07-27 22:17:40.000000000 +0200
+++ drupal-7.66/INSTALL.mysql.txt	2019-04-17 22:20:46.000000000 +0200
@@ -18,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.5/INSTALL.txt drupal-7.66/INSTALL.txt
--- drupal-7.5/INSTALL.txt	2011-07-27 22:17:40.000000000 +0200
+++ drupal-7.66/INSTALL.txt	2019-04-17 22:20:46.000000000 +0200
@@ -20,8 +20,10 @@
   - MySQL 5.0.15 (or greater) (http://www.mysql.com/).
   - MariaDB 5.1.44 (or greater) (http://mariadb.org/). MariaDB is a fully
     compatible drop-in replacement for MySQL.
+  - Percona Server 5.1.70 (or greater) (http://www.percona.com/). Percona
+    Server is a backwards-compatible replacement for MySQL.
   - PostgreSQL 8.3 (or greater) (http://www.postgresql.org/).
-  - SQLite 3.4.2 (or greater) (http://www.sqlite.org/).
+  - SQLite 3.3.7 (or greater) (http://www.sqlite.org/).
 
 For more detailed information about Drupal requirements, including a list of
 PHP extensions and configurations that are required, see "System requirements"
@@ -89,8 +91,8 @@
    - Download a translation file for the correct Drupal version and language
      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/
diff -Naur drupal-7.5/LICENSE.txt drupal-7.66/LICENSE.txt
--- drupal-7.5/LICENSE.txt	2011-02-24 01:47:51.000000000 +0100
+++ drupal-7.66/LICENSE.txt	2016-11-17 00:57:05.000000000 +0100
@@ -1,274 +1,339 @@
-GNU GENERAL PUBLIC LICENSE
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 2, June 1991
 
-              Version 2, June 1991
-
-Copyright (C) 1989, 1991 Free Software Foundation, Inc. 675 Mass Ave,
-Cambridge, MA 02139, USA. Everyone is permitted to copy and distribute
-verbatim copies of this license document, but changing it is not allowed.
-
-                  Preamble
-
-The licenses for most software are designed to take away your freedom to
-share and change it. By contrast, the GNU General Public License is
-intended to guarantee your freedom to share and change free software--to
-make sure the software is free for all its users. This General Public License
-applies to most of the Free Software Foundation's software and to any other
-program whose authors commit to using it. (Some other Free Software
-Foundation software is covered by the GNU Library General Public License
-instead.) You can apply it to your programs, too.
-
-When we speak of free software, we are referring to freedom, not price. Our
-General Public Licenses are designed to make sure that you have the
-freedom to distribute copies of free software (and charge for this service if
-you wish), that you receive source code or can get it if you want it, that you
-can change the software or use pieces of it in new free programs; and that
-you know you can do these things.
-
-To protect your rights, we need to make restrictions that forbid anyone to
-deny you these rights or to ask you to surrender the rights. These restrictions
-translate to certain responsibilities for you if you distribute copies of the
-software, or if you modify it.
-
-For example, if you distribute copies of such a program, whether gratis or for
-a fee, you must give the recipients all the rights that you have. You must make
-sure that they, too, receive or can get the source code. And you must show
-them these terms so they know their rights.
-
-We protect your rights with two steps: (1) copyright the software, and (2)
-offer you this license which gives you legal permission to copy, distribute
-and/or modify the software.
-
-Also, for each author's protection and ours, we want to make certain that
-everyone understands that there is no warranty for this free software. If the
-software is modified by someone else and passed on, we want its recipients
-to know that what they have is not the original, so that any problems
-introduced by others will not reflect on the original authors' reputations.
-
-Finally, any free program is threatened constantly by software patents. We
-wish to avoid the danger that redistributors of a free program will individually
-obtain patent licenses, in effect making the program proprietary. To prevent
-this, we have made it clear that any patent must be licensed for everyone's
-free use or not licensed at all.
-
-The precise terms and conditions for copying, distribution and modification
-follow.
-
-           GNU GENERAL PUBLIC LICENSE
- TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND
-               MODIFICATION
-
-0. This License applies to any program or other work which contains a notice
-placed by the copyright holder saying it may be distributed under the terms
-of this General Public License. The "Program", below, refers to any such
-program or work, and a "work based on the Program" means either the
-Program or any derivative work under copyright law: that is to say, a work
-containing the Program or a portion of it, either verbatim or with
-modifications and/or translated into another language. (Hereinafter, translation
-is included without limitation in the term "modification".) Each licensee is
-addressed as "you".
-
-Activities other than copying, distribution and modification are not covered
-by this License; they are outside its scope. The act of running the Program is
-not restricted, and the output from the Program is covered only if its contents
-constitute a work based on the Program (independent of having been made
-by running the Program). Whether that is true depends on what the Program
-does.
-
-1. You may copy and distribute verbatim copies of the Program's source
-code as you receive it, in any medium, provided that you conspicuously and
-appropriately publish on each copy an appropriate copyright notice and
-disclaimer of warranty; keep intact all the notices that refer to this License
-and to the absence of any warranty; and give any other recipients of the
-Program a copy of this License along with the Program.
-
-You may charge a fee for the physical act of transferring a copy, and you
-may at your option offer warranty protection in exchange for a fee.
-
-2. You may modify your copy or copies of the Program or any portion of it,
-thus forming a work based on the Program, and copy and distribute such
-modifications or work under the terms of Section 1 above, provided that you
-also meet all of these conditions:
-
-a) You must cause the modified files to carry prominent notices stating that
-you changed the files and the date of any change.
-
-b) You must cause any work that you distribute or publish, that in whole or in
-part contains or is derived from the Program or any part thereof, to be
-licensed as a whole at no charge to all third parties under the terms of this
-License.
-
-c) If the modified program normally reads commands interactively when run,
-you must cause it, when started running for such interactive use in the most
-ordinary way, to print or display an announcement including an appropriate
-copyright notice and a notice that there is no warranty (or else, saying that
-you provide a warranty) and that users may redistribute the program under
-these conditions, and telling the user how to view a copy of this License.
-(Exception: if the Program itself is interactive but does not normally print such
-an announcement, your work based on the Program is not required to print
-an announcement.)
-
-These requirements apply to the modified work as a whole. If identifiable
-sections of that work are not derived from the Program, and can be
-reasonably considered independent and separate works in themselves, then
-this License, and its terms, do not apply to those sections when you distribute
-them as separate works. But when you distribute the same sections as part
-of a whole which is a work based on the Program, the distribution of the
-whole must be on the terms of this License, whose permissions for other
-licensees extend to the entire whole, and thus to each and every part
-regardless of who wrote it.
-
-Thus, it is not the intent of this section to claim rights or contest your rights to
-work written entirely by you; rather, the intent is to exercise the right to
-control the distribution of derivative or collective works based on the
-Program.
+ 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
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
 
 In addition, mere aggregation of another work not based on the Program
-with the Program (or with a work based on the Program) on a volume of a
-storage or distribution medium does not bring the other work under the scope
-of this License.
-
-3. You may copy and distribute the Program (or a work based on it, under
-Section 2) in object code or executable form under the terms of Sections 1
-and 2 above provided that you also do one of the following:
-
-a) Accompany it with the complete corresponding machine-readable source
-code, which must be distributed under the terms of Sections 1 and 2 above
-on a medium customarily used for software interchange; or,
-
-b) Accompany it with a written offer, valid for at least three years, to give
-any third party, for a charge no more than your cost of physically performing
-source distribution, a complete machine-readable copy of the corresponding
-source code, to be distributed under the terms of Sections 1 and 2 above on
-a medium customarily used for software interchange; or,
-
-c) Accompany it with the information you received as to the offer to distribute
-corresponding source code. (This alternative is allowed only for
-noncommercial distribution and only if you received the program in object
-code or executable form with such an offer, in accord with Subsection b
-above.)
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
 
 The source code for a work means the preferred form of the work for
-making modifications to it. For an executable work, complete source code
-means all the source code for all modules it contains, plus any associated
-interface definition files, plus the scripts used to control compilation and
-installation of the executable. However, as a special exception, the source
-code distributed need not include anything that is normally distributed (in
-either source or binary form) with the major components (compiler, kernel,
-and so on) of the operating system on which the executable runs, unless that
-component itself accompanies the executable.
-
-If distribution of executable or object code is made by offering access to
-copy from a designated place, then offering equivalent access to copy the
-source code from the same place counts as distribution of the source code,
-even though third parties are not compelled to copy the source along with the
-object code.
-
-4. You may not copy, modify, sublicense, or distribute the Program except as
-expressly provided under this License. Any attempt otherwise to copy,
-modify, sublicense or distribute the Program is void, and will automatically
-terminate your rights under this License. However, parties who have received
-copies, or rights, from you under this License will not have their licenses
-terminated so long as such parties remain in full compliance.
-
-5. You are not required to accept this License, since you have not signed it.
-However, nothing else grants you permission to modify or distribute the
-Program or its derivative works. These actions are prohibited by law if you
-do not accept this License. Therefore, by modifying or distributing the
-Program (or any work based on the Program), you indicate your acceptance
-of this License to do so, and all its terms and conditions for copying,
-distributing or modifying the Program or works based on it.
-
-6. Each time you redistribute the Program (or any work based on the
-Program), the recipient automatically receives a license from the original
-licensor to copy, distribute or modify the Program subject to these terms and
-conditions. You may not impose any further restrictions on the recipients'
-exercise of the rights granted herein. You are not responsible for enforcing
-compliance by third parties to this License.
-
-7. If, as a consequence of a court judgment or allegation of patent
-infringement or for any other reason (not limited to patent issues), conditions
-are imposed on you (whether by court order, agreement or otherwise) that
-contradict the conditions of this License, they do not excuse you from the
-conditions of this License. If you cannot distribute so as to satisfy
-simultaneously your obligations under this License and any other pertinent
-obligations, then as a consequence you may not distribute the Program at all.
-For example, if a patent license would not permit royalty-free redistribution
-of the Program by all those who receive copies directly or indirectly through
-you, then the only way you could satisfy both it and this License would be to
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
 refrain entirely from distribution of the Program.
 
-If any portion of this section is held invalid or unenforceable under any
-particular circumstance, the balance of the section is intended to apply and
-the section as a whole is intended to apply in other circumstances.
-
-It is not the purpose of this section to induce you to infringe any patents or
-other property right claims or to contest validity of any such claims; this
-section has the sole purpose of protecting the integrity of the free software
-distribution system, which is implemented by public license practices. Many
-people have made generous contributions to the wide range of software
-distributed through that system in reliance on consistent application of that
-system; it is up to the author/donor to decide if he or she is willing to
-distribute software through any other system and a licensee cannot impose
-that choice.
-
-This section is intended to make thoroughly clear what is believed to be a
-consequence of the rest of this License.
-
-8. If the distribution and/or use of the Program is restricted in certain
-countries either by patents or by copyrighted interfaces, the original copyright
-holder who places the Program under this License may add an explicit
-geographical distribution limitation excluding those countries, so that
-distribution is permitted only in or among countries not thus excluded. In such
-case, this License incorporates the limitation as if written in the body of this
-License.
-
-9. The Free Software Foundation may publish revised and/or new versions
-of the General Public License from time to time. Such new versions will be
-similar in spirit to the present version, but may differ in detail to address new
-problems or concerns.
-
-Each version is given a distinguishing version number. If the Program specifies
-a version number of this License which applies to it and "any later version",
-you have the option of following the terms and conditions either of that
-version or of any later version published by the Free Software Foundation. If
-the Program does not specify a version number of this License, you may
-choose any version ever published by the Free Software Foundation.
-
-10. If you wish to incorporate parts of the Program into other free programs
-whose distribution conditions are different, write to the author to ask for
-permission. For software which is copyrighted by the Free Software
-Foundation, write to the Free Software Foundation; we sometimes make
-exceptions for this. Our decision will be guided by the two goals of
-preserving the free status of all derivatives of our free software and of
-promoting the sharing and reuse of software generally.
-
-               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 OTHERWISE
-STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
-OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT
-WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED,
-INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
-OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
-PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND
-PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
-PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
-NECESSARY SERVICING, REPAIR OR CORRECTION.
-
-12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR
-AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR
-ANY OTHER PARTY WHO MAY MODIFY AND/OR
-REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE
-LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL,
-SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES
-ARISING OUT OF THE USE OR INABILITY TO USE THE
-PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA
-OR DATA BEING RENDERED INACCURATE OR LOSSES
-SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE
-PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN
-IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF
-THE POSSIBILITY OF SUCH DAMAGES.
-
-          END OF TERMS AND CONDITIONS
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                            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
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+
+            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
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    
+    Copyright (C)   
+
+    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; 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 or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  , 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff -Naur drupal-7.5/MAINTAINERS.txt drupal-7.66/MAINTAINERS.txt
--- drupal-7.5/MAINTAINERS.txt	2011-07-27 22:17:40.000000000 +0200
+++ drupal-7.66/MAINTAINERS.txt	2019-04-17 22:20:46.000000000 +0200
@@ -1,146 +1,163 @@
 
-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
 ---------------------
 
+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' 
-- Randy Fay 'rfay' 
-- Earl Miles 'merlinofchaos' 
+- 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
@@ -150,143 +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
-- Vojtech Kusy 'wojtha' 
-- 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.5/README.txt drupal-7.66/README.txt
--- drupal-7.5/README.txt	2011-07-27 22:17:40.000000000 +0200
+++ drupal-7.66/README.txt	2019-04-17 22:20:46.000000000 +0200
@@ -4,6 +4,7 @@
 
  * About Drupal
  * Configuration and features
+ * Installation profiles
  * Appearance
  * Developing for Drupal
 
@@ -43,6 +44,40 @@
    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
 ----------
 
diff -Naur drupal-7.5/UPGRADE.txt drupal-7.66/UPGRADE.txt
--- drupal-7.5/UPGRADE.txt	2011-07-27 22:17:40.000000000 +0200
+++ drupal-7.66/UPGRADE.txt	2019-04-17 22:20:46.000000000 +0200
@@ -1,4 +1,3 @@
-
 INTRODUCTION
 ------------
 This document describes how to:
@@ -25,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
 ----------------
@@ -40,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
@@ -58,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
@@ -110,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
@@ -132,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.
@@ -149,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.
 
@@ -182,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.
 
@@ -205,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.5/authorize.php drupal-7.66/authorize.php
--- drupal-7.5/authorize.php	2011-07-27 22:17:40.000000000 +0200
+++ drupal-7.66/authorize.php	2019-04-17 22:20:46.000000000 +0200
@@ -4,16 +4,16 @@
  * @file
  * Administrative script for running authorized file operations.
  *
- * Using this script, the site owner (the user actually owning the files on
- * the webserver) can authorize certain file-related operations to proceed
- * with elevated privileges, for example to deploy and upgrade modules or
- * themes. Users should not visit this page directly, but instead use an
- * administrative user interface which knows how to redirect the user to this
- * script as part of a multistep process. This script actually performs the
- * selected operations without loading all of Drupal, to be able to more
- * gracefully recover from errors. Access to the script is controlled by a
- * global killswitch in settings.php ('allow_authorize_operations') and via
- * the 'administer software updates' permission.
+ * Using this script, the site owner (the user actually owning the files on the
+ * webserver) can authorize certain file-related operations to proceed with
+ * elevated privileges, for example to deploy and upgrade modules or themes.
+ * Users should not visit this page directly, but instead use an administrative
+ * user interface which knows how to redirect the user to this script as part of
+ * a multistep process. This script actually performs the selected operations
+ * without loading all of Drupal, to be able to more gracefully recover from
+ * errors. Access to the script is controlled by a global killswitch in
+ * settings.php ('allow_authorize_operations') and via the 'administer software
+ * updates' permission.
  *
  * There are helper functions for setting up an operation to run via this
  * system in modules/system/system.module. For more information, see:
@@ -21,21 +21,22 @@
  */
 
 /**
- * Root directory of Drupal installation.
+ * Defines the root directory of the Drupal installation.
  */
 define('DRUPAL_ROOT', getcwd());
 
 /**
- * Global flag to identify update.php and authorize.php runs, and so
- * avoid various unwanted operations, such as hook_init() and
- * hook_exit() invokes, css/js preprocessing and translation, and
- * solve some theming issues. This flag is checked on several places
- * in Drupal code (not just authorize.php).
+ * Global flag to identify update.php and authorize.php runs.
+ *
+ * Identifies update.php and authorize.php runs, avoiding unwanted operations
+ * such as hook_init() and hook_exit() invokes, css/js preprocessing and
+ * translation, and solves some theming issues. The flag is checked in other
+ * places in Drupal code (not just authorize.php).
  */
 define('MAINTENANCE_MODE', 'update');
 
 /**
- * Render a 403 access denied page for authorize.php
+ * Renders a 403 access denied page for authorize.php.
  */
 function authorize_access_denied_page() {
   drupal_add_http_header('Status', '403 Forbidden');
@@ -45,13 +46,13 @@
 }
 
 /**
- * Determine if the current user is allowed to run authorize.php.
+ * Determines if the current user is allowed to run authorize.php.
  *
  * The killswitch in settings.php overrides all else, otherwise, the user must
  * have access to the 'administer software updates' permission.
  *
  * @return
- *   TRUE if the current user can run authorize.php, otherwise FALSE.
+ *   TRUE if the current user can run authorize.php, and FALSE if not.
  */
 function authorize_access_allowed() {
   return variable_get('allow_authorize_operations', TRUE) && user_access('administer software updates');
@@ -60,7 +61,6 @@
 // *** Real work of the script begins here. ***
 
 require_once DRUPAL_ROOT . '/includes/bootstrap.inc';
-require_once DRUPAL_ROOT . '/includes/session.inc';
 require_once DRUPAL_ROOT . '/includes/common.inc';
 require_once DRUPAL_ROOT . '/includes/file.inc';
 require_once DRUPAL_ROOT . '/includes/module.inc';
@@ -74,7 +74,7 @@
 global $conf;
 
 // We have to enable the user and system modules, even to check access and
-// display errors via the maintainence theme.
+// display errors via the maintenance theme.
 $module_list['system']['filename'] = 'modules/system/system.module';
 $module_list['user']['filename'] = 'modules/user/user.module';
 module_list(TRUE, FALSE, FALSE, $module_list);
@@ -145,7 +145,7 @@
         l(t('Front page'), ''),
       ));
     }
-	
+
     $output .= theme('item_list', array('items' => $links, 'title' => t('Next steps')));
   }
   // If a batch is running, let it run.
@@ -172,4 +172,3 @@
 if (!empty($output)) {
   print theme('update_page', array('content' => $output, 'show_messages' => $show_messages));
 }
-
diff -Naur drupal-7.5/includes/actions.inc drupal-7.66/includes/actions.inc
--- drupal-7.5/includes/actions.inc	2011-07-27 22:17:40.000000000 +0200
+++ drupal-7.66/includes/actions.inc	2019-04-17 22:20:46.000000000 +0200
@@ -22,7 +22,7 @@
  * - $a1, $a2: Optional additional information, which can be passed into
  *   actions_do() and will be passed along to the action function.
  *
- * @} End of "defgroup actions".
+ * @}
  */
 
 /**
@@ -48,6 +48,7 @@
  *   Passed along to the callback.
  * @param $a2
  *   Passed along to the callback.
+ *
  * @return
  *   An associative array containing the results of the functions that
  *   perform the actions, keyed on action ID.
@@ -149,6 +150,7 @@
  *
  * @param $reset
  *   Reset the action info static cache.
+ *
  * @return
  *   An associative array keyed on action function name, with the same format
  *   as the return value of hook_action_info(), containing all
@@ -176,9 +178,9 @@
  * function and the actions returned by actions_list() are partially
  * synchronized. Non-configurable actions from hook_action_info()
  * implementations are put into the database when actions_synchronize() is
- * called, which happens when admin/config/system/actions is visited. Configurable
- * actions are not added to the database until they are configured in the
- * user interface, in which case a database row is created for each
+ * called, which happens when admin/config/system/actions is visited.
+ * Configurable actions are not added to the database until they are configured
+ * in the user interface, in which case a database row is created for each
  * configuration of each action.
  *
  * @return
@@ -205,6 +207,7 @@
  *   An associative array with function names or action IDs as keys
  *   and associative arrays with keys 'label', 'type', etc. as values.
  *   This is usually the output of actions_list() or actions_get_all_actions().
+ *
  * @return
  *   An associative array whose keys are hashes of the input array keys, and
  *   whose corresponding values are associative arrays with components
@@ -223,7 +226,7 @@
 }
 
 /**
- * Given a hash of an action array key, returns the key (function or ID).
+ * Returns an action array key (function or ID), given its hash.
  *
  * Faster than actions_actions_map() when you only need the function name or ID.
  *
@@ -231,6 +234,7 @@
  *   Hash of a function name or action ID array key. The array key
  *   is a key into the return value of actions_list() (array key is the action
  *   function name) or actions_get_all_actions() (array key is the action ID).
+ *
  * @return
  *   The corresponding array key, or FALSE if no match is found.
  */
@@ -311,7 +315,7 @@
       $link = l(t('Remove orphaned actions'), 'admin/config/system/actions/orphan');
       $count = count($actions_in_db);
       $orphans = implode(', ', $orphaned);
-      watchdog('actions', '@count orphaned actions (%orphans) exist in the actions table. !link', array('@count' => $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);
     }
   }
 }
@@ -332,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.
  */
@@ -361,6 +366,7 @@
  *
  * @param $aid
  *   The ID of the action to retrieve.
+ *
  * @return
  *   The appropriate action row from the database as an object.
  */
@@ -380,4 +386,3 @@
     ->execute();
   module_invoke_all('actions_delete', $aid);
 }
-
diff -Naur drupal-7.5/includes/ajax.inc drupal-7.66/includes/ajax.inc
--- drupal-7.5/includes/ajax.inc	2011-07-27 22:17:40.000000000 +0200
+++ drupal-7.66/includes/ajax.inc	2019-04-17 22:20:46.000000000 +0200
@@ -24,7 +24,8 @@
  * ajax_form_callback() and a defined #ajax['callback'] function.
  * However, you may optionally specify a different path to request or a
  * different callback function to invoke, which can return updated HTML or can
- * also return a richer set of @link ajax_commands Ajax framework commands @endlink.
+ * also return a richer set of
+ * @link ajax_commands Ajax framework commands @endlink.
  *
  * Standard form handling is as follows:
  *   - A form element has a #ajax property that includes #ajax['callback'] and
@@ -101,7 +102,7 @@
  * In the above example, the 'changethis' element is Ajax-enabled. The default
  * #ajax['event'] is 'change', so when the 'changethis' element changes,
  * an Ajax call is made. The form is submitted and reprocessed, and then the
- * callback is called.  In this case, the form has been automatically
+ * callback is called. In this case, the form has been automatically
  * built changing $form['replace_textfield']['#description'], so the callback
  * just returns that part of the form.
  *
@@ -143,6 +144,21 @@
  * - #ajax['event']: The JavaScript event to respond to. This is normally
  *   selected automatically for the type of form widget being used, and
  *   is only needed if you need to override the default behavior.
+ * - #ajax['prevent']: A JavaScript event to prevent when 'event' is triggered.
+ *   Defaults to 'click' for #ajax on #type 'submit', 'button', and
+ *   'image_button'. Multiple events may be specified separated by spaces.
+ *   For example, when binding #ajax behaviors to form buttons, 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. Because of that, Ajax behaviors are bound
+ *   to the 'mousedown' event on form buttons by default. However, 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. For this case, 'prevent' can be set to 'click', so an
+ *   additional event handler is bound to prevent such a click from triggering a
+ *   non-Ajax form submission. This also prevents a textfield's ENTER press
+ *   triggering a button's non-Ajax form submission behavior.
  * - #ajax['method']: The jQuery method to use to place the new HTML.
  *   Defaults to 'replaceWith'. May be: 'replaceWith', 'append', 'prepend',
  *   'before', 'after', or 'html'. See the
@@ -152,7 +168,7 @@
  *   displayed while awaiting a response from the callback, and add an optional
  *   message. Possible keys: 'type', 'message', 'url', 'interval'.
  *   More information is available in the
- *   @link http://api.drupal.org/api/drupal/developer--topics--forms_api_reference.html/7 Form API Reference @endlink
+ *   @link forms_api_reference.html Form API Reference @endlink
  *
  * In addition to using Form API for doing in-form modification, Ajax may be
  * enabled by adding classes to buttons and links. By adding the 'use-ajax'
@@ -173,11 +189,11 @@
  * be converted to a JSON object and returned to the client, which will then
  * iterate over the array and process it like a macro language.
  *
- * Each command item is an associative array which will be converted to a command
- * object on the JavaScript side. $command_item['command'] is the type of
- * command, e.g. 'alert' or 'replace', and will correspond to a method in the
- * Drupal.ajax[command] space. The command array may contain any other data
- * that the command needs to process, e.g. 'method', 'selector', 'settings', etc.
+ * Each command item is an associative array which will be converted to a
+ * command object on the JavaScript side. $command_item['command'] is the type
+ * of command, e.g. 'alert' or 'replace', and will correspond to a method in the
+ * Drupal.ajax[command] space. The command array may contain any other data that
+ * the command needs to process, e.g. 'method', 'selector', 'settings', etc.
  *
  * Commands are usually created with a couple of helper functions, so they
  * look like this:
@@ -195,7 +211,7 @@
  *
  * When returning an Ajax command array, it is often useful to have
  * status messages rendered along with other tasks in the command array.
- * In that case the the Ajax commands array may be constructed like this:
+ * In that case the Ajax commands array may be constructed like this:
  * @code
  *   $commands = array();
  *   $commands[] = ajax_command_replace(NULL, $output);
@@ -207,13 +223,17 @@
  */
 
 /**
- * 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()) {
+  // 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
@@ -235,8 +255,8 @@
       //   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.
+      //   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]);
@@ -247,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);
@@ -278,12 +292,11 @@
     $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.
@@ -293,16 +306,17 @@
 }
 
 /**
- * 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
  * 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() {
@@ -322,6 +336,17 @@
     drupal_exit();
   }
 
+  // When a page level cache is enabled, the form-build id might have been
+  // replaced from within form_get_cache. If this is the case, it is also
+  // necessary to update it in the browser by issuing an appropriate Ajax
+  // command.
+  $commands = array();
+  if (isset($form['#build_id_old']) && $form['#build_id_old'] != $form['#build_id']) {
+    // If the form build ID has changed, issue an Ajax command to update it.
+    $commands[] = ajax_command_update_build_id($form);
+    $form_build_id = $form['#build_id'];
+  }
+
   // Since some of the submit handlers are run, redirects need to be disabled.
   $form_state['no_redirect'] = TRUE;
 
@@ -336,7 +361,7 @@
   $form_state['input'] = $_POST;
   $form_id = $form['#form_id'];
 
-  return array($form, $form_state, $form_id, $form_build_id);
+  return array($form, $form_state, $form_id, $form_build_id, $commands);
 }
 
 /**
@@ -353,9 +378,11 @@
  * #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
@@ -367,8 +394,20 @@
   if (!empty($form_state['triggering_element'])) {
     $callback = $form_state['triggering_element']['#ajax']['callback'];
   }
-  if (!empty($callback) && function_exists($callback)) {
-    return $callback($form, $form_state);
+  if (!empty($callback) && is_callable($callback)) {
+    $result = $callback($form, $form_state);
+
+    if (!(is_array($result) && isset($result['#type']) && $result['#type'] == 'ajax')) {
+      // Turn the response into a #type=ajax array if it isn't one already.
+      $result = array(
+        '#type' => 'ajax',
+        '#commands' => ajax_prepare_response($result),
+      );
+    }
+
+    $result['#commands'] = array_merge($commands, $result['#commands']);
+
+    return $result;
   }
 }
 
@@ -388,6 +427,9 @@
  * 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.
+ *
+ * @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'])) {
@@ -406,7 +448,7 @@
 }
 
 /**
- * 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
  * requests. Like that function, it:
@@ -449,6 +491,9 @@
     }
   }
 
+  // Let ajax.js know that this response is safe to process.
+  ajax_set_verification_header();
+
   // Print the response.
   $commands = ajax_prepare_response($page_callback_result);
   $json = ajax_render($commands);
@@ -539,7 +584,30 @@
 }
 
 /**
- * Perform end-of-Ajax-request tasks.
+ * Sets a response header for ajax.js to trust the response body.
+ *
+ * It is not safe to invoke Ajax commands within user-uploaded files, so this
+ * header protects against those being invoked.
+ *
+ * @see Drupal.ajax.options.success()
+ */
+function ajax_set_verification_header() {
+  $added = &drupal_static(__FUNCTION__);
+
+  // User-uploaded files cannot set any response headers, so a custom header is
+  // used to indicate to ajax.js that this response is safe. Note that most
+  // Ajax requests bound using the Form API will be protected by having the URL
+  // flagged as trusted in Drupal.settings, so this header is used only for
+  // things like custom markup that gets Ajax behaviors attached.
+  if (empty($added)) {
+    drupal_add_http_header('X-Drupal-Ajax-Token', '1');
+    // Avoid sending the header twice.
+    $added = TRUE;
+  }
+}
+
+/**
+ * Performs end-of-Ajax-request tasks.
  *
  * This function is the equivalent of drupal_page_footer(), but for Ajax
  * requests.
@@ -562,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.
@@ -581,7 +649,7 @@
 }
 
 /**
- * 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
@@ -591,6 +659,7 @@
  *   An associative array containing the properties of the element.
  *   Properties used:
  *   - #ajax['event']
+ *   - #ajax['prevent']
  *   - #ajax['path']
  *   - #ajax['options']
  *   - #ajax['wrapper']
@@ -619,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':
@@ -712,7 +794,12 @@
 
     $element['#attached']['js'][] = array(
       'type' => 'setting',
-      'data' => array('ajax' => array($element['#id'] => $settings)),
+      'data' => array(
+        'ajax' => array($element['#id'] => $settings),
+        'urlIsAjaxTrusted' => array(
+          $settings['url'] => TRUE,
+        ),
+      ),
     );
 
     // Indicate that Ajax processing was successful.
@@ -808,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(
@@ -1182,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.5/includes/archiver.inc drupal-7.66/includes/archiver.inc
--- drupal-7.5/includes/archiver.inc	2011-07-27 22:17:40.000000000 +0200
+++ drupal-7.66/includes/archiver.inc	2019-04-17 22:20:46.000000000 +0200
@@ -6,61 +6,63 @@
  */
 
 /**
- * Common interface for all Archiver classes.
+ * Defines the common interface for all Archiver classes.
  */
 interface ArchiverInterface {
 
   /**
-   * Constructor for a new archiver instance.
+   * Constructs a new archiver instance.
    *
    * @param $file_path
-   *   The full system path of the archive to manipulate.  Only local files
-   *   are supported.  If the file does not yet exist, it will be created if
+   *   The full system path of the archive to manipulate. Only local files
+   *   are supported. If the file does not yet exist, it will be created if
    *   appropriate.
    */
   public function __construct($file_path);
 
   /**
-   * Add the specified file or directory to the archive.
+   * Adds the specified file or directory to the archive.
    *
    * @param $file_path
    *   The full system path of the file or directory to add. Only local files
    *   and directories are supported.
+   *
    * @return ArchiverInterface
    *   The called object.
    */
   public function add($file_path);
 
   /**
-   * Remove the specified file from the archive.
+   * Removes the specified file from the archive.
    *
    * @param $path
    *   The file name relative to the root of the archive to remove.
+   *
    * @return ArchiverInterface
    *   The called object.
    */
   public function remove($path);
 
   /**
-   * Extract multiple files in the archive to the specified path.
+   * Extracts multiple files in the archive to the specified path.
    *
    * @param $path
    *   A full system path of the directory to which to extract files.
    * @param $files
    *   Optionally specify a list of files to be extracted. Files are
    *   relative to the root of the archive. If not specified, all files
-   *   in the archive will be extracted
+   *   in the archive will be extracted.
+   *
    * @return ArchiverInterface
    *   The called object.
    */
-  public function extract($path, Array $files = array());
+  public function extract($path, array $files = array());
 
   /**
-   * List all files in the archive.
+   * Lists all files in the archive.
    *
    * @return
    *   An array of file names relative to the root of the archive.
    */
   public function listContents();
 }
-
diff -Naur drupal-7.5/includes/authorize.inc drupal-7.66/includes/authorize.inc
--- drupal-7.5/includes/authorize.inc	2011-07-27 22:17:40.000000000 +0200
+++ drupal-7.66/includes/authorize.inc	2019-04-17 22:20:46.000000000 +0200
@@ -6,13 +6,19 @@
  */
 
 /**
- * Build the form for choosing a FileTransfer type and supplying credentials.
+ * Form constructor for the file transfer authorization form.
+ *
+ * Allows the user to choose a FileTransfer type and supply credentials.
+ *
+ * @see authorize_filetransfer_form_validate()
+ * @see authorize_filetransfer_form_submit()
+ * @ingroup forms
  */
 function authorize_filetransfer_form($form, &$form_state) {
   global $base_url, $is_https;
   $form = array();
 
-  // If possible, we want to post this form securely via https.
+  // If possible, we want to post this form securely via HTTPS.
   $form['#https'] = TRUE;
 
   // CSS we depend on lives in modules/system/maintenance.css, which is loaded
@@ -127,10 +133,11 @@
 }
 
 /**
- * Generate the Form API array for the settings for a given connection backend.
+ * Generates the Form API array for a given connection backend's settings.
  *
  * @param $backend
  *   The name of the backend (e.g. 'ftp', 'ssh', etc).
+ *
  * @return
  *   Form API array of connection settings for the given backend.
  *
@@ -151,7 +158,7 @@
 }
 
 /**
- * Recursively fill in the default settings on a file transfer connection form.
+ * Sets the default settings on a file transfer connection form recursively.
  *
  * The default settings for the file transfer connection forms are saved in
  * the database. The settings are stored as a nested array in the case of a
@@ -165,8 +172,6 @@
  *   The key for our current form element, if any.
  * @param array $defaults
  *   The default settings for the file transfer backend we're operating on.
- * @return
- *   Nothing, this function just sets $element['#default_value'] if needed.
  */
 function _authorize_filetransfer_connection_settings_set_defaults(&$element, $key, array $defaults) {
   // If we're operating on a form element which isn't a fieldset, and we have
@@ -186,14 +191,15 @@
 }
 
 /**
- * Validate callback for the filetransfer authorization form.
+ * Form validation handler for authorize_filetransfer_form().
  *
  * @see authorize_filetransfer_form()
+ * @see authorize_filetransfer_submit()
  */
 function authorize_filetransfer_form_validate($form, &$form_state) {
   // Only validate the form if we have collected all of the user input and are
   // ready to proceed with updating or installing.
-  if ($form_state['clicked_button']['#name'] != 'process_updates') {
+  if ($form_state['triggering_element']['#name'] != 'process_updates') {
     return;
   }
 
@@ -218,13 +224,14 @@
 }
 
 /**
- * Submit callback when a file transfer is being authorized.
+ * Form submission handler for authorize_filetransfer_form().
  *
  * @see authorize_filetransfer_form()
+ * @see authorize_filetransfer_validate()
  */
 function authorize_filetransfer_form_submit($form, &$form_state) {
   global $base_url;
-  switch ($form_state['clicked_button']['#name']) {
+  switch ($form_state['triggering_element']['#name']) {
     case 'process_updates':
 
       // Save the connection settings to the DB.
@@ -280,7 +287,7 @@
 }
 
 /**
- * Run the operation specified in $_SESSION['authorize_operation']
+ * Runs the operation specified in $_SESSION['authorize_operation'].
  *
  * @param $filetransfer
  *   The FileTransfer object to use for running the operation.
@@ -298,12 +305,13 @@
 }
 
 /**
- * Get a FileTransfer class for a specific transfer method and settings.
+ * Gets a FileTransfer class for a specific transfer method and settings.
  *
  * @param $backend
  *   The FileTransfer backend to get the class for.
  * @param $settings
  *   Array of settings for the FileTransfer.
+ *
  * @return
  *   An instantiated FileTransfer object for the requested method and settings,
  *   or FALSE if there was an error finding or instantiating it.
diff -Naur drupal-7.5/includes/batch.inc drupal-7.66/includes/batch.inc
--- drupal-7.5/includes/batch.inc	2011-07-27 22:17:40.000000000 +0200
+++ drupal-7.66/includes/batch.inc	2019-04-17 22:20:46.000000000 +0200
@@ -1,6 +1,5 @@
 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));
       }
     }
   }
@@ -521,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()) {
@@ -531,4 +537,3 @@
       ->execute();
   }
 }
-
diff -Naur drupal-7.5/includes/batch.queue.inc drupal-7.66/includes/batch.queue.inc
--- drupal-7.5/includes/batch.queue.inc	2011-07-27 22:17:40.000000000 +0200
+++ drupal-7.66/includes/batch.queue.inc	2019-04-17 22:20:46.000000000 +0200
@@ -1,24 +1,30 @@
  $this->name))->fetchObject();
     if ($item) {
@@ -29,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();
@@ -44,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);
@@ -57,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.5/includes/bootstrap.inc drupal-7.66/includes/bootstrap.inc
--- drupal-7.5/includes/bootstrap.inc	2011-07-27 22:17:40.000000000 +0200
+++ drupal-7.66/includes/bootstrap.inc	2019-04-17 22:20:46.000000000 +0200
@@ -8,7 +8,7 @@
 /**
  * The current system version.
  */
-define('VERSION', '7.5');
+define('VERSION', '7.66');
 
 /**
  * Core API compatibility.
@@ -26,6 +26,21 @@
 define('DRUPAL_MINIMUM_PHP_MEMORY_LIMIT', '32M');
 
 /**
+ * Error reporting level: display no errors.
+ */
+define('ERROR_REPORTING_HIDE', 0);
+
+/**
+ * Error reporting level: display errors and warnings.
+ */
+define('ERROR_REPORTING_DISPLAY_SOME', 1);
+
+/**
+ * Error reporting level: display all messages.
+ */
+define('ERROR_REPORTING_DISPLAY_ALL', 2);
+
+/**
  * Indicates that the item should never be removed unless explicitly selected.
  *
  * The item may be removed using cache_clear_all() with a cache ID.
@@ -38,94 +53,70 @@
 define('CACHE_TEMPORARY', -1);
 
 /**
- * Log message severity -- Emergency: system is unusable.
+ * @defgroup logging_severity_levels Logging severity levels
+ * @{
+ * Logging severity levels as defined in RFC 3164.
  *
  * The WATCHDOG_* constant definitions correspond to the logging severity levels
- * defined in RFC 3164, section 4.1.1: http://www.faqs.org/rfcs/rfc3164.html
- *
+ * defined in RFC 3164, section 4.1.1. PHP supplies predefined LOG_* constants
+ * for use in the syslog() function, but their values on Windows builds do not
+ * correspond to RFC 3164. The associated PHP bug report was closed with the
+ * comment, "And it's also not a bug, as Windows just have less log levels,"
+ * and "So the behavior you're seeing is perfectly normal."
+ *
+ * @see http://www.faqs.org/rfcs/rfc3164.html
+ * @see http://bugs.php.net/bug.php?id=18090
+ * @see http://php.net/manual/function.syslog.php
+ * @see http://php.net/manual/network.constants.php
  * @see watchdog()
  * @see watchdog_severity_levels()
  */
+
+/**
+ * Log message severity -- Emergency: system is unusable.
+ */
 define('WATCHDOG_EMERGENCY', 0);
 
 /**
  * Log message severity -- Alert: action must be taken immediately.
- *
- * The WATCHDOG_* constant definitions correspond to the logging severity levels
- * defined in RFC 3164, section 4.1.1: http://www.faqs.org/rfcs/rfc3164.html
- *
- * @see watchdog()
- * @see watchdog_severity_levels()
  */
 define('WATCHDOG_ALERT', 1);
 
 /**
- * Log message severity -- Critical: critical conditions.
- *
- * The WATCHDOG_* constant definitions correspond to the logging severity levels
- * defined in RFC 3164, section 4.1.1: http://www.faqs.org/rfcs/rfc3164.html
- *
- * @see watchdog()
- * @see watchdog_severity_levels()
+ * Log message severity -- Critical conditions.
  */
 define('WATCHDOG_CRITICAL', 2);
 
 /**
- * Log message severity -- Error: error conditions.
- *
- * The WATCHDOG_* constant definitions correspond to the logging severity levels
- * defined in RFC 3164, section 4.1.1: http://www.faqs.org/rfcs/rfc3164.html
- *
- * @see watchdog()
- * @see watchdog_severity_levels()
+ * Log message severity -- Error conditions.
  */
 define('WATCHDOG_ERROR', 3);
 
 /**
- * Log message severity -- Warning: warning conditions.
- *
- * The WATCHDOG_* constant definitions correspond to the logging severity levels
- * defined in RFC 3164, section 4.1.1: http://www.faqs.org/rfcs/rfc3164.html
- *
- * @see watchdog()
- * @see watchdog_severity_levels()
+ * Log message severity -- Warning conditions.
  */
 define('WATCHDOG_WARNING', 4);
 
 /**
- * Log message severity -- Notice: normal but significant condition.
- *
- * The WATCHDOG_* constant definitions correspond to the logging severity levels
- * defined in RFC 3164, section 4.1.1: http://www.faqs.org/rfcs/rfc3164.html
- *
- * @see watchdog()
- * @see watchdog_severity_levels()
+ * Log message severity -- Normal but significant conditions.
  */
 define('WATCHDOG_NOTICE', 5);
 
 /**
- * Log message severity -- Informational: informational messages.
- *
- * The WATCHDOG_* constant definitions correspond to the logging severity levels
- * defined in RFC 3164, section 4.1.1: http://www.faqs.org/rfcs/rfc3164.html
- *
- * @see watchdog()
- * @see watchdog_severity_levels()
+ * Log message severity -- Informational messages.
  */
 define('WATCHDOG_INFO', 6);
 
 /**
- * Log message severity -- Debug: debug-level messages.
- *
- * The WATCHDOG_* constant definitions correspond to the logging severity levels
- * defined in RFC 3164, section 4.1.1: http://www.faqs.org/rfcs/rfc3164.html
- *
- * @see watchdog()
- * @see watchdog_severity_levels()
+ * Log message severity -- Debug-level messages.
  */
 define('WATCHDOG_DEBUG', 7);
 
 /**
+ * @} End of "defgroup logging_severity_levels".
+ */
+
+/**
  * First bootstrap phase: initialize configuration.
  */
 define('DRUPAL_BOOTSTRAP_CONFIGURATION', 0);
@@ -161,8 +152,7 @@
 define('DRUPAL_BOOTSTRAP_LANGUAGE', 6);
 
 /**
- * Final bootstrap phase: Drupal is fully loaded; validate and fix
- * input data.
+ * Final bootstrap phase: Drupal is fully loaded; validate and fix input data.
  */
 define('DRUPAL_BOOTSTRAP_FULL', 7);
 
@@ -177,8 +167,9 @@
 define('DRUPAL_AUTHENTICATED_RID', 2);
 
 /**
- * The number of bytes in a kilobyte. For more information, visit
- * http://en.wikipedia.org/wiki/Kilobyte.
+ * The number of bytes in a kilobyte.
+ *
+ * For more information, visit http://en.wikipedia.org/wiki/Kilobyte.
  */
 define('DRUPAL_KILOBYTE', 1024);
 
@@ -215,17 +206,28 @@
 define('LANGUAGE_RTL', 1);
 
 /**
- * For convenience, define a short form of the request time global.
+ * Time of the current request in seconds elapsed since the Unix Epoch.
+ *
+ * This differs from $_SERVER['REQUEST_TIME'], which is stored as a float
+ * since PHP 5.4.0. Float timestamps confuse most PHP functions
+ * (including date_create()).
+ *
+ * @see http://php.net/manual/reserved.variables.server.php
+ * @see http://php.net/manual/function.time.php
  */
-define('REQUEST_TIME', $_SERVER['REQUEST_TIME']);
+define('REQUEST_TIME', (int) $_SERVER['REQUEST_TIME']);
 
 /**
- * Flag for drupal_set_title(); text is not sanitized, so run check_plain().
+ * Flag used to indicate that text is not sanitized, so run check_plain().
+ *
+ * @see drupal_set_title()
  */
 define('CHECK_PLAIN', 0);
 
 /**
- * Flag for drupal_set_title(); text has already been sanitized.
+ * Flag used to indicate that text has already been sanitized.
+ *
+ * @see drupal_set_title()
  */
 define('PASS_THROUGH', -1);
 
@@ -242,15 +244,231 @@
 /**
  * Regular expression to match PHP function names.
  *
- * @see http://php.net/manual/en/language.functions.php
+ * @see http://php.net/manual/language.functions.php
  */
 define('DRUPAL_PHP_FUNCTION_PATTERN', '[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*');
 
 /**
- * Start the timer with the specified name. If you start and stop the same
- * timer multiple times, the measured intervals will be accumulated.
+ * A RFC7231 Compliant date.
+ *
+ * http://tools.ietf.org/html/rfc7231#section-7.1.1.1
  *
- * @param name
+ * Example: Sun, 06 Nov 1994 08:49:37 GMT
+ *
+ * This constant was introduced in PHP 7.0.19 and PHP 7.1.5 but needs to be
+ * defined by Drupal for earlier PHP versions.
+ */
+if (!defined('DATE_RFC7231')) {
+  define('DATE_RFC7231', 'D, d M Y H:i:s \G\M\T');
+}
+
+/**
+ * Provides a caching wrapper to be used in place of large array structures.
+ *
+ * This class should be extended by systems that need to cache large amounts
+ * of data and have it represented as an array to calling functions. These
+ * arrays can become very large, so ArrayAccess is used to allow different
+ * strategies to be used for caching internally (lazy loading, building caches
+ * over time etc.). This can dramatically reduce the amount of data that needs
+ * to be loaded from cache backends on each request, and memory usage from
+ * static caches of that same data.
+ *
+ * Note that array_* functions do not work with ArrayAccess. Systems using
+ * DrupalCacheArray should use this only internally. If providing API functions
+ * that return the full array, this can be cached separately or returned
+ * directly. However since DrupalCacheArray holds partial content by design, it
+ * should be a normal PHP array or otherwise contain the full structure.
+ *
+ * Note also that due to limitations in PHP prior to 5.3.4, it is impossible to
+ * write directly to the contents of nested arrays contained in this object.
+ * Only writes to the top-level array elements are possible. So if you
+ * previously had set $object['foo'] = array(1, 2, 'bar' => '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) {
@@ -261,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.
  */
@@ -284,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).
@@ -311,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__, '');
@@ -408,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.
@@ -470,7 +647,7 @@
 }
 
 /**
- * Initialize PHP environment.
+ * Initializes the PHP environment.
  */
 function drupal_environment_initialize() {
   if (!isset($_SERVER['HTTP_REFERER'])) {
@@ -519,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'] = '';
     }
@@ -566,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'];
 
@@ -592,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().
@@ -626,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
@@ -643,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);
     }
   }
 
@@ -722,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
@@ -771,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()
@@ -833,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
@@ -865,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.
@@ -884,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.
@@ -906,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).
@@ -939,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).
@@ -975,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.
@@ -996,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).
  */
@@ -1009,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);
@@ -1039,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()
  */
@@ -1081,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
@@ -1101,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.
@@ -1151,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
@@ -1192,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');
@@ -1223,14 +1672,16 @@
  * 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. So,
- * 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
+ * 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
@@ -1244,40 +1695,54 @@
  * $text = t("@name's blog", array('@name' => format_username($account)));
  * @endcode
  * Basically, you can put variables like @name into your string, and t() will
- * substitute their sanitized values at translation time (see $args below or
- * the Localization API pages referenced above for details). Translators can
- * then rearrange the string as necessary for the language (e.g., in Spanish,
- * it might be "blog de @name").
+ * 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
+ * t('May', array(), array('context' => 'Long month name')
+ * t('May', array(), array('context' => 'Abbreviated month name')
+ * @endcode
+ * See https://localize.drupal.org/node/2109 for more information.
+ *
  * @param $string
  *   A string containing the English string to translate.
  * @param $args
- *   An associative array of replacements to make after translation.
- *   Occurrences in $string of any key in $args are replaced with the
- *   corresponding value, after sanitization. The sanitization function depends
- *   on the first character of the key:
- *   - !variable: Inserted as is. Use this for text that has already been
- *     sanitized.
- *   - @variable: Escaped to HTML using check_plain(). Use this for anything
- *     displayed on a page on the site.
- *   - %variable: Escaped as a placeholder for user-submitted content using
- *     drupal_placeholder(), which shows up as emphasized text.
+ *   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 elements:
  *   - 'langcode' (defaults to the current language): The language code to
  *     translate to a language other than what is used to display the page.
- *   - 'context' (defaults to the empty context): The context the source string
- *     belongs to.
+ *   - 'context' (defaults to the empty context): A string giving the context
+ *     that the source string belongs to. See @ref sec_context above for more
+ *     information.
  *
  * @return
  *   The translated string.
  *
  * @see st()
  * @see get_t()
+ * @see format_string()
  * @ingroup sanitization
  */
 function t($string, array $args = array(), array $options = array()) {
@@ -1311,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
@@ -1373,6 +1876,7 @@
  *
  * @param $text
  *   The text to check.
+ *
  * @return
  *   TRUE if the text is valid UTF-8, FALSE if not.
  */
@@ -1387,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'];
   }
@@ -1413,7 +1918,7 @@
 }
 
 /**
- * Log an exception.
+ * Logs an exception.
  *
  * This is a wrapper function for watchdog() which automatically decodes an
  * exception.
@@ -1427,14 +1932,14 @@
  *   information about the passed-in exception is used.
  * @param $variables
  *   Array of variables to replace in the message on display. Defaults to the
- *   return value of drupal_decode_exception().
+ *   return value of _drupal_decode_exception().
  * @param $severity
  *   The severity of the message, as per RFC 3164.
  * @param $link
  *   A link to associate with the message.
  *
  * @see watchdog()
- * @see drupal_decode_exception()
+ * @see _drupal_decode_exception()
  */
 function watchdog_exception($type, Exception $exception, $message = NULL, $variables = array(), $severity = WATCHDOG_ERROR, $link = NULL) {
 
@@ -1454,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
@@ -1470,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.
  *
@@ -1488,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,
@@ -1496,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
@@ -1514,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 to be displayed to the user. For consistency with other
- *   messages, it should begin with a capital letter and end 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();
     }
@@ -1550,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()) {
@@ -1583,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.
@@ -1600,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
@@ -1625,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
@@ -1634,6 +2183,7 @@
  *
  * @param $ip
  *   IP address to check.
+ *
  * @return bool
  *   TRUE if access is denied, FALSE if access is allowed.
  */
@@ -1659,7 +2209,7 @@
 }
 
 /**
- * Handle denied users.
+ * Handles denied users.
  *
  * @param $ip
  *   IP address to check. Prints a message and exits if access is denied.
@@ -1674,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()
@@ -1714,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);
@@ -1725,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.
@@ -1786,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);
 }
 
 /**
@@ -1837,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();
@@ -1847,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.
@@ -1881,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) {
@@ -1939,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;
@@ -1954,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.
@@ -1965,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';
@@ -1973,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
@@ -2001,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.
@@ -2013,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;
@@ -2072,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
@@ -2121,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;
@@ -2138,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');
@@ -2160,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
@@ -2173,7 +2836,6 @@
  *   HMAC and timestamp.
  */
 function drupal_valid_test_ua() {
-  global $drupal_hash_salt;
   // No reason to reset this.
   static $test_prefix;
 
@@ -2187,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.
@@ -2197,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);
@@ -2233,7 +2895,35 @@
 }
 
 /**
- * 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'; @@ -2248,10 +2938,10 @@ * * This would include implementations of hook_install(), which could run * during the Drupal installation phase, and might also be run during - * non-installation time, such as while installing the module from the the + * non-installation time, such as while installing the module from the * module administration page. * - * Example useage: + * Example usage: * @code * $t = get_t(); * $translated = $t('translate this'); @@ -2276,7 +2966,7 @@ } /** - * Initialize all the defined language types. + * Initializes all the defined language types. */ function drupal_language_initialize() { $types = language_types(); @@ -2301,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 @@ -2316,7 +3006,10 @@ } /** - * 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 @@ -2326,16 +3019,29 @@ } /** - * 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__); @@ -2374,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' => '')); @@ -2408,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 @@ -2444,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 '/' @@ -2493,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 @@ -2527,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); + } } } } @@ -2537,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. @@ -2553,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) { @@ -2594,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. */ @@ -2634,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. */ @@ -2649,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 @@ -2658,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. @@ -2665,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; } @@ -2698,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]; } @@ -2706,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. @@ -2720,7 +3513,7 @@ $lookup_cache[$cache_key] = $file; if ($file) { - require_once DRUPAL_ROOT . '/' . $file; + include_once DRUPAL_ROOT . '/' . $file; return TRUE; } else { @@ -2729,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. @@ -2740,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 @@ -2838,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. * @@ -2864,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. @@ -2909,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))); @@ -2927,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). @@ -2940,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". @@ -2975,7 +3788,7 @@ } /** - * Internal function used to execute registered shutdown functions. + * Executes registered shutdown functions. */ function _drupal_shutdown_function() { $callbacks = &drupal_register_shutdown_function(); @@ -2985,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) { @@ -2998,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.5/includes/cache-install.inc drupal-7.66/includes/cache-install.inc --- drupal-7.5/includes/cache-install.inc 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/includes/cache-install.inc 2019-04-17 22:20:46.000000000 +0200 @@ -6,7 +6,7 @@ */ /** - * A stub cache implementation to be used during the installation process. + * Defines a stub cache implementation to be used during installation. * * The stub implementation is needed when database access is not yet available. * Because Drupal's caching system never requires that cached data be present, @@ -15,17 +15,30 @@ * normal operations would have a negative impact on performance. */ class DrupalFakeCache extends DrupalDatabaseCache implements DrupalCacheInterface { + + /** + * Overrides DrupalDatabaseCache::get(). + */ function get($cid) { return FALSE; } + /** + * Overrides DrupalDatabaseCache::getMultiple(). + */ function getMultiple(&$cids) { return array(); } + /** + * Overrides DrupalDatabaseCache::set(). + */ function set($cid, $data, $expire = CACHE_PERMANENT) { } + /** + * Overrides DrupalDatabaseCache::clear(). + */ function clear($cid = NULL, $wildcard = FALSE) { // If there is a database cache, attempt to clear it whenever possible. The // reason for doing this is that the database cache can accumulate data @@ -52,6 +65,9 @@ } } + /** + * Overrides DrupalDatabaseCache::isEmpty(). + */ function isEmpty() { return TRUE; } diff -Naur drupal-7.5/includes/cache.inc drupal-7.66/includes/cache.inc --- drupal-7.5/includes/cache.inc 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/includes/cache.inc 2019-04-17 22:20:46.000000000 +0200 @@ -1,18 +1,24 @@ 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. */ @@ -65,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. @@ -117,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)) { @@ -170,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. */ @@ -185,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 @@ -222,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 @@ -276,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. @@ -302,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. @@ -310,16 +322,28 @@ 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. @@ -357,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 @@ -373,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. @@ -390,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); } @@ -408,6 +452,9 @@ return $cache; } + /** + * Implements DrupalCacheInterface::set(). + */ function set($cid, $data, $expire = CACHE_PERMANENT) { $fields = array( 'serialized' => 0, @@ -434,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) { @@ -471,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) @@ -496,6 +554,9 @@ } } + /** + * Implements DrupalCacheInterface::isEmpty(). + */ function isEmpty() { $this->garbageCollection(); $query = db_select($this->bin); @@ -505,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.5/includes/common.inc drupal-7.66/includes/common.inc --- drupal-7.5/includes/common.inc 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/includes/common.inc 2019-04-17 22:20:46.000000000 +0200 @@ -70,8 +70,7 @@ define('CSS_THEME', 100); /** - * The default group for JavaScript libraries, settings or jQuery plugins added - * to the page. + * The default group for JavaScript and jQuery libraries added to the page. */ define('JS_LIBRARY', -100); @@ -86,20 +85,27 @@ define('JS_THEME', 100); /** - * Error code indicating that the request made by drupal_http_request() exceeded - * the specified timeout. + * Error code indicating that the request exceeded the specified timeout. + * + * @see drupal_http_request() */ -define('HTTP_REQUEST_TIMEOUT', 1); +define('HTTP_REQUEST_TIMEOUT', -1); /** - * Constants defining cache granularity for blocks and renderable arrays. + * @defgroup block_caching Block Caching + * @{ + * Constants that define each block's caching state. * - * Modules specify the caching patterns for their blocks using binary - * combinations of these constants in their hook_block_info(): - * $block[delta]['cache'] = DRUPAL_CACHE_PER_ROLE | DRUPAL_CACHE_PER_PAGE; - * DRUPAL_CACHE_PER_ROLE is used as a default when no caching pattern is - * specified. Use DRUPAL_CACHE_CUSTOM to disable standard block cache and - * implement + * Modules specify how their blocks can be cached in their hook_block_info() + * implementations. Caching can be turned off (DRUPAL_NO_CACHE), managed by the + * module declaring the block (DRUPAL_CACHE_CUSTOM), or managed by the core + * Block module. If the Block module is managing the cache, you can specify that + * the block is the same for every page and user (DRUPAL_CACHE_GLOBAL), or that + * it can change depending on the page (DRUPAL_CACHE_PER_PAGE) or by user + * (DRUPAL_CACHE_PER_ROLE or DRUPAL_CACHE_PER_USER). Page and user settings can + * be combined with a bitwise-binary or operator; for example, + * DRUPAL_CACHE_PER_ROLE | DRUPAL_CACHE_PER_PAGE means that the block can change + * depending on the user role or page it is on. * * The block cache is cleared in cache_clear_all(), and uses the same clearing * policy than page cache (node, comment, user, taxonomy added or updated...). @@ -110,31 +116,35 @@ */ /** - * The block should not get cached. This setting should be used: - * - for simple blocks (notably those that do not perform any db query), - * where querying the db cache would be more expensive than directly generating - * the content. - * - for blocks that change too frequently. + * The block should not get cached. + * + * This setting should be used: + * - For simple blocks (notably those that do not perform any db query), where + * querying the db cache would be more expensive than directly generating the + * content. + * - For blocks that change too frequently. */ define('DRUPAL_NO_CACHE', -1); /** - * The block is handling its own caching in its hook_block_view(). From the - * perspective of the block cache system, this is equivalent to DRUPAL_NO_CACHE. - * Useful when time based expiration is needed or a site uses a node access - * which invalidates standard block cache. + * The block is handling its own caching in its hook_block_view(). + * + * This setting is useful when time based expiration is needed or a site uses a + * node access which invalidates standard block cache. */ define('DRUPAL_CACHE_CUSTOM', -2); /** - * The block or element can change depending on the roles the user viewing the - * page belongs to. This is the default setting for blocks, used when the block - * does not specify anything. + * The block or element can change depending on the user's roles. + * + * This is the default setting for blocks, used when the block does not specify + * anything. */ define('DRUPAL_CACHE_PER_ROLE', 0x0001); /** - * The block or element can change depending on the user viewing the page. + * The block or element can change depending on the user. + * * This setting can be resource-consuming for sites with large number of users, * and thus should only be used when DRUPAL_CACHE_PER_ROLE is not sufficient. */ @@ -146,12 +156,16 @@ define('DRUPAL_CACHE_PER_PAGE', 0x0004); /** - * The block or element is the same for every user on every page where it is visible. + * The block or element is the same for every user and page that it is visible. */ define('DRUPAL_CACHE_GLOBAL', 0x0008); /** - * Add content to a specified region. + * @} End of "defgroup block_caching". + */ + +/** + * Adds content to a specified region. * * @param $region * Page region the content is added to. @@ -168,7 +182,7 @@ } /** - * Get assigned content for a given region. + * Gets assigned content for a given region. * * @param $region * A specified region to fetch content for. If NULL, all regions will be @@ -194,16 +208,16 @@ } /** - * Get the name of the currently active install profile. + * Gets the name of the currently active installation profile. * * When this function is called during Drupal's initial installation process, * the name of the profile that's about to be installed is stored in the global * installation state. At all other times, the standard Drupal systems variable - * table contains the name of the current profile, and we can call variable_get() - * to determine what one is active. + * table contains the name of the current profile, and we can call + * variable_get() to determine what one is active. * * @return $profile - * The name of the install profile. + * The name of the installation profile. */ function drupal_get_profile() { global $install_state; @@ -220,7 +234,7 @@ /** - * Set the breadcrumb trail for the current page. + * Sets the breadcrumb trail for the current page. * * @param $breadcrumb * Array of links, starting with "home" and proceeding up to but not including @@ -236,7 +250,7 @@ } /** - * Get the breadcrumb trail for the current page. + * Gets the breadcrumb trail for the current page. */ function drupal_get_breadcrumb() { $breadcrumb = drupal_set_breadcrumb(); @@ -265,9 +279,9 @@ } /** - * Add output to the head tag of the HTML page. + * Adds output to the HEAD tag of the HTML page. * - * This function can be called as long the headers aren't sent. Pass no + * This function can be called as long as the headers aren't sent. Pass no * arguments (or NULL for both) to retrieve the currently stored elements. * * @param $data @@ -333,7 +347,7 @@ } /** - * Retrieve output to be displayed in the HEAD tag of the HTML page. + * Retrieves output to be displayed in the HEAD tag of the HTML page. */ function drupal_get_html_head() { $elements = drupal_add_html_head(); @@ -342,7 +356,7 @@ } /** - * Add a feed URL for the current page. + * Adds a feed URL for the current page. * * This function can be called as long the HTML header hasn't been sent. * @@ -370,7 +384,7 @@ } /** - * Get the feed URLs for the current page. + * Gets the feed URLs for the current page. * * @param $delimiter * A delimiter to split feeds by. @@ -387,7 +401,7 @@ */ /** - * Process a URL query parameter array to remove unwanted elements. + * Processes a URL query parameter array to remove unwanted elements. * * @param $query * (optional) An array to be processed. Defaults to $_GET. @@ -432,19 +446,19 @@ } /** - * Split an URL-encoded query string into an array. + * Splits a URL-encoded query string into an array. * * @param $query * The query string to split. * * @return - * An array of url decoded couples $param_name => $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]) : ''; } } @@ -452,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. @@ -473,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)) { @@ -493,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() { @@ -524,37 +544,32 @@ } /** - * Wrapper around parse_url() to parse a system URL string into an associative array, suitable for url(). - * - * 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. + * Parses a URL string into its path, query, and fragment components. * - * The returned array contains a 'path' that may be passed separately to url(). - * For example: - * @code - * $options = drupal_parse_url($_GET['destination']); - * $my_url = url($options['path'], $options); - * $my_link = l('Example link', $options['path'], $options); - * @endcode + * This 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. * - * This is required, because url() does not support relative URLs containing a - * query string or fragment in its $path argument. Instead, any query string - * needs to be parsed into an associative query parameter array in - * $options['query'] and the fragment into $options['fragment']. + * Note that, unlike the RFC, when passed an external URL, this function + * groups the scheme, authority, and path together into the path component. * - * @param $url - * The URL string to parse, f.e. $_GET['destination']. + * @param string $url + * The internal path or external URL string to parse. * - * @return - * An associative array containing the keys: - * - 'path': The path of the URL. If the given $url is external, this includes - * the scheme and host. - * - 'query': An array of query parameters of $url, if existent. - * - 'fragment': The fragment of $url, if existent. + * @return array + * An associative array containing: + * - path: The path component of $url. If $url is an external URL, this + * includes the scheme, authority, and path. + * - query: An array of query parameters from $url, if they exist. + * - fragment: The fragment component from $url, if it exists. * - * @see url() * @see drupal_goto() + * @see l() + * @see url() + * @see http://tools.ietf.org/html/rfc3986 + * * @ingroup php_wrappers */ function drupal_parse_url($url) { @@ -596,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']); } @@ -621,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. @@ -642,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() @@ -670,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. @@ -686,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, @@ -698,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, @@ -710,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. @@ -734,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 @@ -759,8 +787,17 @@ * 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 @@ -921,9 +1002,10 @@ $response = preg_split("/\r\n|\n|\r/", $response); // Parse the response status line. - list($protocol, $code, $status_message) = explode(' ', trim(array_shift($response)), 3); - $result->protocol = $protocol; - $result->status_message = $status_message; + $response_status_array = _drupal_parse_response_status(trim(array_shift($response))); + $result->protocol = $response_status_array['http_version']; + $result->status_message = $response_status_array['reason_phrase']; + $code = $response_status_array['response_code']; $result->headers = array(); @@ -992,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 @@ -1006,6 +1094,11 @@ elseif ($options['max_redirects']) { // Redirect to the new location. $options['max_redirects']--; + + // We need to unset the 'Host' header + // as we are redirecting to a new location. + unset($options['headers']['Host']); + $result = drupal_http_request($location, $options); $result->redirect_code = $code; } @@ -1014,15 +1107,66 @@ } 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'); @@ -1033,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') { @@ -1051,7 +1203,10 @@ } /** - * 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() { static $fixed = FALSE; @@ -1072,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. */ @@ -1086,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. @@ -1095,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. */ @@ -1127,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. @@ -1154,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. @@ -1172,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 @@ -1266,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. @@ -1286,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 @@ -1296,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. @@ -1323,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 @@ -1331,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('% @@ -1369,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. @@ -1384,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 ''; } @@ -1407,7 +1565,7 @@ } if (!isset($allowed_html[strtolower($elem)])) { - // Disallowed HTML element + // Disallowed HTML element. return ''; } @@ -1423,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 : ''; @@ -1448,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'); @@ -1458,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); @@ -1475,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]); @@ -1512,7 +1670,7 @@ } if ($working == 0) { - // not well formed, remove and try again + // Not well formed; remove and try again. $attr = preg_replace('/ ^ ( @@ -1536,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. */ @@ -1598,7 +1757,7 @@ } /** - * Format a single RSS item. + * Formats a single RSS item. * * Arbitrary elements may be added using the $args associative array. */ @@ -1614,7 +1773,7 @@ } /** - * Format XML elements. + * Formats XML elements. * * @param $array * An array where each item represents an element and is either a: @@ -1623,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 = ''; @@ -1638,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"; @@ -1653,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 @@ -1669,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; @@ -1709,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); } @@ -1728,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. */ @@ -1749,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. */ @@ -1788,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, @@ -1813,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--; } @@ -1845,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 @@ -1928,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. */ @@ -1942,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(). @@ -1978,6 +2146,9 @@ /** * Format a username. * + * 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). @@ -2009,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, @@ -2026,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. @@ -2042,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 @@ -2057,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(). * @@ -2075,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. @@ -2120,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. @@ -2147,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, '/'); } } @@ -2183,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. @@ -2191,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'. @@ -2224,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 @@ -2255,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 */ @@ -2270,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; @@ -2335,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 @@ -2443,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: @@ -2463,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? @@ -2473,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', '')); @@ -2489,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); @@ -2504,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', '')); @@ -2545,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(). @@ -2568,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 @@ -2594,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 @@ -2629,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 @@ -2650,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); + } } } @@ -2663,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. @@ -2684,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'. @@ -2743,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. @@ -2778,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 @@ -2830,6 +3062,13 @@ */ function drupal_add_css($data = NULL, $options = NULL) { $css = &drupal_static(__FUNCTION__, array()); + $count = &drupal_static(__FUNCTION__ . '_count', 0); + + // If the $css variable has been reset with drupal_static_reset(), there is + // no longer any CSS being tracked, so set the counter back to 0 also. + if (count($css) === 0) { + $count = 0; + } // Construct the options, taking the defaults into consideration. if (isset($options)) { @@ -2865,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']) { @@ -2885,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 @@ -2926,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]]); @@ -2946,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 @@ -3012,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 @@ -3026,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(); @@ -3108,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')); @@ -3286,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; @@ -3369,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. @@ -3385,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]; } @@ -3448,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__); @@ -3507,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. @@ -3552,7 +3833,7 @@ // Remove certain whitespace. // There are different conditions for removing leading and trailing // whitespace. - // @see http://php.net/manual/en/regexp.reference.subpatterns.php + // @see http://php.net/manual/regexp.reference.subpatterns.php $contents = preg_replace('< # Strip leading and trailing whitespace. \s*([@{};,])\s* @@ -3577,7 +3858,7 @@ // Replaces @import commands with the actual stylesheet content. // This happens recursively but omits external files. - $contents = preg_replace_callback('/@import\s*(?:url\(\s*)?[\'"]?(?![a-z]+:)([^\'"\()]+)[\'"]?\s*\)?\s*;/', '_drupal_load_stylesheet', $contents); + $contents = preg_replace_callback('/@import\s*(?:url\(\s*)?[\'"]?(?![a-z]+:)(?!\/\/)([^\'"\()]+)[\'"]?\s*\)?\s*;/', '_drupal_load_stylesheet', $contents); return $contents; } @@ -3601,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); } /** @@ -3623,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.) @@ -3632,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); @@ -3653,22 +3950,30 @@ } /** - * 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, @@ -3699,7 +4004,11 @@ // be merged with content already on the base page. The HTML IDs must be // unique for the fully merged content. Therefore, initialize $seen_ids to // take into account IDs that are already in use on the base page. - $seen_ids_init = &drupal_static(__FUNCTION__ . ':init'); + static $drupal_static_fast; + if (!isset($drupal_static_fast['seen_ids_init'])) { + $drupal_static_fast['seen_ids_init'] = &drupal_static(__FUNCTION__ . ':init'); + } + $seen_ids_init = &$drupal_static_fast['seen_ids_init']; if (!isset($seen_ids_init)) { // Ideally, Drupal would provide an API to persist state information about // prior page requests in the database, and we'd be able to add this @@ -3719,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); @@ -3735,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(' ' => '-', '_' => '-', '[' => '-', ']' => '')); @@ -3799,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 @@ -3845,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 @@ -3918,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.5/modules/field/field.api.php drupal-7.66/modules/field/field.api.php --- drupal-7.5/modules/field/field.api.php 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/field/field.api.php 2019-04-17 22:20:46.000000000 +0200 @@ -1,7 +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: @@ -210,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. @@ -443,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. @@ -459,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) { @@ -476,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. @@ -492,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') { @@ -557,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. @@ -574,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); @@ -584,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); } } @@ -609,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; } } @@ -637,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, @@ -669,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. @@ -704,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'), @@ -733,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, ), ); } @@ -783,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 @@ -826,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'; + } } /** @@ -855,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. * @@ -918,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. @@ -1076,11 +1256,11 @@ } /** - * @} End of "ingroup field_type" + * @} End of "defgroup field_formatter". */ /** - * @ingroup field_attach + * @addtogroup field_attach * @{ */ @@ -1132,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. } @@ -1142,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.'), + ); + } + } + } + } } /** @@ -1346,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 @@ -1367,6 +1573,8 @@ * An associative array containing: * - entity_type: The type of the entity the field is attached to. * - field: A field data structure. + * + * @ingroup field_language */ function hook_field_available_languages_alter(&$languages, $context) { // Add an unavailable language. @@ -1417,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. @@ -1432,15 +1640,11 @@ } /** - * @} End of "ingroup field_attach" + * @} End of "addtogroup field_attach". */ -/********************************************************************** - * Field Storage API - **********************************************************************/ - /** - * @ingroup field_storage + * @addtogroup field_storage * @{ */ @@ -1580,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); @@ -1690,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, @@ -2094,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 @@ -2106,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(); @@ -2121,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 @@ -2187,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 @@ -2201,6 +2424,8 @@ * - entity_type: The entity type; e.g., 'node' or 'user'. * - bundle: The bundle name. * - view_mode: The view mode, e.g. 'full', 'teaser'... + * + * @ingroup field_types */ function hook_field_extra_fields_display_alter(&$displays, $context) { if ($context['entity_type'] == 'taxonomy_term' && $context['view_mode'] == 'full') { @@ -2209,38 +2434,6 @@ } /** - * 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'; - } -} - -/** * Alters the widget properties of a field instance on a given entity type * before it gets displayed. * @@ -2262,6 +2455,8 @@ * - instance: The instance of the field. * * @see hook_field_widget_properties_alter() + * + * @ingroup field_widget */ function hook_field_widget_properties_ENTITY_TYPE_alter(&$widget, $context) { // Change a widget's type according to the time of day. @@ -2273,15 +2468,7 @@ } /** - * @} End of "ingroup field_storage" - */ - -/********************************************************************** - * Field CRUD API - **********************************************************************/ - -/** - * @ingroup field_crud + * @addtogroup field_crud * @{ */ @@ -2387,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) { @@ -2461,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(); @@ -2475,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); @@ -2492,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') @@ -2513,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); @@ -2530,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. * @@ -2556,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') { @@ -2563,3 +2754,7 @@ } return TRUE; } + +/** + * @} End of "addtogroup hooks". + */ diff -Naur drupal-7.5/modules/field/field.attach.inc drupal-7.66/modules/field/field.attach.inc --- drupal-7.5/modules/field/field.attach.inc 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/field/field.attach.inc 2019-04-17 22:20:46.000000000 +0200 @@ -44,6 +44,9 @@ * * Each field defines which storage backend it uses. The Drupal system variable * 'field_storage_default' identifies the storage backend used by default. + * + * See @link field Field API @endlink for information about the other parts of + * the Field API. */ /** @@ -62,7 +65,7 @@ define('FIELD_STORAGE_INSERT', 'insert'); /** - * @} End of "defgroup field_storage" + * @} End of "defgroup field_storage". */ /** @@ -70,7 +73,7 @@ * @{ * Operate on Field API data attached to Drupal entities. * - * Field Attach API functions load, store, display, generate Form API + * Field Attach API functions load, store, display, generate Field API * structures, and perform a variety of other functions for field data attached * to individual entities. * @@ -117,6 +120,12 @@ * The pre-operation hooks do not make the Field Storage API irrelevant. The * Field Storage API is essentially the "fallback mechanism" for any fields * that aren't being intercepted explicitly by pre-operation hooks. + * + * @link field_language Field Language API @endlink provides information about + * the structure of field objects. + * + * See @link field Field API @endlink for information about the other parts of + * the Field API. */ /** @@ -185,8 +194,10 @@ // Iterate through the instances and collect results. $return = array(); foreach ($instances as $instance) { - $field_name = $instance['field_name']; - $field = field_info_field($field_name); + // field_info_field() is not available for deleted fields, so use + // field_info_field_by_id(). + $field = field_info_field_by_id($instance['field_id']); + $field_name = $field['field_name']; $function = $options['default'] ? 'field_default_' . $op : $field['module'] . '_field_' . $op; if (function_exists($function)) { // Determine the list of languages to iterate on. @@ -272,7 +283,6 @@ 'language' => NULL, ); $options += $default_options; - $field_info = field_info_field_by_ids(); $fields = array(); $grouped_instances = array(); @@ -296,7 +306,7 @@ 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. @@ -308,7 +318,7 @@ // Unless a language suggestion is provided we iterate on all the // available languages. $available_languages = field_available_languages($entity_type, $field); - $language = !empty($options['language'][$id]) ? $options['language'][$id] : $options['language']; + $language = is_array($options['language']) && !empty($options['language'][$id]) ? $options['language'][$id] : $options['language']; $languages = _field_language_suggestion($available_languages, $language, $field_name); foreach ($languages as $langcode) { $grouped_items[$field_id][$langcode][$id] = isset($entity->{$field_name}[$langcode]) ? $entity->{$field_name}[$langcode] : array(); @@ -544,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. @@ -603,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. @@ -681,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; } } @@ -694,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. @@ -757,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. @@ -805,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 @@ -824,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); } } @@ -845,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. @@ -937,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 @@ -1081,9 +1126,16 @@ * @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, $langcode = NULL) { - $options = array('language' => array()); +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. @@ -1155,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; @@ -1309,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); @@ -1348,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); @@ -1360,5 +1412,5 @@ /** - * @} End of "defgroup field_attach" + * @} End of "defgroup field_attach". */ diff -Naur drupal-7.5/modules/field/field.crud.inc drupal-7.66/modules/field/field.crud.inc --- drupal-7.5/modules/field/field.crud.inc 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/field/field.crud.inc 2019-04-17 22:20:46.000000000 +0200 @@ -15,6 +15,9 @@ * * The Field CRUD API uses * @link field Field API data structures @endlink. + * + * See @link field Field API @endlink for information about the other parts of + * the Field API. */ /** @@ -42,10 +45,11 @@ * system variable. * - settings: each omitted setting is given the default value specified in * hook_field_storage_info(). + * * @return * The $field array with the id property filled in. - * @throw - * FieldException + * + * @throws FieldException * * See: @link field Field API data structures @endlink. */ @@ -56,11 +60,11 @@ } // Field type is required. if (empty($field['type'])) { - throw new FieldException('Attempt to create a field with no type.'); + throw new FieldException(format_string('Attempt to create field @field_name with no type.', array('@field_name' => $field['field_name']))); } // Field name cannot contain invalid characters. if (!preg_match('/^[_a-z]+[_a-z0-9]*$/', $field['field_name'])) { - throw new FieldException('Attempt to create a field with invalid characters. Only lowercase alphanumeric characters and underscores are allowed, and only lowercase letters and underscore are allowed as the first character'); + throw new FieldException(format_string('Attempt to create a field @field_name with invalid characters. Only lowercase alphanumeric characters and underscores are allowed, and only lowercase letters and underscore are allowed as the first character', array('@field_name' => $field['field_name']))); } // Field name cannot be longer than 32 characters. We use drupal_strlen() @@ -185,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); @@ -240,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( @@ -282,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); @@ -315,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 @@ -333,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) @@ -407,7 +430,7 @@ ->execute(); // Clear the cache. - field_cache_clear(TRUE); + field_cache_clear(); module_invoke_all('field_delete_field', $field); } @@ -442,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. */ @@ -501,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) { @@ -727,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) { @@ -824,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.5/modules/field/field.default.inc drupal-7.66/modules/field/field.default.inc --- drupal-7.5/modules/field/field.default.inc 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/field/field.default.inc 2019-04-17 22:20:46.000000000 +0200 @@ -168,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 diff -Naur drupal-7.5/modules/field/field.form.inc drupal-7.66/modules/field/field.form.inc --- drupal-7.5/modules/field/field.form.inc 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/field/field.form.inc 2019-04-17 22:20:46.000000000 +0200 @@ -6,7 +6,38 @@ */ /** - * Create a separate form element for each field. + * Creates a form element for a field and can populate it with a default value. + * + * If the form element is not associated with an entity (i.e., $entity is NULL) + * field_get_default_value will be called to supply the default value for the + * field. Also allows other modules to alter the form element by implementing + * their own hooks. + * + * @param $entity_type + * The type of entity (for example 'node' or 'user') that the field belongs + * to. + * @param $entity + * The entity object that the field belongs to. This may be NULL if creating a + * form element with a default value. + * @param $field + * An array representing the field whose editing element is being created. + * @param $instance + * An array representing the structure for $field in its current context. + * @param $langcode + * The language associated with the field. + * @param $items + * An array of the field values. When creating a new entity this may be NULL + * or an empty array to use default values. + * @param $form + * An array representing the form that the editing element will be attached + * to. + * @param $form_state + * An array containing the current state of the form. + * @param $get_delta + * Used to get only a specific delta value of a multiple value field. + * + * @return + * The form element array created for this field. */ function field_default_form($entity_type, $entity, $field, $instance, $langcode, $items, &$form, &$form_state, $get_delta = NULL) { // This could be called with no entity, as when a UI module creates a @@ -37,76 +68,86 @@ // Collect widget elements. $elements = array(); - if (field_access('edit', $field, $entity_type, $entity)) { - // 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) { - $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($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 @@ -122,6 +163,7 @@ // when $langcode is unknown. '#language' => $langcode, $langcode => $elements, + '#access' => field_access('edit', $field, $entity_type, $entity), ); return $addition; @@ -165,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, @@ -193,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; } } @@ -250,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'), ), @@ -335,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); } } @@ -373,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)); @@ -398,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)); diff -Naur drupal-7.5/modules/field/field.info drupal-7.66/modules/field/field.info --- drupal-7.5/modules/field/field.info 2011-07-27 22:26:55.000000000 +0200 +++ drupal-7.66/modules/field/field.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,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-07-27 -version = "7.5" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1311798415" - +datestamp = "1555533576" diff -Naur drupal-7.5/modules/field/field.info.class.inc drupal-7.66/modules/field/field.info.class.inc --- drupal-7.5/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.5/modules/field/field.info.inc drupal-7.66/modules/field/field.info.inc --- drupal-7.5/modules/field/field.info.inc 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/field/field.info.inc 2019-04-17 22:20:46.000000000 +0200 @@ -6,6 +6,32 @@ */ /** + * Retrieves the FieldInfo object for the current request. + * + * @return FieldInfo + * An instance of the FieldInfo class. + */ +function _field_info_field_cache() { + // Use the advanced drupal_static() pattern, since this is called very often. + static $drupal_static_fast; + + if (!isset($drupal_static_fast)) { + $drupal_static_fast['field_info_field_cache'] = &drupal_static(__FUNCTION__); + } + $field_info = &$drupal_static_fast['field_info_field_cache']; + + if (!isset($field_info)) { + // @todo The registry should save the need for an explicit include, but not + // a couple upgrade tests (DisabledNodeTypeTestCase, + // FilterFormatUpgradePathTestCase...) break in a strange way without it. + include_once dirname(__FILE__) . '/field.info.class.inc'; + $field_info = new FieldInfo(); + } + + return $field_info; +} + +/** * @defgroup field_info Field Info API * @{ * Obtain information about Field API configuration. @@ -13,6 +39,9 @@ * The Field Info API exposes information about field types, fields, * instances, bundles, widget types, display formatters, behaviors, * and settings defined by or with the Field API. + * + * See @link field Field API @endlink for information about the other parts of + * the Field API. */ /** @@ -24,13 +53,57 @@ */ function field_info_cache_clear() { drupal_static_reset('field_view_mode_settings'); + drupal_static_reset('field_available_languages'); // @todo: Remove this when field_attach_*_bundle() bundle management // functions are moved to the entity API. entity_info_cache_clear(); _field_info_collate_types(TRUE); - _field_info_collate_fields(TRUE); + _field_info_field_cache()->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, + ); } /** @@ -50,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 @@ -120,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) { @@ -149,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']; - } - - // 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; - } + // 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"); } - - cache_set('field_info_fields', $info, 'cache_field'); } } @@ -248,190 +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... - * - * @param $instance - * The raw instance structure as read from the database. - * @param $field - * The field structure for the instance. + * The functionality has moved to the FieldInfo class. This function is kept as + * a backwards-compatibility layer. See http://drupal.org/node/1915646. * - * @return - * Field instance array. + * @see FieldInfo::prepareInstance() */ function _field_info_prepare_instance($instance, $field) { - // 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); } /** @@ -579,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; } @@ -602,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); } /** @@ -624,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 @@ -634,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 @@ -655,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]; - } - if (isset($info['instances'][$entity_type][$bundle_name])) { - return $info['instances'][$entity_type][$bundle_name]; + return $cache->getInstances($entity_type); } - 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]; } } @@ -758,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(); } /** @@ -885,5 +812,5 @@ } /** - * @} End of "defgroup field_info" + * @} End of "defgroup field_info". */ diff -Naur drupal-7.5/modules/field/field.install drupal-7.66/modules/field/field.install --- drupal-7.5/modules/field/field.install 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/field/field.install 2019-04-17 22:20:46.000000000 +0200 @@ -99,14 +99,13 @@ 'primary key' => 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'), ), @@ -129,7 +128,7 @@ 'not null' => TRUE, 'default' => '' ), - 'entity_type' => array( + 'entity_type' => array( 'type' => 'varchar', 'length' => 32, 'not null' => TRUE, @@ -163,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; } @@ -173,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.` @@ -254,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; @@ -285,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. @@ -323,6 +323,8 @@ * @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(), $key = 'id') { $fields = array(); @@ -357,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. @@ -435,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.5/modules/field/field.module drupal-7.66/modules/field/field.module --- drupal-7.5/modules/field/field.module 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/field/field.module 2019-04-17 22:20:46.000000000 +0200 @@ -60,166 +60,139 @@ * Field definitions are represented as an array of key/value pairs. * * array $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. + * - 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. * @@ -306,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) { @@ -320,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') . '
'; @@ -351,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() { @@ -366,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(); } } @@ -483,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)) { @@ -531,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, - * 'visible' => TRUE if the pseudo-field is visible, FALSE if hidden, - * ), - * 'full' => ... - * ), - * 'extra_field_2' => ... - * ), - * ), - * ); - * @endcode - * - * @param $entity_type + * @param string $entity_type * The type of $entity; e.g., 'node' or 'user'. - * @param $bundle + * @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(), @@ -821,9 +823,9 @@ * * This function can be used by third-party modules that need to output an * isolated field. - * - Do not use inside node (or other entities) templates, use + * - Do not use inside node (or any other entity) templates; use * render($content[FIELD_NAME]) instead. - * - Do not use to display all fields in an entity, use + * - Do not use to display all fields in an entity; use * field_attach_prepare_view() and field_attach_view() instead. * - The field_view_value() function can be used to output a single formatted * field value, without label or wrapping field markup. @@ -875,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 @@ -906,6 +909,7 @@ 'entity' => $entity, 'view_mode' => '_custom', 'display' => $display, + 'language' => $langcode, ); drupal_alter('field_attach_view', $result, $context); @@ -948,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'. * @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. @@ -1090,7 +1110,7 @@ } } /** - * @} End of "defgroup field" + * @} End of "defgroup field". */ /** @@ -1176,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.5/modules/field/field.multilingual.inc drupal-7.66/modules/field/field.multilingual.inc --- drupal-7.5/modules/field/field.multilingual.inc 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/field/field.multilingual.inc 2019-04-17 22:20:46.000000000 +0200 @@ -57,6 +57,9 @@ * even disabled by modules implementing hook_field_language_alter(), making * it possible to choose the first approach. The display language for each * field is returned by field_language(). + * + * See @link field Field API @endlink for information about the other parts of + * the Field API. */ /** diff -Naur drupal-7.5/modules/field/modules/field_sql_storage/field_sql_storage.info drupal-7.66/modules/field/modules/field_sql_storage/field_sql_storage.info --- drupal-7.5/modules/field/modules/field_sql_storage/field_sql_storage.info 2011-07-27 22:26:55.000000000 +0200 +++ drupal-7.66/modules/field/modules/field_sql_storage/field_sql_storage.info 2019-04-17 22:39:36.000000000 +0200 @@ -7,8 +7,7 @@ files[] = field_sql_storage.test required = TRUE -; Information added by drupal.org packaging script on 2011-07-27 -version = "7.5" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1311798415" - +datestamp = "1555533576" diff -Naur drupal-7.5/modules/field/modules/field_sql_storage/field_sql_storage.install drupal-7.66/modules/field/modules/field_sql_storage/field_sql_storage.install --- drupal-7.5/modules/field/modules/field_sql_storage/field_sql_storage.install 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/field/modules/field_sql_storage/field_sql_storage.install 2019-04-17 22:20:46.000000000 +0200 @@ -30,7 +30,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_sql_storage_write($entity_type, $bundle, $entity_id, $revision_id, $field_name, $data) { $table_name = "field_data_{$field_name}"; @@ -211,5 +211,5 @@ } /** - * @} End of "addtogroup updates-6.x-to-7.x" + * @} End of "addtogroup updates-6.x-to-7.x". */ diff -Naur drupal-7.5/modules/field/modules/field_sql_storage/field_sql_storage.module drupal-7.66/modules/field/modules/field_sql_storage/field_sql_storage.module --- drupal-7.5/modules/field/modules/field_sql_storage/field_sql_storage.module 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/field/modules/field_sql_storage/field_sql_storage.module 2019-04-17 22:20:46.000000000 +0200 @@ -65,6 +65,49 @@ } /** + * Generates a table alias for a field data table. + * + * The table alias is unique for each unique combination of field name + * (represented by $tablename), delta_group and language_group. + * + * @param $tablename + * The name of the data table for this field. + * @param $field_key + * The numeric key of this field in this query. + * @param $query + * The EntityFieldQuery that is executed. + * + * @return + * A string containing the generated table alias. + */ +function _field_sql_storage_tablealias($tablename, $field_key, EntityFieldQuery $query) { + // No conditions present: use a unique alias. + if (empty($query->fieldConditions[$field_key])) { + return $tablename . $field_key; + } + + // Find the delta and language condition values and append them to the alias. + $condition = $query->fieldConditions[$field_key]; + $alias = $tablename; + $has_group_conditions = FALSE; + + foreach (array('delta', 'language') as $column) { + if (isset($condition[$column . '_group'])) { + $alias .= '_' . $column . '_' . $condition[$column . '_group']; + $has_group_conditions = TRUE; + } + } + + // Return the alias when it has delta/language group conditions. + if ($has_group_conditions) { + return $alias; + } + + // Return a unique alias in other cases. + return $tablename . $field_key; +} + +/** * Generate a column name for a field data table. * * @param $name @@ -180,7 +223,17 @@ foreach ($field['indexes'] as $index_name => $columns) { $real_name = _field_sql_storage_indexname($field['field_name'], $index_name); foreach ($columns as $column_name) { - $current['indexes'][$real_name][] = _field_sql_storage_columnname($field['field_name'], $column_name); + // Indexes can be specified as either a column name or an array with + // column name and length. Allow for either case. + if (is_array($column_name)) { + $current['indexes'][$real_name][] = array( + _field_sql_storage_columnname($field['field_name'], $column_name[0]), + $column_name[1], + ); + } + else { + $current['indexes'][$real_name][] = _field_sql_storage_columnname($field['field_name'], $column_name); + } } } @@ -188,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; } @@ -236,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 { @@ -265,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); @@ -300,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); @@ -395,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, @@ -468,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'; @@ -478,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); @@ -592,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.5/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.5/modules/field/modules/field_sql_storage/field_sql_storage.test 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/field/modules/field_sql_storage/field_sql_storage.test 2019-04-17 22:20:46.000000000 +0200 @@ -126,7 +126,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 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."); @@ -145,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."); @@ -175,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))); } } @@ -183,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."); } /** @@ -306,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() { @@ -320,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. @@ -330,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"); } /** @@ -362,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))); } } @@ -382,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']['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 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'][$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.5/modules/field/modules/list/list.info drupal-7.66/modules/field/modules/list/list.info --- drupal-7.5/modules/field/modules/list/list.info 2011-07-27 22:26:55.000000000 +0200 +++ drupal-7.66/modules/field/modules/list/list.info 2019-04-17 22:39:36.000000000 +0200 @@ -7,8 +7,7 @@ dependencies[] = options files[] = tests/list.test -; Information added by drupal.org packaging script on 2011-07-27 -version = "7.5" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1311798415" - +datestamp = "1555533576" diff -Naur drupal-7.5/modules/field/modules/list/list.install drupal-7.66/modules/field/modules/list/list.install --- drupal-7.5/modules/field/modules/list/list.install 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/field/modules/list/list.install 2019-04-17 22:20:46.000000000 +0200 @@ -61,7 +61,7 @@ // Additionally, float keys need to be disambiguated ('.5' is '0.5'). if ($field['type'] == 'list_number' && !empty($allowed_values)) { - $keys = array_map(create_function('$a', 'return (string) (float) $a;'), array_keys($allowed_values)); + $keys = array_map('_list_update_7001_float_string_cast', array_keys($allowed_values)); $allowed_values = array_combine($keys, array_values($allowed_values)); } @@ -89,6 +89,13 @@ } /** + * Helper callback function to cast the array element. + */ +function _list_update_7001_float_string_cast($element) { + return (string) (float) $element; +} + +/** * Helper function for list_update_7001: extract allowed values from a string. * * This reproduces the parsing logic in use before D7 RC2. @@ -117,6 +124,11 @@ } /** + * @addtogroup updates-7.x-extra + * @{ + */ + +/** * Re-apply list_update_7001() for deleted fields. */ function list_update_7002() { @@ -126,4 +138,8 @@ // list_update_7001() has the required checks to ensure it is reentrant, so // it can simply be executed once more.. list_update_7001(); -} \ No newline at end of file +} + +/** + * @} End of "addtogroup updates-7.x-extra". + */ diff -Naur drupal-7.5/modules/field/modules/list/list.module drupal-7.66/modules/field/modules/list/list.module --- drupal-7.5/modules/field/modules/list/list.module 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/field/modules/list/list.module 2019-04-17 22:20:46.000000000 +0200 @@ -221,24 +221,39 @@ * * @param $field * The field definition. + * @param $instance + * (optional) A field instance array. Defaults to NULL. + * @param $entity_type + * (optional) The type of entity; e.g. 'node' or 'user'. Defaults to NULL. + * @param $entity + * (optional) The entity object. Defaults to NULL. * * @return * The array of allowed values. Keys of the array are the raw stored values * (number or text), values of the array are the display labels. */ -function list_allowed_values($field) { +function list_allowed_values($field, $instance = NULL, $entity_type = NULL, $entity = NULL) { $allowed_values = &drupal_static(__FUNCTION__, array()); if (!isset($allowed_values[$field['id']])) { $function = $field['settings']['allowed_values_function']; + // If $cacheable is FALSE, then the allowed values are not statically + // cached. See list_test_dynamic_values_callback() for an example of + // generating dynamic and uncached values. + $cacheable = TRUE; if (!empty($function) && function_exists($function)) { - $values = $function($field); + $values = $function($field, $instance, $entity_type, $entity, $cacheable); } else { $values = $field['settings']['allowed_values']; } - $allowed_values[$field['id']] = $values; + if ($cacheable) { + $allowed_values[$field['id']] = $values; + } + else { + return $values; + } } return $allowed_values[$field['id']]; @@ -248,7 +263,7 @@ * Parses a string of 'allowed values' into an array. * * @param $string - * The list of allowed values in string format descibed in + * The list of allowed values in string format described in * list_allowed_values_string(). * @param $field_type * The field type. Either 'list_number' or 'list_text'. @@ -343,9 +358,9 @@ function list_field_update_forbid($field, $prior_field, $has_data) { if ($field['module'] == 'list' && $has_data) { // Forbid any update that removes allowed values with actual data. - $lost_keys = array_diff(array_keys($field['settings']['allowed_values']), array_keys($prior_field['settings']['allowed_values'])); + $lost_keys = array_diff(array_keys($prior_field['settings']['allowed_values']), array_keys($field['settings']['allowed_values'])); if (_list_values_in_use($field, $lost_keys)) { - throw new FieldUpdateForbiddenException(t('Cannot update a list field to not include keys with existing data.')); + throw new FieldUpdateForbiddenException(t('A list field (@field_name) with existing data cannot have its keys changed.', array('@field_name' => $field['field_name']))); } } } @@ -373,7 +388,7 @@ * - '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']])) { @@ -419,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); } /** @@ -447,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.5/modules/field/modules/list/tests/list.test drupal-7.66/modules/field/modules/list/tests/list.test --- drupal-7.5/modules/field/modules/list/tests/list.test 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/field/modules/list/tests/list.test 2019-04-17 22:20:46.000000000 +0200 @@ -51,28 +51,45 @@ // All three options appear. $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'); + + // 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); @@ -90,9 +107,92 @@ $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'); + } } } @@ -112,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. @@ -133,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( @@ -156,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.'); } /** @@ -184,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( @@ -206,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.'); } /** @@ -234,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( @@ -261,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.'); } /** @@ -286,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); @@ -295,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.5/modules/field/modules/list/tests/list_test.info drupal-7.66/modules/field/modules/list/tests/list_test.info --- drupal-7.5/modules/field/modules/list/tests/list_test.info 2011-07-27 22:26:55.000000000 +0200 +++ drupal-7.66/modules/field/modules/list/tests/list_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,8 +5,7 @@ version = VERSION hidden = TRUE -; Information added by drupal.org packaging script on 2011-07-27 -version = "7.5" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1311798415" - +datestamp = "1555533576" diff -Naur drupal-7.5/modules/field/modules/list/tests/list_test.module drupal-7.66/modules/field/modules/list/tests/list_test.module --- drupal-7.5/modules/field/modules/list/tests/list_test.module 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/field/modules/list/tests/list_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -21,3 +21,12 @@ return $values; } + +/** + * An entity-bound allowed values callback. + */ +function list_test_dynamic_values_callback($field, $instance, $entity_type, $entity, &$cacheable) { + $cacheable = FALSE; + // We need the values of the entity as keys. + return drupal_map_assoc(array_merge(array($entity->ftlabel), entity_extract_ids($entity_type, $entity))); +} diff -Naur drupal-7.5/modules/field/modules/number/number.info drupal-7.66/modules/field/modules/number/number.info --- drupal-7.5/modules/field/modules/number/number.info 2011-07-27 22:26:55.000000000 +0200 +++ drupal-7.66/modules/field/modules/number/number.info 2019-04-17 22:39:36.000000000 +0200 @@ -6,8 +6,7 @@ dependencies[] = field files[] = number.test -; Information added by drupal.org packaging script on 2011-07-27 -version = "7.5" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1311798415" - +datestamp = "1555533576" diff -Naur drupal-7.5/modules/field/modules/number/number.module drupal-7.66/modules/field/modules/number/number.module --- drupal-7.5/modules/field/modules/number/number.module 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/field/modules/number/number.module 2019-04-17 22:20:46.000000000 +0200 @@ -98,14 +98,14 @@ '#title' => 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', @@ -164,6 +164,15 @@ } } } + if ($field['type'] == 'number_float') { + // Remove the decimal point from float values with decimal + // point but no decimal numbers. + foreach ($items as $delta => $item) { + if (isset($item['value'])) { + $items[$delta]['value'] = floatval($item['value']); + } + } + } } /** @@ -181,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, @@ -195,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, @@ -215,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; } @@ -261,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); @@ -366,12 +388,12 @@ switch ($type) { case 'float': case 'decimal': - $regexp = '@[^-0-9\\' . $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]@'; + $regexp = '@([^-0-9])|(.-)@'; $message = t('Only numbers are allowed in %field.', array('%field' => $instance['label'])); break; } diff -Naur drupal-7.5/modules/field/modules/number/number.test drupal-7.66/modules/field/modules/number/number.test --- drupal-7.5/modules/field/modules/number/number.test 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/field/modules/number/number.test 2019-04-17 22:20:46.000000000 +0200 @@ -23,7 +23,7 @@ function setUp() { parent::setUp('field_test'); - $this->web_user = $this->drupalCreateUser(array('access field_test content', 'administer field_test content')); + $this->web_user = $this->drupalCreateUser(array('access field_test content', 'administer field_test content', 'administer content types', 'administer fields')); $this->drupalLogin($this->web_user); } @@ -58,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'; @@ -68,8 +68,8 @@ $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( @@ -89,8 +89,113 @@ $this->assertText( t('There should only be one decimal separator (@separator)', array('@separator' => $this->field['settings']['decimal_separator'])), - t('Correctly failed to save decimal value with more than one decimal point.') + '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.5/modules/field/modules/options/options.api.php drupal-7.66/modules/field/modules/options/options.api.php --- drupal-7.5/modules/field/modules/options/options.api.php 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/field/modules/options/options.api.php 2019-04-17 22:20:46.000000000 +0200 @@ -15,6 +15,16 @@ * * @param $field * The field definition. + * @param $instance + * (optional) The instance definition. The hook might be called without an + * $instance parameter in contexts where no specific instance can be targeted. + * It is recommended to only use instance level properties to filter out + * values from a list defined by field level properties. + * @param $entity_type + * The entity type the field is attached to. + * @param $entity + * The entity object the field is attached to, or NULL if no entity + * exists (e.g. in field settings page). * * @return * The array of options for the field. Array keys are the values to be @@ -25,7 +35,7 @@ * widget. The HTML tags defined in _field_filter_xss_allowed_tags() are * allowed, other tags will be filtered. */ -function hook_options_list($field) { +function hook_options_list($field, $instance, $entity_type, $entity) { // Sample structure. $options = array( 0 => t('Zero'), diff -Naur drupal-7.5/modules/field/modules/options/options.info drupal-7.66/modules/field/modules/options/options.info --- drupal-7.5/modules/field/modules/options/options.info 2011-07-27 22:26:55.000000000 +0200 +++ drupal-7.66/modules/field/modules/options/options.info 2019-04-17 22:39:36.000000000 +0200 @@ -6,8 +6,7 @@ dependencies[] = field files[] = options.test -; Information added by drupal.org packaging script on 2011-07-27 -version = "7.5" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1311798415" - +datestamp = "1555533576" diff -Naur drupal-7.5/modules/field/modules/options/options.module drupal-7.66/modules/field/modules/options/options.module --- drupal-7.5/modules/field/modules/options/options.module 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/field/modules/options/options.module 2019-04-17 22:20:46.000000000 +0200 @@ -79,8 +79,11 @@ $has_value = isset($items[0][$value_key]); $properties = _options_properties($type, $multiple, $required, $has_value); + $entity_type = $element['#entity_type']; + $entity = $element['#entity']; + // Prepare the list of options. - $options = _options_get_options($field, $instance, $properties); + $options = _options_get_options($field, $instance, $properties, $entity_type, $entity); // Put current field values in shape. $default_value = _options_storage_to_form($items, $options, $value_key, $properties); @@ -102,10 +105,18 @@ reset($options); $default_value = array(key($options)); } + + // If this is a single-value field, take the first default value, or + // default to NULL so that the form element is properly recognized as + // not having a default value. + if (!$multiple) { + $default_value = $default_value ? reset($default_value) : NULL; + } + $element += array( '#type' => $multiple ? 'checkboxes' : 'radios', // Radio buttons need a scalar value. - '#default_value' => $multiple ? $default_value : reset($default_value), + '#default_value' => $default_value, '#options' => $options, ); break; @@ -174,6 +185,7 @@ $base = array( 'filter_xss' => FALSE, 'strip_tags' => FALSE, + 'strip_tags_and_unescape' => FALSE, 'empty_option' => FALSE, 'optgroups' => FALSE, ); @@ -184,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) { @@ -200,7 +212,7 @@ if (!$required) { $properties['empty_option'] = 'option_none'; } - else if (!$has_value) { + elseif (!$has_value) { $properties['empty_option'] = 'option_select'; } } @@ -229,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); @@ -260,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.5/modules/field/modules/options/options.test drupal-7.66/modules/field/modules/options/options.test --- drupal-7.5/modules/field/modules/options/options.test 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/field/modules/options/options.test 2019-04-17 22:20:46.000000000 +0200 @@ -1,7 +1,7 @@ 'list_integer', 'cardinality' => 1, 'settings' => array( - // Make sure that 0 works as an option. - 'allowed_values' => array(0 => 'Zero', 1 => 'One', 2 => 'Some & unescaped markup'), + 'allowed_values' => array( + // Make sure that 0 works as an option. + 0 => 'Zero', + 1 => 'One', + // Make sure that option text is properly sanitized. + 2 => 'Some & unescaped markup', + // Make sure that HTML entities in option text are not double-encoded. + 3 => 'Some HTML encoded markup with < & >', + ), ), ); $this->card_1 = field_create_field($this->card_1); @@ -35,8 +42,13 @@ 'type' => 'list_integer', 'cardinality' => 2, 'settings' => array( - // Make sure that 0 works as an option. - 'allowed_values' => array(0 => 'Zero', 1 => 'One', 2 => 'Some & unescaped markup'), + 'allowed_values' => array( + // Make sure that 0 works as an option. + 0 => 'Zero', + 1 => 'One', + // Make sure that option text is properly sanitized. + 2 => 'Some & unescaped markup', + ), ), ); $this->card_2 = field_create_field($this->card_2); @@ -47,14 +59,18 @@ 'type' => 'list_boolean', 'cardinality' => 1, 'settings' => array( - // Make sure that 0 works as a 'on' value'. - 'allowed_values' => array(1 => 'Zero', 0 => 'Some & unescaped markup'), + 'allowed_values' => array( + // Make sure that 1 works as a 'on' value'. + 1 => 'Zero', + // Make sure that option text is properly sanitized. + 0 => 'Some & unescaped markup', + ), ), ); $this->bool = field_create_field($this->bool); // Create a web user. - $this->web_user = $this->drupalCreateUser(array('access field_test content', 'administer field_test content')); + $this->web_user = $this->drupalCreateUser(array('access field_test content', 'administer field_test content', 'administer fields')); $this->drupalLogin($this->web_user); } @@ -85,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); @@ -139,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( @@ -178,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( @@ -225,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); @@ -247,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); @@ -259,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')); @@ -276,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); @@ -323,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)); @@ -350,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()); @@ -359,7 +376,7 @@ // Test the 'None' option. - // Check that the 'none' option has no efect if actual options are selected + // Check that the 'none' option has no effect if actual options are selected // as well. $edit = array("card_2[$langcode][]" => array('_none' => '_none', 0 => 0)); $this->drupalPost('test-entity/manage/' . $entity->ftid . '/edit', $edit, t('Save')); @@ -374,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. @@ -393,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)); @@ -438,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); @@ -459,7 +476,7 @@ $this->assertNoFieldChecked("edit-bool-$langcode"); // Create admin user. - $admin_user = $this->drupalCreateUser(array('access content', 'administer content types', 'administer taxonomy')); + $admin_user = $this->drupalCreateUser(array('access content', 'administer content types', 'administer taxonomy', 'administer fields')); $this->drupalLogin($admin_user); // Create a test field instance. @@ -483,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 @@ -502,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.5/modules/field/modules/text/text.info drupal-7.66/modules/field/modules/text/text.info --- drupal-7.5/modules/field/modules/text/text.info 2011-07-27 22:26:55.000000000 +0200 +++ drupal-7.66/modules/field/modules/text/text.info 2019-04-17 22:39:36.000000000 +0200 @@ -7,8 +7,7 @@ files[] = text.test required = TRUE -; Information added by drupal.org packaging script on 2011-07-27 -version = "7.5" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1311798415" - +datestamp = "1555533576" diff -Naur drupal-7.5/modules/field/modules/text/text.install drupal-7.66/modules/field/modules/text/text.install --- drupal-7.5/modules/field/modules/text/text.install 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/field/modules/text/text.install 2019-04-17 22:20:46.000000000 +0200 @@ -67,19 +67,6 @@ } /** - * Implements hook_update_dependencies(). - */ -function text_update_dependencies() { - // Ensure that format columns are only changed after Filter module has changed - // the primary records. - $dependencies['text'][7000] = array( - 'filter' => 7010, - ); - - return $dependencies; -} - -/** * Change text field 'format' columns into varchar. */ function text_update_7000() { diff -Naur drupal-7.5/modules/field/modules/text/text.js drupal-7.66/modules/field/modules/text/text.js --- drupal-7.5/modules/field/modules/text/text.js 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/field/modules/text/text.js 2019-04-17 22:20:46.000000000 +0200 @@ -12,9 +12,9 @@ $summaries.once('text-summary-wrapper').each(function(index) { var $summary = $(this); - var $summaryLabel = $summary.find('label'); + var $summaryLabel = $summary.find('label').first(); var $full = $widget.find('.text-full').eq(index).closest('.form-item'); - var $fullLabel = $full.find('label'); + var $fullLabel = $full.find('label').first(); // Create a placeholder label when the field cardinality is // unlimited or greater than 1. @@ -23,24 +23,28 @@ } // Setup the edit/hide summary link. - var $link = $('(' + Drupal.t('Hide summary') + ')').toggle( - function () { + var $link = $('(' + Drupal.t('Hide summary') + ')'); + var $a = $link.find('a'); + var toggleClick = true; + $link.bind('click', function (e) { + if (toggleClick) { $summary.hide(); - $(this).find('a').html(Drupal.t('Edit summary')).end().appendTo($fullLabel); - return false; - }, - function () { + $a.html(Drupal.t('Edit summary')); + $link.appendTo($fullLabel); + } + else { $summary.show(); - $(this).find('a').html(Drupal.t('Hide summary')).end().appendTo($summaryLabel); - return false; + $a.html(Drupal.t('Hide summary')); + $link.appendTo($summaryLabel); } - ).appendTo($summaryLabel); + toggleClick = !toggleClick; + return false; + }).appendTo($summaryLabel); // If no summary is set, hide the summary field. if ($(this).find('.text-summary').val() == '') { $link.click(); } - return; }); }); } diff -Naur drupal-7.5/modules/field/modules/text/text.module drupal-7.66/modules/field/modules/text/text.module --- drupal-7.5/modules/field/modules/text/text.module 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/field/modules/text/text.module 2019-04-17 22:20:46.000000000 +0200 @@ -71,7 +71,7 @@ '#default_value' => $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, @@ -223,11 +223,13 @@ if (strpos($display['type'], '_trimmed') !== FALSE) { $element['trim_length'] = array( - '#title' => t('Trim length'), + '#title' => t('Trimmed limit'), '#type' => 'textfield', + '#field_suffix' => t('characters'), '#size' => 10, '#default_value' => $settings['trim_length'], - '#element_validate' => array('_element_validate_integer_positive'), + '#element_validate' => array('element_validate_integer_positive'), + '#description' => t('If the summary is not set, the trimmed %label field will be shorter than this character limit.', array('%label' => $instance['label'])), '#required' => TRUE, ); } @@ -245,7 +247,7 @@ $summary = ''; if (strpos($display['type'], '_trimmed') !== FALSE) { - $summary = t('Trim length') . ': ' . $settings['trim_length']; + $summary = t('Trimmed limit: @trim_length characters', array('@trim_length' => $settings['trim_length'])); } return $summary; @@ -480,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 { @@ -489,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.5/modules/field/modules/text/text.test drupal-7.66/modules/field/modules/text/text.test --- drupal-7.5/modules/field/modules/text/text.test 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/field/modules/text/text.test 2019-04-17 22:20:46.000000000 +0200 @@ -110,8 +110,8 @@ // Display creation form. $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]", '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(); @@ -121,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); @@ -179,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() . ''; @@ -190,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. @@ -219,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'); } } @@ -383,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))); } /** @@ -401,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.'); } } @@ -424,6 +424,7 @@ 'administer content types', 'access administration pages', 'bypass node access', + 'administer fields', filter_permission_name($full_html_format), )); $this->translator = $this->drupalCreateUser(array('create article content', 'edit own article content', 'translate content')); @@ -436,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.'); } /** @@ -464,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.'); } /** @@ -476,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(), @@ -484,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. @@ -511,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.5/modules/field/tests/field.test drupal-7.66/modules/field/tests/field.test --- drupal-7.5/modules/field/tests/field.test 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/field/tests/field.test 2019-04-17 22:20:46.000000000 +0200 @@ -64,32 +64,49 @@ $e = clone $entity; field_attach_load('test_entity', array($e->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', @@ -106,7 +123,7 @@ ) ) ); - field_create_instance($this->instance); + field_create_instance($this->$instance); } } @@ -165,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. @@ -178,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))); } } } @@ -249,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]))); } /** @@ -311,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']))); } } @@ -340,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]'))); } } @@ -371,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(); @@ -381,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(); @@ -392,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(); @@ -401,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(); @@ -411,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(); @@ -422,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(); @@ -432,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'); } /** @@ -454,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(); @@ -464,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."); } /** @@ -519,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.'); } /** @@ -640,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); @@ -661,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); @@ -696,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. @@ -758,7 +872,7 @@ 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))); } /** @@ -824,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. @@ -846,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']); @@ -886,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'); } /** @@ -906,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; @@ -919,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); } @@ -928,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.'); } /** @@ -957,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++) { @@ -998,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'); } } @@ -1045,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( @@ -1115,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'); } /** @@ -1148,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.'); } /** @@ -1190,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"); } /** @@ -1224,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); } /** @@ -1237,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 { @@ -1459,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']; @@ -1529,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'); @@ -1543,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)); } @@ -1582,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); @@ -1594,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); @@ -1603,13 +2011,13 @@ // 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.'); } /** @@ -1641,10 +2049,10 @@ // 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.')); + $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( @@ -1670,16 +2078,16 @@ '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.')); + $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, t('Entity 1: the error was flagged on the correct element.')); + $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')), t('Entity 2: the field validation error was reported.')); + $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, t('Entity 2: the error was flagged on the correct element.')); + $this->assertTrue($error_field, 'Entity 2: the error was flagged on the correct element.'); // Test that reordering works on both entities. $edit = array( @@ -1699,10 +2107,10 @@ // '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.')); + $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, @@ -1710,10 +2118,10 @@ '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.')); + $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(); @@ -1784,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. @@ -1798,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( @@ -1821,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 @@ -1832,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 @@ -1842,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))); } } @@ -1859,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. @@ -1875,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. @@ -1891,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 @@ -1901,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 @@ -1911,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))); } } } @@ -1954,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 { @@ -2092,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.'); } /** @@ -2107,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. @@ -2121,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. @@ -2135,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'); } /** @@ -2166,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']); @@ -2353,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); @@ -2373,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.'); } } @@ -2425,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 { @@ -2500,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.'); } /** @@ -2523,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']); @@ -2538,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']); @@ -2552,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. } @@ -2578,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.'); } } @@ -2664,17 +3108,17 @@ $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.'); } /** @@ -2709,10 +3153,10 @@ $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.'); } /** @@ -2726,7 +3170,7 @@ $options = array(); $entities = array(); $entity_type = 'test_entity'; - $entity_count = mt_rand(2, 5); + $entity_count = 5; $available_languages = field_available_languages($this->entity_type, $this->field); for ($id = 1; $id <= $entity_count; ++$id) { @@ -2773,17 +3217,17 @@ $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, t('The result for entity %id/%language is correctly stored.', array('%id' => $id, '%language' => $langcode))); + $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), t('No unavailable language has been processed for entity %id.', array('%id' => $id))); + $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]), t('The result language %language for entity %id was correctly suggested (display language: %display_language).', array('%id' => $id, '%language' => $langcode, '%display_language' => $display_language))); + $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))); } } } @@ -2795,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); @@ -2804,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']); } @@ -2821,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))); } } @@ -2877,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. @@ -2891,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'); } /** @@ -2948,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))); } } } @@ -2987,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'); @@ -3038,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++; } @@ -3103,6 +3582,7 @@ * instance is deleted. */ function testPurgeInstance() { + // Start recording hook invocations. field_test_memorize(); $bundle = reset($this->bundles); @@ -3117,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); @@ -3131,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; + } + foreach ($stubs as $stub) { + $hooks['field_test_field_delete'][] = $stub; } - $this->assertEqual(count($stubs), $count-10, 'hook_field_delete was called with each entity once'); + $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)); @@ -3166,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); @@ -3183,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'); diff -Naur drupal-7.5/modules/field/tests/field_test.entity.inc drupal-7.66/modules/field/tests/field_test.entity.inc --- drupal-7.5/modules/field/tests/field_test.entity.inc 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/field/tests/field_test.entity.inc 2019-04-17 22:20:46.000000000 +0200 @@ -9,6 +9,12 @@ * Implements hook_entity_info(). */ function field_test_entity_info() { + // If requested, clear the field cache while this hook is being called. See + // testFieldInfoCache(). + if (variable_get('field_test_clear_info_cache_in_hook_entity_info', FALSE)) { + field_info_cache_clear(); + } + $bundles = variable_get('field_test_bundles', array('test_bundle' => array('label' => 'Test Bundle'))); $test_entity_modes = array( 'full' => array( @@ -23,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', @@ -38,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( @@ -50,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, @@ -58,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', @@ -72,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', @@ -90,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', @@ -104,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.5/modules/field/tests/field_test.field.inc drupal-7.66/modules/field/tests/field_test.field.inc --- drupal-7.5/modules/field/tests/field_test.field.inc 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/field/tests/field_test.field.inc 2019-04-17 22:20:46.000000000 +0200 @@ -28,7 +28,9 @@ 'shape' => 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', @@ -58,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. @@ -73,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( @@ -350,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.5/modules/field/tests/field_test.info drupal-7.66/modules/field/tests/field_test.info --- drupal-7.5/modules/field/tests/field_test.info 2011-07-27 22:26:55.000000000 +0200 +++ drupal-7.66/modules/field/tests/field_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -6,8 +6,7 @@ version = VERSION hidden = TRUE -; Information added by drupal.org packaging script on 2011-07-27 -version = "7.5" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1311798415" - +datestamp = "1555533576" diff -Naur drupal-7.5/modules/field/tests/field_test.install drupal-7.66/modules/field/tests/field_test.install --- drupal-7.5/modules/field/tests/field_test.install 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/field/tests/field_test.install 2019-04-17 22:20:46.000000000 +0200 @@ -60,7 +60,7 @@ 'description' => 'The base table for test entities with a bundle key.', 'fields' => array( 'ftid' => array( - 'description' => 'The primary indentifier for a test_entity_bundle_key.', + 'description' => 'The primary identifier for a test_entity_bundle_key.', 'type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, @@ -79,7 +79,7 @@ 'description' => 'The base table for test entities with a bundle.', 'fields' => array( 'ftid' => array( - 'description' => 'The primary indentifier for a test_entity_bundle.', + 'description' => 'The primary identifier for a test_entity_bundle.', 'type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, @@ -132,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( @@ -145,6 +157,6 @@ 'not null' => FALSE, ), ), - ); + ) + $foreign_keys; } } diff -Naur drupal-7.5/modules/field/tests/field_test.module drupal-7.66/modules/field/tests/field_test.module --- drupal-7.5/modules/field/tests/field_test.module 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/field/tests/field_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -183,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) { @@ -228,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". @@ -247,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.5/modules/field/tests/field_test.storage.inc drupal-7.66/modules/field/tests/field_test.storage.inc --- drupal-7.5/modules/field/tests/field_test.storage.inc 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/field/tests/field_test.storage.inc 2019-04-17 22:20:46.000000000 +0200 @@ -282,7 +282,7 @@ case '=': $match = $match && $row->{$column} == $value; break; - case '!=': + case '<>': case '<': case '<=': case '>': diff -Naur drupal-7.5/modules/field/theme/field.tpl.php drupal-7.66/modules/field/theme/field.tpl.php --- drupal-7.5/modules/field/theme/field.tpl.php 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/field/theme/field.tpl.php 2019-04-17 22:20:46.000000000 +0200 @@ -4,8 +4,10 @@ * @file field.tpl.php * Default template implementation to display the value of a field. * - * This file is not used and is here as a starting point for customization only. - * @see theme_field() + * This file is not used by Drupal core, which uses theme functions instead for + * performance reasons. The markup is the same, though, so if you want to use + * template files rather than functions to extend field theming, copy this to + * your custom theme. See theme_field() for a discussion of performance. * * Available variables: * - $items: An array of field values. Use render() to output them. @@ -40,20 +42,22 @@ * * @see template_preprocess_field() * @see theme_field() + * + * @ingroup themeable */ ?> -
> - +
> +
>
> - $item) : ?> + $item): ?>
>
diff -Naur drupal-7.5/modules/field_ui/field_ui-rtl.css drupal-7.66/modules/field_ui/field_ui-rtl.css --- drupal-7.5/modules/field_ui/field_ui-rtl.css 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/field_ui/field_ui-rtl.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,3 +1,7 @@ +/** + * @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.5/modules/field_ui/field_ui.admin.inc drupal-7.66/modules/field_ui/field_ui.admin.inc --- drupal-7.5/modules/field_ui/field_ui.admin.inc 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/field_ui/field_ui.admin.inc 2019-04-17 22:20:46.000000000 +0200 @@ -12,17 +12,27 @@ $instances = field_info_instances(); $field_types = field_info_field_types(); $bundles = field_info_bundles(); + + $modules = system_rebuild_module_data(); + $header = array(t('Field name'), t('Field type'), t('Used in')); $rows = array(); foreach ($instances as $entity_type => $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(''); } } } @@ -41,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); @@ -62,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; @@ -81,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']) { @@ -99,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']) { @@ -150,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'] : ''); } @@ -205,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. @@ -220,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), ), ); } @@ -230,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), ), ); } @@ -244,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; } @@ -259,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); @@ -295,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), ), @@ -477,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', @@ -509,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')), @@ -590,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( @@ -609,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); @@ -617,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']; @@ -640,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. @@ -681,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 @@ -719,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']; @@ -753,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'], @@ -778,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'); } } @@ -787,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( @@ -808,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'); } } } @@ -825,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); @@ -882,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'), @@ -938,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', @@ -1177,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']; @@ -1223,7 +1277,7 @@ $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']); @@ -1255,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']; @@ -1333,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 @@ -1342,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 @@ -1353,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. @@ -1377,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__); @@ -1399,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. @@ -1435,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. @@ -1463,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) { @@ -1479,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']; @@ -1555,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']; @@ -1576,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']); @@ -1628,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']; @@ -1650,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']; @@ -1688,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']; @@ -1708,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']; @@ -1897,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']; @@ -1920,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 @@ -1961,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']; @@ -1970,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'])) { @@ -1995,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); @@ -2007,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.5/modules/field_ui/field_ui.api.php drupal-7.66/modules/field_ui/field_ui.api.php --- drupal-7.5/modules/field_ui/field_ui.api.php 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/field_ui/field_ui.api.php 2019-04-17 22:20:46.000000000 +0200 @@ -6,7 +6,7 @@ */ /** - * @ingroup field_ui_field_type + * @addtogroup field_types * @{ */ @@ -42,7 +42,7 @@ '#title' => 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; @@ -82,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, ); } @@ -113,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, ); } @@ -122,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, ); } @@ -132,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. @@ -160,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, ); } @@ -170,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 @@ -200,5 +203,5 @@ } /** - * @} End of "ingroup field_ui_field_type" + * @} End of "addtogroup field_types". */ diff -Naur drupal-7.5/modules/field_ui/field_ui.css drupal-7.66/modules/field_ui/field_ui.css --- drupal-7.5/modules/field_ui/field_ui.css 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/field_ui/field_ui.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,8 @@ - +/** + * @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 */ @@ -8,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; @@ -25,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.5/modules/field_ui/field_ui.info drupal-7.66/modules/field_ui/field_ui.info --- drupal-7.5/modules/field_ui/field_ui.info 2011-07-27 22:26:55.000000000 +0200 +++ drupal-7.66/modules/field_ui/field_ui.info 2019-04-17 22:39:36.000000000 +0200 @@ -6,8 +6,7 @@ dependencies[] = field files[] = field_ui.test -; Information added by drupal.org packaging script on 2011-07-27 -version = "7.5" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1311798415" - +datestamp = "1555533576" diff -Naur drupal-7.5/modules/field_ui/field_ui.js drupal-7.66/modules/field_ui/field_ui.js --- drupal-7.5/modules/field_ui/field_ui.js 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/field_ui/field_ui.js 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,8 @@ - +/** + * @file + * Attaches the behaviors for the Field UI module. + */ + (function($) { Drupal.behaviors.fieldUIFieldOverview = { @@ -24,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; @@ -39,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); @@ -50,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 : ''); } }); @@ -86,7 +97,7 @@ html += ''; }); - $(this).html(html).attr('disabled', disabled ? 'disabled' : ''); + $(this).html(html).attr('disabled', disabled ? 'disabled' : false); }); }; @@ -118,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); } }); @@ -129,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 = {}; @@ -157,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'); @@ -308,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.5/modules/field_ui/field_ui.module drupal-7.66/modules/field_ui/field_ui.module --- drupal-7.5/modules/field_ui/field_ui.module 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/field_ui/field_ui.module 2019-04-17 22:20:46.000000000 +0200 @@ -1,8 +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') . '
'; @@ -48,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() { @@ -96,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', @@ -202,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. @@ -250,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; @@ -302,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); @@ -313,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]; } @@ -340,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. @@ -355,10 +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.5/modules/field_ui/field_ui.test drupal-7.66/modules/field_ui/field_ui.test --- drupal-7.5/modules/field_ui/field_ui.test 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/field_ui/field_ui.test 2019-04-17 22:20:46.000000000 +0200 @@ -6,7 +6,7 @@ */ /** - * Helper class for Field UI test classes. + * Provides common functionality for the Field UI test classes. */ class FieldUITestCase extends DrupalWebTestCase { @@ -22,11 +22,11 @@ parent::setUp($modules); // 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. - $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. @@ -34,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). @@ -59,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). @@ -98,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 @@ -119,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() { @@ -152,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. @@ -165,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. @@ -208,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. @@ -231,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( @@ -256,7 +256,7 @@ } /** - * Assert the field settings. + * Asserts field settings are as expected. * * @param $bundle * The bundle name for the instance. @@ -269,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.'); } /** @@ -303,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.'); } /** @@ -338,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); @@ -360,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'; @@ -398,23 +398,70 @@ '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() { @@ -430,7 +477,7 @@ } /** - * Test formatter formatter settings. + * Tests formatter settings. */ function testFormatterUI() { $manage_fields = 'admin/structure/types/manage/' . $this->hyphen_type; @@ -439,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); @@ -454,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'); @@ -464,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')); @@ -473,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 @@ -506,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. @@ -515,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. @@ -530,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. @@ -538,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( @@ -546,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. @@ -569,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. @@ -587,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. @@ -613,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. @@ -627,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.5/modules/file/file.api.php drupal-7.66/modules/file/file.api.php --- drupal-7.5/modules/file/file.api.php 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/file/file.api.php 2019-04-17 22:20:46.000000000 +0200 @@ -12,8 +12,8 @@ * file is referenced, e.g., only users with access to a node should be allowed * to download files attached to that node. * - * @param $field - * The field to which the file belongs. + * @param array $file_item + * The array of information about the file to check access for. * @param $entity_type * The type of $entity; for example, 'node' or 'user'. * @param $entity @@ -26,7 +26,7 @@ * * @see hook_field_access(). */ -function hook_file_download_access($field, $entity_type, $entity) { +function hook_file_download_access($file_item, $entity_type, $entity) { if ($entity_type == 'node') { return node_access('view', $entity); } @@ -45,20 +45,14 @@ * An array of grants gathered by hook_file_download_access(). The array is * keyed by the module that defines the entity type's access control; the * values are Boolean grant responses for each module. - * @param $field - * The field to which the file belongs. + * @param array $file_item + * The array of information about the file to alter access for. * @param $entity_type * The type of $entity; for example, 'node' or 'user'. * @param $entity * The $entity to which $file is referenced. - * - * @return - * An array of grants, keyed by module name, each with a Boolean grant value. - * Return an empty array to assert FALSE. You may choose to return your own - * module's value in addition to other grants or to overwrite the values set by - * other modules. */ -function hook_file_download_access_alter(&$grants, $field, $entity_type, $entity) { +function hook_file_download_access_alter(&$grants, $file_item, $entity_type, $entity) { // For our example module, we always enforce the rules set by node module. if (isset($grants['node'])) { $grants = array('node' => $grants['node']); diff -Naur drupal-7.5/modules/file/file.css drupal-7.66/modules/file/file.css --- drupal-7.5/modules/file/file.css 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/file/file.css 2019-04-17 22:20:46.000000000 +0200 @@ -1,3 +1,7 @@ +/** + * @file + * Admin stylesheet for file module. + */ /** * Managed file element styles. @@ -19,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.5/modules/file/file.field.inc drupal-7.66/modules/file/file.field.inc --- drupal-7.5/modules/file/file.field.inc 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/file/file.field.inc 2019-04-17 22:20:46.000000000 +0200 @@ -92,6 +92,7 @@ '#description' => 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, @@ -186,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); } } } @@ -215,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); @@ -243,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 @@ -261,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) { @@ -312,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. @@ -358,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. */ @@ -428,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(), ); @@ -447,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); - } - } - - 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]); - } + // 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']; } - // 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'])); @@ -495,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; @@ -532,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. @@ -561,7 +567,7 @@ } /** - * Determine the URI for a file field instance. + * Determines the URI for a file field instance. * * @param $field * A field array. @@ -569,6 +575,7 @@ * A field instance array. * @param $data * An array of token objects to pass to token_replace(). + * * @return * A file directory URI with tokens replaced. * @@ -592,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; } } @@ -625,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'), @@ -643,10 +650,9 @@ // 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.'), ); @@ -720,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. */ @@ -741,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(). * @@ -757,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); } /** @@ -960,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.5/modules/file/file.info drupal-7.66/modules/file/file.info --- drupal-7.5/modules/file/file.info 2011-07-27 22:26:55.000000000 +0200 +++ drupal-7.66/modules/file/file.info 2019-04-17 22:39:36.000000000 +0200 @@ -6,8 +6,7 @@ dependencies[] = field files[] = tests/file.test -; Information added by drupal.org packaging script on 2011-07-27 -version = "7.5" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1311798415" - +datestamp = "1555533576" diff -Naur drupal-7.5/modules/file/file.js drupal-7.66/modules/file/file.js --- drupal-7.5/modules/file/file.js 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/file/file.js 2019-04-17 22:20:46.000000000 +0200 @@ -1,4 +1,3 @@ - /** * @file * Provides JavaScript additions to the managed file field type. @@ -74,10 +73,17 @@ var acceptableMatch = new RegExp('\\.(' + extensionPattern + ')$', 'gi'); if (!acceptableMatch.test(this.value)) { var error = Drupal.t("The selected file %filename cannot be uploaded. Only files with the following extensions are allowed: %extensions.", { - '%filename': this.value, + // According to the specifications of HTML5, a file upload control + // should not reveal the real local path to the file that a user + // has selected. Some web browsers implement this restriction by + // replacing the local path with "C:\fakepath\", which can cause + // confusion by leaving the user thinking perhaps Drupal could not + // find the file because it messed up the file path. To avoid this + // confusion, therefore, we strip out the bogus fakepath string. + '%filename': this.value.replace('C:\\fakepath\\', ''), '%extensions': extensionPattern.replace(/\|/g, ', ') }); - $(this).parents('div.form-managed-file').prepend('
' + error + '
'); + $(this).closest('div.form-managed-file').prepend('
' + error + '
'); this.value = ''; return false; } @@ -96,8 +102,8 @@ // 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 @@ -111,7 +117,7 @@ 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); }, /** @@ -119,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. @@ -133,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.5/modules/file/file.module drupal-7.66/modules/file/file.module --- drupal-7.5/modules/file/file.module 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/file/file.module 2019-04-17 22:20:46.000000000 +0200 @@ -16,7 +16,7 @@ case 'admin/help#file': $output = ''; $output .= '

' . 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') . '
'; @@ -45,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, @@ -57,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'); @@ -73,6 +72,7 @@ '#progress_message' => NULL, '#upload_validators' => array(), '#upload_location' => NULL, + '#size' => 22, '#extended' => FALSE, '#attached' => array( 'css' => array($file_path . '/file.css'), @@ -92,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', @@ -140,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. @@ -162,24 +166,27 @@ // Try to load $entity and $field. $entity = entity_load($entity_type, array($id)); $entity = reset($entity); - $field = NULL; + $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 @@ -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. @@ -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'] = '
'; + $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,6 +1092,11 @@ $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(); } } diff -Naur drupal-7.5/modules/file/tests/file.test drupal-7.66/modules/file/tests/file.test --- drupal-7.5/modules/file/tests/file.test 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/file/tests/file.test 2019-04-17 22:20:46.000000000 +0200 @@ -6,19 +6,28 @@ */ /** - * This class provides methods specifically for testing File's field handling. + * Provides methods specifically for testing File module's field handling. */ class FileFieldTestCase extends DrupalWebTestCase { protected $admin_user; function setUp() { - parent::setUp('file', 'file_module_test'); - $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')); + // 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. @@ -31,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. @@ -65,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. @@ -99,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); @@ -110,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(), @@ -124,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. @@ -140,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. */ @@ -154,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( @@ -167,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. @@ -229,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) { @@ -239,19 +399,31 @@ // 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. foreach (array(FALSE, TRUE) as $ajax) { @@ -266,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); @@ -279,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); @@ -293,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.'); } } } @@ -301,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() { @@ -313,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 @@ -336,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) { @@ -359,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 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))); } } /** - * Tests upload and remove buttons, with and without Ajax, for a multi-valued File field. + * 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 @@ -379,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. + // 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')); @@ -457,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.'); } } @@ -483,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.'); } /** @@ -508,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')); @@ -549,17 +923,17 @@ $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); @@ -571,13 +945,13 @@ // Ensures normal user can no longer download the file. $this->drupalLogin($user); $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.'); } } /** - * Test class to test file handling with node revisions. + * Tests file handling with node revisions. */ class FileFieldRevisionTestCase extends FileFieldTestCase { public static function getInfo() { @@ -589,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 @@ -618,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. @@ -644,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(); @@ -671,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); @@ -681,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() { @@ -704,7 +1078,7 @@ } /** - * Test normal formatter display on node display. + * Tests normal formatter display on node display. */ function testNodeDisplay() { $field_name = strtolower($this->randomName()); @@ -712,6 +1086,7 @@ $field_settings = array( 'display_field' => '1', 'display_default' => '1', + 'cardinality' => FIELD_CARDINALITY_UNLIMITED, ); $instance_settings = array( 'description_field' => '1', @@ -721,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. @@ -731,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; @@ -758,7 +1185,7 @@ } /** - * Test required property on file fields. + * Tests the required property on file fields. */ function testRequired() { $type_name = 'article'; @@ -773,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); @@ -792,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'; @@ -834,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. @@ -850,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'; @@ -877,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')); @@ -886,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")); @@ -895,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); @@ -904,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() { @@ -916,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()); @@ -930,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')); @@ -941,7 +1368,7 @@ // 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. @@ -957,11 +1384,11 @@ // 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 @@ -984,7 +1411,7 @@ } /** - * Test file token replacement in strings. + * Tests the file token replacement in strings. */ class FileTokenReplaceTestCase extends FileFieldTestCase { public static function getInfo() { @@ -1013,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->assertEqual($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->assertEqual($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.5/modules/file/tests/file_module_test.info drupal-7.66/modules/file/tests/file_module_test.info --- drupal-7.5/modules/file/tests/file_module_test.info 2011-07-27 22:26:55.000000000 +0200 +++ drupal-7.66/modules/file/tests/file_module_test.info 2019-04-17 22:39:36.000000000 +0200 @@ -5,8 +5,7 @@ core = 7.x hidden = TRUE -; Information added by drupal.org packaging script on 2011-07-27 -version = "7.5" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1311798415" - +datestamp = "1555533576" diff -Naur drupal-7.5/modules/file/tests/file_module_test.module drupal-7.66/modules/file/tests/file_module_test.module --- drupal-7.5/modules/file/tests/file_module_test.module 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/file/tests/file_module_test.module 2019-04-17 22:20:46.000000000 +0200 @@ -22,7 +22,10 @@ } /** - * Form builder for testing a 'managed_file' element. + * Form constructor for testing a 'managed_file' element. + * + * @see file_module_test_form_submit() + * @ingroup forms */ function file_module_test_form($form, &$form_state, $tree = TRUE, $extended = FALSE, $default_fid = NULL) { $form['#tree'] = (bool) $tree; @@ -33,6 +36,7 @@ '#upload_location' => '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; @@ -63,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.5/modules/filter/filter.admin.inc drupal-7.66/modules/filter/filter.admin.inc --- drupal-7.5/modules/filter/filter.admin.inc 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/filter/filter.admin.inc 2019-04-17 22:20:46.000000000 +0200 @@ -2,13 +2,14 @@ /** * @file - * Admin page callbacks for the filter module. + * Administrative page callbacks for the Filter module. */ /** - * Menu callback; Displays a list of all text formats and allows them to be rearranged. + * Page callback: Form constructor for a form to list and reorder text formats. * * @ingroup forms + * @see filter_menu() * @see filter_admin_overview_submit() */ function filter_admin_overview($form) { @@ -45,6 +46,9 @@ return $form; } +/** + * Form submission handler for filter_admin_overview(). + */ function filter_admin_overview_submit($form, &$form_state) { foreach ($form_state['values']['formats'] as $id => $data) { if (is_array($data) && isset($data['weight'])) { @@ -95,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)) { @@ -109,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()); @@ -287,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']); @@ -304,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. @@ -336,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; @@ -353,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']; @@ -362,4 +406,3 @@ $form_state['redirect'] = 'admin/config/content/formats'; } - diff -Naur drupal-7.5/modules/filter/filter.api.php drupal-7.66/modules/filter/filter.api.php --- drupal-7.5/modules/filter/filter.api.php 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/filter/filter.api.php 2019-04-17 22:20:46.000000000 +0200 @@ -57,20 +57,20 @@ * - description: Additional administrative information about the filter's * behavior, if needed for clarification. * - settings callback: The name of a function that returns configuration form - * elements for the filter. See hook_filter_FILTER_settings() for details. + * elements for the filter. See callback_filter_settings() for details. * - default settings: An associative array containing default settings for * the filter, to be applied when the filter has not been configured yet. * - prepare callback: The name of a function that escapes the content before - * the actual filtering happens. See hook_filter_FILTER_prepare() for + * the actual filtering happens. See callback_filter_prepare() for * details. * - process callback: (required) The name the function that performs the - * actual filtering. See hook_filter_FILTER_process() for details. + * actual filtering. See callback_filter_process() for details. * - cache (default TRUE): Specifies whether the filtered text can be cached. * Note that setting this to FALSE makes the entire text format not * cacheable, which may have an impact on the site's overall performance. * See filter_format_allowcache() for details. * - tips callback: The name of a function that returns end-user-facing filter - * usage guidelines for the filter. See hook_filter_FILTER_tips() for + * usage guidelines for the filter. See callback_filter_tips() for * details. * - weight: A default weight for the filter in new text formats. * @@ -122,11 +122,9 @@ */ /** - * Settings callback for hook_filter_info(). + * Provide a settings form for filter settings. * - * Note: This is not really a hook. The function name is manually specified via - * 'settings callback' in hook_filter_info(), with this recommended callback - * name pattern. It is called from filter_admin_format_form(). + * Callback for hook_filter_info(). * * This callback function is used to provide a settings form for filter * settings, for filters that need settings on a per-text-format basis. This @@ -158,8 +156,10 @@ * @return * An array of form elements defining settings for the filter. Array keys * should match the array keys in $filter->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(); @@ -172,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, @@ -199,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. @@ -232,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. @@ -260,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.5/modules/filter/filter.info drupal-7.66/modules/filter/filter.info --- drupal-7.5/modules/filter/filter.info 2011-07-27 22:26:55.000000000 +0200 +++ drupal-7.66/modules/filter/filter.info 2019-04-17 22:39:36.000000000 +0200 @@ -7,8 +7,7 @@ required = TRUE configure = admin/config/content/formats -; Information added by drupal.org packaging script on 2011-07-27 -version = "7.5" +; Information added by Drupal.org packaging script on 2019-04-17 +version = "7.66" project = "drupal" -datestamp = "1311798415" - +datestamp = "1555533576" diff -Naur drupal-7.5/modules/filter/filter.install drupal-7.66/modules/filter/filter.install --- drupal-7.5/modules/filter/filter.install 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/filter/filter.install 2019-04-17 22:20:46.000000000 +0200 @@ -2,7 +2,7 @@ /** * @file - * Install, update and uninstall functions for the filter module. + * Install, update, and uninstall functions for the Filter module. */ /** @@ -152,8 +152,9 @@ * Implements hook_update_dependencies(). */ function filter_update_dependencies() { - // Filter update 7007 migrates permissions and therefore needs to run after - // the {role} table is properly set up. + // filter_update_7005() 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['filter'][7005] = array( 'user' => 7007, ); @@ -489,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.5/modules/filter/filter.js drupal-7.66/modules/filter/filter.js --- drupal-7.5/modules/filter/filter.js 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/filter/filter.js 2019-04-17 22:20:46.000000000 +0200 @@ -7,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.5/modules/filter/filter.module drupal-7.66/modules/filter/filter.module --- drupal-7.5/modules/filter/filter.module 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/filter/filter.module 2019-04-17 22:20:46.000000000 +0200 @@ -2,7 +2,7 @@ /** * @file - * Framework for handling filtering of content. + * Framework for handling the filtering of content. */ /** @@ -13,7 +13,7 @@ case 'admin/help#filter': $output = ''; $output .= '

' . 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') . '
'; @@ -71,6 +71,7 @@ * Implements hook_element_info(). * * @see filter_process_format() + * @see text_format_wrapper() */ function filter_element_info() { $type['text_format'] = array( @@ -92,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.', @@ -132,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. @@ -146,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. @@ -164,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); @@ -215,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']; @@ -269,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 @@ -311,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; @@ -323,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, ); @@ -331,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.')), @@ -348,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). @@ -378,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. @@ -425,7 +452,7 @@ } /** - * Resets text format caches. + * Resets the text format caches. * * @see filter_formats() */ @@ -441,6 +468,7 @@ * * @param $format * An object representing the text format. + * * @return * An array of role names, keyed by role ID. */ @@ -459,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. @@ -492,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. * @@ -523,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() @@ -548,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()); @@ -555,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()); @@ -586,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 @@ -601,6 +642,7 @@ * * @param $format_id * The text format ID to check. + * * @return * TRUE if the given text format allows caching, FALSE otherwise. */ @@ -617,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. @@ -638,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 @@ -671,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. @@ -691,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 @@ -702,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 */ @@ -759,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; @@ -778,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'; @@ -789,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 @@ -927,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 @@ -970,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. @@ -992,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; @@ -1026,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. */ @@ -1048,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. */ @@ -1064,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); } /** @@ -1086,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) { @@ -1103,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); } @@ -1117,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'))) . '

'; } /** @@ -1143,7 +1220,7 @@ /** * @defgroup standard_filters Standard filters * @{ - * Filters implemented by the filter.module. + * Filters implemented by the Filter module. */ /** @@ -1191,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; @@ -1217,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); @@ -1236,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; @@ -1334,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; @@ -1347,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. @@ -1381,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. @@ -1405,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; @@ -1475,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. @@ -1488,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. @@ -1501,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. @@ -1514,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(); @@ -1537,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 ""; } @@ -1568,21 +1669,30 @@ } /** - * 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) { @@ -1648,7 +1758,11 @@ } /** - * Filter tips callback for auto-paragraph filter. + * Implements callback_filter_tips(). + * + * Provides help for the auto-paragraph filter. + * + * @see filter_filter_info() */ function _filter_autop_tips($filter, $format, $long = FALSE) { if ($long) { @@ -1660,6 +1774,8 @@ } /** + * Implements callback_filter_process(). + * * Escapes all HTML tags, so they will be visible instead of being effective. */ function _filter_html_escape($text) { @@ -1667,7 +1783,11 @@ } /** - * Filter tips callback for HTML escaping filter. + * Implements callback_filter_tips(). + * + * Provides help for the HTML escaping filter. + * + * @see filter_filter_info() */ function _filter_html_escape_tips($filter, $format, $long = FALSE) { return t('No HTML tags allowed.'); diff -Naur drupal-7.5/modules/filter/filter.pages.inc drupal-7.66/modules/filter/filter.pages.inc --- drupal-7.5/modules/filter/filter.pages.inc 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/filter/filter.pages.inc 2019-04-17 22:20:46.000000000 +0200 @@ -2,17 +2,21 @@ /** * @file - * User page callbacks for the filter module. + * User page callbacks for the Filter module. */ - /** - * Menu callback; show a page with long filter tips. + * Page callback: Displays a page with long filter tips. + * + * @return string + * An HTML-formatted string. + * + * @see filter_menu() + * @see theme_filter_tips() */ -function filter_tips_long() { - $format_id = arg(2); - if ($format_id) { - $output = theme('filter_tips', array('tips' => _filter_tips($format_id, TRUE), 'long' => TRUE)); +function filter_tips_long($format = NULL) { + if (!empty($format)) { + $output = theme('filter_tips', array('tips' => _filter_tips($format->format, TRUE), 'long' => TRUE)); } else { $output = theme('filter_tips', array('tips' => _filter_tips(-1, TRUE), 'long' => TRUE)); @@ -20,13 +24,12 @@ return $output; } - /** * Returns HTML for a set of filter tips. * * @param $variables * An associative array containing: - * - tips: An array containing descriptions and a CSS id in the form of + * - tips: An array containing descriptions and a CSS ID in the form of * 'module-name/filter-id' (only used when $long is TRUE) for each * filter in one or more text formats. Example: * @code @@ -64,7 +67,7 @@ foreach ($tips as $name => $tiplist) { if ($multiple) { $output .= '

'; - $output .= '

' . $name . '

'; + $output .= '

' . check_plain($name) . '

'; } if (count($tiplist) > 0) { diff -Naur drupal-7.5/modules/filter/filter.test drupal-7.66/modules/filter/filter.test --- drupal-7.5/modules/filter/filter.test 2011-07-27 22:17:40.000000000 +0200 +++ drupal-7.66/modules/filter/filter.test 2019-04-17 22:20:46.000000000 +0200 @@ -22,7 +22,7 @@ } /** - * Test CRUD operations for text formats and filters. + * Tests CRUD operations for text formats and filters. */ function testTextFormatCRUD() { // Add a text format with minimum data only. @@ -67,13 +67,22 @@ filter_format_disable($format); $db_format = db_query("SELECT * FROM {filter_format} WHERE format = :format", array(':format' => $format->format))->fetchObject(); - $this->assertFalse($db_format->status, t('Database: Disabled text format is marked as disabled.')); + $this->assertFalse($db_format->status, 'Database: Disabled text format is marked as disabled.'); $formats = filter_formats(); - $this->assertTrue(!isset($formats[$format->format]), t('filter_formats: Disabled text format no longer exists.')); + $this->assertTrue(!isset($formats[$format->format]), 'filter_formats: Disabled text format no longer exists.'); + + // Add a new format to check for Xss in format name. + $format = new stdClass(); + $format->format = 'xss_format'; + $format->name = ''; + filter_format_save($format); + user_role_change_permissions(DRUPAL_ANONYMOUS_RID, array(filter_permission_name($format) => 1)); + $this->drupalGet('filter/tips'); + $this->assertNoRaw($format->name, 'Text format name contains no xss.'); } /** - * Verify that a text format is properly stored. + * Verifies that a text format is properly stored. */ function verifyTextFormat($format) { $t_args = array('%format' => $format->name); @@ -83,17 +92,17 @@ ->condition('format', $format->format) ->execute() ->fetchObject(); - $this->assertEqual($db_format->format, $format->format, t('Database: Proper format id for text format %format.', $t_args)); - $this->assertEqual($db_format->name, $format->name, t('Database: Proper title for text format %format.', $t_args)); - $this->assertEqual($db_format->cache, $format->cache, t('Database: Proper cache indicator for text format %format.', $t_args)); - $this->assertEqual($db_format->weight, $format->weight, t('Database: Proper weight for text format %format.', $t_args)); + $this->assertEqual($db_format->format, $format->format, format_string('Database: Proper format id for text format %format.', $t_args)); + $this->assertEqual($db_format->name, $format->name, format_string('Database: Proper title for text format %format.', $t_args)); + $this->assertEqual($db_format->cache, $format->cache, format_string('Database: Proper cache indicator for text format %format.', $t_args)); + $this->assertEqual($db_format->weight, $format->weight, format_string('Database: Proper weight for text format %format.', $t_args)); // Verify filter_format_load(). $filter_format = filter_format_load($format->format); - $this->assertEqual($filter_format->format, $format->format, t('filter_format_load: Proper format id for text format %format.', $t_args)); - $this->assertEqual($filter_format->name, $format->name, t('filter_format_load: Proper title for text format %format.', $t_args)); - $this->assertEqual($filter_format->cache, $format->cache, t('filter_format_load: Proper cache indicator for text format %format.', $t_args)); - $this->assertEqual($filter_format->weight, $format->weight, t('filter_format_load: Proper weight for text format %format.', $t_args)); + $this->assertEqual($filter_format->format, $format->format, format_string('filter_format_load: Proper format id for text format %format.', $t_args)); + $this->assertEqual($filter_format->name, $format->name, format_string('filter_format_load: Proper title for text format %format.', $t_args)); + $this->assertEqual($filter_format->cache, $format->cache, format_string('filter_format_load: Proper cache indicator for text format %format.', $t_args)); + $this->assertEqual($filter_format->weight, $format->weight, format_string('filter_format_load: Proper weight for text format %format.', $t_args)); // Verify the 'cache' text format property according to enabled filters. $filter_info = filter_get_filters(); @@ -107,11 +116,11 @@ break; } } - $this->assertEqual($filter_format->cache, $cacheable, t('Text format contains proper cache property.')); + $this->assertEqual($filter_format->cache, $cacheable, 'Text format contains proper cache property.'); } /** - * Verify that filters are properly stored for a text format. + * Verifies that filters are properly stored for a text format. */ function verifyFilters($format) { // Verify filter database records. @@ -121,20 +130,20 @@ $t_args = array('%format' => $format->name, '%filter' => $name); // Verify that filter status is properly stored. - $this->assertEqual($filter->status, $format_filters[$name]['status'], t('Database: Proper status for %filter in text format %format.', $t_args)); + $this->assertEqual($filter->status, $format_filters[$name]['status'], format_string('Database: Proper status for %filter in text format %format.', $t_args)); // Verify that filter settings were properly stored. - $this->assertEqual(unserialize($filter->settings), isset($format_filters[$name]['settings']) ? $format_filters[$name]['settings'] : array(), t('Database: Proper filter settings for %filter in text format %format.', $t_args)); + $this->assertEqual(unserialize($filter->settings), isset($format_filters[$name]['settings']) ? $format_filters[$name]['settings'] : array(), format_string('Database: Proper filter settings for %filter in text format %format.', $t_args)); // Verify that each filter has a module name assigned. - $this->assertTrue(!empty($filter->module), t('Database: Proper module name for %filter in text format %format.', $t_args)); + $this->assertTrue(!empty($filter->module), format_string('Database: Proper module name for %filter in text format %format.', $t_args)); // Remove the filter from the copy of saved $format to check whether all // filters have been processed later. unset($format_filters[$name]); } // Verify that all filters have been processed. - $this->assertTrue(empty($format_filters), t('Database contains values for all filters in the saved format.')); + $this->assertTrue(empty($format_filters), 'Database contains values for all filters in the saved format.'); // Verify filter_list_format(). $filters = filter_list_format($format->format); @@ -143,23 +152,26 @@ $t_args = array('%format' => $format->name, '%filter' => $name); // Verify that filter status is properly stored. - $this->assertEqual($filter->status, $format_filters[$name]['status'], t('filter_list_format: Proper status for %filter in text format %format.', $t_args)); + $this->assertEqual($filter->status, $format_filters[$name]['status'], format_string('filter_list_format: Proper status for %filter in text format %format.', $t_args)); // Verify that filter settings were properly stored. - $this->assertEqual($filter->settings, isset($format_filters[$name]['settings']) ? $format_filters[$name]['settings'] : array(), t('filter_list_format: Proper filter settings for %filter in text format %format.', $t_args)); + $this->assertEqual($filter->settings, isset($format_filters[$name]['settings']) ? $format_filters[$name]['settings'] : array(), format_string('filter_list_format: Proper filter settings for %filter in text format %format.', $t_args)); // Verify that each filter has a module name assigned. - $this->assertTrue(!empty($filter->module), t('filter_list_format: Proper module name for %filter in text format %format.', $t_args)); + $this->assertTrue(!empty($filter->module), format_string('filter_list_format: Proper module name for %filter in text format %format.', $t_args)); // Remove the filter from the copy of saved $format to check whether all // filters have been processed later. unset($format_filters[$name]); } // Verify that all filters have been processed. - $this->assertTrue(empty($format_filters), t('filter_list_format: Loaded filters contain values for all filters in the saved format.')); + $this->assertTrue(empty($format_filters), 'filter_list_format: Loaded filters contain values for all filters in the saved format.'); } } +/** + * Tests the administrative functionality of the Filter module. + */ class FilterAdminTestCase extends DrupalWebTestCase { public static function getInfo() { return array( @@ -185,6 +197,9 @@ $this->drupalLogin($this->admin_user); } + /** + * Tests the format administration functionality. + */ function testFormatAdmin() { // Add text format. $this->drupalGet('admin/config/content/formats'); @@ -199,14 +214,14 @@ // Verify default weight of the text format. $this->drupalGet('admin/config/content/formats'); - $this->assertFieldByName("formats[$format_id][weight]", 0, t('Text format weight was saved.')); + $this->assertFieldByName("formats[$format_id][weight]", 0, 'Text format weight was saved.'); // Change the weight of the text format. $edit = array( "formats[$format_id][weight]" => 5, ); $this->drupalPost('admin/config/content/formats', $edit, t('Save changes')); - $this->assertFieldByName("formats[$format_id][weight]", 5, t('Text format weight was saved.')); + $this->assertFieldByName("formats[$format_id][weight]", 5, 'Text format weight was saved.'); // Edit text format. $this->drupalGet('admin/config/content/formats'); @@ -216,7 +231,7 @@ // Verify that the custom weight of the text format has been retained. $this->drupalGet('admin/config/content/formats'); - $this->assertFieldByName("formats[$format_id][weight]", 5, t('Text format weight was retained.')); + $this->assertFieldByName("formats[$format_id][weight]", 5, 'Text format weight was retained.'); // Disable text format. $this->assertLinkByHref('admin/config/content/formats/' . $format_id . '/disable'); @@ -225,7 +240,7 @@ // Verify that disabled text format no longer exists. $this->drupalGet('admin/config/content/formats/' . $format_id); - $this->assertResponse(404, t('Disabled text format no longer exists.')); + $this->assertResponse(404, 'Disabled text format no longer exists.'); // Attempt to create a format of the same machine name as the disabled // format but with a different human readable name. @@ -249,7 +264,7 @@ } /** - * Test filter administration functionality. + * Tests filter administration functionality. */ function testFilterAdmin() { // URL filter. @@ -262,44 +277,44 @@ $plain = 'plain_text'; // Check that the fallback format exists and cannot be disabled. - $this->assertTrue($plain == filter_fallback_format(), t('The fallback format is set to plain text.')); + $this->assertTrue($plain == filter_fallback_format(), 'The fallback format is set to plain text.'); $this->drupalGet('admin/config/content/formats'); - $this->assertNoRaw('admin/config/content/formats/' . $plain . '/disable', t('Disable link for the fallback format not found.')); + $this->assertNoRaw('admin/config/content/formats/' . $plain . '/disable', 'Disable link for the fallback format not found.'); $this->drupalGet('admin/config/content/formats/' . $plain . '/disable'); - $this->assertResponse(403, t('The fallback format cannot be disabled.')); + $this->assertResponse(403, 'The fallback format cannot be disabled.'); // Verify access permissions to Full HTML format. - $this->assertTrue(filter_access(filter_format_load($full), $this->admin_user), t('Admin user may use Full HTML.')); - $this->assertFalse(filter_access(filter_format_load($full), $this->web_user), t('Web user may not use Full HTML.')); + $this->assertTrue(filter_access(filter_format_load($full), $this->admin_user), 'Admin user may use Full HTML.'); + $this->assertFalse(filter_access(filter_format_load($full), $this->web_user), 'Web user may not use Full HTML.'); // Add an additional tag. $edit = array(); $edit['filters[filter_html][settings][allowed_html]'] = '