diff --git a/CHANGELOG.txt b/CHANGELOG.txt index ef848fb1..0bd51e04 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,300 @@ +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 @@ -69,11 +363,11 @@ Drupal 7.33, 2014-11-07 - 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 @@ -88,7 +382,7 @@ Drupal 7.30, 2014-07-24 - 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 @@ -134,11 +428,11 @@ Drupal 7.28, 2014-05-08 - 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 @@ -204,7 +498,7 @@ Drupal 7.25, 2014-01-02 - 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 @@ -458,8 +752,8 @@ Drupal 7.15, 2012-08-01 - Numerous API documentation improvements. - Additional automated test coverage. -Drupal 7.14 2012-05-02 ----------------------- +Drupal 7.14, 2012-05-02 +----------------------- - Fixed "integrity constraint" fatal errors when rebuilding registry. - Fixed custom logo and favicon functionality referencing incorrect paths. - Fixed DB Case Sensitivity: Allow BINARY attribute in MySQL. @@ -507,12 +801,12 @@ Drupal 7.14 2012-05-02 - system_update_7061() converts filepaths too aggressively. - Trigger upgrade path: Node triggers removed when upgrading to 7-x from 6.25. -Drupal 7.13 2012-05-02 ----------------------- +Drupal 7.13, 2012-05-02 +----------------------- - Fixed security issues (Multiple vulnerabilities), see SA-CORE-2012-002. Drupal 7.12, 2012-02-01 ----------------------- +----------------------- - Fixed bug preventing custom menus from receiving an active trail. - Fixed hook_field_delete() no longer invoked during field_purge_data(). - Fixed bug causing entity info cache to not be cleared with the rest of caches. @@ -546,11 +840,11 @@ Drupal 7.12, 2012-02-01 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 @@ -903,7 +1197,7 @@ Drupal 7.0, 2011-01-05 requests. Drupal 6.23-dev, xxxx-xx-xx (development release) ------------------------ +--------------------------- Drupal 6.22, 2011-05-25 ----------------------- @@ -913,25 +1207,25 @@ Drupal 6.22, 2011-05-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 @@ -940,7 +1234,7 @@ Drupal 6.17, 2010-06-02 - 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. @@ -952,12 +1246,12 @@ Drupal 6.16, 2010-03-03 - 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 @@ -966,18 +1260,18 @@ Drupal 6.14, 2009-09-16 - 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 @@ -985,7 +1279,7 @@ Drupal 6.11, 2009-04-29 - 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. @@ -1381,7 +1675,7 @@ Drupal 4.7.9, 2007-12-05 - 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 @@ -1494,7 +1788,7 @@ Drupal 4.6.11, 2007-01-05 - Fixed security issue (DoS), see SA-2007-002 Drupal 4.6.10, 2006-10-18 ------------------------- +------------------------- - Fixed security issue (XSS), see SA-2006-024 - Fixed security issue (CSRF), see SA-2006-025 - Fixed security issue (Form action attribute injection), see SA-2006-026 diff --git a/INSTALL.txt b/INSTALL.txt index 6f02c05a..e00c8bad 100644 --- a/INSTALL.txt +++ b/INSTALL.txt @@ -23,7 +23,7 @@ Drupal requires: - Percona Server 5.1.70 (or greater) (http://www.percona.com/). Percona Server is a backwards-compatible replacement for MySQL. - PostgreSQL 8.3 (or greater) (http://www.postgresql.org/). - - SQLite 3.4.2 (or greater) (http://www.sqlite.org/). + - SQLite 3.3.7 (or greater) (http://www.sqlite.org/). For more detailed information about Drupal requirements, including a list of PHP extensions and configurations that are required, see "System requirements" diff --git a/MAINTAINERS.txt b/MAINTAINERS.txt index 36563145..5603a432 100644 --- a/MAINTAINERS.txt +++ b/MAINTAINERS.txt @@ -1,7 +1,8 @@ Drupal core is built and maintained by the Drupal project community. Everyone is encouraged to submit issues and changes (patches) to improve Drupal, and to -contribute in other ways -- see http://drupal.org/contribute to find out how. +contribute in other ways -- see https://www.drupal.org/contribute to find out +how. Branch maintainers ------------------ @@ -9,160 +10,154 @@ Branch maintainers The Drupal Core branch maintainers oversee the development of Drupal as a whole. The branch maintainers for Drupal 7 are: -- Dries Buytaert 'dries' http://drupal.org/user/1 -- Angela Byron 'webchick' http://drupal.org/user/24967 -- David Rothstein 'David_Rothstein' http://drupal.org/user/124982 +- Dries Buytaert 'dries' https://www.drupal.org/u/dries +- Angela Byron 'webchick' https://www.drupal.org/u/webchick +- Fabian Franz 'Fabianx' https://www.drupal.org/u/fabianx +- David Rothstein 'David_Rothstein' https://www.drupal.org/u/david_rothstein +- Stefan Ruijsenaars 'stefan.r' https://www.drupal.org/u/stefanr-0 Component maintainers --------------------- The Drupal Core component maintainers oversee the development of Drupal -subsystems. See http://drupal.org/contribute/core-maintainers for more +subsystems. See https://www.drupal.org/contribute/core-maintainers for more information on their responsibilities, and to find out how to become a component maintainer. Current component maintainers for Drupal 7: Ajax system -- Alex Bronstein 'effulgentsia' http://drupal.org/user/78040 -- Earl Miles 'merlinofchaos' http://drupal.org/user/26979 +- Alex Bronstein 'effulgentsia' https://www.drupal.org/u/effulgentsia +- Earl Miles 'merlinofchaos' https://www.drupal.org/u/merlinofchaos Base system -- Károly Négyesi 'chx' http://drupal.org/user/9446 -- Damien Tournoud 'DamZ' http://drupal.org/user/22211 -- Moshe Weitzman 'moshe weitzman' http://drupal.org/user/23 +- Damien Tournoud 'DamZ' https://www.drupal.org/u/damien-tournoud +- Moshe Weitzman 'moshe weitzman' https://www.drupal.org/u/moshe-weitzman Batch system -- Yves Chedemois 'yched' http://drupal.org/user/39567 +- Yves Chedemois 'yched' https://www.drupal.org/u/yched Cache system -- Damien Tournoud 'DamZ' http://drupal.org/user/22211 -- Nathaniel Catchpole 'catch' http://drupal.org/user/35733 +- Damien Tournoud 'DamZ' https://www.drupal.org/u/damien-tournoud +- Nathaniel Catchpole 'catch' https://www.drupal.org/u/catch Cron system -- Károly Négyesi 'chx' http://drupal.org/user/9446 -- Derek Wright 'dww' http://drupal.org/user/46549 +- Derek Wright 'dww' https://www.drupal.org/u/dww Database system -- Larry Garfield 'Crell' http://drupal.org/user/26398 +- Larry Garfield 'Crell' https://www.drupal.org/u/crell - MySQL driver - - Larry Garfield 'Crell' http://drupal.org/user/26398 - - David Strauss 'David Strauss' http://drupal.org/user/93254 + - Larry Garfield 'Crell' https://www.drupal.org/u/crell + - David Strauss 'David Strauss' https://www.drupal.org/u/david-strauss - PostgreSQL driver - - Damien Tournoud 'DamZ' http://drupal.org/user/22211 - - Josh Waihi 'fiasco' http://drupal.org/user/188162 + - Damien Tournoud 'DamZ' https://www.drupal.org/u/damien-tournoud + - Josh Waihi 'fiasco' https://www.drupal.org/u/josh-waihi - Sqlite driver - - Damien Tournoud 'DamZ' http://drupal.org/user/22211 - - Károly Négyesi 'chx' http://drupal.org/user/9446 + - Damien Tournoud 'DamZ' https://www.drupal.org/u/damien-tournoud Database update system -- Károly Négyesi 'chx' http://drupal.org/user/9446 -- Ashok Modi 'BTMash' http://drupal.org/user/60422 +- Ashok Modi 'BTMash' https://www.drupal.org/u/btmash Entity system -- Wolfgang Ziegler 'fago' http://drupal.org/user/16747 -- Nathaniel Catchpole 'catch' http://drupal.org/user/35733 -- Franz Heinzmann 'Frando' http://drupal.org/user/21850 +- Wolfgang Ziegler 'fago' https://www.drupal.org/u/fago +- Nathaniel Catchpole 'catch' https://www.drupal.org/u/catch +- Franz Heinzmann 'Frando' https://www.drupal.org/u/frando File system -- Andrew Morton 'drewish' http://drupal.org/user/34869 -- Aaron Winborn 'aaron' http://drupal.org/user/33420 +- Andrew Morton 'drewish' https://www.drupal.org/u/drewish +- Aaron Winborn 'aaron' https://www.drupal.org/u/aaron Form system -- Károly Négyesi 'chx' http://drupal.org/user/9446 -- Alex Bronstein 'effulgentsia' http://drupal.org/user/78040 -- Wolfgang Ziegler 'fago' http://drupal.org/user/16747 -- Daniel F. Kudwien 'sun' http://drupal.org/user/54136 -- Franz Heinzmann 'Frando' http://drupal.org/user/21850 +- Alex Bronstein 'effulgentsia' https://www.drupal.org/u/effulgentsia +- Wolfgang Ziegler 'fago' https://www.drupal.org/u/fago +- Daniel F. Kudwien 'sun' https://www.drupal.org/u/sun +- Franz Heinzmann 'Frando' https://www.drupal.org/u/frando Image system -- Andrew Morton 'drewish' http://drupal.org/user/34869 -- Nathan Haug 'quicksketch' http://drupal.org/user/35821 +- Andrew Morton 'drewish' https://www.drupal.org/u/drewish +- Nathan Haug 'quicksketch' https://www.drupal.org/u/quicksketch Install system -- David Rothstein 'David_Rothstein' http://drupal.org/user/124982 +- David Rothstein 'David_Rothstein' https://www.drupal.org/u/david_rothstein JavaScript -- Théodore Biadala 'nod_' http://drupal.org/user/598310 -- Steve De Jonghe 'seutje' http://drupal.org/user/264148 -- Jesse Renée Beach 'jessebeach' http://drupal.org/user/748566 +- Théodore Biadala 'nod_' https://www.drupal.org/u/nod_ +- Steve De Jonghe 'seutje' https://www.drupal.org/u/seutje Language system -- Francesco Placella 'plach' http://drupal.org/user/183211 -- Daniel F. Kudwien 'sun' http://drupal.org/user/54136 +- Francesco Placella 'plach' https://www.drupal.org/u/plach +- Daniel F. Kudwien 'sun' https://www.drupal.org/u/sun Lock system -- Damien Tournoud 'DamZ' http://drupal.org/user/22211 +- Damien Tournoud 'DamZ' https://www.drupal.org/u/damien-tournoud Mail system - ? Markup -- Jacine Luisi 'Jacine' http://drupal.org/user/88931 -- Daniel F. Kudwien 'sun' http://drupal.org/user/54136 +- Jacine Luisi 'Jacine' https://www.drupal.org/u/jacine +- Daniel F. Kudwien 'sun' https://www.drupal.org/u/sun Menu system -- Peter Wolanin 'pwolanin' http://drupal.org/user/49851 -- Károly Négyesi 'chx' http://drupal.org/user/9446 +- Peter Wolanin 'pwolanin' https://www.drupal.org/u/pwolanin Path system -- Dave Reid 'davereid' http://drupal.org/user/53892 -- Nathaniel Catchpole 'catch' http://drupal.org/user/35733 +- Dave Reid 'davereid' https://www.drupal.org/u/dave-reid +- Nathaniel Catchpole 'catch' https://www.drupal.org/u/catch Render system -- Moshe Weitzman 'moshe weitzman' http://drupal.org/user/23 -- Alex Bronstein 'effulgentsia' http://drupal.org/user/78040 -- Franz Heinzmann 'Frando' http://drupal.org/user/21850 +- Moshe Weitzman 'moshe weitzman' https://www.drupal.org/u/moshe-weitzman +- Alex Bronstein 'effulgentsia' https://www.drupal.org/u/effulgentsia +- Franz Heinzmann 'Frando' https://www.drupal.org/u/frando Theme system -- Earl Miles 'merlinofchaos' http://drupal.org/user/26979 -- Alex Bronstein 'effulgentsia' http://drupal.org/user/78040 -- Joon Park 'dvessel' http://drupal.org/user/56782 -- John Albin Wilkins 'JohnAlbin' http://drupal.org/user/32095 +- Earl Miles 'merlinofchaos' https://www.drupal.org/u/merlinofchaos +- Alex Bronstein 'effulgentsia' https://www.drupal.org/u/effulgentsia +- Joon Park 'dvessel' https://www.drupal.org/u/dvessel +- John Albin Wilkins 'JohnAlbin' https://www.drupal.org/u/johnalbin Token system -- Dave Reid 'davereid' http://drupal.org/user/53892 +- Dave Reid 'davereid' https://www.drupal.org/u/dave-reid XML-RPC system -- Frederic G. Marand 'fgm' http://drupal.org/user/27985 +- Frederic G. Marand 'fgm' https://www.drupal.org/u/fgm Topic coordinators ------------------ Accessibility -- Everett Zufelt 'Everett Zufelt' http://drupal.org/user/406552 -- Brandon Bowersox-Johnson 'bowersox' http://drupal.org/user/186415 +- Everett Zufelt 'Everett Zufelt' https://www.drupal.org/u/everett-zufelt +- Brandon Bowersox-Johnson 'bowersox' https://www.drupal.org/u/bowersox Documentation -- Jennifer Hodgdon 'jhodgdon' http://drupal.org/user/155601 +- Jennifer Hodgdon 'jhodgdon' https://www.drupal.org/u/jhodgdon Translations -- Gerhard Killesreiter 'killes' http://drupal.org/user/83 +- Gerhard Killesreiter 'killes' https://www.drupal.org/u/gerhard-killesreiter User experience and usability -- Roy Scholten 'yoroy' http://drupal.org/user/41502 -- Bojhan Somers 'Bojhan' http://drupal.org/user/87969 +- Roy Scholten 'yoroy' https://www.drupal.org/u/yoroy +- Bojhan Somers 'Bojhan' https://www.drupal.org/u/bojhan Node Access -- Moshe Weitzman 'moshe weitzman' http://drupal.org/user/23 -- Ken Rickard 'agentrickard' http://drupal.org/user/20975 -- Jess Myrbo 'xjm' http://drupal.org/user/65776 +- Moshe Weitzman 'moshe weitzman' https://www.drupal.org/u/moshe-weitzman +- Ken Rickard 'agentrickard' https://www.drupal.org/u/agentrickard Security team ----------------- -To report a security issue, see: https://drupal.org/security-team/report-issue +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 http://drupal.org/security-team for more information. The -security team lead is: +documentation. See https://www.drupal.org/security-team for more information. +The security team lead is: -- Michael Hess 'mlhess' https://drupal.org/user/102818 +- Michael Hess 'mlhess' https://www.drupal.org/u/mlhess Module maintainers @@ -172,143 +167,141 @@ Aggregator module - ? Block module -- John Albin Wilkins 'JohnAlbin' http://drupal.org/user/32095 +- John Albin Wilkins 'JohnAlbin' https://www.drupal.org/u/johnalbin Blog module - ? Book module -- Peter Wolanin 'pwolanin' http://drupal.org/user/49851 +- Peter Wolanin 'pwolanin' https://www.drupal.org/u/pwolanin Color module - ? Comment module -- Nathaniel Catchpole 'catch' http://drupal.org/user/35733 +- Nathaniel Catchpole 'catch' https://www.drupal.org/u/catch Contact module -- Dave Reid 'davereid' http://drupal.org/user/53892 +- Dave Reid 'davereid' https://www.drupal.org/u/dave-reid Contextual module -- Daniel F. Kudwien 'sun' http://drupal.org/user/54136 +- Daniel F. Kudwien 'sun' https://www.drupal.org/u/sun Dashboard module - ? Database logging module -- Khalid Baheyeldin 'kbahey' http://drupal.org/user/4063 +- Khalid Baheyeldin 'kbahey' https://www.drupal.org/u/kbahey Field module -- Yves Chedemois 'yched' http://drupal.org/user/39567 -- Barry Jaspan 'bjaspan' http://drupal.org/user/46413 +- Yves Chedemois 'yched' https://www.drupal.org/u/yched +- Barry Jaspan 'bjaspan' https://www.drupal.org/u/bjaspan Field UI module -- Yves Chedemois 'yched' http://drupal.org/user/39567 +- Yves Chedemois 'yched' https://www.drupal.org/u/yched File module -- Aaron Winborn 'aaron' http://drupal.org/user/33420 +- Aaron Winborn 'aaron' https://www.drupal.org/u/aaron Filter module -- Daniel F. Kudwien 'sun' http://drupal.org/user/54136 +- Daniel F. Kudwien 'sun' https://www.drupal.org/u/sun Forum module -- Lee Rowlands 'larowlan' http://drupal.org/user/395439 +- Lee Rowlands 'larowlan' https://www.drupal.org/u/larowlan Help module - ? Image module -- Nathan Haug 'quicksketch' http://drupal.org/user/35821 +- Nathan Haug 'quicksketch' https://www.drupal.org/u/quicksketch Locale module -- Gábor Hojtsy 'Gábor Hojtsy' http://drupal.org/user/4166 +- Gábor Hojtsy 'Gábor Hojtsy' https://www.drupal.org/u/gábor-hojtsy Menu module - ? Node module -- Moshe Weitzman 'moshe weitzman' http://drupal.org/user/23 -- David Strauss 'David Strauss' http://drupal.org/user/93254 +- Moshe Weitzman 'moshe weitzman' https://www.drupal.org/u/moshe-weitzman +- David Strauss 'David Strauss' https://www.drupal.org/u/david-strauss OpenID module -- Vojtech Kusy 'wojtha' http://drupal.org/user/56154 -- Christian Schmidt 'c960657' http://drupal.org/user/216078 -- Damien Tournoud 'DamZ' http://drupal.org/user/22211 +- Vojtech Kusy 'wojtha' https://www.drupal.org/u/wojtha +- Christian Schmidt 'c960657' https://www.drupal.org/u/c960657 +- Damien Tournoud 'DamZ' https://www.drupal.org/u/damien-tournoud Overlay module -- Katherine Senzee 'ksenzee' http://drupal.org/user/139855 +- Katherine Senzee 'ksenzee' https://www.drupal.org/u/ksenzee Path module -- Dave Reid 'davereid' http://drupal.org/user/53892 +- Dave Reid 'davereid' https://www.drupal.org/u/dave-reid PHP module - ? Poll module -- Andrei Mateescu 'amateescu' http://drupal.org/user/729614 +- Andrei Mateescu 'amateescu' https://www.drupal.org/u/amateescu Profile module - ? RDF module -- Stéphane Corlosquet 'scor' http://drupal.org/user/52142 +- Stéphane Corlosquet 'scor' https://www.drupal.org/u/scor Search module -- Doug Green 'douggreen' http://drupal.org/user/29191 +- Doug Green 'douggreen' https://www.drupal.org/u/douggreen Shortcut module -- David Rothstein 'David_Rothstein' http://drupal.org/user/124982 +- David Rothstein 'David_Rothstein' https://www.drupal.org/u/david_rothstein Simpletest module -- Jimmy Berry 'boombatower' http://drupal.org/user/214218 -- Károly Négyesi 'chx' http://drupal.org/user/9446 +- Jimmy Berry 'boombatower' https://www.drupal.org/u/boombatower Statistics module -- Tim Millwood 'timmillwood' http://drupal.org/user/227849 +- Tim Millwood 'timmillwood' https://www.drupal.org/u/timmillwood Syslog module -- Khalid Baheyeldin 'kbahey' http://drupal.org/user/4063 +- Khalid Baheyeldin 'kbahey' https://www.drupal.org/u/kbahey System module - ? Taxonomy module -- Jess Myrbo 'xjm' http://drupal.org/user/65776 -- Nathaniel Catchpole 'catch' http://drupal.org/user/35733 -- Benjamin Doherty 'bangpound' http://drupal.org/user/100456 +- Nathaniel Catchpole 'catch' https://www.drupal.org/u/catch +- Benjamin Doherty 'bangpound' https://www.drupal.org/u/bangpound Toolbar module - ? Tracker module -- David Strauss 'David Strauss' http://drupal.org/user/93254 +- David Strauss 'David Strauss' https://www.drupal.org/u/david-strauss Translation module -- Francesco Placella 'plach' http://drupal.org/user/183211 +- Francesco Placella 'plach' https://www.drupal.org/u/plach Trigger module - ? Update module -- Derek Wright 'dww' http://drupal.org/user/46549 +- Derek Wright 'dww' https://www.drupal.org/u/dww User module -- Moshe Weitzman 'moshe weitzman' http://drupal.org/user/23 -- David Strauss 'David Strauss' http://drupal.org/user/93254 +- Moshe Weitzman 'moshe weitzman' https://www.drupal.org/u/moshe-weitzman +- David Strauss 'David Strauss' https://www.drupal.org/u/david-strauss Theme maintainers ----------------- Bartik theme -- Jen Simmons 'jensimmons' http://drupal.org/user/140882 -- Jeff Burns 'Jeff Burnz' http://drupal.org/user/61393 +- Jen Simmons 'jensimmons' https://www.drupal.org/u/jensimmons +- Jeff Burns 'Jeff Burnz' https://www.drupal.org/u/jeff-burnz Garland theme -- John Albin Wilkins 'JohnAlbin' http://drupal.org/user/32095 +- John Albin Wilkins 'JohnAlbin' https://www.drupal.org/u/johnalbin Seven theme -- Jeff Burns 'Jeff Burnz' http://drupal.org/user/61393 +- Jeff Burns 'Jeff Burnz' https://www.drupal.org/u/jeff-burnz Stark theme -- John Albin Wilkins 'JohnAlbin' http://drupal.org/user/32095 +- John Albin Wilkins 'JohnAlbin' https://www.drupal.org/u/johnalbin diff --git a/UPGRADE.txt b/UPGRADE.txt index e870ff0f..ae733ca1 100644 --- a/UPGRADE.txt +++ b/UPGRADE.txt @@ -64,6 +64,9 @@ following the instructions in the INTRODUCTION section at the top of this file: Sometimes an update includes changes to default.settings.php (this will be noted in the release notes). If that's the case, follow these steps: + - Locate your settings.php file in the /sites/* directory. (Typically + sites/default.) + - Make a backup copy of your settings.php file, with a different file name. - Make a copy of the new default.settings.php file, and name the copy @@ -74,6 +77,13 @@ following the instructions in the INTRODUCTION section at the top of this file: database information, and you will also want to copy in any other customizations you have added. + You can find the release notes for your version at + https://www.drupal.org/project/drupal. At bottom of the project page under + "Downloads" use the link for your version of Drupal to view the release + notes. If your version is not listed, use the 'View all releases' link. From + this page you can scroll down or use the filter to find your version and its + release notes. + 4. Download the latest Drupal 7.x release from http://drupal.org to a directory outside of your web root. Extract the archive and copy the files into your Drupal directory. diff --git a/includes/ajax.inc b/includes/ajax.inc index 10877a24..f059209b 100644 --- a/includes/ajax.inc +++ b/includes/ajax.inc @@ -211,7 +211,7 @@ * * When returning an Ajax command array, it is often useful to have * status messages rendered along with other tasks in the command array. - * In that case the the Ajax commands array may be constructed like this: + * In that case the Ajax commands array may be constructed like this: * @code * $commands = array(); * $commands[] = ajax_command_replace(NULL, $output); @@ -230,6 +230,10 @@ * functions. */ function ajax_render($commands = array()) { + // Although ajax_deliver() does this, some contributed and custom modules + // render Ajax responses without using that delivery callback. + ajax_set_verification_header(); + // Ajax responses aren't rendered with html.tpl.php, so we have to call // drupal_get_css() and drupal_get_js() here, in order to have new files added // during this request to be loaded by the page. We only want to send back @@ -390,7 +394,7 @@ function ajax_form_callback() { if (!empty($form_state['triggering_element'])) { $callback = $form_state['triggering_element']['#ajax']['callback']; } - if (!empty($callback) && function_exists($callback)) { + if (!empty($callback) && is_callable($callback)) { $result = $callback($form, $form_state); if (!(is_array($result) && isset($result['#type']) && $result['#type'] == 'ajax')) { @@ -487,6 +491,9 @@ function ajax_deliver($page_callback_result) { } } + // 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); @@ -576,6 +583,29 @@ function ajax_prepare_response($page_callback_result) { return $commands; } +/** + * Sets a response header for ajax.js to trust the response body. + * + * It is not safe to invoke Ajax commands within user-uploaded files, so this + * header protects against those being invoked. + * + * @see Drupal.ajax.options.success() + */ +function ajax_set_verification_header() { + $added = &drupal_static(__FUNCTION__); + + // User-uploaded files cannot set any response headers, so a custom header is + // used to indicate to ajax.js that this response is safe. Note that most + // Ajax requests bound using the Form API will be protected by having the URL + // flagged as trusted in Drupal.settings, so this header is used only for + // things like custom markup that gets Ajax behaviors attached. + if (empty($added)) { + drupal_add_http_header('X-Drupal-Ajax-Token', '1'); + // Avoid sending the header twice. + $added = TRUE; + } +} + /** * Performs end-of-Ajax-request tasks. * @@ -764,7 +794,12 @@ function ajax_pre_render_element($element) { $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. diff --git a/includes/batch.inc b/includes/batch.inc index 061acd4c..e89ab8de 100644 --- a/includes/batch.inc +++ b/includes/batch.inc @@ -460,10 +460,10 @@ function _batch_finished() { if (isset($batch_set['file']) && is_file($batch_set['file'])) { include_once DRUPAL_ROOT . '/' . $batch_set['file']; } - if (function_exists($batch_set['finished'])) { + if (is_callable($batch_set['finished'])) { $queue = _batch_queue($batch_set); $operations = $queue->getAllItems(); - $batch_set['finished']($batch_set['success'], $batch_set['results'], $operations, format_interval($batch_set['elapsed'] / 1000)); + call_user_func($batch_set['finished'], $batch_set['success'], $batch_set['results'], $operations, format_interval($batch_set['elapsed'] / 1000)); } } } diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc index 744fc8fe..ed6d864c 100644 --- a/includes/bootstrap.inc +++ b/includes/bootstrap.inc @@ -8,7 +8,7 @@ /** * The current system version. */ -define('VERSION', '7.34'); +define('VERSION', '7.53'); /** * Core API compatibility. @@ -529,9 +529,8 @@ function timer_stop($name) { * Returns the appropriate configuration directory. * * Returns the configuration path based on the site's hostname, port, and - * pathname. Uses find_conf_path() to find the current configuration directory. - * See default.settings.php for examples on how the URL is converted to a - * directory. + * pathname. See default.settings.php for examples on how the URL is converted + * to a directory. * * @param bool $require_settings * Only configuration directories with an existing settings.php file @@ -829,14 +828,21 @@ function drupal_settings_initialize() { * @param $filename * The filename of the item if it is to be set explicitly rather * than by consulting the database. + * @param bool $trigger_error + * Whether to trigger an error when a file is missing or has unexpectedly + * moved. This defaults to TRUE, but can be set to FALSE by calling code that + * merely wants to check whether an item exists in the filesystem. * * @return * The filename of the requested item or NULL if the item is not found. */ -function drupal_get_filename($type, $name, $filename = NULL) { +function drupal_get_filename($type, $name, $filename = NULL, $trigger_error = TRUE) { + // The $files static variable will hold the locations of all requested files. + // We can be sure that any file listed in this static variable actually + // exists as all additions have gone through a file_exists() check. // The location of files will not change during the request, so do not use // drupal_static(). - static $files = array(), $dirs = array(); + static $files = array(); // Profiles are a special case: they have a fixed location and naming. if ($type == 'profile') { @@ -848,59 +854,41 @@ function drupal_get_filename($type, $name, $filename = NULL) { } 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 !== 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 (!isset($dirs[$dir][$extension])) { - $dirs[$dir][$extension] = TRUE; - if (!function_exists('drupal_system_listing')) { - require_once DRUPAL_ROOT . '/includes/common.inc'; - } - // Scan the appropriate directories for all files with the requested - // extension, not just the file we are currently looking for. This - // prevents unnecessary scans from being repeated when this function is - // called more than once in the same page request. - $matches = drupal_system_listing("/^" . DRUPAL_PHP_FUNCTION_PATTERN . "\.$extension$/", $dir, 'name', 0); - foreach ($matches as $matched_name => $file) { - $files[$type][$matched_name] = $file->uri; - } - } + $files[$type][$name] = _drupal_get_filename_fallback($type, $name, $trigger_error, $database_unavailable); } } @@ -909,6 +897,256 @@ function drupal_get_filename($type, $name, $filename = NULL) { } } +/** + * 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. * @@ -1056,7 +1294,7 @@ function drupal_page_get_cache($check_only = FALSE) { * 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. @@ -1246,23 +1484,10 @@ function drupal_send_headers($default_headers = array(), $only_default = FALSE) * fresh page on every request. This prevents authenticated users from seeing * locally cached pages. * - * Also give each page a unique ETag. This will force clients to include both - * an If-Modified-Since header and an If-None-Match header when doing - * conditional requests for the page (required by RFC 2616, section 13.3.4), - * making the validation more robust. This is a workaround for a bug in Mozilla - * Firefox that is triggered when Drupal's caching is enabled and the user - * accesses Drupal via an HTTP proxy (see - * https://bugzilla.mozilla.org/show_bug.cgi?id=269303): When an authenticated - * user requests a page, and then logs out and requests the same page again, - * Firefox may send a conditional request based on the page that was cached - * locally when the user was logged in. If this page did not have an ETag - * header, the request only contains an If-Modified-Since header. The date will - * be recent, because with authenticated users the Last-Modified header always - * refers to the time of the request. If the user accesses Drupal via a proxy - * server, and the proxy already has a cached copy of the anonymous page with an - * older Last-Modified date, the proxy may respond with 304 Not Modified, making - * the client think that the anonymous and authenticated pageviews are - * identical. + * ETag and Last-Modified headers are not set per default for authenticated + * users so that browsers do not send If-Modified-Since headers from + * authenticated user pages. drupal_serve_page_from_cache() will set appropriate + * ETag and Last-Modified headers for cached pages. * * @see drupal_page_set_cache() */ @@ -1275,9 +1500,11 @@ function drupal_page_header() { $default_headers = array( 'Expires' => 'Sun, 19 Nov 1978 05:00:00 GMT', - 'Last-Modified' => gmdate(DATE_RFC7231, 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); } @@ -1451,6 +1678,23 @@ function drupal_unpack($obj, $field = 'data') { * 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 @@ -1461,8 +1705,9 @@ function drupal_unpack($obj, $field = 'data') { * 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. @@ -1659,14 +1904,14 @@ function request_uri() { * information about the passed-in exception is used. * @param $variables * Array of variables to replace in the message on display. Defaults to the - * return value of drupal_decode_exception(). + * return value of _drupal_decode_exception(). * @param $severity * The severity of the message, as per RFC 3164. * @param $link * A link to associate with the message. * * @see watchdog() - * @see drupal_decode_exception() + * @see _drupal_decode_exception() */ function watchdog_exception($type, Exception $exception, $message = NULL, $variables = array(), $severity = WATCHDOG_ERROR, $link = NULL) { @@ -1792,7 +2037,7 @@ function watchdog($type, $message, $variables = array(), $severity = WATCHDOG_NO * @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(); } @@ -2480,6 +2725,9 @@ function _drupal_bootstrap_database() { // 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'); + } } /** @@ -2497,6 +2745,26 @@ function _drupal_bootstrap_variables() { // Load bootstrap modules. require_once DRUPAL_ROOT . '/includes/module.inc'; module_load_all(TRUE); + + // Sanitize the destination parameter (which is often used for redirects) to + // prevent open redirect attacks leading to other domains. Sanitize both + // $_GET['destination'] and $_REQUEST['destination'] to protect code that + // relies on either, but do not sanitize $_POST to avoid interfering with + // unrelated form submissions. The sanitization happens here because + // url_is_external() requires the variable system to be available. + if (isset($_GET['destination']) || isset($_REQUEST['destination'])) { + require_once DRUPAL_ROOT . '/includes/common.inc'; + // If the destination is an external URL, remove it. + if (isset($_GET['destination']) && url_is_external($_GET['destination'])) { + unset($_GET['destination']); + unset($_REQUEST['destination']); + } + // If there's still something in $_REQUEST['destination'] that didn't come + // from $_GET, check it too. + if (isset($_REQUEST['destination']) && (!isset($_GET['destination']) || $_REQUEST['destination'] != $_GET['destination']) && url_is_external($_REQUEST['destination'])) { + unset($_REQUEST['destination']); + } + } } /** @@ -2633,7 +2901,7 @@ function drupal_installation_attempted() { * * This would include implementations of hook_install(), which could run * during the Drupal installation phase, and might also be run during - * non-installation time, such as while installing the module from the the + * non-installation time, such as while installing the module from the * module administration page. * * Example usage: @@ -2775,10 +3043,14 @@ function language_list($field = 'language') { } /** - * Returns the default language used on the site + * Returns the default language, as an object, or one of its properties. * * @param $property - * Optional property of the language object to return + * (optional) The property of the language object to return. + * + * @return + * Either the language object for the default language used on the site, + * or the property of that object named in the $property parameter. */ function language_default($property = NULL) { $language = variable_get('language_default', (object) array('language' => 'en', 'name' => 'English', 'native' => 'English', 'direction' => 0, 'enabled' => 1, 'plurals' => 0, 'formula' => '', 'domain' => '', 'prefix' => '', 'weight' => 0, 'javascript' => '')); @@ -2930,8 +3202,15 @@ function ip_address() { // 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); + } } } } @@ -2948,7 +3227,9 @@ function ip_address() { * 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. @@ -3103,6 +3384,22 @@ function drupal_autoload_class($class) { return _registry_check_code('class', $class); } +/** + * 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. * @@ -3121,7 +3418,7 @@ function drupal_autoload_class($class) { 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; } @@ -3154,7 +3451,7 @@ function _registry_check_code($type, $name = NULL) { $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]; } @@ -3162,10 +3459,13 @@ function _registry_check_code($type, $name = NULL) { // This function may get called when the default database is not active, but // there is no reason we'd ever want to not use the default database for // this query. - $file = Database::getConnection('default', 'default')->query("SELECT filename FROM {registry} WHERE name = :name AND type = :type", array( - ':name' => $name, - ':type' => $type, - )) + $file = Database::getConnection('default', 'default') + ->select('registry', 'r', array('target' => 'default')) + ->fields('r', array('filename')) + // Use LIKE here to make the query case-insensitive. + ->condition('r.name', db_like($name), 'LIKE') + ->condition('r.type', $type) + ->execute() ->fetchField(); // Flag that we've run a lookup query and need to update the cache. @@ -3176,7 +3476,7 @@ function _registry_check_code($type, $name = NULL) { $lookup_cache[$cache_key] = $file; if ($file) { - require_once DRUPAL_ROOT . '/' . $file; + include_once DRUPAL_ROOT . '/' . $file; return TRUE; } else { @@ -3503,3 +3803,34 @@ function drupal_check_memory_limit($required, $memory_limit = NULL) { // - The memory limit is greater than the memory required for the operation. return ((!$memory_limit) || ($memory_limit == -1) || (parse_size($memory_limit) >= parse_size($required))); } + +/** + * Invalidates a PHP file from any active opcode caches. + * + * If the opcode cache does not support the invalidation of individual files, + * the entire cache will be flushed. + * + * @param string $filepath + * The absolute path of the PHP file to invalidate. + */ +function drupal_clear_opcode_cache($filepath) { + if (!defined('PHP_VERSION_ID') || PHP_VERSION_ID < 50300) { + // Below PHP 5.3, clearstatcache does not accept any function parameters. + clearstatcache(); + } + else { + clearstatcache(TRUE, $filepath); + } + + // Zend OPcache. + if (function_exists('opcache_invalidate')) { + opcache_invalidate($filepath, TRUE); + } + // APC. + if (function_exists('apc_delete_file')) { + // apc_delete_file() throws a PHP warning in case the specified file was + // not compiled yet. + // @see http://php.net/apc-delete-file + @apc_delete_file($filepath); + } +} diff --git a/includes/cache.inc b/includes/cache.inc index 207bf661..4c2bed32 100644 --- a/includes/cache.inc +++ b/includes/cache.inc @@ -14,6 +14,7 @@ * * @param $bin * The cache bin for which the cache object should be returned. + * * @return DrupalCacheInterface * The cache object associated with the specified bin. * diff --git a/includes/common.inc b/includes/common.inc index 20cc82be..339a69b3 100644 --- a/includes/common.inc +++ b/includes/common.inc @@ -688,6 +688,13 @@ function drupal_goto($path = '', array $options = array(), $http_response_code = $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. @@ -753,7 +760,8 @@ function drupal_access_denied() { * - 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 @@ -778,6 +786,8 @@ function drupal_access_denied() { * 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 @@ -1057,6 +1067,12 @@ function drupal_http_request($url, array $options = array()) { 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 @@ -1522,7 +1538,7 @@ function _filter_xss_split($m, $store = FALSE) { return '<'; } - if (!preg_match('%^<\s*(/\s*)?([a-zA-Z0-9]+)([^>]*)>?|()$%', $string, $matches)) { + if (!preg_match('%^<\s*(/\s*)?([a-zA-Z0-9\-]+)([^>]*)>?|()$%', $string, $matches)) { // Seriously malformed. return ''; } @@ -1754,9 +1770,15 @@ function format_rss_item($title, $link, $description, $args = array()) { * - '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 = ''; @@ -1769,7 +1791,7 @@ function format_xml_elements($array) { } 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"; @@ -2215,13 +2237,7 @@ function url($path = NULL, array $options = array()) { ); 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'] = url_is_external($path); } // Preserve the original path before altering or aliasing. @@ -2259,6 +2275,11 @@ function url($path = NULL, array $options = array()) { return $path . $options['fragment']; } + // Strip leading slashes from internal paths to prevent them becoming external + // URLs without protocol. /example.com should not be turned into + // //example.com. + $path = ltrim($path, '/'); + global $base_url, $base_secure_url, $base_insecure_url; // The base_url might be rewritten from the language rewrite in domain mode. @@ -2336,10 +2357,21 @@ function url($path = NULL, array $options = array()) { */ function url_is_external($path) { $colonpos = strpos($path, ':'); - // Avoid calling drupal_strip_dangerous_protocols() if there is any - // slash (/), hash (#) or question_mark (?) before the colon (:) - // occurrence - if any - as this would clearly mean it is not a URL. - return $colonpos !== FALSE && !preg_match('![/?#]!', substr($path, 0, $colonpos)) && drupal_strip_dangerous_protocols($path) == $path; + // 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); } /** @@ -2621,6 +2653,15 @@ function drupal_deliver_html_page($page_callback_result) { 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? @@ -2636,7 +2677,10 @@ function drupal_deliver_html_page($page_callback_result) { // Keep old path for reference, and to allow forms to redirect to it. if (!isset($_GET['destination'])) { - $_GET['destination'] = $_GET['q']; + // Make sure that the current path is not interpreted as external URL. + if (!url_is_external($_GET['q'])) { + $_GET['destination'] = $_GET['q']; + } } $path = drupal_get_normal_path(variable_get('site_404', '')); @@ -2665,7 +2709,10 @@ function drupal_deliver_html_page($page_callback_result) { // Keep old path for reference, and to allow forms to redirect to it. if (!isset($_GET['destination'])) { - $_GET['destination'] = $_GET['q']; + // Make sure that the current path is not interpreted as external URL. + if (!url_is_external($_GET['q'])) { + $_GET['destination'] = $_GET['q']; + } } $path = drupal_get_normal_path(variable_get('site_403', '')); @@ -2729,6 +2776,7 @@ function drupal_page_footer() { _registry_check_code(REGISTRY_WRITE_LOOKUP_CACHE); drupal_cache_system_paths(); module_implements_write_cache(); + drupal_file_scan_write_cache(); system_run_automated_cron(); } @@ -2790,11 +2838,11 @@ function drupal_map_assoc($array, $function = NULL) { * 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 @@ -2811,7 +2859,11 @@ function drupal_map_assoc($array, $function = NULL) { */ 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); + } } } @@ -2992,6 +3044,13 @@ function drupal_add_html_head_link($attributes, $header = FALSE) { */ 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)) { @@ -3027,7 +3086,8 @@ function drupal_add_css($data = NULL, $options = NULL) { } // 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']) { @@ -3780,7 +3840,7 @@ function drupal_load_stylesheet_content($contents, $optimize = FALSE) { // 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; } @@ -3840,6 +3900,21 @@ function drupal_delete_file_if_stale($uri) { * 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); @@ -4140,6 +4215,13 @@ function drupal_region_class($region) { * else being the same, JavaScript added by a call to drupal_add_js() that * happened later in the page request gets added to the page after one for * which drupal_add_js() happened earlier in the page request. + * - requires_jquery: Set this to FALSE if the JavaScript you are adding does + * not have a dependency on jQuery. Defaults to TRUE, except for JavaScript + * settings where it defaults to FALSE. This is used on sites that have the + * 'javascript_always_use_jquery' variable set to FALSE; on those sites, if + * all the JavaScript added to the page by drupal_add_js() does not have a + * dependency on jQuery, then for improved front-end performance Drupal + * will not add jQuery and related libraries and settings to the page. * - defer: If set to TRUE, the defer attribute is set on the 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 --git a/modules/field/field.api.php b/modules/field/field.api.php index e8361c22..f9370912 100644 --- a/modules/field/field.api.php +++ b/modules/field/field.api.php @@ -1897,7 +1897,7 @@ function hook_field_storage_write($entity_type, $entity, $op, $fields) { $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, diff --git a/modules/field/field.crud.inc b/modules/field/field.crud.inc index ba377083..7c0e3a15 100644 --- a/modules/field/field.crud.inc +++ b/modules/field/field.crud.inc @@ -189,7 +189,7 @@ function field_create_field($field) { } // Clear caches - field_cache_clear(TRUE); + field_cache_clear(); // Invoke external hooks after the cache is cleared for API consistency. module_invoke_all('field_create_field', $field); @@ -288,7 +288,7 @@ function field_update_field($field) { drupal_write_record('field_config', $field, $primary_key); // Clear caches - field_cache_clear(TRUE); + field_cache_clear(); // Invoke external hooks after the cache is cleared for API consistency. module_invoke_all('field_update_field', $field, $prior_field, $has_data); @@ -430,7 +430,7 @@ function field_delete_field($field_name) { ->execute(); // Clear the cache. - field_cache_clear(TRUE); + field_cache_clear(); module_invoke_all('field_delete_field', $field); } diff --git a/modules/field/field.info b/modules/field/field.info index b885d609..2157d242 100644 --- a/modules/field/field.info +++ b/modules/field/field.info @@ -11,8 +11,8 @@ dependencies[] = field_sql_storage required = TRUE stylesheets[all][] = theme/field.css -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/field/field.info.class.inc b/modules/field/field.info.class.inc index f4f1f630..772cd451 100644 --- a/modules/field/field.info.class.inc +++ b/modules/field/field.info.class.inc @@ -612,10 +612,12 @@ class FieldInfo { // Fill in default values. $display += array( 'label' => 'above', - 'type' => $field_type_info['default_formatter'], 'settings' => array(), 'weight' => 0, ); + if (empty($display['type'])) { + $display['type'] = $field_type_info['default_formatter']; + } if ($display['type'] != 'hidden') { $formatter_type_info = field_info_formatter_types($display['type']); // Fall back to default formatter if formatter type is not available. diff --git a/modules/field/field.install b/modules/field/field.install index f6948e3b..c5dd2dc7 100644 --- a/modules/field/field.install +++ b/modules/field/field.install @@ -467,6 +467,27 @@ 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 --git a/modules/field/field.module b/modules/field/field.module index a593e511..8d66813f 100644 --- a/modules/field/field.module +++ b/modules/field/field.module @@ -316,6 +316,21 @@ function field_help($path, $arg) { } } +/** + * 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(). */ @@ -894,6 +909,7 @@ function field_view_field($entity_type, $entity, $field_name, $display = array() 'entity' => $entity, 'view_mode' => '_custom', 'display' => $display, + 'language' => $langcode, ); drupal_alter('field_attach_view', $result, $context); diff --git a/modules/field/modules/field_sql_storage/field_sql_storage.info b/modules/field/modules/field_sql_storage/field_sql_storage.info index c69226c1..13bf2640 100644 --- a/modules/field/modules/field_sql_storage/field_sql_storage.info +++ b/modules/field/modules/field_sql_storage/field_sql_storage.info @@ -7,8 +7,8 @@ dependencies[] = field files[] = field_sql_storage.test required = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/field/modules/field_sql_storage/field_sql_storage.module b/modules/field/modules/field_sql_storage/field_sql_storage.module index 7ab4ee5c..842893ad 100644 --- a/modules/field/modules/field_sql_storage/field_sql_storage.module +++ b/modules/field/modules/field_sql_storage/field_sql_storage.module @@ -223,7 +223,17 @@ function _field_sql_storage_schema($field) { 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); + } } } @@ -332,7 +342,17 @@ function field_sql_storage_field_storage_update_field($field, $prior_field, $has $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); @@ -465,7 +485,7 @@ function field_sql_storage_field_storage_write($entity_type, $entity, $op, $fiel $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, diff --git a/modules/field/modules/field_sql_storage/field_sql_storage.test b/modules/field/modules/field_sql_storage/field_sql_storage.test index 072739cb..7c88ac77 100644 --- a/modules/field/modules/field_sql_storage/field_sql_storage.test +++ b/modules/field/modules/field_sql_storage/field_sql_storage.test @@ -355,14 +355,14 @@ class FieldSqlStorageTestCase extends DrupalWebTestCase { field_attach_insert('test_entity', $entity); // Add an index - $field = array('field_name' => $field_name, 'indexes' => array('value' => array('value'))); + $field = array('field_name' => $field_name, 'indexes' => array('value' => array(array('value', 255)))); field_update_field($field); foreach ($tables as $table) { $this->assertTrue(Database::getConnection()->schema()->indexExists($table, "{$field_name}_value"), format_string("Index on value created in %table", array('%table' => $table))); } // Add a different index, removing the existing custom one. - $field = array('field_name' => $field_name, 'indexes' => array('value_format' => array('value', 'format'))); + $field = array('field_name' => $field_name, 'indexes' => array('value_format' => array(array('value', 127), array('format', 127)))); field_update_field($field); foreach ($tables as $table) { $this->assertTrue(Database::getConnection()->schema()->indexExists($table, "{$field_name}_value_format"), format_string("Index on value_format created in %table", array('%table' => $table))); diff --git a/modules/field/modules/list/list.info b/modules/field/modules/list/list.info index 3c9807f1..658433a4 100644 --- a/modules/field/modules/list/list.info +++ b/modules/field/modules/list/list.info @@ -7,8 +7,8 @@ dependencies[] = field dependencies[] = options files[] = tests/list.test -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/field/modules/list/tests/list.test b/modules/field/modules/list/tests/list.test index 84de7e89..b476b5aa 100644 --- a/modules/field/modules/list/tests/list.test +++ b/modules/field/modules/list/tests/list.test @@ -212,7 +212,7 @@ class ListFieldUITestCase extends FieldTestCase { parent::setUp('field_test', 'field_ui'); // Create test user. - $admin_user = $this->drupalCreateUser(array('access content', 'administer content types', 'administer taxonomy')); + $admin_user = $this->drupalCreateUser(array('access content', 'administer content types', 'administer taxonomy', 'administer fields')); $this->drupalLogin($admin_user); // Create content type, with underscores. diff --git a/modules/field/modules/list/tests/list_test.info b/modules/field/modules/list/tests/list_test.info index 09f553db..a15b62b1 100644 --- a/modules/field/modules/list/tests/list_test.info +++ b/modules/field/modules/list/tests/list_test.info @@ -5,8 +5,8 @@ package = Testing version = VERSION hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/field/modules/number/number.info b/modules/field/modules/number/number.info index 26527021..104e32a2 100644 --- a/modules/field/modules/number/number.info +++ b/modules/field/modules/number/number.info @@ -6,8 +6,8 @@ core = 7.x dependencies[] = field files[] = number.test -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/field/modules/number/number.module b/modules/field/modules/number/number.module index 60465442..0b8660dc 100644 --- a/modules/field/modules/number/number.module +++ b/modules/field/modules/number/number.module @@ -188,7 +188,7 @@ function number_field_formatter_info() { 'label' => t('Default'), 'field types' => array('number_integer'), 'settings' => array( - 'thousand_separator' => ' ', + 'thousand_separator' => '', // The 'decimal_separator' and 'scale' settings are not configurable // through the UI, and will therefore keep their default values. They // are only present so that the 'number_integer' and 'number_decimal' @@ -202,7 +202,7 @@ function number_field_formatter_info() { 'label' => t('Default'), 'field types' => array('number_decimal', 'number_float'), 'settings' => array( - 'thousand_separator' => ' ', + 'thousand_separator' => '', 'decimal_separator' => '.', 'scale' => 2, 'prefix_suffix' => TRUE, @@ -222,6 +222,8 @@ function number_field_formatter_settings_form($field, $instance, $view_mode, $fo $display = $instance['display'][$view_mode]; $settings = $display['settings']; + $element = array(); + if ($display['type'] == 'number_decimal' || $display['type'] == 'number_integer') { $options = array( '' => t(''), diff --git a/modules/field/modules/number/number.test b/modules/field/modules/number/number.test index 88029cdd..c88b4c18 100644 --- a/modules/field/modules/number/number.test +++ b/modules/field/modules/number/number.test @@ -23,7 +23,7 @@ class NumberFieldTestCase extends DrupalWebTestCase { function setUp() { parent::setUp('field_test'); - $this->web_user = $this->drupalCreateUser(array('access field_test content', 'administer field_test content', 'administer content types')); + $this->web_user = $this->drupalCreateUser(array('access field_test content', 'administer field_test content', 'administer content types', 'administer fields')); $this->drupalLogin($this->web_user); } diff --git a/modules/field/modules/options/options.info b/modules/field/modules/options/options.info index 17642a1e..d3198a3a 100644 --- a/modules/field/modules/options/options.info +++ b/modules/field/modules/options/options.info @@ -6,8 +6,8 @@ core = 7.x dependencies[] = field files[] = options.test -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/field/modules/options/options.module b/modules/field/modules/options/options.module index 3862ba77..041b84a6 100644 --- a/modules/field/modules/options/options.module +++ b/modules/field/modules/options/options.module @@ -185,6 +185,7 @@ function _options_properties($type, $multiple, $required, $has_value) { $base = array( 'filter_xss' => FALSE, 'strip_tags' => FALSE, + 'strip_tags_and_unescape' => FALSE, 'empty_option' => FALSE, 'optgroups' => FALSE, ); @@ -195,7 +196,7 @@ function _options_properties($type, $multiple, $required, $has_value) { case 'select': $properties = array( // Select boxes do not support any HTML tag. - 'strip_tags' => TRUE, + 'strip_tags_and_unescape' => TRUE, 'optgroups' => TRUE, ); if ($multiple) { @@ -271,9 +272,16 @@ function _options_prepare_options(&$options, $properties) { _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 --git a/modules/field/modules/options/options.test b/modules/field/modules/options/options.test index 7183311b..1cbb3854 100644 --- a/modules/field/modules/options/options.test +++ b/modules/field/modules/options/options.test @@ -24,7 +24,7 @@ class OptionsWidgetsTestCase extends FieldTestCase { '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(0 => 'Zero', 1 => 'One', 2 => 'Some & unescaped markup', 3 => 'Some HTML encoded markup with < & >'), ), ); $this->card_1 = field_create_field($this->card_1); @@ -54,7 +54,7 @@ class OptionsWidgetsTestCase extends FieldTestCase { $this->bool = field_create_field($this->bool); // Create a web user. - $this->web_user = $this->drupalCreateUser(array('access field_test content', 'administer field_test content')); + $this->web_user = $this->drupalCreateUser(array('access field_test content', 'administer field_test content', 'administer fields')); $this->drupalLogin($this->web_user); } @@ -233,6 +233,7 @@ class OptionsWidgetsTestCase extends FieldTestCase { $this->assertNoOptionSelected("edit-card-1-$langcode", 1); $this->assertNoOptionSelected("edit-card-1-$langcode", 2); $this->assertRaw('Some dangerous & unescaped markup', 'Option text was properly filtered.'); + $this->assertRaw('Some HTML encoded markup with < & >', 'HTML entities in option text were properly handled and not double-encoded'); // Submit form: select invalid 'none' option. $edit = array("card_1[$langcode]" => '_none'); @@ -459,7 +460,7 @@ class OptionsWidgetsTestCase extends FieldTestCase { $this->assertNoFieldChecked("edit-bool-$langcode"); // Create admin user. - $admin_user = $this->drupalCreateUser(array('access content', 'administer content types', 'administer taxonomy')); + $admin_user = $this->drupalCreateUser(array('access content', 'administer content types', 'administer taxonomy', 'administer fields')); $this->drupalLogin($admin_user); // Create a test field instance. diff --git a/modules/field/modules/text/text.info b/modules/field/modules/text/text.info index 4d3afd34..54cc28d6 100644 --- a/modules/field/modules/text/text.info +++ b/modules/field/modules/text/text.info @@ -7,8 +7,8 @@ dependencies[] = field files[] = text.test required = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/field/modules/text/text.module b/modules/field/modules/text/text.module index 68fc3cb4..bf0d29d5 100644 --- a/modules/field/modules/text/text.module +++ b/modules/field/modules/text/text.module @@ -223,11 +223,13 @@ function text_field_formatter_settings_form($field, $instance, $view_mode, $form if (strpos($display['type'], '_trimmed') !== FALSE) { $element['trim_length'] = array( - '#title' => t('Trim length'), + '#title' => t('Trimmed limit'), '#type' => 'textfield', + '#field_suffix' => t('characters'), '#size' => 10, '#default_value' => $settings['trim_length'], '#element_validate' => array('element_validate_integer_positive'), + '#description' => t('If the summary is not set, the trimmed %label field will be shorter than this character limit.', array('%label' => $instance['label'])), '#required' => TRUE, ); } @@ -245,7 +247,7 @@ function text_field_formatter_settings_summary($field, $instance, $view_mode) { $summary = ''; if (strpos($display['type'], '_trimmed') !== FALSE) { - $summary = t('Trim length') . ': ' . check_plain($settings['trim_length']); + $summary = t('Trimmed limit: @trim_length characters', array('@trim_length' => $settings['trim_length'])); } return $summary; diff --git a/modules/field/modules/text/text.test b/modules/field/modules/text/text.test index 2f147382..ad803cf4 100644 --- a/modules/field/modules/text/text.test +++ b/modules/field/modules/text/text.test @@ -424,6 +424,7 @@ class TextTranslationTestCase extends DrupalWebTestCase { 'administer content types', 'access administration pages', 'bypass node access', + 'administer fields', filter_permission_name($full_html_format), )); $this->translator = $this->drupalCreateUser(array('create article content', 'edit own article content', 'translate content')); diff --git a/modules/field/tests/field.test b/modules/field/tests/field.test index b279d6a8..adf2413b 100644 --- a/modules/field/tests/field.test +++ b/modules/field/tests/field.test @@ -2206,11 +2206,12 @@ class FieldDisplayAPITestCase extends FieldTestCase { 'alter' => TRUE, ), ); - $output = field_view_field('test_entity', $this->entity, $this->field_name, $display); + $output = field_view_field('test_entity', $this->entity, $this->field_name, $display, LANGUAGE_NONE); $this->drupalSetContent(drupal_render($output)); $setting = $display['settings']['test_formatter_setting_multiple']; $this->assertNoText($this->label, 'Label was not displayed.'); $this->assertText('field_test_field_attach_view_alter', 'Alter fired, display passed.'); + $this->assertText('field language is ' . LANGUAGE_NONE, 'Language is placed onto the context.'); $array = array(); foreach ($this->values as $delta => $value) { $array[] = $delta . ':' . $value['value']; diff --git a/modules/field/tests/field_test.info b/modules/field/tests/field_test.info index a47d95f8..7c0e5b7d 100644 --- a/modules/field/tests/field_test.info +++ b/modules/field/tests/field_test.info @@ -6,8 +6,8 @@ files[] = field_test.entity.inc version = VERSION hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/field/tests/field_test.module b/modules/field/tests/field_test.module index 9daa2c30..7e9bba0d 100644 --- a/modules/field/tests/field_test.module +++ b/modules/field/tests/field_test.module @@ -220,6 +220,10 @@ function field_test_field_attach_view_alter(&$output, $context) { 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']); + } } /** diff --git a/modules/field_ui/field_ui.admin.inc b/modules/field_ui/field_ui.admin.inc index 5d74a5ca..7d09d6f8 100644 --- a/modules/field_ui/field_ui.admin.inc +++ b/modules/field_ui/field_ui.admin.inc @@ -2105,6 +2105,10 @@ 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 --git a/modules/field_ui/field_ui.info b/modules/field_ui/field_ui.info index afb55d2c..5df082c9 100644 --- a/modules/field_ui/field_ui.info +++ b/modules/field_ui/field_ui.info @@ -6,8 +6,8 @@ core = 7.x dependencies[] = field files[] = field_ui.test -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/field_ui/field_ui.js b/modules/field_ui/field_ui.js index 65b28d04..c2e8978d 100644 --- a/modules/field_ui/field_ui.js +++ b/modules/field_ui/field_ui.js @@ -168,7 +168,7 @@ Drupal.fieldUIOverview = { 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'); @@ -319,7 +319,7 @@ Drupal.fieldUIDisplayOverview.field.prototype = { 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 --git a/modules/field_ui/field_ui.module b/modules/field_ui/field_ui.module index ed833feb..3b5f28a0 100644 --- a/modules/field_ui/field_ui.module +++ b/modules/field_ui/field_ui.module @@ -106,9 +106,19 @@ function field_ui_menu() { $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', @@ -392,3 +402,13 @@ function field_ui_form_node_type_form_submit($form, &$form_state) { $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 --git a/modules/field_ui/field_ui.test b/modules/field_ui/field_ui.test index 21767d64..e09355b5 100644 --- a/modules/field_ui/field_ui.test +++ b/modules/field_ui/field_ui.test @@ -22,7 +22,7 @@ class FieldUITestCase extends DrupalWebTestCase { 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. @@ -445,6 +445,19 @@ class FieldUIManageFieldsTestCase extends FieldUITestCase { $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.'); + } } /** @@ -682,7 +695,7 @@ class FieldUIAlterTestCase extends DrupalWebTestCase { parent::setUp(array('field_test')); // Create test user. - $admin_user = $this->drupalCreateUser(array('access content', 'administer content types', 'administer users')); + $admin_user = $this->drupalCreateUser(array('access content', 'administer content types', 'administer users', 'administer fields')); $this->drupalLogin($admin_user); } diff --git a/modules/file/file.field.inc b/modules/file/file.field.inc index b26d7e45..d592381b 100644 --- a/modules/file/file.field.inc +++ b/modules/file/file.field.inc @@ -252,6 +252,12 @@ function file_field_insert($entity_type, $entity, $field, $instance, $langcode, * 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 @@ -626,7 +632,7 @@ function file_field_widget_process($element, &$form_state, $form) { $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'), diff --git a/modules/file/file.info b/modules/file/file.info index 6ac9f888..4bf08121 100644 --- a/modules/file/file.info +++ b/modules/file/file.info @@ -6,8 +6,8 @@ core = 7.x dependencies[] = field files[] = tests/file.test -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/file/file.module b/modules/file/file.module index a4a7f03d..bf7b07d8 100644 --- a/modules/file/file.module +++ b/modules/file/file.module @@ -92,7 +92,7 @@ function file_theme() { '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', @@ -357,6 +357,10 @@ function file_file_delete($file) { * support for a default value. */ function file_managed_file_process($element, &$form_state, $form) { + // Append the '-upload' to the #id so the field label's 'for' attribute + // corresponds with the file element. + $original_id = $element['#id']; + $element['#id'] .= '-upload'; $fid = isset($element['#value']['fid']) ? $element['#value']['fid'] : 0; // Set some default element properties. @@ -366,7 +370,7 @@ function file_managed_file_process($element, &$form_state, $form) { $ajax_settings = array( 'path' => 'file/ajax/' . implode('/', $element['#array_parents']) . '/' . $form['form_build_id']['#value'], - 'wrapper' => $element['#id'] . '-ajax-wrapper', + 'wrapper' => $original_id . '-ajax-wrapper', 'effect' => 'fade', 'progress' => array( 'type' => $element['#progress_indicator'], @@ -453,6 +457,17 @@ function file_managed_file_process($element, &$form_state, $form) { '#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. @@ -461,13 +476,13 @@ function file_managed_file_process($element, &$form_state, $form) { $element['upload']['#attached']['js'] = array( array( 'type' => 'setting', - 'data' => array('file' => array('elements' => array('#' . $element['#id'] . '-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; @@ -515,14 +530,40 @@ function file_managed_file_value(&$element, $input = FALSE, $form_state = NULL) // public file) to confirm it exists and that the current user has access // to it. if (isset($input['fid']) && ($file = file_load($input['fid']))) { - if (file_uri_scheme($file->uri) == 'public' || file_download_access($file->uri)) { - $fid = $file->fid; - } - // If the current user doesn't have access, don't let the file be - // changed. - else { + // 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; + } } } } @@ -735,7 +776,32 @@ function theme_file_link($variables) { $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 @@ -765,16 +831,19 @@ function theme_file_link($variables) { * - 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) . ''; } /** diff --git a/modules/file/tests/file.test b/modules/file/tests/file.test index 0f6a578d..1510a697 100644 --- a/modules/file/tests/file.test +++ b/modules/file/tests/file.test @@ -22,7 +22,7 @@ class FileFieldTestCase extends DrupalWebTestCase { $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')); + $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); } @@ -218,6 +218,30 @@ class FileFieldTestCase extends DrupalWebTestCase { $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; + } } /** @@ -377,6 +401,18 @@ class FileManagedFileElementTestCase extends FileFieldTestCase { $this->drupalPost($path, array(), t('Save')); $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)); @@ -474,6 +510,15 @@ class FileFieldWidgetTestCase extends FileFieldTestCase { $node_file = (object) $node->{$field_name}[LANGUAGE_NONE][0]; $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, 'Confirmed that the generated URL is correct by downloading the shipped file.'); @@ -505,6 +550,120 @@ class FileFieldWidgetTestCase extends FileFieldTestCase { } } + /** + * 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); + } + + /** + * 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. */ @@ -877,6 +1036,7 @@ class FileFieldDisplayTestCase extends FileFieldTestCase { $field_settings = array( 'display_field' => '1', 'display_default' => '1', + 'cardinality' => FIELD_CARDINALITY_UNLIMITED, ); $instance_settings = array( 'description_field' => '1', @@ -917,6 +1077,45 @@ class FileFieldDisplayTestCase extends FileFieldTestCase { $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.'); } } @@ -1304,3 +1503,178 @@ class FilePrivateTestCase extends FileFieldTestCase { $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.'); } } + +/** + * 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; + } + +} diff --git a/modules/file/tests/file_module_test.info b/modules/file/tests/file_module_test.info index 5f0bbb2f..a6ad129a 100644 --- a/modules/file/tests/file_module_test.info +++ b/modules/file/tests/file_module_test.info @@ -5,8 +5,8 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/filter/filter.info b/modules/filter/filter.info index b3a51464..a513f5fc 100644 --- a/modules/filter/filter.info +++ b/modules/filter/filter.info @@ -7,8 +7,8 @@ files[] = filter.test required = TRUE configure = admin/config/content/formats -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/filter/filter.module b/modules/filter/filter.module index 5c2b8c97..c710ee70 100644 --- a/modules/filter/filter.module +++ b/modules/filter/filter.module @@ -93,6 +93,14 @@ function filter_menu() { '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.', @@ -340,6 +348,7 @@ function filter_admin_format_title($format) { 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, ); @@ -348,9 +357,7 @@ function filter_permission() { 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.')), @@ -1120,18 +1127,23 @@ function filter_dom_serialize($dom_document) { $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); } /** @@ -1485,7 +1497,7 @@ function _filter_url($text, $filter) { $tasks['_filter_url_parse_full_links'] = $pattern; // Match e-mail addresses. - $url_pattern = "[A-Za-z0-9._-]{1,254}@(?:$domain)"; + $url_pattern = "[A-Za-z0-9._+-]{1,254}@(?:$domain)"; $pattern = "`($url_pattern)`"; $tasks['_filter_url_parse_email_links'] = $pattern; diff --git a/modules/filter/filter.pages.inc b/modules/filter/filter.pages.inc index e602bcef..0f13da84 100644 --- a/modules/filter/filter.pages.inc +++ b/modules/filter/filter.pages.inc @@ -14,10 +14,9 @@ * @see filter_menu() * @see theme_filter_tips() */ -function filter_tips_long() { - $format_id = arg(2); - if ($format_id) { - $output = theme('filter_tips', array('tips' => _filter_tips($format_id, TRUE), 'long' => TRUE)); +function filter_tips_long($format = NULL) { + if (!empty($format)) { + $output = theme('filter_tips', array('tips' => _filter_tips($format->format, TRUE), 'long' => TRUE)); } else { $output = theme('filter_tips', array('tips' => _filter_tips(-1, TRUE), 'long' => TRUE)); diff --git a/modules/filter/filter.test b/modules/filter/filter.test index fe9cfc36..34dcf043 100644 --- a/modules/filter/filter.test +++ b/modules/filter/filter.test @@ -555,6 +555,27 @@ class FilterFormatAccessTestCase extends DrupalWebTestCase { $this->assertTrue(isset($options[$this->allowed_format->format]), 'The allowed text format appears as an option when adding a new node.'); $this->assertFalse(isset($options[$this->disallowed_format->format]), 'The disallowed text format does not appear as an option when adding a new node.'); $this->assertTrue(isset($options[filter_fallback_format()]), 'The fallback format appears as an option when adding a new node.'); + + // Check regular user access to the filter tips pages. + $this->drupalGet('filter/tips/' . $this->allowed_format->format); + $this->assertResponse(200); + $this->drupalGet('filter/tips/' . $this->disallowed_format->format); + $this->assertResponse(403); + $this->drupalGet('filter/tips/' . filter_fallback_format()); + $this->assertResponse(200); + $this->drupalGet('filter/tips/invalid-format'); + $this->assertResponse(404); + + // Check admin user access to the filter tips pages. + $this->drupalLogin($this->admin_user); + $this->drupalGet('filter/tips/' . $this->allowed_format->format); + $this->assertResponse(200); + $this->drupalGet('filter/tips/' . $this->disallowed_format->format); + $this->assertResponse(200); + $this->drupalGet('filter/tips/' . filter_fallback_format()); + $this->assertResponse(200); + $this->drupalGet('filter/tips/invalid-format'); + $this->assertResponse(404); } /** @@ -1099,8 +1120,12 @@ class FilterUnitTestCase extends DrupalUnitTestCase { $f = filter_xss("", array('img')); $this->assertNoNormalized($f, 'cript', 'HTML scheme clearing evasion -- embedded nulls.'); - $f = filter_xss('', array('img')); - $this->assertNoNormalized($f, 'javascript', 'HTML scheme clearing evasion -- spaces and metacharacters before scheme.'); + // @todo This dataset currently fails under 5.4 because of + // https://www.drupal.org/node/1210798. Restore after it's fixed. + if (version_compare(PHP_VERSION, '5.4.0', '<')) { + $f = filter_xss('', array('img')); + $this->assertNoNormalized($f, 'javascript', 'HTML scheme clearing evasion -- spaces and metacharacters before scheme.'); + } $f = filter_xss('', array('img')); $this->assertNoNormalized($f, 'vbscript', 'HTML scheme clearing evasion -- another scheme.'); @@ -1148,7 +1173,7 @@ class FilterUnitTestCase extends DrupalUnitTestCase { // Setup dummy filter object. $filter = new stdClass(); $filter->settings = array( - 'allowed_html' => '
    1. ', + 'allowed_html' => '
        1. ', 'filter_html_help' => 1, 'filter_html_nofollow' => 0, ); @@ -1184,6 +1209,10 @@ class FilterUnitTestCase extends DrupalUnitTestCase { $f = _filter_html(' ', $filter); $this->assertNoNormalized($f, 'onerror', 'HTML filter should remove empty on* attributes on default.'); + + // Custom tags are supported and should be allowed through. + $f = _filter_html('', $filter); + $this->assertNormalized($f, 'test-element', 'HTML filter should allow custom elements.'); } /** @@ -1269,6 +1298,7 @@ class FilterUnitTestCase extends DrupalUnitTestCase { // Create a e-mail that is too long. $long_email = str_repeat('a', 254) . '@example.com'; $too_long_email = str_repeat('b', 255) . '@example.com'; + $email_with_plus_sign = 'one+two@example.com'; // Filter selection/pattern matching. @@ -1282,12 +1312,13 @@ http://example.com or www.example.com ), // MAILTO URLs. ' -person@example.com or mailto:person2@example.com or ' . $long_email . ' but not ' . $too_long_email . ' +person@example.com or mailto:person2@example.com or ' . $email_with_plus_sign . ' or ' . $long_email . ' but not ' . $too_long_email . ' ' => array( 'person@example.com' => TRUE, 'mailto:person2@example.com' => TRUE, '' . $long_email . '' => TRUE, '' . $too_long_email . '' => FALSE, + '' . $email_with_plus_sign . '' => TRUE, ), // URI parts and special characters. ' @@ -1979,3 +2010,26 @@ class FilterSettingsTestCase extends DrupalWebTestCase { } } } + +/** + * Tests DOMDocument serialization. + */ +class FilterDOMSerializeTestCase extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Serialization', + 'description' => 'Test serialization of DOMDocument objects.', + 'group' => 'Filter', + ); + } + + /** + * Tests empty DOMDocument object. + */ + function testFilterEmptyDOMSerialization() { + $document = new DOMDocument(); + $result = filter_dom_serialize($document); + $this->assertEqual('', $result); + } +} diff --git a/modules/forum/forum.info b/modules/forum/forum.info index 56870408..74cc2d03 100644 --- a/modules/forum/forum.info +++ b/modules/forum/forum.info @@ -9,8 +9,8 @@ files[] = forum.test configure = admin/structure/forum stylesheets[all][] = forum.css -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/help/help.info b/modules/help/help.info index 8db8096f..062b30e6 100644 --- a/modules/help/help.info +++ b/modules/help/help.info @@ -5,8 +5,8 @@ version = VERSION core = 7.x files[] = help.test -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/image/image.info b/modules/image/image.info index 99a2375e..47fb5a5a 100644 --- a/modules/image/image.info +++ b/modules/image/image.info @@ -7,8 +7,8 @@ dependencies[] = file files[] = image.test configure = admin/config/media/image-styles -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/image/image.module b/modules/image/image.module index a2a0f416..dab88361 100644 --- a/modules/image/image.module +++ b/modules/image/image.module @@ -64,7 +64,7 @@ function image_help($path, $arg) { $effect = image_effect_definition_load($arg[7]); return isset($effect['help']) ? ('

          ' . $effect['help'] . '

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

          ' . $effect['help'] . '

          ') : NULL; } } @@ -801,6 +801,8 @@ function image_style_options($include_empty = TRUE, $output = CHECK_PLAIN) { * * @param $style * The image style + * @param $scheme + * The file scheme, for example 'public' for public files. */ function image_style_deliver($style, $scheme) { $args = func_get_args(); @@ -833,8 +835,8 @@ function image_style_deliver($style, $scheme) { file_download($scheme, file_uri_target($derivative_uri)); } else { - $headers = module_invoke_all('file_download', $image_uri); - if (in_array(-1, $headers) || empty($headers)) { + $headers = file_download_headers($image_uri); + if (empty($headers)) { return MENU_ACCESS_DENIED; } if (count($headers)) { @@ -1027,7 +1029,15 @@ function image_style_url($style_name, $path) { // The token query is added even if the 'image_allow_insecure_derivatives' // variable is TRUE, so that the emitted links remain valid if it is changed // back to the default FALSE. - $token_query = array(IMAGE_DERIVATIVE_TOKEN => image_style_path_token($style_name, $original_uri)); + // However, sites which need to prevent the token query from being emitted at + // all can additionally set the 'image_suppress_itok_output' variable to TRUE + // to achieve that (if both are set, the security token will neither be + // emitted in the image derivative URL nor checked for in + // image_style_deliver()). + $token_query = array(); + if (!variable_get('image_suppress_itok_output', FALSE)) { + $token_query = array(IMAGE_DERIVATIVE_TOKEN => image_style_path_token($style_name, $original_uri)); + } // If not using clean URLs, the image derivative callback is only available // with the query string. If the file does not exist, use url() to ensure @@ -1039,8 +1049,12 @@ function image_style_url($style_name, $path) { } $file_url = file_create_url($uri); - // Append the query string with the token. - return $file_url . (strpos($file_url, '?') !== FALSE ? '&' : '?') . drupal_http_build_query($token_query); + // Append the query string with the token, if necessary. + if ($token_query) { + $file_url .= (strpos($file_url, '?') !== FALSE ? '&' : '?') . drupal_http_build_query($token_query); + } + + return $file_url; } /** diff --git a/modules/image/image.test b/modules/image/image.test index 2387314c..0c26ffa8 100644 --- a/modules/image/image.test +++ b/modules/image/image.test @@ -32,7 +32,7 @@ class ImageFieldTestCase extends DrupalWebTestCase { function setUp() { parent::setUp('image'); - $this->admin_user = $this->drupalCreateUser(array('access content', 'access administration pages', 'administer site configuration', 'administer content types', 'administer nodes', 'create article content', 'edit any article content', 'delete any article content', 'administer image styles')); + $this->admin_user = $this->drupalCreateUser(array('access content', 'access administration pages', 'administer site configuration', 'administer content types', 'administer nodes', 'create article content', 'edit any article content', 'delete any article content', 'administer image styles', 'administer fields')); $this->drupalLogin($this->admin_user); } @@ -77,6 +77,24 @@ class ImageFieldTestCase extends DrupalWebTestCase { return field_create_instance($instance); } + /** + * Create a random style. + * + * @return array + * A list containing the details of the generated image style. + */ + function createRandomStyle() { + $style_name = strtolower($this->randomName(10)); + $style_label = $this->randomString(); + image_style_save(array('name' => $style_name, 'label' => $style_label)); + $style_path = 'admin/config/media/image-styles/edit/' . $style_name; + return array( + 'name' => $style_name, + 'label' => $style_label, + 'path' => $style_path, + ); + } + /** * Upload an image to a node. * @@ -183,6 +201,22 @@ class ImageStylesPathAndUrlTestCase extends DrupalWebTestCase { $this->assertResponse(404, 'Accessing an image style URL with a source image that does not exist provides a 404 error response.'); } + /** + * Test that we do not pass an array to drupal_add_http_header. + */ + function testImageContentTypeHeaders() { + $files = $this->drupalGetTestFiles('image'); + $file = array_shift($files); + // Copy the test file to private folder. + $private_file = file_copy($file, 'private://', FILE_EXISTS_RENAME); + // Tell image_module_test module to return the headers we want to test. + variable_set('image_module_test_invalid_headers', $private_file->uri); + // Invoke image_style_deliver so it will try to set headers. + $generated_url = image_style_url($this->style_name, $private_file->uri); + $this->drupalGet($generated_url); + variable_del('image_module_test_invalid_headers'); + } + /** * Test image_style_url(). */ @@ -251,7 +285,7 @@ class ImageStylesPathAndUrlTestCase extends DrupalWebTestCase { $this->assertEqual($this->drupalGetHeader('Content-Length'), $generated_image_info['file_size'], 'Expected Content-Length was reported.'); if ($scheme == 'private') { $this->assertEqual($this->drupalGetHeader('Expires'), 'Sun, 19 Nov 1978 05:00:00 GMT', 'Expires header was sent.'); - $this->assertEqual($this->drupalGetHeader('Cache-Control'), 'no-cache, must-revalidate, post-check=0, pre-check=0', 'Cache-Control header was set to prevent caching.'); + $this->assertEqual($this->drupalGetHeader('Cache-Control'), 'no-cache, must-revalidate', 'Cache-Control header was set to prevent caching.'); $this->assertEqual($this->drupalGetHeader('X-Image-Owned-By'), 'image_module_test', 'Expected custom header has been added.'); // Make sure that a second request to the already existing derivate works @@ -330,6 +364,15 @@ class ImageStylesPathAndUrlTestCase extends DrupalWebTestCase { $this->drupalGet($nested_url); $this->assertResponse(200, 'Image was accessible when a correct token was provided in the URL.'); + // Suppress the security token in the URL, then get the URL of a file. Check + // that the security token is not present in the URL but that the image is + // still accessible. + variable_set('image_suppress_itok_output', TRUE); + $generate_url = image_style_url($this->style_name, $original_uri); + $this->assertIdentical(strpos($generate_url, IMAGE_DERIVATIVE_TOKEN . '='), FALSE, 'The security token does not appear in the image style URL.'); + $this->drupalGet($generate_url); + $this->assertResponse(200, 'Image was accessible at the URL with a missing token.'); + // Check that requesting a nonexistent image does not create any new // directories in the file system. $directory = $scheme . '://styles/' . $this->style_name . '/' . $scheme . '/' . $this->randomName(); @@ -460,6 +503,58 @@ class ImageEffectsUnitTest extends ImageToolkitTestCase { } } +/** + * Tests the administrative user interface. + */ +class ImageAdminUiTestCase extends ImageFieldTestCase { + public static function getInfo() { + return array( + 'name' => 'Administrative user interface', + 'description' => 'Tests the forms used in the administrative user interface.', + 'group' => 'Image', + ); + } + + function setUp() { + parent::setUp(array('image')); + } + + /** + * Test if the help text is available on the add effect form. + */ + function testAddEffectHelpText() { + // Create a random image style. + $style = $this->createRandomStyle(); + + // Open the add effect form and check for the help text. + $this->drupalGet($style['path'] . '/add/image_crop'); + $this->assertText(t('Cropping will remove portions of an image to make it the specified dimensions.'), 'The image style effect help text was displayed on the add effect page.'); + } + + /** + * Test if the help text is available on the edit effect form. + */ + function testEditEffectHelpText() { + // Create a random image style. + $random_style = $this->createRandomStyle(); + + // Add the crop effect to the image style. + $edit = array(); + $edit['data[width]'] = 20; + $edit['data[height]'] = 20; + $this->drupalPost($random_style['path'] . '/add/image_crop', $edit, t('Add effect')); + + // Open the edit effect form and check for the help text. + drupal_static_reset('image_styles'); + $style = image_style_load($random_style['name']); + + foreach ($style['effects'] as $ieid => $effect) { + $this->drupalGet($random_style['path'] . '/effects/' . $ieid); + $this->assertText(t('Cropping will remove portions of an image to make it the specified dimensions.'), 'The image style effect help text was displayed on the edit effect page.'); + } + } +} + /** * Tests creation, deletion, and editing of image styles and effects. */ diff --git a/modules/image/tests/image_module_test.info b/modules/image/tests/image_module_test.info index aad82924..f9f3f484 100644 --- a/modules/image/tests/image_module_test.info +++ b/modules/image/tests/image_module_test.info @@ -6,8 +6,8 @@ core = 7.x files[] = image_module_test.module hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/image/tests/image_module_test.module b/modules/image/tests/image_module_test.module index 8a322fb9..fc66d9b8 100644 --- a/modules/image/tests/image_module_test.module +++ b/modules/image/tests/image_module_test.module @@ -9,6 +9,9 @@ function image_module_test_file_download($uri) { if (variable_get('image_module_test_file_download', FALSE) == $uri) { return array('X-Image-Owned-By' => 'image_module_test'); } + if (variable_get('image_module_test_invalid_headers', FALSE) == $uri) { + return array('Content-Type' => 'image/png'); + } } /** diff --git a/modules/locale/locale.admin.inc b/modules/locale/locale.admin.inc index b736f79b..acf6eb2e 100644 --- a/modules/locale/locale.admin.inc +++ b/modules/locale/locale.admin.inc @@ -1139,11 +1139,11 @@ function locale_translate_edit_form($form, &$form_state, $lid) { '#value' => $source->location ); - // Include default form controls with empty values for all languages. - // This ensures that the languages are always in the same order in forms. + // Include both translated and not yet translated target languages in the + // list. The source language is English for built-in strings and the default + // language for other strings. $languages = language_list(); $default = language_default(); - // We don't need the default language value, that value is in $source. $omit = $source->textgroup == 'default' ? 'en' : $default->language; unset($languages[($omit)]); $form['translations'] = array('#tree' => TRUE); @@ -1194,7 +1194,7 @@ function locale_translate_edit_form_submit($form, &$form_state) { $translation = db_query("SELECT translation FROM {locales_target} WHERE lid = :lid AND language = :language", array(':lid' => $lid, ':language' => $key))->fetchField(); if (!empty($value)) { // Only update or insert if we have a value to use. - if (!empty($translation)) { + if (is_string($translation)) { db_update('locales_target') ->fields(array( 'translation' => $value, diff --git a/modules/locale/locale.info b/modules/locale/locale.info index 2f6e408b..520d4ecd 100644 --- a/modules/locale/locale.info +++ b/modules/locale/locale.info @@ -6,8 +6,8 @@ core = 7.x files[] = locale.test configure = admin/config/regional/language -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/locale/locale.test b/modules/locale/locale.test index 9ffec9f1..6fcf06fe 100644 --- a/modules/locale/locale.test +++ b/modules/locale/locale.test @@ -393,6 +393,16 @@ class LocaleTranslationFunctionalTest extends DrupalWebTestCase { // The indicator should not be here. $this->assertNoRaw($language_indicator, 'String is translated.'); + // Verify that a translation set which has an empty target string can be + // updated without any database error. + db_update('locales_target') + ->fields(array('translation' => '')) + ->condition('language', $langcode, '=') + ->condition('lid', $lid, '=') + ->execute(); + $this->drupalPost('admin/config/regional/translate/edit/' . $lid, $edit, t('Save translations')); + $this->assertText(t('The string has been saved.'), 'The string has been saved.'); + // Try to edit a non-existent string and ensure we're redirected correctly. // Assuming we don't have 999,999 strings already. $random_lid = 999999; @@ -1202,7 +1212,7 @@ EOF; * Helper function that returns a .po file with context. */ function getPoFileWithContext() { - // Croatian (code hr) is one the the languages that have a different + // Croatian (code hr) is one of the languages that have a different // form for the full name and the abbreviated name for the month May. return <<< EOF msgid "" @@ -2237,6 +2247,37 @@ class LocaleContentFunctionalTest extends DrupalWebTestCase { $this->drupalLogout(); } + + /** + * Verifies that nodes may be created with different languages. + */ + function testNodeCreationWithLanguage() { + // Create an admin user and log them in. + $perms = array( + // Standard node permissions. + 'create page content', + 'administer content types', + 'administer nodes', + 'bypass node access', + // Locale. + 'administer languages', + ); + $web_user = $this->drupalCreateUser($perms); + $this->drupalLogin($web_user); + + // Create some test nodes using different langcodes. + foreach (array(LANGUAGE_NONE, 'en', 'fr') as $langcode) { + $node_args = array( + 'type' => 'page', + 'promote' => 1, + 'language' => $langcode, + ); + $node = $this->drupalCreateNode($node_args); + $node_reloaded = node_load($node->nid, NULL, TRUE); + $this->assertEqual($node_reloaded->language, $langcode, format_string('The language code of the node was successfully set to @langcode.', array('@langcode' => $langcode))); + } + } + } /** @@ -2629,6 +2670,68 @@ class LocaleUrlRewritingTest extends DrupalWebTestCase { $this->drupalGet("$prefix/$path"); $this->assertResponse(404, $message2); } + + /** + * Check URL rewriting when using a domain name and a non-standard port. + */ + function testDomainNameNegotiationPort() { + $language_domain = 'example.fr'; + $edit = array( + 'locale_language_negotiation_url_part' => 1, + ); + $this->drupalPost('admin/config/regional/language/configure/url', $edit, t('Save configuration')); + $edit = array( + 'prefix' => '', + 'domain' => $language_domain + ); + $this->drupalPost('admin/config/regional/language/edit/fr', $edit, t('Save language')); + + // Enable domain configuration. + variable_set('locale_language_negotiation_url_part', LOCALE_LANGUAGE_NEGOTIATION_URL_DOMAIN); + + // Reset static caching. + drupal_static_reset('language_list'); + drupal_static_reset('language_url_outbound_alter'); + drupal_static_reset('language_url_rewrite_url'); + + // In case index.php is part of the URLs, we need to adapt the asserted + // URLs as well. + $index_php = strpos(url('', array('absolute' => TRUE)), 'index.php') !== FALSE; + + // Remember current HTTP_HOST. + $http_host = $_SERVER['HTTP_HOST']; + + // Fake a different port. + $_SERVER['HTTP_HOST'] .= ':88'; + + // Create an absolute French link. + $languages = language_list(); + $language = $languages['fr']; + $url = url('', array( + 'absolute' => TRUE, + 'language' => $language + )); + + $expected = 'http://example.fr:88/'; + $expected .= $index_php ? 'index.php/' : ''; + + $this->assertEqual($url, $expected, 'The right port is used.'); + + // If we set the port explicitly in url(), it should not be overriden. + $url = url('', array( + 'absolute' => TRUE, + 'language' => $language, + 'base_url' => $GLOBALS['base_url'] . ':90', + )); + + $expected = 'http://example.fr:90/'; + $expected .= $index_php ? 'index.php/' : ''; + + $this->assertEqual($url, $expected, 'A given port is not overriden.'); + + // Restore HTTP_HOST. + $_SERVER['HTTP_HOST'] = $http_host; + } } /** @@ -3141,3 +3244,46 @@ class LocaleCSSAlterTest extends DrupalWebTestCase { $this->assertRaw('@import url("' . $base_url . '/modules/system/system.messages.css' . $query_string . '");' . "\n" . '@import url("' . $base_url . '/modules/system/system.messages-rtl.css' . $query_string . '");' . "\n", 'CSS: system.messages-rtl.css is added directly after system.messages.css.'); } } + +/** + * Tests locale translation safe string handling. + */ +class LocaleStringIsSafeTest extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'Test if a string is safe', + 'description' => 'Tests locale translation safe string handling.', + 'group' => 'Locale', + ); + } + + function setUp() { + parent::setUp('locale'); + } + + /** + * Tests for locale_string_is_safe(). + */ + public function testLocaleStringIsSafe() { + // Check a translatable string without HTML. + $string = 'Hello world!'; + $result = locale_string_is_safe($string); + $this->assertTrue($result); + + // Check a translatable string which includes trustable HTML. + $string = 'Hello world!'; + $result = locale_string_is_safe($string); + $this->assertTrue($result); + + // Check an untranslatable string which includes untrustable HTML (according + // to the locale_string_is_safe() function definition). + $string = 'Hello world!'; + $result = locale_string_is_safe($string); + $this->assertFalse($result); + + // Check a translatable string which includes a token in an href attribute. + $string = 'Hi user'; + $result = locale_string_is_safe($string); + $this->assertTrue($result); + } +} diff --git a/modules/locale/tests/locale_test.info b/modules/locale/tests/locale_test.info index 3daa74b4..b51de9f3 100644 --- a/modules/locale/tests/locale_test.info +++ b/modules/locale/tests/locale_test.info @@ -5,8 +5,8 @@ package = Testing version = VERSION hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/menu/menu.admin.inc b/modules/menu/menu.admin.inc index 66bd6f3b..a24703c9 100644 --- a/modules/menu/menu.admin.inc +++ b/modules/menu/menu.admin.inc @@ -281,6 +281,7 @@ function menu_edit_item($form, &$form_state, $type, $item, $menu) { $form['link_title'] = array( '#type' => 'textfield', '#title' => t('Menu link title'), + '#maxlength' => 255, '#default_value' => $item['link_title'], '#description' => t('The text to be used for this link in the menu.'), '#required' => TRUE, @@ -305,7 +306,7 @@ function menu_edit_item($form, &$form_state, $type, $item, $menu) { '#title' => t('Path'), '#maxlength' => 255, '#default_value' => $path, - '#description' => t('The path for this menu link. This can be an internal Drupal path such as %add-node or an external URL such as %drupal. Enter %front to link to the front page.', array('%front' => '', '%add-node' => 'node/add', '%drupal' => 'http://drupal.org')), + '#description' => t('The path for this menu link. This can be an internal path such as %add-node or an external URL such as %example. Enter %front to link to the front page.', array('%front' => '', '%add-node' => 'node/add', '%example' => 'http://example.com')), '#required' => TRUE, ); $form['actions']['delete'] = array( diff --git a/modules/menu/menu.info b/modules/menu/menu.info index 14db656a..9c6a8d19 100644 --- a/modules/menu/menu.info +++ b/modules/menu/menu.info @@ -6,8 +6,8 @@ core = 7.x files[] = menu.test configure = admin/structure/menu -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/menu/menu.module b/modules/menu/menu.module index dc8f015d..27b1675a 100644 --- a/modules/menu/menu.module +++ b/modules/menu/menu.module @@ -674,6 +674,7 @@ function menu_form_node_form_alter(&$form, $form_state) { $form['menu']['link']['link_title'] = array( '#type' => 'textfield', '#title' => t('Menu link title'), + '#maxlength' => 255, '#default_value' => $link['link_title'], ); diff --git a/modules/menu/menu.test b/modules/menu/menu.test index a9bdb5f2..bb792ee8 100644 --- a/modules/menu/menu.test +++ b/modules/menu/menu.test @@ -72,6 +72,17 @@ class MenuTestCase extends DrupalWebTestCase { $saved_item = menu_link_load($item['mlid']); $this->assertEqual($description, $saved_item['options']['attributes']['title'], 'Saving an existing link updates the description (title attribute)'); $this->resetMenuLink($item, $old_title); + + // Test that the page title is correct when a local task appears in a + // top-level menu item. See https://www.drupal.org/node/1973262. + $item = $this->addMenuLink(0, 'user/register', 'user-menu'); + $this->drupalGet('user/password'); + $this->assertNoTitle('Home | Drupal'); + $this->drupalLogout(); + $this->drupalGet('user/register'); + $this->assertTitle($item['link_title'] . ' | Drupal'); + $this->drupalGet('user'); + $this->assertNoTitle('Home | Drupal'); } /** @@ -637,7 +648,12 @@ class MenuNodeTestCase extends DrupalWebTestCase { ); $this->drupalPost('admin/structure/types/manage/page', $edit, t('Save content type')); - // Create a node. + // Verify that the menu link title on the node add form has the correct + // maxlength. + $this->drupalGet('node/add/page'); + $this->assertPattern('//', 'Menu link title field has correct maxlength in node add form.'); + + // Create a node with menu link disabled. $node_title = $this->randomName(); $language = LANGUAGE_NONE; $edit = array( @@ -673,6 +689,10 @@ class MenuNodeTestCase extends DrupalWebTestCase { $this->drupalGet('node/' . $node->nid . '/edit'); $this->assertOptionSelected('edit-menu-weight', 17, 'Menu weight correct in edit form'); + // Verify that the menu link title on the node edit form has the correct + // maxlength. + $this->assertPattern('//', 'Menu link title field has correct maxlength in node edit form.'); + // Edit the node and remove the menu link. $edit = array( 'menu[enabled]' => FALSE, diff --git a/modules/node/content_types.inc b/modules/node/content_types.inc index 55af6670..c451dc7e 100644 --- a/modules/node/content_types.inc +++ b/modules/node/content_types.inc @@ -11,7 +11,7 @@ function node_overview_types() { $types = node_type_get_types(); $names = node_type_get_names(); - $field_ui = module_exists('field_ui'); + $field_ui = module_exists('field_ui') && user_access('administer fields'); $header = array(t('Name'), array('data' => t('Operations'), 'colspan' => $field_ui ? '4' : '2')); $rows = array(); diff --git a/modules/node/node.admin.inc b/modules/node/node.admin.inc index 35f4c1d5..eead4ea9 100644 --- a/modules/node/node.admin.inc +++ b/modules/node/node.admin.inc @@ -329,6 +329,8 @@ function _node_mass_update_helper($nid, $updates) { } /** + * Implements callback_batch_operation(). + * * Executes a batch operation for node_mass_update(). * * @param array $nodes @@ -367,7 +369,9 @@ function _node_mass_update_batch_process($nodes, $updates, &$context) { } /** - * Menu callback: Reports the status of batch operation for node_mass_update(). + * Implements callback_batch_finished(). + * + * Reports the status of batch operation for node_mass_update(). * * @param bool $success * A boolean indicating whether the batch mass update operation successfully @@ -504,14 +508,17 @@ function node_admin_nodes() { $options = array(); foreach ($nodes as $node) { $langcode = entity_language('node', $node); - $l_options = $langcode != LANGUAGE_NONE && isset($languages[$langcode]) ? array('language' => $languages[$langcode]) : array(); + $uri = entity_uri('node', $node); + if ($langcode != LANGUAGE_NONE && isset($languages[$langcode])) { + $uri['options']['language'] = $languages[$langcode]; + } $options[$node->nid] = array( 'title' => array( 'data' => array( '#type' => 'link', '#title' => $node->title, - '#href' => 'node/' . $node->nid, - '#options' => $l_options, + '#href' => $uri['path'], + '#options' => $uri['options'], '#suffix' => ' ' . theme('mark', array('type' => node_mark($node->nid, $node->changed))), ), ), diff --git a/modules/node/node.api.php b/modules/node/node.api.php index a83dee94..c8176a7d 100644 --- a/modules/node/node.api.php +++ b/modules/node/node.api.php @@ -950,7 +950,7 @@ function hook_node_info() { * 'recent', or 'comments'. The values should be arrays themselves, with the * following keys available: * - title: (required) The human readable name of the ranking mechanism. - * - join: (optional) The part of a query string to join to any additional + * - join: (optional) An array with information to join any additional * necessary table. This is not necessary if the table required is already * joined to by the base query, such as for the {node} table. Other tables * should use the full table name as an alias to avoid naming collisions. @@ -974,7 +974,12 @@ function hook_ranking() { 'title' => t('Average vote'), // Note that we use i.sid, the search index's search item id, rather than // n.nid. - 'join' => 'LEFT JOIN {vote_node_data} vote_node_data ON vote_node_data.nid = i.sid', + 'join' => array( + 'type' => 'LEFT', + 'table' => 'vote_node_data', + 'alias' => 'vote_node_data', + 'on' => 'vote_node_data.nid = i.sid', + ), // The highest possible score should be 1, and the lowest possible score, // always 0, should be 0. 'score' => 'vote_node_data.average / CAST(%f AS DECIMAL)', @@ -1079,19 +1084,9 @@ function hook_delete($node) { * @ingroup node_api_hooks */ function hook_prepare($node) { - $file = file_save_upload($field_name, _image_filename($file->filename, NULL, TRUE)); - if ($file) { - if (!image_get_info($file->uri)) { - form_set_error($field_name, t('Uploaded file is not a valid image')); - return; - } + if (!isset($node->mymodule_value)) { + $node->mymodule_value = 'foo'; } - else { - return; - } - $node->images['_original'] = $file->uri; - _image_build_derivatives($node, TRUE); - $node->new_file = TRUE; } /** @@ -1299,6 +1294,10 @@ function hook_validate($node, $form, &$form_state) { * The node to be displayed, as returned by node_load(). * @param $view_mode * View mode, e.g. 'full', 'teaser', ... + * @param $langcode + * (optional) A language code to use for rendering. Defaults to the global + * content language of the current request. + * * @return * The passed $node parameter should be modified as necessary and returned so * it can be properly presented. Nodes are prepared for display by assembling @@ -1312,7 +1311,7 @@ function hook_validate($node, $form, &$form_state) { * * @ingroup node_api_hooks */ -function hook_view($node, $view_mode) { +function hook_view($node, $view_mode, $langcode = NULL) { if ($view_mode == 'full' && node_is_page($node)) { $breadcrumb = array(); $breadcrumb[] = l(t('Home'), NULL); diff --git a/modules/node/node.info b/modules/node/node.info index 964ec402..a8c61fb3 100644 --- a/modules/node/node.info +++ b/modules/node/node.info @@ -9,8 +9,8 @@ required = TRUE configure = admin/structure/types stylesheets[all][] = node.css -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/node/node.install b/modules/node/node.install index 76c2aec2..3c4e7c2a 100644 --- a/modules/node/node.install +++ b/modules/node/node.install @@ -410,6 +410,7 @@ function node_schema() { 'nid' => array( 'description' => 'The {node}.nid that was read.', 'type' => 'int', + 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0, ), @@ -933,6 +934,33 @@ function node_update_7014() { db_add_index('node', 'language', array('language')); } +/** + * Enable node types that may have been erroneously disabled in Drupal 7.36. + */ +function node_update_7015() { + db_update('node_type') + ->fields(array('disabled' => 0)) + ->condition('base', 'node_content') + ->execute(); +} + +/** + * Change {history}.nid to an unsigned int in order to match {node}.nid. + */ +function node_update_7016() { + db_drop_primary_key('history'); + db_drop_index('history', 'nid'); + db_change_field('history', 'nid', 'nid', array( + 'description' => 'The {node}.nid that was read.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + )); + db_add_primary_key('history', array('uid', 'nid')); + db_add_index('history', 'nid', array('nid')); +} + /** * @} End of "addtogroup updates-7.x-extra". */ diff --git a/modules/node/node.module b/modules/node/node.module index dbb1a650..1d88834c 100644 --- a/modules/node/node.module +++ b/modules/node/node.module @@ -506,7 +506,8 @@ function node_type_load($name) { * - custom: TRUE or FALSE indicating whether this type is defined by a module * (FALSE) or by a user (TRUE) via Add Content Type. * - modified: TRUE or FALSE indicating whether this type has been modified by - * an administrator. Currently not used in any way. + * an administrator. When modifying an existing node type, set to TRUE, or + * the change will be ignored on node_types_rebuild(). * - locked: TRUE or FALSE indicating whether the administrator can change the * machine name of this type. * - disabled: TRUE or FALSE indicating whether this type has been disabled. @@ -1580,9 +1581,7 @@ function node_permission() { ), 'access content overview' => array( 'title' => t('Access the content overview page'), - 'description' => user_access('access content overview') - ? t('Get an overview of all content.', array('@url' => url('admin/content'))) - : t('Get an overview of all content.'), + 'description' => t('Get an overview of all content.', array('@url' => url('admin/content'))), ), 'access content' => array( 'title' => t('View published content'), @@ -1610,7 +1609,7 @@ function node_permission() { } /** - * Gathers the rankings from the the hook_ranking() implementations. + * Gathers the rankings from the hook_ranking() implementations. * * @param $query * A query object that has been extended with the Search DB Extender. @@ -2954,7 +2953,10 @@ function node_search_validate($form, &$form_state) { * system. When adding a node listing to your module, be sure to use a dynamic * query created by db_select() and add a tag of "node_access". This will allow * modules dealing with node access to ensure only nodes to which the user has - * access are retrieved, through the use of hook_query_TAG_alter(). + * access are retrieved, through the use of hook_query_TAG_alter(). Tagging a + * query with "node_access" does not check the published/unpublished status of + * nodes, so the base query is responsible for ensuring that unpublished nodes + * are not displayed to inappropriate users. * * Note: Even a single module returning NODE_ACCESS_DENY from hook_node_access() * will block access to the node. Therefore, implementers should take care to @@ -3670,6 +3672,8 @@ function node_access_rebuild($batch_mode = FALSE) { } /** + * Implements callback_batch_operation(). + * * Performs batch operation for node_access_rebuild(). * * This is a multistep operation: we go through all nodes by packs of 20. The @@ -3684,7 +3688,7 @@ function _node_access_rebuild_batch_operation(&$context) { // Initiate multistep processing. $context['sandbox']['progress'] = 0; $context['sandbox']['current_node'] = 0; - $context['sandbox']['max'] = db_query('SELECT COUNT(DISTINCT nid) FROM {node}')->fetchField(); + $context['sandbox']['max'] = db_query('SELECT COUNT(nid) FROM {node}')->fetchField(); } // Process the next 20 nodes. @@ -3708,6 +3712,8 @@ function _node_access_rebuild_batch_operation(&$context) { } /** + * Implements callback_batch_finished(). + * * Performs post-processing for node_access_rebuild(). * * @param bool $success diff --git a/modules/node/node.pages.inc b/modules/node/node.pages.inc index 62674636..72b0ea7c 100644 --- a/modules/node/node.pages.inc +++ b/modules/node/node.pages.inc @@ -371,35 +371,37 @@ function node_form_build_preview($form, &$form_state) { * @see node_form_build_preview() */ function node_preview($node) { - if (node_access('create', $node) || node_access('update', $node)) { - _field_invoke_multiple('load', 'node', array($node->nid => $node)); + // Clone the node before previewing it to prevent the node itself from being + // modified. + $cloned_node = clone $node; + if (node_access('create', $cloned_node) || node_access('update', $cloned_node)) { + _field_invoke_multiple('load', 'node', array($cloned_node->nid => $cloned_node)); // Load the user's name when needed. - if (isset($node->name)) { + if (isset($cloned_node->name)) { // The use of isset() is mandatory in the context of user IDs, because // user ID 0 denotes the anonymous user. - if ($user = user_load_by_name($node->name)) { - $node->uid = $user->uid; - $node->picture = $user->picture; + if ($user = user_load_by_name($cloned_node->name)) { + $cloned_node->uid = $user->uid; + $cloned_node->picture = $user->picture; } else { - $node->uid = 0; // anonymous user + $cloned_node->uid = 0; // anonymous user } } - elseif ($node->uid) { - $user = user_load($node->uid); - $node->name = $user->name; - $node->picture = $user->picture; + elseif ($cloned_node->uid) { + $user = user_load($cloned_node->uid); + $cloned_node->name = $user->name; + $cloned_node->picture = $user->picture; } - $node->changed = REQUEST_TIME; - $nodes = array($node->nid => $node); - field_attach_prepare_view('node', $nodes, 'full'); + $cloned_node->changed = REQUEST_TIME; + $nodes = array($cloned_node->nid => $cloned_node); // Display a preview of the node. if (!form_get_errors()) { - $node->in_preview = TRUE; - $output = theme('node_preview', array('node' => $node)); - unset($node->in_preview); + $cloned_node->in_preview = TRUE; + $output = theme('node_preview', array('node' => $cloned_node)); + unset($cloned_node->in_preview); } drupal_set_title(t('Preview'), PASS_THROUGH); diff --git a/modules/node/node.test b/modules/node/node.test index 5c9118eb..e8eb459e 100644 --- a/modules/node/node.test +++ b/modules/node/node.test @@ -457,10 +457,70 @@ class PagePreviewTestCase extends DrupalWebTestCase { } function setUp() { - parent::setUp(); + parent::setUp(array('taxonomy', 'node')); $web_user = $this->drupalCreateUser(array('edit own page content', 'create page content')); $this->drupalLogin($web_user); + + // Add a vocabulary so we can test different view modes. + $vocabulary = (object) array( + 'name' => $this->randomName(), + 'description' => $this->randomName(), + 'machine_name' => drupal_strtolower($this->randomName()), + 'help' => '', + 'nodes' => array('page' => 'page'), + ); + taxonomy_vocabulary_save($vocabulary); + + $this->vocabulary = $vocabulary; + + // Add a term to the vocabulary. + $term = (object) array( + 'name' => $this->randomName(), + 'description' => $this->randomName(), + // Use the first available text format. + 'format' => db_query_range('SELECT format FROM {filter_format}', 0, 1)->fetchField(), + 'vid' => $this->vocabulary->vid, + 'vocabulary_machine_name' => $vocabulary->machine_name, + ); + taxonomy_term_save($term); + + $this->term = $term; + + // Set up a field and instance. + $this->field_name = drupal_strtolower($this->randomName()); + $this->field = array( + 'field_name' => $this->field_name, + 'type' => 'taxonomy_term_reference', + 'settings' => array( + 'allowed_values' => array( + array( + 'vocabulary' => $this->vocabulary->machine_name, + 'parent' => '0', + ), + ), + ) + ); + + field_create_field($this->field); + $this->instance = array( + 'field_name' => $this->field_name, + 'entity_type' => 'node', + 'bundle' => 'page', + 'widget' => array( + 'type' => 'options_select', + ), + // Hide on full display but render on teaser. + 'display' => array( + 'default' => array( + 'type' => 'hidden', + ), + 'teaser' => array( + 'type' => 'taxonomy_term_reference_link', + ), + ), + ); + field_create_instance($this->instance); } /** @@ -470,21 +530,26 @@ class PagePreviewTestCase extends DrupalWebTestCase { $langcode = LANGUAGE_NONE; $title_key = "title"; $body_key = "body[$langcode][0][value]"; + $term_key = "{$this->field_name}[$langcode]"; // Fill in node creation form and preview node. $edit = array(); $edit[$title_key] = $this->randomName(8); $edit[$body_key] = $this->randomName(16); + $edit[$term_key] = $this->term->tid; $this->drupalPost('node/add/page', $edit, t('Preview')); - // Check that the preview is displaying the title and body. + // Check that the preview is displaying the title, body, and term. $this->assertTitle(t('Preview | Drupal'), 'Basic page title is preview.'); $this->assertText($edit[$title_key], 'Title displayed.'); $this->assertText($edit[$body_key], 'Body displayed.'); + $this->assertText($this->term->name, 'Term displayed.'); - // Check that the title and body fields are displayed with the correct values. + // Check that the title, body, and term fields are displayed with the + // correct values. $this->assertFieldByName($title_key, $edit[$title_key], 'Title field displayed.'); $this->assertFieldByName($body_key, $edit[$body_key], 'Body field displayed.'); + $this->assertFieldByName($term_key, $edit[$term_key], 'Term field displayed.'); } /** @@ -494,6 +559,7 @@ class PagePreviewTestCase extends DrupalWebTestCase { $langcode = LANGUAGE_NONE; $title_key = "title"; $body_key = "body[$langcode][0][value]"; + $term_key = "{$this->field_name}[$langcode]"; // Force revision on "Basic page" content. variable_set('node_options_page', array('status', 'revision')); @@ -501,17 +567,21 @@ class PagePreviewTestCase extends DrupalWebTestCase { $edit = array(); $edit[$title_key] = $this->randomName(8); $edit[$body_key] = $this->randomName(16); + $edit[$term_key] = $this->term->tid; $edit['log'] = $this->randomName(32); $this->drupalPost('node/add/page', $edit, t('Preview')); - // Check that the preview is displaying the title and body. + // Check that the preview is displaying the title, body, and term. $this->assertTitle(t('Preview | Drupal'), 'Basic page title is preview.'); $this->assertText($edit[$title_key], 'Title displayed.'); $this->assertText($edit[$body_key], 'Body displayed.'); + $this->assertText($this->term->name, 'Term displayed.'); - // Check that the title and body fields are displayed with the correct values. + // Check that the title, body, and term fields are displayed with the + // correct values. $this->assertFieldByName($title_key, $edit[$title_key], 'Title field displayed.'); $this->assertFieldByName($body_key, $edit[$body_key], 'Body field displayed.'); + $this->assertFieldByName($term_key, $edit[$term_key], 'Term field displayed.'); // Check that the log field has the correct value. $this->assertFieldByName('log', $edit['log'], 'Log field displayed.'); @@ -1448,7 +1518,7 @@ class NodeTypeTestCase extends DrupalWebTestCase { * Tests editing a node type using the UI. */ function testNodeTypeEditing() { - $web_user = $this->drupalCreateUser(array('bypass node access', 'administer content types')); + $web_user = $this->drupalCreateUser(array('bypass node access', 'administer content types', 'administer fields')); $this->drupalLogin($web_user); $instance = field_info_instance('node', 'body', 'page'); @@ -2698,8 +2768,8 @@ class NodeAccessFieldTestCase extends NodeWebTestCase { node_access_rebuild(); // Create some users. - $this->admin_user = $this->drupalCreateUser(array('access content', 'bypass node access')); - $this->content_admin_user = $this->drupalCreateUser(array('access content', 'administer content types')); + $this->admin_user = $this->drupalCreateUser(array('access content', 'bypass node access', 'administer fields')); + $this->content_admin_user = $this->drupalCreateUser(array('access content', 'administer content types', 'administer fields')); // Add a custom field to the page content type. $this->field_name = drupal_strtolower($this->randomName() . '_field_name'); @@ -2916,3 +2986,36 @@ class NodePageCacheTest extends NodeWebTestCase { $this->assertResponse(404); } } + +/** + * Tests that multi-byte UTF-8 characters are stored and retrieved correctly. + */ +class NodeMultiByteUtf8Test extends NodeWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Multi-byte UTF-8', + 'description' => 'Test that multi-byte UTF-8 characters are stored and retrieved correctly.', + 'group' => 'Node', + ); + } + + /** + * Tests that multi-byte UTF-8 characters are stored and retrieved correctly. + */ + public function testMultiByteUtf8() { + $connection = Database::getConnection(); + // On MySQL, this test will only run if 'charset' is set to 'utf8mb4' in + // settings.php. + if (!($connection->utf8mb4IsSupported() && $connection->utf8mb4IsActive())) { + return; + } + $title = '🐙'; + $this->assertTrue(drupal_strlen($title, 'utf-8') < strlen($title), 'Title has multi-byte characters.'); + $node = $this->drupalCreateNode(array('title' => $title)); + $this->drupalGet('node/' . $node->nid); + $result = $this->xpath('//h1[@id="page-title"]'); + $this->assertEqual(trim((string) $result[0]), $title, 'The passed title was returned.'); + } + +} diff --git a/modules/node/tests/node_access_test.info b/modules/node/tests/node_access_test.info index 17946efd..7ba4455b 100644 --- a/modules/node/tests/node_access_test.info +++ b/modules/node/tests/node_access_test.info @@ -5,8 +5,8 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/node/tests/node_access_test.module b/modules/node/tests/node_access_test.module index ec35c41f..7932c552 100644 --- a/modules/node/tests/node_access_test.module +++ b/modules/node/tests/node_access_test.module @@ -211,7 +211,7 @@ function node_access_test_node_insert($node) { } /** - * Implements hook_nodeapi_update(). + * Implements hook_node_update(). */ function node_access_test_node_update($node) { _node_access_test_node_write($node); diff --git a/modules/node/tests/node_test.info b/modules/node/tests/node_test.info index b9b946e8..67de1b36 100644 --- a/modules/node/tests/node_test.info +++ b/modules/node/tests/node_test.info @@ -5,8 +5,8 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/node/tests/node_test_exception.info b/modules/node/tests/node_test_exception.info index 5db96296..387ccde2 100644 --- a/modules/node/tests/node_test_exception.info +++ b/modules/node/tests/node_test_exception.info @@ -5,8 +5,8 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/openid/openid.info b/modules/openid/openid.info index 1b97354b..f1d0a5a5 100644 --- a/modules/openid/openid.info +++ b/modules/openid/openid.info @@ -5,8 +5,8 @@ package = Core core = 7.x files[] = openid.test -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/openid/openid.module b/modules/openid/openid.module index a28f452a..a52dbc3d 100644 --- a/modules/openid/openid.module +++ b/modules/openid/openid.module @@ -365,14 +365,20 @@ function openid_complete($response = array()) { // to the OpenID Provider, we need to do discovery on the returned // identififer to make sure that the provider is authorized to // respond on behalf of this. - if ($response_claimed_id != $claimed_id) { + if ($response_claimed_id != $claimed_id || $response_claimed_id != $response['openid.identity']) { $discovery = openid_discovery($response['openid.claimed_id']); + $uris = array(); if ($discovery && !empty($discovery['services'])) { - $uris = array(); foreach ($discovery['services'] as $discovered_service) { - if (in_array('http://specs.openid.net/auth/2.0/server', $discovered_service['types']) || in_array('http://specs.openid.net/auth/2.0/signon', $discovered_service['types'])) { - $uris[] = $discovered_service['uri']; + if (!in_array('http://specs.openid.net/auth/2.0/server', $discovered_service['types']) && !in_array('http://specs.openid.net/auth/2.0/signon', $discovered_service['types'])) { + continue; } + // The OP-Local Identifier (if different than the Claimed + // Identifier) must be present in the XRDS document. + if ($response_claimed_id != $response['openid.identity'] && (!isset($discovered_service['identity']) || $discovered_service['identity'] != $response['openid.identity'])) { + continue; + } + $uris[] = $discovered_service['uri']; } } if (!in_array($service['uri'], $uris)) { diff --git a/modules/openid/openid.test b/modules/openid/openid.test index 41af3f82..d0708e03 100644 --- a/modules/openid/openid.test +++ b/modules/openid/openid.test @@ -94,7 +94,7 @@ class OpenIDFunctionalTestCase extends OpenIDWebTestCase { $identity = url('openid-test/yadis/xrds/dummy-user', array('absolute' => TRUE, 'fragment' => $this->randomName())); // Tell openid_test.module to respond with this identifier. If the fragment // part is present in the identifier, it should be retained. - variable_set('openid_test_response', array('openid.claimed_id' => $identity)); + variable_set('openid_test_response', array('openid.claimed_id' => $identity, 'openid.identity' => openid_normalize($identity))); $this->addIdentity(url('openid-test/yadis/xrds/server', array('absolute' => TRUE)), 2, 'http://specs.openid.net/auth/2.0/identifier_select', $identity); variable_set('openid_test_response', array()); @@ -680,11 +680,11 @@ class OpenIDTestCase extends DrupalWebTestCase { * Test _openid_dh_XXX_to_XXX() functions. */ function testConversion() { - $this->assertEqual(_openid_dh_long_to_base64('12345678901234567890123456789012345678901234567890'), 'CHJ/Y2mq+DyhUCZ0evjH8ZbOPwrS', '_openid_dh_long_to_base64() returned expected result.'); - $this->assertEqual(_openid_dh_base64_to_long('BsH/g8Nrpn2dtBSdu/sr1y8hxwyx'), '09876543210987654321098765432109876543210987654321', '_openid_dh_base64_to_long() returned expected result.'); + $this->assertIdentical(_openid_dh_long_to_base64('12345678901234567890123456789012345678901234567890'), 'CHJ/Y2mq+DyhUCZ0evjH8ZbOPwrS', '_openid_dh_long_to_base64() returned expected result.'); + $this->assertIdentical(_openid_dh_base64_to_long('BsH/g8Nrpn2dtBSdu/sr1y8hxwyx'), '9876543210987654321098765432109876543210987654321', '_openid_dh_base64_to_long() returned expected result.'); - $this->assertEqual(_openid_dh_long_to_binary('12345678901234567890123456789012345678901234567890'), "\x08r\x7fci\xaa\xf8<\xa1P&tz\xf8\xc7\xf1\x96\xce?\x0a\xd2", '_openid_dh_long_to_binary() returned expected result.'); - $this->assertEqual(_openid_dh_binary_to_long("\x06\xc1\xff\x83\xc3k\xa6}\x9d\xb4\x14\x9d\xbb\xfb+\xd7/!\xc7\x0c\xb1"), '09876543210987654321098765432109876543210987654321', '_openid_dh_binary_to_long() returned expected result.'); + $this->assertIdentical(_openid_dh_long_to_binary('12345678901234567890123456789012345678901234567890'), "\x08r\x7fci\xaa\xf8<\xa1P&tz\xf8\xc7\xf1\x96\xce?\x0a\xd2", '_openid_dh_long_to_binary() returned expected result.'); + $this->assertIdentical(_openid_dh_binary_to_long("\x06\xc1\xff\x83\xc3k\xa6}\x9d\xb4\x14\x9d\xbb\xfb+\xd7/!\xc7\x0c\xb1"), '9876543210987654321098765432109876543210987654321', '_openid_dh_binary_to_long() returned expected result.'); } /** diff --git a/modules/openid/tests/openid_test.info b/modules/openid/tests/openid_test.info index a33d5963..48520267 100644 --- a/modules/openid/tests/openid_test.info +++ b/modules/openid/tests/openid_test.info @@ -6,8 +6,8 @@ core = 7.x dependencies[] = openid hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/openid/tests/openid_test.module b/modules/openid/tests/openid_test.module index bcf9f425..3d6e2926 100644 --- a/modules/openid/tests/openid_test.module +++ b/modules/openid/tests/openid_test.module @@ -150,6 +150,7 @@ function openid_test_yadis_xrds() { http://specs.openid.net/auth/2.0/server ' . url('openid-test/endpoint', array('absolute' => TRUE)) . ' + ' . url('openid-test/yadis/xrds/server', array('absolute' => TRUE)) . ' '; } elseif (arg(3) == 'delegate') { diff --git a/modules/overlay/overlay-parent.js b/modules/overlay/overlay-parent.js index 7452a515..efb26370 100644 --- a/modules/overlay/overlay-parent.js +++ b/modules/overlay/overlay-parent.js @@ -350,7 +350,7 @@ Drupal.overlay.setFocusBefore = function ($element, document) { * TRUE if the URL represents an administrative link, FALSE otherwise. */ Drupal.overlay.isAdminLink = function (url) { - if (Drupal.overlay.isExternalLink(url)) { + if (!Drupal.urlIsLocal(url)) { return false; } @@ -378,6 +378,8 @@ Drupal.overlay.isAdminLink = function (url) { /** * Determine whether a link is external to the site. * + * Deprecated. Use Drupal.urlIsLocal() instead. + * * @param url * The URL to be tested. * @@ -385,8 +387,28 @@ Drupal.overlay.isAdminLink = function (url) { * TRUE if the URL is external to the site, FALSE otherwise. */ Drupal.overlay.isExternalLink = function (url) { - var re = RegExp('^((f|ht)tps?:)?//(?!' + window.location.host + ')'); - return re.test(url); + return !Drupal.urlIsLocal(url); +}; + +/** + * Constructs an internal URL (relative to this site) from the provided path. + * + * For example, if the provided path is 'admin' and the site is installed at + * http://example.com/drupal, this function will return '/drupal/admin'. + * + * @param path + * The internal path, without any leading slash. + * + * @return + * The internal URL derived from the provided path, or null if a valid + * internal path cannot be constructed (for example, if an attempt to create + * an external link is detected). + */ +Drupal.overlay.getInternalUrl = function (path) { + var url = Drupal.settings.basePath + path; + if (Drupal.urlIsLocal(url)) { + return url; + } }; /** @@ -577,7 +599,7 @@ Drupal.overlay.eventhandlerOverrideLink = function (event) { // If the link contains the overlay-restore class and the overlay-context // state is set, also update the parent window's location. var parentLocation = ($target.hasClass('overlay-restore') && typeof $.bbq.getState('overlay-context') == 'string') - ? Drupal.settings.basePath + $.bbq.getState('overlay-context') + ? this.getInternalUrl($.bbq.getState('overlay-context')) : null; href = this.fragmentizeLink($target.get(0), parentLocation); // Only override default behavior when left-clicking and user is not @@ -657,11 +679,15 @@ Drupal.overlay.eventhandlerOperateByURLFragment = function (event) { } // Get the overlay URL from the current URL fragment. + var internalUrl = null; var state = $.bbq.getState('overlay'); if (state) { + internalUrl = this.getInternalUrl(state); + } + if (internalUrl) { // Append render variable, so the server side can choose the right // rendering and add child frame code to the page if needed. - var url = $.param.querystring(Drupal.settings.basePath + state, { render: 'overlay' }); + var url = $.param.querystring(internalUrl, { render: 'overlay' }); this.open(url); this.resetActiveClass(this.getPath(Drupal.settings.basePath + state)); diff --git a/modules/overlay/overlay.info b/modules/overlay/overlay.info index 269e323e..dd1f3f4b 100644 --- a/modules/overlay/overlay.info +++ b/modules/overlay/overlay.info @@ -4,8 +4,8 @@ package = Core version = VERSION core = 7.x -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/overlay/overlay.module b/modules/overlay/overlay.module index 7b2fc939..7e54734e 100644 --- a/modules/overlay/overlay.module +++ b/modules/overlay/overlay.module @@ -78,6 +78,20 @@ function overlay_theme() { ); } +/** + * Implements hook_form_alter(). + */ +function overlay_form_alter(&$form, &$form_state) { + // Add a hidden element to prevent dropping out of the overlay when a form is + // submitted inside the overlay using a GET method. + if (isset($form['#method']) && $form['#method'] == 'get' && isset($_REQUEST['render']) && $_REQUEST['render'] == 'overlay' && !isset($form['render'])) { + $form['render'] = array( + '#type' => 'hidden', + '#value' => 'overlay', + ); + } +} + /** * Implements hook_form_FORM_ID_alter(). */ diff --git a/modules/path/path.info b/modules/path/path.info index 3445b7c6..f17fdaf1 100644 --- a/modules/path/path.info +++ b/modules/path/path.info @@ -6,8 +6,8 @@ core = 7.x files[] = path.test configure = admin/config/search/path -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/path/path.module b/modules/path/path.module index 81c7bb2c..4614b0fa 100644 --- a/modules/path/path.module +++ b/modules/path/path.module @@ -185,7 +185,7 @@ function path_form_element_validate($element, &$form_state, $complete_form) { * Implements hook_node_insert(). */ function path_node_insert($node) { - if (isset($node->path)) { + if (isset($node->path) && isset($node->path['alias'])) { $path = $node->path; $path['alias'] = trim($path['alias']); // Only save a non-empty alias. @@ -205,9 +205,9 @@ function path_node_insert($node) { function path_node_update($node) { if (isset($node->path)) { $path = $node->path; - $path['alias'] = trim($path['alias']); + $path['alias'] = isset($path['alias']) ? trim($path['alias']) : ''; // Delete old alias if user erased it. - if (!empty($path['pid']) && empty($path['alias'])) { + if (!empty($path['pid']) && !$path['alias']) { path_delete($path['pid']); } path_node_insert($node); diff --git a/modules/php/php.info b/modules/php/php.info index 1fcb89d5..0247be60 100644 --- a/modules/php/php.info +++ b/modules/php/php.info @@ -5,8 +5,8 @@ version = VERSION core = 7.x files[] = php.test -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/poll/poll.info b/modules/poll/poll.info index 101b99e2..c4734261 100644 --- a/modules/poll/poll.info +++ b/modules/poll/poll.info @@ -6,8 +6,8 @@ core = 7.x files[] = poll.test stylesheets[all][] = poll.css -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/poll/poll.module b/modules/poll/poll.module index d3d64b17..336e4456 100644 --- a/modules/poll/poll.module +++ b/modules/poll/poll.module @@ -191,7 +191,6 @@ function poll_node_info() { 'base' => 'poll', 'description' => t('A poll is a question with a set of possible responses. A poll, once created, automatically provides a simple running count of the number of votes received for each response.'), 'title_label' => t('Question'), - 'has_body' => FALSE, ) ); } @@ -632,9 +631,6 @@ function poll_delete($node) { * The node object to load. */ function poll_block_latest_poll_view($node) { - global $user; - $output = ''; - // This is necessary for shared objects because PHP doesn't copy objects, but // passes them by reference. So when the objects are cached it can result in // the wrong output being displayed on subsequent calls. The cloning and @@ -675,9 +671,6 @@ function poll_block_latest_poll_view($node) { * Implements hook_view(). */ function poll_view($node, $view_mode) { - global $user; - $output = ''; - if (!empty($node->allowvotes) && empty($node->show_results)) { $node->content['poll_view_voting'] = drupal_get_form('poll_view_voting', $node); } @@ -695,7 +688,7 @@ function poll_view($node, $view_mode) { function poll_teaser($node) { $teaser = NULL; if (is_array($node->choice)) { - foreach ($node->choice as $k => $choice) { + foreach ($node->choice as $choice) { if ($choice['chtext'] != '') { $teaser .= '* ' . check_plain($choice['chtext']) . "\n"; } diff --git a/modules/profile/profile.info b/modules/profile/profile.info index 593be3ee..dfa174c5 100644 --- a/modules/profile/profile.info +++ b/modules/profile/profile.info @@ -11,8 +11,8 @@ configure = admin/config/people/profile ; See user_system_info_alter(). hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/profile/profile.test b/modules/profile/profile.test index 42a1a42d..18924714 100644 --- a/modules/profile/profile.test +++ b/modules/profile/profile.test @@ -339,12 +339,22 @@ class ProfileTestAutocomplete extends ProfileTestCase { $this->setProfileField($field, $field['value']); // Set some html for what we want to see in the page output later. - $autocomplete_html = ''; - $field_html = ''; + // Autocomplete always uses non-clean URLs. + $current_clean_url = isset($GLOBALS['conf']['clean_url']) ? $GLOBALS['conf']['clean_url'] : NULL; + $GLOBALS['conf']['clean_url'] = 0; + $autocomplete_url = url('profile/autocomplete/' . $field['fid'], array('absolute' => TRUE, 'script' => 'index.php')); + $GLOBALS['conf']['clean_url'] = $current_clean_url; + $autocomplete_id = drupal_html_id('edit-' . $field['form_name'] . '-autocomplete'); + $autocomplete_html = ''; // Check that autocompletion html is found on the user's profile edit page. $this->drupalGet('user/' . $this->admin_user->uid . '/edit/' . $category); $this->assertRaw($autocomplete_html, 'Autocomplete found.'); + $this->assertFieldByXPath( + '//input[@type="text" and @name="' . $field['form_name'] . '" and contains(@class, "form-autocomplete")]', + '', + 'Text input field found' + ); $this->assertRaw('misc/autocomplete.js', 'Autocomplete JavaScript found.'); $this->assertRaw('class="form-text form-autocomplete"', 'Autocomplete form element class found.'); diff --git a/modules/rdf/rdf.info b/modules/rdf/rdf.info index e5c963cd..44f04bd0 100644 --- a/modules/rdf/rdf.info +++ b/modules/rdf/rdf.info @@ -5,8 +5,8 @@ version = VERSION core = 7.x files[] = rdf.test -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/rdf/tests/rdf_test.info b/modules/rdf/tests/rdf_test.info index 7602a942..8195c999 100644 --- a/modules/rdf/tests/rdf_test.info +++ b/modules/rdf/tests/rdf_test.info @@ -4,9 +4,10 @@ package = Testing version = VERSION core = 7.x hidden = TRUE +dependencies[] = blog -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/search/search.api.php b/modules/search/search.api.php index 62d53b85..8c17bb4f 100644 --- a/modules/search/search.api.php +++ b/modules/search/search.api.php @@ -30,8 +30,9 @@ * * @return * Array with optional keys: - * - title: Title for the tab on the search page for this module. Defaults - * to the module name if not given. + * - title: Title for the tab on the search page for this module. Title must + * be untranslated. Outside of this return array, pass the title through the + * t() function to register it as a translatable string. * - path: Path component after 'search/' for searching with this module. * Defaults to the module name if not given. * - conditions_callback: An implementation of callback_search_conditions(). @@ -39,6 +40,9 @@ * @ingroup search */ function hook_search_info() { + // Make the title translatable. + t('Content'); + return array( 'title' => 'Content', 'path' => 'node', diff --git a/modules/search/search.extender.inc b/modules/search/search.extender.inc index 67094666..40742569 100644 --- a/modules/search/search.extender.inc +++ b/modules/search/search.extender.inc @@ -149,6 +149,17 @@ class SearchQuery extends SelectQueryExtender { $this->searchExpression = $expression; $this->type = $module; + // Add a search_* tag. This needs to be added before any preExecute methods + // for decorated queries are called, as $this->prepared will be set to TRUE + // and tags added in the execute method will never get used. For example, + // if $query is extended by 'SearchQuery' then 'PagerDefault', the + // search-specific tag will be added too late (when preExecute() has + // already been called from the PagerDefault extender), and as a + // consequence will not be available to hook_query_alter() implementations, + // nor will the correct hook_query_TAG_alter() implementations get invoked. + // See node_search_execute(). + $this->addTag('search_' . $module); + return $this; } @@ -398,10 +409,10 @@ class SearchQuery extends SelectQueryExtender { * used. However, if at least one call to addScore() has taken place, the * keyword relevance score is not automatically added. * - * Also note that if you call orderBy() directly on the query, search scores - * will not automatically be used to order search results. Your orderBy() - * expression can reference 'calculated_score', which will be the total - * calculated score value. + * Note that you must use this method to add ordering to your searches, and + * not call orderBy() directly, when using the SearchQuery extender. This is + * because of the two-pass system the SearchQuery class uses to normalize + * scores. * * @param $score * The score expression, which should evaluate to a number between 0 and 1. @@ -494,9 +505,8 @@ class SearchQuery extends SelectQueryExtender { $this->orderBy('calculated_score', 'DESC'); } - // Add tag and useful metadata. + // Add useful metadata. $this - ->addTag('search_' . $this->type) ->addMetaData('normalize', $this->normalize) ->fields('i', array('type', 'sid')); diff --git a/modules/search/search.info b/modules/search/search.info index 0a1bcc2d..a092ea9e 100644 --- a/modules/search/search.info +++ b/modules/search/search.info @@ -8,8 +8,8 @@ files[] = search.test configure = admin/config/search/settings stylesheets[all][] = search.css -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/search/search.pages.inc b/modules/search/search.pages.inc index 9dd00a6d..2123dd75 100644 --- a/modules/search/search.pages.inc +++ b/modules/search/search.pages.inc @@ -49,7 +49,7 @@ function search_view($module = NULL, $keys = '') { // which will get us back to this page callback. In other words, the search // form submits with POST but redirects to GET. This way we can keep // the search query URL clean as a whistle. - if (empty($_POST['form_id']) || $_POST['form_id'] != 'search_form') { + if (empty($_POST['form_id']) || ($_POST['form_id'] != 'search_form' && $_POST['form_id'] != 'search_block_form')) { $conditions = NULL; if (isset($info['conditions_callback']) && function_exists($info['conditions_callback'])) { // Build an optional array of more search conditions. diff --git a/modules/search/search.test b/modules/search/search.test index 19f4e551..913d1989 100644 --- a/modules/search/search.test +++ b/modules/search/search.test @@ -666,6 +666,24 @@ class SearchBlockTestCase extends DrupalWebTestCase { url('search/node/', array('absolute' => TRUE)), 'Redirected to correct url.' ); + + // Test that after entering a too-short keyword in the form, you can then + // search again with a longer keyword. First test using the block form. + $terms = array('search_block_form' => 'a'); + $this->drupalPost('node', $terms, t('Search')); + $this->assertText('You must include at least one positive keyword with 3 characters or more'); + $terms = array('search_block_form' => 'foo'); + $this->drupalPost(NULL, $terms, t('Search')); + $this->assertNoText('You must include at least one positive keyword with 3 characters or more'); + $this->assertText('Your search yielded no results'); + + // Same test again, using the search page form for the second search this time. + $terms = array('search_block_form' => 'a'); + $this->drupalPost('node', $terms, t('Search')); + $terms = array('keys' => 'foo'); + $this->drupalPost(NULL, $terms, t('Search')); + $this->assertNoText('You must include at least one positive keyword with 3 characters or more'); + $this->assertText('Your search yielded no results'); } } @@ -2029,10 +2047,11 @@ class SearchNodeAccessTest extends DrupalWebTestCase { } /** - * Tests that search returns results with punctuation in the search phrase. + * Tests that search works with punctuation and HTML entities. */ function testPhraseSearchPunctuation() { $node = $this->drupalCreateNode(array('body' => array(LANGUAGE_NONE => array(array('value' => "The bunny's ears were fuzzy."))))); + $node2 = $this->drupalCreateNode(array('body' => array(LANGUAGE_NONE => array(array('value' => 'Dignissim Aliquam & Quieligo meus natu quae quia te. Damnum© erat— neo pneum. Facilisi feugiat ibidem ratis.'))))); // Update the search index. module_invoke_all('update_index'); @@ -2045,6 +2064,58 @@ class SearchNodeAccessTest extends DrupalWebTestCase { $edit = array('keys' => '"bunny\'s"'); $this->drupalPost('search/node', $edit, t('Search')); $this->assertText($node->title); + + // Search for "&" and verify entities are not broken up in the output. + $edit = array('keys' => '&'); + $this->drupalPost('search/node', $edit, t('Search')); + $this->assertNoRaw('&amp;'); + $this->assertText('You must include at least one positive keyword'); + + $edit = array('keys' => '&'); + $this->drupalPost('search/node', $edit, t('Search')); + $this->assertNoRaw('&amp;'); + $this->assertText('You must include at least one positive keyword'); + } +} + +/** + * Tests node search with query tags. + */ +class SearchNodeTagTest extends DrupalWebTestCase { + public $test_user; + + public static function getInfo() { + return array( + 'name' => 'Node search query tags', + 'description' => 'Tests Node search tags functionality.', + 'group' => 'Search', + ); + } + + function setUp() { + parent::setUp('search', 'search_node_tags'); + node_access_rebuild(); + + // Create a test user and log in. + $this->test_user = $this->drupalCreateUser(array('search content')); + $this->drupalLogin($this->test_user); + } + + /** + * Tests that the correct tags are available and hooks invoked. + */ + function testNodeSearchQueryTags() { + $this->drupalCreateNode(array('body' => array(LANGUAGE_NONE => array(array('value' => 'testing testing testing.'))))); + + // Update the search index. + module_invoke_all('update_index'); + search_update_totals(); + + $edit = array('keys' => 'testing'); + $this->drupalPost('search/node', $edit, t('Search')); + + $this->assertTrue(variable_get('search_node_tags_test_query_tag', FALSE), 'hook_query_alter() was invoked and the query contained the "search_node" tag.'); + $this->assertTrue(variable_get('search_node_tags_test_query_tag_hook', FALSE), 'hook_query_search_node_alter() was invoked.'); } } diff --git a/modules/search/tests/search_embedded_form.info b/modules/search/tests/search_embedded_form.info index 2c789cc3..6423cef5 100644 --- a/modules/search/tests/search_embedded_form.info +++ b/modules/search/tests/search_embedded_form.info @@ -5,8 +5,8 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/search/tests/search_extra_type.info b/modules/search/tests/search_extra_type.info index af8ba670..0ae00a58 100644 --- a/modules/search/tests/search_extra_type.info +++ b/modules/search/tests/search_extra_type.info @@ -5,8 +5,8 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/search/tests/search_node_tags.info b/modules/search/tests/search_node_tags.info new file mode 100644 index 00000000..b2cc66ab --- /dev/null +++ b/modules/search/tests/search_node_tags.info @@ -0,0 +1,12 @@ +name = "Test search node tags" +description = "Support module for Node search tags testing." +package = Testing +version = VERSION +core = 7.x +hidden = TRUE + +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" +project = "drupal" +datestamp = "1481152423" + diff --git a/modules/search/tests/search_node_tags.module b/modules/search/tests/search_node_tags.module new file mode 100644 index 00000000..b66dd9ea --- /dev/null +++ b/modules/search/tests/search_node_tags.module @@ -0,0 +1,23 @@ +hasTag('search_node')) { + variable_set('search_node_tags_test_query_tag', TRUE); + } +} + +/** + * Implements hook_query_TAG_alter(). + */ +function search_node_tags_query_search_node_alter(QueryAlterableInterface $query) { + variable_set('search_node_tags_test_query_tag_hook', TRUE); +} diff --git a/modules/shortcut/shortcut.info b/modules/shortcut/shortcut.info index 2aa55da7..c12f11e4 100644 --- a/modules/shortcut/shortcut.info +++ b/modules/shortcut/shortcut.info @@ -6,8 +6,8 @@ core = 7.x files[] = shortcut.test configure = admin/config/user-interface/shortcut -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/simpletest/drupal_web_test_case.php b/modules/simpletest/drupal_web_test_case.php index 8ecddb50..08452f31 100644 --- a/modules/simpletest/drupal_web_test_case.php +++ b/modules/simpletest/drupal_web_test_case.php @@ -853,6 +853,13 @@ class DrupalWebTestCase extends DrupalTestCase { */ protected $cookieFile = NULL; + /** + * The cookies of the page currently loaded in the internal browser. + * + * @var array + */ + protected $cookies = array(); + /** * Additional cURL options. * @@ -942,7 +949,6 @@ class DrupalWebTestCase extends DrupalTestCase { protected function drupalCreateNode($settings = array()) { // Populate defaults array. $settings += array( - 'body' => array(LANGUAGE_NONE => array(array())), 'title' => $this->randomName(8), 'comment' => 2, 'changed' => REQUEST_TIME, @@ -957,6 +963,12 @@ class DrupalWebTestCase extends DrupalTestCase { 'language' => LANGUAGE_NONE, ); + // Add the body after the language is defined so that it may be set + // properly. + $settings += array( + 'body' => array($settings['language'] => array(array())), + ); + // Use the original node's created time for existing nodes. if (isset($settings['created']) && !isset($settings['date'])) { $settings['date'] = format_date($settings['created'], 'custom', 'Y-m-d H:i:s O'); @@ -1015,9 +1027,7 @@ class DrupalWebTestCase extends DrupalTestCase { 'description' => '', 'help' => '', 'title_label' => 'Title', - 'body_label' => 'Body', 'has_title' => 1, - 'has_body' => 1, ); // Imposed values for a custom type. $forced = array( @@ -1067,7 +1077,7 @@ class DrupalWebTestCase extends DrupalTestCase { $lines = array(16, 256, 1024, 2048, 20480); $count = 0; foreach ($lines as $line) { - simpletest_generate_file('text-' . $count++, 64, $line); + simpletest_generate_file('text-' . $count++, 64, $line, 'text'); } // Copy other test files from simpletest. @@ -1695,8 +1705,10 @@ class DrupalWebTestCase extends DrupalTestCase { $GLOBALS['conf']['language_default'] = $this->originalLanguageDefault; } - // Close the CURL handler. + // Close the CURL handler and reset the cookies array so test classes + // containing multiple tests are not polluted. $this->curlClose(); + $this->cookies = array(); } /** @@ -1769,14 +1781,24 @@ class DrupalWebTestCase extends DrupalTestCase { protected function curlExec($curl_options, $redirect = FALSE) { $this->curlInitialize(); - // cURL incorrectly handles URLs with a fragment by including the - // fragment in the request to the server, causing some web servers - // to reject the request citing "400 - Bad Request". To prevent - // this, we strip the fragment from the request. - // TODO: Remove this for Drupal 8, since fixed in curl 7.20.0. - if (!empty($curl_options[CURLOPT_URL]) && strpos($curl_options[CURLOPT_URL], '#')) { - $original_url = $curl_options[CURLOPT_URL]; - $curl_options[CURLOPT_URL] = strtok($curl_options[CURLOPT_URL], '#'); + if (!empty($curl_options[CURLOPT_URL])) { + // Forward XDebug activation if present. + if (isset($_COOKIE['XDEBUG_SESSION'])) { + $options = drupal_parse_url($curl_options[CURLOPT_URL]); + $options += array('query' => array()); + $options['query'] += array('XDEBUG_SESSION_START' => $_COOKIE['XDEBUG_SESSION']); + $curl_options[CURLOPT_URL] = url($options['path'], $options); + } + + // cURL incorrectly handles URLs with a fragment by including the + // fragment in the request to the server, causing some web servers + // to reject the request citing "400 - Bad Request". To prevent + // this, we strip the fragment from the request. + // TODO: Remove this for Drupal 8, since fixed in curl 7.20.0. + if (strpos($curl_options[CURLOPT_URL], '#')) { + $original_url = $curl_options[CURLOPT_URL]; + $curl_options[CURLOPT_URL] = strtok($curl_options[CURLOPT_URL], '#'); + } } $url = empty($curl_options[CURLOPT_URL]) ? curl_getinfo($this->curlHandle, CURLINFO_EFFECTIVE_URL) : $curl_options[CURLOPT_URL]; @@ -2211,6 +2233,7 @@ class DrupalWebTestCase extends DrupalTestCase { // Submit the POST request. $return = drupal_json_decode($this->drupalPost(NULL, $edit, array('path' => $ajax_path, 'triggering_element' => $triggering_element), $options, $headers, $form_html_id, $extra_post)); + $this->assertIdentical($this->drupalGetHeader('X-Drupal-Ajax-Token'), '1', 'Ajax response header found.'); // Change the page content by applying the returned commands. if (!empty($ajax_settings) && !empty($return)) { @@ -2247,8 +2270,13 @@ class DrupalWebTestCase extends DrupalTestCase { if ($wrapperNode) { // ajax.js adds an enclosing DIV to work around a Safari bug. $newDom = new DOMDocument(); + // DOM can load HTML soup. But, HTML soup can throw warnings, + // suppress them. $newDom->loadHTML('
          ' . $command['data'] . '
          '); - $newNode = $dom->importNode($newDom->documentElement->firstChild->firstChild, TRUE); + // Suppress warnings thrown when duplicate HTML IDs are + // encountered. This probably means we are replacing an element + // with the same ID. + $newNode = @$dom->importNode($newDom->documentElement->firstChild->firstChild, TRUE); $method = isset($command['method']) ? $command['method'] : $ajax_settings['method']; // The "method" is a jQuery DOM manipulation function. Emulate // each one using PHP's DOMNode API. @@ -2570,6 +2598,11 @@ class DrupalWebTestCase extends DrupalTestCase { * * @param $xpath * The xpath string to use in the search. + * @param array $arguments + * An array of arguments with keys in the form ':name' matching the + * placeholders in the query. The values may be either strings or numeric + * values. + * * @return * The return value of the xpath search. For details on the xpath string * format and return values see the SimpleXML documentation, @@ -2741,7 +2774,7 @@ class DrupalWebTestCase extends DrupalTestCase { $path = substr($path, $length); } // Ensure that we have an absolute path. - if ($path[0] !== '/') { + if (empty($path) || $path[0] !== '/') { $path = '/' . $path; } // Finally, prepend the $base_url. diff --git a/modules/simpletest/files/css_test_files/css_input_with_import.css b/modules/simpletest/files/css_test_files/css_input_with_import.css index 87afcb35..484db837 100644 --- a/modules/simpletest/files/css_test_files/css_input_with_import.css +++ b/modules/simpletest/files/css_test_files/css_input_with_import.css @@ -1,5 +1,7 @@ +@import url("http://example.com/style.css"); +@import url("//example.com/style.css"); @import "import1.css"; @import "import2.css"; diff --git a/modules/simpletest/files/css_test_files/css_input_with_import.css.optimized.css b/modules/simpletest/files/css_test_files/css_input_with_import.css.optimized.css index a05f939f..a2af7b36 100644 --- a/modules/simpletest/files/css_test_files/css_input_with_import.css.optimized.css +++ b/modules/simpletest/files/css_test_files/css_input_with_import.css.optimized.css @@ -1,4 +1,4 @@ -ul,select{font:1em/160% Verdana,sans-serif;color:#494949;}.ui-icon{background-image:url(images/icon.png);}.data .double-quote{background-image:url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7");}.data .single-quote{background-image:url('data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDACAWGBwYFCAcGhwkIiAmMFA0MCwsMGJGSjpQdGZ6eHJmcG6AkLicgIiuim5woNqirr7EztDOfJri8uDI8LjKzsb/2wBDASIkJDAqMF40NF7GhHCExsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsb/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAb/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhEDEQA/AKAAH//Z');}.data .no-quote{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACEAAAAEAQAAAAAo/mtHAAAAIElEQVQIHWMRnWHwcRNLN8NZ7QYWwT8PlBlYsgqVBRsAankIMw5MtnoAAAAASUVORK5CYII=);} +@import url("http://example.com/style.css");@import url("//example.com/style.css");ul,select{font:1em/160% Verdana,sans-serif;color:#494949;}.ui-icon{background-image:url(images/icon.png);}.data .double-quote{background-image:url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7");}.data .single-quote{background-image:url('data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDACAWGBwYFCAcGhwkIiAmMFA0MCwsMGJGSjpQdGZ6eHJmcG6AkLicgIiuim5woNqirr7EztDOfJri8uDI8LjKzsb/2wBDASIkJDAqMF40NF7GhHCExsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsb/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAb/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhEDEQA/AKAAH//Z');}.data .no-quote{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACEAAAAEAQAAAAAo/mtHAAAAIElEQVQIHWMRnWHwcRNLN8NZ7QYWwT8PlBlYsgqVBRsAankIMw5MtnoAAAAASUVORK5CYII=);} p,select{font:1em/160% Verdana,sans-serif;color:#494949;} body{margin:0;padding:0;background:#edf5fa;font:76%/170% Verdana,sans-serif;color:#494949;}.this .is .a .test{font:1em/100% Verdana,sans-serif;color:#494949;}.this .is diff --git a/modules/simpletest/files/css_test_files/css_input_with_import.css.unoptimized.css b/modules/simpletest/files/css_test_files/css_input_with_import.css.unoptimized.css index b8c7778d..bc3c7b64 100644 --- a/modules/simpletest/files/css_test_files/css_input_with_import.css.unoptimized.css +++ b/modules/simpletest/files/css_test_files/css_input_with_import.css.unoptimized.css @@ -1,5 +1,7 @@ +@import url("http://example.com/style.css"); +@import url("//example.com/style.css"); ul, select { font: 1em/160% Verdana, sans-serif; diff --git a/modules/simpletest/files/image-test-no-transparency.gif b/modules/simpletest/files/image-test-no-transparency.gif new file mode 100644 index 00000000..15ae7772 Binary files /dev/null and b/modules/simpletest/files/image-test-no-transparency.gif differ diff --git a/modules/simpletest/files/image-test-transparent-out-of-range.gif b/modules/simpletest/files/image-test-transparent-out-of-range.gif new file mode 100644 index 00000000..a54df7a9 Binary files /dev/null and b/modules/simpletest/files/image-test-transparent-out-of-range.gif differ diff --git a/modules/simpletest/simpletest.info b/modules/simpletest/simpletest.info index 4c6fcbe3..d613ff2d 100644 --- a/modules/simpletest/simpletest.info +++ b/modules/simpletest/simpletest.info @@ -11,6 +11,7 @@ configure = admin/config/development/testing/settings files[] = tests/actions.test files[] = tests/ajax.test files[] = tests/batch.test +files[] = tests/boot.test files[] = tests/bootstrap.test files[] = tests/cache.test files[] = tests/common.test @@ -56,8 +57,8 @@ files[] = tests/upgrade/update.trigger.test files[] = tests/upgrade/update.field.test files[] = tests/upgrade/update.user.test -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/simpletest/simpletest.module b/modules/simpletest/simpletest.module index 3103af0e..cf830478 100644 --- a/modules/simpletest/simpletest.module +++ b/modules/simpletest/simpletest.module @@ -154,7 +154,7 @@ function simpletest_run_tests($test_list, $reporter = 'drupal') { } /** - * Batch operation callback. + * Implements callback_batch_operation(). */ function _simpletest_batch_operation($test_list_init, $test_id, &$context) { simpletest_classloader_register(); @@ -205,6 +205,9 @@ function _simpletest_batch_operation($test_list_init, $test_id, &$context) { $context['finished'] = 1 - $size / $max; } +/** + * Implements callback_batch_finished(). + */ function _simpletest_batch_finished($success, $results, $operations, $elapsed) { if ($success) { drupal_set_message(t('The test run finished in @elapsed.', array('@elapsed' => $elapsed))); @@ -328,25 +331,32 @@ function simpletest_test_get_all() { // Also discover PSR-0 test classes, if the PHP version allows it. if (version_compare(PHP_VERSION, '5.3') > 0) { - // Select all PSR-0 classes in the Tests namespace of all modules. + // Select all PSR-0 and PSR-4 classes in the Tests namespace of all + // modules. $system_list = db_query("SELECT name, filename FROM {system}")->fetchAllKeyed(); foreach ($system_list as $name => $filename) { - // Build directory in which the test files would reside. - $tests_dir = DRUPAL_ROOT . '/' . dirname($filename) . '/lib/Drupal/' . $name . '/Tests'; - // Scan it for test files if it exists. - if (is_dir($tests_dir)) { - $files = file_scan_directory($tests_dir, '/.*\.php/'); - if (!empty($files)) { - $basedir = DRUPAL_ROOT . '/' . dirname($filename) . '/lib/'; - foreach ($files as $file) { - // Convert the file name into the namespaced class name. - $replacements = array( - '/' => '\\', - $basedir => '', - '.php' => '', - ); - $classes[] = strtr($file->uri, $replacements); + $module_dir = DRUPAL_ROOT . '/' . dirname($filename); + // Search both the 'lib/Drupal/mymodule' directory (for PSR-0 classes) + // and the 'src' directory (for PSR-4 classes). + foreach(array('lib/Drupal/' . $name, 'src') as $subdir) { + // Build directory in which the test files would reside. + $tests_dir = $module_dir . '/' . $subdir . '/Tests'; + // Scan it for test files if it exists. + if (is_dir($tests_dir)) { + $files = file_scan_directory($tests_dir, '/.*\.php/'); + if (!empty($files)) { + foreach ($files as $file) { + // Convert the file name into the namespaced class name. + $replacements = array( + '/' => '\\', + $module_dir . '/' => '', + 'lib/' => '', + 'src/' => 'Drupal\\' . $name . '\\', + '.php' => '', + ); + $classes[] = strtr($file->uri, $replacements); + } } } } @@ -364,7 +374,10 @@ function simpletest_test_get_all() { // If this test class requires a non-existing module, skip it. if (!empty($info['dependencies'])) { foreach ($info['dependencies'] as $module) { - if (!drupal_get_filename('module', $module)) { + // Pass FALSE as fourth argument so no error gets created for + // the missing file. + $found_module = drupal_get_filename('module', $module, NULL, FALSE); + if (!$found_module) { continue 2; } } @@ -406,17 +419,20 @@ function simpletest_classloader_register() { // Only register PSR-0 class loading if we are on PHP 5.3 or higher. if (version_compare(PHP_VERSION, '5.3') > 0) { - spl_autoload_register('_simpletest_autoload_psr0'); + spl_autoload_register('_simpletest_autoload_psr4_psr0'); } } /** - * Autoload callback to find PSR-0 test classes. + * Autoload callback to find PSR-4 and PSR-0 test classes. + * + * Looks in the 'src/Tests' and in the 'lib/Drupal/mymodule/Tests' directory of + * modules for the class. * * This will only work on classes where the namespace is of the pattern * "Drupal\$extension\Tests\.." */ -function _simpletest_autoload_psr0($class) { +function _simpletest_autoload_psr4_psr0($class) { // Static cache for extension paths. // This cache is lazily filled as soon as it is needed. @@ -446,14 +462,26 @@ function _simpletest_autoload_psr0($class) { $namespace = substr($class, 0, $nspos); $classname = substr($class, $nspos + 1); - // Build the filepath where we expect the class to be defined. - $path = dirname($extensions[$extension]) . '/lib/' . - str_replace('\\', '/', $namespace) . '/' . + // Try the PSR-4 location first, and the PSR-0 location as a fallback. + // Build the PSR-4 filepath where we expect the class to be defined. + $psr4_path = dirname($extensions[$extension]) . '/src/' . + str_replace('\\', '/', substr($namespace, strlen('Drupal\\' . $extension . '\\'))) . '/' . str_replace('_', '/', $classname) . '.php'; // Include the file, if it does exist. - if (file_exists($path)) { - include $path; + if (file_exists($psr4_path)) { + include $psr4_path; + } + else { + // Build the PSR-0 filepath where we expect the class to be defined. + $psr0_path = dirname($extensions[$extension]) . '/lib/' . + str_replace('\\', '/', $namespace) . '/' . + str_replace('_', '/', $classname) . '.php'; + + // Include the file, if it does exist. + if (file_exists($psr0_path)) { + include $psr0_path; + } } } } @@ -487,25 +515,25 @@ function simpletest_registry_files_alter(&$files, $modules) { * Generate test file. */ function simpletest_generate_file($filename, $width, $lines, $type = 'binary-text') { - $size = $width * $lines - $lines; - - // Generate random text $text = ''; - for ($i = 0; $i < $size; $i++) { - switch ($type) { - case 'text': - $text .= chr(rand(32, 126)); - break; - case 'binary': - $text .= chr(rand(0, 31)); - break; - case 'binary-text': - default: - $text .= rand(0, 1); - break; + for ($i = 0; $i < $lines; $i++) { + // Generate $width - 1 characters to leave space for the "\n" character. + for ($j = 0; $j < $width - 1; $j++) { + switch ($type) { + case 'text': + $text .= chr(rand(32, 126)); + break; + case 'binary': + $text .= chr(rand(0, 31)); + break; + case 'binary-text': + default: + $text .= rand(0, 1); + break; + } } + $text .= "\n"; } - $text = wordwrap($text, $width - 1, "\n", TRUE) . "\n"; // Add \n for symmetrical file. // Create filename. file_put_contents('public://' . $filename . '.txt', $text); diff --git a/modules/simpletest/simpletest.test b/modules/simpletest/simpletest.test index dde162ec..5d1c718c 100644 --- a/modules/simpletest/simpletest.test +++ b/modules/simpletest/simpletest.test @@ -322,6 +322,14 @@ class SimpleTestFunctionalTest extends DrupalWebTestCase { * Test internal testing framework browser. */ class SimpleTestBrowserTestCase extends DrupalWebTestCase { + + /** + * A flag indicating whether a cookie has been set in a test. + * + * @var bool + */ + protected static $cookieSet = FALSE; + public static function getInfo() { return array( 'name' => 'SimpleTest browser', @@ -380,6 +388,46 @@ EOF; $urls = $this->xpath('//a[text()=:text]', array(':text' => 'A second "even more weird" link, in memory of George O\'Malley')); $this->assertEqual($urls[0]['href'], 'link2', 'Match with mixed single and double quotes.'); } + + /** + * Tests that cookies set during a request are available for testing. + */ + public function testCookies() { + // Check that the $this->cookies property is populated when a user logs in. + $user = $this->drupalCreateUser(); + $edit = array('name' => $user->name, 'pass' => $user->pass_raw); + $this->drupalPost('', $edit, t('Log in')); + $this->assertEqual(count($this->cookies), 1, 'A cookie is set when the user logs in.'); + + // Check that the name and value of the cookie match the request data. + $cookie_header = $this->drupalGetHeader('set-cookie', TRUE); + + // The name and value are located at the start of the string, separated by + // an equals sign and ending in a semicolon. + preg_match('/^([^=]+)=([^;]+)/', $cookie_header, $matches); + $name = $matches[1]; + $value = $matches[2]; + + $this->assertTrue(array_key_exists($name, $this->cookies), 'The cookie name is correct.'); + $this->assertEqual($value, $this->cookies[$name]['value'], 'The cookie value is correct.'); + + // Set a flag indicating that a cookie has been set in this test. + // @see SimpleTestBrowserTestCase::testCookieDoesNotBleed(). + self::$cookieSet = TRUE; + } + + /** + * Tests that the cookies from a previous test do not bleed into a new test. + * + * @see SimpleTestBrowserTestCase::testCookies(). + */ + public function testCookieDoesNotBleed() { + // In order for this test to be effective it should always run after the + // testCookies() test. + $this->assertTrue(self::$cookieSet, 'Tests have been executed in the expected order.'); + $this->assertEqual(count($this->cookies), 0, 'No cookies are present at the start of a new test.'); + } + } class SimpleTestMailCaptureTestCase extends DrupalWebTestCase { @@ -703,7 +751,9 @@ class SimpleTestDiscoveryTestCase extends DrupalWebTestCase { $classes_all = simpletest_test_get_all(); foreach (array( 'Drupal\\simpletest\\Tests\\PSR0WebTest', + 'Drupal\\simpletest\\Tests\\PSR4WebTest', 'Drupal\\psr_0_test\\Tests\\ExampleTest', + 'Drupal\\psr_4_test\\Tests\\ExampleTest', ) as $class) { $this->assert(!empty($classes_all['SimpleTest'][$class]), t('Class @class must be discovered by simpletest_test_get_all().', array('@class' => $class))); } @@ -726,15 +776,20 @@ class SimpleTestDiscoveryTestCase extends DrupalWebTestCase { // Don't expect PSR-0 tests to be discovered on older PHP versions. return; } - // This one is provided by simpletest itself via PSR-0. + // These are provided by simpletest itself via PSR-0 and PSR-4. $this->assertText('PSR0 web test'); + $this->assertText('PSR4 web test'); $this->assertText('PSR0 example test: PSR-0 in disabled modules.'); + $this->assertText('PSR4 example test: PSR-4 in disabled modules.'); $this->assertText('PSR0 example test: PSR-0 in nested subfolders.'); + $this->assertText('PSR4 example test: PSR-4 in nested subfolders.'); // Test each test individually. foreach (array( 'Drupal\\psr_0_test\\Tests\\ExampleTest', 'Drupal\\psr_0_test\\Tests\\Nested\\NestedExampleTest', + 'Drupal\\psr_4_test\\Tests\\ExampleTest', + 'Drupal\\psr_4_test\\Tests\\Nested\\NestedExampleTest', ) as $class) { $this->drupalGet('admin/config/development/testing'); $edit = array($class => TRUE); diff --git a/modules/simpletest/src/Tests/PSR4WebTest.php b/modules/simpletest/src/Tests/PSR4WebTest.php new file mode 100644 index 00000000..24c8d899 --- /dev/null +++ b/modules/simpletest/src/Tests/PSR4WebTest.php @@ -0,0 +1,18 @@ + 'PSR4 web test', + 'description' => 'We want to assert that this PSR-4 test case is being discovered.', + 'group' => 'SimpleTest', + ); + } + + function testArithmetics() { + $this->assert(1 + 1 == 2, '1 + 1 == 2'); + } +} diff --git a/modules/simpletest/tests/actions_loop_test.info b/modules/simpletest/tests/actions_loop_test.info index e3384381..71cd14f3 100644 --- a/modules/simpletest/tests/actions_loop_test.info +++ b/modules/simpletest/tests/actions_loop_test.info @@ -5,8 +5,8 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/simpletest/tests/ajax.test b/modules/simpletest/tests/ajax.test index c38a325f..afe02306 100644 --- a/modules/simpletest/tests/ajax.test +++ b/modules/simpletest/tests/ajax.test @@ -293,7 +293,7 @@ class AJAXCommandsTestCase extends AJAXTestCase { $this->assertCommand($commands, $expected, "'changed' AJAX command (with asterisk) issued with correct selector"); // Tests the 'css' command. - $commands = $this->drupalPostAJAX($form_path, $edit, array('op' => t("Set the the '#box' div to be blue."))); + $commands = $this->drupalPostAJAX($form_path, $edit, array('op' => t("Set the '#box' div to be blue."))); $expected = array( 'command' => 'css', 'selector' => '#css_div', diff --git a/modules/simpletest/tests/ajax_forms_test.info b/modules/simpletest/tests/ajax_forms_test.info index db582d41..c9d9cb6d 100644 --- a/modules/simpletest/tests/ajax_forms_test.info +++ b/modules/simpletest/tests/ajax_forms_test.info @@ -5,8 +5,8 @@ package = Testing version = VERSION hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/simpletest/tests/ajax_forms_test.module b/modules/simpletest/tests/ajax_forms_test.module index 260d9112..de2fa0ba 100644 --- a/modules/simpletest/tests/ajax_forms_test.module +++ b/modules/simpletest/tests/ajax_forms_test.module @@ -157,7 +157,7 @@ function ajax_forms_test_ajax_commands_form($form, &$form_state) { // Shows the Ajax 'css' command. $form['css_command_example'] = array( - '#value' => t("Set the the '#box' div to be blue."), + '#value' => t("Set the '#box' div to be blue."), '#type' => 'submit', '#ajax' => array( 'callback' => 'ajax_forms_test_advanced_commands_css_callback', diff --git a/modules/simpletest/tests/ajax_test.info b/modules/simpletest/tests/ajax_test.info index 736c0804..f572a005 100644 --- a/modules/simpletest/tests/ajax_test.info +++ b/modules/simpletest/tests/ajax_test.info @@ -5,8 +5,8 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/simpletest/tests/batch_test.callbacks.inc b/modules/simpletest/tests/batch_test.callbacks.inc index 75e66553..6564413d 100644 --- a/modules/simpletest/tests/batch_test.callbacks.inc +++ b/modules/simpletest/tests/batch_test.callbacks.inc @@ -7,6 +7,8 @@ */ /** + * Implements callback_batch_operation(). + * * Simple batch operation. */ function _batch_test_callback_1($id, $sleep, &$context) { @@ -20,6 +22,8 @@ function _batch_test_callback_1($id, $sleep, &$context) { } /** + * Implements callback_batch_operation(). + * * Multistep batch operation. */ function _batch_test_callback_2($start, $total, $sleep, &$context) { @@ -53,6 +57,8 @@ function _batch_test_callback_2($start, $total, $sleep, &$context) { } /** + * Implements callback_batch_operation(). + * * Simple batch operation. */ function _batch_test_callback_5($id, $sleep, &$context) { @@ -68,6 +74,8 @@ function _batch_test_callback_5($id, $sleep, &$context) { } /** + * Implements callback_batch_operation(). + * * Batch operation setting up its own batch. */ function _batch_test_nested_batch_callback() { @@ -76,6 +84,8 @@ function _batch_test_nested_batch_callback() { } /** + * Implements callback_batch_finished(). + * * Common 'finished' callbacks for batches 1 to 4. */ function _batch_test_finished_helper($batch_id, $success, $results, $operations) { @@ -99,6 +109,8 @@ function _batch_test_finished_helper($batch_id, $success, $results, $operations) } /** + * Implements callback_batch_finished(). + * * 'finished' callback for batch 0. */ function _batch_test_finished_0($success, $results, $operations) { @@ -106,6 +118,8 @@ function _batch_test_finished_0($success, $results, $operations) { } /** + * Implements callback_batch_finished(). + * * 'finished' callback for batch 1. */ function _batch_test_finished_1($success, $results, $operations) { @@ -113,6 +127,8 @@ function _batch_test_finished_1($success, $results, $operations) { } /** + * Implements callback_batch_finished(). + * * 'finished' callback for batch 2. */ function _batch_test_finished_2($success, $results, $operations) { @@ -120,6 +136,8 @@ function _batch_test_finished_2($success, $results, $operations) { } /** + * Implements callback_batch_finished(). + * * 'finished' callback for batch 3. */ function _batch_test_finished_3($success, $results, $operations) { @@ -127,6 +145,8 @@ function _batch_test_finished_3($success, $results, $operations) { } /** + * Implements callback_batch_finished(). + * * 'finished' callback for batch 4. */ function _batch_test_finished_4($success, $results, $operations) { @@ -134,6 +154,8 @@ function _batch_test_finished_4($success, $results, $operations) { } /** + * Implements callback_batch_finished(). + * * 'finished' callback for batch 5. */ function _batch_test_finished_5($success, $results, $operations) { diff --git a/modules/simpletest/tests/batch_test.info b/modules/simpletest/tests/batch_test.info index 4ee2142e..dc2aae56 100644 --- a/modules/simpletest/tests/batch_test.info +++ b/modules/simpletest/tests/batch_test.info @@ -5,8 +5,8 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/simpletest/tests/boot.test b/modules/simpletest/tests/boot.test new file mode 100644 index 00000000..562b082e --- /dev/null +++ b/modules/simpletest/tests/boot.test @@ -0,0 +1,38 @@ + 'Early bootstrap test', + 'description' => 'Confirm that calling module_implements() during early bootstrap does not pollute the module_implements() cache.', + 'group' => 'System', + ); + } + + function setUp() { + parent::setUp('boot_test_1', 'boot_test_2'); + } + + /** + * Test hook_boot() on both regular and "early exit" pages. + */ + public function testHookBoot() { + $paths = array('', 'early_exit'); + foreach ($paths as $path) { + // Empty the module_implements() caches. + module_implements(NULL, FALSE, TRUE); + // Do a request to the front page, which will call module_implements() + // during hook_boot(). + $this->drupalGet($path); + // Reset the static cache so we get implementation data from the persistent + // cache. + drupal_static_reset(); + // Make sure we get a full list of all modules implementing hook_help(). + $modules = module_implements('help'); + $this->assertTrue(in_array('boot_test_2', $modules)); + } + } +} diff --git a/modules/simpletest/tests/boot_test_1.info b/modules/simpletest/tests/boot_test_1.info new file mode 100644 index 00000000..85a5ea65 --- /dev/null +++ b/modules/simpletest/tests/boot_test_1.info @@ -0,0 +1,12 @@ +name = Early bootstrap tests +description = A support module for hook_boot testing. +core = 7.x +package = Testing +version = VERSION +hidden = TRUE + +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" +project = "drupal" +datestamp = "1481152423" + diff --git a/modules/simpletest/tests/boot_test_1.module b/modules/simpletest/tests/boot_test_1.module new file mode 100644 index 00000000..a452e289 --- /dev/null +++ b/modules/simpletest/tests/boot_test_1.module @@ -0,0 +1,21 @@ +proxy_ip; + $_SERVER['HTTP_X_FORWARDED_FOR'] = $this->proxy_ip; + drupal_static_reset('ip_address'); + $this->assertTrue( + ip_address() == $this->proxy_ip, + 'Visiting from trusted proxy got proxy IP address.' + ); + // Multi-tier architecture with comma separated values in header. $_SERVER['REMOTE_ADDR'] = $this->proxy_ip; $_SERVER['HTTP_X_FORWARDED_FOR'] = implode(', ', array($this->untrusted_ip, $this->forwarded_ip, $this->proxy2_ip)); @@ -152,7 +161,9 @@ class BootstrapPageCacheTestCase extends DrupalWebTestCase { $this->drupalLogin($user); $this->drupalGet('', array(), array('If-Modified-Since: ' . $last_modified, 'If-None-Match: ' . $etag)); $this->assertResponse(200, 'Conditional request returned 200 OK for authenticated user.'); - $this->assertFalse($this->drupalGetHeader('X-Drupal-Cache'), 'Absense of Page was not cached.'); + $this->assertFalse($this->drupalGetHeader('X-Drupal-Cache'), 'Absence of Page was not cached.'); + $this->assertFalse($this->drupalGetHeader('ETag'), 'ETag HTTP headers are not present for logged in users.'); + $this->assertFalse($this->drupalGetHeader('Last-Modified'), 'Last-Modified HTTP headers are not present for logged in users.'); } /** @@ -189,7 +200,7 @@ class BootstrapPageCacheTestCase extends DrupalWebTestCase { $this->drupalGet('system-test/set-header', array('query' => array('name' => 'Foo', 'value' => 'bar'))); $this->assertFalse($this->drupalGetHeader('X-Drupal-Cache'), 'Caching was bypassed.'); $this->assertTrue(strpos($this->drupalGetHeader('Vary'), 'Cookie') === FALSE, 'Vary: Cookie header was not sent.'); - $this->assertEqual($this->drupalGetHeader('Cache-Control'), 'no-cache, must-revalidate, post-check=0, pre-check=0', 'Cache-Control header was sent.'); + $this->assertEqual($this->drupalGetHeader('Cache-Control'), 'no-cache, must-revalidate', 'Cache-Control header was sent.'); $this->assertEqual($this->drupalGetHeader('Expires'), 'Sun, 19 Nov 1978 05:00:00 GMT', 'Expires header was sent.'); $this->assertEqual($this->drupalGetHeader('Foo'), 'bar', 'Custom header was sent.'); @@ -286,6 +297,39 @@ class BootstrapVariableTestCase extends DrupalWebTestCase { } +/** + * Tests the auto-loading behavior of the code registry. + */ +class BootstrapAutoloadTestCase extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Code registry', + 'description' => 'Test that the code registry functions correctly.', + 'group' => 'Bootstrap', + ); + } + + function setUp() { + parent::setUp('drupal_autoload_test'); + } + + /** + * Tests that autoloader name matching is not case sensitive. + */ + function testAutoloadCase() { + // Test interface autoloader. + $this->assertTrue(drupal_autoload_interface('drupalautoloadtestinterface'), 'drupal_autoload_interface() recognizes DrupalAutoloadTestInterface in lower case.'); + // Test class autoloader. + $this->assertTrue(drupal_autoload_class('drupalautoloadtestclass'), 'drupal_autoload_class() recognizes DrupalAutoloadTestClass in lower case.'); + // Test trait autoloader. + if (version_compare(PHP_VERSION, '5.4') >= 0) { + $this->assertTrue(drupal_autoload_trait('drupalautoloadtesttrait'), 'drupal_autoload_trait() recognizes DrupalAutoloadTestTrait in lower case.'); + } + } + +} + /** * Test hook_boot() and hook_exit(). */ @@ -344,12 +388,19 @@ class BootstrapGetFilenameTestCase extends DrupalUnitTestCase { public static function getInfo() { return array( - 'name' => 'Get filename test', - 'description' => 'Test that drupal_get_filename() works correctly when the file is not found in the database.', + 'name' => 'Get filename test (without the system table)', + 'description' => 'Test that drupal_get_filename() works correctly when the database is not available.', 'group' => 'Bootstrap', ); } + /** + * The last file-related error message triggered by the filename test. + * + * Used by BootstrapGetFilenameTestCase::testDrupalGetFilename(). + */ + protected $getFilenameTestTriggeredError; + /** * Test that drupal_get_filename() works correctly when the file is not found in the database. */ @@ -379,6 +430,203 @@ class BootstrapGetFilenameTestCase extends DrupalUnitTestCase { // automatically check there for 'script' files, just as it does for (e.g.) // 'module' files in modules. $this->assertIdentical(drupal_get_filename('script', 'test'), 'scripts/test.script', t('Retrieve test script location.')); + + // When searching for a module that does not exist, drupal_get_filename() + // should return NULL and trigger an appropriate error message. + $this->getFilenameTestTriggeredError = NULL; + set_error_handler(array($this, 'fileNotFoundErrorHandler')); + $non_existing_module = $this->randomName(); + $this->assertNull(drupal_get_filename('module', $non_existing_module), 'Searching for a module that does not exist returns NULL.'); + $this->assertTrue(strpos($this->getFilenameTestTriggeredError, format_string('The following module is missing from the file system: %name', array('%name' => $non_existing_module))) === 0, 'Searching for an item that does not exist triggers the correct error.'); + restore_error_handler(); + + // Check that the result is stored in the file system scan cache. + $file_scans = _drupal_file_scan_cache(); + $this->assertIdentical($file_scans['module'][$non_existing_module], FALSE, 'Searching for a module that does not exist creates a record in the missing and moved files static variable.'); + + // Performing the search again in the same request still should not find + // the file, but the error message should not be repeated (therefore we do + // not override the error handler here). + $this->assertNull(drupal_get_filename('module', $non_existing_module), 'Searching for a module that does not exist returns NULL during the second search.'); + } + + /** + * Skips handling of "file not found" errors. + */ + public function fileNotFoundErrorHandler($error_level, $message, $filename, $line, $context) { + // Skip error handling if this is a "file not found" error. + if (strpos($message, 'is missing from the file system:') !== FALSE || strpos($message, 'has moved within the file system:') !== FALSE) { + $this->getFilenameTestTriggeredError = $message; + return; + } + _drupal_error_handler($error_level, $message, $filename, $line, $context); + } +} + +/** + * Test drupal_get_filename() in the context of a full Drupal installation. + */ +class BootstrapGetFilenameWebTestCase extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Get filename test (full installation)', + 'description' => 'Test that drupal_get_filename() works correctly in the context of a full Drupal installation.', + 'group' => 'Bootstrap', + ); + } + + function setUp() { + parent::setUp('system_test'); + } + + /** + * The last file-related error message triggered by the filename test. + * + * Used by BootstrapGetFilenameWebTestCase::testDrupalGetFilename(). + */ + protected $getFilenameTestTriggeredError; + + /** + * Test that drupal_get_filename() works correctly with a full Drupal site. + */ + function testDrupalGetFilename() { + // Search for a module that exists in the file system and the {system} + // table and make sure that it is found. + $this->assertIdentical(drupal_get_filename('module', 'node'), 'modules/node/node.module', 'Module found at expected location.'); + + // Search for a module that does not exist in either the file system or the + // {system} table. Make sure that an appropriate error is triggered and + // that the module winds up in the static and persistent cache. + $this->getFilenameTestTriggeredError = NULL; + set_error_handler(array($this, 'fileNotFoundErrorHandler')); + $non_existing_module = $this->randomName(); + $this->assertNull(drupal_get_filename('module', $non_existing_module), 'Searching for a module that does not exist returns NULL.'); + $this->assertTrue(strpos($this->getFilenameTestTriggeredError, format_string('The following module is missing from the file system: %name', array('%name' => $non_existing_module))) === 0, 'Searching for a module that does not exist triggers the correct error.'); + restore_error_handler(); + $file_scans = _drupal_file_scan_cache(); + $this->assertIdentical($file_scans['module'][$non_existing_module], FALSE, 'Searching for a module that does not exist creates a record in the missing and moved files static variable.'); + drupal_file_scan_write_cache(); + $cache = cache_get('_drupal_file_scan_cache', 'cache_bootstrap'); + $this->assertIdentical($cache->data['module'][$non_existing_module], FALSE, 'Searching for a module that does not exist creates a record in the missing and moved files persistent cache.'); + + // Simulate moving a module to a location that does not match the location + // in the {system} table and perform similar tests as above. + db_update('system') + ->fields(array('filename' => 'modules/simpletest/tests/fake_location/module_test.module')) + ->condition('name', 'module_test') + ->condition('type', 'module') + ->execute(); + $this->getFilenameTestTriggeredError = NULL; + set_error_handler(array($this, 'fileNotFoundErrorHandler')); + $this->assertIdentical(drupal_get_filename('module', 'module_test'), 'modules/simpletest/tests/module_test.module', 'Searching for a module that has moved finds the module at its new location.'); + $this->assertTrue(strpos($this->getFilenameTestTriggeredError, format_string('The following module has moved within the file system: %name', array('%name' => 'module_test'))) === 0, 'Searching for a module that has moved triggers the correct error.'); + restore_error_handler(); + $file_scans = _drupal_file_scan_cache(); + $this->assertIdentical($file_scans['module']['module_test'], 'modules/simpletest/tests/module_test.module', 'Searching for a module that has moved creates a record in the missing and moved files static variable.'); + drupal_file_scan_write_cache(); + $cache = cache_get('_drupal_file_scan_cache', 'cache_bootstrap'); + $this->assertIdentical($cache->data['module']['module_test'], 'modules/simpletest/tests/module_test.module', 'Searching for a module that has moved creates a record in the missing and moved files persistent cache.'); + + // Simulate a module that exists in the {system} table but does not exist + // in the file system and perform similar tests as above. + $non_existing_module = $this->randomName(); + db_update('system') + ->fields(array('name' => $non_existing_module)) + ->condition('name', 'module_test') + ->condition('type', 'module') + ->execute(); + $this->getFilenameTestTriggeredError = NULL; + set_error_handler(array($this, 'fileNotFoundErrorHandler')); + $this->assertNull(drupal_get_filename('module', $non_existing_module), 'Searching for a module that exists in the system table but not in the file system returns NULL.'); + $this->assertTrue(strpos($this->getFilenameTestTriggeredError, format_string('The following module is missing from the file system: %name', array('%name' => $non_existing_module))) === 0, 'Searching for a module that exists in the system table but not in the file system triggers the correct error.'); + restore_error_handler(); + $file_scans = _drupal_file_scan_cache(); + $this->assertIdentical($file_scans['module'][$non_existing_module], FALSE, 'Searching for a module that exists in the system table but not in the file system creates a record in the missing and moved files static variable.'); + drupal_file_scan_write_cache(); + $cache = cache_get('_drupal_file_scan_cache', 'cache_bootstrap'); + $this->assertIdentical($cache->data['module'][$non_existing_module], FALSE, 'Searching for a module that exists in the system table but not in the file system creates a record in the missing and moved files persistent cache.'); + + // Simulate a module that exists in the file system but not in the {system} + // table and perform similar tests as above. + db_delete('system') + ->condition('name', 'common_test') + ->condition('type', 'module') + ->execute(); + system_list_reset(); + $this->getFilenameTestTriggeredError = NULL; + set_error_handler(array($this, 'fileNotFoundErrorHandler')); + $this->assertIdentical(drupal_get_filename('module', 'common_test'), 'modules/simpletest/tests/common_test.module', 'Searching for a module that does not exist in the system table finds the module at its actual location.'); + $this->assertTrue(strpos($this->getFilenameTestTriggeredError, format_string('The following module has moved within the file system: %name', array('%name' => 'common_test'))) === 0, 'Searching for a module that does not exist in the system table triggers the correct error.'); + restore_error_handler(); + $file_scans = _drupal_file_scan_cache(); + $this->assertIdentical($file_scans['module']['common_test'], 'modules/simpletest/tests/common_test.module', 'Searching for a module that does not exist in the system table creates a record in the missing and moved files static variable.'); + drupal_file_scan_write_cache(); + $cache = cache_get('_drupal_file_scan_cache', 'cache_bootstrap'); + $this->assertIdentical($cache->data['module']['common_test'], 'modules/simpletest/tests/common_test.module', 'Searching for a module that does not exist in the system table creates a record in the missing and moved files persistent cache.'); + } + + /** + * Skips handling of "file not found" errors. + */ + public function fileNotFoundErrorHandler($error_level, $message, $filename, $line, $context) { + // Skip error handling if this is a "file not found" error. + if (strpos($message, 'is missing from the file system:') !== FALSE || strpos($message, 'has moved within the file system:') !== FALSE) { + $this->getFilenameTestTriggeredError = $message; + return; + } + _drupal_error_handler($error_level, $message, $filename, $line, $context); + } + + /** + * Test that watchdog messages about missing files are correctly recorded. + */ + public function testWatchdog() { + // Search for a module that does not exist in either the file system or the + // {system} table. Make sure that an appropriate warning is recorded in the + // logs. + $non_existing_module = $this->randomName(); + $query_parameters = array( + ':type' => 'php', + ':severity' => WATCHDOG_WARNING, + ); + $this->assertEqual(db_query('SELECT COUNT(*) FROM {watchdog} WHERE type = :type AND severity = :severity', $query_parameters)->fetchField(), 0, 'No warning message appears in the logs before searching for a module that does not exist.'); + // Trigger the drupal_get_filename() call. This must be done via a request + // to a separate URL since the watchdog() will happen in a shutdown + // function, and so that SimpleTest can be told to ignore (and not fail as + // a result of) the expected PHP warnings generated during this process. + variable_set('system_test_drupal_get_filename_test_module_name', $non_existing_module); + $this->drupalGet('system-test/drupal-get-filename'); + $message_variables = db_query('SELECT variables FROM {watchdog} WHERE type = :type AND severity = :severity', $query_parameters)->fetchCol(); + $this->assertEqual(count($message_variables), 1, 'A single warning message appears in the logs after searching for a module that does not exist.'); + $variables = reset($message_variables); + $variables = unserialize($variables); + $this->assertTrue(isset($variables['!message']) && strpos($variables['!message'], format_string('The following module is missing from the file system: %name', array('%name' => $non_existing_module))) !== FALSE, 'The warning message that appears in the logs after searching for a module that does not exist contains the expected text.'); + } + + /** + * Test that drupal_get_filename() does not break recursive rebuilds. + */ + public function testRecursiveRebuilds() { + // Ensure that the drupal_get_filename() call due to a missing module does + // not break the data returned by an attempted recursive rebuild. The code + // path which is tested is as follows: + // - Call drupal_get_schema(). + // - Within a hook_schema() implementation, trigger a drupal_get_filename() + // search for a nonexistent module. + // - In the watchdog() call that results from that, trigger + // drupal_get_schema() again. + // Without some kind of recursion protection, this could cause the second + // drupal_get_schema() call to return incomplete results. This test ensures + // that does not happen. + $non_existing_module = $this->randomName(); + variable_set('system_test_drupal_get_filename_test_module_name', $non_existing_module); + $this->drupalGet('system-test/drupal-get-filename-with-schema-rebuild'); + $original_drupal_get_schema_tables = variable_get('system_test_drupal_get_filename_with_schema_rebuild_original_tables'); + $final_drupal_get_schema_tables = variable_get('system_test_drupal_get_filename_with_schema_rebuild_final_tables'); + $this->assertTrue(!empty($original_drupal_get_schema_tables)); + $this->assertTrue(!empty($final_drupal_get_schema_tables)); + $this->assertEqual($original_drupal_get_schema_tables, $final_drupal_get_schema_tables); } } @@ -546,3 +794,85 @@ class BootstrapOverrideServerVariablesTestCase extends DrupalUnitTestCase { } } } + +/** + * Tests for $_GET['destination'] and $_REQUEST['destination'] validation. + */ +class BootstrapDestinationTestCase extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'URL destination validation', + 'description' => 'Test that $_GET[\'destination\'] and $_REQUEST[\'destination\'] cannot contain external URLs.', + 'group' => 'Bootstrap', + ); + } + + function setUp() { + parent::setUp('system_test'); + } + + /** + * Tests that $_GET/$_REQUEST['destination'] only contain internal URLs. + * + * @see _drupal_bootstrap_variables() + * @see system_test_get_destination() + * @see system_test_request_destination() + */ + public function testDestination() { + $test_cases = array( + array( + 'input' => 'node', + 'output' => 'node', + 'message' => "Standard internal example node path is present in the 'destination' parameter.", + ), + array( + 'input' => '/example.com', + 'output' => '/example.com', + 'message' => 'Internal path with one leading slash is allowed.', + ), + array( + 'input' => '//example.com/test', + 'output' => '', + 'message' => 'External URL without scheme is not allowed.', + ), + array( + 'input' => 'example:test', + 'output' => 'example:test', + 'message' => 'Internal URL using a colon is allowed.', + ), + array( + 'input' => 'http://example.com', + 'output' => '', + 'message' => 'External URL is not allowed.', + ), + array( + 'input' => 'javascript:alert(0)', + 'output' => 'javascript:alert(0)', + 'message' => 'Javascript URL is allowed because it is treated as an internal URL.', + ), + ); + foreach ($test_cases as $test_case) { + // Test $_GET['destination']. + $this->drupalGet('system-test/get-destination', array('query' => array('destination' => $test_case['input']))); + $this->assertIdentical($test_case['output'], $this->drupalGetContent(), $test_case['message']); + // Test $_REQUEST['destination']. There's no form to submit to, so + // drupalPost() won't work here; this just tests a direct $_POST request + // instead. + $curl_parameters = array( + CURLOPT_URL => $this->getAbsoluteUrl('system-test/request-destination'), + CURLOPT_POST => TRUE, + CURLOPT_POSTFIELDS => 'destination=' . urlencode($test_case['input']), + CURLOPT_HTTPHEADER => array(), + ); + $post_output = $this->curlExec($curl_parameters); + $this->assertIdentical($test_case['output'], $post_output, $test_case['message']); + } + + // Make sure that 404 pages do not populate $_GET['destination'] with + // external URLs. + variable_set('site_404', 'system-test/get-destination'); + $this->drupalGet('http://example.com', array('external' => FALSE)); + $this->assertIdentical('', $this->drupalGetContent(), 'External URL is not allowed on 404 pages.'); + } +} diff --git a/modules/simpletest/tests/common.test b/modules/simpletest/tests/common.test index eebfdbe4..0f991c3c 100644 --- a/modules/simpletest/tests/common.test +++ b/modules/simpletest/tests/common.test @@ -209,7 +209,16 @@ class CommonURLUnitTest extends DrupalWebTestCase { // Test that drupal can recognize an absolute URL. Used to prevent attack vectors. $this->assertTrue(url_is_external($url), 'Correctly identified an external URL.'); + // External URL without an explicit protocol. + $url = '//drupal.org/foo/bar?foo=bar&bar=baz&baz#foo'; + $this->assertTrue(url_is_external($url), 'Correctly identified an external URL without a protocol part.'); + + // Internal URL starting with a slash. + $url = '/drupal.org'; + $this->assertFalse(url_is_external($url), 'Correctly identified an internal URL with a leading slash.'); + // Test the parsing of absolute URLs. + $url = 'http://drupal.org/foo/bar?foo=bar&bar=baz&baz#foo'; $result = array( 'path' => 'http://drupal.org/foo/bar', 'query' => array('foo' => 'bar', 'bar' => 'baz', 'baz' => ''), @@ -349,6 +358,76 @@ class CommonURLUnitTest extends DrupalWebTestCase { $query = array($this->randomName(5) => $this->randomName(5)); $result = url($url, array('query' => $query)); $this->assertEqual($url . '&' . http_build_query($query, '', '&'), $result, 'External URL query string can be extended with a custom query string in $options.'); + + // Verify that an internal URL does not result in an external URL without + // protocol part. + $url = '/drupal.org'; + $result = url($url); + $this->assertTrue(strpos($result, '//') === FALSE, 'Internal URL does not turn into an external URL.'); + + // Verify that an external URL without protocol part is recognized as such. + $url = '//drupal.org'; + $result = url($url); + $this->assertEqual($url, $result, 'External URL without protocol is not altered.'); + } +} + +/** + * Tests url_is_external(). + */ +class UrlIsExternalUnitTest extends DrupalUnitTestCase { + + public static function getInfo() { + return array( + 'name' => 'External URL checking', + 'description' => 'Performs tests on url_is_external().', + 'group' => 'System', + ); + } + + /** + * Tests if each URL is external or not. + */ + function testUrlIsExternal() { + foreach ($this->examples() as $path => $expected) { + $this->assertIdentical(url_is_external($path), $expected, $path); + } + } + + /** + * Provides data for testUrlIsExternal(). + * + * @return array + * An array of test data, keyed by a path, with the expected value where + * TRUE is external, and FALSE is not external. + */ + protected function examples() { + return array( + // Simple external URLs. + 'http://example.com' => TRUE, + 'https://example.com' => TRUE, + 'http://drupal.org/foo/bar?foo=bar&bar=baz&baz#foo' => TRUE, + '//drupal.org' => TRUE, + // Some browsers ignore or strip leading control characters. + "\x00//www.example.com" => TRUE, + "\x08//www.example.com" => TRUE, + "\x1F//www.example.com" => TRUE, + "\n//www.example.com" => TRUE, + // JSON supports decoding directly from UTF-8 code points. + json_decode('"\u00AD"') . "//www.example.com" => TRUE, + json_decode('"\u200E"') . "//www.example.com" => TRUE, + json_decode('"\uE0020"') . "//www.example.com" => TRUE, + json_decode('"\uE000"') . "//www.example.com" => TRUE, + // Backslashes should be normalized to forward. + '\\\\example.com' => TRUE, + // Local URLs. + 'node' => FALSE, + '/system/ajax' => FALSE, + '?q=foo:bar' => FALSE, + 'node/edit:me' => FALSE, + '/drupal.org' => FALSE, + '' => FALSE, + ); } } @@ -868,6 +947,31 @@ class DrupalHTMLIdentifierTestCase extends DrupalUnitTestCase { // Verify that invalid characters (including non-breaking space) are stripped from the identifier. $this->assertIdentical(drupal_clean_css_identifier('invalid !"#$%&\'()*+,./:;<=>?@[\\]^`{|}~ identifier', array()), 'invalididentifier', 'Strip invalid characters.'); + + // Verify that double underscores are replaced in the identifier by default. + $identifier = 'css__identifier__with__double__underscores'; + $expected = 'css--identifier--with--double--underscores'; + $this->assertIdentical(drupal_clean_css_identifier($identifier), $expected, 'Verify double underscores are replaced with double hyphens by default.'); + + // Verify that double underscores are preserved in the identifier if the + // variable allow_css_double_underscores is set to TRUE. + $this->setAllowCSSDoubleUnderscores(TRUE); + $this->assertIdentical(drupal_clean_css_identifier($identifier), $identifier, 'Verify double underscores are preserved if the allow_css_double_underscores set to TRUE.'); + + // To avoid affecting other test cases, set the variable + // allow_css_double_underscores to FALSE which is the default value. + $this->setAllowCSSDoubleUnderscores(FALSE); + } + + /** + * Set the variable allow_css_double_underscores and reset the cache. + * + * @param $value bool + * A new value to be set to allow_css_double_underscores. + */ + function setAllowCSSDoubleUnderscores($value) { + $GLOBALS['conf']['allow_css_double_underscores'] = $value; + drupal_static_reset('drupal_clean_css_identifier:allow_css_double_underscores'); } /** @@ -922,6 +1026,7 @@ class CascadingStylesheetsUnitTest extends DrupalUnitTestCase { * - Proper URLs in imported files. (https://drupal.org/node/265719) * - Retain pseudo-selectors. (https://drupal.org/node/460448) * - Don't adjust data URIs. (https://drupal.org/node/2142441) + * - Files imported from external URLs. (https://drupal.org/node/2014851) */ function testLoadCssBasic() { // Array of files to test living in 'simpletest/files/css_test_files/'. @@ -1174,7 +1279,7 @@ class DrupalSetContentTestCase extends DrupalWebTestCase { function testRegions() { global $theme_key; - $block_regions = array_keys(system_region_list($theme_key)); + $block_regions = system_region_list($theme_key, REGIONS_ALL, FALSE); $delimiter = $this->randomName(32); $values = array(); // Set some random content for each region available. @@ -1235,6 +1340,15 @@ class DrupalGotoTest extends DrupalWebTestCase { $this->assertText('drupal_goto', 'Drupal goto redirect succeeded.'); $this->assertEqual($this->getUrl(), url('common-test/drupal_goto', array('query' => array('foo' => '123'), 'absolute' => TRUE)), 'Drupal goto redirected to expected URL.'); + // Test that calling drupal_goto() on the current path is not dangerous. + variable_set('common_test_redirect_current_path', TRUE); + $this->drupalGet('', array('query' => array('q' => 'http://www.example.com/'))); + $headers = $this->drupalGetHeaders(TRUE); + list(, $status) = explode(' ', $headers[0][':status'], 3); + $this->assertEqual($status, 302, 'Expected response code was sent.'); + $this->assertNotEqual($this->getUrl(), 'http://www.example.com/', 'Drupal goto did not redirect to external URL.'); + $this->assertTrue(strpos($this->getUrl(), url('', array('absolute' => TRUE))) === 0, 'Drupal redirected to itself.'); + variable_del('common_test_redirect_current_path'); // Test that drupal_goto() respects ?destination=xxx. Use an complicated URL // to test that the path is encoded and decoded properly. $destination = 'common-test/drupal_goto/destination?foo=%2525&bar=123'; @@ -1420,6 +1534,127 @@ class JavaScriptTestCase extends DrupalWebTestCase { $this->assertTrue(strpos($javascript, $inline) > 0, 'Rendered JavaScript footer returns the inline code.'); } + /** + * Test the 'javascript_always_use_jquery' variable. + */ + function testJavaScriptAlwaysUseJQuery() { + // The default front page of the site should use jQuery and other standard + // scripts and settings. + $this->drupalGet(''); + $this->assertRaw('misc/jquery.js', 'Default behavior: The front page of the site includes jquery.js.'); + $this->assertRaw('misc/drupal.js', 'Default behavior: The front page of the site includes drupal.js.'); + $this->assertRaw('Drupal.settings', 'Default behavior: The front page of the site includes Drupal settings.'); + $this->assertRaw('basePath', 'Default behavior: The front page of the site includes the basePath Drupal setting.'); + + // The default front page should not use jQuery and other standard scripts + // and settings when the 'javascript_always_use_jquery' variable is set to + // FALSE. + variable_set('javascript_always_use_jquery', FALSE); + $this->drupalGet(''); + $this->assertNoRaw('misc/jquery.js', 'When "javascript_always_use_jquery" is FALSE: The front page of the site does not include jquery.js.'); + $this->assertNoRaw('misc/drupal.js', 'When "javascript_always_use_jquery" is FALSE: The front page of the site does not include drupal.js.'); + $this->assertNoRaw('Drupal.settings', 'When "javascript_always_use_jquery" is FALSE: The front page of the site does not include Drupal settings.'); + $this->assertNoRaw('basePath', 'When "javascript_always_use_jquery" is FALSE: The front page of the site does not include the basePath Drupal setting.'); + variable_del('javascript_always_use_jquery'); + + // When only settings have been added via drupal_add_js(), drupal_get_js() + // should still return jQuery and other standard scripts and settings. + $this->resetStaticVariables(); + drupal_add_js(array('testJavaScriptSetting' => 'test'), 'setting'); + $javascript = drupal_get_js(); + $this->assertTrue(strpos($javascript, 'misc/jquery.js') !== FALSE, 'Default behavior: The JavaScript returned by drupal_get_js() when only settings have been added includes jquery.js.'); + $this->assertTrue(strpos($javascript, 'misc/drupal.js') !== FALSE, 'Default behavior: The JavaScript returned by drupal_get_js() when only settings have been added includes drupal.js.'); + $this->assertTrue(strpos($javascript, 'Drupal.settings') !== FALSE, 'Default behavior: The JavaScript returned by drupal_get_js() when only settings have been added includes Drupal.settings.'); + $this->assertTrue(strpos($javascript, 'basePath') !== FALSE, 'Default behavior: The JavaScript returned by drupal_get_js() when only settings have been added includes the basePath Drupal setting.'); + $this->assertTrue(strpos($javascript, 'testJavaScriptSetting') !== FALSE, 'Default behavior: The JavaScript returned by drupal_get_js() when only settings have been added includes the added Drupal settings.'); + + // When only settings have been added via drupal_add_js() and the + // 'javascript_always_use_jquery' variable is set to FALSE, drupal_get_js() + // should not return jQuery and other standard scripts and settings, nor + // should it return the requested settings (since they cannot actually be + // addded to the page without jQuery). + $this->resetStaticVariables(); + variable_set('javascript_always_use_jquery', FALSE); + drupal_add_js(array('testJavaScriptSetting' => 'test'), 'setting'); + $javascript = drupal_get_js(); + $this->assertTrue(strpos($javascript, 'misc/jquery.js') === FALSE, 'When "javascript_always_use_jquery" is FALSE: The JavaScript returned by drupal_get_js() when only settings have been added does not include jquery.js.'); + $this->assertTrue(strpos($javascript, 'misc/drupal.js') === FALSE, 'When "javascript_always_use_jquery" is FALSE: The JavaScript returned by drupal_get_js() when only settings have been added does not include drupal.js.'); + $this->assertTrue(strpos($javascript, 'Drupal.settings') === FALSE, 'When "javascript_always_use_jquery" is FALSE: The JavaScript returned by drupal_get_js() when only settings have been added does not include Drupal.settings.'); + $this->assertTrue(strpos($javascript, 'basePath') === FALSE, 'When "javascript_always_use_jquery" is FALSE: The JavaScript returned by drupal_get_js() when only settings have been added does not include the basePath Drupal setting.'); + $this->assertTrue(strpos($javascript, 'testJavaScriptSetting') === FALSE, 'When "javascript_always_use_jquery" is FALSE: The JavaScript returned by drupal_get_js() when only settings have been added does not include the added Drupal settings.'); + variable_del('javascript_always_use_jquery'); + + // When a regular file has been added via drupal_add_js(), drupal_get_js() + // should return jQuery and other standard scripts and settings. + $this->resetStaticVariables(); + drupal_add_js('misc/collapse.js'); + $javascript = drupal_get_js(); + $this->assertTrue(strpos($javascript, 'misc/jquery.js') !== FALSE, 'Default behavior: The JavaScript returned by drupal_get_js() when a custom JavaScript file has been added includes jquery.js.'); + $this->assertTrue(strpos($javascript, 'misc/drupal.js') !== FALSE, 'Default behavior: The JavaScript returned by drupal_get_js() when a custom JavaScript file has been added includes drupal.js.'); + $this->assertTrue(strpos($javascript, 'Drupal.settings') !== FALSE, 'Default behavior: The JavaScript returned by drupal_get_js() when a custom JavaScript file has been added includes Drupal.settings.'); + $this->assertTrue(strpos($javascript, 'basePath') !== FALSE, 'Default behavior: The JavaScript returned by drupal_get_js() when a custom JavaScript file has been added includes the basePath Drupal setting.'); + $this->assertTrue(strpos($javascript, 'misc/collapse.js') !== FALSE, 'Default behavior: The JavaScript returned by drupal_get_js() when a custom JavaScript file has been added includes the custom file.'); + + // When a regular file has been added via drupal_add_js() and the + // 'javascript_always_use_jquery' variable is set to FALSE, drupal_get_js() + // should still return jQuery and other standard scripts and settings + // (since the file is assumed to require jQuery by default). + $this->resetStaticVariables(); + variable_set('javascript_always_use_jquery', FALSE); + drupal_add_js('misc/collapse.js'); + $javascript = drupal_get_js(); + $this->assertTrue(strpos($javascript, 'misc/jquery.js') !== FALSE, 'When "javascript_always_use_jquery" is FALSE: The JavaScript returned by drupal_get_js() when a custom JavaScript file has been added includes jquery.js.'); + $this->assertTrue(strpos($javascript, 'misc/drupal.js') !== FALSE, 'When "javascript_always_use_jquery" is FALSE: The JavaScript returned by drupal_get_js() when a custom JavaScript file has been added includes drupal.js.'); + $this->assertTrue(strpos($javascript, 'Drupal.settings') !== FALSE, 'When "javascript_always_use_jquery" is FALSE: The JavaScript returned by drupal_get_js() when a custom JavaScript file has been added includes Drupal.settings.'); + $this->assertTrue(strpos($javascript, 'basePath') !== FALSE, 'When "javascript_always_use_jquery" is FALSE: The JavaScript returned by drupal_get_js() when a custom JavaScript file has been added includes the basePath Drupal setting.'); + $this->assertTrue(strpos($javascript, 'misc/collapse.js') !== FALSE, 'When "javascript_always_use_jquery" is FALSE: The JavaScript returned by drupal_get_js() when a custom JavaScript file has been added includes the custom file.'); + variable_del('javascript_always_use_jquery'); + + // When a file that does not require jQuery has been added via + // drupal_add_js(), drupal_get_js() should still return jQuery and other + // standard scripts and settings by default. + $this->resetStaticVariables(); + drupal_add_js('misc/collapse.js', array('requires_jquery' => FALSE)); + $javascript = drupal_get_js(); + $this->assertTrue(strpos($javascript, 'misc/jquery.js') !== FALSE, 'Default behavior: The JavaScript returned by drupal_get_js() when a custom JavaScript file that does not require jQuery has been added includes jquery.js.'); + $this->assertTrue(strpos($javascript, 'misc/drupal.js') !== FALSE, 'Default behavior: The JavaScript returned by drupal_get_js() when a custom JavaScript file that does not require jQuery has been added includes drupal.js.'); + $this->assertTrue(strpos($javascript, 'Drupal.settings') !== FALSE, 'Default behavior: The JavaScript returned by drupal_get_js() when a custom JavaScript file that does not require jQuery has been added includes Drupal.settings.'); + $this->assertTrue(strpos($javascript, 'basePath') !== FALSE, 'Default behavior: The JavaScript returned by drupal_get_js() when a custom JavaScript file that does not require jQuery has been added includes the basePath Drupal setting.'); + $this->assertTrue(strpos($javascript, 'misc/collapse.js') !== FALSE, 'Default behavior: The JavaScript returned by drupal_get_js() when a custom JavaScript file that does not require jQuery has been added includes the custom file.'); + + // When a file that does not require jQuery has been added via + // drupal_add_js() and the 'javascript_always_use_jquery' variable is set + // to FALSE, drupal_get_js() should not return jQuery and other standard + // scripts and setting, but it should still return the requested file. + $this->resetStaticVariables(); + variable_set('javascript_always_use_jquery', FALSE); + drupal_add_js('misc/collapse.js', array('requires_jquery' => FALSE)); + $javascript = drupal_get_js(); + $this->assertTrue(strpos($javascript, 'misc/jquery.js') === FALSE, 'When "javascript_always_use_jquery" is FALSE: The JavaScript returned by drupal_get_js() when a custom JavaScript file that does not require jQuery has been added does not include jquery.js.'); + $this->assertTrue(strpos($javascript, 'misc/drupal.js') === FALSE, 'When "javascript_always_use_jquery" is FALSE: The JavaScript returned by drupal_get_js() when a custom JavaScript file that does not require jQuery has been added does not include drupal.js.'); + $this->assertTrue(strpos($javascript, 'Drupal.settings') === FALSE, 'When "javascript_always_use_jquery" is FALSE: The JavaScript returned by drupal_get_js() when a custom JavaScript file that does not require jQuery has been added does not include Drupal.settings.'); + $this->assertTrue(strpos($javascript, 'basePath') === FALSE, 'When "javascript_always_use_jquery" is FALSE: The JavaScript returned by drupal_get_js() when a custom JavaScript file that does not require jQuery has been added does not include the basePath Drupal setting.'); + $this->assertTrue(strpos($javascript, 'misc/collapse.js') !== FALSE, 'When "javascript_always_use_jquery" is FALSE: The JavaScript returned by drupal_get_js() when a custom JavaScript file that does not require jQuery has been added includes the custom file.'); + variable_del('javascript_always_use_jquery'); + + // When 'javascript_always_use_jquery' is set to FALSE and a file that does + // not require jQuery is added, followed by one that does, drupal_get_js() + // should return jQuery and other standard scripts and settings, in + // addition to both of the requested files. + $this->resetStaticVariables(); + variable_set('javascript_always_use_jquery', FALSE); + drupal_add_js('misc/collapse.js', array('requires_jquery' => FALSE)); + drupal_add_js('misc/ajax.js'); + $javascript = drupal_get_js(); + $this->assertTrue(strpos($javascript, 'misc/jquery.js') !== FALSE, 'When "javascript_always_use_jquery" is FALSE: The JavaScript returned by drupal_get_js() when at least one custom JavaScript file that requires jQuery has been added includes jquery.js.'); + $this->assertTrue(strpos($javascript, 'misc/drupal.js') !== FALSE, 'When "javascript_always_use_jquery" is FALSE: The JavaScript returned by drupal_get_js() when at least one custom JavaScript file that requires jQuery has been added includes drupal.js.'); + $this->assertTrue(strpos($javascript, 'Drupal.settings') !== FALSE, 'When "javascript_always_use_jquery" is FALSE: The JavaScript returned by drupal_get_js() when at least one custom JavaScript file that requires jQuery has been added includes Drupal.settings.'); + $this->assertTrue(strpos($javascript, 'basePath') !== FALSE, 'When "javascript_always_use_jquery" is FALSE: The JavaScript returned by drupal_get_js() when at least one custom JavaScript file that requires jQuery has been added includes the basePath Drupal setting.'); + $this->assertTrue(strpos($javascript, 'misc/collapse.js') !== FALSE, 'When "javascript_always_use_jquery" is FALSE: The JavaScript returned by drupal_get_js() when at least one custom JavaScript file that requires jQuery has been added includes the first custom file.'); + $this->assertTrue(strpos($javascript, 'misc/ajax.js') !== FALSE, 'When "javascript_always_use_jquery" is FALSE: The JavaScript returned by drupal_get_js() when at least one custom JavaScript file that requires jQuery has been added includes the second custom file.'); + variable_del('javascript_always_use_jquery'); + } + /** * Test drupal_add_js() sets preproccess to false when cache is set to false. */ @@ -1648,6 +1883,15 @@ class JavaScriptTestCase extends DrupalWebTestCase { $query_string = variable_get('css_js_query_string', '0'); $this->assertRaw(drupal_get_path('module', 'node') . '/node.js?' . $query_string, 'Query string was appended correctly to js.'); } + + /** + * Resets static variables related to adding JavaScript to a page. + */ + function resetStaticVariables() { + drupal_static_reset('drupal_add_js'); + drupal_static_reset('drupal_add_library'); + drupal_static_reset('drupal_get_library'); + } } /** @@ -1966,7 +2210,7 @@ class DrupalRenderTestCase extends DrupalWebTestCase { } /** - * Tests caching of an empty render item. + * Tests caching of render items. */ function testDrupalRenderCache() { // Force a request via GET. @@ -1992,6 +2236,59 @@ class DrupalRenderTestCase extends DrupalWebTestCase { drupal_render($element); $this->assertFalse(isset($element['#printed']), 'Cache hit'); + // Test that user 1 does not share the cache with other users who have the + // same roles, even when DRUPAL_CACHE_PER_ROLE is used. + $user1 = user_load(1); + $first_authenticated_user = $this->drupalCreateUser(); + $second_authenticated_user = $this->drupalCreateUser(); + $user1->roles = array_intersect_key($user1->roles, array(DRUPAL_AUTHENTICATED_RID => TRUE)); + user_save($user1); + // Load all the accounts again, to make sure we have complete account + // objects. + $user1 = user_load(1); + $first_authenticated_user = user_load($first_authenticated_user->uid); + $second_authenticated_user = user_load($second_authenticated_user->uid); + $this->assertEqual($user1->roles, $first_authenticated_user->roles, 'User 1 has the same roles as an authenticated user.'); + // Impersonate user 1 and render content that only user 1 should have + // permission to see. + $original_user = $GLOBALS['user']; + $original_session_state = drupal_save_session(); + drupal_save_session(FALSE); + $GLOBALS['user'] = $user1; + $test_element = array( + '#cache' => array( + 'keys' => array('test'), + 'granularity' => DRUPAL_CACHE_PER_ROLE, + ), + ); + $element = $test_element; + $element['#markup'] = 'content for user 1'; + $output = drupal_render($element); + $this->assertEqual($output, 'content for user 1'); + // Verify the cache is working by rendering the same element but with + // different markup passed in; the result should be the same. + $element = $test_element; + $element['#markup'] = 'should not be used'; + $output = drupal_render($element); + $this->assertEqual($output, 'content for user 1'); + // Verify that the first authenticated user does not see the same content + // as user 1. + $GLOBALS['user'] = $first_authenticated_user; + $element = $test_element; + $element['#markup'] = 'content for authenticated users'; + $output = drupal_render($element); + $this->assertEqual($output, 'content for authenticated users'); + // Verify that the second authenticated user shares the cache with the + // first authenticated user. + $GLOBALS['user'] = $second_authenticated_user; + $element = $test_element; + $element['#markup'] = 'should not be used'; + $output = drupal_render($element); + $this->assertEqual($output, 'content for authenticated users'); + // Restore the original logged-in user. + $GLOBALS['user'] = $original_user; + drupal_save_session($original_session_state); + // Restore the previous request method. $_SERVER['REQUEST_METHOD'] = $request_method; } diff --git a/modules/simpletest/tests/common_test.info b/modules/simpletest/tests/common_test.info index c8f4aeae..7d466800 100644 --- a/modules/simpletest/tests/common_test.info +++ b/modules/simpletest/tests/common_test.info @@ -7,8 +7,8 @@ stylesheets[all][] = common_test.css stylesheets[print][] = common_test.print.css hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/simpletest/tests/common_test.module b/modules/simpletest/tests/common_test.module index 674a4944..2eb8cd5d 100644 --- a/modules/simpletest/tests/common_test.module +++ b/modules/simpletest/tests/common_test.module @@ -92,6 +92,15 @@ function common_test_drupal_goto_alter(&$path, &$options, &$http_response_code) } } +/** + * Implements hook_init(). + */ +function common_test_init() { + if (variable_get('common_test_redirect_current_path', FALSE)) { + drupal_goto(current_path()); + } +} + /** * Print destination query parameter. */ diff --git a/modules/simpletest/tests/common_test_cron_helper.info b/modules/simpletest/tests/common_test_cron_helper.info index 780e5449..55dbd67a 100644 --- a/modules/simpletest/tests/common_test_cron_helper.info +++ b/modules/simpletest/tests/common_test_cron_helper.info @@ -5,8 +5,8 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/simpletest/tests/database_test.info b/modules/simpletest/tests/database_test.info index d755deb0..0e31a226 100644 --- a/modules/simpletest/tests/database_test.info +++ b/modules/simpletest/tests/database_test.info @@ -5,8 +5,8 @@ package = Testing version = VERSION hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/simpletest/tests/database_test.test b/modules/simpletest/tests/database_test.test index 12ddb343..59d2e5d6 100644 --- a/modules/simpletest/tests/database_test.test +++ b/modules/simpletest/tests/database_test.test @@ -238,7 +238,7 @@ class DatabaseConnectionTestCase extends DatabaseTestCase { // Open the default target so we have an object to compare. $db1 = Database::getConnection('default', 'default'); - // Try to close the the default connection, then open a new one. + // Try to close the default connection, then open a new one. Database::closeConnection('default', 'default'); $db2 = Database::getConnection('default', 'default'); @@ -1414,10 +1414,47 @@ class DatabaseSelectTestCase extends DatabaseTestCase { } $query = (string)$query; - $expected = "/* Testing query comments SELECT nid FROM {node}; -- */ SELECT test.name AS name, test.age AS age\nFROM \n{test} test"; + $expected = "/* Testing query comments * / SELECT nid FROM {node}; -- */ SELECT test.name AS name, test.age AS age\nFROM \n{test} test"; $this->assertEqual($num_records, 4, 'Returned the correct number of rows.'); $this->assertEqual($query, $expected, 'The flattened query contains the sanitised comment string.'); + + $connection = Database::getConnection(); + foreach ($this->makeCommentsProvider() as $test_set) { + list($expected, $comments) = $test_set; + $this->assertEqual($expected, $connection->makeComment($comments)); + } + } + + /** + * Provides expected and input values for testVulnerableComment(). + */ + function makeCommentsProvider() { + return array( + array( + '/* */ ', + array(''), + ), + // Try and close the comment early. + array( + '/* Exploit * / DROP TABLE node; -- */ ', + array('Exploit */ DROP TABLE node; --'), + ), + // Variations on comment closing. + array( + '/* Exploit * / * / DROP TABLE node; -- */ ', + array('Exploit */*/ DROP TABLE node; --'), + ), + array( + '/* Exploit * * // DROP TABLE node; -- */ ', + array('Exploit **// DROP TABLE node; --'), + ), + // Try closing the comment in the second string which is appended. + array( + '/* Exploit * / DROP TABLE node; --; Another try * / DROP TABLE node; -- */ ', + array('Exploit */ DROP TABLE node; --', 'Another try */ DROP TABLE node; --'), + ), + ); } /** @@ -3454,12 +3491,14 @@ class DatabaseTransactionTestCase extends DatabaseTestCase { } /** - * Helper method for transaction unit test. This "outer layer" transaction - * starts and then encapsulates the "inner layer" transaction. This nesting - * is used to evaluate whether the the database transaction API properly - * supports nesting. By "properly supports," we mean the outer transaction - * continues to exist regardless of what functions are called and whether - * those functions start their own transactions. + * Helper method for transaction unit test. + * + * This "outer layer" transaction starts and then encapsulates the + * "inner layer" transaction. This nesting is used to evaluate whether the + * database transaction API properly supports nesting. By "properly supports," + * we mean the outer transaction continues to exist regardless of what + * functions are called and whether those functions start their own + * transactions. * * In contrast, a typical database would commit the outer transaction, start * a new transaction for the inner layer, commit the inner layer transaction, diff --git a/modules/simpletest/tests/drupal_autoload_test/drupal_autoload_test.info b/modules/simpletest/tests/drupal_autoload_test/drupal_autoload_test.info new file mode 100644 index 00000000..18044661 --- /dev/null +++ b/modules/simpletest/tests/drupal_autoload_test/drupal_autoload_test.info @@ -0,0 +1,14 @@ +name = "Drupal code registry test" +description = "Support module for testing the code registry." +files[] = drupal_autoload_test_interface.inc +files[] = drupal_autoload_test_class.inc +package = Testing +version = VERSION +core = 7.x +hidden = TRUE + +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" +project = "drupal" +datestamp = "1481152423" + diff --git a/modules/simpletest/tests/drupal_autoload_test/drupal_autoload_test.module b/modules/simpletest/tests/drupal_autoload_test/drupal_autoload_test.module new file mode 100644 index 00000000..edd5d77c --- /dev/null +++ b/modules/simpletest/tests/drupal_autoload_test/drupal_autoload_test.module @@ -0,0 +1,22 @@ +name == 'drupal_autoload_test' && version_compare(PHP_VERSION, '5.4') >= 0) { + $files["$module->dir/drupal_autoload_test_trait.sh"] = array( + 'module' => $module->name, + 'weight' => $module->weight, + ); + } + } +} diff --git a/modules/simpletest/tests/drupal_autoload_test/drupal_autoload_test_class.inc b/modules/simpletest/tests/drupal_autoload_test/drupal_autoload_test_class.inc new file mode 100644 index 00000000..afd72fe6 --- /dev/null +++ b/modules/simpletest/tests/drupal_autoload_test/drupal_autoload_test_class.inc @@ -0,0 +1,11 @@ +drupalCreateUser(); - // Create a file with a size of 1000 bytes, and quotas of only 1 byte. $file = new stdClass(); $file->filesize = 1000; @@ -498,9 +491,6 @@ class FileValidatorTest extends DrupalWebTestCase { $this->assertEqual(count($errors), 1, 'Error for the user being over their limit.', 'File'); $errors = file_validate_size($file, 1, 1); $this->assertEqual(count($errors), 2, 'Errors for both the file and their limit.', 'File'); - - $user = $original_user; - drupal_save_session(TRUE); } } diff --git a/modules/simpletest/tests/file_test.info b/modules/simpletest/tests/file_test.info index dbeb7505..6f8d7529 100644 --- a/modules/simpletest/tests/file_test.info +++ b/modules/simpletest/tests/file_test.info @@ -6,8 +6,8 @@ core = 7.x files[] = file_test.module hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/simpletest/tests/filter_test.info b/modules/simpletest/tests/filter_test.info index d30cfa5a..315b0472 100644 --- a/modules/simpletest/tests/filter_test.info +++ b/modules/simpletest/tests/filter_test.info @@ -5,8 +5,8 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/simpletest/tests/form.test b/modules/simpletest/tests/form.test index f90b854c..6bf2d9e8 100644 --- a/modules/simpletest/tests/form.test +++ b/modules/simpletest/tests/form.test @@ -470,6 +470,64 @@ class FormsTestCase extends DrupalWebTestCase { $this->drupalPost(NULL, array('checkboxes[one]' => TRUE, 'checkboxes[two]' => TRUE), t('Submit')); $this->assertText('An illegal choice has been detected.', 'Input forgery was detected.'); } + + /** + * Tests that submitted values are converted to scalar strings for textfields. + */ + public function testTextfieldStringValue() { + // Check multivalued submissions. + $multivalue = array('evil' => 'multivalue', 'not so' => 'good'); + $this->checkFormValue('textfield', $multivalue, ''); + $this->checkFormValue('password', $multivalue, ''); + $this->checkFormValue('textarea', $multivalue, ''); + $this->checkFormValue('machine_name', $multivalue, ''); + $this->checkFormValue('password_confirm', $multivalue, array('pass1' => '', 'pass2' => '')); + // Check integer submissions. + $integer = 5; + $string = '5'; + $this->checkFormValue('textfield', $integer, $string); + $this->checkFormValue('password', $integer, $string); + $this->checkFormValue('textarea', $integer, $string); + $this->checkFormValue('machine_name', $integer, $string); + $this->checkFormValue('password_confirm', array('pass1' => $integer, 'pass2' => $integer), array('pass1' => $string, 'pass2' => $string)); + // Check that invalid array keys are ignored for password confirm elements. + $this->checkFormValue('password_confirm', array('pass1' => 'test', 'pass2' => 'test', 'extra' => 'invalid'), array('pass1' => 'test', 'pass2' => 'test')); + } + + /** + * Checks that a given form input value is sanitized to the expected result. + * + * @param string $element_type + * The form element type. Example: textfield. + * @param mixed $input_value + * The submitted user input value for the form element. + * @param mixed $expected_value + * The sanitized result value in the form state after calling + * form_builder(). + */ + protected function checkFormValue($element_type, $input_value, $expected_value) { + $form_id = $this->randomName(); + $form = array(); + $form_state = form_state_defaults(); + $form['op'] = array('#type' => 'submit', '#value' => t('Submit')); + $form[$element_type] = array( + '#type' => $element_type, + '#title' => 'test', + ); + + $form_state['input'][$element_type] = $input_value; + $form_state['input']['form_id'] = $form_id; + $form_state['method'] = 'post'; + $form_state['values'] = array(); + drupal_prepare_form($form_id, $form, $form_state); + + // This is the main function we want to test: it is responsible for + // populating user supplied $form_state['input'] to sanitized + // $form_state['values']. + form_builder($form_id, $form, $form_state); + + $this->assertIdentical($form_state['values'][$element_type], $expected_value, format_string('Form submission for the "@element_type" element type has been correctly sanitized.', array('@element_type' => $element_type))); + } } /** @@ -936,6 +994,26 @@ class FormsElementsTableSelectFunctionalTest extends DrupalWebTestCase { $this->assertTrue(isset($errors['tableselect']), 'Option checker disallows invalid values for radio buttons.'); } + /** + * Test presence of ajax functionality + */ + function testAjax() { + $rows = array('row1', 'row2', 'row3'); + // Test checkboxes (#multiple == TRUE). + foreach ($rows as $row) { + $element = 'tableselect[' . $row . ']'; + $edit = array($element => TRUE); + $result = $this->drupalPostAJAX('form_test/tableselect/multiple-true', $edit, $element); + $this->assertFalse(empty($result), t('Ajax triggers on checkbox for @row.', array('@row' => $row))); + } + // Test radios (#multiple == FALSE). + $element = 'tableselect'; + foreach ($rows as $row) { + $edit = array($element => $row); + $result = $this->drupalPostAjax('form_test/tableselect/multiple-false', $edit, $element); + $this->assertFalse(empty($result), t('Ajax triggers on radio for @row.', array('@row' => $row))); + } + } /** * Helper function for the option check test to submit a form while collecting errors. @@ -2041,3 +2119,36 @@ class HTMLIdTestCase extends DrupalWebTestCase { $this->assertNoDuplicateIds('There are no duplicate IDs'); } } + +/** + * Tests for form textarea. + */ +class FormTextareaTestCase extends DrupalUnitTestCase { + + public static function getInfo() { + return array( + 'name' => 'Form textarea', + 'description' => 'Tests form textarea related functions.', + 'group' => 'Form API', + ); + } + + /** + * Tests that textarea value is properly set. + */ + public function testValueCallback() { + $element = array(); + $form_state = array(); + $test_cases = array( + array(NULL, FALSE), + array(NULL, NULL), + array('', array('test')), + array('test', 'test'), + array('123', 123), + ); + foreach ($test_cases as $test_case) { + list($expected, $input) = $test_case; + $this->assertIdentical($expected, form_type_textarea_value($element, $input, $form_state)); + } + } +} diff --git a/modules/simpletest/tests/form_test.info b/modules/simpletest/tests/form_test.info index 406bcd27..c2b36e78 100644 --- a/modules/simpletest/tests/form_test.info +++ b/modules/simpletest/tests/form_test.info @@ -5,8 +5,8 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/simpletest/tests/form_test.module b/modules/simpletest/tests/form_test.module index 602b4090..4fd708f1 100644 --- a/modules/simpletest/tests/form_test.module +++ b/modules/simpletest/tests/form_test.module @@ -589,11 +589,17 @@ function _form_test_tableselect_form_builder($form, $form_state, $element_proper $form['tableselect'] = $element_properties; $form['tableselect'] += array( + '#prefix' => '
          ', + '#suffix' => '
          ', '#type' => 'tableselect', '#header' => $header, '#options' => $options, '#multiple' => FALSE, '#empty' => t('Empty text.'), + '#ajax' => array( + 'callback' => '_form_test_tableselect_ajax_callback', + 'wrapper' => 'tableselect-wrapper', + ), ); $form['submit'] = array( @@ -697,6 +703,13 @@ function _form_test_vertical_tabs_form($form, &$form_state) { return $form; } +/** +* Ajax callback that returns the form element. +*/ +function _form_test_tableselect_ajax_callback($form, &$form_state) { + return $form['tableselect']; +} + /** * A multistep form for testing the form storage. * diff --git a/modules/simpletest/tests/image.test b/modules/simpletest/tests/image.test index dc95a6e2..7ca1d3a0 100644 --- a/modules/simpletest/tests/image.test +++ b/modules/simpletest/tests/image.test @@ -207,9 +207,11 @@ class ImageToolkitGdTestCase extends DrupalWebTestCase { protected $green = array(0, 255, 0, 0); protected $blue = array(0, 0, 255, 0); protected $yellow = array(255, 255, 0, 0); - protected $fuchsia = array(255, 0, 255, 0); // Used as background colors. - protected $transparent = array(0, 0, 0, 127); protected $white = array(255, 255, 255, 0); + protected $transparent = array(0, 0, 0, 127); + // Used as rotate background colors. + protected $fuchsia = array(255, 0, 255, 0); + protected $rotate_transparent = array(255, 255, 255, 127); protected $width = 40; protected $height = 20; @@ -261,6 +263,7 @@ class ImageToolkitGdTestCase extends DrupalWebTestCase { */ function testManipulations() { // If GD isn't available don't bother testing this. + module_load_include('inc', 'system', 'image.gd'); if (!function_exists('image_gd_check_settings') || !image_gd_check_settings()) { $this->pass(t('Image manipulations for the GD toolkit were skipped because the GD toolkit is not available.')); return; @@ -274,6 +277,7 @@ class ImageToolkitGdTestCase extends DrupalWebTestCase { $files = array( 'image-test.png', 'image-test.gif', + 'image-test-no-transparency.gif', 'image-test.jpg', ); @@ -333,13 +337,6 @@ class ImageToolkitGdTestCase extends DrupalWebTestCase { // Systems using non-bundled GD2 don't have imagerotate. Test if available. if (function_exists('imagerotate')) { $operations += array( - 'rotate_5' => array( - 'function' => 'rotate', - 'arguments' => array(5, 0xFF00FF), // Fuchsia background. - 'width' => 42, - 'height' => 24, - 'corners' => array_fill(0, 4, $this->fuchsia), - ), 'rotate_90' => array( 'function' => 'rotate', 'arguments' => array(90, 0xFF00FF), // Fuchsia background. @@ -347,13 +344,6 @@ class ImageToolkitGdTestCase extends DrupalWebTestCase { 'height' => 40, 'corners' => array($this->fuchsia, $this->red, $this->green, $this->blue), ), - 'rotate_transparent_5' => array( - 'function' => 'rotate', - 'arguments' => array(5), - 'width' => 42, - 'height' => 24, - 'corners' => array_fill(0, 4, $this->transparent), - ), 'rotate_transparent_90' => array( 'function' => 'rotate', 'arguments' => array(90), @@ -362,6 +352,49 @@ class ImageToolkitGdTestCase extends DrupalWebTestCase { 'corners' => array($this->transparent, $this->red, $this->green, $this->blue), ), ); + // As of PHP version 5.5, GD uses a different algorithm to rotate images + // than version 5.4 and below, resulting in different dimensions. + // See https://bugs.php.net/bug.php?id=65148. + // For the 40x20 test images, the dimensions resulting from rotation will + // be 1 pixel smaller in both width and height in PHP 5.5 and above. + // @todo: If and when the PHP bug gets solved, add an upper limit + // version check. + if (version_compare(PHP_VERSION, '5.5', '>=')) { + $operations += array( + 'rotate_5' => array( + 'function' => 'rotate', + 'arguments' => array(5, 0xFF00FF), // Fuchsia background. + 'width' => 41, + 'height' => 23, + 'corners' => array_fill(0, 4, $this->fuchsia), + ), + 'rotate_transparent_5' => array( + 'function' => 'rotate', + 'arguments' => array(5), + 'width' => 41, + 'height' => 23, + 'corners' => array_fill(0, 4, $this->rotate_transparent), + ), + ); + } + else { + $operations += array( + 'rotate_5' => array( + 'function' => 'rotate', + 'arguments' => array(5, 0xFF00FF), // Fuchsia background. + 'width' => 42, + 'height' => 24, + 'corners' => array_fill(0, 4, $this->fuchsia), + ), + 'rotate_transparent_5' => array( + 'function' => 'rotate', + 'arguments' => array(5), + 'width' => 42, + 'height' => 24, + 'corners' => array_fill(0, 4, $this->rotate_transparent), + ), + ); + } } // Systems using non-bundled GD2 don't have imagefilter. Test if available. @@ -379,7 +412,7 @@ class ImageToolkitGdTestCase extends DrupalWebTestCase { array_fill(0, 3, 76) + array(3 => 0), array_fill(0, 3, 149) + array(3 => 0), array_fill(0, 3, 29) + array(3 => 0), - array_fill(0, 3, 0) + array(3 => 127) + array_fill(0, 3, 225) + array(3 => 127) ), ), ); @@ -394,11 +427,14 @@ class ImageToolkitGdTestCase extends DrupalWebTestCase { continue 2; } - // Transparent GIFs and the imagefilter function don't work together. - // There is a todo in image.gd.inc to correct this. + // All images should be converted to truecolor when loaded. + $image_truecolor = imageistruecolor($image->resource); + $this->assertTrue($image_truecolor, format_string('Image %file after load is a truecolor image.', array('%file' => $file))); + if ($image->info['extension'] == 'gif') { if ($op == 'desaturate') { - $values['corners'][3] = $this->white; + // Transparent GIFs and the imagefilter function don't work together. + $values['corners'][3][3] = 0; } } @@ -426,6 +462,11 @@ class ImageToolkitGdTestCase extends DrupalWebTestCase { } // Now check each of the corners to ensure color correctness. foreach ($values['corners'] as $key => $corner) { + // The test gif that does not have transparency has yellow where the + // others have transparent. + if ($file === 'image-test-no-transparency.gif' && $corner === $this->transparent) { + $corner = $this->yellow; + } // Get the location of the corner. switch ($key) { case 0: @@ -451,7 +492,8 @@ class ImageToolkitGdTestCase extends DrupalWebTestCase { $directory = file_default_scheme() . '://imagetests'; file_prepare_directory($directory, FILE_CREATE_DIRECTORY); - image_save($image, $directory . '/' . $op . '.' . $image->info['extension']); + $file_path = $directory . '/' . $op . '.' . $image->info['extension']; + image_save($image, $file_path); $this->assertTrue($correct_dimensions_real, format_string('Image %file after %action action has proper dimensions.', array('%file' => $file, '%action' => $op))); $this->assertTrue($correct_dimensions_object, format_string('Image %file object after %action action is reporting the proper height and width values.', array('%file' => $file, '%action' => $op))); @@ -460,8 +502,37 @@ class ImageToolkitGdTestCase extends DrupalWebTestCase { $this->assertTrue($correct_colors, format_string('Image %file object after %action action has the correct color placement.', array('%file' => $file, '%action' => $op))); } } - } + // Check that saved image reloads without raising PHP errors. + $image_reloaded = image_load($file_path); + } + } + + /** + * Tests loading an image whose transparent color index is out of range. + */ + function testTransparentColorOutOfRange() { + // This image was generated by taking an initial image with a palette size + // of 6 colors, and setting the transparent color index to 6 (one higher + // than the largest allowed index), as follows: + // @code + // $image = imagecreatefromgif('modules/simpletest/files/image-test.gif'); + // imagecolortransparent($image, 6); + // imagegif($image, 'modules/simpletest/files/image-test-transparent-out-of-range.gif'); + // @endcode + // This allows us to test that an image with an out-of-range color index + // can be loaded correctly. + $file = 'image-test-transparent-out-of-range.gif'; + $image = image_load(drupal_get_path('module', 'simpletest') . '/files/' . $file); + + if (!$image) { + $this->fail(format_string('Could not load image %file.', array('%file' => $file))); + } + else { + // All images should be converted to truecolor when loaded. + $image_truecolor = imageistruecolor($image->resource); + $this->assertTrue($image_truecolor, format_string('Image %file after load is a truecolor image.', array('%file' => $file))); + } } } diff --git a/modules/simpletest/tests/image_test.info b/modules/simpletest/tests/image_test.info index b851329d..a6efccb6 100644 --- a/modules/simpletest/tests/image_test.info +++ b/modules/simpletest/tests/image_test.info @@ -5,8 +5,8 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/simpletest/tests/mail.test b/modules/simpletest/tests/mail.test index 70a43cb4..3e40e13a 100644 --- a/modules/simpletest/tests/mail.test +++ b/modules/simpletest/tests/mail.test @@ -441,7 +441,7 @@ class DrupalHtmlToTextTestCase extends DrupalWebTestCase { * is 1000 characters." */ function testVeryLongLineWrap() { - $input = 'Drupal

          ' . str_repeat('x', 2100) . '
          Drupal'; + $input = 'Drupal

          ' . str_repeat('x', 2100) . '


          Drupal'; $output = drupal_html_to_text($input); // This awkward construct comes from includes/mail.inc lines 8-13. $eol = variable_get('mail_line_endings', MAIL_LINE_ENDINGS); @@ -455,7 +455,6 @@ class DrupalHtmlToTextTestCase extends DrupalWebTestCase { $maximum_line_length = max($maximum_line_length, strlen($line . $eol)); } $verbose = 'Maximum line length found was ' . $maximum_line_length . ' octets.'; - // @todo This should assert that $maximum_line_length <= 1000. - $this->pass($verbose); + $this->assertTrue($maximum_line_length <= 1000, $verbose); } } diff --git a/modules/simpletest/tests/menu_test.info b/modules/simpletest/tests/menu_test.info index e7509b5e..92e04302 100644 --- a/modules/simpletest/tests/menu_test.info +++ b/modules/simpletest/tests/menu_test.info @@ -5,8 +5,8 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/simpletest/tests/module.test b/modules/simpletest/tests/module.test index 371339f3..eea3b51e 100644 --- a/modules/simpletest/tests/module.test +++ b/modules/simpletest/tests/module.test @@ -302,3 +302,45 @@ class ModuleUninstallTestCase extends DrupalWebTestCase { $this->assertEqual(0, $count, 'Permissions were all removed.'); } } + +class ModuleImplementsAlterTestCase extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'Module implements alter', + 'description' => 'Tests hook_module_implements_alter().', + 'group' => 'Module', + ); + } + + /** + * Tests hook_module_implements_alter() adding an implementation. + */ + function testModuleImplementsAlter() { + module_enable(array('module_test'), FALSE); + $this->assertTrue(module_exists('module_test'), 'Test module is enabled.'); + + // Assert that module_test.module is now included. + $this->assertTrue(function_exists('module_test_permission'), + 'The file module_test.module was successfully included.'); + + $modules = module_implements('permission'); + $this->assertTrue(in_array('module_test', $modules), 'module_test implements hook_permission.'); + + $modules = module_implements('module_implements_alter'); + $this->assertTrue(in_array('module_test', $modules), 'module_test implements hook_module_implements_alter().'); + + // Assert that module_test.implementations.inc is not included yet. + $this->assertFalse(function_exists('module_test_altered_test_hook'), + 'The file module_test.implementations.inc is not included yet.'); + + // Assert that module_test_module_implements_alter(*, 'altered_test_hook') + // has added an implementation + $this->assertTrue(in_array('module_test', module_implements('altered_test_hook')), + 'module_test implements hook_altered_test_hook().'); + + // Assert that module_test.implementations.inc was included as part of the process. + $this->assertTrue(function_exists('module_test_altered_test_hook'), + 'The file module_test.implementations.inc was included.'); + } + +} diff --git a/modules/simpletest/tests/module_test.implementations.inc b/modules/simpletest/tests/module_test.implementations.inc new file mode 100644 index 00000000..63c866ea --- /dev/null +++ b/modules/simpletest/tests/module_test.implementations.inc @@ -0,0 +1,10 @@ + 'PSR4 example test: PSR-4 in disabled modules.', + 'description' => 'We want to assert that this test case is being discovered.', + 'group' => 'SimpleTest', + ); + } + + function testArithmetics() { + $this->assert(1 + 1 == 2, '1 + 1 == 2'); + } +} diff --git a/modules/simpletest/tests/psr_4_test/src/Tests/Nested/NestedExampleTest.php b/modules/simpletest/tests/psr_4_test/src/Tests/Nested/NestedExampleTest.php new file mode 100644 index 00000000..ff3ac29d --- /dev/null +++ b/modules/simpletest/tests/psr_4_test/src/Tests/Nested/NestedExampleTest.php @@ -0,0 +1,18 @@ + 'PSR4 example test: PSR-4 in nested subfolders.', + 'description' => 'We want to assert that this PSR-4 test case is being discovered.', + 'group' => 'SimpleTest', + ); + } + + function testArithmetics() { + $this->assert(1 + 1 == 2, '1 + 1 == 2'); + } +} diff --git a/modules/simpletest/tests/requirements1_test.info b/modules/simpletest/tests/requirements1_test.info index 7d6b0114..6a3a6f8e 100644 --- a/modules/simpletest/tests/requirements1_test.info +++ b/modules/simpletest/tests/requirements1_test.info @@ -5,8 +5,8 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/simpletest/tests/requirements2_test.info b/modules/simpletest/tests/requirements2_test.info index af0032d6..b9738f95 100644 --- a/modules/simpletest/tests/requirements2_test.info +++ b/modules/simpletest/tests/requirements2_test.info @@ -7,8 +7,8 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/simpletest/tests/session.test b/modules/simpletest/tests/session.test index 097503b6..893d03e9 100644 --- a/modules/simpletest/tests/session.test +++ b/modules/simpletest/tests/session.test @@ -477,6 +477,56 @@ class SessionHttpsTestCase extends DrupalWebTestCase { $this->assertResponse(200); } + /** + * Tests that empty session IDs do not cause unrelated sessions to load. + */ + public function testEmptySessionId() { + global $is_https; + + if ($is_https) { + $secure_session_name = session_name(); + } + else { + $secure_session_name = 'S' . session_name(); + } + + // Enable mixed mode for HTTP and HTTPS. + variable_set('https', TRUE); + + $admin_user = $this->drupalCreateUser(array('access administration pages')); + $standard_user = $this->drupalCreateUser(array('access content')); + + // First log in as the admin user on HTTP. + // We cannot use $this->drupalLogin() here because we need to use the + // special http.php URLs. + $edit = array( + 'name' => $admin_user->name, + 'pass' => $admin_user->pass_raw + ); + $this->drupalGet('user'); + $form = $this->xpath('//form[@id="user-login"]'); + $form[0]['action'] = $this->httpUrl('user'); + $this->drupalPost(NULL, $edit, t('Log in')); + + $this->curlClose(); + + // Now start a session for the standard user on HTTPS. + $edit = array( + 'name' => $standard_user->name, + 'pass' => $standard_user->pass_raw + ); + $this->drupalGet('user'); + $form = $this->xpath('//form[@id="user-login"]'); + $form[0]['action'] = $this->httpsUrl('user'); + $this->drupalPost(NULL, $edit, t('Log in')); + + // Make the secure session cookie blank. + curl_setopt($this->curlHandle, CURLOPT_COOKIE, "$secure_session_name="); + $this->drupalGet($this->httpsUrl('user')); + $this->assertNoText($admin_user->name, 'User is not logged in as admin'); + $this->assertNoText($standard_user->name, "The user's own name is not displayed because the invalid session cookie has logged them out."); + } + /** * Test that there exists a session with two specific session IDs. * diff --git a/modules/simpletest/tests/session_test.info b/modules/simpletest/tests/session_test.info index 5851c3bb..20b846cf 100644 --- a/modules/simpletest/tests/session_test.info +++ b/modules/simpletest/tests/session_test.info @@ -5,8 +5,8 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/simpletest/tests/system_dependencies_test.info b/modules/simpletest/tests/system_dependencies_test.info index 8c198a06..1e74f9de 100644 --- a/modules/simpletest/tests/system_dependencies_test.info +++ b/modules/simpletest/tests/system_dependencies_test.info @@ -6,8 +6,8 @@ core = 7.x hidden = TRUE dependencies[] = _missing_dependency -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/simpletest/tests/system_incompatible_core_version_dependencies_test.info b/modules/simpletest/tests/system_incompatible_core_version_dependencies_test.info index cfa6983f..d918475f 100644 --- a/modules/simpletest/tests/system_incompatible_core_version_dependencies_test.info +++ b/modules/simpletest/tests/system_incompatible_core_version_dependencies_test.info @@ -6,8 +6,8 @@ core = 7.x hidden = TRUE dependencies[] = system_incompatible_core_version_test -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/simpletest/tests/system_incompatible_core_version_test.info b/modules/simpletest/tests/system_incompatible_core_version_test.info index 976bfe2f..1a8adc4a 100644 --- a/modules/simpletest/tests/system_incompatible_core_version_test.info +++ b/modules/simpletest/tests/system_incompatible_core_version_test.info @@ -5,8 +5,8 @@ version = VERSION core = 5.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/simpletest/tests/system_incompatible_module_version_dependencies_test.info b/modules/simpletest/tests/system_incompatible_module_version_dependencies_test.info index 3eb98492..b081db4e 100644 --- a/modules/simpletest/tests/system_incompatible_module_version_dependencies_test.info +++ b/modules/simpletest/tests/system_incompatible_module_version_dependencies_test.info @@ -7,8 +7,8 @@ hidden = TRUE ; system_incompatible_module_version_test declares version 1.0 dependencies[] = system_incompatible_module_version_test (>2.0) -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/simpletest/tests/system_incompatible_module_version_test.info b/modules/simpletest/tests/system_incompatible_module_version_test.info index be297a36..80440861 100644 --- a/modules/simpletest/tests/system_incompatible_module_version_test.info +++ b/modules/simpletest/tests/system_incompatible_module_version_test.info @@ -5,8 +5,8 @@ version = 1.0 core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/simpletest/tests/system_project_namespace_test.info b/modules/simpletest/tests/system_project_namespace_test.info new file mode 100644 index 00000000..d248a099 --- /dev/null +++ b/modules/simpletest/tests/system_project_namespace_test.info @@ -0,0 +1,13 @@ +name = "System project namespace test" +description = "Support module for testing project namespace dependencies." +package = Testing +version = VERSION +core = 7.x +hidden = TRUE +dependencies[] = drupal:filter + +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" +project = "drupal" +datestamp = "1481152423" + diff --git a/modules/simpletest/tests/system_project_namespace_test.module b/modules/simpletest/tests/system_project_namespace_test.module new file mode 100644 index 00000000..b3d9bbc7 --- /dev/null +++ b/modules/simpletest/tests/system_project_namespace_test.module @@ -0,0 +1 @@ + MENU_CALLBACK, ); + $items['system-test/drupal-set-message'] = array( + 'title' => 'Set messages with drupal_set_message()', + 'page callback' => 'system_test_drupal_set_message', + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); + $items['system-test/main-content-handling'] = array( 'title' => 'Test main content handling', 'page callback' => 'system_test_main_content_fallback', @@ -106,6 +113,34 @@ function system_test_menu() { 'type' => MENU_CALLBACK, ); + $items['system-test/get-destination'] = array( + 'title' => 'Test $_GET[\'destination\']', + 'page callback' => 'system_test_get_destination', + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); + + $items['system-test/request-destination'] = array( + 'title' => 'Test $_REQUEST[\'destination\']', + 'page callback' => 'system_test_request_destination', + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); + + $items['system-test/drupal-get-filename'] = array( + 'title' => 'Test drupal_get_filename()', + 'page callback' => 'system_test_drupal_get_filename', + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); + + $items['system-test/drupal-get-filename-with-schema-rebuild'] = array( + 'title' => 'Test drupal_get_filename() with a schema rebuild', + 'page callback' => 'system_test_drupal_get_filename_with_schema_rebuild', + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); + return $items; } @@ -275,6 +310,9 @@ function system_test_system_info_alter(&$info, $file, $type) { } } + if ($file->name == 'system_project_namespace_test') { + $info['hidden'] = FALSE; + } // Make the system_dependencies_test visible by default. if ($file->name == 'system_dependencies_test') { $info['hidden'] = FALSE; @@ -420,3 +458,114 @@ function system_test_authorize_init_page($page_title) { system_authorized_init('system_test_authorize_run', drupal_get_path('module', 'system_test') . '/system_test.module', array(), $page_title); drupal_goto($authorize_url); } + +/** + * Sets two messages and removes the first one before the messages are displayed. + */ +function system_test_drupal_set_message() { + // Set two messages. + drupal_set_message('First message (removed).'); + drupal_set_message('Second message (not removed).'); + + // Remove the first. + unset($_SESSION['messages']['status'][0]); + + return ''; +} + +/** + * Page callback to print out $_GET['destination'] for testing. + */ +function system_test_get_destination() { + if (isset($_GET['destination'])) { + print $_GET['destination']; + } + // No need to render the whole page, we are just interested in this bit of + // information. + exit; +} + +/** + * Page callback to print out $_REQUEST['destination'] for testing. + */ +function system_test_request_destination() { + if (isset($_REQUEST['destination'])) { + print $_REQUEST['destination']; + } + // No need to render the whole page, we are just interested in this bit of + // information. + exit; +} + +/** + * Page callback to run drupal_get_filename() on a particular module. + */ +function system_test_drupal_get_filename() { + // Prevent SimpleTest from failing as a result of the expected PHP warnings + // this function causes. Any warnings will be recorded in the database logs + // for examination by the tests. + define('SIMPLETEST_COLLECT_ERRORS', FALSE); + + $module_name = variable_get('system_test_drupal_get_filename_test_module_name'); + drupal_get_filename('module', $module_name); + + return ''; +} + +/** + * Page callback to run drupal_get_filename() and do a schema rebuild. + */ +function system_test_drupal_get_filename_with_schema_rebuild() { + // Prevent SimpleTest from failing as a result of the expected PHP warnings + // this function causes. + define('SIMPLETEST_COLLECT_ERRORS', FALSE); + + // Record the original database tables from drupal_get_schema(). + variable_set('system_test_drupal_get_filename_with_schema_rebuild_original_tables', array_keys(drupal_get_schema(NULL, TRUE))); + + // Trigger system_test_schema() and system_test_watchdog() to perform an + // attempted recursive rebuild when drupal_get_schema() is called. See + // BootstrapGetFilenameWebTestCase::testRecursiveRebuilds(). + variable_set('system_test_drupal_get_filename_attempt_recursive_rebuild', TRUE); + drupal_get_schema(NULL, TRUE); + + return ''; +} + +/** + * Implements hook_watchdog(). + */ +function system_test_watchdog($log_entry) { + // If an attempted recursive schema rebuild has been triggered by + // system_test_drupal_get_filename_with_schema_rebuild(), perform the rebuild + // in response to the missing file message triggered by system_test_schema(). + if (!variable_get('system_test_drupal_get_filename_attempt_recursive_rebuild')) { + return; + } + if ($log_entry['type'] != 'php' || $log_entry['severity'] != WATCHDOG_WARNING) { + return; + } + $module_name = variable_get('system_test_drupal_get_filename_test_module_name'); + if (!isset($log_entry['variables']['!message']) || strpos($log_entry['variables']['!message'], format_string('The following module is missing from the file system: %name', array('%name' => $module_name))) === FALSE) { + return; + } + variable_set('system_test_drupal_get_filename_with_schema_rebuild_final_tables', array_keys(drupal_get_schema())); +} + +/** + * Implements hook_module_implements_alter(). + */ +function system_test_module_implements_alter(&$implementations, $hook) { + // For BootstrapGetFilenameWebTestCase::testRecursiveRebuilds() to work + // correctly, this module's hook_schema() implementation cannot be either the + // first implementation (since that would trigger a potential recursive + // rebuild before anything is in the drupal_get_schema() cache) or the last + // implementation (since that would trigger a potential recursive rebuild + // after the cache is already complete). So put it somewhere in the middle. + if ($hook == 'schema') { + $group = $implementations['system_test']; + unset($implementations['system_test']); + $count = count($implementations); + $implementations = array_merge(array_slice($implementations, 0, $count / 2, TRUE), array('system_test' => $group), array_slice($implementations, $count / 2, NULL, TRUE)); + } +} diff --git a/modules/simpletest/tests/taxonomy_test.info b/modules/simpletest/tests/taxonomy_test.info index e6d525fb..1dffb58c 100644 --- a/modules/simpletest/tests/taxonomy_test.info +++ b/modules/simpletest/tests/taxonomy_test.info @@ -6,8 +6,8 @@ core = 7.x hidden = TRUE dependencies[] = taxonomy -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/simpletest/tests/taxonomy_test.module b/modules/simpletest/tests/taxonomy_test.module index f82950c3..f4144380 100644 --- a/modules/simpletest/tests/taxonomy_test.module +++ b/modules/simpletest/tests/taxonomy_test.module @@ -109,3 +109,33 @@ function taxonomy_test_get_antonym($tid) { ->execute() ->fetchField(); } + +/** + * Implements hook_query_alter(). + */ +function taxonomy_test_query_alter(QueryAlterableInterface $query) { + $value = variable_get(__FUNCTION__); + if (isset($value)) { + variable_set(__FUNCTION__, ++$value); + } +} + +/** + * Implements hook_query_TAG_alter(). + */ +function taxonomy_test_query_term_access_alter(QueryAlterableInterface $query) { + $value = variable_get(__FUNCTION__); + if (isset($value)) { + variable_set(__FUNCTION__, ++$value); + } +} + +/** + * Implements hook_query_TAG_alter(). + */ +function taxonomy_test_query_taxonomy_term_access_alter(QueryAlterableInterface $query) { + $value = variable_get(__FUNCTION__); + if (isset($value)) { + variable_set(__FUNCTION__, ++$value); + } +} diff --git a/modules/simpletest/tests/theme.test b/modules/simpletest/tests/theme.test index f1a743e0..f5ddfa9b 100644 --- a/modules/simpletest/tests/theme.test +++ b/modules/simpletest/tests/theme.test @@ -155,6 +155,15 @@ class ThemeTestCase extends DrupalWebTestCase { $this->assertNotEqual(theme_get_setting('subtheme_override', 'test_basetheme'), theme_get_setting('subtheme_override', 'test_subtheme'), 'Base theme\'s default settings values can be overridden by subtheme.'); $this->assertIdentical(theme_get_setting('basetheme_only', 'test_subtheme'), 'base theme value', 'Base theme\'s default settings values are inherited by subtheme.'); } + + /** + * Test the drupal_add_region_content() function. + */ + function testDrupalAddRegionContent() { + $this->drupalGet('theme-test/drupal-add-region-content'); + $this->assertText('Hello'); + $this->assertText('World'); + } } /** diff --git a/modules/simpletest/tests/theme_test.info b/modules/simpletest/tests/theme_test.info index 49696626..ab7d0b70 100644 --- a/modules/simpletest/tests/theme_test.info +++ b/modules/simpletest/tests/theme_test.info @@ -5,8 +5,8 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/simpletest/tests/theme_test.module b/modules/simpletest/tests/theme_test.module index 61a12bb7..948d8175 100644 --- a/modules/simpletest/tests/theme_test.module +++ b/modules/simpletest/tests/theme_test.module @@ -53,6 +53,11 @@ function theme_test_menu() { 'access callback' => TRUE, 'type' => MENU_CALLBACK, ); + $items['theme-test/drupal-add-region-content'] = array( + 'page callback' => '_theme_test_drupal_add_region_content', + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); return $items; } @@ -126,6 +131,14 @@ function _theme_test_suggestion() { return theme(array('theme_test__suggestion', 'theme_test'), array()); } +/** + * Page callback, calls drupal_add_region_content. + */ +function _theme_test_drupal_add_region_content() { + drupal_add_region_content('content', 'World'); + return 'Hello'; +} + /** * Theme function for testing theme('theme_test_foo'). */ diff --git a/modules/simpletest/tests/themes/test_basetheme/test_basetheme.info b/modules/simpletest/tests/themes/test_basetheme/test_basetheme.info index 534537e3..ea39d8e9 100644 --- a/modules/simpletest/tests/themes/test_basetheme/test_basetheme.info +++ b/modules/simpletest/tests/themes/test_basetheme/test_basetheme.info @@ -6,8 +6,8 @@ hidden = TRUE settings[basetheme_only] = base theme value settings[subtheme_override] = base theme value -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/simpletest/tests/themes/test_subtheme/test_subtheme.info b/modules/simpletest/tests/themes/test_subtheme/test_subtheme.info index 3797325b..60bb4e5c 100644 --- a/modules/simpletest/tests/themes/test_subtheme/test_subtheme.info +++ b/modules/simpletest/tests/themes/test_subtheme/test_subtheme.info @@ -6,8 +6,8 @@ hidden = TRUE settings[subtheme_override] = subtheme value -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/simpletest/tests/themes/test_theme/test_theme.info b/modules/simpletest/tests/themes/test_theme/test_theme.info index c6bf3406..0370bc6a 100644 --- a/modules/simpletest/tests/themes/test_theme/test_theme.info +++ b/modules/simpletest/tests/themes/test_theme/test_theme.info @@ -17,8 +17,8 @@ stylesheets[all][] = system.base.css settings[theme_test_setting] = default value -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/simpletest/tests/update_script_test.info b/modules/simpletest/tests/update_script_test.info index ab974c51..af18d119 100644 --- a/modules/simpletest/tests/update_script_test.info +++ b/modules/simpletest/tests/update_script_test.info @@ -5,8 +5,8 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/simpletest/tests/update_script_test.install b/modules/simpletest/tests/update_script_test.install index 6955ef11..4024fb4a 100644 --- a/modules/simpletest/tests/update_script_test.install +++ b/modules/simpletest/tests/update_script_test.install @@ -31,6 +31,19 @@ function update_script_test_requirements($phase) { 'severity' => REQUIREMENT_ERROR, ); break; + case REQUIREMENT_INFO: + $requirements['update_script_test_stop'] = array( + 'title' => 'Update script test stop', + 'value' => 'Error', + 'description' => 'This is a requirements error provided by the update_script_test module to stop the page redirect for the info.', + 'severity' => REQUIREMENT_ERROR, + ); + $requirements['update_script_test'] = array( + 'title' => 'Update script test', + 'description' => 'This is a requirements info provided by the update_script_test module.', + 'severity' => REQUIREMENT_INFO, + ); + break; } } diff --git a/modules/simpletest/tests/update_test_1.info b/modules/simpletest/tests/update_test_1.info index 01763a35..b64ad361 100644 --- a/modules/simpletest/tests/update_test_1.info +++ b/modules/simpletest/tests/update_test_1.info @@ -5,8 +5,8 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/simpletest/tests/update_test_2.info b/modules/simpletest/tests/update_test_2.info index 01763a35..b64ad361 100644 --- a/modules/simpletest/tests/update_test_2.info +++ b/modules/simpletest/tests/update_test_2.info @@ -5,8 +5,8 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/simpletest/tests/update_test_3.info b/modules/simpletest/tests/update_test_3.info index 01763a35..b64ad361 100644 --- a/modules/simpletest/tests/update_test_3.info +++ b/modules/simpletest/tests/update_test_3.info @@ -5,8 +5,8 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/simpletest/tests/upgrade/drupal-6.filled.database.php b/modules/simpletest/tests/upgrade/drupal-6.filled.database.php index a9162813..10b9040c 100644 --- a/modules/simpletest/tests/upgrade/drupal-6.filled.database.php +++ b/modules/simpletest/tests/upgrade/drupal-6.filled.database.php @@ -19919,7 +19919,7 @@ db_insert('vocabulary')->fields(array( 'vid' => '1', 'name' => 'vocabulary 1 (i=0)', 'description' => 'description of vocabulary 1 (i=0)', - 'help' => '', + 'help' => 'help for vocabulary 1 (i=0)', 'relations' => '1', 'hierarchy' => '0', 'multiple' => '0', @@ -19932,7 +19932,7 @@ db_insert('vocabulary')->fields(array( 'vid' => '2', 'name' => 'vocabulary 2 (i=1)', 'description' => 'description of vocabulary 2 (i=1)', - 'help' => '', + 'help' => 'help for vocabulary 2 (i=1)', 'relations' => '1', 'hierarchy' => '1', 'multiple' => '1', @@ -19945,7 +19945,7 @@ db_insert('vocabulary')->fields(array( 'vid' => '3', 'name' => 'vocabulary 3 (i=2)', 'description' => 'description of vocabulary 3 (i=2)', - 'help' => '', + 'help' => 'help for vocabulary 3 (i=2)', 'relations' => '1', 'hierarchy' => '2', 'multiple' => '0', @@ -19958,7 +19958,7 @@ db_insert('vocabulary')->fields(array( 'vid' => '4', 'name' => 'vocabulary 4 (i=3)', 'description' => 'description of vocabulary 4 (i=3)', - 'help' => '', + 'help' => 'help for vocabulary 4 (i=3)', 'relations' => '1', 'hierarchy' => '0', 'multiple' => '1', @@ -19971,7 +19971,7 @@ db_insert('vocabulary')->fields(array( 'vid' => '5', 'name' => 'vocabulary 5 (i=4)', 'description' => 'description of vocabulary 5 (i=4)', - 'help' => '', + 'help' => 'help for vocabulary 5 (i=4)', 'relations' => '1', 'hierarchy' => '1', 'multiple' => '0', @@ -19984,7 +19984,7 @@ db_insert('vocabulary')->fields(array( 'vid' => '6', 'name' => 'vocabulary 6 (i=5)', 'description' => 'description of vocabulary 6 (i=5)', - 'help' => '', + 'help' => 'help for vocabulary 6 (i=5)', 'relations' => '1', 'hierarchy' => '2', 'multiple' => '1', @@ -19997,7 +19997,7 @@ db_insert('vocabulary')->fields(array( 'vid' => '7', 'name' => 'vocabulary 7 (i=6)', 'description' => 'description of vocabulary 7 (i=6)', - 'help' => '', + 'help' => 'help for vocabulary 7 (i=6)', 'relations' => '1', 'hierarchy' => '0', 'multiple' => '0', @@ -20010,7 +20010,7 @@ db_insert('vocabulary')->fields(array( 'vid' => '8', 'name' => 'vocabulary 8 (i=7)', 'description' => 'description of vocabulary 8 (i=7)', - 'help' => '', + 'help' => 'help for vocabulary 8 (i=7)', 'relations' => '1', 'hierarchy' => '1', 'multiple' => '1', @@ -20023,7 +20023,7 @@ db_insert('vocabulary')->fields(array( 'vid' => '9', 'name' => 'vocabulary 9 (i=8)', 'description' => 'description of vocabulary 9 (i=8)', - 'help' => '', + 'help' => 'help for vocabulary 9 (i=8)', 'relations' => '1', 'hierarchy' => '2', 'multiple' => '0', @@ -20036,7 +20036,7 @@ db_insert('vocabulary')->fields(array( 'vid' => '10', 'name' => 'vocabulary 10 (i=9)', 'description' => 'description of vocabulary 10 (i=9)', - 'help' => '', + 'help' => 'help for vocabulary 10 (i=9)', 'relations' => '1', 'hierarchy' => '0', 'multiple' => '1', @@ -20049,7 +20049,7 @@ db_insert('vocabulary')->fields(array( 'vid' => '11', 'name' => 'vocabulary 11 (i=10)', 'description' => 'description of vocabulary 11 (i=10)', - 'help' => '', + 'help' => 'help for vocabulary 11 (i=10)', 'relations' => '1', 'hierarchy' => '1', 'multiple' => '0', @@ -20062,7 +20062,7 @@ db_insert('vocabulary')->fields(array( 'vid' => '12', 'name' => 'vocabulary 12 (i=11)', 'description' => 'description of vocabulary 12 (i=11)', - 'help' => '', + 'help' => 'help for vocabulary 12 (i=11)', 'relations' => '1', 'hierarchy' => '2', 'multiple' => '1', @@ -20075,7 +20075,7 @@ db_insert('vocabulary')->fields(array( 'vid' => '13', 'name' => 'vocabulary 13 (i=12)', 'description' => 'description of vocabulary 13 (i=12)', - 'help' => '', + 'help' => 'help for vocabulary 13 (i=12)', 'relations' => '1', 'hierarchy' => '0', 'multiple' => '0', @@ -20088,7 +20088,7 @@ db_insert('vocabulary')->fields(array( 'vid' => '14', 'name' => 'vocabulary 14 (i=13)', 'description' => 'description of vocabulary 14 (i=13)', - 'help' => '', + 'help' => 'help for vocabulary 14 (i=13)', 'relations' => '1', 'hierarchy' => '1', 'multiple' => '1', @@ -20101,7 +20101,7 @@ db_insert('vocabulary')->fields(array( 'vid' => '15', 'name' => 'vocabulary 15 (i=14)', 'description' => 'description of vocabulary 15 (i=14)', - 'help' => '', + 'help' => 'help for vocabulary 15 (i=14)', 'relations' => '1', 'hierarchy' => '2', 'multiple' => '0', @@ -20114,7 +20114,7 @@ db_insert('vocabulary')->fields(array( 'vid' => '16', 'name' => 'vocabulary 16 (i=15)', 'description' => 'description of vocabulary 16 (i=15)', - 'help' => '', + 'help' => 'help for vocabulary 16 (i=15)', 'relations' => '1', 'hierarchy' => '0', 'multiple' => '1', @@ -20127,7 +20127,7 @@ db_insert('vocabulary')->fields(array( 'vid' => '17', 'name' => 'vocabulary 17 (i=16)', 'description' => 'description of vocabulary 17 (i=16)', - 'help' => '', + 'help' => 'help for vocabulary 17 (i=16)', 'relations' => '1', 'hierarchy' => '1', 'multiple' => '0', @@ -20140,7 +20140,7 @@ db_insert('vocabulary')->fields(array( 'vid' => '18', 'name' => 'vocabulary 18 (i=17)', 'description' => 'description of vocabulary 18 (i=17)', - 'help' => '', + 'help' => 'help for vocabulary 18 (i=17)', 'relations' => '1', 'hierarchy' => '2', 'multiple' => '1', @@ -20153,7 +20153,7 @@ db_insert('vocabulary')->fields(array( 'vid' => '19', 'name' => 'vocabulary 19 (i=18)', 'description' => 'description of vocabulary 19 (i=18)', - 'help' => '', + 'help' => 'help for vocabulary 19 (i=18)', 'relations' => '1', 'hierarchy' => '0', 'multiple' => '0', @@ -20166,7 +20166,7 @@ db_insert('vocabulary')->fields(array( 'vid' => '20', 'name' => 'vocabulary 20 (i=19)', 'description' => 'description of vocabulary 20 (i=19)', - 'help' => '', + 'help' => 'help for vocabulary 20 (i=19)', 'relations' => '1', 'hierarchy' => '1', 'multiple' => '1', @@ -20179,7 +20179,7 @@ db_insert('vocabulary')->fields(array( 'vid' => '21', 'name' => 'vocabulary 21 (i=20)', 'description' => 'description of vocabulary 21 (i=20)', - 'help' => '', + 'help' => 'help for vocabulary 21 (i=20)', 'relations' => '1', 'hierarchy' => '2', 'multiple' => '0', @@ -20192,7 +20192,7 @@ db_insert('vocabulary')->fields(array( 'vid' => '22', 'name' => 'vocabulary 22 (i=21)', 'description' => 'description of vocabulary 22 (i=21)', - 'help' => '', + 'help' => 'help for vocabulary 22 (i=21)', 'relations' => '1', 'hierarchy' => '0', 'multiple' => '1', @@ -20205,7 +20205,7 @@ db_insert('vocabulary')->fields(array( 'vid' => '23', 'name' => 'vocabulary 23 (i=22)', 'description' => 'description of vocabulary 23 (i=22)', - 'help' => '', + 'help' => 'help for vocabulary 23 (i=22)', 'relations' => '1', 'hierarchy' => '1', 'multiple' => '0', @@ -20218,7 +20218,7 @@ db_insert('vocabulary')->fields(array( 'vid' => '24', 'name' => 'vocabulary 24 (i=23)', 'description' => 'description of vocabulary 24 (i=23)', - 'help' => '', + 'help' => 'help for vocabulary 24 (i=23)', 'relations' => '1', 'hierarchy' => '2', 'multiple' => '1', diff --git a/modules/simpletest/tests/upgrade/drupal-6.upload.database.php b/modules/simpletest/tests/upgrade/drupal-6.upload.database.php index 46ebe2cb..3fd602af 100644 --- a/modules/simpletest/tests/upgrade/drupal-6.upload.database.php +++ b/modules/simpletest/tests/upgrade/drupal-6.upload.database.php @@ -127,6 +127,38 @@ db_insert('files')->fields(array( 'status' => '1', 'timestamp' => '1285708958', )) +// On some Drupal 6 sites, more than one file can have the same filepath. See +// https://www.drupal.org/node/1260938. +->values(array( + 'fid' => '12', + 'uid' => '1', + 'filename' => 'duplicate-name.png', + 'filepath' => 'sites/default/files/duplicate-name.png', + 'filemime' => 'image/png', + 'filesize' => '314', + 'status' => '1', + 'timestamp' => '1285708958', +)) +->values(array( + 'fid' => '13', + 'uid' => '1', + 'filename' => 'duplicate-name.png', + 'filepath' => 'sites/default/files/duplicate-name.png', + 'filemime' => 'image/png', + 'filesize' => '315', + 'status' => '1', + 'timestamp' => '1285708958', +)) +->values(array( + 'fid' => '14', + 'uid' => '1', + 'filename' => 'duplicate-name.png', + 'filepath' => 'sites/default/files/duplicate-name.png', + 'filemime' => 'image/png', + 'filesize' => '316', + 'status' => '1', + 'timestamp' => '1285708958', +)) ->execute(); db_insert('node')->fields(array( @@ -196,6 +228,23 @@ db_insert('node')->fields(array( 'sticky' => '0', 'tnid' => '0', 'translate' => '0', +)) +->values(array( + 'nid' => '41', + 'vid' => '55', + 'type' => 'page', + 'language' => '', + 'title' => 'node title 41 revision 55', + 'uid' => '1', + 'status' => '1', + 'created' => '1285709012', + 'changed' => '1285709012', + 'comment' => '0', + 'promote' => '0', + 'moderate' => '0', + 'sticky' => '0', + 'tnid' => '0', + 'translate' => '0', )) ->execute(); @@ -253,6 +302,28 @@ db_insert('node_revisions')->fields(array( 'log' => '', 'timestamp' => '1285709012', 'format' => '1', +)) +->values(array( + 'nid' => '41', + 'vid' => '54', + 'uid' => '1', + 'title' => 'node title 41 revision 54', + 'body' => "Attachments:\r\nduplicate-name.png", + 'teaser' => "Attachments:\r\nduplicate-name.png", + 'log' => '', + 'timestamp' => '1285709012', + 'format' => '1', +)) +->values(array( + 'nid' => '41', + 'vid' => '55', + 'uid' => '1', + 'title' => 'node title 41 revision 55', + 'body' => "Attachments:\r\nduplicate-name.png\r\nduplicate-name.png", + 'teaser' => "Attachments:\r\nduplicate-name.png\r\nduplicate-name.png", + 'log' => '', + 'timestamp' => '1285709012', + 'format' => '1', )) ->execute(); @@ -415,6 +486,30 @@ db_insert('upload')->fields(array( 'list' => '1', 'weight' => '0', )) +->values(array( + 'fid' => '12', + 'nid' => '41', + 'vid' => '54', + 'description' => 'duplicate-name.png', + 'list' => '1', + 'weight' => '0', +)) +->values(array( + 'fid' => '13', + 'nid' => '41', + 'vid' => '55', + 'description' => 'first description', + 'list' => '0', + 'weight' => '0', +)) +->values(array( + 'fid' => '14', + 'nid' => '41', + 'vid' => '55', + 'description' => 'second description', + 'list' => '1', + 'weight' => '0', +)) ->execute(); // Add series of entries for invalid node vids to the {upload} table. @@ -431,7 +526,7 @@ for ($i = 30; $i < 250; $i += 2) { ->values(array( 'fid' => $i, 'nid' => '40', - 'vid' => 24 + $i, + 'vid' => 26 + $i, 'description' => 'crazy-basename.png', 'list' => '1', 'weight' => '0', @@ -440,7 +535,7 @@ for ($i = 30; $i < 250; $i += 2) { ->values(array( 'fid' => 2, 'nid' => '40', - 'vid' => 24 + $i + 1, + 'vid' => 26 + $i + 1, 'description' => 'crazy-basename.png', 'list' => '1', 'weight' => '0', diff --git a/modules/simpletest/tests/upgrade/upgrade.taxonomy.test b/modules/simpletest/tests/upgrade/upgrade.taxonomy.test index 58a4d5c1..51402ed7 100644 --- a/modules/simpletest/tests/upgrade/upgrade.taxonomy.test +++ b/modules/simpletest/tests/upgrade/upgrade.taxonomy.test @@ -74,9 +74,10 @@ class UpgradePathTaxonomyTestCase extends UpgradePathTestCase { $this->assertEqual($voc_keys, $inst_keys, 'Node type page has instances for every vocabulary.'); // Ensure instance variables are getting through. - foreach ($instances as $instance) { - $this->assertTrue(isset($instance['required']), 'The required setting was preserved during the upgrade path.'); - $this->assertTrue($instance['description'], 'The description was preserved during the upgrade path'); + foreach (array_unique($instances) as $instance) { + $field_instance = field_info_instance('node', $instance, 'page'); + $this->assertTrue(isset($field_instance['required']), 'The required setting was preserved during the upgrade path.'); + $this->assertTrue($field_instance['description'], 'The description was preserved during the upgrade path'); } // Node type 'story' was not explicitly in $vocabulary->nodes but diff --git a/modules/simpletest/tests/upgrade/upgrade.upload.test b/modules/simpletest/tests/upgrade/upgrade.upload.test index be352bd4..dfa94a00 100644 --- a/modules/simpletest/tests/upgrade/upgrade.upload.test +++ b/modules/simpletest/tests/upgrade/upgrade.upload.test @@ -64,12 +64,35 @@ class UploadUpgradePathTestCase extends UpgradePathTestCase { } $this->assertIdentical($filenames, $recorded_filenames, 'The uploaded files are present in the same order after the upgrade.'); } + // Test for the file with repeating basename to only have the streaming // path replaced. $node = node_load(40, 53); $repeated_basename_file = $node->upload[LANGUAGE_NONE][4]; $this->assertEqual($repeated_basename_file['uri'], 'private://drupal-6/file/directory/path/crazy-basename.png', "The file with the repeated basename path only had the stream portion replaced"); + // Ensure that filepaths are deduplicated. + $node0 = node_load(41, 54); + $node1 = node_load(41, 55); + // Ensure that both revisions point to the same file ID. + $items0 = field_get_items('node', $node0, 'upload'); + $this->assertEqual(count($items0), 1); + $items1 = field_get_items('node', $node1, 'upload'); + $this->assertEqual(count($items1), 2); + $this->assertEqual($items0[0]['fid'], $items1[0]['fid']); + $this->assertEqual($items0[0]['fid'], $items1[1]['fid']); + // The revision with more than one reference to the same file should retain + // the original settings for each reference. + $this->assertEqual($items1[0]['description'], 'first description'); + $this->assertEqual($items1[0]['display'], 0); + $this->assertEqual($items1[1]['description'], 'second description'); + $this->assertEqual($items1[1]['display'], 1); + // Ensure that the latest version of the files are used. + $this->assertEqual($items1[0]['filesize'], 316); + $this->assertEqual($items1[1]['filesize'], 316); + // No duplicate files should remain on the Drupal 7 site. + $this->assertEqual(0, db_query("SELECT COUNT(*) FROM {file_managed} GROUP BY uri HAVING COUNT(fid) > 1")->fetchField()); + // Make sure the file settings were properly migrated. $d6_file_directory_temp = '/drupal-6/file/directory/temp'; $d6_file_directory_path = '/drupal-6/file/directory/path'; diff --git a/modules/simpletest/tests/url_alter_test.info b/modules/simpletest/tests/url_alter_test.info index af00b0f5..f7924d30 100644 --- a/modules/simpletest/tests/url_alter_test.info +++ b/modules/simpletest/tests/url_alter_test.info @@ -5,8 +5,8 @@ package = Testing version = VERSION hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/simpletest/tests/xmlrpc.test b/modules/simpletest/tests/xmlrpc.test index 1a9ef234..bb74f059 100644 --- a/modules/simpletest/tests/xmlrpc.test +++ b/modules/simpletest/tests/xmlrpc.test @@ -246,4 +246,38 @@ class XMLRPCMessagesTestCase extends DrupalWebTestCase { $this->assertEqual($removed, 'system.methodSignature', 'Hiding builting system.methodSignature with hook_xmlrpc_alter works'); } + /** + * Test limits on system.multicall that can prevent brute-force attacks. + */ + function testMulticallLimit() { + $url = url(NULL, array('absolute' => TRUE)) . 'xmlrpc.php'; + $multicall_args = array(); + $num_method_calls = 10; + for ($i = 0; $i < $num_method_calls; $i++) { + $struct = array('i' => $i); + $multicall_args[] = array('methodName' => 'validator1.echoStructTest', 'params' => array($struct)); + } + // Test limits of 1, 5, 9, 13. + for ($limit = 1; $limit < $num_method_calls + 4; $limit += 4) { + variable_set('xmlrpc_multicall_duplicate_method_limit', $limit); + $results = xmlrpc($url, array('system.multicall' => array($multicall_args))); + $this->assertEqual($num_method_calls, count($results)); + for ($i = 0; $i < min($limit, $num_method_calls); $i++) { + $x = array_shift($results); + $this->assertTrue(empty($x->is_error), "Result $i is not an error"); + $this->assertEqual($multicall_args[$i]['params'][0], $x); + } + for (; $i < $num_method_calls; $i++) { + $x = array_shift($results); + $this->assertFalse(empty($x->is_error), "Result $i is an error"); + $this->assertEqual(-156579, $x->code); + } + } + variable_set('xmlrpc_multicall_duplicate_method_limit', -1); + $results = xmlrpc($url, array('system.multicall' => array($multicall_args))); + $this->assertEqual($num_method_calls, count($results)); + foreach ($results as $i => $x) { + $this->assertTrue(empty($x->is_error), "Result $i is not an error"); + } + } } diff --git a/modules/simpletest/tests/xmlrpc_test.info b/modules/simpletest/tests/xmlrpc_test.info index 11139833..cbc93637 100644 --- a/modules/simpletest/tests/xmlrpc_test.info +++ b/modules/simpletest/tests/xmlrpc_test.info @@ -5,8 +5,8 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/statistics/statistics.admin.inc b/modules/statistics/statistics.admin.inc index e83a115f..f061081e 100644 --- a/modules/statistics/statistics.admin.inc +++ b/modules/statistics/statistics.admin.inc @@ -59,7 +59,7 @@ function statistics_recent_hits() { * statistics settings form, but is dependent on cron running. * * @return - * A render array containing information about the the top pages. + * A render array containing information about the top pages. */ function statistics_top_pages() { $header = array( diff --git a/modules/statistics/statistics.info b/modules/statistics/statistics.info index 6c818c3e..03d8a30f 100644 --- a/modules/statistics/statistics.info +++ b/modules/statistics/statistics.info @@ -6,8 +6,8 @@ core = 7.x files[] = statistics.test configure = admin/config/system/statistics -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/statistics/statistics.module b/modules/statistics/statistics.module index f665a14f..b2561d24 100644 --- a/modules/statistics/statistics.module +++ b/modules/statistics/statistics.module @@ -118,10 +118,9 @@ function statistics_node_view($node, $view_mode) { // Attach Ajax node count statistics if configured. if (variable_get('statistics_count_content_views', 0) && variable_get('statistics_count_content_views_ajax', 0)) { if (!empty($node->nid) && $view_mode == 'full' && node_is_page($node) && empty($node->in_preview)) { - $node->content['#attached']['js'] = array( - drupal_get_path('module', 'statistics') . '/statistics.js' => array( - 'scope' => 'footer' - ), + $statistics = drupal_get_path('module', 'statistics') . '/statistics.js'; + $node->content['#attached']['js'][$statistics] = array( + 'scope' => 'footer', ); $settings = array('data' => array('nid' => $node->nid), 'url' => url(drupal_get_path('module', 'statistics') . '/statistics.php')); $node->content['#attached']['js'][] = array( diff --git a/modules/statistics/statistics.php b/modules/statistics/statistics.php index f00e0397..48340c89 100644 --- a/modules/statistics/statistics.php +++ b/modules/statistics/statistics.php @@ -15,17 +15,19 @@ chdir(DRUPAL_ROOT); include_once DRUPAL_ROOT . '/includes/bootstrap.inc'; drupal_bootstrap(DRUPAL_BOOTSTRAP_VARIABLES); if (variable_get('statistics_count_content_views', 0) && variable_get('statistics_count_content_views_ajax', 0)) { - $nid = $_POST['nid']; - if (is_numeric($nid)) { - db_merge('node_counter') - ->key(array('nid' => $nid)) - ->fields(array( - 'daycount' => 1, - 'totalcount' => 1, - 'timestamp' => REQUEST_TIME, - )) - ->expression('daycount', 'daycount + 1') - ->expression('totalcount', 'totalcount + 1') - ->execute(); + if (isset($_POST['nid'])) { + $nid = $_POST['nid']; + if (is_numeric($nid)) { + db_merge('node_counter') + ->key(array('nid' => $nid)) + ->fields(array( + 'daycount' => 1, + 'totalcount' => 1, + 'timestamp' => REQUEST_TIME, + )) + ->expression('daycount', 'daycount + 1') + ->expression('totalcount', 'totalcount + 1') + ->execute(); + } } } diff --git a/modules/statistics/statistics.test b/modules/statistics/statistics.test index 0498bb76..50accd74 100644 --- a/modules/statistics/statistics.test +++ b/modules/statistics/statistics.test @@ -35,7 +35,7 @@ class StatisticsTestCase extends DrupalWebTestCase { 'title' => 'test', 'path' => 'node/1', 'url' => 'http://example.com', - 'hostname' => '192.168.1.1', + 'hostname' => '1.2.3.3', 'uid' => 0, 'sid' => 10, 'timer' => 10, @@ -268,7 +268,7 @@ class StatisticsBlockVisitorsTestCase extends StatisticsTestCase { */ function testIPAddressBlocking() { // IP address for testing. - $test_ip_address = '192.168.1.1'; + $test_ip_address = '1.2.3.3'; // Verify the IP address from accesslog appears on the top visitors page // and that a 'block IP address' link is displayed. @@ -414,7 +414,7 @@ class StatisticsAdminTestCase extends DrupalWebTestCase { $timestamp = time(); $this->drupalPost(NULL, NULL, t('Cancel account')); // Confirm account cancellation request. - $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login)); + $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid)); $this->assertFalse(user_load($account->uid, TRUE), 'User is not found in the database.'); $this->drupalGet('admin/reports/visitors'); diff --git a/modules/syslog/syslog.info b/modules/syslog/syslog.info index b12b1270..00ad4ff7 100644 --- a/modules/syslog/syslog.info +++ b/modules/syslog/syslog.info @@ -6,8 +6,8 @@ core = 7.x files[] = syslog.test configure = admin/config/development/logging -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/system/image.gd.inc b/modules/system/image.gd.inc index d9035e4c..3d0797e4 100644 --- a/modules/system/image.gd.inc +++ b/modules/system/image.gd.inc @@ -116,38 +116,62 @@ function image_gd_rotate(stdClass $image, $degrees, $background = NULL) { return FALSE; } - $width = $image->info['width']; - $height = $image->info['height']; + // PHP 5.5 GD bug: https://bugs.php.net/bug.php?id=65148: To prevent buggy + // behavior on negative multiples of 90 degrees we convert any negative + // angle to a positive one between 0 and 360 degrees. + $degrees -= floor($degrees / 360) * 360; - // Convert the hexadecimal background value to a color index value. + // Convert the hexadecimal background value to a RGBA array. if (isset($background)) { - $rgb = array(); - for ($i = 16; $i >= 0; $i -= 8) { - $rgb[] = (($background >> $i) & 0xFF); - } - $background = imagecolorallocatealpha($image->resource, $rgb[0], $rgb[1], $rgb[2], 0); + $background = array( + 'red' => $background >> 16 & 0xFF, + 'green' => $background >> 8 & 0xFF, + 'blue' => $background & 0xFF, + 'alpha' => 0, + ); } - // Set the background color as transparent if $background is NULL. else { - // Get the current transparent color. - $background = imagecolortransparent($image->resource); - - // If no transparent colors, use white. - if ($background == 0) { - $background = imagecolorallocatealpha($image->resource, 255, 255, 255, 0); - } + // Background color is not specified: use transparent white as background. + $background = array( + 'red' => 255, + 'green' => 255, + 'blue' => 255, + 'alpha' => 127 + ); } + // Store the color index for the background as that is what GD uses. + $background_idx = imagecolorallocatealpha($image->resource, $background['red'], $background['green'], $background['blue'], $background['alpha']); + // Images are assigned a new color palette when rotating, removing any // transparency flags. For GIF images, keep a record of the transparent color. if ($image->info['extension'] == 'gif') { - $transparent_index = imagecolortransparent($image->resource); - if ($transparent_index != 0) { - $transparent_gif_color = imagecolorsforindex($image->resource, $transparent_index); + // GIF does not work with a transparency channel, but can define 1 color + // in its palette to act as transparent. + + // Get the current transparent color, if any. + $gif_transparent_id = imagecolortransparent($image->resource); + if ($gif_transparent_id !== -1) { + // The gif already has a transparent color set: remember it to set it on + // the rotated image as well. + $transparent_gif_color = imagecolorsforindex($image->resource, $gif_transparent_id); + + if ($background['alpha'] >= 127) { + // We want a transparent background: use the color already set to act + // as transparent, as background. + $background_idx = $gif_transparent_id; + } + } + else { + // The gif does not currently have a transparent color set. + if ($background['alpha'] >= 127) { + // But as the background is transparent, it should get one. + $transparent_gif_color = $background; + } } } - $image->resource = imagerotate($image->resource, 360 - $degrees, $background); + $image->resource = imagerotate($image->resource, 360 - $degrees, $background_idx); // GIFs need to reassign the transparent color after performing the rotate. if (isset($transparent_gif_color)) { @@ -229,7 +253,24 @@ function image_gd_desaturate(stdClass $image) { function image_gd_load(stdClass $image) { $extension = str_replace('jpg', 'jpeg', $image->info['extension']); $function = 'imagecreatefrom' . $extension; - return (function_exists($function) && $image->resource = $function($image->source)); + if (function_exists($function) && $image->resource = $function($image->source)) { + if (imageistruecolor($image->resource)) { + return TRUE; + } + else { + // Convert indexed images to truecolor, copying the image to a new + // truecolor resource, so that filters work correctly and don't result + // in unnecessary dither. + $resource = image_gd_create_tmp($image, $image->info['width'], $image->info['height']); + if ($resource) { + imagecopy($resource, $image->resource, 0, 0, 0, 0, imagesx($resource), imagesy($resource)); + imagedestroy($image->resource); + $image->resource = $resource; + } + } + return (bool) $image->resource; + } + return FALSE; } /** @@ -297,17 +338,31 @@ function image_gd_create_tmp(stdClass $image, $width, $height) { $res = imagecreatetruecolor($width, $height); if ($image->info['extension'] == 'gif') { - // Grab transparent color index from image resource. + // Find out if a transparent color is set, will return -1 if no + // transparent color has been defined in the image. $transparent = imagecolortransparent($image->resource); if ($transparent >= 0) { - // The original must have a transparent color, allocate to the new image. - $transparent_color = imagecolorsforindex($image->resource, $transparent); - $transparent = imagecolorallocate($res, $transparent_color['red'], $transparent_color['green'], $transparent_color['blue']); + // Find out the number of colors in the image palette. It will be 0 for + // truecolor images. + $palette_size = imagecolorstotal($image->resource); + if ($palette_size == 0 || $transparent < $palette_size) { + // Set the transparent color in the new resource, either if it is a + // truecolor image or if the transparent color is part of the palette. + // Since the index of the transparency color is a property of the + // image rather than of the palette, it is possible that an image + // could be created with this index set outside the palette size (see + // http://stackoverflow.com/a/3898007). + $transparent_color = imagecolorsforindex($image->resource, $transparent); + $transparent = imagecolorallocate($res, $transparent_color['red'], $transparent_color['green'], $transparent_color['blue']); - // Flood with our new transparent color. - imagefill($res, 0, 0, $transparent); - imagecolortransparent($res, $transparent); + // Flood with our new transparent color. + imagefill($res, 0, 0, $transparent); + imagecolortransparent($res, $transparent); + } + else { + imagefill($res, 0, 0, imagecolorallocate($res, 255, 255, 255)); + } } } elseif ($image->info['extension'] == 'png') { diff --git a/modules/system/system.admin.inc b/modules/system/system.admin.inc index 22c202c3..cdcc78fb 100644 --- a/modules/system/system.admin.inc +++ b/modules/system/system.admin.inc @@ -1856,7 +1856,7 @@ function system_image_toolkit_settings() { if (count($toolkits_available) == 0) { variable_del('image_toolkit'); $form['image_toolkit_help'] = array( - '#markup' => t("No image toolkits were detected. Drupal includes support for PHP's built-in image processing functions but they were not detected on this system. You should consult your system administrator to have them enabled, or try using a third party toolkit.", array('gd-link' => url('http://php.net/gd'))), + '#markup' => t("No image toolkits were detected. Drupal includes support for PHP's built-in image processing functions but they were not detected on this system. You should consult your system administrator to have them enabled, or try using a third party toolkit.", array('!gd-link' => url('http://php.net/gd'))), ); return $form; } @@ -2202,6 +2202,11 @@ function system_add_date_format_type_form_submit($form, &$form_state) { * Return the date for a given format string via Ajax. */ function system_date_time_lookup() { + // This callback is protected with a CSRF token because user input from the + // query string is reflected in the output. + if (!isset($_GET['token']) || !drupal_valid_token($_GET['token'], 'admin/config/regional/date-time/formats/lookup')) { + return MENU_ACCESS_DENIED; + } $result = format_date(REQUEST_TIME, 'custom', $_GET['format']); drupal_json_output($result); } @@ -2592,6 +2597,8 @@ function theme_status_report($variables) { if (empty($requirement['#type'])) { $severity = $severities[isset($requirement['severity']) ? (int) $requirement['severity'] : REQUIREMENT_OK]; $severity['icon'] = '
          ' . $severity['title'] . '
          '; + // The requirement's 'value' key is optional, provide a default value. + $requirement['value'] = isset($requirement['value']) ? $requirement['value'] : ''; // Output table row(s) if (!empty($requirement['description'])) { @@ -2646,8 +2653,8 @@ function theme_system_modules_fieldset($variables) { } $row[] = array('data' => $description, 'class' => array('description')); // Display links (such as help or permissions) in their own columns. - foreach (array('help', 'permissions', 'configure') as $key) { - $row[] = array('data' => drupal_render($module['links'][$key]), 'class' => array($key)); + foreach (array('help', 'permissions', 'configure') as $link_type) { + $row[] = array('data' => drupal_render($module['links'][$link_type]), 'class' => array($link_type)); } $rows[] = $row; } @@ -2875,13 +2882,14 @@ function system_date_time_formats() { * Allow users to add additional date formats. */ function system_configure_date_formats_form($form, &$form_state, $dfid = 0) { + $ajax_path = 'admin/config/regional/date-time/formats/lookup'; $js_settings = array( 'type' => 'setting', 'data' => array( 'dateTime' => array( 'date-format' => array( 'text' => t('Displayed as'), - 'lookup' => url('admin/config/regional/date-time/formats/lookup'), + 'lookup' => url($ajax_path, array('query' => array('token' => drupal_get_token($ajax_path)))), ), ), ), diff --git a/modules/system/system.api.php b/modules/system/system.api.php index b60f50de..3152139a 100644 --- a/modules/system/system.api.php +++ b/modules/system/system.api.php @@ -113,21 +113,21 @@ function hook_hook_info_alter(&$hooks) { * translation handlers. Array keys are the module names, array values * can be any data structure the module uses to provide field translation. * Any empty value disallows the module to appear as a translation handler. - * - entity keys: An array describing how the Field API can extract the - * information it needs from the objects of the type. Elements: + * - entity keys: (optional) An array describing how the Field API can extract + * the information it needs from the objects of the type. Elements: * - id: The name of the property that contains the primary id of the * entity. Every entity object passed to the Field API must have this * property and its value must be numeric. * - revision: The name of the property that contains the revision id of * the entity. The Field API assumes that all revision ids are unique * across all entities of a type. This entry can be omitted if the - * entities of this type are not versionable. + * entities of this type are not versionable. Defaults to an empty string. * - bundle: The name of the property that contains the bundle name for the * entity. The bundle name defines which set of fields are attached to * the entity (e.g. what nodes call "content type"). This entry can be * omitted if this entity type exposes a single bundle (all entities have * the same collection of fields). The name of this single bundle will be - * the same as the entity type. + * the same as the entity type. Defaults to an empty string. * - label: The name of the property that contains the entity label. For * example, if the entity's label is located in $entity->subject, then * 'subject' should be specified here. If complex logic is required to @@ -606,8 +606,8 @@ function hook_cron() { * @return * An associative array where the key is the queue name and the value is * again an associative array. Possible keys are: - * - 'worker callback': The name of the function to call. It will be called - * with one argument, the item created via DrupalQueue::createItem(). + * - 'worker callback': The name of an implementation of + * callback_queue_worker(). * - 'time': (optional) How much time Drupal should spend on calling this * worker in seconds. Defaults to 15. * - 'skip on cron': (optional) Set to TRUE to avoid being processed during @@ -1797,6 +1797,8 @@ function hook_form_BASE_FORM_ID_alter(&$form, &$form_state, $form_id) { * the $form_id input matched your module's format for dynamically-generated * form IDs, and if so, act appropriately. * + * Third, forms defined in classes can be defined this way. + * * @param $form_id * The unique string identifying the desired form. * @param $args @@ -1807,19 +1809,22 @@ function hook_form_BASE_FORM_ID_alter(&$form, &$form_state, $form_id) { * @return * An associative array whose keys define form_ids and whose values are an * associative array defining the following keys: - * - callback: The name of the form builder function to invoke. This will be - * used for the base form ID, for example, to target a base form using - * hook_form_BASE_FORM_ID_alter(). + * - callback: The callable returning the form array. If it is the name of + * the form builder function then this will be used for the base + * form ID, for example, to target a base form using + * hook_form_BASE_FORM_ID_alter(). Otherwise use the base_form_id key to + * define the base form ID. * - callback arguments: (optional) Additional arguments to pass to the * function defined in 'callback', which are prepended to $args. - * - wrapper_callback: (optional) The name of a form builder function to - * invoke before the form builder defined in 'callback' is invoked. This - * wrapper callback may prepopulate the $form array with form elements, - * which will then be already contained in the $form that is passed on to - * the form builder defined in 'callback'. For example, a wrapper callback - * could setup wizard-alike form buttons that are the same for a variety of - * forms that belong to the wizard, which all share the same wrapper - * callback. + * - base_form_id: The base form ID can be specified explicitly. This is + * required when callback is not the name of a function. + * - wrapper_callback: (optional) Any callable to invoke before the form + * builder defined in 'callback' is invoked. This wrapper callback may + * prepopulate the $form array with form elements, which will then be + * already contained in the $form that is passed on to the form builder + * defined in 'callback'. For example, a wrapper callback could setup + * wizard-like form buttons that are the same for a variety of forms that + * belong to the wizard, which all share the same wrapper callback. */ function hook_forms($form_id, $args) { // Simply reroute the (non-existing) $form_id 'mymodule_first_form' to @@ -1843,6 +1848,15 @@ function hook_forms($form_id, $args) { 'wrapper_callback' => 'mymodule_main_form_wrapper', ); + // Build a form with a static class callback. + $forms['mymodule_class_generated_form'] = array( + // This will call: MyClass::generateMainForm(). + 'callback' => array('MyClass', 'generateMainForm'), + // The base_form_id is required when the callback is a static function in + // a class. This can also be used to keep newer code backwards compatible. + 'base_form_id' => 'mymodule_main_form', + ); + return $forms; } @@ -1890,9 +1904,8 @@ function hook_init() { /** * Define image toolkits provided by this module. * - * The file which includes each toolkit's functions must be declared as part of - * the files array in the module .info file so that the registry will find and - * parse it. + * The file which includes each toolkit's functions must be included in this + * hook. * * The toolkit's functions must be named image_toolkitname_operation(). * where the operation may be: @@ -2633,6 +2646,8 @@ function hook_flush_caches() { * module_enable() for a detailed description of the order in which install and * enable hooks are invoked. * + * This hook should be implemented in a .module file, not in an .install file. + * * @param $modules * An array of the modules that were installed. * @@ -3174,7 +3189,9 @@ function hook_requirements($phase) { * creation and alteration of the supported database engines. * * See the Schema API Handbook at http://drupal.org/node/146843 for details on - * schema definition structures. + * schema definition structures. Note that foreign key definitions are for + * documentation purposes only; foreign keys are not created in the database, + * nor are they enforced by Drupal. * * @return array * A schema definition structure array. For each element of the @@ -3226,6 +3243,8 @@ function hook_schema() { 'nid_vid' => array('nid', 'vid'), 'vid' => array('vid'), ), + // For documentation purposes only; foreign keys are not created in the + // database. 'foreign keys' => array( 'node_revision' => array( 'table' => 'node_revision', @@ -3694,8 +3713,9 @@ function hook_registry_files_alter(&$files, $modules) { * * Any tasks you define here will be run, in order, after the installer has * finished the site configuration step but before it has moved on to the - * final import of languages and the end of the installation. You can have any - * number of custom tasks to perform during this phase. + * final import of languages and the end of the installation. This is invoked + * by install_tasks(). You can have any number of custom tasks to perform + * during this phase. * * Each task you define here corresponds to a callback function which you must * separately define and which is called when your task is run. This function @@ -3788,6 +3808,8 @@ function hook_registry_files_alter(&$files, $modules) { * * @see install_state_defaults() * @see batch_set() + * @see hook_install_tasks_alter() + * @see install_tasks() */ function hook_install_tasks(&$install_state) { // Here, we define a variable to allow tasks to indicate that a particular, @@ -3890,6 +3912,8 @@ function hook_html_head_alter(&$head_elements) { /** * Alter the full list of installation tasks. * + * This hook is invoked on the install profile in install_tasks(). + * * @param $tasks * An array of all available installation tasks, including those provided by * Drupal core. You can modify this array to change or replace any part of @@ -3897,6 +3921,9 @@ function hook_html_head_alter(&$head_elements) { * is selected. * @param $install_state * An array of information about the current installation state. + * + * @see hook_install_tasks() + * @see install_tasks() */ function hook_install_tasks_alter(&$tasks, $install_state) { // Replace the "Choose language" installation task provided by Drupal core @@ -4783,6 +4810,28 @@ function hook_filetransfer_info_alter(&$filetransfer_info) { * @{ */ +/** + * Work on a single queue item. + * + * Callback for hook_cron_queue_info(). + * + * @param $queue_item_data + * The data that was passed to DrupalQueueInterface::createItem() when the + * item was queued. + * + * @throws Exception + * The worker callback may throw an exception to indicate there was a problem. + * The cron process will log the exception, and leave the item in the queue to + * be processed again later. + * + * @see drupal_cron_run() + */ +function callback_queue_worker($queue_item_data) { + $node = node_load($queue_item_data); + $node->title = 'Updated title'; + node_save($node); +} + /** * Return the URI for an entity. * diff --git a/modules/system/system.info b/modules/system/system.info index d8ee86e9..5aa40c95 100644 --- a/modules/system/system.info +++ b/modules/system/system.info @@ -12,8 +12,8 @@ files[] = system.test required = TRUE configure = admin/config/system -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/system/system.install b/modules/system/system.install index 43c7383a..ae55b892 100644 --- a/modules/system/system.install +++ b/modules/system/system.install @@ -196,6 +196,12 @@ function system_requirements($phase) { ); } + // Test database-specific multi-byte UTF-8 related requirements. + $charset_requirements = _system_check_db_utf8mb4_requirements($phase); + if (!empty($charset_requirements)) { + $requirements['database_charset'] = $charset_requirements; + } + // Test PHP memory_limit $memory_limit = ini_get('memory_limit'); $requirements['php_memory_limit'] = array( @@ -517,6 +523,75 @@ function system_requirements($phase) { return $requirements; } +/** + * Checks whether the requirements for multi-byte UTF-8 support are met. + * + * @param string $phase + * The hook_requirements() stage. + * + * @return array + * A requirements array with the result of the charset check. + */ +function _system_check_db_utf8mb4_requirements($phase) { + global $install_state; + // In the requirements check of the installer, skip the utf8mb4 check unless + // the database connection info has been preconfigured by hand with valid + // information before running the installer, as otherwise we cannot get a + // valid database connection object. + if (isset($install_state['settings_verified']) && !$install_state['settings_verified']) { + return array(); + } + + $connection = Database::getConnection(); + $t = get_t(); + $requirements['title'] = $t('Database 4 byte UTF-8 support'); + + $utf8mb4_configurable = $connection->utf8mb4IsConfigurable(); + $utf8mb4_active = $connection->utf8mb4IsActive(); + $utf8mb4_supported = $connection->utf8mb4IsSupported(); + $driver = $connection->driver(); + $documentation_url = 'https://www.drupal.org/node/2754539'; + + if ($utf8mb4_active) { + if ($utf8mb4_supported) { + if ($phase != 'install' && $utf8mb4_configurable && !variable_get('drupal_all_databases_are_utf8mb4', FALSE)) { + // Supported, active, and configurable, but not all database tables + // have been converted yet. + $requirements['value'] = $t('Enabled, but database tables need conversion'); + $requirements['description'] = $t('Please convert all database tables to utf8mb4 prior to enabling it in settings.php. See the documentation on adding 4 byte UTF-8 support for more information.', array('@url' => $documentation_url)); + $requirements['severity'] = REQUIREMENT_ERROR; + } + else { + // Supported, active. + $requirements['value'] = $t('Enabled'); + $requirements['description'] = $t('4 byte UTF-8 for @driver is enabled.', array('@driver' => $driver)); + $requirements['severity'] = REQUIREMENT_OK; + } + } + else { + // Not supported, active. + $requirements['value'] = $t('Not supported'); + $requirements['description'] = $t('4 byte UTF-8 for @driver is activated, but not supported on your system. Please turn this off in settings.php, or ensure that all database-related requirements are met. See the documentation on adding 4 byte UTF-8 support for more information.', array('@driver' => $driver, '@url' => $documentation_url)); + $requirements['severity'] = REQUIREMENT_ERROR; + } + } + else { + if ($utf8mb4_supported) { + // Supported, not active. + $requirements['value'] = $t('Not enabled'); + $requirements['description'] = $t('4 byte UTF-8 for @driver is not activated, but it is supported on your system. It is recommended that you enable this to allow 4-byte UTF-8 input such as emojis, Asian symbols and mathematical symbols to be stored correctly. See the documentation on adding 4 byte UTF-8 support for more information.', array('@driver' => $driver, '@url' => $documentation_url)); + $requirements['severity'] = REQUIREMENT_INFO; + } + else { + // Not supported, not active. + $requirements['value'] = $t('Disabled'); + $requirements['description'] = $t('4 byte UTF-8 for @driver is disabled. See the documentation on adding 4 byte UTF-8 support for more information.', array('@driver' => $driver, '@url' => $documentation_url)); + $requirements['severity'] = REQUIREMENT_INFO; + } + } + return $requirements; +} + /** * Implements hook_install(). */ @@ -532,6 +607,9 @@ function system_install() { module_list(TRUE); module_implements('', FALSE, TRUE); + // Ensure the schema versions are not based on a previous module list. + drupal_static_reset('drupal_get_schema_versions'); + // Load system theme data appropriately. system_rebuild_theme_data(); @@ -800,6 +878,7 @@ function system_schema() { 'type' => 'varchar', 'length' => 100, 'not null' => TRUE, + 'binary' => TRUE, ), 'type' => array( 'description' => 'The date format type, e.g. medium.', @@ -2803,6 +2882,16 @@ function system_update_7061(&$sandbox) { ->from($query) ->execute(); + // Retrieve a list of duplicate files with the same filepath. Only the + // most-recently uploaded of these will be moved to the new {file_managed} + // table (and all references will be updated to point to it), since + // duplicate file URIs are not allowed in Drupal 7. + // Since the Drupal 6 to 7 upgrade path leaves the {files} table behind + // after it's done, custom or contributed modules which need to migrate + // file references of their own can use a similar query to determine the + // file IDs that duplicate filepaths were mapped to. + $sandbox['duplicate_filepath_fids_to_use'] = db_query("SELECT filepath, MAX(fid) FROM {files} GROUP BY filepath HAVING COUNT(*) > 1")->fetchAllKeyed(); + // Initialize batch update information. $sandbox['progress'] = 0; $sandbox['last_vid_processed'] = -1; @@ -2832,6 +2921,16 @@ function system_update_7061(&$sandbox) { continue; } + // If this file has a duplicate filepath, replace it with the + // most-recently uploaded file that has the same filepath. + if (isset($sandbox['duplicate_filepath_fids_to_use'][$file['filepath']]) && $record->fid != $sandbox['duplicate_filepath_fids_to_use'][$file['filepath']]) { + $file = db_select('files', 'f') + ->fields('f', array('fid', 'uid', 'filename', 'filepath', 'filemime', 'filesize', 'status', 'timestamp')) + ->condition('f.fid', $sandbox['duplicate_filepath_fids_to_use'][$file['filepath']]) + ->execute() + ->fetchAssoc(); + } + // Add in the file information from the upload table. $file['description'] = $record->description; $file['display'] = $record->list; @@ -2854,7 +2953,14 @@ function system_update_7061(&$sandbox) { // We will convert filepaths to URI using the default scheme // and stripping off the existing file directory path. $file['uri'] = $scheme . preg_replace('!^' . preg_quote($basename) . '!', '', $file['filepath']); - $file['uri'] = file_stream_wrapper_uri_normalize($file['uri']); + // Normalize the URI but don't call file_stream_wrapper_uri_normalize() + // directly, since that is a higher-level API function which invokes + // hooks while validating the scheme, and those will not work during + // the upgrade. Instead, use a simpler version that just assumes the + // scheme from above is already valid. + if (($file_uri_scheme = file_uri_scheme($file['uri'])) && ($file_uri_target = file_uri_target($file['uri']))) { + $file['uri'] = $file_uri_scheme . '://' . $file_uri_target; + } unset($file['filepath']); // Insert into the file_managed table. // Each fid should only be stored once in file_managed. @@ -3150,6 +3256,35 @@ function system_update_7079() { db_change_field('file_managed', 'filesize', 'filesize', $spec); } +/** + * Convert the 'format' column in {date_format_locale} to case sensitive varchar. + */ +function system_update_7080() { + $spec = array( + 'description' => 'The date format string.', + 'type' => 'varchar', + 'length' => 100, + 'not null' => TRUE, + 'binary' => TRUE, + ); + db_change_field('date_format_locale', 'format', 'format', $spec); +} + +/** + * Remove the Drupal 6 default install profile if it is still in the database. + */ +function system_update_7081() { + // Sites which used the default install profile in Drupal 6 and then updated + // to Drupal 7.44 or earlier will still have a record of this install profile + // in the database that needs to be deleted. + db_delete('system') + ->condition('filename', 'profiles/default/default.profile') + ->condition('type', 'module') + ->condition('status', 0) + ->condition('schema_version', 0) + ->execute(); +} + /** * @} End of "defgroup updates-7.x-extra". * The next series of updates should start at 8000. diff --git a/modules/system/system.js b/modules/system/system.js index 910fb5d3..c0e76d38 100644 --- a/modules/system/system.js +++ b/modules/system/system.js @@ -105,7 +105,7 @@ Drupal.behaviors.dateTime = { // Attach keyup handler to custom format inputs. $('input' + source, context).once('date-time').keyup(function () { var input = $(this); - var url = fieldSettings.lookup + (/\?q=/.test(fieldSettings.lookup) ? '&format=' : '?format=') + encodeURIComponent(input.val()); + var url = fieldSettings.lookup + (/\?/.test(fieldSettings.lookup) ? '&format=' : '?format=') + encodeURIComponent(input.val()); $.getJSON(url, function (data) { $(suffix).empty().append(' ' + fieldSettings.text + ': ' + data + ''); }); diff --git a/modules/system/system.module b/modules/system/system.module index a2749357..ae7c4323 100644 --- a/modules/system/system.module +++ b/modules/system/system.module @@ -359,7 +359,7 @@ function system_element_info() { '#size' => 60, '#maxlength' => 128, '#autocomplete_path' => FALSE, - '#process' => array('ajax_process_form'), + '#process' => array('form_process_autocomplete', 'ajax_process_form'), '#theme' => 'textfield', '#theme_wrappers' => array('form_element'), ); @@ -374,6 +374,9 @@ function system_element_info() { '#element_validate' => array('form_validate_machine_name'), '#theme' => 'textfield', '#theme_wrappers' => array('form_element'), + // Use the same value callback as for textfields; this ensures that we only + // get string values. + '#value_callback' => 'form_type_textfield_value', ); $types['password'] = array( '#input' => TRUE, @@ -382,6 +385,9 @@ function system_element_info() { '#process' => array('ajax_process_form'), '#theme' => 'password', '#theme_wrappers' => array('form_element'), + // Use the same value callback as for textfields; this ensures that we only + // get string values. + '#value_callback' => 'form_type_textfield_value', ); $types['password_confirm'] = array( '#input' => TRUE, @@ -2024,7 +2030,6 @@ function system_user_timezone(&$form, &$form_state) { '#description' => t('Select the desired local time and time zone. Dates and times throughout this site will be displayed using this time zone.'), ); if (!isset($account->timezone) && $account->uid == $user->uid && empty($form_state['input']['timezone'])) { - $form['timezone']['#description'] = t('Your time zone setting will be automatically detected if possible. Confirm the selection and click save.'); $form['timezone']['timezone']['#attributes'] = array('class' => array('timezone-detect')); drupal_add_js('misc/timezone.js'); } @@ -2406,6 +2411,10 @@ function _system_rebuild_module_data() { // Merge in defaults and save. $modules[$key]->info = $module->info + $defaults; + // The "name" key is required, but to avoid a fatal error in the menu system + // we set a reasonable default if it is not provided. + $modules[$key]->info += array('name' => $key); + // Prefix stylesheets and scripts with module path. $path = dirname($module->uri); if (isset($module->info['stylesheets'])) { @@ -2541,6 +2550,10 @@ function _system_rebuild_theme_data() { $themes[$key]->filename = $theme->uri; $themes[$key]->info = drupal_parse_info_file($theme->uri) + $defaults; + // The "name" key is required, but to avoid a fatal error in the menu system + // we set a reasonable default if it is not provided. + $themes[$key]->info += array('name' => $key); + // Add the info file modification time, so it becomes available for // contributed modules to use for ordering theme lists. $themes[$key]->info['mtime'] = filemtime($theme->uri); @@ -2692,10 +2705,17 @@ function system_find_base_themes($themes, $key, $used_keys = array()) { * @param $show * Possible values: REGIONS_ALL or REGIONS_VISIBLE. Visible excludes hidden * regions. - * @return - * An array of regions in the form $region['name'] = 'description'. + * @param bool $labels + * (optional) Boolean to specify whether the human readable machine names + * should be returned or not. Defaults to TRUE, but calling code can set + * this to FALSE for better performance, if it only needs machine names. + * + * @return array + * An associative array of regions in the form $region['name'] = 'description' + * if $labels is set to TRUE, or $region['name'] = 'name', if $labels is set + * to FALSE. */ -function system_region_list($theme_key, $show = REGIONS_ALL) { +function system_region_list($theme_key, $show = REGIONS_ALL, $labels = TRUE) { $themes = list_themes(); if (!isset($themes[$theme_key])) { return array(); @@ -2706,10 +2726,14 @@ function system_region_list($theme_key, $show = REGIONS_ALL) { // If requested, suppress hidden regions. See block_admin_display_form(). foreach ($info['regions'] as $name => $label) { if ($show == REGIONS_ALL || !isset($info['regions_hidden']) || !in_array($name, $info['regions_hidden'])) { - $list[$name] = t($label); + if ($labels) { + $list[$name] = t($label); + } + else { + $list[$name] = $name; + } } } - return $list; } @@ -2730,12 +2754,13 @@ function system_system_info_alter(&$info, $file, $type) { * * @param $theme * The name of a theme. + * * @return * A string that is the region name. */ function system_default_region($theme) { - $regions = array_keys(system_region_list($theme, REGIONS_VISIBLE)); - return isset($regions[0]) ? $regions[0] : ''; + $regions = system_region_list($theme, REGIONS_VISIBLE, FALSE); + return $regions ? reset($regions) : ''; } /** @@ -2802,7 +2827,7 @@ function system_settings_form_submit($form, &$form_state) { function _system_sort_requirements($a, $b) { if (!isset($a['weight'])) { if (!isset($b['weight'])) { - return strcmp($a['title'], $b['title']); + return strcasecmp($a['title'], $b['title']); } return -$b['weight']; } @@ -2858,7 +2883,7 @@ function confirm_form($form, $question, $path, $description = NULL, $yes = NULL, // Prepare cancel link. if (isset($_GET['destination'])) { - $options = drupal_parse_url(urldecode($_GET['destination'])); + $options = drupal_parse_url($_GET['destination']); } elseif (is_array($path)) { $options = $path; @@ -3043,8 +3068,20 @@ function system_cron() { } } - $core = array('cache', 'cache_path', 'cache_filter', 'cache_page', 'cache_form', 'cache_menu'); - $cache_tables = array_merge(module_invoke_all('flush_caches'), $core); + // Delete expired cache entries. + // Avoid invoking hook_flush_cashes() on every cron run because some modules + // use this hook to perform expensive rebuilding operations (which are only + // designed to happen on full cache clears), rather than just returning a + // list of cache tables to be cleared. + $cache_object = cache_get('system_cache_tables'); + if (empty($cache_object)) { + $core = array('cache', 'cache_path', 'cache_filter', 'cache_page', 'cache_form', 'cache_menu'); + $cache_tables = array_merge(module_invoke_all('flush_caches'), $core); + cache_set('system_cache_tables', $cache_tables); + } + else { + $cache_tables = $cache_object->data; + } foreach ($cache_tables as $table) { cache_clear_all(NULL, $table); } @@ -3292,7 +3329,7 @@ function system_goto_action_form($context) { $form['url'] = array( '#type' => 'textfield', '#title' => t('URL'), - '#description' => t('The URL to which the user should be redirected. This can be an internal URL like node/1234 or an external URL like http://drupal.org.'), + '#description' => t('The URL to which the user should be redirected. This can be an internal path like node/1234 or an external URL like http://example.com.'), '#default_value' => isset($context['url']) ? $context['url'] : '', '#required' => TRUE, ); @@ -3329,7 +3366,8 @@ function system_goto_action($entity, $context) { */ function system_block_ip_action() { $ip = ip_address(); - db_insert('blocked_ips') + db_merge('blocked_ips') + ->key(array('ip' => $ip)) ->fields(array('ip' => $ip)) ->execute(); watchdog('action', 'Banned IP address %ip', array('%ip' => $ip)); @@ -3491,8 +3529,7 @@ function system_retrieve_file($url, $destination = NULL, $managed = FALSE, $repl function system_page_alter(&$page) { // Find all non-empty page regions, and add a theme wrapper function that // allows them to be consistently themed. - $regions = system_region_list($GLOBALS['theme']); - foreach (array_keys($regions) as $region) { + foreach (system_region_list($GLOBALS['theme'], REGIONS_ALL, FALSE) as $region) { if (!empty($page[$region])) { $page[$region]['#theme_wrappers'][] = 'region'; $page[$region]['#region'] = $region; diff --git a/modules/system/system.queue.inc b/modules/system/system.queue.inc index 901c4d6d..c17084de 100644 --- a/modules/system/system.queue.inc +++ b/modules/system/system.queue.inc @@ -231,7 +231,7 @@ class SystemQueue implements DrupalReliableQueueInterface { // until an item is successfully claimed or we are reasonably sure there // are no unclaimed items left. while (TRUE) { - $item = db_query_range('SELECT data, item_id FROM {queue} q WHERE expire = 0 AND name = :name ORDER BY created ASC', 0, 1, array(':name' => $this->name))->fetchObject(); + $item = db_query_range('SELECT data, item_id FROM {queue} q WHERE expire = 0 AND name = :name ORDER BY created, item_id ASC', 0, 1, array(':name' => $this->name))->fetchObject(); if ($item) { // Try to update the item. Only one thread can succeed in UPDATEing the // same row. We cannot rely on REQUEST_TIME because items might be @@ -326,6 +326,7 @@ class MemoryQueue implements DrupalQueueInterface { $item->created = time(); $item->expire = 0; $this->queue[$item->item_id] = $item; + return TRUE; } public function numberOfItems() { diff --git a/modules/system/system.tar.inc b/modules/system/system.tar.inc index 32bf7f06..86e4e3de 100644 --- a/modules/system/system.tar.inc +++ b/modules/system/system.tar.inc @@ -30,81 +30,148 @@ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * - * - * @category File_Formats - * @package Archive_Tar - * @author Vincent Blavet - * @copyright 1997-2008 The Authors - * @license http://www.opensource.org/licenses/bsd-license.php New BSD License - * @version CVS: Id: Tar.php,v 1.43 2008/10/30 17:58:42 dufuz Exp - * @link http://pear.php.net/package/Archive_Tar + * @category File_Formats + * @package Archive_Tar + * @author Vincent Blavet + * @copyright 1997-2010 The Authors + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id$ + * @link http://pear.php.net/package/Archive_Tar */ -//require_once 'PEAR.php'; -// -// -define ('ARCHIVE_TAR_ATT_SEPARATOR', 90001); -define ('ARCHIVE_TAR_END_BLOCK', pack("a512", '')); + /** + * Note on Drupal 8 porting. + * This file origin is Tar.php, release 1.4.0 (stable) with some code + * from PEAR.php, release 1.9.5 (stable) both at http://pear.php.net. + * To simplify future porting from pear of this file, you should not + * do cosmetic or other non significant changes to this file. + * The following changes have been done: + * Added namespace Drupal\Core\Archiver. + * Removed require_once 'PEAR.php'. + * Added defintion of OS_WINDOWS taken from PEAR.php. + * Renamed class to ArchiveTar. + * Removed extends PEAR from class. + * Removed call parent:: __construct(). + * Changed PEAR::loadExtension($extname) to this->loadExtension($extname). + * Added function loadExtension() taken from PEAR.php. + * Changed all calls of unlink() to drupal_unlink(). + * Changed $this->error_object = &$this->raiseError($p_message) + * to throw new \Exception($p_message). + */ + + /** + * Note on Drupal 7 backporting from Drupal 8. + * File origin is core/lib/Drupal/Core/Archiver/ArchiveTar.php from Drupal 8. + * The following changes have been done: + * Removed namespace Drupal\Core\Archiver. + * Renamed class to Archive_Tar. + * Changed \Exception to Exception. + */ + + +// Drupal removal require_once 'PEAR.php'. + +// Drupal addition OS_WINDOWS as defined in PEAR.php. +if (substr(PHP_OS, 0, 3) == 'WIN') { + define('OS_WINDOWS', true); +} else { + define('OS_WINDOWS', false); +} + +define('ARCHIVE_TAR_ATT_SEPARATOR', 90001); +define('ARCHIVE_TAR_END_BLOCK', pack("a512", '')); + +if (!function_exists('gzopen') && function_exists('gzopen64')) { + function gzopen($filename, $mode, $use_include_path = 0) + { + return gzopen64($filename, $mode, $use_include_path); + } +} + +if (!function_exists('gztell') && function_exists('gztell64')) { + function gztell($zp) + { + return gztell64($zp); + } +} + +if (!function_exists('gzseek') && function_exists('gzseek64')) { + function gzseek($zp, $offset, $whence = SEEK_SET) + { + return gzseek64($zp, $offset, $whence); + } +} /** -* Creates a (compressed) Tar archive -* -* @author Vincent Blavet -* @version Revision: 1.43 -* @license http://www.opensource.org/licenses/bsd-license.php New BSD License -* @package Archive_Tar -*/ -class Archive_Tar // extends PEAR + * Creates a (compressed) Tar archive + * + * @package Archive_Tar + * @author Vincent Blavet + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @version $Revision$ + */ +// Drupal change class Archive_Tar extends PEAR. +class Archive_Tar { /** - * @var string Name of the Tar - */ - var $_tarname=''; + * @var string Name of the Tar + */ + public $_tarname = ''; /** - * @var boolean if true, the Tar file will be gzipped - */ - var $_compress=false; + * @var boolean if true, the Tar file will be gzipped + */ + public $_compress = false; /** - * @var string Type of compression : 'none', 'gz' or 'bz2' - */ - var $_compress_type='none'; + * @var string Type of compression : 'none', 'gz', 'bz2' or 'lzma2' + */ + public $_compress_type = 'none'; /** - * @var string Explode separator - */ - var $_separator=' '; + * @var string Explode separator + */ + public $_separator = ' '; /** - * @var file descriptor - */ - var $_file=0; + * @var file descriptor + */ + public $_file = 0; /** - * @var string Local Tar name of a remote Tar (http:// or ftp://) - */ - var $_temp_tarname=''; + * @var string Local Tar name of a remote Tar (http:// or ftp://) + */ + public $_temp_tarname = ''; - // {{{ constructor /** - * Archive_Tar Class constructor. This flavour of the constructor only - * declare a new Archive_Tar object, identifying it by the name of the - * tar file. - * If the compress argument is set the tar will be read or created as a - * gzip or bz2 compressed TAR file. - * - * @param string $p_tarname The name of the tar archive to create - * @param string $p_compress can be null, 'gz' or 'bz2'. This - * parameter indicates if gzip or bz2 compression - * is required. For compatibility reason the - * boolean value 'true' means 'gz'. - * @access public - */ -// function Archive_Tar($p_tarname, $p_compress = null) - function __construct($p_tarname, $p_compress = null) + * @var string regular expression for ignoring files or directories + */ + public $_ignore_regexp = ''; + + /** + * @var object PEAR_Error object + */ + public $error_object = null; + + /** + * Archive_Tar Class constructor. This flavour of the constructor only + * declare a new Archive_Tar object, identifying it by the name of the + * tar file. + * If the compress argument is set the tar will be read or created as a + * gzip or bz2 compressed TAR file. + * + * @param string $p_tarname The name of the tar archive to create + * @param string $p_compress can be null, 'gz', 'bz2' or 'lzma2'. This + * parameter indicates if gzip, bz2 or lzma2 compression + * is required. For compatibility reason the + * boolean value 'true' means 'gz'. + * + * @return bool + */ + public function __construct($p_tarname, $p_compress = null) { -// $this->PEAR(); + // Drupal removal parent::__construct(). + $this->_compress = false; $this->_compress_type = 'none'; if (($p_compress === null) || ($p_compress == '')) { @@ -116,10 +183,13 @@ class Archive_Tar // extends PEAR if ($data == "\37\213") { $this->_compress = true; $this->_compress_type = 'gz'; - // No sure it's enought for a magic code .... + // No sure it's enought for a magic code .... } elseif ($data == "BZ") { $this->_compress = true; $this->_compress_type = 'bz2'; + } elseif (file_get_contents($p_tarname, false, null, 1, 4) == '7zXZ') { + $this->_compress = true; + $this->_compress_type = 'lzma2'; } } } else { @@ -129,151 +199,177 @@ class Archive_Tar // extends PEAR $this->_compress = true; $this->_compress_type = 'gz'; } elseif ((substr($p_tarname, -3) == 'bz2') || - (substr($p_tarname, -2) == 'bz')) { + (substr($p_tarname, -2) == 'bz') + ) { $this->_compress = true; $this->_compress_type = 'bz2'; + } else { + if (substr($p_tarname, -2) == 'xz') { + $this->_compress = true; + $this->_compress_type = 'lzma2'; + } } } } else { if (($p_compress === true) || ($p_compress == 'gz')) { $this->_compress = true; $this->_compress_type = 'gz'; - } else if ($p_compress == 'bz2') { - $this->_compress = true; - $this->_compress_type = 'bz2'; } else { - die("Unsupported compression type '$p_compress'\n". - "Supported types are 'gz' and 'bz2'.\n"); - return false; + if ($p_compress == 'bz2') { + $this->_compress = true; + $this->_compress_type = 'bz2'; + } else { + if ($p_compress == 'lzma2') { + $this->_compress = true; + $this->_compress_type = 'lzma2'; + } else { + $this->_error( + "Unsupported compression type '$p_compress'\n" . + "Supported types are 'gz', 'bz2' and 'lzma2'.\n" + ); + return false; + } + } } } $this->_tarname = $p_tarname; - if ($this->_compress) { // assert zlib or bz2 extension support - if ($this->_compress_type == 'gz') + if ($this->_compress) { // assert zlib or bz2 or xz extension support + if ($this->_compress_type == 'gz') { $extname = 'zlib'; - else if ($this->_compress_type == 'bz2') - $extname = 'bz2'; + } else { + if ($this->_compress_type == 'bz2') { + $extname = 'bz2'; + } else { + if ($this->_compress_type == 'lzma2') { + $extname = 'xz'; + } + } + } if (!extension_loaded($extname)) { -// PEAR::loadExtension($extname); + // Drupal change PEAR::loadExtension($extname). $this->loadExtension($extname); } if (!extension_loaded($extname)) { - die("The extension '$extname' couldn't be found.\n". - "Please make sure your version of PHP was built ". - "with '$extname' support.\n"); + $this->_error( + "The extension '$extname' couldn't be found.\n" . + "Please make sure your version of PHP was built " . + "with '$extname' support.\n" + ); return false; } } } - // }}} + public function __destruct() + { + $this->_close(); + // ----- Look for a local copy to delete + if ($this->_temp_tarname != '') { + @drupal_unlink($this->_temp_tarname); + } + } + + // Drupal addition from PEAR.php. /** * OS independent PHP extension load. Remember to take care * on the correct extension name for case sensitive OSes. - * The function is the copy of PEAR::loadExtension(). * * @param string $ext The extension name * @return bool Success or not on the dl() call */ function loadExtension($ext) { - if (!extension_loaded($ext)) { - // if either returns true dl() will produce a FATAL error, stop that - if ((ini_get('enable_dl') != 1) || (ini_get('safe_mode') == 1)) { - return false; - } - - if (OS_WINDOWS) { - $suffix = '.dll'; - } elseif (PHP_OS == 'HP-UX') { - $suffix = '.sl'; - } elseif (PHP_OS == 'AIX') { - $suffix = '.a'; - } elseif (PHP_OS == 'OSX') { - $suffix = '.bundle'; - } else { - $suffix = '.so'; - } - - return @dl('php_'.$ext.$suffix) || @dl($ext.$suffix); + if (extension_loaded($ext)) { + return true; } - return true; + // if either returns true dl() will produce a FATAL error, stop that + if ( + function_exists('dl') === false || + ini_get('enable_dl') != 1 || + ini_get('safe_mode') == 1 + ) { + return false; + } + + if (OS_WINDOWS) { + $suffix = '.dll'; + } elseif (PHP_OS == 'HP-UX') { + $suffix = '.sl'; + } elseif (PHP_OS == 'AIX') { + $suffix = '.a'; + } elseif (PHP_OS == 'OSX') { + $suffix = '.bundle'; + } else { + $suffix = '.so'; + } + + return @dl('php_'.$ext.$suffix) || @dl($ext.$suffix); } - // {{{ destructor -// function _Archive_Tar() - function __destruct() - { - $this->_close(); - // ----- Look for a local copy to delete - if ($this->_temp_tarname != '') - @drupal_unlink($this->_temp_tarname); -// $this->_PEAR(); - } - // }}} - - // {{{ create() /** - * This method creates the archive file and add the files / directories - * that are listed in $p_filelist. - * If a file with the same name exist and is writable, it is replaced - * by the new tar. - * The method return false and a PEAR error text. - * The $p_filelist parameter can be an array of string, each string - * representing a filename or a directory name with their path if - * needed. It can also be a single string with names separated by a - * single blank. - * For each directory added in the archive, the files and - * sub-directories are also added. - * See also createModify() method for more details. - * - * @param array $p_filelist An array of filenames and directory names, or a - * single string with names separated by a single - * blank space. - * @return true on success, false on error. - * @see createModify() - * @access public - */ - function create($p_filelist) + * This method creates the archive file and add the files / directories + * that are listed in $p_filelist. + * If a file with the same name exist and is writable, it is replaced + * by the new tar. + * The method return false and a PEAR error text. + * The $p_filelist parameter can be an array of string, each string + * representing a filename or a directory name with their path if + * needed. It can also be a single string with names separated by a + * single blank. + * For each directory added in the archive, the files and + * sub-directories are also added. + * See also createModify() method for more details. + * + * @param array $p_filelist An array of filenames and directory names, or a + * single string with names separated by a single + * blank space. + * + * @return true on success, false on error. + * @see createModify() + */ + public function create($p_filelist) { return $this->createModify($p_filelist, '', ''); } - // }}} - // {{{ add() /** - * This method add the files / directories that are listed in $p_filelist in - * the archive. If the archive does not exist it is created. - * The method return false and a PEAR error text. - * The files and directories listed are only added at the end of the archive, - * even if a file with the same name is already archived. - * See also createModify() method for more details. - * - * @param array $p_filelist An array of filenames and directory names, or a - * single string with names separated by a single - * blank space. - * @return true on success, false on error. - * @see createModify() - * @access public - */ - function add($p_filelist) + * This method add the files / directories that are listed in $p_filelist in + * the archive. If the archive does not exist it is created. + * The method return false and a PEAR error text. + * The files and directories listed are only added at the end of the archive, + * even if a file with the same name is already archived. + * See also createModify() method for more details. + * + * @param array $p_filelist An array of filenames and directory names, or a + * single string with names separated by a single + * blank space. + * + * @return true on success, false on error. + * @see createModify() + * @access public + */ + public function add($p_filelist) { return $this->addModify($p_filelist, '', ''); } - // }}} - // {{{ extract() - function extract($p_path='') + /** + * @param string $p_path + * @param bool $p_preserve + * @return bool + */ + public function extract($p_path = '', $p_preserve = false) { - return $this->extractModify($p_path, ''); + return $this->extractModify($p_path, '', $p_preserve); } - // }}} - // {{{ listContent() - function listContent() + /** + * @return array|int + */ + public function listContent() { $v_list_detail = array(); @@ -287,57 +383,56 @@ class Archive_Tar // extends PEAR return $v_list_detail; } - // }}} - // {{{ createModify() /** - * This method creates the archive file and add the files / directories - * that are listed in $p_filelist. - * If the file already exists and is writable, it is replaced by the - * new tar. It is a create and not an add. If the file exists and is - * read-only or is a directory it is not replaced. The method return - * false and a PEAR error text. - * The $p_filelist parameter can be an array of string, each string - * representing a filename or a directory name with their path if - * needed. It can also be a single string with names separated by a - * single blank. - * The path indicated in $p_remove_dir will be removed from the - * memorized path of each file / directory listed when this path - * exists. By default nothing is removed (empty path '') - * The path indicated in $p_add_dir will be added at the beginning of - * the memorized path of each file / directory listed. However it can - * be set to empty ''. The adding of a path is done after the removing - * of path. - * The path add/remove ability enables the user to prepare an archive - * for extraction in a different path than the origin files are. - * See also addModify() method for file adding properties. - * - * @param array $p_filelist An array of filenames and directory names, - * or a single string with names separated by - * a single blank space. - * @param string $p_add_dir A string which contains a path to be added - * to the memorized path of each element in - * the list. - * @param string $p_remove_dir A string which contains a path to be - * removed from the memorized path of each - * element in the list, when relevant. - * @return boolean true on success, false on error. - * @access public - * @see addModify() - */ - function createModify($p_filelist, $p_add_dir, $p_remove_dir='') + * This method creates the archive file and add the files / directories + * that are listed in $p_filelist. + * If the file already exists and is writable, it is replaced by the + * new tar. It is a create and not an add. If the file exists and is + * read-only or is a directory it is not replaced. The method return + * false and a PEAR error text. + * The $p_filelist parameter can be an array of string, each string + * representing a filename or a directory name with their path if + * needed. It can also be a single string with names separated by a + * single blank. + * The path indicated in $p_remove_dir will be removed from the + * memorized path of each file / directory listed when this path + * exists. By default nothing is removed (empty path '') + * The path indicated in $p_add_dir will be added at the beginning of + * the memorized path of each file / directory listed. However it can + * be set to empty ''. The adding of a path is done after the removing + * of path. + * The path add/remove ability enables the user to prepare an archive + * for extraction in a different path than the origin files are. + * See also addModify() method for file adding properties. + * + * @param array $p_filelist An array of filenames and directory names, + * or a single string with names separated by + * a single blank space. + * @param string $p_add_dir A string which contains a path to be added + * to the memorized path of each element in + * the list. + * @param string $p_remove_dir A string which contains a path to be + * removed from the memorized path of each + * element in the list, when relevant. + * + * @return boolean true on success, false on error. + * @see addModify() + */ + public function createModify($p_filelist, $p_add_dir, $p_remove_dir = '') { $v_result = true; - if (!$this->_openWrite()) + if (!$this->_openWrite()) { return false; + } if ($p_filelist != '') { - if (is_array($p_filelist)) + if (is_array($p_filelist)) { $v_list = $p_filelist; - elseif (is_string($p_filelist)) + } elseif (is_string($p_filelist)) { $v_list = explode($this->_separator, $p_filelist); - else { + } else { $this->_cleanFile(); $this->_error('Invalid file list'); return false; @@ -349,67 +444,69 @@ class Archive_Tar // extends PEAR if ($v_result) { $this->_writeFooter(); $this->_close(); - } else + } else { $this->_cleanFile(); + } return $v_result; } - // }}} - // {{{ addModify() /** - * This method add the files / directories listed in $p_filelist at the - * end of the existing archive. If the archive does not yet exists it - * is created. - * The $p_filelist parameter can be an array of string, each string - * representing a filename or a directory name with their path if - * needed. It can also be a single string with names separated by a - * single blank. - * The path indicated in $p_remove_dir will be removed from the - * memorized path of each file / directory listed when this path - * exists. By default nothing is removed (empty path '') - * The path indicated in $p_add_dir will be added at the beginning of - * the memorized path of each file / directory listed. However it can - * be set to empty ''. The adding of a path is done after the removing - * of path. - * The path add/remove ability enables the user to prepare an archive - * for extraction in a different path than the origin files are. - * If a file/dir is already in the archive it will only be added at the - * end of the archive. There is no update of the existing archived - * file/dir. However while extracting the archive, the last file will - * replace the first one. This results in a none optimization of the - * archive size. - * If a file/dir does not exist the file/dir is ignored. However an - * error text is send to PEAR error. - * If a file/dir is not readable the file/dir is ignored. However an - * error text is send to PEAR error. - * - * @param array $p_filelist An array of filenames and directory - * names, or a single string with names - * separated by a single blank space. - * @param string $p_add_dir A string which contains a path to be - * added to the memorized path of each - * element in the list. - * @param string $p_remove_dir A string which contains a path to be - * removed from the memorized path of - * each element in the list, when - * relevant. - * @return true on success, false on error. - * @access public - */ - function addModify($p_filelist, $p_add_dir, $p_remove_dir='') + * This method add the files / directories listed in $p_filelist at the + * end of the existing archive. If the archive does not yet exists it + * is created. + * The $p_filelist parameter can be an array of string, each string + * representing a filename or a directory name with their path if + * needed. It can also be a single string with names separated by a + * single blank. + * The path indicated in $p_remove_dir will be removed from the + * memorized path of each file / directory listed when this path + * exists. By default nothing is removed (empty path '') + * The path indicated in $p_add_dir will be added at the beginning of + * the memorized path of each file / directory listed. However it can + * be set to empty ''. The adding of a path is done after the removing + * of path. + * The path add/remove ability enables the user to prepare an archive + * for extraction in a different path than the origin files are. + * If a file/dir is already in the archive it will only be added at the + * end of the archive. There is no update of the existing archived + * file/dir. However while extracting the archive, the last file will + * replace the first one. This results in a none optimization of the + * archive size. + * If a file/dir does not exist the file/dir is ignored. However an + * error text is send to PEAR error. + * If a file/dir is not readable the file/dir is ignored. However an + * error text is send to PEAR error. + * + * @param array $p_filelist An array of filenames and directory + * names, or a single string with names + * separated by a single blank space. + * @param string $p_add_dir A string which contains a path to be + * added to the memorized path of each + * element in the list. + * @param string $p_remove_dir A string which contains a path to be + * removed from the memorized path of + * each element in the list, when + * relevant. + * + * @return true on success, false on error. + */ + public function addModify($p_filelist, $p_add_dir, $p_remove_dir = '') { $v_result = true; - if (!$this->_isArchive()) - $v_result = $this->createModify($p_filelist, $p_add_dir, - $p_remove_dir); - else { - if (is_array($p_filelist)) + if (!$this->_isArchive()) { + $v_result = $this->createModify( + $p_filelist, + $p_add_dir, + $p_remove_dir + ); + } else { + if (is_array($p_filelist)) { $v_list = $p_filelist; - elseif (is_string($p_filelist)) + } elseif (is_string($p_filelist)) { $v_list = explode($this->_separator, $p_filelist); - else { + } else { $this->_error('Invalid file list'); return false; } @@ -419,24 +516,41 @@ class Archive_Tar // extends PEAR return $v_result; } - // }}} - // {{{ addString() /** - * This method add a single string as a file at the - * end of the existing archive. If the archive does not yet exists it - * is created. - * - * @param string $p_filename A string which contains the full - * filename path that will be associated - * with the string. - * @param string $p_string The content of the file added in - * the archive. - * @return true on success, false on error. - * @access public - */ - function addString($p_filename, $p_string) + * This method add a single string as a file at the + * end of the existing archive. If the archive does not yet exists it + * is created. + * + * @param string $p_filename A string which contains the full + * filename path that will be associated + * with the string. + * @param string $p_string The content of the file added in + * the archive. + * @param bool|int $p_datetime A custom date/time (unix timestamp) + * for the file (optional). + * @param array $p_params An array of optional params: + * stamp => the datetime (replaces + * datetime above if it exists) + * mode => the permissions on the + * file (600 by default) + * type => is this a link? See the + * tar specification for details. + * (default = regular file) + * uid => the user ID of the file + * (default = 0 = root) + * gid => the group ID of the file + * (default = 0 = root) + * + * @return true on success, false on error. + */ + public function addString($p_filename, $p_string, $p_datetime = false, $p_params = array()) { + $p_stamp = @$p_params["stamp"] ? $p_params["stamp"] : ($p_datetime ? $p_datetime : time()); + $p_mode = @$p_params["mode"] ? $p_params["mode"] : 0600; + $p_type = @$p_params["type"] ? $p_params["type"] : ""; + $p_uid = @$p_params["uid"] ? $p_params["uid"] : ""; + $p_gid = @$p_params["gid"] ? $p_params["gid"] : ""; $v_result = true; if (!$this->_isArchive()) { @@ -446,11 +560,12 @@ class Archive_Tar // extends PEAR $this->_close(); } - if (!$this->_openAppend()) + if (!$this->_openAppend()) { return false; + } // Need to check the get back to the temporary file ? .... - $v_result = $this->_addString($p_filename, $p_string); + $v_result = $this->_addString($p_filename, $p_string, $p_datetime, $p_params); $this->_writeFooter(); @@ -458,131 +573,138 @@ class Archive_Tar // extends PEAR return $v_result; } - // }}} - // {{{ extractModify() /** - * This method extract all the content of the archive in the directory - * indicated by $p_path. When relevant the memorized path of the - * files/dir can be modified by removing the $p_remove_path path at the - * beginning of the file/dir path. - * While extracting a file, if the directory path does not exists it is - * created. - * While extracting a file, if the file already exists it is replaced - * without looking for last modification date. - * While extracting a file, if the file already exists and is write - * protected, the extraction is aborted. - * While extracting a file, if a directory with the same name already - * exists, the extraction is aborted. - * While extracting a directory, if a file with the same name already - * exists, the extraction is aborted. - * While extracting a file/directory if the destination directory exist - * and is write protected, or does not exist but can not be created, - * the extraction is aborted. - * If after extraction an extracted file does not show the correct - * stored file size, the extraction is aborted. - * When the extraction is aborted, a PEAR error text is set and false - * is returned. However the result can be a partial extraction that may - * need to be manually cleaned. - * - * @param string $p_path The path of the directory where the - * files/dir need to by extracted. - * @param string $p_remove_path Part of the memorized path that can be - * removed if present at the beginning of - * the file/dir path. - * @return boolean true on success, false on error. - * @access public - * @see extractList() - */ - function extractModify($p_path, $p_remove_path) + * This method extract all the content of the archive in the directory + * indicated by $p_path. When relevant the memorized path of the + * files/dir can be modified by removing the $p_remove_path path at the + * beginning of the file/dir path. + * While extracting a file, if the directory path does not exists it is + * created. + * While extracting a file, if the file already exists it is replaced + * without looking for last modification date. + * While extracting a file, if the file already exists and is write + * protected, the extraction is aborted. + * While extracting a file, if a directory with the same name already + * exists, the extraction is aborted. + * While extracting a directory, if a file with the same name already + * exists, the extraction is aborted. + * While extracting a file/directory if the destination directory exist + * and is write protected, or does not exist but can not be created, + * the extraction is aborted. + * If after extraction an extracted file does not show the correct + * stored file size, the extraction is aborted. + * When the extraction is aborted, a PEAR error text is set and false + * is returned. However the result can be a partial extraction that may + * need to be manually cleaned. + * + * @param string $p_path The path of the directory where the + * files/dir need to by extracted. + * @param string $p_remove_path Part of the memorized path that can be + * removed if present at the beginning of + * the file/dir path. + * @param boolean $p_preserve Preserve user/group ownership of files + * + * @return boolean true on success, false on error. + * @see extractList() + */ + public function extractModify($p_path, $p_remove_path, $p_preserve = false) { $v_result = true; $v_list_detail = array(); if ($v_result = $this->_openRead()) { - $v_result = $this->_extractList($p_path, $v_list_detail, - "complete", 0, $p_remove_path); + $v_result = $this->_extractList( + $p_path, + $v_list_detail, + "complete", + 0, + $p_remove_path, + $p_preserve + ); $this->_close(); } return $v_result; } - // }}} - // {{{ extractInString() /** - * This method extract from the archive one file identified by $p_filename. - * The return value is a string with the file content, or NULL on error. - * @param string $p_filename The path of the file to extract in a string. - * @return a string with the file content or NULL. - * @access public - */ - function extractInString($p_filename) + * This method extract from the archive one file identified by $p_filename. + * The return value is a string with the file content, or NULL on error. + * + * @param string $p_filename The path of the file to extract in a string. + * + * @return a string with the file content or NULL. + */ + public function extractInString($p_filename) { if ($this->_openRead()) { $v_result = $this->_extractInString($p_filename); $this->_close(); } else { - $v_result = NULL; + $v_result = null; } return $v_result; } - // }}} - // {{{ extractList() /** - * This method extract from the archive only the files indicated in the - * $p_filelist. These files are extracted in the current directory or - * in the directory indicated by the optional $p_path parameter. - * If indicated the $p_remove_path can be used in the same way as it is - * used in extractModify() method. - * @param array $p_filelist An array of filenames and directory names, - * or a single string with names separated - * by a single blank space. - * @param string $p_path The path of the directory where the - * files/dir need to by extracted. - * @param string $p_remove_path Part of the memorized path that can be - * removed if present at the beginning of - * the file/dir path. - * @return true on success, false on error. - * @access public - * @see extractModify() - */ - function extractList($p_filelist, $p_path='', $p_remove_path='') + * This method extract from the archive only the files indicated in the + * $p_filelist. These files are extracted in the current directory or + * in the directory indicated by the optional $p_path parameter. + * If indicated the $p_remove_path can be used in the same way as it is + * used in extractModify() method. + * + * @param array $p_filelist An array of filenames and directory names, + * or a single string with names separated + * by a single blank space. + * @param string $p_path The path of the directory where the + * files/dir need to by extracted. + * @param string $p_remove_path Part of the memorized path that can be + * removed if present at the beginning of + * the file/dir path. + * @param boolean $p_preserve Preserve user/group ownership of files + * + * @return true on success, false on error. + * @see extractModify() + */ + public function extractList($p_filelist, $p_path = '', $p_remove_path = '', $p_preserve = false) { $v_result = true; $v_list_detail = array(); - if (is_array($p_filelist)) + if (is_array($p_filelist)) { $v_list = $p_filelist; - elseif (is_string($p_filelist)) + } elseif (is_string($p_filelist)) { $v_list = explode($this->_separator, $p_filelist); - else { + } else { $this->_error('Invalid string list'); return false; } if ($v_result = $this->_openRead()) { - $v_result = $this->_extractList($p_path, $v_list_detail, "partial", - $v_list, $p_remove_path); + $v_result = $this->_extractList( + $p_path, + $v_list_detail, + "partial", + $v_list, + $p_remove_path, + $p_preserve + ); $this->_close(); } return $v_result; } - // }}} - // {{{ setAttribute() /** - * This method set specific attributes of the archive. It uses a variable - * list of parameters, in the format attribute code + attribute values : - * $arch->setAttribute(ARCHIVE_TAR_ATT_SEPARATOR, ','); - * @param mixed $argv variable list of attributes and values - * @return true on success, false on error. - * @access public - */ - function setAttribute() + * This method set specific attributes of the archive. It uses a variable + * list of parameters, in the format attribute code + attribute values : + * $arch->setAttribute(ARCHIVE_TAR_ATT_SEPARATOR, ','); + * + * @return true on success, false on error. + */ + public function setAttribute() { $v_result = true; @@ -592,30 +714,32 @@ class Archive_Tar // extends PEAR } // ----- Get the arguments - $v_att_list = &func_get_args(); + $v_att_list = & func_get_args(); // ----- Read the attributes - $i=0; - while ($i<$v_size) { + $i = 0; + while ($i < $v_size) { // ----- Look for next option switch ($v_att_list[$i]) { // ----- Look for options that request a string value case ARCHIVE_TAR_ATT_SEPARATOR : // ----- Check the number of parameters - if (($i+1) >= $v_size) { - $this->_error('Invalid number of parameters for ' - .'attribute ARCHIVE_TAR_ATT_SEPARATOR'); + if (($i + 1) >= $v_size) { + $this->_error( + 'Invalid number of parameters for ' + . 'attribute ARCHIVE_TAR_ATT_SEPARATOR' + ); return false; } // ----- Get the value - $this->_separator = $v_att_list[$i+1]; + $this->_separator = $v_att_list[$i + 1]; $i++; - break; + break; default : - $this->_error('Unknow attribute code '.$v_att_list[$i].''); + $this->_error('Unknown attribute code ' . $v_att_list[$i] . ''); return false; } @@ -625,151 +749,248 @@ class Archive_Tar // extends PEAR return $v_result; } - // }}} - // {{{ _error() - function _error($p_message) + /** + * This method sets the regular expression for ignoring files and directories + * at import, for example: + * $arch->setIgnoreRegexp("#CVS|\.svn#"); + * + * @param string $regexp regular expression defining which files or directories to ignore + */ + public function setIgnoreRegexp($regexp) { - // ----- To be completed -// $this->raiseError($p_message); + $this->_ignore_regexp = $regexp; + } + + /** + * This method sets the regular expression for ignoring all files and directories + * matching the filenames in the array list at import, for example: + * $arch->setIgnoreList(array('CVS', '.svn', 'bin/tool')); + * + * @param array $list a list of file or directory names to ignore + * + * @access public + */ + public function setIgnoreList($list) + { + $regexp = str_replace(array('#', '.', '^', '$'), array('\#', '\.', '\^', '\$'), $list); + $regexp = '#/' . join('$|/', $list) . '#'; + $this->setIgnoreRegexp($regexp); + } + + /** + * @param string $p_message + */ + public function _error($p_message) + { + // Drupal change $this->error_object = $this->raiseError($p_message). throw new Exception($p_message); } - // }}} - // {{{ _warning() - function _warning($p_message) + /** + * @param string $p_message + */ + public function _warning($p_message) { - // ----- To be completed -// $this->raiseError($p_message); + // Drupal change $this->error_object = $this->raiseError($p_message). throw new Exception($p_message); } - // }}} - // {{{ _isArchive() - function _isArchive($p_filename=NULL) + /** + * @param string $p_filename + * @return bool + */ + public function _isArchive($p_filename = null) { - if ($p_filename == NULL) { + if ($p_filename == null) { $p_filename = $this->_tarname; } clearstatcache(); return @is_file($p_filename) && !@is_link($p_filename); } - // }}} - // {{{ _openWrite() - function _openWrite() + /** + * @return bool + */ + public function _openWrite() { - if ($this->_compress_type == 'gz') + if ($this->_compress_type == 'gz' && function_exists('gzopen')) { $this->_file = @gzopen($this->_tarname, "wb9"); - else if ($this->_compress_type == 'bz2') - $this->_file = @bzopen($this->_tarname, "w"); - else if ($this->_compress_type == 'none') - $this->_file = @fopen($this->_tarname, "wb"); - else - $this->_error('Unknown or missing compression type (' - .$this->_compress_type.')'); + } else { + if ($this->_compress_type == 'bz2' && function_exists('bzopen')) { + $this->_file = @bzopen($this->_tarname, "w"); + } else { + if ($this->_compress_type == 'lzma2' && function_exists('xzopen')) { + $this->_file = @xzopen($this->_tarname, 'w'); + } else { + if ($this->_compress_type == 'none') { + $this->_file = @fopen($this->_tarname, "wb"); + } else { + $this->_error( + 'Unknown or missing compression type (' + . $this->_compress_type . ')' + ); + return false; + } + } + } + } if ($this->_file == 0) { - $this->_error('Unable to open in write mode \'' - .$this->_tarname.'\''); + $this->_error( + 'Unable to open in write mode \'' + . $this->_tarname . '\'' + ); return false; } return true; } - // }}} - // {{{ _openRead() - function _openRead() + /** + * @return bool + */ + public function _openRead() { if (strtolower(substr($this->_tarname, 0, 7)) == 'http://') { - // ----- Look if a local copy need to be done - if ($this->_temp_tarname == '') { - $this->_temp_tarname = uniqid('tar').'.tmp'; - if (!$v_file_from = @fopen($this->_tarname, 'rb')) { - $this->_error('Unable to open in read mode \'' - .$this->_tarname.'\''); - $this->_temp_tarname = ''; - return false; - } - if (!$v_file_to = @fopen($this->_temp_tarname, 'wb')) { - $this->_error('Unable to open in write mode \'' - .$this->_temp_tarname.'\''); - $this->_temp_tarname = ''; - return false; - } - while ($v_data = @fread($v_file_from, 1024)) - @fwrite($v_file_to, $v_data); - @fclose($v_file_from); - @fclose($v_file_to); - } + // ----- Look if a local copy need to be done + if ($this->_temp_tarname == '') { + $this->_temp_tarname = uniqid('tar') . '.tmp'; + if (!$v_file_from = @fopen($this->_tarname, 'rb')) { + $this->_error( + 'Unable to open in read mode \'' + . $this->_tarname . '\'' + ); + $this->_temp_tarname = ''; + return false; + } + if (!$v_file_to = @fopen($this->_temp_tarname, 'wb')) { + $this->_error( + 'Unable to open in write mode \'' + . $this->_temp_tarname . '\'' + ); + $this->_temp_tarname = ''; + return false; + } + while ($v_data = @fread($v_file_from, 1024)) { + @fwrite($v_file_to, $v_data); + } + @fclose($v_file_from); + @fclose($v_file_to); + } - // ----- File to open if the local copy - $v_filename = $this->_temp_tarname; + // ----- File to open if the local copy + $v_filename = $this->_temp_tarname; + } else { + // ----- File to open if the normal Tar file - } else - // ----- File to open if the normal Tar file - $v_filename = $this->_tarname; + $v_filename = $this->_tarname; + } - if ($this->_compress_type == 'gz') + if ($this->_compress_type == 'gz' && function_exists('gzopen')) { $this->_file = @gzopen($v_filename, "rb"); - else if ($this->_compress_type == 'bz2') - $this->_file = @bzopen($v_filename, "r"); - else if ($this->_compress_type == 'none') - $this->_file = @fopen($v_filename, "rb"); - else - $this->_error('Unknown or missing compression type (' - .$this->_compress_type.')'); + } else { + if ($this->_compress_type == 'bz2' && function_exists('bzopen')) { + $this->_file = @bzopen($v_filename, "r"); + } else { + if ($this->_compress_type == 'lzma2' && function_exists('xzopen')) { + $this->_file = @xzopen($v_filename, "r"); + } else { + if ($this->_compress_type == 'none') { + $this->_file = @fopen($v_filename, "rb"); + } else { + $this->_error( + 'Unknown or missing compression type (' + . $this->_compress_type . ')' + ); + return false; + } + } + } + } if ($this->_file == 0) { - $this->_error('Unable to open in read mode \''.$v_filename.'\''); + $this->_error('Unable to open in read mode \'' . $v_filename . '\''); return false; } return true; } - // }}} - // {{{ _openReadWrite() - function _openReadWrite() + /** + * @return bool + */ + public function _openReadWrite() { - if ($this->_compress_type == 'gz') + if ($this->_compress_type == 'gz') { $this->_file = @gzopen($this->_tarname, "r+b"); - else if ($this->_compress_type == 'bz2') { - $this->_error('Unable to open bz2 in read/write mode \'' - .$this->_tarname.'\' (limitation of bz2 extension)'); - return false; - } else if ($this->_compress_type == 'none') - $this->_file = @fopen($this->_tarname, "r+b"); - else - $this->_error('Unknown or missing compression type (' - .$this->_compress_type.')'); + } else { + if ($this->_compress_type == 'bz2') { + $this->_error( + 'Unable to open bz2 in read/write mode \'' + . $this->_tarname . '\' (limitation of bz2 extension)' + ); + return false; + } else { + if ($this->_compress_type == 'lzma2') { + $this->_error( + 'Unable to open lzma2 in read/write mode \'' + . $this->_tarname . '\' (limitation of lzma2 extension)' + ); + return false; + } else { + if ($this->_compress_type == 'none') { + $this->_file = @fopen($this->_tarname, "r+b"); + } else { + $this->_error( + 'Unknown or missing compression type (' + . $this->_compress_type . ')' + ); + return false; + } + } + } + } if ($this->_file == 0) { - $this->_error('Unable to open in read/write mode \'' - .$this->_tarname.'\''); + $this->_error( + 'Unable to open in read/write mode \'' + . $this->_tarname . '\'' + ); return false; } return true; } - // }}} - // {{{ _close() - function _close() + /** + * @return bool + */ + public function _close() { //if (isset($this->_file)) { if (is_resource($this->_file)) { - if ($this->_compress_type == 'gz') + if ($this->_compress_type == 'gz') { @gzclose($this->_file); - else if ($this->_compress_type == 'bz2') - @bzclose($this->_file); - else if ($this->_compress_type == 'none') - @fclose($this->_file); - else - $this->_error('Unknown or missing compression type (' - .$this->_compress_type.')'); + } else { + if ($this->_compress_type == 'bz2') { + @bzclose($this->_file); + } else { + if ($this->_compress_type == 'lzma2') { + @xzclose($this->_file); + } else { + if ($this->_compress_type == 'none') { + @fclose($this->_file); + } else { + $this->_error( + 'Unknown or missing compression type (' + . $this->_compress_type . ')' + ); + } + } + } + } $this->_file = 0; } @@ -783,10 +1004,11 @@ class Archive_Tar // extends PEAR return true; } - // }}} - // {{{ _cleanFile() - function _cleanFile() + /** + * @return bool + */ + public function _cleanFile() { $this->_close(); @@ -803,296 +1025,419 @@ class Archive_Tar // extends PEAR return true; } - // }}} - // {{{ _writeBlock() - function _writeBlock($p_binary_data, $p_len=null) + /** + * @param mixed $p_binary_data + * @param integer $p_len + * @return bool + */ + public function _writeBlock($p_binary_data, $p_len = null) { - if (is_resource($this->_file)) { - if ($p_len === null) { - if ($this->_compress_type == 'gz') - @gzputs($this->_file, $p_binary_data); - else if ($this->_compress_type == 'bz2') - @bzwrite($this->_file, $p_binary_data); - else if ($this->_compress_type == 'none') - @fputs($this->_file, $p_binary_data); - else - $this->_error('Unknown or missing compression type (' - .$this->_compress_type.')'); - } else { - if ($this->_compress_type == 'gz') - @gzputs($this->_file, $p_binary_data, $p_len); - else if ($this->_compress_type == 'bz2') - @bzwrite($this->_file, $p_binary_data, $p_len); - else if ($this->_compress_type == 'none') - @fputs($this->_file, $p_binary_data, $p_len); - else - $this->_error('Unknown or missing compression type (' - .$this->_compress_type.')'); - - } - } - return true; + if (is_resource($this->_file)) { + if ($p_len === null) { + if ($this->_compress_type == 'gz') { + @gzputs($this->_file, $p_binary_data); + } else { + if ($this->_compress_type == 'bz2') { + @bzwrite($this->_file, $p_binary_data); + } else { + if ($this->_compress_type == 'lzma2') { + @xzwrite($this->_file, $p_binary_data); + } else { + if ($this->_compress_type == 'none') { + @fputs($this->_file, $p_binary_data); + } else { + $this->_error( + 'Unknown or missing compression type (' + . $this->_compress_type . ')' + ); + } + } + } + } + } else { + if ($this->_compress_type == 'gz') { + @gzputs($this->_file, $p_binary_data, $p_len); + } else { + if ($this->_compress_type == 'bz2') { + @bzwrite($this->_file, $p_binary_data, $p_len); + } else { + if ($this->_compress_type == 'lzma2') { + @xzwrite($this->_file, $p_binary_data, $p_len); + } else { + if ($this->_compress_type == 'none') { + @fputs($this->_file, $p_binary_data, $p_len); + } else { + $this->_error( + 'Unknown or missing compression type (' + . $this->_compress_type . ')' + ); + } + } + } + } + } + } + return true; } - // }}} - // {{{ _readBlock() - function _readBlock() + /** + * @return null|string + */ + public function _readBlock() { - $v_block = null; - if (is_resource($this->_file)) { - if ($this->_compress_type == 'gz') - $v_block = @gzread($this->_file, 512); - else if ($this->_compress_type == 'bz2') - $v_block = @bzread($this->_file, 512); - else if ($this->_compress_type == 'none') - $v_block = @fread($this->_file, 512); - else - $this->_error('Unknown or missing compression type (' - .$this->_compress_type.')'); - } - return $v_block; + $v_block = null; + if (is_resource($this->_file)) { + if ($this->_compress_type == 'gz') { + $v_block = @gzread($this->_file, 512); + } else { + if ($this->_compress_type == 'bz2') { + $v_block = @bzread($this->_file, 512); + } else { + if ($this->_compress_type == 'lzma2') { + $v_block = @xzread($this->_file, 512); + } else { + if ($this->_compress_type == 'none') { + $v_block = @fread($this->_file, 512); + } else { + $this->_error( + 'Unknown or missing compression type (' + . $this->_compress_type . ')' + ); + } + } + } + } + } + return $v_block; } - // }}} - // {{{ _jumpBlock() - function _jumpBlock($p_len=null) + /** + * @param null $p_len + * @return bool + */ + public function _jumpBlock($p_len = null) { - if (is_resource($this->_file)) { - if ($p_len === null) - $p_len = 1; + if (is_resource($this->_file)) { + if ($p_len === null) { + $p_len = 1; + } - if ($this->_compress_type == 'gz') { - @gzseek($this->_file, gztell($this->_file)+($p_len*512)); - } - else if ($this->_compress_type == 'bz2') { - // ----- Replace missing bztell() and bzseek() - for ($i=0; $i<$p_len; $i++) - $this->_readBlock(); - } else if ($this->_compress_type == 'none') - @fseek($this->_file, ftell($this->_file)+($p_len*512)); - else - $this->_error('Unknown or missing compression type (' - .$this->_compress_type.')'); - - } - return true; + if ($this->_compress_type == 'gz') { + @gzseek($this->_file, gztell($this->_file) + ($p_len * 512)); + } else { + if ($this->_compress_type == 'bz2') { + // ----- Replace missing bztell() and bzseek() + for ($i = 0; $i < $p_len; $i++) { + $this->_readBlock(); + } + } else { + if ($this->_compress_type == 'lzma2') { + // ----- Replace missing xztell() and xzseek() + for ($i = 0; $i < $p_len; $i++) { + $this->_readBlock(); + } + } else { + if ($this->_compress_type == 'none') { + @fseek($this->_file, $p_len * 512, SEEK_CUR); + } else { + $this->_error( + 'Unknown or missing compression type (' + . $this->_compress_type . ')' + ); + } + } + } + } + } + return true; } - // }}} - // {{{ _writeFooter() - function _writeFooter() + /** + * @return bool + */ + public function _writeFooter() { - if (is_resource($this->_file)) { - // ----- Write the last 0 filled block for end of archive - $v_binary_data = pack('a1024', ''); - $this->_writeBlock($v_binary_data); - } - return true; + if (is_resource($this->_file)) { + // ----- Write the last 0 filled block for end of archive + $v_binary_data = pack('a1024', ''); + $this->_writeBlock($v_binary_data); + } + return true; } - // }}} - // {{{ _addList() - function _addList($p_list, $p_add_dir, $p_remove_dir) + /** + * @param array $p_list + * @param string $p_add_dir + * @param string $p_remove_dir + * @return bool + */ + public function _addList($p_list, $p_add_dir, $p_remove_dir) { - $v_result=true; - $v_header = array(); + $v_result = true; + $v_header = array(); - // ----- Remove potential windows directory separator - $p_add_dir = $this->_translateWinPath($p_add_dir); - $p_remove_dir = $this->_translateWinPath($p_remove_dir, false); + // ----- Remove potential windows directory separator + $p_add_dir = $this->_translateWinPath($p_add_dir); + $p_remove_dir = $this->_translateWinPath($p_remove_dir, false); - if (!$this->_file) { - $this->_error('Invalid file descriptor'); - return false; - } - - if (sizeof($p_list) == 0) - return true; - - foreach ($p_list as $v_filename) { - if (!$v_result) { - break; - } - - // ----- Skip the current tar name - if ($v_filename == $this->_tarname) - continue; - - if ($v_filename == '') - continue; - - if (!file_exists($v_filename)) { - $this->_warning("File '$v_filename' does not exist"); - continue; + if (!$this->_file) { + $this->_error('Invalid file descriptor'); + return false; } - // ----- Add the file or directory header - if (!$this->_addFile($v_filename, $v_header, $p_add_dir, $p_remove_dir)) - return false; + if (sizeof($p_list) == 0) { + return true; + } - if (@is_dir($v_filename) && !@is_link($v_filename)) { - if (!($p_hdir = opendir($v_filename))) { - $this->_warning("Directory '$v_filename' can not be read"); + foreach ($p_list as $v_filename) { + if (!$v_result) { + break; + } + + // ----- Skip the current tar name + if ($v_filename == $this->_tarname) { continue; } - while (false !== ($p_hitem = readdir($p_hdir))) { - if (($p_hitem != '.') && ($p_hitem != '..')) { - if ($v_filename != ".") - $p_temp_list[0] = $v_filename.'/'.$p_hitem; - else - $p_temp_list[0] = $p_hitem; - $v_result = $this->_addList($p_temp_list, - $p_add_dir, - $p_remove_dir); + if ($v_filename == '') { + continue; + } + + // ----- ignore files and directories matching the ignore regular expression + if ($this->_ignore_regexp && preg_match($this->_ignore_regexp, '/' . $v_filename)) { + $this->_warning("File '$v_filename' ignored"); + continue; + } + + if (!file_exists($v_filename) && !is_link($v_filename)) { + $this->_warning("File '$v_filename' does not exist"); + continue; + } + + // ----- Add the file or directory header + if (!$this->_addFile($v_filename, $v_header, $p_add_dir, $p_remove_dir)) { + return false; + } + + if (@is_dir($v_filename) && !@is_link($v_filename)) { + if (!($p_hdir = opendir($v_filename))) { + $this->_warning("Directory '$v_filename' can not be read"); + continue; + } + while (false !== ($p_hitem = readdir($p_hdir))) { + if (($p_hitem != '.') && ($p_hitem != '..')) { + if ($v_filename != ".") { + $p_temp_list[0] = $v_filename . '/' . $p_hitem; + } else { + $p_temp_list[0] = $p_hitem; + } + + $v_result = $this->_addList( + $p_temp_list, + $p_add_dir, + $p_remove_dir + ); + } + } + + unset($p_temp_list); + unset($p_hdir); + unset($p_hitem); + } + } + + return $v_result; + } + + /** + * @param string $p_filename + * @param mixed $p_header + * @param string $p_add_dir + * @param string $p_remove_dir + * @param null $v_stored_filename + * @return bool + */ + public function _addFile($p_filename, &$p_header, $p_add_dir, $p_remove_dir, $v_stored_filename = null) + { + if (!$this->_file) { + $this->_error('Invalid file descriptor'); + return false; + } + + if ($p_filename == '') { + $this->_error('Invalid file name'); + return false; + } + + if (is_null($v_stored_filename)) { + // ----- Calculate the stored filename + $p_filename = $this->_translateWinPath($p_filename, false); + $v_stored_filename = $p_filename; + + if (strcmp($p_filename, $p_remove_dir) == 0) { + return true; + } + + if ($p_remove_dir != '') { + if (substr($p_remove_dir, -1) != '/') { + $p_remove_dir .= '/'; + } + + if (substr($p_filename, 0, strlen($p_remove_dir)) == $p_remove_dir) { + $v_stored_filename = substr($p_filename, strlen($p_remove_dir)); } } - unset($p_temp_list); - unset($p_hdir); - unset($p_hitem); + $v_stored_filename = $this->_translateWinPath($v_stored_filename); + if ($p_add_dir != '') { + if (substr($p_add_dir, -1) == '/') { + $v_stored_filename = $p_add_dir . $v_stored_filename; + } else { + $v_stored_filename = $p_add_dir . '/' . $v_stored_filename; + } + } + + $v_stored_filename = $this->_pathReduction($v_stored_filename); } - } - return $v_result; + if ($this->_isArchive($p_filename)) { + if (($v_file = @fopen($p_filename, "rb")) == 0) { + $this->_warning( + "Unable to open file '" . $p_filename + . "' in binary read mode" + ); + return true; + } + + if (!$this->_writeHeader($p_filename, $v_stored_filename)) { + return false; + } + + while (($v_buffer = fread($v_file, 512)) != '') { + $v_binary_data = pack("a512", "$v_buffer"); + $this->_writeBlock($v_binary_data); + } + + fclose($v_file); + } else { + // ----- Only header for dir + if (!$this->_writeHeader($p_filename, $v_stored_filename)) { + return false; + } + } + + return true; } - // }}} - // {{{ _addFile() - function _addFile($p_filename, &$p_header, $p_add_dir, $p_remove_dir) + /** + * @param string $p_filename + * @param string $p_string + * @param bool $p_datetime + * @param array $p_params + * @return bool + */ + public function _addString($p_filename, $p_string, $p_datetime = false, $p_params = array()) { - if (!$this->_file) { - $this->_error('Invalid file descriptor'); - return false; - } - - if ($p_filename == '') { - $this->_error('Invalid file name'); - return false; - } - - // ----- Calculate the stored filename - $p_filename = $this->_translateWinPath($p_filename, false);; - $v_stored_filename = $p_filename; - if (strcmp($p_filename, $p_remove_dir) == 0) { - return true; - } - if ($p_remove_dir != '') { - if (substr($p_remove_dir, -1) != '/') - $p_remove_dir .= '/'; - - if (substr($p_filename, 0, strlen($p_remove_dir)) == $p_remove_dir) - $v_stored_filename = substr($p_filename, strlen($p_remove_dir)); - } - $v_stored_filename = $this->_translateWinPath($v_stored_filename); - if ($p_add_dir != '') { - if (substr($p_add_dir, -1) == '/') - $v_stored_filename = $p_add_dir.$v_stored_filename; - else - $v_stored_filename = $p_add_dir.'/'.$v_stored_filename; - } - - $v_stored_filename = $this->_pathReduction($v_stored_filename); - - if ($this->_isArchive($p_filename)) { - if (($v_file = @fopen($p_filename, "rb")) == 0) { - $this->_warning("Unable to open file '".$p_filename - ."' in binary read mode"); - return true; - } - - if (!$this->_writeHeader($p_filename, $v_stored_filename)) - return false; - - while (($v_buffer = fread($v_file, 512)) != '') { - $v_binary_data = pack("a512", "$v_buffer"); - $this->_writeBlock($v_binary_data); - } - - fclose($v_file); - - } else { - // ----- Only header for dir - if (!$this->_writeHeader($p_filename, $v_stored_filename)) - return false; - } - - return true; - } - // }}} - - // {{{ _addString() - function _addString($p_filename, $p_string) - { - if (!$this->_file) { - $this->_error('Invalid file descriptor'); - return false; - } - - if ($p_filename == '') { - $this->_error('Invalid file name'); - return false; - } - - // ----- Calculate the stored filename - $p_filename = $this->_translateWinPath($p_filename, false);; - - if (!$this->_writeHeaderBlock($p_filename, strlen($p_string), - time(), 384, "", 0, 0)) - return false; - - $i=0; - while (($v_buffer = substr($p_string, (($i++)*512), 512)) != '') { - $v_binary_data = pack("a512", $v_buffer); - $this->_writeBlock($v_binary_data); - } - - return true; - } - // }}} - - // {{{ _writeHeader() - function _writeHeader($p_filename, $p_stored_filename) - { - if ($p_stored_filename == '') - $p_stored_filename = $p_filename; - $v_reduce_filename = $this->_pathReduction($p_stored_filename); - - if (strlen($v_reduce_filename) > 99) { - if (!$this->_writeLongHeader($v_reduce_filename)) + $p_stamp = @$p_params["stamp"] ? $p_params["stamp"] : ($p_datetime ? $p_datetime : time()); + $p_mode = @$p_params["mode"] ? $p_params["mode"] : 0600; + $p_type = @$p_params["type"] ? $p_params["type"] : ""; + $p_uid = @$p_params["uid"] ? $p_params["uid"] : 0; + $p_gid = @$p_params["gid"] ? $p_params["gid"] : 0; + if (!$this->_file) { + $this->_error('Invalid file descriptor'); return false; } - $v_info = lstat($p_filename); - $v_uid = sprintf("%6s ", DecOct($v_info[4])); - $v_gid = sprintf("%6s ", DecOct($v_info[5])); - $v_perms = sprintf("%6s ", DecOct($v_info['mode'])); + if ($p_filename == '') { + $this->_error('Invalid file name'); + return false; + } - $v_mtime = sprintf("%11s", DecOct($v_info['mode'])); + // ----- Calculate the stored filename + $p_filename = $this->_translateWinPath($p_filename, false); + + // ----- If datetime is not specified, set current time + if ($p_datetime === false) { + $p_datetime = time(); + } + + if (!$this->_writeHeaderBlock( + $p_filename, + strlen($p_string), + $p_stamp, + $p_mode, + $p_type, + $p_uid, + $p_gid + ) + ) { + return false; + } + + $i = 0; + while (($v_buffer = substr($p_string, (($i++) * 512), 512)) != '') { + $v_binary_data = pack("a512", $v_buffer); + $this->_writeBlock($v_binary_data); + } + + return true; + } + + /** + * @param string $p_filename + * @param string $p_stored_filename + * @return bool + */ + public function _writeHeader($p_filename, $p_stored_filename) + { + if ($p_stored_filename == '') { + $p_stored_filename = $p_filename; + } + $v_reduce_filename = $this->_pathReduction($p_stored_filename); + + if (strlen($v_reduce_filename) > 99) { + if (!$this->_writeLongHeader($v_reduce_filename)) { + return false; + } + } + + $v_info = lstat($p_filename); + $v_uid = sprintf("%07s", DecOct($v_info[4])); + $v_gid = sprintf("%07s", DecOct($v_info[5])); + $v_perms = sprintf("%07s", DecOct($v_info['mode'] & 000777)); + + $v_mtime = sprintf("%011s", DecOct($v_info['mtime'])); $v_linkname = ''; if (@is_link($p_filename)) { - $v_typeflag = '2'; - $v_linkname = readlink($p_filename); - $v_size = sprintf("%11s ", DecOct(0)); + $v_typeflag = '2'; + $v_linkname = readlink($p_filename); + $v_size = sprintf("%011s", DecOct(0)); } elseif (@is_dir($p_filename)) { - $v_typeflag = "5"; - $v_size = sprintf("%11s ", DecOct(0)); + $v_typeflag = "5"; + $v_size = sprintf("%011s", DecOct(0)); } else { - $v_typeflag = ''; - clearstatcache(); - $v_size = sprintf("%11s ", DecOct($v_info['size'])); + $v_typeflag = '0'; + clearstatcache(); + $v_size = sprintf("%011s", DecOct($v_info['size'])); } - $v_magic = ''; + $v_magic = 'ustar '; - $v_version = ''; + $v_version = ' '; - $v_uname = ''; + if (function_exists('posix_getpwuid')) { + $userinfo = posix_getpwuid($v_info[4]); + $groupinfo = posix_getgrgid($v_info[5]); - $v_gname = ''; + $v_uname = $userinfo['name']; + $v_gname = $groupinfo['name']; + } else { + $v_uname = ''; + $v_gname = ''; + } $v_devmajor = ''; @@ -1100,31 +1445,49 @@ class Archive_Tar // extends PEAR $v_prefix = ''; - $v_binary_data_first = pack("a100a8a8a8a12A12", - $v_reduce_filename, $v_perms, $v_uid, - $v_gid, $v_size, $v_mtime); - $v_binary_data_last = pack("a1a100a6a2a32a32a8a8a155a12", - $v_typeflag, $v_linkname, $v_magic, - $v_version, $v_uname, $v_gname, - $v_devmajor, $v_devminor, $v_prefix, ''); + $v_binary_data_first = pack( + "a100a8a8a8a12a12", + $v_reduce_filename, + $v_perms, + $v_uid, + $v_gid, + $v_size, + $v_mtime + ); + $v_binary_data_last = pack( + "a1a100a6a2a32a32a8a8a155a12", + $v_typeflag, + $v_linkname, + $v_magic, + $v_version, + $v_uname, + $v_gname, + $v_devmajor, + $v_devminor, + $v_prefix, + '' + ); // ----- Calculate the checksum $v_checksum = 0; // ..... First part of the header - for ($i=0; $i<148; $i++) - $v_checksum += ord(substr($v_binary_data_first,$i,1)); + for ($i = 0; $i < 148; $i++) { + $v_checksum += ord(substr($v_binary_data_first, $i, 1)); + } // ..... Ignore the checksum value and replace it by ' ' (space) - for ($i=148; $i<156; $i++) + for ($i = 148; $i < 156; $i++) { $v_checksum += ord(' '); + } // ..... Last part of the header - for ($i=156, $j=0; $i<512; $i++, $j++) - $v_checksum += ord(substr($v_binary_data_last,$j,1)); + for ($i = 156, $j = 0; $i < 512; $i++, $j++) { + $v_checksum += ord(substr($v_binary_data_last, $j, 1)); + } // ----- Write the first 148 bytes of the header in the archive $this->_writeBlock($v_binary_data_first, 148); // ----- Write the calculated checksum - $v_checksum = sprintf("%6s ", DecOct($v_checksum)); + $v_checksum = sprintf("%06s ", DecOct($v_checksum)); $v_binary_data = pack("a8", $v_checksum); $this->_writeBlock($v_binary_data, 8); @@ -1133,40 +1496,62 @@ class Archive_Tar // extends PEAR return true; } - // }}} - // {{{ _writeHeaderBlock() - function _writeHeaderBlock($p_filename, $p_size, $p_mtime=0, $p_perms=0, - $p_type='', $p_uid=0, $p_gid=0) - { + /** + * @param string $p_filename + * @param int $p_size + * @param int $p_mtime + * @param int $p_perms + * @param string $p_type + * @param int $p_uid + * @param int $p_gid + * @return bool + */ + public function _writeHeaderBlock( + $p_filename, + $p_size, + $p_mtime = 0, + $p_perms = 0, + $p_type = '', + $p_uid = 0, + $p_gid = 0 + ) { $p_filename = $this->_pathReduction($p_filename); if (strlen($p_filename) > 99) { - if (!$this->_writeLongHeader($p_filename)) - return false; + if (!$this->_writeLongHeader($p_filename)) { + return false; + } } if ($p_type == "5") { - $v_size = sprintf("%11s ", DecOct(0)); + $v_size = sprintf("%011s", DecOct(0)); } else { - $v_size = sprintf("%11s ", DecOct($p_size)); + $v_size = sprintf("%011s", DecOct($p_size)); } - $v_uid = sprintf("%6s ", DecOct($p_uid)); - $v_gid = sprintf("%6s ", DecOct($p_gid)); - $v_perms = sprintf("%6s ", DecOct($p_perms)); + $v_uid = sprintf("%07s", DecOct($p_uid)); + $v_gid = sprintf("%07s", DecOct($p_gid)); + $v_perms = sprintf("%07s", DecOct($p_perms & 000777)); $v_mtime = sprintf("%11s", DecOct($p_mtime)); $v_linkname = ''; - $v_magic = ''; + $v_magic = 'ustar '; - $v_version = ''; + $v_version = ' '; - $v_uname = ''; + if (function_exists('posix_getpwuid')) { + $userinfo = posix_getpwuid($p_uid); + $groupinfo = posix_getgrgid($p_gid); - $v_gname = ''; + $v_uname = $userinfo['name']; + $v_gname = $groupinfo['name']; + } else { + $v_uname = ''; + $v_gname = ''; + } $v_devmajor = ''; @@ -1174,31 +1559,49 @@ class Archive_Tar // extends PEAR $v_prefix = ''; - $v_binary_data_first = pack("a100a8a8a8a12A12", - $p_filename, $v_perms, $v_uid, $v_gid, - $v_size, $v_mtime); - $v_binary_data_last = pack("a1a100a6a2a32a32a8a8a155a12", - $p_type, $v_linkname, $v_magic, - $v_version, $v_uname, $v_gname, - $v_devmajor, $v_devminor, $v_prefix, ''); + $v_binary_data_first = pack( + "a100a8a8a8a12A12", + $p_filename, + $v_perms, + $v_uid, + $v_gid, + $v_size, + $v_mtime + ); + $v_binary_data_last = pack( + "a1a100a6a2a32a32a8a8a155a12", + $p_type, + $v_linkname, + $v_magic, + $v_version, + $v_uname, + $v_gname, + $v_devmajor, + $v_devminor, + $v_prefix, + '' + ); // ----- Calculate the checksum $v_checksum = 0; // ..... First part of the header - for ($i=0; $i<148; $i++) - $v_checksum += ord(substr($v_binary_data_first,$i,1)); + for ($i = 0; $i < 148; $i++) { + $v_checksum += ord(substr($v_binary_data_first, $i, 1)); + } // ..... Ignore the checksum value and replace it by ' ' (space) - for ($i=148; $i<156; $i++) + for ($i = 148; $i < 156; $i++) { $v_checksum += ord(' '); + } // ..... Last part of the header - for ($i=156, $j=0; $i<512; $i++, $j++) - $v_checksum += ord(substr($v_binary_data_last,$j,1)); + for ($i = 156, $j = 0; $i < 512; $i++, $j++) { + $v_checksum += ord(substr($v_binary_data_last, $j, 1)); + } // ----- Write the first 148 bytes of the header in the archive $this->_writeBlock($v_binary_data_first, 148); // ----- Write the calculated checksum - $v_checksum = sprintf("%6s ", DecOct($v_checksum)); + $v_checksum = sprintf("%06s ", DecOct($v_checksum)); $v_binary_data = pack("a8", $v_checksum); $this->_writeBlock($v_binary_data, 8); @@ -1207,10 +1610,12 @@ class Archive_Tar // extends PEAR return true; } - // }}} - // {{{ _writeLongHeader() - function _writeLongHeader($p_filename) + /** + * @param string $p_filename + * @return bool + */ + public function _writeLongHeader($p_filename) { $v_size = sprintf("%11s ", DecOct(strlen($p_filename))); @@ -1232,30 +1637,49 @@ class Archive_Tar // extends PEAR $v_prefix = ''; - $v_binary_data_first = pack("a100a8a8a8a12A12", - '././@LongLink', 0, 0, 0, $v_size, 0); - $v_binary_data_last = pack("a1a100a6a2a32a32a8a8a155a12", - $v_typeflag, $v_linkname, $v_magic, - $v_version, $v_uname, $v_gname, - $v_devmajor, $v_devminor, $v_prefix, ''); + $v_binary_data_first = pack( + "a100a8a8a8a12a12", + '././@LongLink', + 0, + 0, + 0, + $v_size, + 0 + ); + $v_binary_data_last = pack( + "a1a100a6a2a32a32a8a8a155a12", + $v_typeflag, + $v_linkname, + $v_magic, + $v_version, + $v_uname, + $v_gname, + $v_devmajor, + $v_devminor, + $v_prefix, + '' + ); // ----- Calculate the checksum $v_checksum = 0; // ..... First part of the header - for ($i=0; $i<148; $i++) - $v_checksum += ord(substr($v_binary_data_first,$i,1)); + for ($i = 0; $i < 148; $i++) { + $v_checksum += ord(substr($v_binary_data_first, $i, 1)); + } // ..... Ignore the checksum value and replace it by ' ' (space) - for ($i=148; $i<156; $i++) + for ($i = 148; $i < 156; $i++) { $v_checksum += ord(' '); + } // ..... Last part of the header - for ($i=156, $j=0; $i<512; $i++, $j++) - $v_checksum += ord(substr($v_binary_data_last,$j,1)); + for ($i = 156, $j = 0; $i < 512; $i++, $j++) { + $v_checksum += ord(substr($v_binary_data_last, $j, 1)); + } // ----- Write the first 148 bytes of the header in the archive $this->_writeBlock($v_binary_data_first, 148); // ----- Write the calculated checksum - $v_checksum = sprintf("%6s ", DecOct($v_checksum)); + $v_checksum = sprintf("%06s ", DecOct($v_checksum)); $v_binary_data = pack("a8", $v_checksum); $this->_writeBlock($v_binary_data, 8); @@ -1263,27 +1687,30 @@ class Archive_Tar // extends PEAR $this->_writeBlock($v_binary_data_last, 356); // ----- Write the filename as content of the block - $i=0; - while (($v_buffer = substr($p_filename, (($i++)*512), 512)) != '') { + $i = 0; + while (($v_buffer = substr($p_filename, (($i++) * 512), 512)) != '') { $v_binary_data = pack("a512", "$v_buffer"); $this->_writeBlock($v_binary_data); } return true; } - // }}} - // {{{ _readHeader() - function _readHeader($v_binary_data, &$v_header) + /** + * @param mixed $v_binary_data + * @param mixed $v_header + * @return bool + */ + public function _readHeader($v_binary_data, &$v_header) { - if (strlen($v_binary_data)==0) { + if (strlen($v_binary_data) == 0) { $v_header['filename'] = ''; return true; } if (strlen($v_binary_data) != 512) { $v_header['filename'] = ''; - $this->_error('Invalid block size : '.strlen($v_binary_data)); + $this->_error('Invalid block size : ' . strlen($v_binary_data)); return false; } @@ -1293,19 +1720,32 @@ class Archive_Tar // extends PEAR // ----- Calculate the checksum $v_checksum = 0; // ..... First part of the header - for ($i=0; $i<148; $i++) - $v_checksum+=ord(substr($v_binary_data,$i,1)); + for ($i = 0; $i < 148; $i++) { + $v_checksum += ord(substr($v_binary_data, $i, 1)); + } // ..... Ignore the checksum value and replace it by ' ' (space) - for ($i=148; $i<156; $i++) + for ($i = 148; $i < 156; $i++) { $v_checksum += ord(' '); + } // ..... Last part of the header - for ($i=156; $i<512; $i++) - $v_checksum+=ord(substr($v_binary_data,$i,1)); + for ($i = 156; $i < 512; $i++) { + $v_checksum += ord(substr($v_binary_data, $i, 1)); + } - $v_data = unpack("a100filename/a8mode/a8uid/a8gid/a12size/a12mtime/" - ."a8checksum/a1typeflag/a100link/a6magic/a2version/" - ."a32uname/a32gname/a8devmajor/a8devminor", - $v_binary_data); + if (version_compare(PHP_VERSION, "5.5.0-dev") < 0) { + $fmt = "a100filename/a8mode/a8uid/a8gid/a12size/a12mtime/" . + "a8checksum/a1typeflag/a100link/a6magic/a2version/" . + "a32uname/a32gname/a8devmajor/a8devminor/a131prefix"; + } else { + $fmt = "Z100filename/Z8mode/Z8uid/Z8gid/Z12size/Z12mtime/" . + "Z8checksum/Z1typeflag/Z100link/Z6magic/Z2version/" . + "Z32uname/Z32gname/Z8devmajor/Z8devminor/Z131prefix"; + } + $v_data = unpack($fmt, $v_binary_data); + + if (strlen($v_data["prefix"]) > 0) { + $v_data["filename"] = "$v_data[prefix]/$v_data[filename]"; + } // ----- Extract the checksum $v_header['checksum'] = OctDec(trim($v_data['checksum'])); @@ -1313,20 +1753,25 @@ class Archive_Tar // extends PEAR $v_header['filename'] = ''; // ----- Look for last block (empty block) - if (($v_checksum == 256) && ($v_header['checksum'] == 0)) + if (($v_checksum == 256) && ($v_header['checksum'] == 0)) { return true; + } - $this->_error('Invalid checksum for file "'.$v_data['filename'] - .'" : '.$v_checksum.' calculated, ' - .$v_header['checksum'].' expected'); + $this->_error( + 'Invalid checksum for file "' . $v_data['filename'] + . '" : ' . $v_checksum . ' calculated, ' + . $v_header['checksum'] . ' expected' + ); return false; } // ----- Extract the properties - $v_header['filename'] = trim($v_data['filename']); + $v_header['filename'] = rtrim($v_data['filename'], "\0"); if ($this->_maliciousFilename($v_header['filename'])) { - $this->_error('Malicious .tar detected, file "' . $v_header['filename'] . - '" will not install in desired directory tree'); + $this->_error( + 'Malicious .tar detected, file "' . $v_header['filename'] . + '" will not install in desired directory tree' + ); return false; } $v_header['mode'] = OctDec(trim($v_data['mode'])); @@ -1335,11 +1780,11 @@ class Archive_Tar // extends PEAR $v_header['size'] = OctDec(trim($v_data['size'])); $v_header['mtime'] = OctDec(trim($v_data['mtime'])); if (($v_header['typeflag'] = $v_data['typeflag']) == "5") { - $v_header['size'] = 0; + $v_header['size'] = 0; } $v_header['link'] = trim($v_data['link']); /* ----- All these fields are removed form the header because - they do not carry interesting info + they do not carry interesting info $v_header[magic] = trim($v_data[magic]); $v_header[version] = trim($v_data[version]); $v_header[uname] = trim($v_data[uname]); @@ -1350,17 +1795,15 @@ class Archive_Tar // extends PEAR return true; } - // }}} - // {{{ _maliciousFilename() /** * Detect and report a malicious file name * * @param string $file + * * @return bool - * @access private */ - function _maliciousFilename($file) + private function _maliciousFilename($file) { if (strpos($file, '/../') !== false) { return true; @@ -1370,386 +1813,507 @@ class Archive_Tar // extends PEAR } return false; } - // }}} - // {{{ _readLongHeader() - function _readLongHeader(&$v_header) - { - $v_filename = ''; - $n = floor($v_header['size']/512); - for ($i=0; $i<$n; $i++) { - $v_content = $this->_readBlock(); - $v_filename .= $v_content; - } - if (($v_header['size'] % 512) != 0) { - $v_content = $this->_readBlock(); - $v_filename .= $v_content; - } - - // ----- Read the next header - $v_binary_data = $this->_readBlock(); - - if (!$this->_readHeader($v_binary_data, $v_header)) - return false; - - $v_filename = trim($v_filename); - $v_header['filename'] = $v_filename; - if ($this->_maliciousFilename($v_filename)) { - $this->_error('Malicious .tar detected, file "' . $v_filename . - '" will not install in desired directory tree'); - return false; - } - - return true; - } - // }}} - - // {{{ _extractInString() /** - * This method extract from the archive one file identified by $p_filename. - * The return value is a string with the file content, or NULL on error. - * @param string $p_filename The path of the file to extract in a string. - * @return a string with the file content or NULL. - * @access private - */ - function _extractInString($p_filename) + * @param $v_header + * @return bool + */ + public function _readLongHeader(&$v_header) { - $v_result_str = ""; - - While (strlen($v_binary_data = $this->_readBlock()) != 0) - { - if (!$this->_readHeader($v_binary_data, $v_header)) - return NULL; - - if ($v_header['filename'] == '') - continue; - - // ----- Look for long filename - if ($v_header['typeflag'] == 'L') { - if (!$this->_readLongHeader($v_header)) - return NULL; - } - - if ($v_header['filename'] == $p_filename) { - if ($v_header['typeflag'] == "5") { - $this->_error('Unable to extract in string a directory ' - .'entry {'.$v_header['filename'].'}'); - return NULL; - } else { - $n = floor($v_header['size']/512); - for ($i=0; $i<$n; $i++) { - $v_result_str .= $this->_readBlock(); - } - if (($v_header['size'] % 512) != 0) { - $v_content = $this->_readBlock(); - $v_result_str .= substr($v_content, 0, - ($v_header['size'] % 512)); - } - return $v_result_str; - } - } else { - $this->_jumpBlock(ceil(($v_header['size']/512))); - } + $v_filename = ''; + $v_filesize = $v_header['size']; + $n = floor($v_header['size'] / 512); + for ($i = 0; $i < $n; $i++) { + $v_content = $this->_readBlock(); + $v_filename .= $v_content; + } + if (($v_header['size'] % 512) != 0) { + $v_content = $this->_readBlock(); + $v_filename .= $v_content; } - return NULL; - } - // }}} + // ----- Read the next header + $v_binary_data = $this->_readBlock(); - // {{{ _extractList() - function _extractList($p_path, &$p_list_detail, $p_mode, - $p_file_list, $p_remove_path) - { - $v_result=true; - $v_nb = 0; - $v_extract_all = true; - $v_listing = false; - - $p_path = $this->_translateWinPath($p_path, false); - if ($p_path == '' || (substr($p_path, 0, 1) != '/' - && substr($p_path, 0, 3) != "../" && !strpos($p_path, ':'))) { - $p_path = "./".$p_path; - } - $p_remove_path = $this->_translateWinPath($p_remove_path); - - // ----- Look for path to remove format (should end by /) - if (($p_remove_path != '') && (substr($p_remove_path, -1) != '/')) - $p_remove_path .= '/'; - $p_remove_path_size = strlen($p_remove_path); - - switch ($p_mode) { - case "complete" : - $v_extract_all = TRUE; - $v_listing = FALSE; - break; - case "partial" : - $v_extract_all = FALSE; - $v_listing = FALSE; - break; - case "list" : - $v_extract_all = FALSE; - $v_listing = TRUE; - break; - default : - $this->_error('Invalid extract mode ('.$p_mode.')'); - return false; - } - - clearstatcache(); - - while (strlen($v_binary_data = $this->_readBlock()) != 0) - { - $v_extract_file = FALSE; - $v_extraction_stopped = 0; - - if (!$this->_readHeader($v_binary_data, $v_header)) - return false; - - if ($v_header['filename'] == '') { - continue; - } - - // ----- Look for long filename - if ($v_header['typeflag'] == 'L') { - if (!$this->_readLongHeader($v_header)) - return false; - } - - if ((!$v_extract_all) && (is_array($p_file_list))) { - // ----- By default no unzip if the file is not found - $v_extract_file = false; - - for ($i=0; $i strlen($p_file_list[$i])) - && (substr($v_header['filename'], 0, strlen($p_file_list[$i])) - == $p_file_list[$i])) { - $v_extract_file = TRUE; - break; - } - } - - // ----- It is a file, so compare the file names - elseif ($p_file_list[$i] == $v_header['filename']) { - $v_extract_file = TRUE; - break; - } - } - } else { - $v_extract_file = TRUE; - } - - // ----- Look if this file need to be extracted - if (($v_extract_file) && (!$v_listing)) - { - if (($p_remove_path != '') - && (substr($v_header['filename'], 0, $p_remove_path_size) - == $p_remove_path)) - $v_header['filename'] = substr($v_header['filename'], - $p_remove_path_size); - if (($p_path != './') && ($p_path != '/')) { - while (substr($p_path, -1) == '/') - $p_path = substr($p_path, 0, strlen($p_path)-1); - - if (substr($v_header['filename'], 0, 1) == '/') - $v_header['filename'] = $p_path.$v_header['filename']; - else - $v_header['filename'] = $p_path.'/'.$v_header['filename']; - } - if (file_exists($v_header['filename'])) { - if ( (@is_dir($v_header['filename'])) - && ($v_header['typeflag'] == '')) { - $this->_error('File '.$v_header['filename'] - .' already exists as a directory'); - return false; - } - if ( ($this->_isArchive($v_header['filename'])) - && ($v_header['typeflag'] == "5")) { - $this->_error('Directory '.$v_header['filename'] - .' already exists as a file'); - return false; - } - if (!is_writeable($v_header['filename'])) { - $this->_error('File '.$v_header['filename'] - .' already exists and is write protected'); - return false; - } - if (filemtime($v_header['filename']) > $v_header['mtime']) { - // To be completed : An error or silent no replace ? - } - } - - // ----- Check the directory availability and create it if necessary - elseif (($v_result - = $this->_dirCheck(($v_header['typeflag'] == "5" - ?$v_header['filename'] - :dirname($v_header['filename'])))) != 1) { - $this->_error('Unable to create path for '.$v_header['filename']); + if (!$this->_readHeader($v_binary_data, $v_header)) { return false; } - if ($v_extract_file) { - if ($v_header['typeflag'] == "5") { - if (!@file_exists($v_header['filename'])) { - // Drupal integration. - // Changed the code to use drupal_mkdir() instead of mkdir(). - if (!@drupal_mkdir($v_header['filename'], 0777)) { - $this->_error('Unable to create directory {' - .$v_header['filename'].'}'); - return false; - } - } - } elseif ($v_header['typeflag'] == "2") { - if (@file_exists($v_header['filename'])) { - @drupal_unlink($v_header['filename']); - } - if (!@symlink($v_header['link'], $v_header['filename'])) { - $this->_error('Unable to extract symbolic link {' - .$v_header['filename'].'}'); - return false; - } - } else { - if (($v_dest_file = @fopen($v_header['filename'], "wb")) == 0) { - $this->_error('Error while opening {'.$v_header['filename'] - .'} in write binary mode'); - return false; - } else { - $n = floor($v_header['size']/512); - for ($i=0; $i<$n; $i++) { - $v_content = $this->_readBlock(); - fwrite($v_dest_file, $v_content, 512); - } - if (($v_header['size'] % 512) != 0) { - $v_content = $this->_readBlock(); - fwrite($v_dest_file, $v_content, ($v_header['size'] % 512)); - } - - @fclose($v_dest_file); - - // ----- Change the file mode, mtime - @touch($v_header['filename'], $v_header['mtime']); - if ($v_header['mode'] & 0111) { - // make file executable, obey umask - $mode = fileperms($v_header['filename']) | (~umask() & 0111); - @chmod($v_header['filename'], $mode); - } - } - - // ----- Check the file size - clearstatcache(); - if (filesize($v_header['filename']) != $v_header['size']) { - $this->_error('Extracted file '.$v_header['filename'] - .' does not have the correct file size \'' - .filesize($v_header['filename']) - .'\' ('.$v_header['size'] - .' expected). Archive may be corrupted.'); - return false; - } - } - } else { - $this->_jumpBlock(ceil(($v_header['size']/512))); + $v_filename = rtrim(substr($v_filename, 0, $v_filesize), "\0"); + $v_header['filename'] = $v_filename; + if ($this->_maliciousFilename($v_filename)) { + $this->_error( + 'Malicious .tar detected, file "' . $v_filename . + '" will not install in desired directory tree' + ); + return false; } - } else { - $this->_jumpBlock(ceil(($v_header['size']/512))); - } - - /* TBC : Seems to be unused ... - if ($this->_compress) - $v_end_of_file = @gzeof($this->_file); - else - $v_end_of_file = @feof($this->_file); - */ - - if ($v_listing || $v_extract_file || $v_extraction_stopped) { - // ----- Log extracted files - if (($v_file_dir = dirname($v_header['filename'])) - == $v_header['filename']) - $v_file_dir = ''; - if ((substr($v_header['filename'], 0, 1) == '/') && ($v_file_dir == '')) - $v_file_dir = '/'; - - $p_list_detail[$v_nb++] = $v_header; - if (is_array($p_file_list) && (count($p_list_detail) == count($p_file_list))) { - return true; - } - } - } return true; } - // }}} - // {{{ _openAppend() - function _openAppend() + /** + * This method extract from the archive one file identified by $p_filename. + * The return value is a string with the file content, or null on error. + * + * @param string $p_filename The path of the file to extract in a string. + * + * @return a string with the file content or null. + */ + private function _extractInString($p_filename) { - if (filesize($this->_tarname) == 0) - return $this->_openWrite(); + $v_result_str = ""; + + while (strlen($v_binary_data = $this->_readBlock()) != 0) { + if (!$this->_readHeader($v_binary_data, $v_header)) { + return null; + } + + if ($v_header['filename'] == '') { + continue; + } + + // ----- Look for long filename + if ($v_header['typeflag'] == 'L') { + if (!$this->_readLongHeader($v_header)) { + return null; + } + } + + if ($v_header['filename'] == $p_filename) { + if ($v_header['typeflag'] == "5") { + $this->_error( + 'Unable to extract in string a directory ' + . 'entry {' . $v_header['filename'] . '}' + ); + return null; + } else { + $n = floor($v_header['size'] / 512); + for ($i = 0; $i < $n; $i++) { + $v_result_str .= $this->_readBlock(); + } + if (($v_header['size'] % 512) != 0) { + $v_content = $this->_readBlock(); + $v_result_str .= substr( + $v_content, + 0, + ($v_header['size'] % 512) + ); + } + return $v_result_str; + } + } else { + $this->_jumpBlock(ceil(($v_header['size'] / 512))); + } + } + + return null; + } + + /** + * @param string $p_path + * @param string $p_list_detail + * @param string $p_mode + * @param string $p_file_list + * @param string $p_remove_path + * @param bool $p_preserve + * @return bool + */ + public function _extractList( + $p_path, + &$p_list_detail, + $p_mode, + $p_file_list, + $p_remove_path, + $p_preserve = false + ) { + $v_result = true; + $v_nb = 0; + $v_extract_all = true; + $v_listing = false; + + $p_path = $this->_translateWinPath($p_path, false); + if ($p_path == '' || (substr($p_path, 0, 1) != '/' + && substr($p_path, 0, 3) != "../" && !strpos($p_path, ':')) + ) { + $p_path = "./" . $p_path; + } + $p_remove_path = $this->_translateWinPath($p_remove_path); + + // ----- Look for path to remove format (should end by /) + if (($p_remove_path != '') && (substr($p_remove_path, -1) != '/')) { + $p_remove_path .= '/'; + } + $p_remove_path_size = strlen($p_remove_path); + + switch ($p_mode) { + case "complete" : + $v_extract_all = true; + $v_listing = false; + break; + case "partial" : + $v_extract_all = false; + $v_listing = false; + break; + case "list" : + $v_extract_all = false; + $v_listing = true; + break; + default : + $this->_error('Invalid extract mode (' . $p_mode . ')'); + return false; + } + + clearstatcache(); + + while (strlen($v_binary_data = $this->_readBlock()) != 0) { + $v_extract_file = false; + $v_extraction_stopped = 0; + + if (!$this->_readHeader($v_binary_data, $v_header)) { + return false; + } + + if ($v_header['filename'] == '') { + continue; + } + + // ----- Look for long filename + if ($v_header['typeflag'] == 'L') { + if (!$this->_readLongHeader($v_header)) { + return false; + } + } + + // ignore extended / pax headers + if ($v_header['typeflag'] == 'x' || $v_header['typeflag'] == 'g') { + $this->_jumpBlock(ceil(($v_header['size'] / 512))); + continue; + } + + if ((!$v_extract_all) && (is_array($p_file_list))) { + // ----- By default no unzip if the file is not found + $v_extract_file = false; + + for ($i = 0; $i < sizeof($p_file_list); $i++) { + // ----- Look if it is a directory + if (substr($p_file_list[$i], -1) == '/') { + // ----- Look if the directory is in the filename path + if ((strlen($v_header['filename']) > strlen($p_file_list[$i])) + && (substr($v_header['filename'], 0, strlen($p_file_list[$i])) + == $p_file_list[$i]) + ) { + $v_extract_file = true; + break; + } + } // ----- It is a file, so compare the file names + elseif ($p_file_list[$i] == $v_header['filename']) { + $v_extract_file = true; + break; + } + } + } else { + $v_extract_file = true; + } + + // ----- Look if this file need to be extracted + if (($v_extract_file) && (!$v_listing)) { + if (($p_remove_path != '') + && (substr($v_header['filename'] . '/', 0, $p_remove_path_size) + == $p_remove_path) + ) { + $v_header['filename'] = substr( + $v_header['filename'], + $p_remove_path_size + ); + if ($v_header['filename'] == '') { + continue; + } + } + if (($p_path != './') && ($p_path != '/')) { + while (substr($p_path, -1) == '/') { + $p_path = substr($p_path, 0, strlen($p_path) - 1); + } + + if (substr($v_header['filename'], 0, 1) == '/') { + $v_header['filename'] = $p_path . $v_header['filename']; + } else { + $v_header['filename'] = $p_path . '/' . $v_header['filename']; + } + } + if (file_exists($v_header['filename'])) { + if ((@is_dir($v_header['filename'])) + && ($v_header['typeflag'] == '') + ) { + $this->_error( + 'File ' . $v_header['filename'] + . ' already exists as a directory' + ); + return false; + } + if (($this->_isArchive($v_header['filename'])) + && ($v_header['typeflag'] == "5") + ) { + $this->_error( + 'Directory ' . $v_header['filename'] + . ' already exists as a file' + ); + return false; + } + if (!is_writeable($v_header['filename'])) { + $this->_error( + 'File ' . $v_header['filename'] + . ' already exists and is write protected' + ); + return false; + } + if (filemtime($v_header['filename']) > $v_header['mtime']) { + // To be completed : An error or silent no replace ? + } + } // ----- Check the directory availability and create it if necessary + elseif (($v_result + = $this->_dirCheck( + ($v_header['typeflag'] == "5" + ? $v_header['filename'] + : dirname($v_header['filename'])) + )) != 1 + ) { + $this->_error('Unable to create path for ' . $v_header['filename']); + return false; + } + + if ($v_extract_file) { + if ($v_header['typeflag'] == "5") { + if (!@file_exists($v_header['filename'])) { + if (!@mkdir($v_header['filename'], 0777)) { + $this->_error( + 'Unable to create directory {' + . $v_header['filename'] . '}' + ); + return false; + } + } + } elseif ($v_header['typeflag'] == "2") { + if (@file_exists($v_header['filename'])) { + @drupal_unlink($v_header['filename']); + } + if (!@symlink($v_header['link'], $v_header['filename'])) { + $this->_error( + 'Unable to extract symbolic link {' + . $v_header['filename'] . '}' + ); + return false; + } + } else { + if (($v_dest_file = @fopen($v_header['filename'], "wb")) == 0) { + $this->_error( + 'Error while opening {' . $v_header['filename'] + . '} in write binary mode' + ); + return false; + } else { + $n = floor($v_header['size'] / 512); + for ($i = 0; $i < $n; $i++) { + $v_content = $this->_readBlock(); + fwrite($v_dest_file, $v_content, 512); + } + if (($v_header['size'] % 512) != 0) { + $v_content = $this->_readBlock(); + fwrite($v_dest_file, $v_content, ($v_header['size'] % 512)); + } + + @fclose($v_dest_file); + + if ($p_preserve) { + @chown($v_header['filename'], $v_header['uid']); + @chgrp($v_header['filename'], $v_header['gid']); + } + + // ----- Change the file mode, mtime + @touch($v_header['filename'], $v_header['mtime']); + if ($v_header['mode'] & 0111) { + // make file executable, obey umask + $mode = fileperms($v_header['filename']) | (~umask() & 0111); + @chmod($v_header['filename'], $mode); + } + } + + // ----- Check the file size + clearstatcache(); + if (!is_file($v_header['filename'])) { + $this->_error( + 'Extracted file ' . $v_header['filename'] + . 'does not exist. Archive may be corrupted.' + ); + return false; + } + + $filesize = filesize($v_header['filename']); + if ($filesize != $v_header['size']) { + $this->_error( + 'Extracted file ' . $v_header['filename'] + . ' does not have the correct file size \'' + . $filesize + . '\' (' . $v_header['size'] + . ' expected). Archive may be corrupted.' + ); + return false; + } + } + } else { + $this->_jumpBlock(ceil(($v_header['size'] / 512))); + } + } else { + $this->_jumpBlock(ceil(($v_header['size'] / 512))); + } + + /* TBC : Seems to be unused ... + if ($this->_compress) + $v_end_of_file = @gzeof($this->_file); + else + $v_end_of_file = @feof($this->_file); + */ + + if ($v_listing || $v_extract_file || $v_extraction_stopped) { + // ----- Log extracted files + if (($v_file_dir = dirname($v_header['filename'])) + == $v_header['filename'] + ) { + $v_file_dir = ''; + } + if ((substr($v_header['filename'], 0, 1) == '/') && ($v_file_dir == '')) { + $v_file_dir = '/'; + } + + $p_list_detail[$v_nb++] = $v_header; + if (is_array($p_file_list) && (count($p_list_detail) == count($p_file_list))) { + return true; + } + } + } + + return true; + } + + /** + * @return bool + */ + public function _openAppend() + { + if (filesize($this->_tarname) == 0) { + return $this->_openWrite(); + } if ($this->_compress) { $this->_close(); - if (!@rename($this->_tarname, $this->_tarname.".tmp")) { - $this->_error('Error while renaming \''.$this->_tarname - .'\' to temporary file \''.$this->_tarname - .'.tmp\''); - return false; - } - - if ($this->_compress_type == 'gz') - $v_temp_tar = @gzopen($this->_tarname.".tmp", "rb"); - elseif ($this->_compress_type == 'bz2') - $v_temp_tar = @bzopen($this->_tarname.".tmp", "r"); - - if ($v_temp_tar == 0) { - $this->_error('Unable to open file \''.$this->_tarname - .'.tmp\' in binary read mode'); - @rename($this->_tarname.".tmp", $this->_tarname); - return false; - } - - if (!$this->_openWrite()) { - @rename($this->_tarname.".tmp", $this->_tarname); + if (!@rename($this->_tarname, $this->_tarname . ".tmp")) { + $this->_error( + 'Error while renaming \'' . $this->_tarname + . '\' to temporary file \'' . $this->_tarname + . '.tmp\'' + ); return false; } if ($this->_compress_type == 'gz') { + $v_temp_tar = @gzopen($this->_tarname . ".tmp", "rb"); + } elseif ($this->_compress_type == 'bz2') { + $v_temp_tar = @bzopen($this->_tarname . ".tmp", "r"); + } elseif ($this->_compress_type == 'lzma2') { + $v_temp_tar = @xzopen($this->_tarname . ".tmp", "r"); + } + + + if ($v_temp_tar == 0) { + $this->_error( + 'Unable to open file \'' . $this->_tarname + . '.tmp\' in binary read mode' + ); + @rename($this->_tarname . ".tmp", $this->_tarname); + return false; + } + + if (!$this->_openWrite()) { + @rename($this->_tarname . ".tmp", $this->_tarname); + return false; + } + + if ($this->_compress_type == 'gz') { + $end_blocks = 0; + while (!@gzeof($v_temp_tar)) { $v_buffer = @gzread($v_temp_tar, 512); - if ($v_buffer == ARCHIVE_TAR_END_BLOCK) { + if ($v_buffer == ARCHIVE_TAR_END_BLOCK || strlen($v_buffer) == 0) { + $end_blocks++; // do not copy end blocks, we will re-make them // after appending continue; + } elseif ($end_blocks > 0) { + for ($i = 0; $i < $end_blocks; $i++) { + $this->_writeBlock(ARCHIVE_TAR_END_BLOCK); + } + $end_blocks = 0; } $v_binary_data = pack("a512", $v_buffer); $this->_writeBlock($v_binary_data); } @gzclose($v_temp_tar); - } - elseif ($this->_compress_type == 'bz2') { + } elseif ($this->_compress_type == 'bz2') { + $end_blocks = 0; + while (strlen($v_buffer = @bzread($v_temp_tar, 512)) > 0) { - if ($v_buffer == ARCHIVE_TAR_END_BLOCK) { + if ($v_buffer == ARCHIVE_TAR_END_BLOCK || strlen($v_buffer) == 0) { + $end_blocks++; + // do not copy end blocks, we will re-make them + // after appending continue; + } elseif ($end_blocks > 0) { + for ($i = 0; $i < $end_blocks; $i++) { + $this->_writeBlock(ARCHIVE_TAR_END_BLOCK); + } + $end_blocks = 0; } $v_binary_data = pack("a512", $v_buffer); $this->_writeBlock($v_binary_data); } @bzclose($v_temp_tar); + } elseif ($this->_compress_type == 'lzma2') { + $end_blocks = 0; + + while (strlen($v_buffer = @xzread($v_temp_tar, 512)) > 0) { + if ($v_buffer == ARCHIVE_TAR_END_BLOCK || strlen($v_buffer) == 0) { + $end_blocks++; + // do not copy end blocks, we will re-make them + // after appending + continue; + } elseif ($end_blocks > 0) { + for ($i = 0; $i < $end_blocks; $i++) { + $this->_writeBlock(ARCHIVE_TAR_END_BLOCK); + } + $end_blocks = 0; + } + $v_binary_data = pack("a512", $v_buffer); + $this->_writeBlock($v_binary_data); + } + + @xzclose($v_temp_tar); } - if (!@drupal_unlink($this->_tarname.".tmp")) { - $this->_error('Error while deleting temporary file \'' - .$this->_tarname.'.tmp\''); + if (!@drupal_unlink($this->_tarname . ".tmp")) { + $this->_error( + 'Error while deleting temporary file \'' + . $this->_tarname . '.tmp\'' + ); } - } else { // ----- For not compressed tar, just add files before the last - // one or two 512 bytes block - if (!$this->_openReadWrite()) - return false; + // one or two 512 bytes block + if (!$this->_openReadWrite()) { + return false; + } clearstatcache(); $v_size = filesize($this->_tarname); @@ -1760,32 +2324,34 @@ class Archive_Tar // extends PEAR fseek($this->_file, $v_size - 1024); if (fread($this->_file, 512) == ARCHIVE_TAR_END_BLOCK) { fseek($this->_file, $v_size - 1024); - } - elseif (fread($this->_file, 512) == ARCHIVE_TAR_END_BLOCK) { + } elseif (fread($this->_file, 512) == ARCHIVE_TAR_END_BLOCK) { fseek($this->_file, $v_size - 512); } } return true; } - // }}} - // {{{ _append() - function _append($p_filelist, $p_add_dir='', $p_remove_dir='') + /** + * @param $p_filelist + * @param string $p_add_dir + * @param string $p_remove_dir + * @return bool + */ + public function _append($p_filelist, $p_add_dir = '', $p_remove_dir = '') { - if (!$this->_openAppend()) + if (!$this->_openAppend()) { return false; + } - if ($this->_addList($p_filelist, $p_add_dir, $p_remove_dir)) - $this->_writeFooter(); + if ($this->_addList($p_filelist, $p_add_dir, $p_remove_dir)) { + $this->_writeFooter(); + } $this->_close(); return true; } - // }}} - - // {{{ _dirCheck() /** * Check if a directory exists and create it (including parent @@ -1793,24 +2359,25 @@ class Archive_Tar // extends PEAR * * @param string $p_dir directory to check * - * @return bool TRUE if the directory exists or was created + * @return bool true if the directory exists or was created */ - function _dirCheck($p_dir) + public function _dirCheck($p_dir) { clearstatcache(); - if ((@is_dir($p_dir)) || ($p_dir == '')) + if ((@is_dir($p_dir)) || ($p_dir == '')) { return true; + } $p_parent_dir = dirname($p_dir); if (($p_parent_dir != $p_dir) && ($p_parent_dir != '') && - (!$this->_dirCheck($p_parent_dir))) - return false; + (!$this->_dirCheck($p_parent_dir)) + ) { + return false; + } - // Drupal integration. - // Changed the code to use drupal_mkdir() instead of mkdir(). - if (!@drupal_mkdir($p_dir, 0777)) { + if (!@mkdir($p_dir, 0777)) { $this->_error("Unable to create directory '$p_dir'"); return false; } @@ -1818,10 +2385,6 @@ class Archive_Tar // extends PEAR return true; } - // }}} - - // {{{ _pathReduction() - /** * Compress path by changing for example "/dir/foo/../bar" to "/dir/bar", * rand emove double slashes. @@ -1829,11 +2392,8 @@ class Archive_Tar // extends PEAR * @param string $p_dir path to reduce * * @return string reduced path - * - * @access private - * */ - function _pathReduction($p_dir) + private function _pathReduction($p_dir) { $v_result = ''; @@ -1843,50 +2403,57 @@ class Archive_Tar // extends PEAR $v_list = explode('/', $p_dir); // ----- Study directories from last to first - for ($i=sizeof($v_list)-1; $i>=0; $i--) { + for ($i = sizeof($v_list) - 1; $i >= 0; $i--) { // ----- Look for current path if ($v_list[$i] == ".") { // ----- Ignore this directory // Should be the first $i=0, but no check is done - } - else if ($v_list[$i] == "..") { - // ----- Ignore it and ignore the $i-1 - $i--; - } - else if ( ($v_list[$i] == '') - && ($i!=(sizeof($v_list)-1)) - && ($i!=0)) { - // ----- Ignore only the double '//' in path, - // but not the first and last / } else { - $v_result = $v_list[$i].($i!=(sizeof($v_list)-1)?'/' - .$v_result:''); + if ($v_list[$i] == "..") { + // ----- Ignore it and ignore the $i-1 + $i--; + } else { + if (($v_list[$i] == '') + && ($i != (sizeof($v_list) - 1)) + && ($i != 0) + ) { + // ----- Ignore only the double '//' in path, + // but not the first and last / + } else { + $v_result = $v_list[$i] . ($i != (sizeof($v_list) - 1) ? '/' + . $v_result : ''); + } + } } } } - $v_result = strtr($v_result, '\\', '/'); + + if (defined('OS_WINDOWS') && OS_WINDOWS) { + $v_result = strtr($v_result, '\\', '/'); + } + return $v_result; } - // }}} - - // {{{ _translateWinPath() - function _translateWinPath($p_path, $p_remove_disk_letter=true) + /** + * @param $p_path + * @param bool $p_remove_disk_letter + * @return string + */ + public function _translateWinPath($p_path, $p_remove_disk_letter = true) { - if (defined('OS_WINDOWS') && OS_WINDOWS) { - // ----- Look for potential disk letter - if ( ($p_remove_disk_letter) - && (($v_position = strpos($p_path, ':')) != false)) { - $p_path = substr($p_path, $v_position+1); - } - // ----- Change potential windows directory separator - if ((strpos($p_path, '\\') > 0) || (substr($p_path, 0,1) == '\\')) { - $p_path = strtr($p_path, '\\', '/'); - } - } - return $p_path; + if (defined('OS_WINDOWS') && OS_WINDOWS) { + // ----- Look for potential disk letter + if (($p_remove_disk_letter) + && (($v_position = strpos($p_path, ':')) != false) + ) { + $p_path = substr($p_path, $v_position + 1); + } + // ----- Change potential windows directory separator + if ((strpos($p_path, '\\') > 0) || (substr($p_path, 0, 1) == '\\')) { + $p_path = strtr($p_path, '\\', '/'); + } + } + return $p_path; } - // }}} - } -?> diff --git a/modules/system/system.test b/modules/system/system.test index aefbfbc4..ec71093d 100644 --- a/modules/system/system.test +++ b/modules/system/system.test @@ -389,6 +389,18 @@ class ModuleDependencyTestCase extends ModuleTestCase { ); } + /** + * Checks functionality of project namespaces for dependencies. + */ + function testProjectNamespaceForDependencies() { + // Enable module with project namespace to ensure nothing breaks. + $edit = array( + 'modules[Testing][system_project_namespace_test][enable]' => TRUE, + ); + $this->drupalPost('admin/modules', $edit, t('Save configuration')); + $this->assertModules(array('system_project_namespace_test'), TRUE); + } + /** * Attempt to enable translation module without locale enabled. */ @@ -714,7 +726,7 @@ class IPAddressBlockingTestCase extends DrupalWebTestCase { // Block a valid IP address. $edit = array(); - $edit['ip'] = '192.168.1.1'; + $edit['ip'] = '1.2.3.3'; $this->drupalPost('admin/config/people/ip-blocking', $edit, t('Add')); $ip = db_query("SELECT iid from {blocked_ips} WHERE ip = :ip", array(':ip' => $edit['ip']))->fetchField(); $this->assertTrue($ip, t('IP address found in database.')); @@ -722,7 +734,7 @@ class IPAddressBlockingTestCase extends DrupalWebTestCase { // Try to block an IP address that's already blocked. $edit = array(); - $edit['ip'] = '192.168.1.1'; + $edit['ip'] = '1.2.3.3'; $this->drupalPost('admin/config/people/ip-blocking', $edit, t('Add')); $this->assertText(t('This IP address is already blocked.')); @@ -758,6 +770,25 @@ class IPAddressBlockingTestCase extends DrupalWebTestCase { // $this->drupalPost('admin/config/people/ip-blocking', $edit, t('Save')); // $this->assertText(t('You may not block your own IP address.')); } + + /** + * Test duplicate IP addresses are not present in the 'blocked_ips' table. + */ + function testDuplicateIpAddress() { + drupal_static_reset('ip_address'); + $submit_ip = $_SERVER['REMOTE_ADDR'] = '192.168.1.1'; + system_block_ip_action(); + system_block_ip_action(); + $ip_count = db_query("SELECT iid from {blocked_ips} WHERE ip = :ip", array(':ip' => $submit_ip))->rowCount(); + $this->assertEqual('1', $ip_count); + drupal_static_reset('ip_address'); + $submit_ip = $_SERVER['REMOTE_ADDR'] = ' '; + system_block_ip_action(); + system_block_ip_action(); + system_block_ip_action(); + $ip_count = db_query("SELECT iid from {blocked_ips} WHERE ip = :ip", array(':ip' => $submit_ip))->rowCount(); + $this->assertEqual('1', $ip_count); + } } class CronRunTestCase extends DrupalWebTestCase { @@ -893,6 +924,29 @@ class CronRunTestCase extends DrupalWebTestCase { $result = variable_get('common_test_cron'); $this->assertEqual($result, 'success', 'Cron correctly handles exceptions thrown during hook_cron() invocations.'); } + + /** + * Tests that hook_flush_caches() is not invoked on every single cron run. + * + * @see system_cron() + */ + public function testCronCacheExpiration() { + module_enable(array('system_cron_test')); + variable_del('system_cron_test_flush_caches'); + + // Invoke cron the first time: hook_flush_caches() should be called and then + // get cached. + drupal_cron_run(); + $this->assertEqual(variable_get('system_cron_test_flush_caches'), 1, 'hook_flush_caches() was invoked the first time.'); + $cache = cache_get('system_cache_tables'); + $this->assertEqual(empty($cache), FALSE, 'Cache is filled with cache table data.'); + + // Run cron again and ensure that hook_flush_caches() is not called. + variable_del('system_cron_test_flush_caches'); + drupal_cron_run(); + $this->assertNull(variable_get('system_cron_test_flush_caches'), 'hook_flush_caches() was not invoked the second time.'); + } + } /** @@ -911,7 +965,7 @@ class CronQueueTestCase extends DrupalWebTestCase { } function setUp() { - parent::setUp(array('common_test', 'common_test_cron_helper')); + parent::setUp(array('common_test', 'common_test_cron_helper', 'cron_queue_test')); } /** @@ -931,6 +985,23 @@ class CronQueueTestCase extends DrupalWebTestCase { $this->assertEqual($queue->numberOfItems(), 1, 'Failing item still in the queue after throwing an exception.'); } + /** + * Tests worker defined as a class method callable. + */ + function testCallable() { + $queue = DrupalQueue::get('cron_queue_test_callback'); + + // Enqueue an item for processing. + $queue->createItem(array($this->randomName() => $this->randomName())); + + // Run cron; the worker should perform the task and delete the item from the + // queue. + $this->cronRun(); + + // The queue should be empty. + $this->assertEqual($queue->numberOfItems(), 0); + } + } class AdminMetaTagTestCase extends DrupalWebTestCase { @@ -1068,6 +1139,11 @@ class PageNotFoundTestCase extends DrupalWebTestCase { ); $node = $this->drupalCreateNode($edit); + // As node IDs must be integers, make sure requests for non-integer IDs + // return a page not found error. + $this->drupalGet('node/invalid'); + $this->assertResponse(404); + // Use a custom 404 page. $this->drupalPost('admin/config/system/site-information', array('site_404' => 'node/' . $node->nid), t('Save configuration')); @@ -1293,7 +1369,23 @@ class DateTimeFunctionalTest extends DrupalWebTestCase { $this->assertEqual($this->getUrl(), url('admin/config/regional/date-time/formats', array('absolute' => TRUE)), 'Correct page redirection.'); $this->assertText(t('Custom date format updated.'), 'Custom date format successfully updated.'); + // Check that ajax callback is protected by CSRF token. + $this->drupalGet('admin/config/regional/date-time/formats/lookup', array('query' => array('format' => 'Y m d'))); + $this->assertResponse(403, 'Access denied with no token'); + $this->drupalGet('admin/config/regional/date-time/formats/lookup', array('query' => array('token' => 'invalid', 'format' => 'Y m d'))); + $this->assertResponse(403, 'Access denied with invalid token'); + $this->drupalGet('admin/config/regional/date-time/formats'); + $this->clickLink(t('edit')); + $settings = $this->drupalGetSettings(); + $lookup_url = $settings['dateTime']['date-format']['lookup']; + preg_match('/token=([^&]+)/', $lookup_url, $matches); + $this->assertFalse(empty($matches[1]), 'Found token value'); + $this->drupalGet('admin/config/regional/date-time/formats/lookup', array('query' => array('token' => $matches[1], 'format' => 'Y m d'))); + $this->assertResponse(200, 'Access allowed with valid token'); + $this->assertText(format_date(time(), 'custom', 'Y m d')); + // Delete custom date format. + $this->drupalGet('admin/config/regional/date-time/formats'); $this->clickLink(t('delete')); $this->drupalPost($this->getUrl(), array(), t('Remove')); $this->assertEqual($this->getUrl(), url('admin/config/regional/date-time/formats', array('absolute' => TRUE)), 'Correct page redirection.'); @@ -2281,6 +2373,20 @@ class UpdateScriptFunctionalTest extends DrupalWebTestCase { $this->update_user = $this->drupalCreateUser(array('administer software updates')); } + /** + * Tests that there are no pending updates for the first test method. + */ + function testNoPendingUpdates() { + // Ensure that for the first test method in a class, there are no pending + // updates. This tests a drupal_get_schema_versions() bug that previously + // led to the wrong schema version being recorded for the initial install + // of a child site during automated testing. + $this->drupalLogin($this->update_user); + $this->drupalGet($this->update_url, array('external' => TRUE)); + $this->drupalPost(NULL, array(), t('Continue')); + $this->assertText(t('No pending updates.'), 'End of update process was reached.'); + } + /** * Tests access to the update script. */ @@ -2362,6 +2468,12 @@ class UpdateScriptFunctionalTest extends DrupalWebTestCase { $this->assertText('This is a requirements error provided by the update_script_test module.'); $this->clickLink('try again'); $this->assertText('This is a requirements error provided by the update_script_test module.'); + + // Check if the optional 'value' key displays without a notice. + variable_set('update_script_test_requirement_type', REQUIREMENT_INFO); + $this->drupalGet($this->update_url, array('external' => TRUE)); + $this->assertText('This is a requirements info provided by the update_script_test module.'); + $this->assertNoText('Notice: Undefined index: value in theme_status_report()'); } /** @@ -2825,3 +2937,75 @@ class SystemValidTokenTest extends DrupalUnitTestCase { return TRUE; } } + +/** + * Tests drupal_set_message() and related functions. + */ +class DrupalSetMessageTest extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Messages', + 'description' => 'Tests that messages can be displayed using drupal_set_message().', + 'group' => 'System', + ); + } + + function setUp() { + parent::setUp('system_test'); + } + + /** + * Tests setting messages and removing one before it is displayed. + */ + function testSetRemoveMessages() { + // The page at system-test/drupal-set-message sets two messages and then + // removes the first before it is displayed. + $this->drupalGet('system-test/drupal-set-message'); + $this->assertNoText('First message (removed).'); + $this->assertText('Second message (not removed).'); + } +} + +/** + * Tests confirm form destinations. + */ +class ConfirmFormTest extends DrupalWebTestCase { + protected $admin_user; + + public static function getInfo() { + return array( + 'name' => 'Confirm form', + 'description' => 'Tests that the confirm form does not use external destinations.', + 'group' => 'System', + ); + } + + function setUp() { + parent::setUp(); + + $this->admin_user = $this->drupalCreateUser(array('administer users')); + $this->drupalLogin($this->admin_user); + } + + /** + * Tests that the confirm form does not use external destinations. + */ + function testConfirmForm() { + $this->drupalGet('user/1/cancel'); + $this->assertCancelLinkUrl(url('user/1')); + $this->drupalGet('user/1/cancel', array('query' => array('destination' => 'node'))); + $this->assertCancelLinkUrl(url('node')); + $this->drupalGet('user/1/cancel', array('query' => array('destination' => 'http://example.com'))); + $this->assertCancelLinkUrl(url('user/1')); + } + + /** + * Asserts that a cancel link is present pointing to the provided URL. + */ + function assertCancelLinkUrl($url, $message = '', $group = 'Other') { + $links = $this->xpath('//a[normalize-space(text())=:label and @href=:url]', array(':label' => t('Cancel'), ':url' => $url)); + $message = ($message ? $message : format_string('Cancel link with url %url found.', array('%url' => $url))); + return $this->assertTrue(isset($links[0]), $message, $group); + } +} diff --git a/modules/system/system.updater.inc b/modules/system/system.updater.inc index a14d788b..2a32c4b5 100644 --- a/modules/system/system.updater.inc +++ b/modules/system/system.updater.inc @@ -24,7 +24,7 @@ class ModuleUpdater extends Updater implements DrupalUpdaterInterface { * found on your system, and if there was a copy in sites/all, we'd see it. */ public function getInstallDirectory() { - if ($relative_path = drupal_get_path('module', $this->name)) { + if ($this->isInstalled() && ($relative_path = drupal_get_path('module', $this->name))) { $relative_path = dirname($relative_path); } else { @@ -34,7 +34,7 @@ class ModuleUpdater extends Updater implements DrupalUpdaterInterface { } public function isInstalled() { - return (bool) drupal_get_path('module', $this->name); + return (bool) drupal_get_filename('module', $this->name, NULL, FALSE); } public static function canUpdateDirectory($directory) { @@ -109,7 +109,7 @@ class ThemeUpdater extends Updater implements DrupalUpdaterInterface { * found on your system, and if there was a copy in sites/all, we'd see it. */ public function getInstallDirectory() { - if ($relative_path = drupal_get_path('theme', $this->name)) { + if ($this->isInstalled() && ($relative_path = drupal_get_path('theme', $this->name))) { $relative_path = dirname($relative_path); } else { @@ -119,7 +119,7 @@ class ThemeUpdater extends Updater implements DrupalUpdaterInterface { } public function isInstalled() { - return (bool) drupal_get_path('theme', $this->name); + return (bool) drupal_get_filename('theme', $this->name, NULL, FALSE); } static function canUpdateDirectory($directory) { diff --git a/modules/system/tests/cron_queue_test.info b/modules/system/tests/cron_queue_test.info index 3e26e251..13fefda3 100644 --- a/modules/system/tests/cron_queue_test.info +++ b/modules/system/tests/cron_queue_test.info @@ -5,8 +5,8 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/system/tests/cron_queue_test.module b/modules/system/tests/cron_queue_test.module index e95c6b6a..0df6396a 100644 --- a/modules/system/tests/cron_queue_test.module +++ b/modules/system/tests/cron_queue_test.module @@ -7,9 +7,21 @@ function cron_queue_test_cron_queue_info() { $queues['cron_queue_test_exception'] = array( 'worker callback' => 'cron_queue_test_exception', ); + $queues['cron_queue_test_callback'] = array( + 'worker callback' => array('CronQueueTestCallbackClass', 'foo'), + ); + return $queues; } function cron_queue_test_exception($item) { throw new Exception('That is not supposed to happen.'); } + +class CronQueueTestCallbackClass { + + static public function foo() { + // Do nothing. + } + +} diff --git a/modules/system/tests/system_cron_test.info b/modules/system/tests/system_cron_test.info new file mode 100644 index 00000000..ed37d109 --- /dev/null +++ b/modules/system/tests/system_cron_test.info @@ -0,0 +1,12 @@ +name = System Cron Test +description = 'Support module for testing the system_cron().' +package = Testing +version = VERSION +core = 7.x +hidden = TRUE + +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" +project = "drupal" +datestamp = "1481152423" + diff --git a/modules/system/tests/system_cron_test.module b/modules/system/tests/system_cron_test.module new file mode 100644 index 00000000..9ef80e23 --- /dev/null +++ b/modules/system/tests/system_cron_test.module @@ -0,0 +1,15 @@ + $bundle->type, 'settings' => array(), 'description' => 'Debris left over after upgrade from Drupal 6', + 'required' => FALSE, 'widget' => array( 'type' => 'taxonomy_autocomplete', 'module' => 'taxonomy', @@ -557,7 +558,7 @@ function taxonomy_update_7005(&$sandbox) { // of term references stored so far for the current revision, which // provides the delta value for each term reference data insert. The // deltas are reset for each new revision. - + $conditions = array( 'type' => 'taxonomy_term_reference', 'deleted' => 0, diff --git a/modules/taxonomy/taxonomy.module b/modules/taxonomy/taxonomy.module index e147c1ca..981649d2 100644 --- a/modules/taxonomy/taxonomy.module +++ b/modules/taxonomy/taxonomy.module @@ -25,7 +25,7 @@ function taxonomy_help($path, $arg) { $output .= '

          ' . t('Uses') . '

          '; $output .= '
          '; $output .= '
          ' . t('Creating vocabularies') . '
          '; - $output .= '
          ' . t('Users with sufficient permissions can create vocabularies and terms through the Taxonomy page. The page listing the terms provides a drag-and-drop interface for controlling the order of the terms and sub-terms within a vocabulary, in a hierarchical fashion. A controlled vocabulary classifying music by genre with terms and sub-terms could look as follows:', array('@taxo' => url('admin/structure/taxonomy'), '@perm' => url('admin/people/permissions', array('fragment'=>'module-taxonomy')))); + $output .= '
          ' . t('Users with sufficient permissions can create vocabularies and terms through the Taxonomy page. The page listing the terms provides a drag-and-drop interface for controlling the order of the terms and sub-terms within a vocabulary, in a hierarchical fashion. A controlled vocabulary classifying music by genre with terms and sub-terms could look as follows:', array('@taxo' => url('admin/structure/taxonomy'), '@perm' => url('admin/people/permissions', array('fragment' => 'module-taxonomy')))); $output .= '
          • ' . t('vocabulary: Music') . '
          • '; $output .= '
            • ' . t('term: Jazz') . '
            • '; $output .= '
              • ' . t('sub-term: Swing') . '
              • '; @@ -1023,7 +1023,7 @@ function taxonomy_get_parents($tid) { $query->join('taxonomy_term_hierarchy', 'h', 'h.parent = t.tid'); $query->addField('t', 'tid'); $query->condition('h.tid', $tid); - $query->addTag('term_access'); + $query->addTag('taxonomy_term_access'); $query->orderBy('t.weight'); $query->orderBy('t.name'); $tids = $query->execute()->fetchCol(); @@ -1081,7 +1081,7 @@ function taxonomy_get_children($tid, $vid = 0) { if ($vid) { $query->condition('t.vid', $vid); } - $query->addTag('term_access'); + $query->addTag('taxonomy_term_access'); $query->orderBy('t.weight'); $query->orderBy('t.name'); $tids = $query->execute()->fetchCol(); @@ -1129,7 +1129,7 @@ function taxonomy_get_tree($vid, $parent = 0, $max_depth = NULL, $load_entities $query->join('taxonomy_term_hierarchy', 'h', 'h.tid = t.tid'); $result = $query ->addTag('translatable') - ->addTag('term_access') + ->addTag('taxonomy_term_access') ->fields('t') ->fields('h', array('parent')) ->condition('t.vid', $vid) @@ -1249,7 +1249,7 @@ class TaxonomyTermController extends DrupalDefaultEntityController { protected function buildQuery($ids, $conditions = array(), $revision_id = FALSE) { $query = parent::buildQuery($ids, $conditions, $revision_id); $query->addTag('translatable'); - $query->addTag('term_access'); + $query->addTag('taxonomy_term_access'); // When name is passed as a condition use LIKE. if (isset($conditions['name'])) { $query_conditions = &$query->conditions(); diff --git a/modules/taxonomy/taxonomy.pages.inc b/modules/taxonomy/taxonomy.pages.inc index 975ff120..38b24b3b 100644 --- a/modules/taxonomy/taxonomy.pages.inc +++ b/modules/taxonomy/taxonomy.pages.inc @@ -150,7 +150,7 @@ function taxonomy_autocomplete($field_name = '', $tags_typed = '') { $query = db_select('taxonomy_term_data', 't'); $query->addTag('translatable'); - $query->addTag('term_access'); + $query->addTag('taxonomy_term_access'); // Do not select already entered terms. if (!empty($tags_typed)) { diff --git a/modules/taxonomy/taxonomy.test b/modules/taxonomy/taxonomy.test index fdf354b7..a4b7ee83 100644 --- a/modules/taxonomy/taxonomy.test +++ b/modules/taxonomy/taxonomy.test @@ -1025,7 +1025,7 @@ class TaxonomyRSSTestCase extends TaxonomyWebTestCase { function setUp() { parent::setUp('taxonomy'); - $this->admin_user = $this->drupalCreateUser(array('administer taxonomy', 'bypass node access', 'administer content types')); + $this->admin_user = $this->drupalCreateUser(array('administer taxonomy', 'bypass node access', 'administer content types', 'administer fields')); $this->drupalLogin($this->admin_user); $this->vocabulary = $this->createVocabulary(); @@ -1983,3 +1983,113 @@ class TaxonomyEFQTestCase extends TaxonomyWebTestCase { } } + +/** + * Tests that appropriate query tags are added. + */ +class TaxonomyQueryAlterTestCase extends TaxonomyWebTestCase { + public static function getInfo() { + return array( + 'name' => 'Taxonomy query tags', + 'description' => 'Verifies that taxonomy_term_access tags are added to queries.', + 'group' => 'Taxonomy', + ); + } + + public function setUp() { + parent::setUp('taxonomy_test'); + } + + /** + * Tests that appropriate tags are added when querying the database. + */ + public function testTaxonomyQueryAlter() { + // Create a new vocabulary and add a few terms to it. + $vocabulary = $this->createVocabulary(); + $terms = array(); + for ($i = 0; $i < 5; $i++) { + $terms[$i] = $this->createTerm($vocabulary); + } + + // Set up hierarchy. Term 2 is a child of 1. + $terms[2]->parent = array($terms[1]->tid); + taxonomy_term_save($terms[2]); + + $this->setupQueryTagTestHooks(); + $loaded_term = taxonomy_term_load($terms[0]->tid); + $this->assertEqual($loaded_term->tid, $terms[0]->tid, 'First term was loaded'); + $this->assertQueryTagTestResult(1, 'taxonomy_term_load()'); + + $this->setupQueryTagTestHooks(); + $loaded_terms = taxonomy_get_tree($vocabulary->vid); + $this->assertEqual(count($loaded_terms), count($terms), 'All terms were loaded'); + $this->assertQueryTagTestResult(1, 'taxonomy_get_tree()'); + + $this->setupQueryTagTestHooks(); + $loaded_terms = taxonomy_get_parents($terms[2]->tid); + $this->assertEqual(count($loaded_terms), 1, 'All parent terms were loaded'); + $this->assertQueryTagTestResult(2, 'taxonomy_get_parents()'); + + $this->setupQueryTagTestHooks(); + $loaded_terms = taxonomy_get_children($terms[1]->tid); + $this->assertEqual(count($loaded_terms), 1, 'All child terms were loaded'); + $this->assertQueryTagTestResult(2, 'taxonomy_get_children()'); + + $this->setupQueryTagTestHooks(); + $query = db_select('taxonomy_term_data', 't'); + $query->addField('t', 'tid'); + $query->addTag('taxonomy_term_access'); + $tids = $query->execute()->fetchCol(); + $this->assertEqual(count($tids), count($terms), 'All term IDs were retrieved'); + $this->assertQueryTagTestResult(1, 'custom db_select() with taxonomy_term_access tag (preferred)'); + + $this->setupQueryTagTestHooks(); + $query = db_select('taxonomy_term_data', 't'); + $query->addField('t', 'tid'); + $query->addTag('term_access'); + $tids = $query->execute()->fetchCol(); + $this->assertEqual(count($tids), count($terms), 'All term IDs were retrieved'); + $this->assertQueryTagTestResult(1, 'custom db_select() with term_access tag (deprecated)'); + + $this->setupQueryTagTestHooks(); + $query = new EntityFieldQuery(); + $query->entityCondition('entity_type', 'taxonomy_term'); + $query->addTag('taxonomy_term_access'); + $result = $query->execute(); + $this->assertEqual(count($result['taxonomy_term']), count($terms), 'All term IDs were retrieved'); + $this->assertQueryTagTestResult(1, 'custom EntityFieldQuery with taxonomy_term_access tag (preferred)'); + + $this->setupQueryTagTestHooks(); + $query = new EntityFieldQuery(); + $query->entityCondition('entity_type', 'taxonomy_term'); + $query->addTag('term_access'); + $result = $query->execute(); + $this->assertEqual(count($result['taxonomy_term']), count($terms), 'All term IDs were retrieved'); + $this->assertQueryTagTestResult(1, 'custom EntityFieldQuery with term_access tag (deprecated)'); + } + + /** + * Sets up the hooks in the test module. + */ + protected function setupQueryTagTestHooks() { + taxonomy_terms_static_reset(); + variable_set('taxonomy_test_query_alter', 0); + variable_set('taxonomy_test_query_term_access_alter', 0); + variable_set('taxonomy_test_query_taxonomy_term_access_alter', 0); + } + + /** + * Verifies invocation of the hooks in the test module. + * + * @param int $expected_invocations + * The number of times the hooks are expected to have been invoked. + * @param string $method + * A string describing the invoked function which generated the query. + */ + protected function assertQueryTagTestResult($expected_invocations, $method) { + $this->assertIdentical($expected_invocations, variable_get('taxonomy_test_query_alter'), 'hook_query_alter() invoked when executing ' . $method); + $this->assertIdentical($expected_invocations, variable_get('taxonomy_test_query_term_access_alter'), 'Deprecated hook_query_term_access_alter() invoked when executing ' . $method); + $this->assertIdentical($expected_invocations, variable_get('taxonomy_test_query_taxonomy_term_access_alter'), 'Preferred hook_query_taxonomy_term_access_alter() invoked when executing ' . $method); + } + +} diff --git a/modules/toolbar/toolbar.info b/modules/toolbar/toolbar.info index a35b6ba7..3b0675fa 100644 --- a/modules/toolbar/toolbar.info +++ b/modules/toolbar/toolbar.info @@ -4,8 +4,8 @@ core = 7.x package = Core version = VERSION -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/tracker/tracker.info b/modules/tracker/tracker.info index d9994778..2b643cda 100644 --- a/modules/tracker/tracker.info +++ b/modules/tracker/tracker.info @@ -6,8 +6,8 @@ version = VERSION core = 7.x files[] = tracker.test -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/tracker/tracker.test b/modules/tracker/tracker.test index 8a48ea81..e4729788 100644 --- a/modules/tracker/tracker.test +++ b/modules/tracker/tracker.test @@ -151,7 +151,6 @@ class TrackerTest extends DrupalWebTestCase { $node = $this->drupalCreateNode(array( 'comment' => 2, - 'title' => array(LANGUAGE_NONE => array(array('value' => $this->randomName(8)))), )); // Add a comment to the page. diff --git a/modules/translation/tests/translation_test.info b/modules/translation/tests/translation_test.info index aecb9841..1976f1c1 100644 --- a/modules/translation/tests/translation_test.info +++ b/modules/translation/tests/translation_test.info @@ -5,8 +5,8 @@ package = Testing version = VERSION hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/translation/translation.info b/modules/translation/translation.info index 1ff8a134..6b3a2f17 100644 --- a/modules/translation/translation.info +++ b/modules/translation/translation.info @@ -6,8 +6,8 @@ version = VERSION core = 7.x files[] = translation.test -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/translation/translation.module b/modules/translation/translation.module index 53c4641e..580d0007 100644 --- a/modules/translation/translation.module +++ b/modules/translation/translation.module @@ -428,7 +428,7 @@ function translation_node_delete($node) { * A node object. */ function translation_remove_from_set($node) { - if (isset($node->tnid)) { + if (isset($node->tnid) && $node->tnid) { $query = db_update('node') ->fields(array( 'tnid' => 0, diff --git a/modules/trigger/tests/trigger_test.info b/modules/trigger/tests/trigger_test.info index f725f905..8ee06149 100644 --- a/modules/trigger/tests/trigger_test.info +++ b/modules/trigger/tests/trigger_test.info @@ -4,8 +4,8 @@ package = Testing core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/trigger/trigger.info b/modules/trigger/trigger.info index 30fc8bba..517536de 100644 --- a/modules/trigger/trigger.info +++ b/modules/trigger/trigger.info @@ -6,8 +6,8 @@ core = 7.x files[] = trigger.test configure = admin/structure/trigger -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/trigger/trigger.test b/modules/trigger/trigger.test index 9e5f1142..09169b72 100644 --- a/modules/trigger/trigger.test +++ b/modules/trigger/trigger.test @@ -85,7 +85,7 @@ class TriggerContentTestCase extends TriggerWebTestCase { $this->assertRaw(t('!post %title has been created.', array('!post' => 'Basic page', '%title' => $edit["title"])), 'Make sure the Basic page has actually been created'); // Action should have been fired. $loaded_node = $this->drupalGetNodeByTitle($edit["title"]); - $this->assertTrue($loaded_node->$info['property'] == $info['expected'], format_string('Make sure the @action action fired.', array('@action' => $info['name']))); + $this->assertTrue($loaded_node->{$info['property']} == $info['expected'], format_string('Make sure the @action action fired.', array('@action' => $info['name']))); // Leave action assigned for next test // There should be an error when the action is assigned to the trigger diff --git a/modules/update/tests/aaa_update_test.info b/modules/update/tests/aaa_update_test.info index 935852d5..9c69bde6 100644 --- a/modules/update/tests/aaa_update_test.info +++ b/modules/update/tests/aaa_update_test.info @@ -4,8 +4,8 @@ package = Testing core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/update/tests/bbb_update_test.info b/modules/update/tests/bbb_update_test.info index 83830dc1..7327e7f0 100644 --- a/modules/update/tests/bbb_update_test.info +++ b/modules/update/tests/bbb_update_test.info @@ -4,8 +4,8 @@ package = Testing core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/update/tests/ccc_update_test.info b/modules/update/tests/ccc_update_test.info index 54499be4..3a876fc3 100644 --- a/modules/update/tests/ccc_update_test.info +++ b/modules/update/tests/ccc_update_test.info @@ -4,8 +4,8 @@ package = Testing core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/update/tests/themes/update_test_admintheme/update_test_admintheme.info b/modules/update/tests/themes/update_test_admintheme/update_test_admintheme.info new file mode 100644 index 00000000..00c1dd6b --- /dev/null +++ b/modules/update/tests/themes/update_test_admintheme/update_test_admintheme.info @@ -0,0 +1,10 @@ +name = Update test admin theme +description = Test theme which is used as admin theme. +core = 7.x +hidden = TRUE + +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" +project = "drupal" +datestamp = "1481152423" + diff --git a/modules/update/tests/themes/update_test_basetheme/update_test_basetheme.info b/modules/update/tests/themes/update_test_basetheme/update_test_basetheme.info index 904c0b1e..6a858fd9 100644 --- a/modules/update/tests/themes/update_test_basetheme/update_test_basetheme.info +++ b/modules/update/tests/themes/update_test_basetheme/update_test_basetheme.info @@ -3,8 +3,8 @@ description = Test theme which acts as a base theme for other test subthemes. core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/update/tests/themes/update_test_subtheme/update_test_subtheme.info b/modules/update/tests/themes/update_test_subtheme/update_test_subtheme.info index a08108a5..b579bb51 100644 --- a/modules/update/tests/themes/update_test_subtheme/update_test_subtheme.info +++ b/modules/update/tests/themes/update_test_subtheme/update_test_subtheme.info @@ -4,8 +4,8 @@ core = 7.x base theme = update_test_basetheme hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/update/tests/update_test.info b/modules/update/tests/update_test.info index 0aa8d80d..657b2c1c 100644 --- a/modules/update/tests/update_test.info +++ b/modules/update/tests/update_test.info @@ -5,8 +5,8 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/update/tests/update_test.module b/modules/update/tests/update_test.module index 6fe4bdde..594f80f0 100644 --- a/modules/update/tests/update_test.module +++ b/modules/update/tests/update_test.module @@ -11,6 +11,7 @@ function update_test_system_theme_info() { $themes['update_test_basetheme'] = drupal_get_path('module', 'update_test') . '/themes/update_test_basetheme/update_test_basetheme.info'; $themes['update_test_subtheme'] = drupal_get_path('module', 'update_test') . '/themes/update_test_subtheme/update_test_subtheme.info'; + $themes['update_test_admintheme'] = drupal_get_path('module', 'update_test') . '/themes/update_test_admintheme/update_test_admintheme.info'; return $themes; } diff --git a/modules/update/update.authorize.inc b/modules/update/update.authorize.inc index 6ddd2c53..03d37049 100644 --- a/modules/update/update.authorize.inc +++ b/modules/update/update.authorize.inc @@ -97,7 +97,9 @@ function update_authorize_run_install($filetransfer, $project, $updater_name, $l } /** - * Batch callback: Copies project to its proper place when authorized to do so. + * Implements callback_batch_operation(). + * + * Copies project to its proper place when authorized to do so. * * @param string $project * The canonical short name of the project being installed. @@ -168,7 +170,9 @@ function update_authorize_batch_copy_project($project, $updater_name, $local_url } /** - * Batch callback: Performs actions when the authorized update batch is done. + * Implements callback_batch_finished(). + * + * Performs actions when the authorized update batch is done. * * This processes the results and stashes them into SESSION such that * authorize.php will render a report. Also responsible for putting the site @@ -235,7 +239,9 @@ function update_authorize_update_batch_finished($success, $results) { } /** - * Batch callback: Performs actions when the authorized install batch is done. + * Implements callback_batch_finished(). + * + * Performs actions when the authorized install batch is done. * * This processes the results and stashes them into SESSION such that * authorize.php will render a report. Also responsible for putting the site diff --git a/modules/update/update.compare.inc b/modules/update/update.compare.inc index 072a0daa..e3e0de3b 100644 --- a/modules/update/update.compare.inc +++ b/modules/update/update.compare.inc @@ -104,7 +104,13 @@ function update_get_projects() { * @see update_get_projects() */ function _update_process_info_list(&$projects, $list, $project_type, $status) { + $admin_theme = variable_get('admin_theme', 'seven'); foreach ($list as $file) { + // The admin theme is a special case. It should always be considered enabled + // for the purposes of update checking. + if ($file->name === $admin_theme) { + $file->status = TRUE; + } // A disabled base theme of an enabled sub-theme still has all of its code // run by the sub-theme, so we include it in our "enabled" projects list. if ($status && !$file->status && !empty($file->sub_themes)) { diff --git a/modules/update/update.fetch.inc b/modules/update/update.fetch.inc index 9dd2f0ba..428cace6 100644 --- a/modules/update/update.fetch.inc +++ b/modules/update/update.fetch.inc @@ -29,7 +29,9 @@ function update_manual_status() { } /** - * Batch callback: Processes a step in batch for fetching available update data. + * Implements callback_batch_operation(). + * + * Processes a step in batch for fetching available update data. * * @param $context * Reference to an array used for Batch API storage. @@ -77,7 +79,9 @@ function update_fetch_data_batch(&$context) { } /** - * Batch callback: Performs actions when all fetch tasks have been completed. + * Implements callback_batch_finished(). + * + * Performs actions when all fetch tasks have been completed. * * @param $success * TRUE if the batch operation was successful; FALSE if there were errors. diff --git a/modules/update/update.info b/modules/update/update.info index dbdd67dc..1efa370b 100644 --- a/modules/update/update.info +++ b/modules/update/update.info @@ -6,8 +6,8 @@ core = 7.x files[] = update.test configure = admin/reports/updates/settings -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/update/update.manager.inc b/modules/update/update.manager.inc index 85b587de..c7c4e4a6 100644 --- a/modules/update/update.manager.inc +++ b/modules/update/update.manager.inc @@ -59,7 +59,7 @@ * @see update_menu() * @ingroup forms */ -function update_manager_update_form($form, $form_state = array(), $context) { +function update_manager_update_form($form, $form_state, $context) { if (!_update_manager_check_backends($form, 'update')) { return $form; } @@ -335,6 +335,8 @@ function update_manager_update_form_submit($form, &$form_state) { } /** + * Implements callback_batch_finished(). + * * Batch callback: Performs actions when the download batch is completed. * * @param $success @@ -847,7 +849,9 @@ function update_manager_file_get($url) { } /** - * Batch callback: Downloads, unpacks, and verifies a project. + * Implements callback_batch_operation(). + * + * Downloads, unpacks, and verifies a project. * * This function assumes that the provided URL points to a file archive of some * sort. The URL can have any scheme that we have a file stream wrapper to diff --git a/modules/update/update.module b/modules/update/update.module index d1f0d856..a59c7d7f 100644 --- a/modules/update/update.module +++ b/modules/update/update.module @@ -278,12 +278,15 @@ function update_theme() { ), 'update_report' => array( 'variables' => array('data' => NULL), + 'file' => 'update.report.inc', ), 'update_version' => array( 'variables' => array('version' => NULL, 'tag' => NULL, 'class' => array()), + 'file' => 'update.report.inc', ), 'update_status_label' => array( 'variables' => array('status' => NULL), + 'file' => 'update.report.inc', ), ); } diff --git a/modules/update/update.settings.inc b/modules/update/update.settings.inc index 5cd24149..75de6cdd 100644 --- a/modules/update/update.settings.inc +++ b/modules/update/update.settings.inc @@ -26,7 +26,7 @@ function update_settings($form) { $form['update_check_disabled'] = array( '#type' => 'checkbox', - '#title' => t('Check for updates of disabled modules and themes'), + '#title' => t('Check for updates of disabled and uninstalled modules and themes'), '#default_value' => variable_get('update_check_disabled', FALSE), ); @@ -98,10 +98,11 @@ function update_settings_validate($form, &$form_state) { * Form submission handler for update_settings(). * * Also invalidates the cache of available updates if the "Check for updates of - * disabled modules and themes" setting is being changed. The available updates - * report needs to refetch available update data after this setting changes or - * it would show misleading things (e.g., listing the disabled projects on the - * site with the "No available releases found" warning). + * disabled and uninstalled modules and themes" setting is being changed. The + * available updates report needs to refetch available update data after this + * setting changes or it would show misleading things (e.g., listing the + * disabled projects on the site with the "No available releases found" + * warning). * * @see update_settings_validate() */ diff --git a/modules/update/update.test b/modules/update/update.test index 9e04cdae..5ce5bb88 100644 --- a/modules/update/update.test +++ b/modules/update/update.test @@ -462,6 +462,55 @@ class UpdateTestContribCase extends UpdateTestHelper { $this->assertRaw(l(t('Update test base theme'), 'http://example.com/project/update_test_basetheme'), 'Link to the Update test base theme project appears.'); } + /** + * Tests that the admin theme is always notified about security updates. + */ + function testUpdateAdminThemeSecurityUpdate() { + // Disable the admin theme. + db_update('system') + ->fields(array('status' => 0)) + ->condition('type', 'theme') + ->condition('name', 'update_test_%', 'LIKE') + ->execute(); + + variable_set('admin_theme', 'update_test_admintheme'); + + // Define the initial state for core and the themes. + $system_info = array( + '#all' => array( + 'version' => '7.0', + ), + 'update_test_admintheme' => array( + 'project' => 'update_test_admintheme', + 'version' => '7.x-1.0', + 'hidden' => FALSE, + ), + 'update_test_basetheme' => array( + 'project' => 'update_test_basetheme', + 'version' => '7.x-1.1', + 'hidden' => FALSE, + ), + 'update_test_subtheme' => array( + 'project' => 'update_test_subtheme', + 'version' => '7.x-1.0', + 'hidden' => FALSE, + ), + ); + variable_set('update_test_system_info', $system_info); + variable_set('update_check_disabled', FALSE); + $xml_mapping = array( + // This is enough because we don't check the update status of the admin + // theme. We want to check that the admin theme is included in the list. + 'drupal' => '0', + ); + $this->refreshUpdateStatus($xml_mapping); + // The admin theme is displayed even if it's disabled. + $this->assertText('update_test_admintheme', "The admin theme is checked for update even if it's disabled"); + // The other disabled themes are not displayed. + $this->assertNoText('update_test_basetheme', 'Disabled theme is not checked for update in the list.'); + $this->assertNoText('update_test_subtheme', 'Disabled theme is not checked for update in the list.'); + } + /** * Tests that disabled themes are only shown when desired. */ @@ -800,4 +849,4 @@ class UpdateCoreUnitTestCase extends DrupalUnitTestCase { $this->assertEqual($url, $expected, "When ? is present, '$url' should be '$expected'."); } -} \ No newline at end of file +} diff --git a/modules/user/tests/user_form_test.info b/modules/user/tests/user_form_test.info index 5a30a0b5..921bd89e 100644 --- a/modules/user/tests/user_form_test.info +++ b/modules/user/tests/user_form_test.info @@ -5,8 +5,8 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/user/tests/user_form_test.module b/modules/user/tests/user_form_test.module index 4e907f36..382bc57b 100644 --- a/modules/user/tests/user_form_test.module +++ b/modules/user/tests/user_form_test.module @@ -62,3 +62,21 @@ function user_form_test_current_password($form, &$form_state, $account) { function user_form_test_current_password_submit($form, &$form_state) { drupal_set_message(t('The password has been validated and the form submitted successfully.')); } + +/** + * Implements hook_form_FORM_ID_alter(). + */ +function user_form_test_form_user_profile_form_alter(&$form, &$form_state) { + if (variable_get('user_form_test_user_profile_form_rebuild', FALSE)) { + $form['#submit'][] = 'user_form_test_user_account_submit'; + } +} + +/** + * Submit function for user_profile_form(). + */ +function user_form_test_user_account_submit($form, &$form_state) { + // Rebuild the form instead of letting the process end. This allows us to + // test for bugs that can be triggered in contributed modules. + $form_state['rebuild'] = TRUE; +} diff --git a/modules/user/user-picture.tpl.php b/modules/user/user-picture.tpl.php index ee821878..11d92cc5 100644 --- a/modules/user/user-picture.tpl.php +++ b/modules/user/user-picture.tpl.php @@ -17,7 +17,7 @@ */ ?> -
                +
                diff --git a/modules/user/user.api.php b/modules/user/user.api.php index e88c7a97..f205a85b 100644 --- a/modules/user/user.api.php +++ b/modules/user/user.api.php @@ -123,8 +123,8 @@ function hook_user_cancel($edit, $account, $method) { * description is NOT used for the radio button, but instead should provide * additional explanation to the user seeking to cancel their account. * - access: (optional) A boolean value indicating whether the user can access - * a method. If #access is defined, the method cannot be configured as default - * method. + * a method. If access is defined, the method cannot be configured as the + * default method. * * @param $methods * An array containing user account cancellation methods, keyed by method id. @@ -183,7 +183,23 @@ function hook_user_operations() { } /** - * Retrieve a list of user setting or profile information categories. + * Define a list of user settings or profile information categories. + * + * There are two steps to using hook_user_categories(): + * - Create the category with hook_user_categories(). + * - Display that category on the form ID of "user_profile_form" with + * hook_form_FORM_ID_alter(). + * + * Step one builds out the category but it won't be visible on your form until + * you explicitly tell it to do so. + * + * The function in step two should contain the following code in order to + * display your new category: + * @code + * if ($form['#user_category'] == 'mycategory') { + * // Return your form here. + * } + * @endcode * * @return * An array of associative arrays. Each inner array has elements: @@ -327,14 +343,6 @@ function hook_user_logout($account) { * The module should format its custom additions for display and add them to the * $account->content array. * - * Note that when this hook is invoked, the changes have not yet been written to - * the database, because a database transaction is still in progress. The - * transaction is not finalized until the save operation is entirely completed - * and user_save() goes out of scope. You should not rely on data in the - * database at this time as it is not updated yet. You should also note that any - * write/update database queries executed from this hook are also not committed - * immediately. Check user_save() and db_transaction() for more info. - * * @param $account * The user object on which the operation is being performed. * @param $view_mode diff --git a/modules/user/user.info b/modules/user/user.info index f3cf393b..2136902b 100644 --- a/modules/user/user.info +++ b/modules/user/user.info @@ -9,8 +9,8 @@ required = TRUE configure = admin/config/people stylesheets[all][] = user.css -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/modules/user/user.install b/modules/user/user.install index 4e1a3c22..7a74766a 100644 --- a/modules/user/user.install +++ b/modules/user/user.install @@ -49,6 +49,9 @@ function user_schema() { 'columns' => array('uid' => 'uid'), ), ), + 'indexes' => array( + 'uid_module' => array('uid', 'module'), + ), ); $schema['role_permission'] = array( @@ -81,7 +84,7 @@ function user_schema() { ), 'foreign keys' => array( 'role' => array( - 'table' => 'roles', + 'table' => 'role', 'columns' => array('rid' => 'rid'), ), ), @@ -278,7 +281,7 @@ function user_schema() { 'columns' => array('uid' => 'uid'), ), 'role' => array( - 'table' => 'roles', + 'table' => 'role', 'columns' => array('rid' => 'rid'), ), ), @@ -356,11 +359,13 @@ function user_update_dependencies() { 'filter' => 7000, ); - // user_update_7012() uses the file API, which relies on the {file_managed} - // table, so it must run after system_update_7034(), which creates that - // table. + // user_update_7012() uses the file API and inserts records into the + // {file_managed} table, so it therefore must run after system_update_7061(), + // which inserts files with specific IDs into the table and therefore relies + // on the table being empty (otherwise it would accidentally overwrite + // existing records). $dependencies['user'][7012] = array( - 'system' => 7034, + 'system' => 7061, ); // user_update_7013() uses the file usage API, which relies on the @@ -908,6 +913,15 @@ function user_update_7018() { } } +/** + * Ensure there is a combined index on {authmap}.uid and {authmap}.module. + */ +function user_update_7019() { + // Check first in case it was already added manually. + if (!db_index_exists('authmap', 'uid_module')) { + db_add_index('authmap', 'uid_module', array('uid', 'module')); + } +} /** * @} End of "addtogroup updates-7.x-extra". */ diff --git a/modules/user/user.js b/modules/user/user.js index d182066a..4cf98161 100644 --- a/modules/user/user.js +++ b/modules/user/user.js @@ -93,6 +93,8 @@ Drupal.behaviors.password = { * Returns the estimated strength and the relevant output message. */ Drupal.evaluatePasswordStrength = function (password, translate) { + password = $.trim(password); + var weaknesses = 0, strength = 100, msg = []; var hasLowercase = /[a-z]+/.test(password); diff --git a/modules/user/user.module b/modules/user/user.module index 60f32a15..b818d79a 100644 --- a/modules/user/user.module +++ b/modules/user/user.module @@ -32,7 +32,7 @@ define('USER_REGISTER_VISITORS', 1); define('USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL', 2); /** - * Implement hook_help(). + * Implements hook_help(). */ function user_help($path, $arg) { global $user; @@ -418,13 +418,11 @@ function user_load_by_name($name) { * * @return * A fully-loaded $user object upon successful save or FALSE if the save failed. - * - * @todo D8: Drop $edit and fix user_save() to be consistent with others. */ function user_save($account, $edit = array(), $category = 'account') { $transaction = db_transaction(); try { - if (!empty($edit['pass'])) { + if (isset($edit['pass']) && strlen(trim($edit['pass'])) > 0) { // Allow alternate password hashing schemes. require_once DRUPAL_ROOT . '/' . variable_get('password_inc', 'includes/password.inc'); $edit['pass'] = user_hash_password(trim($edit['pass'])); @@ -791,7 +789,7 @@ function user_role_permissions($roles = array()) { * (optional) The account to check, if not given use currently logged in user. * * @return - * Boolean TRUE if the current user has the requested permission. + * Boolean TRUE if the user has the requested permission. * * All permission checks in Drupal should go through this function. This * way, we guarantee consistent behavior, and ensure that the superuser @@ -848,6 +846,26 @@ function user_is_blocked($name) { ->execute()->fetchObject(); } +/** + * Checks if a user has a role. + * + * @param int $rid + * A role ID. + * + * @param object|null $account + * (optional) A user account. Defaults to the current user. + * + * @return bool + * TRUE if the user has the role, or FALSE if not. + */ +function user_has_role($rid, $account = NULL) { + if (!$account) { + $account = $GLOBALS['user']; + } + + return isset($account->roles[$rid]); +} + /** * Implements hook_permission(). */ @@ -938,6 +956,8 @@ function user_search_access() { */ function user_search_execute($keys = NULL, $conditions = NULL) { $find = array(); + // Escape for LIKE matching. + $keys = db_like($keys); // Replace wildcards with MySQL/PostgreSQL wildcards. $keys = preg_replace('!\*+!', '%', $keys); $query = db_select('users')->extend('PagerDefault'); @@ -947,13 +967,13 @@ function user_search_execute($keys = NULL, $conditions = NULL) { // and they don't need to be restricted to only active users. $query->fields('users', array('mail')); $query->condition(db_or()-> - condition('name', '%' . db_like($keys) . '%', 'LIKE')-> - condition('mail', '%' . db_like($keys) . '%', 'LIKE')); + condition('name', '%' . $keys . '%', 'LIKE')-> + condition('mail', '%' . $keys . '%', 'LIKE')); } else { // Regular users can only search via usernames, and we do not show them // blocked accounts. - $query->condition('name', '%' . db_like($keys) . '%', 'LIKE') + $query->condition('name', '%' . $keys . '%', 'LIKE') ->condition('status', 1); } $uids = $query @@ -1140,7 +1160,7 @@ function user_account_form(&$form, &$form_state) { $form['account']['roles'] = array( '#type' => 'checkboxes', '#title' => t('Roles'), - '#default_value' => (!$register && isset($account->roles) ? array_keys($account->roles) : array()), + '#default_value' => (!$register && !empty($account->roles) ? array_keys(array_filter($account->roles)) : array()), '#options' => $roles, '#access' => $roles && user_access('administer permissions'), DRUPAL_AUTHENTICATED_RID => $checkbox_authenticated, @@ -1210,7 +1230,7 @@ function user_validate_current_pass(&$form, &$form_state) { // that prevent them from being empty if they are changed. if ((strlen(trim($form_state['values'][$key])) > 0) && ($form_state['values'][$key] != $account->$key)) { require_once DRUPAL_ROOT . '/' . variable_get('password_inc', 'includes/password.inc'); - $current_pass_failed = empty($form_state['values']['current_pass']) || !user_check_password($form_state['values']['current_pass'], $account); + $current_pass_failed = strlen(trim($form_state['values']['current_pass'])) == 0 || !user_check_password($form_state['values']['current_pass'], $account); if ($current_pass_failed) { form_set_error('current_pass', t("Your current password is missing or incorrect; it's required to change the %name.", array('%name' => $name))); form_set_error($key); @@ -1286,10 +1306,12 @@ function user_user_presave(&$edit, $account, $category) { elseif (!empty($edit['picture_delete'])) { $edit['picture'] = NULL; } - // Prepare user roles. - if (isset($edit['roles'])) { - $edit['roles'] = array_filter($edit['roles']); - } + } + + // Filter out roles with empty values to avoid granting extra roles when + // processing custom form submissions. + if (isset($edit['roles'])) { + $edit['roles'] = array_filter($edit['roles']); } // Move account cancellation information into $user->data. @@ -1731,9 +1753,11 @@ function user_menu() { $items['admin/people/create'] = array( 'title' => 'Add user', + 'page callback' => 'user_admin', 'page arguments' => array('create'), 'access arguments' => array('administer users'), 'type' => MENU_LOCAL_ACTION, + 'file' => 'user.admin.inc', ); // Administration pages. @@ -1891,13 +1915,13 @@ function user_menu_link_alter(&$link) { // for authenticated users. Authenticated users should see "My account", but // anonymous users should not see it at all. Therefore, invoke // user_translated_menu_link_alter() to conditionally hide the link. - if ($link['link_path'] == 'user' && $link['module'] == 'system') { + if ($link['link_path'] == 'user' && isset($link['module']) && $link['module'] == 'system') { $link['options']['alter'] = TRUE; } // Force the Logout link to appear on the top-level of 'user-menu' menu by // default (i.e., unless it has been customized). - if ($link['link_path'] == 'user/logout' && $link['module'] == 'system' && empty($link['customized'])) { + if ($link['link_path'] == 'user/logout' && isset($link['module']) && $link['module'] == 'system' && empty($link['customized'])) { $link['plid'] = 0; } } @@ -2141,7 +2165,7 @@ function user_login_name_validate($form, &$form_state) { */ function user_login_authenticate_validate($form, &$form_state) { $password = trim($form_state['values']['pass']); - if (!empty($form_state['values']['name']) && !empty($password)) { + if (!empty($form_state['values']['name']) && strlen(trim($password)) > 0) { // Do not allow any login from the current user's IP if the limit has been // reached. Default is 50 failed attempts allowed in one hour. This is // independent of the per-user limit to catch attempts from one IP to log @@ -2205,7 +2229,11 @@ function user_login_final_validate($form, &$form_state) { } } else { - form_set_error('name', t('Sorry, unrecognized username or password. Have you forgotten your password?', array('@password' => url('user/password', array('query' => array('name' => $form_state['values']['name'])))))); + // Use $form_state['input']['name'] here to guarantee that we send + // exactly what the user typed in. $form_state['values']['name'] may have + // been modified by validation handlers that ran earlier than this one. + $query = isset($form_state['input']['name']) ? array('name' => $form_state['input']['name']) : array(); + form_set_error('name', t('Sorry, unrecognized username or password. Have you forgotten your password?', array('@password' => url('user/password', array('query' => $query))))); watchdog('user', 'Login attempt failed for %user.', array('%user' => $form_state['values']['name'])); } } @@ -2228,7 +2256,7 @@ function user_login_final_validate($form, &$form_state) { */ function user_authenticate($name, $password) { $uid = FALSE; - if (!empty($name) && !empty($password)) { + if (!empty($name) && strlen(trim($password)) > 0) { $account = user_load_by_name($name); if ($account) { // Allow alternate password hashing schemes. @@ -2335,7 +2363,7 @@ function user_external_login_register($name, $module) { */ function user_pass_reset_url($account) { $timestamp = REQUEST_TIME; - return url("user/reset/$account->uid/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login), array('absolute' => TRUE)); + return url("user/reset/$account->uid/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid), array('absolute' => TRUE)); } /** @@ -2357,7 +2385,7 @@ function user_pass_reset_url($account) { */ function user_cancel_url($account) { $timestamp = REQUEST_TIME; - return url("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login), array('absolute' => TRUE)); + return url("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid), array('absolute' => TRUE)); } /** @@ -2377,12 +2405,33 @@ function user_cancel_url($account) { * A UNIX timestamp, typically REQUEST_TIME. * @param int $login * The UNIX timestamp of the user's last login. + * @param int $uid + * The user ID of the user account. * * @return * A string that is safe for use in URLs and SQL statements. */ -function user_pass_rehash($password, $timestamp, $login) { - return drupal_hmac_base64($timestamp . $login, drupal_get_hash_salt() . $password); +function user_pass_rehash($password, $timestamp, $login, $uid) { + // Backwards compatibility: Try to determine a $uid if one was not passed. + // (Since $uid is a required parameter to this function, a PHP warning will + // be generated if it's not provided, which is an indication that the calling + // code should be updated. But the code below will try to generate a correct + // hash in the meantime.) + if (!isset($uid)) { + $uids = db_query_range('SELECT uid FROM {users} WHERE pass = :password AND login = :login AND uid > 0', 0, 2, array(':password' => $password, ':login' => $login))->fetchCol(); + // If exactly one user account matches the provided password and login + // timestamp, proceed with that $uid. + if (count($uids) == 1) { + $uid = reset($uids); + } + // Otherwise there is no safe hash to return, so return a random string + // that will never be treated as a valid token. + else { + return drupal_random_key(); + } + } + + return drupal_hmac_base64($timestamp . $login . $uid, drupal_get_hash_salt() . $password); } /** @@ -2447,7 +2496,9 @@ function user_cancel($edit, $uid, $method) { } /** - * Last batch processing step for cancelling a user account. + * Implements callback_batch_operation(). + * + * Last step for cancelling a user account. * * Since batch and session API require a valid user account, the actual * cancellation of a user account needs to happen last. @@ -2495,6 +2546,8 @@ function _user_cancel($edit, $account, $method) { } /** + * Implements callback_batch_finished(). + * * Finished batch processing callback for cancelling a user account. * * @see user_cancel() @@ -2998,6 +3051,11 @@ function user_role_delete($role) { $role = user_role_load_by_name($role); } + // If this is the administrator role, delete the user_admin_role variable. + if ($role->rid == variable_get('user_admin_role')) { + variable_del('user_admin_role'); + } + db_delete('role') ->condition('rid', $role->rid) ->execute(); @@ -3613,12 +3671,7 @@ function user_form_process_password_confirm($element) { ); $element['#attached']['js'][] = drupal_get_path('module', 'user') . '/user.js'; - // Ensure settings are only added once per page. - static $already_added = FALSE; - if (!$already_added) { - $already_added = TRUE; - $element['#attached']['js'][] = array('data' => $js_settings, 'type' => 'setting'); - } + $element['#attached']['js'][] = array('data' => $js_settings, 'type' => 'setting'); return $element; } diff --git a/modules/user/user.pages.inc b/modules/user/user.pages.inc index 8ec2348d..2a1b291b 100644 --- a/modules/user/user.pages.inc +++ b/modules/user/user.pages.inc @@ -44,6 +44,12 @@ function user_pass() { $form['name']['#value'] = $user->mail; $form['mail'] = array( '#prefix' => '

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

                ', ); @@ -54,6 +60,11 @@ function user_pass() { return $form; } +/** + * Form validation handler for user_pass(). + * + * @see user_pass_submit() + */ function user_pass_validate($form, &$form_state) { $name = trim($form_state['values']['name']); // Try to load by email. @@ -72,6 +83,11 @@ function user_pass_validate($form, &$form_state) { } } +/** + * Form submission handler for user_pass(). + * + * @see user_pass_validate() + */ function user_pass_submit($form, &$form_state) { global $language; @@ -96,22 +112,33 @@ function user_pass_reset($form, &$form_state, $uid, $timestamp, $hashed_pass, $a // When processing the one-time login link, we have to make sure that a user // isn't already logged in. if ($user->uid) { - // The existing user is already logged in. + // The existing user is already logged in. Log them out and reload the + // current page so the password reset process can continue. if ($user->uid == $uid) { - drupal_set_message(t('You are logged in as %user. Change your password.', array('%user' => $user->name, '!user_edit' => url("user/$user->uid/edit")))); + // Preserve the current destination (if any) and ensure the redirect goes + // back to the current page; any custom destination set in + // hook_user_logout() and intended for regular logouts would not be + // appropriate here. + $destination = array(); + if (isset($_GET['destination'])) { + $destination = drupal_get_destination(); + } + user_logout_current_user(); + unset($_GET['destination']); + drupal_goto(current_path(), array('query' => drupal_get_query_parameters() + $destination)); } // A different user is already logged in on the computer. else { $reset_link_account = user_load($uid); if (!empty($reset_link_account)) { drupal_set_message(t('Another user (%other_user) is already logged into the site on this computer, but you tried to use a one-time link for user %resetting_user. Please logout and try using the link again.', - array('%other_user' => $user->name, '%resetting_user' => $reset_link_account->name, '!logout' => url('user/logout')))); + array('%other_user' => $user->name, '%resetting_user' => $reset_link_account->name, '!logout' => url('user/logout'))), 'warning'); } else { // Invalid one-time link specifies an unknown user. - drupal_set_message(t('The one-time login link you clicked is invalid.')); + drupal_set_message(t('The one-time login link you clicked is invalid.'), 'error'); } + drupal_goto(); } - drupal_goto(); } else { // Time out, in seconds, until login URL expires. Defaults to 24 hours = @@ -123,10 +150,10 @@ function user_pass_reset($form, &$form_state, $uid, $timestamp, $hashed_pass, $a if ($timestamp <= $current && $account = reset($users)) { // No time out for first time login. if ($account->login && $current - $timestamp > $timeout) { - drupal_set_message(t('You have tried to use a one-time login link that has expired. Please request a new one using the form below.')); + drupal_set_message(t('You have tried to use a one-time login link that has expired. Please request a new one using the form below.'), 'error'); drupal_goto('user/password'); } - elseif ($account->uid && $timestamp >= $account->login && $timestamp <= $current && $hashed_pass == user_pass_rehash($account->pass, $timestamp, $account->login)) { + elseif ($account->uid && $timestamp >= $account->login && $timestamp <= $current && $hashed_pass == user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid)) { // First stage is a confirmation form, then login if ($action == 'login') { // Set the new user. @@ -151,7 +178,7 @@ function user_pass_reset($form, &$form_state, $uid, $timestamp, $hashed_pass, $a } } else { - drupal_set_message(t('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.')); + drupal_set_message(t('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.'), 'error'); drupal_goto('user/password'); } } @@ -168,6 +195,14 @@ function user_pass_reset($form, &$form_state, $uid, $timestamp, $hashed_pass, $a * Menu callback; logs the current user out, and redirects to the home page. */ function user_logout() { + user_logout_current_user(); + drupal_goto(); +} + +/** + * Logs the current user out. + */ +function user_logout_current_user() { global $user; watchdog('user', 'Session closed for %name.', array('%name' => $user->name)); @@ -176,8 +211,6 @@ function user_logout() { // Destroy the current session, and reset $user to the anonymous user. session_destroy(); - - drupal_goto(); } /** @@ -294,14 +327,18 @@ function user_profile_form($form, &$form_state, $account, $category = 'account') } /** - * Validation function for the user account and profile editing form. + * Form validation handler for user_profile_form(). + * + * @see user_profile_form_submit() */ function user_profile_form_validate($form, &$form_state) { entity_form_field_validate('user', $form, $form_state); } /** - * Submit function for the user account and profile editing form. + * Form submission handler for user_profile_form(). + * + * @see user_profile_form_validate() */ function user_profile_form_submit($form, &$form_state) { $account = $form_state['user']; @@ -359,7 +396,6 @@ function user_cancel_confirm_form($form, &$form_state, $account) { $form['_account'] = array('#type' => 'value', '#value' => $account); // Display account cancellation method selection, if allowed. - $default_method = variable_get('user_cancel_method', 'user_cancel_block'); $admin_access = user_access('administer users'); $can_select_method = $admin_access || user_access('select account cancellation method'); $form['user_cancel_method'] = array( @@ -523,7 +559,7 @@ function user_cancel_confirm($account, $timestamp = 0, $hashed_pass = '') { // Basic validation of arguments. if (isset($account->data['user_cancel_method']) && !empty($timestamp) && !empty($hashed_pass)) { // Validate expiration and hashed password/login. - if ($timestamp <= $current && $current - $timestamp < $timeout && $account->uid && $timestamp >= $account->login && $hashed_pass == user_pass_rehash($account->pass, $timestamp, $account->login)) { + if ($timestamp <= $current && $current - $timestamp < $timeout && $account->uid && $timestamp >= $account->login && $hashed_pass == user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid)) { $edit = array( 'user_cancel_notify' => isset($account->data['user_cancel_notify']) ? $account->data['user_cancel_notify'] : variable_get('user_mail_status_canceled_notify', FALSE), ); @@ -534,7 +570,7 @@ function user_cancel_confirm($account, $timestamp = 0, $hashed_pass = '') { batch_process(''); } else { - drupal_set_message(t('You have tried to use an account cancellation link that has expired. Please request a new one using the form below.')); + drupal_set_message(t('You have tried to use an account cancellation link that has expired. Please request a new one using the form below.'), 'error'); drupal_goto("user/$account->uid/cancel"); } } diff --git a/modules/user/user.test b/modules/user/user.test index 03f0bbcd..63143c3c 100644 --- a/modules/user/user.test +++ b/modules/user/user.test @@ -480,6 +480,34 @@ class UserPasswordResetTestCase extends DrupalWebTestCase { $this->assertText(t('Further instructions have been sent to your e-mail address.'), 'Password reset instructions mailed message displayed.'); } + /** + * Test user password reset while logged in. + */ + function testUserPasswordResetLoggedIn() { + $account = $this->drupalCreateUser(); + $this->drupalLogin($account); + // Make sure the test account has a valid password. + user_save($account, array('pass' => user_password())); + + // Generate one time login link. + $reset_url = user_pass_reset_url($account); + $this->drupalGet($reset_url); + + $this->assertText('Reset password'); + $this->drupalPost(NULL, NULL, t('Log in')); + + $this->assertText('You have just used your one-time login link. It is no longer necessary to use this link to log in. Please change your password.'); + + $pass = user_password(); + $edit = array( + 'pass[pass1]' => $pass, + 'pass[pass2]' => $pass, + ); + $this->drupalPost(NULL, $edit, t('Save')); + + $this->assertText('The changes have been saved.'); + } + /** * Attempts login using an expired password reset link. */ @@ -498,7 +526,7 @@ class UserPasswordResetTestCase extends DrupalWebTestCase { // To attempt an expired password reset, create a password reset link as if // its request time was 60 seconds older than the allowed limit of timeout. $bogus_timestamp = REQUEST_TIME - variable_get('user_password_reset_timeout', 86400) - 60; - $this->drupalGet("user/reset/$account->uid/$bogus_timestamp/" . user_pass_rehash($account->pass, $bogus_timestamp, $account->login)); + $this->drupalGet("user/reset/$account->uid/$bogus_timestamp/" . user_pass_rehash($account->pass, $bogus_timestamp, $account->login, $account->uid)); $this->assertText(t('You have tried to use a one-time login link that has expired. Please request a new one using the form below.'), 'Expired password reset request rejected.'); } @@ -519,6 +547,74 @@ class UserPasswordResetTestCase extends DrupalWebTestCase { $this->assertFieldByName('name', $edit['name'], 'User name found.'); } + /** + * Make sure that users cannot forge password reset URLs of other users. + */ + function testResetImpersonation() { + // Make sure user 1 has a valid password, so it does not interfere with the + // test user accounts that are created below. + $account = user_load(1); + user_save($account, array('pass' => user_password())); + + // Create two identical user accounts except for the user name. They must + // have the same empty password, so we can't use $this->drupalCreateUser(). + $edit = array(); + $edit['name'] = $this->randomName(); + $edit['mail'] = $edit['name'] . '@example.com'; + $edit['status'] = 1; + + $user1 = user_save(drupal_anonymous_user(), $edit); + + $edit['name'] = $this->randomName(); + $user2 = user_save(drupal_anonymous_user(), $edit); + + // The password reset URL must not be valid for the second user when only + // the user ID is changed in the URL. + $reset_url = user_pass_reset_url($user1); + $attack_reset_url = str_replace("user/reset/$user1->uid", "user/reset/$user2->uid", $reset_url); + $this->drupalGet($attack_reset_url); + $this->assertNoText($user2->name, 'The invalid password reset page does not show the user name.'); + $this->assertUrl('user/password', array(), 'The user is redirected to the password reset request page.'); + $this->assertText('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.'); + + // When legacy code calls user_pass_rehash() without providing the $uid + // parameter, neither password reset URL should be valid since it is + // impossible for the system to determine which user account the token was + // intended for. + $timestamp = REQUEST_TIME; + // Pass an explicit NULL for the $uid parameter of user_pass_rehash() + // rather than not passing it at all, to avoid triggering PHP warnings in + // the test. + $reset_url_token = user_pass_rehash($user1->pass, $timestamp, $user1->login, NULL); + $reset_url = url("user/reset/$user1->uid/$timestamp/$reset_url_token", array('absolute' => TRUE)); + $this->drupalGet($reset_url); + $this->assertNoText($user1->name, 'The invalid password reset page does not show the user name.'); + $this->assertUrl('user/password', array(), 'The user is redirected to the password reset request page.'); + $this->assertText('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.'); + $attack_reset_url = str_replace("user/reset/$user1->uid", "user/reset/$user2->uid", $reset_url); + $this->drupalGet($attack_reset_url); + $this->assertNoText($user2->name, 'The invalid password reset page does not show the user name.'); + $this->assertUrl('user/password', array(), 'The user is redirected to the password reset request page.'); + $this->assertText('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.'); + + // To verify that user_pass_rehash() never returns a valid result in the + // above situation (even if legacy code also called it to attempt to + // validate the token, rather than just to generate the URL), check that a + // second call with the same parameters produces a different result. + $new_reset_url_token = user_pass_rehash($user1->pass, $timestamp, $user1->login, NULL); + $this->assertNotEqual($reset_url_token, $new_reset_url_token); + + // However, when the duplicate account is removed, the password reset URL + // should be valid. + user_delete($user2->uid); + $reset_url_token = user_pass_rehash($user1->pass, $timestamp, $user1->login, NULL); + $reset_url = url("user/reset/$user1->uid/$timestamp/$reset_url_token", array('absolute' => TRUE)); + $this->drupalGet($reset_url); + $this->assertText($user1->name, 'The valid password reset page shows the user name.'); + $this->assertUrl($reset_url, array(), 'The user remains on the password reset login page.'); + $this->assertNoText('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.'); + } + } /** @@ -558,7 +654,7 @@ class UserCancelTestCase extends DrupalWebTestCase { // Attempt bogus account cancellation request confirmation. $timestamp = $account->login; - $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login)); + $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid)); $this->assertResponse(403, 'Bogus cancelling request rejected.'); $account = user_load($account->uid); $this->assertTrue($account->status == 1, 'User account was not canceled.'); @@ -631,14 +727,14 @@ class UserCancelTestCase extends DrupalWebTestCase { // Attempt bogus account cancellation request confirmation. $bogus_timestamp = $timestamp + 60; - $this->drupalGet("user/$account->uid/cancel/confirm/$bogus_timestamp/" . user_pass_rehash($account->pass, $bogus_timestamp, $account->login)); + $this->drupalGet("user/$account->uid/cancel/confirm/$bogus_timestamp/" . user_pass_rehash($account->pass, $bogus_timestamp, $account->login, $account->uid)); $this->assertText(t('You have tried to use an account cancellation link that has expired. Please request a new one using the form below.'), 'Bogus cancelling request rejected.'); $account = user_load($account->uid); $this->assertTrue($account->status == 1, 'User account was not canceled.'); // Attempt expired account cancellation request confirmation. $bogus_timestamp = $timestamp - 86400 - 60; - $this->drupalGet("user/$account->uid/cancel/confirm/$bogus_timestamp/" . user_pass_rehash($account->pass, $bogus_timestamp, $account->login)); + $this->drupalGet("user/$account->uid/cancel/confirm/$bogus_timestamp/" . user_pass_rehash($account->pass, $bogus_timestamp, $account->login, $account->uid)); $this->assertText(t('You have tried to use an account cancellation link that has expired. Please request a new one using the form below.'), 'Expired cancel account request rejected.'); $accounts = user_load_multiple(array($account->uid), array('status' => 1)); $this->assertTrue(reset($accounts), 'User account was not canceled.'); @@ -675,7 +771,7 @@ class UserCancelTestCase extends DrupalWebTestCase { $this->assertText(t('A confirmation request to cancel your account has been sent to your e-mail address.'), 'Account cancellation request mailed message displayed.'); // Confirm account cancellation request. - $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login)); + $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid)); $account = user_load($account->uid, TRUE); $this->assertTrue($account->status == 0, 'User has been blocked.'); @@ -713,7 +809,7 @@ class UserCancelTestCase extends DrupalWebTestCase { $this->assertText(t('A confirmation request to cancel your account has been sent to your e-mail address.'), 'Account cancellation request mailed message displayed.'); // Confirm account cancellation request. - $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login)); + $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid)); $account = user_load($account->uid, TRUE); $this->assertTrue($account->status == 0, 'User has been blocked.'); @@ -763,7 +859,7 @@ class UserCancelTestCase extends DrupalWebTestCase { $this->assertText(t('A confirmation request to cancel your account has been sent to your e-mail address.'), 'Account cancellation request mailed message displayed.'); // Confirm account cancellation request. - $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login)); + $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid)); $this->assertFalse(user_load($account->uid, TRUE), 'User is not found in the database.'); // Confirm that user's content has been attributed to anonymous user. @@ -827,7 +923,7 @@ class UserCancelTestCase extends DrupalWebTestCase { $this->assertText(t('A confirmation request to cancel your account has been sent to your e-mail address.'), 'Account cancellation request mailed message displayed.'); // Confirm account cancellation request. - $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login)); + $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid)); $this->assertFalse(user_load($account->uid, TRUE), 'User is not found in the database.'); // Confirm that user's content has been deleted. @@ -1781,6 +1877,19 @@ class UserCreateTestCase extends DrupalWebTestCase { $this->drupalGet('admin/people'); $this->assertText($edit['name'], 'User found in list of users'); } + + // Test that the password '0' is considered a password. + $name = $this->randomName(); + $edit = array( + 'name' => $name, + 'mail' => $name . '@example.com', + 'pass[pass1]' => 0, + 'pass[pass2]' => 0, + 'notify' => FALSE, + ); + $this->drupalPost('admin/people/create', $edit, t('Create new account')); + $this->assertText(t('Created a new user account for @name. No e-mail has been sent.', array('@name' => $edit['name'])), 'User created with password 0'); + $this->assertNoText('Password field is required'); } } @@ -1858,6 +1967,74 @@ class UserEditTestCase extends DrupalWebTestCase { $this->drupalLogin($user1); $this->drupalLogout(); } + + /** + * Tests setting the password to "0". + */ + public function testUserWith0Password() { + $admin = $this->drupalCreateUser(array('administer users')); + $this->drupalLogin($admin); + // Create a regular user. + $user1 = $this->drupalCreateUser(array()); + + $edit = array('pass[pass1]' => '0', 'pass[pass2]' => '0'); + $this->drupalPost("user/" . $user1->uid . "/edit", $edit, t('Save')); + $this->assertRaw(t("The changes have been saved.")); + + $this->drupalLogout(); + $user1->pass_raw = '0'; + $this->drupalLogin($user1); + $this->drupalLogout(); + } +} + +/** + * Tests editing a user account with and without a form rebuild. + */ +class UserEditRebuildTestCase extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'User edit with form rebuild', + 'description' => 'Test user edit page when a form rebuild is triggered.', + 'group' => 'User', + ); + } + + function setUp() { + parent::setUp('user_form_test'); + } + + /** + * Test user edit page when the form is set to rebuild. + */ + function testUserEditFormRebuild() { + $user1 = $this->drupalCreateUser(array('change own username')); + $this->drupalLogin($user1); + + $roles = array_keys($user1->roles); + // Save the user form twice. + $edit = array(); + $edit['current_pass'] = $user1->pass_raw; + $this->drupalPost("user/$user1->uid/edit", $edit, t('Save')); + $this->assertRaw(t("The changes have been saved.")); + $this->drupalPost(NULL, $edit, t('Save')); + $this->assertRaw(t("The changes have been saved.")); + $saved_user1 = entity_load_unchanged('user', $user1->uid); + $this->assertEqual(count($roles), count($saved_user1->roles), 'Count of user roles in database matches original count.'); + $diff = array_diff(array_keys($saved_user1->roles), $roles); + $this->assertTrue(empty($diff), format_string('User roles in database match original: @roles', array('@roles' => implode(', ', $saved_user1->roles)))); + // Set variable that causes the form to be rebuilt in user_form_test.module. + variable_set('user_form_test_user_profile_form_rebuild', TRUE); + $this->drupalPost("user/$user1->uid/edit", $edit, t('Save')); + $this->assertRaw(t("The changes have been saved.")); + $this->drupalPost(NULL, $edit, t('Save')); + $this->assertRaw(t("The changes have been saved.")); + $saved_user1 = entity_load_unchanged('user', $user1->uid); + $this->assertEqual(count($roles), count($saved_user1->roles), 'Count of user roles in database matches original count.'); + $diff = array_diff(array_keys($saved_user1->roles), $roles); + $this->assertTrue(empty($diff), format_string('User roles in database match original: @roles', array('@roles' => implode(', ', $saved_user1->roles)))); + } } /** @@ -2027,12 +2204,16 @@ class UserRoleAdminTestCase extends DrupalWebTestCase { $this->assertFalse(user_role_load_by_name($old_name), 'The role can no longer be retrieved from the database using its old name.'); $this->assertTrue(is_object(user_role_load_by_name($role_name)), 'The role can be retrieved from the database using its new name.'); - // Test deleting a role. + // Test deleting the default administrator role. + $role_name = 'administrator'; + $role = user_role_load_by_name($role_name); $this->drupalPost("admin/people/permissions/roles/edit/{$role->rid}", NULL, t('Delete role')); $this->drupalPost(NULL, NULL, t('Delete')); $this->assertText(t('The role has been deleted.'), 'The role has been deleted'); $this->assertNoLinkByHref("admin/people/permissions/roles/edit/{$role->rid}", 'Role edit link removed.'); $this->assertFalse(user_role_load_by_name($role_name), 'A deleted role can no longer be loaded.'); + // Make sure this role is no longer configured as the administrator role. + $this->assertNull(variable_get('user_admin_role'), 'The administrator role is no longer configured as the administrator role.'); // Make sure that the system-defined roles cannot be edited via the user // interface. @@ -2158,6 +2339,20 @@ class UserUserSearchTestCase extends DrupalWebTestCase { $this->drupalPost('search/user/', $edit, t('Search')); $this->assertText($keys); + // Verify that wildcard search works. + $keys = $user1->name; + $keys = substr($keys, 0, 2) . '*' . substr($keys, 4, 2); + $edit = array('keys' => $keys); + $this->drupalPost('search/user/', $edit, t('Search')); + $this->assertText($user1->name, 'Search for username wildcard resulted in user name on page for administrative user.'); + + // Verify that wildcard search works for email. + $keys = $user1->mail; + $keys = substr($keys, 0, 2) . '*' . substr($keys, 4, 2); + $edit = array('keys' => $keys); + $this->drupalPost('search/user/', $edit, t('Search')); + $this->assertText($user1->name, 'Search for email wildcard resulted in user name on page for administrative user.'); + // Create a blocked user. $blocked_user = $this->drupalCreateUser(); $edit = array('status' => 0); diff --git a/profiles/README.txt b/profiles/README.txt new file mode 100644 index 00000000..91d012ba --- /dev/null +++ b/profiles/README.txt @@ -0,0 +1,28 @@ +Installation profiles define additional steps that run after the base +installation provided by Drupal core when Drupal is first installed. + +WHAT TO PLACE IN THIS DIRECTORY? +-------------------------------- + +Place downloaded and custom installation profiles in this directory. +Installation profiles are generally provided as part of a Drupal distribution. +They only impact the installation of your site. They do not have any effect on +an already running site. + +DOWNLOAD ADDITIONAL DISTRIBUTIONS +--------------------------------- + +Contributed distributions from the Drupal community may be downloaded at +https://www.drupal.org/project/project_distribution. + +MULTISITE CONFIGURATION +----------------------- + +In multisite configurations, installation profiles found in this directory are +available to all sites during their initial site installation. + +MORE INFORMATION +---------------- + +Refer to the "Installation profiles" section of the README.txt in the Drupal +root directory for further information on extending Drupal with custom profiles. diff --git a/profiles/minimal/minimal.info b/profiles/minimal/minimal.info index 6d6ce1a5..ebc9f129 100644 --- a/profiles/minimal/minimal.info +++ b/profiles/minimal/minimal.info @@ -5,8 +5,8 @@ core = 7.x dependencies[] = block dependencies[] = dblog -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/profiles/standard/standard.info b/profiles/standard/standard.info index 81eba8c4..bc743d50 100644 --- a/profiles/standard/standard.info +++ b/profiles/standard/standard.info @@ -24,8 +24,8 @@ dependencies[] = field_ui dependencies[] = file dependencies[] = rdf -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/profiles/testing/modules/drupal_system_listing_compatible_test/drupal_system_listing_compatible_test.info b/profiles/testing/modules/drupal_system_listing_compatible_test/drupal_system_listing_compatible_test.info index 0b1d3140..f0f43f33 100644 --- a/profiles/testing/modules/drupal_system_listing_compatible_test/drupal_system_listing_compatible_test.info +++ b/profiles/testing/modules/drupal_system_listing_compatible_test/drupal_system_listing_compatible_test.info @@ -6,8 +6,8 @@ core = 7.x hidden = TRUE files[] = drupal_system_listing_compatible_test.test -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/profiles/testing/modules/drupal_system_listing_incompatible_test/drupal_system_listing_incompatible_test.info b/profiles/testing/modules/drupal_system_listing_incompatible_test/drupal_system_listing_incompatible_test.info index 21c13265..17434350 100644 --- a/profiles/testing/modules/drupal_system_listing_incompatible_test/drupal_system_listing_incompatible_test.info +++ b/profiles/testing/modules/drupal_system_listing_incompatible_test/drupal_system_listing_incompatible_test.info @@ -8,8 +8,8 @@ version = VERSION core = 6.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/profiles/testing/testing.info b/profiles/testing/testing.info index df8cdfe1..020330eb 100644 --- a/profiles/testing/testing.info +++ b/profiles/testing/testing.info @@ -4,8 +4,8 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/robots.txt b/robots.txt index 6f20eaf3..a2ee32ec 100644 --- a/robots.txt +++ b/robots.txt @@ -12,12 +12,42 @@ # # For more information about the robots.txt standard, see: # http://www.robotstxt.org/robotstxt.html -# -# For syntax checking, see: -# http://www.frobee.com/robots-txt-check User-agent: * Crawl-delay: 10 +# CSS, JS, Images +Allow: /misc/*.css$ +Allow: /misc/*.css? +Allow: /misc/*.js$ +Allow: /misc/*.js? +Allow: /misc/*.gif +Allow: /misc/*.jpg +Allow: /misc/*.jpeg +Allow: /misc/*.png +Allow: /modules/*.css$ +Allow: /modules/*.css? +Allow: /modules/*.js$ +Allow: /modules/*.js? +Allow: /modules/*.gif +Allow: /modules/*.jpg +Allow: /modules/*.jpeg +Allow: /modules/*.png +Allow: /profiles/*.css$ +Allow: /profiles/*.css? +Allow: /profiles/*.js$ +Allow: /profiles/*.js? +Allow: /profiles/*.gif +Allow: /profiles/*.jpg +Allow: /profiles/*.jpeg +Allow: /profiles/*.png +Allow: /themes/*.css$ +Allow: /themes/*.css? +Allow: /themes/*.js$ +Allow: /themes/*.js? +Allow: /themes/*.gif +Allow: /themes/*.jpg +Allow: /themes/*.jpeg +Allow: /themes/*.png # Directories Disallow: /includes/ Disallow: /misc/ diff --git a/scripts/generate-d6-content.sh b/scripts/generate-d6-content.sh index fc4c68f9..cd33e4da 100644 --- a/scripts/generate-d6-content.sh +++ b/scripts/generate-d6-content.sh @@ -67,6 +67,7 @@ for ($i = 0; $i < 24; $i++) { ++$voc_id; $vocabulary['name'] = "vocabulary $voc_id (i=$i)"; $vocabulary['description'] = "description of ". $vocabulary['name']; + $vocabulary['help'] = "help for ". $vocabulary['name']; $vocabulary['nodes'] = $i > 11 ? array('page' => TRUE) : array(); $vocabulary['multiple'] = $multiple[$i % 12]; $vocabulary['required'] = $required[$i % 12]; diff --git a/scripts/password-hash.sh b/scripts/password-hash.sh index 004421a8..1afe4387 100755 --- a/scripts/password-hash.sh +++ b/scripts/password-hash.sh @@ -1,4 +1,4 @@ -#!/usr/bin/php +#!/usr/bin/env php useDefaults(array('test_id'))->execute(); // Execute tests. -simpletest_script_execute_batch($test_id, simpletest_script_get_test_list()); +$status = simpletest_script_execute_batch($test_id, simpletest_script_get_test_list()); // Retrieve the last database prefix used for testing and the last test class // that was run from. Use the information to read the lgo file in case any @@ -100,7 +104,7 @@ if ($args['xml']) { simpletest_clean_results_table($test_id); // Test complete, exit. -exit; +exit($status); /** * Print help text. @@ -142,6 +146,8 @@ All arguments are long options. --file Run tests identified by specific file names, instead of group names. Specify the path and the extension (i.e. 'modules/user/user.test'). + --directory Run all tests found within the specified file directory. + --xml If provided, test results will be written as xml files to this path. @@ -190,6 +196,7 @@ function simpletest_script_parse_args() { 'all' => FALSE, 'class' => FALSE, 'file' => FALSE, + 'directory' => '', 'color' => FALSE, 'verbose' => FALSE, 'test_names' => array(), @@ -222,7 +229,7 @@ function simpletest_script_parse_args() { else { // Argument not found in list. simpletest_script_print_error("Unknown argument '$arg'."); - exit; + exit(SIMPLETEST_SCRIPT_EXIT_FAILURE); } } else { @@ -235,7 +242,7 @@ function simpletest_script_parse_args() { // Validate the concurrency argument if (!is_numeric($args['concurrency']) || $args['concurrency'] <= 0) { simpletest_script_print_error("--concurrency must be a strictly positive integer."); - exit; + exit(SIMPLETEST_SCRIPT_EXIT_FAILURE); } return array($args, $count); @@ -265,7 +272,7 @@ function simpletest_script_init($server_software) { else { simpletest_script_print_error('Unable to automatically determine the path to the PHP interpreter. Supply the --php command line argument.'); simpletest_script_help(); - exit(); + exit(SIMPLETEST_SCRIPT_EXIT_FAILURE); } // Get URL from arguments. @@ -310,6 +317,8 @@ function simpletest_script_init($server_software) { function simpletest_script_execute_batch($test_id, $test_classes) { global $args; + $total_status = SIMPLETEST_SCRIPT_EXIT_SUCCESS; + // Multi-process execution. $children = array(); while (!empty($test_classes) || !empty($children)) { @@ -325,7 +334,7 @@ function simpletest_script_execute_batch($test_id, $test_classes) { if (!is_resource($process)) { echo "Unable to fork test process. Aborting.\n"; - exit; + exit(SIMPLETEST_SCRIPT_EXIT_FAILURE); } // Register our new child. @@ -345,13 +354,22 @@ function simpletest_script_execute_batch($test_id, $test_classes) { if (empty($status['running'])) { // The child exited, unregister it. proc_close($child['process']); - if ($status['exitcode']) { + if ($status['exitcode'] == SIMPLETEST_SCRIPT_EXIT_FAILURE) { + if ($status['exitcode'] > $total_status) { + $total_status = $status['exitcode']; + } + } + elseif ($status['exitcode']) { + $total_status = $status['exitcode']; echo 'FATAL ' . $test_class . ': test runner returned a non-zero error code (' . $status['exitcode'] . ').' . "\n"; } + + // Remove this child. unset($children[$cid]); } } } + return $total_status; } /** @@ -374,11 +392,14 @@ function simpletest_script_run_one_test($test_id, $test_class) { simpletest_script_print($info['name'] . ' ' . _simpletest_format_summary_line($test->results) . "\n", simpletest_script_color_code($status)); // Finished, kill this runner. - exit(0); + if ($had_fails || $had_exceptions) { + exit(SIMPLETEST_SCRIPT_EXIT_FAILURE); + } + exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS); } catch (Exception $e) { echo (string) $e; - exit(1); + exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION); } } @@ -432,7 +453,7 @@ function simpletest_script_get_test_list() { } simpletest_script_print_error('Test class not found: ' . $test_class); simpletest_script_print_alternatives($test_class, $all_classes, 6); - exit(1); + exit(SIMPLETEST_SCRIPT_EXIT_FAILURE); } } } @@ -451,6 +472,51 @@ function simpletest_script_get_test_list() { } } } + elseif ($args['directory']) { + // Extract test case class names from specified directory. + // Find all tests in the PSR-X structure; Drupal\$extension\Tests\*.php + // Since we do not want to hard-code too many structural file/directory + // assumptions about PSR-0/4 files and directories, we check for the + // minimal conditions only; i.e., a '*.php' file that has '/Tests/' in + // its path. + // Ignore anything from third party vendors, and ignore template files used in tests. + // And any api.php files. + $ignore = array('nomask' => '/vendor|\.tpl\.php|\.api\.php/'); + $files = array(); + if ($args['directory'][0] === '/') { + $directory = $args['directory']; + } + else { + $directory = DRUPAL_ROOT . "/" . $args['directory']; + } + $file_list = file_scan_directory($directory, '/\.php|\.test$/', $ignore); + foreach ($file_list as $file) { + // '/Tests/' can be contained anywhere in the file's path (there can be + // sub-directories below /Tests), but must be contained literally. + // Case-insensitive to match all Simpletest and PHPUnit tests: + // ./lib/Drupal/foo/Tests/Bar/Baz.php + // ./foo/src/Tests/Bar/Baz.php + // ./foo/tests/Drupal/foo/Tests/FooTest.php + // ./foo/tests/src/FooTest.php + // $file->filename doesn't give us a directory, so we use $file->uri + // Strip the drupal root directory and trailing slash off the URI + $filename = substr($file->uri, strlen(DRUPAL_ROOT)+1); + if (stripos($filename, '/Tests/')) { + $files[drupal_realpath($filename)] = 1; + } else if (stripos($filename, '.test')){ + $files[drupal_realpath($filename)] = 1; + } + } + + // Check for valid class names. + foreach ($all_tests as $class_name) { + $refclass = new ReflectionClass($class_name); + $classfile = $refclass->getFileName(); + if (isset($files[$classfile])) { + $test_list[] = $class_name; + } + } + } else { // Check for valid group names and get all valid classes in group. foreach ($args['test_names'] as $group_name) { @@ -460,7 +526,7 @@ function simpletest_script_get_test_list() { else { simpletest_script_print_error('Test group not found: ' . $group_name); simpletest_script_print_alternatives($group_name, array_keys($groups)); - exit(1); + exit(SIMPLETEST_SCRIPT_EXIT_FAILURE); } } } @@ -468,7 +534,7 @@ function simpletest_script_get_test_list() { if (empty($test_list)) { simpletest_script_print_error('No valid tests were specified.'); - exit; + exit(SIMPLETEST_SCRIPT_EXIT_FAILURE); } return $test_list; } diff --git a/sites/all/modules/features/admin/admin.features.user_permission.inc b/sites/all/modules/features/admin/admin.features.user_permission.inc index db893dab..12c9400a 100644 --- a/sites/all/modules/features/admin/admin.features.user_permission.inc +++ b/sites/all/modules/features/admin/admin.features.user_permission.inc @@ -87,24 +87,6 @@ function admin_user_default_permissions() { 'module' => 'node', ); - // Exported permission: 'access core content overview'. - $permissions['access core content overview'] = array( - 'name' => 'access core content overview', - 'roles' => array( - 'root' => 'root', - ), - 'module' => 'contentsadminrelink', - ); - - // Exported permission: 'access core media overview'. - $permissions['access core media overview'] = array( - 'name' => 'access core media overview', - 'roles' => array( - 'root' => 'root', - ), - 'module' => 'contentsadminrelink', - ); - // Exported permission: 'access devel information'. $permissions['access devel information'] = array( 'name' => 'access devel information', diff --git a/sites/all/modules/features/admin/admin.info b/sites/all/modules/features/admin/admin.info index d4d9fe68..a7ab3487 100644 --- a/sites/all/modules/features/admin/admin.info +++ b/sites/all/modules/features/admin/admin.info @@ -1,6 +1,7 @@ name = Admin core = 7.x package = GUI +dependencies[] = admin_menu dependencies[] = admin_menu_source dependencies[] = admin_theme dependencies[] = adminimal_admin_menu @@ -8,7 +9,6 @@ dependencies[] = auto_nodetitle dependencies[] = backup_migrate dependencies[] = block dependencies[] = content_type_extras -dependencies[] = contentsadminrelink dependencies[] = context_ui dependencies[] = ctools dependencies[] = devel @@ -24,18 +24,22 @@ dependencies[] = i18n_string dependencies[] = image dependencies[] = imce dependencies[] = linkit +dependencies[] = locale dependencies[] = login_destination dependencies[] = logintoboggan_rules dependencies[] = logintoboggan_variable +dependencies[] = menu dependencies[] = menu_admin_per_menu dependencies[] = menu_editor dependencies[] = module_filter dependencies[] = node dependencies[] = nodeformcols dependencies[] = page_manager +dependencies[] = path dependencies[] = pathauto dependencies[] = queue_ui dependencies[] = redirect +dependencies[] = rules dependencies[] = shortcut dependencies[] = strongarm dependencies[] = styles_ui @@ -48,6 +52,7 @@ dependencies[] = user dependencies[] = variable_admin dependencies[] = variable_realm dependencies[] = video_filter +dependencies[] = views dependencies[] = workflow dependencies[] = workflow_access dependencies[] = workflow_actions @@ -71,8 +76,6 @@ features[user_permission][] = access backup and migrate features[user_permission][] = access backup files features[user_permission][] = access content features[user_permission][] = access content overview -features[user_permission][] = access core content overview -features[user_permission][] = access core media overview features[user_permission][] = access devel information features[user_permission][] = access private fields features[user_permission][] = access rules debug @@ -317,8 +320,8 @@ features[variable][] = views_ui_show_preview_information features[variable][] = views_ui_show_sql_query features[variable][] = views_ui_show_sql_query_where features[variable][] = workflow_access_priority +features[views_view][] = admin_contents features[workflow][] = Publication features[workflow_access][] = Publication features[wysiwyg][] = filtred_html -features_exclude[dependencies][views] = views project path = sites/all/modules/features diff --git a/sites/all/modules/features/admin/admin.views_default.inc b/sites/all/modules/features/admin/admin.views_default.inc new file mode 100644 index 00000000..bd79e620 --- /dev/null +++ b/sites/all/modules/features/admin/admin.views_default.inc @@ -0,0 +1,471 @@ +name = 'admin_contents'; + $view->description = 'Administrative view for content'; + $view->tag = 'default'; + $view->base_table = 'node'; + $view->human_name = 'Admin Content'; + $view->core = 7; + $view->api_version = '3.0'; + $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Master */ + $handler = $view->new_display('default', 'Master', 'default'); + $handler->display->display_options['title'] = 'Contenu'; + $handler->display->display_options['use_more_always'] = FALSE; + $handler->display->display_options['use_more_text'] = 'plus'; + $handler->display->display_options['access']['type'] = 'role'; + $handler->display->display_options['access']['role'] = array( + 3 => '3', + 4 => '4', + ); + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['query']['type'] = 'views_query'; + $handler->display->display_options['query']['options']['distinct'] = TRUE; + $handler->display->display_options['query']['options']['query_comment'] = FALSE; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['exposed_form']['options']['submit_button'] = 'Filtrer'; + $handler->display->display_options['exposed_form']['options']['reset_button'] = TRUE; + $handler->display->display_options['exposed_form']['options']['reset_button_label'] = 'réinitialiser'; + $handler->display->display_options['exposed_form']['options']['exposed_sorts_label'] = 'Trier par'; + $handler->display->display_options['exposed_form']['options']['autosubmit'] = TRUE; + $handler->display->display_options['pager']['type'] = 'mini'; + $handler->display->display_options['pager']['options']['items_per_page'] = '20'; + $handler->display->display_options['pager']['options']['offset'] = '0'; + $handler->display->display_options['pager']['options']['id'] = '0'; + $handler->display->display_options['pager']['options']['expose']['items_per_page_label'] = 'Éléments par page'; + $handler->display->display_options['pager']['options']['expose']['items_per_page_options_all_label'] = '- Tout -'; + $handler->display->display_options['pager']['options']['expose']['offset_label'] = 'Décalage'; + $handler->display->display_options['pager']['options']['tags']['first'] = '« premier'; + $handler->display->display_options['pager']['options']['tags']['previous'] = '‹ précédent'; + $handler->display->display_options['pager']['options']['tags']['next'] = 'suivant ›'; + $handler->display->display_options['pager']['options']['tags']['last'] = 'dernier »'; + $handler->display->display_options['style_plugin'] = 'table'; + $handler->display->display_options['style_options']['columns'] = array( + 'views_bulk_operations' => 'views_bulk_operations', + 'type' => 'title', + 'title' => 'title', + 'edit_node' => 'title', + 'status' => 'status', + 'language' => 'language', + 'translate_node' => 'language', + 'changed' => 'changed', + 'delete_node' => 'delete_node', + ); + $handler->display->display_options['style_options']['default'] = 'changed'; + $handler->display->display_options['style_options']['info'] = array( + 'views_bulk_operations' => array( + 'align' => '', + 'separator' => '', + 'empty_column' => 0, + ), + 'type' => array( + 'sortable' => 0, + 'default_sort_order' => 'asc', + 'align' => '', + 'separator' => '
                ', + 'empty_column' => 0, + ), + 'title' => array( + 'sortable' => 0, + 'default_sort_order' => 'asc', + 'align' => '', + 'separator' => '
                ', + 'empty_column' => 0, + ), + 'edit_node' => array( + 'align' => '', + 'separator' => '', + 'empty_column' => 0, + ), + 'status' => array( + 'sortable' => 0, + 'default_sort_order' => 'asc', + 'align' => '', + 'separator' => '
                ', + 'empty_column' => 0, + ), + 'language' => array( + 'sortable' => 0, + 'default_sort_order' => 'asc', + 'align' => '', + 'separator' => '
                ', + 'empty_column' => 0, + ), + 'translate_node' => array( + 'align' => '', + 'separator' => '', + 'empty_column' => 0, + ), + 'changed' => array( + 'sortable' => 0, + 'default_sort_order' => 'desc', + 'align' => '', + 'separator' => '', + 'empty_column' => 0, + ), + 'delete_node' => array( + 'align' => '', + 'separator' => '', + 'empty_column' => 0, + ), + ); + $handler->display->display_options['style_options']['override'] = FALSE; + /* En-tête: Global : Result summary */ + $handler->display->display_options['header']['result']['id'] = 'result'; + $handler->display->display_options['header']['result']['table'] = 'views'; + $handler->display->display_options['header']['result']['field'] = 'result'; + /* Champ: Opérations en masse : Contenu */ + $handler->display->display_options['fields']['views_bulk_operations']['id'] = 'views_bulk_operations'; + $handler->display->display_options['fields']['views_bulk_operations']['table'] = 'node'; + $handler->display->display_options['fields']['views_bulk_operations']['field'] = 'views_bulk_operations'; + $handler->display->display_options['fields']['views_bulk_operations']['label'] = ''; + $handler->display->display_options['fields']['views_bulk_operations']['element_label_colon'] = FALSE; + $handler->display->display_options['fields']['views_bulk_operations']['vbo_settings']['display_type'] = '0'; + $handler->display->display_options['fields']['views_bulk_operations']['vbo_settings']['enable_select_all_pages'] = 1; + $handler->display->display_options['fields']['views_bulk_operations']['vbo_settings']['force_single'] = 0; + $handler->display->display_options['fields']['views_bulk_operations']['vbo_settings']['entity_load_capacity'] = '10'; + $handler->display->display_options['fields']['views_bulk_operations']['vbo_operations'] = array( + 'action::node_assign_owner_action' => array( + 'selected' => 0, + 'skip_confirmation' => 0, + 'override_label' => 0, + 'label' => '', + 'postpone_processing' => 0, + ), + 'action::node_unpublish_action' => array( + 'selected' => 1, + 'skip_confirmation' => 0, + 'override_label' => 0, + 'label' => '', + 'postpone_processing' => 0, + ), + 'action::node_unpublish_by_keyword_action' => array( + 'selected' => 0, + 'skip_confirmation' => 0, + 'override_label' => 0, + 'label' => '', + 'postpone_processing' => 0, + ), + 'action::node_save_action' => array( + 'selected' => 0, + 'skip_confirmation' => 0, + 'override_label' => 0, + 'label' => '', + 'postpone_processing' => 0, + ), + 'action::system_send_email_action' => array( + 'selected' => 0, + 'skip_confirmation' => 0, + 'override_label' => 0, + 'label' => '', + 'postpone_processing' => 0, + ), + 'action::views_bulk_operations_script_action' => array( + 'selected' => 0, + 'skip_confirmation' => 0, + 'override_label' => 0, + 'label' => '', + 'postpone_processing' => 0, + ), + 'action::views_bulk_operations_argument_selector_action' => array( + 'selected' => 0, + 'skip_confirmation' => 0, + 'override_label' => 0, + 'label' => '', + 'settings' => array( + 'url' => '', + ), + ), + 'action::node_promote_action' => array( + 'selected' => 0, + 'skip_confirmation' => 0, + 'override_label' => 0, + 'label' => '', + 'postpone_processing' => 0, + ), + 'action::node_publish_action' => array( + 'selected' => 1, + 'skip_confirmation' => 0, + 'override_label' => 0, + 'label' => '', + 'postpone_processing' => 0, + ), + 'action::node_unpromote_action' => array( + 'selected' => 0, + 'skip_confirmation' => 0, + 'override_label' => 0, + 'label' => '', + 'postpone_processing' => 0, + ), + 'action::node_make_unsticky_action' => array( + 'selected' => 0, + 'skip_confirmation' => 0, + 'override_label' => 0, + 'label' => '', + 'postpone_processing' => 0, + ), + 'action::views_bulk_operations_delete_item' => array( + 'selected' => 0, + 'skip_confirmation' => 0, + 'override_label' => 0, + 'label' => '', + 'postpone_processing' => 0, + ), + 'action::node_make_sticky_action' => array( + 'selected' => 0, + 'skip_confirmation' => 0, + 'override_label' => 0, + 'label' => '', + 'postpone_processing' => 0, + ), + ); + /* Champ: Contenu : Type */ + $handler->display->display_options['fields']['type']['id'] = 'type'; + $handler->display->display_options['fields']['type']['table'] = 'node'; + $handler->display->display_options['fields']['type']['field'] = 'type'; + $handler->display->display_options['fields']['type']['hide_alter_empty'] = FALSE; + /* Champ: Contenu : Titre */ + $handler->display->display_options['fields']['title']['id'] = 'title'; + $handler->display->display_options['fields']['title']['table'] = 'node'; + $handler->display->display_options['fields']['title']['field'] = 'title'; + $handler->display->display_options['fields']['title']['alter']['word_boundary'] = FALSE; + $handler->display->display_options['fields']['title']['alter']['ellipsis'] = FALSE; + $handler->display->display_options['fields']['title']['hide_alter_empty'] = FALSE; + /* Champ: Contenu : Lien de modification */ + $handler->display->display_options['fields']['edit_node']['id'] = 'edit_node'; + $handler->display->display_options['fields']['edit_node']['table'] = 'views_entity_node'; + $handler->display->display_options['fields']['edit_node']['field'] = 'edit_node'; + $handler->display->display_options['fields']['edit_node']['label'] = ''; + $handler->display->display_options['fields']['edit_node']['alter']['alter_text'] = TRUE; + $handler->display->display_options['fields']['edit_node']['alter']['text'] = '[edit_node]'; + $handler->display->display_options['fields']['edit_node']['element_label_colon'] = FALSE; + $handler->display->display_options['fields']['edit_node']['hide_alter_empty'] = FALSE; + /* Champ: Contenu : Publié */ + $handler->display->display_options['fields']['status']['id'] = 'status'; + $handler->display->display_options['fields']['status']['table'] = 'node'; + $handler->display->display_options['fields']['status']['field'] = 'status'; + $handler->display->display_options['fields']['status']['element_label_colon'] = FALSE; + $handler->display->display_options['fields']['status']['hide_alter_empty'] = FALSE; + $handler->display->display_options['fields']['status']['not'] = 0; + /* Champ: Contenu : Langue */ + $handler->display->display_options['fields']['language']['id'] = 'language'; + $handler->display->display_options['fields']['language']['table'] = 'node'; + $handler->display->display_options['fields']['language']['field'] = 'language'; + $handler->display->display_options['fields']['language']['element_label_colon'] = FALSE; + $handler->display->display_options['fields']['language']['hide_empty'] = TRUE; + /* Champ: Traduction du contenu : Lien de traduction */ + $handler->display->display_options['fields']['translate_node']['id'] = 'translate_node'; + $handler->display->display_options['fields']['translate_node']['table'] = 'node'; + $handler->display->display_options['fields']['translate_node']['field'] = 'translate_node'; + $handler->display->display_options['fields']['translate_node']['alter']['alter_text'] = TRUE; + $handler->display->display_options['fields']['translate_node']['alter']['text'] = '[translate_node]'; + $handler->display->display_options['fields']['translate_node']['hide_empty'] = TRUE; + $handler->display->display_options['fields']['translate_node']['text'] = 'traduire'; + /* Champ: Contenu : Date de mise à jour */ + $handler->display->display_options['fields']['changed']['id'] = 'changed'; + $handler->display->display_options['fields']['changed']['table'] = 'node'; + $handler->display->display_options['fields']['changed']['field'] = 'changed'; + $handler->display->display_options['fields']['changed']['date_format'] = 'short'; + /* Champ: Contenu : Lien de suppression */ + $handler->display->display_options['fields']['delete_node']['id'] = 'delete_node'; + $handler->display->display_options['fields']['delete_node']['table'] = 'views_entity_node'; + $handler->display->display_options['fields']['delete_node']['field'] = 'delete_node'; + $handler->display->display_options['fields']['delete_node']['alter']['alter_text'] = TRUE; + $handler->display->display_options['fields']['delete_node']['alter']['text'] = '[delete_node]'; + $handler->display->display_options['fields']['delete_node']['element_label_colon'] = FALSE; + $handler->display->display_options['fields']['delete_node']['hide_empty'] = TRUE; + $handler->display->display_options['fields']['delete_node']['text'] = 'supprimer'; + $handler->display->display_options['filter_groups']['groups'] = array( + 1 => 'AND', + 2 => 'AND', + ); + /* Critère de filtrage: Contenu : Type */ + $handler->display->display_options['filters']['type']['id'] = 'type'; + $handler->display->display_options['filters']['type']['table'] = 'node'; + $handler->display->display_options['filters']['type']['field'] = 'type'; + $handler->display->display_options['filters']['type']['value'] = array( + 'all' => 'all', + 'article' => 'article', + 'collection' => 'collection', + 'event' => 'event', + 'location' => 'location', + 'page' => 'page', + 'people' => 'people', + 'program' => 'program', + 'work' => 'work', + ); + $handler->display->display_options['filters']['type']['group'] = 1; + $handler->display->display_options['filters']['type']['exposed'] = TRUE; + $handler->display->display_options['filters']['type']['expose']['operator_id'] = 'type_op'; + $handler->display->display_options['filters']['type']['expose']['label'] = 'Type'; + $handler->display->display_options['filters']['type']['expose']['operator'] = 'type_op'; + $handler->display->display_options['filters']['type']['expose']['identifier'] = 'type'; + $handler->display->display_options['filters']['type']['expose']['remember'] = TRUE; + $handler->display->display_options['filters']['type']['expose']['reduce'] = TRUE; + /* Critère de filtrage: Contenu : Publié */ + $handler->display->display_options['filters']['status']['id'] = 'status'; + $handler->display->display_options['filters']['status']['table'] = 'node'; + $handler->display->display_options['filters']['status']['field'] = 'status'; + $handler->display->display_options['filters']['status']['value'] = 'All'; + $handler->display->display_options['filters']['status']['group'] = 1; + $handler->display->display_options['filters']['status']['exposed'] = TRUE; + $handler->display->display_options['filters']['status']['expose']['operator_id'] = ''; + $handler->display->display_options['filters']['status']['expose']['label'] = 'Publié'; + $handler->display->display_options['filters']['status']['expose']['operator'] = 'status_op'; + $handler->display->display_options['filters']['status']['expose']['identifier'] = 'status'; + $handler->display->display_options['filters']['status']['expose']['remember'] = TRUE; + /* Critère de filtrage: Contenu : Langue */ + $handler->display->display_options['filters']['language']['id'] = 'language'; + $handler->display->display_options['filters']['language']['table'] = 'node'; + $handler->display->display_options['filters']['language']['field'] = 'language'; + $handler->display->display_options['filters']['language']['value'] = array( + 'und' => 'und', + 'en' => 'en', + 'fr' => 'fr', + ); + $handler->display->display_options['filters']['language']['group'] = 1; + $handler->display->display_options['filters']['language']['exposed'] = TRUE; + $handler->display->display_options['filters']['language']['expose']['operator_id'] = 'language_op'; + $handler->display->display_options['filters']['language']['expose']['label'] = 'Langue'; + $handler->display->display_options['filters']['language']['expose']['operator'] = 'language_op'; + $handler->display->display_options['filters']['language']['expose']['identifier'] = 'language'; + $handler->display->display_options['filters']['language']['expose']['remember'] = TRUE; + $handler->display->display_options['filters']['language']['expose']['reduce'] = TRUE; + /* Critère de filtrage: current user can update */ + $handler->display->display_options['filters']['php']['id'] = 'php'; + $handler->display->display_options['filters']['php']['table'] = 'views'; + $handler->display->display_options['filters']['php']['field'] = 'php'; + $handler->display->display_options['filters']['php']['ui_name'] = 'current user can update'; + $handler->display->display_options['filters']['php']['group'] = 1; + $handler->display->display_options['filters']['php']['use_php_setup'] = 0; + $handler->display->display_options['filters']['php']['php_filter'] = 'global $user; +//dsm($data); +return !(node_access(\'update\', node_load($data->nid), $user)); +'; + + /* Display: Page : all */ + $handler = $view->new_display('page', 'Page : all', 'page'); + $handler->display->display_options['defaults']['pager'] = FALSE; + $handler->display->display_options['pager']['type'] = 'mini'; + $handler->display->display_options['pager']['options']['items_per_page'] = '30'; + $handler->display->display_options['pager']['options']['offset'] = '0'; + $handler->display->display_options['pager']['options']['id'] = '0'; + $handler->display->display_options['pager']['options']['expose']['items_per_page'] = TRUE; + $handler->display->display_options['pager']['options']['expose']['items_per_page_label'] = 'Éléments par page'; + $handler->display->display_options['pager']['options']['expose']['items_per_page_options_all'] = TRUE; + $handler->display->display_options['pager']['options']['expose']['items_per_page_options_all_label'] = '- Tout -'; + $handler->display->display_options['pager']['options']['expose']['offset_label'] = 'Décalage'; + $handler->display->display_options['pager']['options']['tags']['first'] = '« premier'; + $handler->display->display_options['pager']['options']['tags']['previous'] = '‹ précédent'; + $handler->display->display_options['pager']['options']['tags']['next'] = 'suivant ›'; + $handler->display->display_options['pager']['options']['tags']['last'] = 'dernier »'; + $handler->display->display_options['path'] = 'admin/content/nodes'; + $handler->display->display_options['menu']['type'] = 'tab'; + $handler->display->display_options['menu']['title'] = 'Content'; + $handler->display->display_options['menu']['weight'] = '-100'; + $handler->display->display_options['menu']['context'] = 0; + $handler->display->display_options['tab_options']['type'] = 'normal'; + $handler->display->display_options['tab_options']['title'] = 'dashboard'; + $handler->display->display_options['tab_options']['weight'] = '0'; + + /* Display: Block */ + $handler = $view->new_display('block', 'Block', 'block_1'); + $handler->display->display_options['defaults']['title'] = FALSE; + $handler->display->display_options['title'] = 'Recent Content'; + $handler->display->display_options['defaults']['pager'] = FALSE; + $handler->display->display_options['pager']['type'] = 'some'; + $handler->display->display_options['pager']['options']['items_per_page'] = '10'; + $handler->display->display_options['pager']['options']['offset'] = '0'; + $handler->display->display_options['defaults']['fields'] = FALSE; + /* Champ: Contenu : Type */ + $handler->display->display_options['fields']['type']['id'] = 'type'; + $handler->display->display_options['fields']['type']['table'] = 'node'; + $handler->display->display_options['fields']['type']['field'] = 'type'; + $handler->display->display_options['fields']['type']['hide_alter_empty'] = FALSE; + /* Champ: Contenu : Titre */ + $handler->display->display_options['fields']['title']['id'] = 'title'; + $handler->display->display_options['fields']['title']['table'] = 'node'; + $handler->display->display_options['fields']['title']['field'] = 'title'; + $handler->display->display_options['fields']['title']['alter']['word_boundary'] = FALSE; + $handler->display->display_options['fields']['title']['alter']['ellipsis'] = FALSE; + $handler->display->display_options['fields']['title']['hide_alter_empty'] = FALSE; + /* Champ: Contenu : Lien de modification */ + $handler->display->display_options['fields']['edit_node']['id'] = 'edit_node'; + $handler->display->display_options['fields']['edit_node']['table'] = 'views_entity_node'; + $handler->display->display_options['fields']['edit_node']['field'] = 'edit_node'; + $handler->display->display_options['fields']['edit_node']['label'] = 'Modifier'; + $handler->display->display_options['fields']['edit_node']['alter']['alter_text'] = TRUE; + $handler->display->display_options['fields']['edit_node']['alter']['text'] = '[edit_node]'; + $handler->display->display_options['fields']['edit_node']['hide_alter_empty'] = FALSE; + /* Champ: Contenu : Publié */ + $handler->display->display_options['fields']['status']['id'] = 'status'; + $handler->display->display_options['fields']['status']['table'] = 'node'; + $handler->display->display_options['fields']['status']['field'] = 'status'; + $handler->display->display_options['fields']['status']['element_label_colon'] = FALSE; + $handler->display->display_options['fields']['status']['hide_alter_empty'] = FALSE; + $handler->display->display_options['fields']['status']['not'] = 0; + /* Champ: Contenu : Langue */ + $handler->display->display_options['fields']['language']['id'] = 'language'; + $handler->display->display_options['fields']['language']['table'] = 'node'; + $handler->display->display_options['fields']['language']['field'] = 'language'; + $handler->display->display_options['fields']['language']['element_label_colon'] = FALSE; + $handler->display->display_options['fields']['language']['hide_alter_empty'] = FALSE; + /* Champ: Traduction du contenu : Lien de traduction */ + $handler->display->display_options['fields']['translate_node']['id'] = 'translate_node'; + $handler->display->display_options['fields']['translate_node']['table'] = 'node'; + $handler->display->display_options['fields']['translate_node']['field'] = 'translate_node'; + $handler->display->display_options['fields']['translate_node']['element_label_colon'] = FALSE; + $handler->display->display_options['fields']['translate_node']['hide_alter_empty'] = FALSE; + $handler->display->display_options['fields']['translate_node']['text'] = 'Traduire'; + $translatables['admin_contents'] = array( + t('Master'), + t('Contenu'), + t('plus'), + t('Filtrer'), + t('réinitialiser'), + t('Trier par'), + t('Asc'), + t('Desc'), + t('Éléments par page'), + t('- Tout -'), + t('Décalage'), + t('« premier'), + t('‹ précédent'), + t('suivant ›'), + t('dernier »'), + t('Displaying @start - @end of @total'), + t('- Choisir une opération -'), + t('Type'), + t('Titre'), + t('[edit_node]'), + t('Publié'), + t('Langue'), + t('Lien de traduction'), + t('[translate_node]'), + t('traduire'), + t('Date de mise à jour'), + t('Lien de suppression'), + t('[delete_node]'), + t('supprimer'), + t('Page : all'), + t('Block'), + t('Recent Content'), + t('Modifier'), + t('Traduire'), + ); + $export['admin_contents'] = $view; + + return $export; +} diff --git a/themes/bartik/bartik.info b/themes/bartik/bartik.info index 60135d8a..007d0a53 100644 --- a/themes/bartik/bartik.info +++ b/themes/bartik/bartik.info @@ -34,8 +34,8 @@ regions[footer] = Footer settings[shortcut_module_link] = 0 -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/themes/garland/garland.info b/themes/garland/garland.info index 12b8e352..00920141 100644 --- a/themes/garland/garland.info +++ b/themes/garland/garland.info @@ -7,8 +7,8 @@ stylesheets[all][] = style.css stylesheets[print][] = print.css settings[garland_width] = fluid -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/themes/garland/template.php b/themes/garland/template.php index 416a43a8..ef7d4771 100644 --- a/themes/garland/template.php +++ b/themes/garland/template.php @@ -19,22 +19,22 @@ function garland_breadcrumb($variables) { /** * Override or insert variables into the maintenance page template. */ -function garland_preprocess_maintenance_page(&$vars) { +function garland_preprocess_maintenance_page(&$variables) { // While markup for normal pages is split into page.tpl.php and html.tpl.php, // the markup for the maintenance page is all in the single // maintenance-page.tpl.php template. So, to have what's done in // garland_preprocess_html() also happen on the maintenance page, it has to be // called here. - garland_preprocess_html($vars); + garland_preprocess_html($variables); } /** * Override or insert variables into the html template. */ -function garland_preprocess_html(&$vars) { +function garland_preprocess_html(&$variables) { // Toggle fixed or fluid width. if (theme_get_setting('garland_width') == 'fluid') { - $vars['classes_array'][] = 'fluid-width'; + $variables['classes_array'][] = 'fluid-width'; } // Add conditional CSS for IE6. drupal_add_css(path_to_theme() . '/fix-ie.css', array('group' => CSS_THEME, 'browsers' => array('IE' => 'lt IE 7', '!IE' => FALSE), 'preprocess' => FALSE)); @@ -43,27 +43,27 @@ function garland_preprocess_html(&$vars) { /** * Override or insert variables into the html template. */ -function garland_process_html(&$vars) { +function garland_process_html(&$variables) { // Hook into color.module if (module_exists('color')) { - _color_html_alter($vars); + _color_html_alter($variables); } } /** * Override or insert variables into the page template. */ -function garland_preprocess_page(&$vars) { +function garland_preprocess_page(&$variables) { // Move secondary tabs into a separate variable. - $vars['tabs2'] = array( + $variables['tabs2'] = array( '#theme' => 'menu_local_tasks', - '#secondary' => $vars['tabs']['#secondary'], + '#secondary' => $variables['tabs']['#secondary'], ); - unset($vars['tabs']['#secondary']); + unset($variables['tabs']['#secondary']); - if (isset($vars['main_menu'])) { - $vars['primary_nav'] = theme('links__system_main_menu', array( - 'links' => $vars['main_menu'], + if (isset($variables['main_menu'])) { + $variables['primary_nav'] = theme('links__system_main_menu', array( + 'links' => $variables['main_menu'], 'attributes' => array( 'class' => array('links', 'inline', 'main-menu'), ), @@ -75,11 +75,11 @@ function garland_preprocess_page(&$vars) { )); } else { - $vars['primary_nav'] = FALSE; + $variables['primary_nav'] = FALSE; } - if (isset($vars['secondary_menu'])) { - $vars['secondary_nav'] = theme('links__system_secondary_menu', array( - 'links' => $vars['secondary_menu'], + if (isset($variables['secondary_menu'])) { + $variables['secondary_nav'] = theme('links__system_secondary_menu', array( + 'links' => $variables['secondary_menu'], 'attributes' => array( 'class' => array('links', 'inline', 'secondary-menu'), ), @@ -91,66 +91,66 @@ function garland_preprocess_page(&$vars) { )); } else { - $vars['secondary_nav'] = FALSE; + $variables['secondary_nav'] = FALSE; } // Prepare header. $site_fields = array(); - if (!empty($vars['site_name'])) { - $site_fields[] = $vars['site_name']; + if (!empty($variables['site_name'])) { + $site_fields[] = $variables['site_name']; } - if (!empty($vars['site_slogan'])) { - $site_fields[] = $vars['site_slogan']; + if (!empty($variables['site_slogan'])) { + $site_fields[] = $variables['site_slogan']; } - $vars['site_title'] = implode(' ', $site_fields); + $variables['site_title'] = implode(' ', $site_fields); if (!empty($site_fields)) { $site_fields[0] = '' . $site_fields[0] . ''; } - $vars['site_html'] = implode(' ', $site_fields); + $variables['site_html'] = implode(' ', $site_fields); // Set a variable for the site name title and logo alt attributes text. - $slogan_text = $vars['site_slogan']; - $site_name_text = $vars['site_name']; - $vars['site_name_and_slogan'] = $site_name_text . ' ' . $slogan_text; + $slogan_text = $variables['site_slogan']; + $site_name_text = $variables['site_name']; + $variables['site_name_and_slogan'] = $site_name_text . ' ' . $slogan_text; } /** * Override or insert variables into the node template. */ -function garland_preprocess_node(&$vars) { - $vars['submitted'] = $vars['date'] . ' — ' . $vars['name']; +function garland_preprocess_node(&$variables) { + $variables['submitted'] = $variables['date'] . ' — ' . $variables['name']; } /** * Override or insert variables into the comment template. */ -function garland_preprocess_comment(&$vars) { - $vars['submitted'] = $vars['created'] . ' — ' . $vars['author']; +function garland_preprocess_comment(&$variables) { + $variables['submitted'] = $variables['created'] . ' — ' . $variables['author']; } /** * Override or insert variables into the block template. */ -function garland_preprocess_block(&$vars) { - $vars['title_attributes_array']['class'][] = 'title'; - $vars['classes_array'][] = 'clearfix'; +function garland_preprocess_block(&$variables) { + $variables['title_attributes_array']['class'][] = 'title'; + $variables['classes_array'][] = 'clearfix'; } /** * Override or insert variables into the page template. */ -function garland_process_page(&$vars) { +function garland_process_page(&$variables) { // Hook into color.module if (module_exists('color')) { - _color_page_alter($vars); + _color_page_alter($variables); } } /** * Override or insert variables into the region template. */ -function garland_preprocess_region(&$vars) { - if ($vars['region'] == 'header') { - $vars['classes_array'][] = 'clearfix'; +function garland_preprocess_region(&$variables) { + if ($variables['region'] == 'header') { + $variables['classes_array'][] = 'clearfix'; } } diff --git a/themes/seven/seven.info b/themes/seven/seven.info index 91414555..85e22afd 100644 --- a/themes/seven/seven.info +++ b/themes/seven/seven.info @@ -13,8 +13,8 @@ regions[page_bottom] = Page bottom regions[sidebar_first] = First sidebar regions_hidden[] = sidebar_first -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/themes/stark/stark.info b/themes/stark/stark.info index 23c36119..651c4fa7 100644 --- a/themes/stark/stark.info +++ b/themes/stark/stark.info @@ -5,8 +5,8 @@ version = VERSION core = 7.x stylesheets[all][] = layout.css -; Information added by Drupal.org packaging script on 2014-11-19 -version = "7.34" +; Information added by Drupal.org packaging script on 2016-12-07 +version = "7.53" project = "drupal" -datestamp = "1416429488" +datestamp = "1481152423" diff --git a/web.config b/web.config index 09983d92..c6fb5c84 100644 --- a/web.config +++ b/web.config @@ -6,7 +6,7 @@ - +