From 51392c687d58e6ea3d49a7b3e0103a0d83abcf1c Mon Sep 17 00:00:00 2001 From: Robert Date: Mon, 24 May 2021 15:37:55 +0200 Subject: [PATCH] Drupal: update code 7.80 --- frontend/drupal/CHANGELOG.txt | 11 + frontend/drupal/includes/bootstrap.inc | 91 +- frontend/drupal/includes/common.inc | 13 +- .../drupal/includes/database/database.inc | 48 +- .../includes/database/mysql/database.inc | 61 +- .../includes/database/pgsql/database.inc | 6 +- frontend/drupal/includes/database/select.inc | 2 +- .../includes/database/sqlite/database.inc | 12 +- frontend/drupal/includes/errors.inc | 5 +- frontend/drupal/includes/menu.inc | 4 +- frontend/drupal/includes/session.inc | 50 +- frontend/drupal/misc/ajax.js | 2 +- .../drupal/modules/aggregator/aggregator.info | 6 +- .../aggregator/tests/aggregator_test.info | 6 +- frontend/drupal/modules/block/block.info | 6 +- .../modules/block/tests/block_test.info | 6 +- .../block_test_theme/block_test_theme.info | 6 +- frontend/drupal/modules/blog/blog.info | 6 +- frontend/drupal/modules/book/book.info | 6 +- frontend/drupal/modules/book/book.test | 8 +- frontend/drupal/modules/color/color.info | 6 +- frontend/drupal/modules/comment/comment.info | 6 +- frontend/drupal/modules/contact/contact.info | 6 +- .../drupal/modules/contextual/contextual.info | 6 +- .../drupal/modules/dashboard/dashboard.info | 6 +- frontend/drupal/modules/dblog/dblog.info | 6 +- frontend/drupal/modules/field/field.info | 6 +- .../field_sql_storage/field_sql_storage.info | 6 +- .../field_sql_storage.module | 96 + .../field_sql_storage/field_sql_storage.test | 63 + .../modules/field/modules/list/list.info | 6 +- .../field/modules/list/tests/list_test.info | 6 +- .../modules/field/modules/number/number.info | 6 +- .../field/modules/options/options.info | 6 +- .../modules/field/modules/text/text.info | 6 +- .../modules/field/tests/field_test.info | 6 +- .../field/tests/field_test.storage.inc | 105 - .../drupal/modules/field_ui/field_ui.info | 6 +- frontend/drupal/modules/file/file.field.inc | 13 +- frontend/drupal/modules/file/file.info | 6 +- .../modules/file/tests/file_module_test.info | 6 +- frontend/drupal/modules/filter/filter.info | 6 +- frontend/drupal/modules/forum/forum.info | 6 +- frontend/drupal/modules/help/help.info | 6 +- frontend/drupal/modules/image/image.info | 6 +- frontend/drupal/modules/image/image.test | 2 +- .../image/tests/image_module_test.info | 6 +- frontend/drupal/modules/locale/locale.info | 6 +- .../modules/locale/tests/locale_test.info | 6 +- frontend/drupal/modules/menu/menu.info | 6 +- frontend/drupal/modules/node/node.info | 6 +- .../modules/node/tests/node_access_test.info | 6 +- .../drupal/modules/node/tests/node_test.info | 6 +- .../node/tests/node_test_exception.info | 6 +- frontend/drupal/modules/openid/openid.inc | 3 +- frontend/drupal/modules/openid/openid.info | 6 +- frontend/drupal/modules/openid/openid.module | 2 +- .../modules/openid/tests/openid_test.info | 6 +- frontend/drupal/modules/overlay/overlay.info | 6 +- frontend/drupal/modules/path/path.info | 6 +- frontend/drupal/modules/php/php.info | 6 +- frontend/drupal/modules/poll/poll.info | 6 +- frontend/drupal/modules/profile/profile.info | 6 +- frontend/drupal/modules/rdf/rdf.info | 6 +- .../drupal/modules/rdf/tests/rdf_test.info | 6 +- frontend/drupal/modules/search/search.info | 6 +- .../search/tests/search_embedded_form.info | 6 +- .../search/tests/search_extra_type.info | 6 +- .../search/tests/search_node_tags.info | 6 +- .../drupal/modules/shortcut/shortcut.info | 6 +- .../simpletest/drupal_web_test_case.php | 16 +- .../drupal/modules/simpletest/simpletest.info | 6 +- .../drupal/modules/simpletest/simpletest.test | 19 +- .../simpletest/tests/actions_loop_test.info | 6 +- .../simpletest/tests/ajax_forms_test.info | 6 +- .../modules/simpletest/tests/ajax_test.info | 6 +- .../modules/simpletest/tests/batch_test.info | 6 +- .../modules/simpletest/tests/boot_test_1.info | 6 +- .../modules/simpletest/tests/boot_test_2.info | 6 +- .../modules/simpletest/tests/bootstrap.test | 8 +- .../modules/simpletest/tests/common.test | 4 +- .../modules/simpletest/tests/common_test.info | 6 +- .../tests/common_test_cron_helper.info | 6 +- .../simpletest/tests/database_test.info | 6 +- .../simpletest/tests/database_test.test | 29 +- .../drupal_autoload_test.info | 6 +- ...drupal_system_listing_compatible_test.info | 6 +- ...upal_system_listing_incompatible_test.info | 6 +- .../simpletest/tests/entity_cache_test.info | 6 +- .../tests/entity_cache_test_dependency.info | 6 +- .../tests/entity_crud_hook_test.info | 6 +- .../tests/entity_query_access_test.info | 6 +- .../modules/simpletest/tests/error.test | 5 +- .../modules/simpletest/tests/error_test.info | 6 +- .../simpletest/tests/error_test.module | 8 +- .../modules/simpletest/tests/file_test.info | 6 +- .../modules/simpletest/tests/filter_test.info | 6 +- .../drupal/modules/simpletest/tests/form.test | 8 +- .../modules/simpletest/tests/form_test.info | 6 +- .../modules/simpletest/tests/image_test.info | 6 +- .../modules/simpletest/tests/menu_test.info | 6 +- .../modules/simpletest/tests/module_test.info | 6 +- .../modules/simpletest/tests/path_test.info | 6 +- .../tests/psr_0_test/psr_0_test.info | 6 +- .../tests/psr_4_test/psr_4_test.info | 6 +- .../simpletest/tests/requirements1_test.info | 6 +- .../simpletest/tests/requirements2_test.info | 6 +- .../modules/simpletest/tests/session.test | 195 ++ .../simpletest/tests/session_test.info | 6 +- .../simpletest/tests/session_test.module | 13 + .../tests/system_dependencies_test.info | 6 +- ...atible_core_version_dependencies_test.info | 6 +- ...system_incompatible_core_version_test.info | 6 +- ...ible_module_version_dependencies_test.info | 6 +- ...stem_incompatible_module_version_test.info | 6 +- .../tests/system_project_namespace_test.info | 6 +- .../modules/simpletest/tests/system_test.info | 6 +- .../simpletest/tests/taxonomy_test.info | 6 +- .../modules/simpletest/tests/theme_test.info | 6 +- .../themes/test_basetheme/test_basetheme.info | 6 +- .../themes/test_subtheme/test_subtheme.info | 6 +- .../tests/themes/test_theme/test_theme.info | 6 +- .../test_theme_nyan_cat.info | 6 +- .../simpletest/tests/update_script_test.info | 6 +- .../simpletest/tests/update_test_1.info | 6 +- .../simpletest/tests/update_test_2.info | 6 +- .../simpletest/tests/update_test_3.info | 6 +- .../tests/upgrade/upgrade.locale.test | 2 +- .../simpletest/tests/url_alter_test.info | 6 +- .../modules/simpletest/tests/xmlrpc_test.info | 6 +- .../drupal/modules/statistics/statistics.info | 6 +- frontend/drupal/modules/syslog/syslog.info | 6 +- frontend/drupal/modules/system/system.info | 6 +- frontend/drupal/modules/system/system.tar.inc | 2 +- .../modules/system/tests/cron_queue_test.info | 6 +- .../system/tests/system_cron_test.info | 6 +- .../drupal/modules/taxonomy/taxonomy.info | 6 +- frontend/drupal/modules/toolbar/toolbar.info | 6 +- frontend/drupal/modules/tracker/tracker.info | 6 +- .../translation/tests/translation_test.info | 6 +- .../modules/translation/translation.info | 6 +- .../modules/trigger/tests/trigger_test.info | 6 +- frontend/drupal/modules/trigger/trigger.info | 6 +- .../modules/update/tests/aaa_update_test.info | 6 +- .../modules/update/tests/bbb_update_test.info | 6 +- .../modules/update/tests/ccc_update_test.info | 6 +- .../update_test_admintheme.info | 6 +- .../update_test_basetheme.info | 6 +- .../update_test_subtheme.info | 6 +- .../modules/update/tests/update_test.info | 6 +- frontend/drupal/modules/update/update.info | 6 +- .../modules/user/tests/user_flood_test.info | 6 +- .../modules/user/tests/user_form_test.info | 6 +- .../modules/user/tests/user_form_test.module | 39 + .../modules/user/tests/user_session_test.info | 6 +- frontend/drupal/modules/user/user.admin.inc | 3 +- frontend/drupal/modules/user/user.info | 6 +- frontend/drupal/modules/user/user.pages.inc | 52 +- frontend/drupal/modules/user/user.test | 98 +- frontend/drupal/profiles/minimal/minimal.info | 6 +- .../drupal/profiles/standard/standard.info | 6 +- ...drupal_system_listing_compatible_test.info | 6 +- ...upal_system_listing_incompatible_test.info | 6 +- frontend/drupal/profiles/testing/testing.info | 6 +- .../sites/all/modules/breakpoints/LICENSE.txt | 339 +++ .../sites/all/modules/breakpoints/README.txt | 76 + .../modules/breakpoints/breakpoints.admin.inc | 1225 ++++++++++ .../all/modules/breakpoints/breakpoints.info | 13 + .../modules/breakpoints/breakpoints.install | 186 ++ .../modules/breakpoints/breakpoints.module | 970 ++++++++ .../all/modules/breakpoints/breakpoints.test | 913 +++++++ .../breakpoints/css/breakpoints.admin.css | 11 + .../plugins/export_ui/breakpoints.inc | 7 + .../tests/breakpoints_theme_test.info | 12 + .../tests/breakpoints_theme_test.module | 13 + .../breakpoints_test_theme.info | 17 + .../sites/all/modules/picture/LICENSE.txt | 339 +++ .../sites/all/modules/picture/README.txt | 22 + .../picture/ckeditor/plugins/plugin.js | 268 ++ .../plugins/export_ui/picture_mapping.inc | 56 + .../flexslider_picture.info | 15 + .../flexslider_picture.install | 159 ++ .../flexslider_picture.module | 249 ++ .../theme/flexslider_picture.theme.inc | 147 ++ .../picture/includes/PictureMapping.php | 397 +++ .../modules/picture/lazysizes/lazysizes.js | 547 +++++ .../picture/lazysizes/lazysizes.min.js | 2 + .../plugins/aspectratio/ls.aspectratio.css | 3 + .../plugins/aspectratio/ls.aspectratio.js | 204 ++ .../plugins/aspectratio/ls.aspectratio.min.js | 2 + .../all/modules/picture/picture.admin.inc | 431 ++++ .../all/modules/picture/picture.drush.inc | 71 + .../modules/picture/picture.file_entity_1.inc | 198 ++ .../sites/all/modules/picture/picture.info | 17 + .../sites/all/modules/picture/picture.install | 243 ++ .../sites/all/modules/picture/picture.js | 42 + .../sites/all/modules/picture/picture.min.js | 1 + .../sites/all/modules/picture/picture.module | 2168 +++++++++++++++++ .../all/modules/picture/picture_colorbox.css | 10 + .../all/modules/picture/picture_colorbox.js | 39 + .../all/modules/picture/picture_wysiwyg.css | 42 + .../picture/picturefill2/picturefill.js | 736 ++++++ .../picture/picturefill2/picturefill.min.js | 4 + frontend/drupal/themes/bartik/bartik.info | 6 +- frontend/drupal/themes/garland/garland.info | 6 +- frontend/drupal/themes/seven/seven.info | 6 +- frontend/drupal/themes/stark/stark.info | 6 +- sql/update2.sql | 4 + 208 files changed, 11474 insertions(+), 614 deletions(-) create mode 100644 frontend/drupal/sites/all/modules/breakpoints/LICENSE.txt create mode 100644 frontend/drupal/sites/all/modules/breakpoints/README.txt create mode 100644 frontend/drupal/sites/all/modules/breakpoints/breakpoints.admin.inc create mode 100644 frontend/drupal/sites/all/modules/breakpoints/breakpoints.info create mode 100644 frontend/drupal/sites/all/modules/breakpoints/breakpoints.install create mode 100644 frontend/drupal/sites/all/modules/breakpoints/breakpoints.module create mode 100644 frontend/drupal/sites/all/modules/breakpoints/breakpoints.test create mode 100644 frontend/drupal/sites/all/modules/breakpoints/css/breakpoints.admin.css create mode 100644 frontend/drupal/sites/all/modules/breakpoints/plugins/export_ui/breakpoints.inc create mode 100644 frontend/drupal/sites/all/modules/breakpoints/tests/breakpoints_theme_test.info create mode 100644 frontend/drupal/sites/all/modules/breakpoints/tests/breakpoints_theme_test.module create mode 100644 frontend/drupal/sites/all/modules/breakpoints/tests/themes/breakpoints_test_theme/breakpoints_test_theme.info create mode 100644 frontend/drupal/sites/all/modules/picture/LICENSE.txt create mode 100644 frontend/drupal/sites/all/modules/picture/README.txt create mode 100644 frontend/drupal/sites/all/modules/picture/ckeditor/plugins/plugin.js create mode 100644 frontend/drupal/sites/all/modules/picture/ctools/plugins/export_ui/picture_mapping.inc create mode 100644 frontend/drupal/sites/all/modules/picture/flexslider_picture/flexslider_picture.info create mode 100644 frontend/drupal/sites/all/modules/picture/flexslider_picture/flexslider_picture.install create mode 100644 frontend/drupal/sites/all/modules/picture/flexslider_picture/flexslider_picture.module create mode 100644 frontend/drupal/sites/all/modules/picture/flexslider_picture/theme/flexslider_picture.theme.inc create mode 100644 frontend/drupal/sites/all/modules/picture/includes/PictureMapping.php create mode 100644 frontend/drupal/sites/all/modules/picture/lazysizes/lazysizes.js create mode 100644 frontend/drupal/sites/all/modules/picture/lazysizes/lazysizes.min.js create mode 100644 frontend/drupal/sites/all/modules/picture/lazysizes/plugins/aspectratio/ls.aspectratio.css create mode 100644 frontend/drupal/sites/all/modules/picture/lazysizes/plugins/aspectratio/ls.aspectratio.js create mode 100644 frontend/drupal/sites/all/modules/picture/lazysizes/plugins/aspectratio/ls.aspectratio.min.js create mode 100644 frontend/drupal/sites/all/modules/picture/picture.admin.inc create mode 100644 frontend/drupal/sites/all/modules/picture/picture.drush.inc create mode 100644 frontend/drupal/sites/all/modules/picture/picture.file_entity_1.inc create mode 100644 frontend/drupal/sites/all/modules/picture/picture.info create mode 100644 frontend/drupal/sites/all/modules/picture/picture.install create mode 100644 frontend/drupal/sites/all/modules/picture/picture.js create mode 100644 frontend/drupal/sites/all/modules/picture/picture.min.js create mode 100644 frontend/drupal/sites/all/modules/picture/picture.module create mode 100644 frontend/drupal/sites/all/modules/picture/picture_colorbox.css create mode 100644 frontend/drupal/sites/all/modules/picture/picture_colorbox.js create mode 100644 frontend/drupal/sites/all/modules/picture/picture_wysiwyg.css create mode 100644 frontend/drupal/sites/all/modules/picture/picturefill2/picturefill.js create mode 100644 frontend/drupal/sites/all/modules/picture/picturefill2/picturefill.min.js diff --git a/frontend/drupal/CHANGELOG.txt b/frontend/drupal/CHANGELOG.txt index f9b010c58..dcef8ea97 100644 --- a/frontend/drupal/CHANGELOG.txt +++ b/frontend/drupal/CHANGELOG.txt @@ -1,3 +1,14 @@ +Drupal 7.80, 2021-04-20 +----------------------- +- Fixed security issues: + - SA-CORE-2021-002 + +Drupal 7.79, 2021-04-07 +----------------------- +- Initial support for PHP 8 +- Support for SameSite cookie attribute +- Avoid write for unchanged fields (opt-in) + Drupal 7.78, 2021-01-19 ----------------------- - Fixed security issues: diff --git a/frontend/drupal/includes/bootstrap.inc b/frontend/drupal/includes/bootstrap.inc index 2b6d7ff48..f9202da9f 100644 --- a/frontend/drupal/includes/bootstrap.inc +++ b/frontend/drupal/includes/bootstrap.inc @@ -8,7 +8,7 @@ /** * The current system version. */ -define('VERSION', '7.78'); +define('VERSION', '7.80'); /** * Core API compatibility. @@ -2596,13 +2596,10 @@ function drupal_get_hash_salt() { * The filename that the error was raised in. * @param $line * The line number the error was raised at. - * @param $context - * An array that points to the active symbol table at the point the error - * occurred. */ -function _drupal_error_handler($error_level, $message, $filename, $line, $context) { +function _drupal_error_handler($error_level, $message, $filename, $line) { require_once DRUPAL_ROOT . '/includes/errors.inc'; - _drupal_error_handler_real($error_level, $message, $filename, $line, $context); + _drupal_error_handler_real($error_level, $message, $filename, $line); } /** @@ -3879,3 +3876,85 @@ function drupal_clear_opcode_cache($filepath) { @apc_delete_file($filepath); } } + +/** + * Drupal's wrapper around PHP's setcookie() function. + * + * This allows the cookie's $value and $options to be altered. + * + * @param $name + * The name of the cookie. + * @param $value + * The value of the cookie. + * @param $options + * An associative array which may have any of the keys expires, path, domain, + * secure, httponly, samesite. + * + * @see setcookie() + * @ingroup php_wrappers + */ +function drupal_setcookie($name, $value, $options) { + $options = _drupal_cookie_params($options); + if (\PHP_VERSION_ID >= 70300) { + setcookie($name, $value, $options); + } + else { + setcookie($name, $value, $options['expires'], $options['path'], $options['domain'], $options['secure'], $options['httponly']); + } +} + +/** + * Process the params for cookies. This emulates support for the SameSite + * attribute in earlier versions of PHP, and allows the value of that attribute + * to be overridden. + * + * @param $options + * An associative array which may have any of the keys expires, path, domain, + * secure, httponly, samesite. + * + * @return + * An associative array which may have any of the keys expires, path, domain, + * secure, httponly, and samesite. + */ +function _drupal_cookie_params($options) { + $options['samesite'] = _drupal_samesite_cookie($options); + if (\PHP_VERSION_ID < 70300) { + // Emulate SameSite support in older PHP versions. + if (!empty($options['samesite'])) { + // Ensure the SameSite attribute is only added once. + if (!preg_match('/SameSite=/i', $options['path'])) { + $options['path'] .= '; SameSite=' . $options['samesite']; + } + } + } + return $options; +} + +/** + * Determine the value for the samesite cookie attribute, in the following order + * of precedence: + * + * 1) A value explicitly passed to drupal_setcookie() + * 2) A value set in $conf['samesite_cookie_value'] + * 3) The setting from php ini + * 4) The default of None, or FALSE (no attribute) if the cookie is not Secure + * + * @param $options + * An associative array as passed to drupal_setcookie(). + * @return + * The value for the samesite cookie attribute. + */ +function _drupal_samesite_cookie($options) { + if (isset($options['samesite'])) { + return $options['samesite']; + } + $override = variable_get('samesite_cookie_value', NULL); + if ($override !== NULL) { + return $override; + } + $ini_options = session_get_cookie_params(); + if (isset($ini_options['samesite'])) { + return $ini_options['samesite']; + } + return empty($options['secure']) ? FALSE : 'None'; +} diff --git a/frontend/drupal/includes/common.inc b/frontend/drupal/includes/common.inc index 7b7955855..690c00478 100644 --- a/frontend/drupal/includes/common.inc +++ b/frontend/drupal/includes/common.inc @@ -1559,7 +1559,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\-]+)\s*([^>]*)>?|()$%', $string, $matches)) { // Seriously malformed. return ''; } @@ -1618,7 +1618,13 @@ function _filter_xss_attributes($attr) { // Attribute name, href for instance. if (preg_match('/^([-a-zA-Z]+)/', $attr, $match)) { $attrname = strtolower($match[1]); - $skip = ($attrname == 'style' || substr($attrname, 0, 2) == 'on'); + $skip = ( + $attrname == 'style' || + substr($attrname, 0, 2) == 'on' || + substr($attrname, 0, 1) == '-' || + // Ignore long attributes to avoid unnecessary processing overhead. + strlen($attrname) > 96 + ); $working = $mode = 1; $attr = preg_replace('/^[-a-zA-Z]+/', '', $attr); } @@ -2329,6 +2335,7 @@ function url($path = NULL, array $options = array()) { } elseif (!empty($path) && !$options['alias']) { $language = isset($options['language']) && isset($options['language']->language) ? $options['language']->language : ''; + require_once DRUPAL_ROOT . '/' . variable_get('path_inc', 'includes/path.inc'); $alias = drupal_get_path_alias($original_path, $language); if ($alias != $original_path) { // Strip leading slashes from internal path aliases to prevent them @@ -5166,6 +5173,8 @@ function drupal_build_js_cache($files) { $contents .= file_get_contents($path) . ";\n"; } } + // Remove JS source and source mapping urls or these may cause 404 errors. + $contents = preg_replace('/\/\/(#|@)\s(sourceURL|sourceMappingURL)=\s*(\S*?)\s*$/m', '', $contents); // Prefix filename to prevent blocking by firewalls which reject files // starting with "ad*". $filename = 'js_' . drupal_hash_base64($contents) . '.js'; diff --git a/frontend/drupal/includes/database/database.inc b/frontend/drupal/includes/database/database.inc index d4d2d8f02..61ac44f78 100644 --- a/frontend/drupal/includes/database/database.inc +++ b/frontend/drupal/includes/database/database.inc @@ -184,7 +184,7 @@ * * @see http://php.net/manual/book.pdo.php */ -abstract class DatabaseConnection extends PDO { +abstract class DatabaseConnection { /** * The database target this connection is for. @@ -261,6 +261,13 @@ abstract class DatabaseConnection extends PDO { */ protected $temporaryNameIndex = 0; + /** + * The actual PDO connection. + * + * @var \PDO + */ + protected $connection; + /** * The connection information for this connection object. * @@ -325,14 +332,27 @@ abstract class DatabaseConnection extends PDO { $driver_options[PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION; // Call PDO::__construct and PDO::setAttribute. - parent::__construct($dsn, $username, $password, $driver_options); + $this->connection = new PDO($dsn, $username, $password, $driver_options); // Set a Statement class, unless the driver opted out. if (!empty($this->statementClass)) { - $this->setAttribute(PDO::ATTR_STATEMENT_CLASS, array($this->statementClass, array($this))); + $this->connection->setAttribute(PDO::ATTR_STATEMENT_CLASS, array($this->statementClass, array($this))); } } + /** + * Proxy possible direct calls to the \PDO methods. + * + * Since PHP8.0 the signature of the the \PDO::query() method has changed, + * and this class can't extending \PDO any more. + * + * However, for the BC, proxy any calls to the \PDO methods to the actual + * PDO connection object. + */ + public function __call($name, $arguments) { + return call_user_func_array(array($this->connection, $name), $arguments); + } + /** * Destroys this Connection object. * @@ -346,7 +366,7 @@ abstract class DatabaseConnection extends PDO { // The Statement class attribute only accepts a new value that presents a // proper callable, so we reset it to PDOStatement. if (!empty($this->statementClass)) { - $this->setAttribute(PDO::ATTR_STATEMENT_CLASS, array('PDOStatement', array())); + $this->connection->setAttribute(PDO::ATTR_STATEMENT_CLASS, array('PDOStatement', array())); } $this->schema = NULL; } @@ -521,7 +541,7 @@ abstract class DatabaseConnection extends PDO { $query = $this->prefixTables($query); // Call PDO::prepare. - return parent::prepare($query); + return $this->connection->prepare($query); } /** @@ -733,7 +753,7 @@ abstract class DatabaseConnection extends PDO { case Database::RETURN_AFFECTED: return $stmt->rowCount(); case Database::RETURN_INSERT_ID: - return $this->lastInsertId(); + return $this->connection->lastInsertId(); case Database::RETURN_NULL: return; default: @@ -1116,7 +1136,7 @@ abstract class DatabaseConnection extends PDO { $rolled_back_other_active_savepoints = TRUE; } } - parent::rollBack(); + $this->connection->rollBack(); if ($rolled_back_other_active_savepoints) { throw new DatabaseTransactionOutOfOrderException(); } @@ -1144,7 +1164,7 @@ abstract class DatabaseConnection extends PDO { $this->query('SAVEPOINT ' . $name); } else { - parent::beginTransaction(); + $this->connection->beginTransaction(); } $this->transactionLayers[$name] = $name; } @@ -1195,7 +1215,7 @@ abstract class DatabaseConnection extends PDO { // If there are no more layers left then we should commit. unset($this->transactionLayers[$name]); if (empty($this->transactionLayers)) { - if (!parent::commit()) { + if (!$this->connection->commit()) { throw new DatabaseTransactionCommitFailedException(); } } @@ -1279,7 +1299,7 @@ abstract class DatabaseConnection extends PDO { * Returns the version of the database server. */ public function version() { - return $this->getAttribute(PDO::ATTR_SERVER_VERSION); + return $this->connection->getAttribute(PDO::ATTR_SERVER_VERSION); } /** @@ -1724,12 +1744,16 @@ abstract class Database { * * @param $key * The connection key. + * @param $close + * Whether to close the connection. * @return * TRUE in case of success, FALSE otherwise. */ - final public static function removeConnection($key) { + final public static function removeConnection($key, $close = TRUE) { if (isset(self::$databaseInfo[$key])) { - self::closeConnection(NULL, $key); + if ($close) { + self::closeConnection(NULL, $key); + } unset(self::$databaseInfo[$key]); return TRUE; } diff --git a/frontend/drupal/includes/database/mysql/database.inc b/frontend/drupal/includes/database/mysql/database.inc index 00df3c13e..b83611198 100644 --- a/frontend/drupal/includes/database/mysql/database.inc +++ b/frontend/drupal/includes/database/mysql/database.inc @@ -345,10 +345,10 @@ class DatabaseConnection_mysql extends DatabaseConnection { // certain one has been set; otherwise, MySQL defaults to 'utf8_general_ci' // for UTF-8. if (!empty($connection_options['collation'])) { - $this->exec('SET NAMES ' . $charset . ' COLLATE ' . $connection_options['collation']); + $this->connection->exec('SET NAMES ' . $charset . ' COLLATE ' . $connection_options['collation']); } else { - $this->exec('SET NAMES ' . $charset); + $this->connection->exec('SET NAMES ' . $charset); } // Set MySQL init_commands if not already defined. Default Drupal's MySQL @@ -366,7 +366,7 @@ class DatabaseConnection_mysql extends DatabaseConnection { $sql_mode = 'REAL_AS_FLOAT,PIPES_AS_CONCAT,ANSI_QUOTES,IGNORE_SPACE,STRICT_TRANS_TABLES,STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO'; // NO_AUTO_CREATE_USER was removed in MySQL 8.0.11 // https://dev.mysql.com/doc/relnotes/mysql/8.0/en/news-8-0-11.html#mysqld-8-0-11-deprecation-removal - if (version_compare($this->getAttribute(PDO::ATTR_SERVER_VERSION), '8.0.11', '<')) { + if (version_compare($this->connection->getAttribute(PDO::ATTR_SERVER_VERSION), '8.0.11', '<')) { $sql_mode .= ',NO_AUTO_CREATE_USER'; } $connection_options['init_commands'] += array( @@ -375,7 +375,7 @@ class DatabaseConnection_mysql extends DatabaseConnection { // Execute initial commands. foreach ($connection_options['init_commands'] as $sql) { - $this->exec($sql); + $this->connection->exec($sql); } } @@ -536,7 +536,7 @@ class DatabaseConnection_mysql extends DatabaseConnection { // If there are no more layers left then we should commit. unset($this->transactionLayers[$name]); if (empty($this->transactionLayers)) { - if (!PDO::commit()) { + if (!$this->doCommit()) { throw new DatabaseTransactionCommitFailedException(); } } @@ -559,7 +559,7 @@ class DatabaseConnection_mysql extends DatabaseConnection { $this->transactionLayers = array(); // We also have to explain to PDO that the transaction stack has // been cleaned-up. - PDO::commit(); + $this->doCommit(); } else { throw $e; @@ -569,6 +569,53 @@ class DatabaseConnection_mysql extends DatabaseConnection { } } + /** + * Do the actual commit, including a workaround for PHP 8 behaviour changes. + * + * @return bool + * Success or otherwise of the commit. + */ + protected function doCommit() { + if ($this->connection->inTransaction()) { + return $this->connection->commit(); + } + else { + // In PHP 8.0 a PDOException is thrown when a commit is attempted with no + // transaction active. In previous PHP versions this failed silently. + return TRUE; + } + } + + /** + * {@inheritdoc} + */ + public function rollback($savepoint_name = 'drupal_transaction') { + // MySQL will automatically commit transactions when tables are altered or + // created (DDL transactions are not supported). Prevent triggering an + // exception to ensure that the error that has caused the rollback is + // properly reported. + if (!$this->connection->inTransaction()) { + // Before PHP 8 $this->connection->inTransaction() will return TRUE and + // $this->connection->rollback() does not throw an exception; the + // following code is unreachable. + + // If \DatabaseConnection::rollback() would throw an + // exception then continue to throw an exception. + if (!$this->inTransaction()) { + throw new DatabaseTransactionNoActiveException(); + } + // A previous rollback to an earlier savepoint may mean that the savepoint + // in question has already been accidentally committed. + if (!isset($this->transactionLayers[$savepoint_name])) { + throw new DatabaseTransactionNoActiveException(); + } + + trigger_error('Rollback attempted when there is no active transaction. This can cause data integrity issues.', E_USER_WARNING); + return; + } + return parent::rollback($savepoint_name); + } + public function utf8mb4IsConfigurable() { return TRUE; } @@ -579,7 +626,7 @@ class DatabaseConnection_mysql extends DatabaseConnection { public function utf8mb4IsSupported() { // Ensure that the MySQL driver supports utf8mb4 encoding. - $version = $this->getAttribute(PDO::ATTR_CLIENT_VERSION); + $version = $this->connection->getAttribute(PDO::ATTR_CLIENT_VERSION); if (strpos($version, 'mysqlnd') !== FALSE) { // The mysqlnd driver supports utf8mb4 starting at version 5.0.9. $version = preg_replace('/^\D+([\d.]+).*/', '$1', $version); diff --git a/frontend/drupal/includes/database/pgsql/database.inc b/frontend/drupal/includes/database/pgsql/database.inc index fb3d0ab51..96ffc1d3e 100644 --- a/frontend/drupal/includes/database/pgsql/database.inc +++ b/frontend/drupal/includes/database/pgsql/database.inc @@ -66,11 +66,11 @@ class DatabaseConnection_pgsql extends DatabaseConnection { parent::__construct($dsn, $connection_options['username'], $connection_options['password'], $connection_options['pdo']); // Force PostgreSQL to use the UTF-8 character set by default. - $this->exec("SET NAMES 'UTF8'"); + $this->connection->exec("SET NAMES 'UTF8'"); // Execute PostgreSQL init_commands. if (isset($connection_options['init_commands'])) { - $this->exec(implode('; ', $connection_options['init_commands'])); + $this->connection->exec(implode('; ', $connection_options['init_commands'])); } } @@ -117,7 +117,7 @@ class DatabaseConnection_pgsql extends DatabaseConnection { case Database::RETURN_AFFECTED: return $stmt->rowCount(); case Database::RETURN_INSERT_ID: - return $this->lastInsertId($options['sequence_name']); + return $this->connection->lastInsertId($options['sequence_name']); case Database::RETURN_NULL: return; default: diff --git a/frontend/drupal/includes/database/select.inc b/frontend/drupal/includes/database/select.inc index 84098bdf7..674c6b53a 100644 --- a/frontend/drupal/includes/database/select.inc +++ b/frontend/drupal/includes/database/select.inc @@ -964,7 +964,7 @@ class SelectQuery extends Query implements SelectQueryInterface { */ protected $forUpdate = FALSE; - public function __construct($table, $alias = NULL, DatabaseConnection $connection, $options = array()) { + public function __construct($table, $alias, DatabaseConnection $connection, $options = array()) { $options['return'] = Database::RETURN_STATEMENT; parent::__construct($connection, $options); $this->where = new DatabaseCondition('AND'); diff --git a/frontend/drupal/includes/database/sqlite/database.inc b/frontend/drupal/includes/database/sqlite/database.inc index c50f08ec5..2cf83ccb5 100644 --- a/frontend/drupal/includes/database/sqlite/database.inc +++ b/frontend/drupal/includes/database/sqlite/database.inc @@ -121,7 +121,7 @@ class DatabaseConnection_sqlite extends DatabaseConnection { // Execute sqlite init_commands. if (isset($connection_options['init_commands'])) { - $this->exec(implode('; ', $connection_options['init_commands'])); + $this->connection->exec(implode('; ', $connection_options['init_commands'])); } } @@ -259,7 +259,7 @@ class DatabaseConnection_sqlite extends DatabaseConnection { * expose this function to the world. */ public function PDOPrepare($query, array $options = array()) { - return parent::prepare($query, $options); + return $this->connection->prepare($query, $options); } public function queryRange($query, $from, $count, array $args = array(), array $options = array()) { @@ -350,7 +350,7 @@ class DatabaseConnection_sqlite extends DatabaseConnection { } } if ($this->supportsTransactions()) { - PDO::rollBack(); + $this->connection->rollBack(); } } @@ -365,7 +365,7 @@ class DatabaseConnection_sqlite extends DatabaseConnection { throw new DatabaseTransactionNameNonUniqueException($name . " is already in use."); } if (!$this->inTransaction()) { - PDO::beginTransaction(); + $this->connection->beginTransaction(); } $this->transactionLayers[$name] = $name; } @@ -390,9 +390,9 @@ class DatabaseConnection_sqlite extends DatabaseConnection { // If there was any rollback() we should roll back whole transaction. if ($this->willRollback) { $this->willRollback = FALSE; - PDO::rollBack(); + $this->connection->rollBack(); } - elseif (!PDO::commit()) { + elseif (!$this->connection->commit()) { throw new DatabaseTransactionCommitFailedException(); } } diff --git a/frontend/drupal/includes/errors.inc b/frontend/drupal/includes/errors.inc index 3548d1fd8..4401ebe87 100644 --- a/frontend/drupal/includes/errors.inc +++ b/frontend/drupal/includes/errors.inc @@ -48,11 +48,8 @@ function drupal_error_levels() { * The filename that the error was raised in. * @param $line * The line number the error was raised at. - * @param $context - * An array that points to the active symbol table at the point the error - * occurred. */ -function _drupal_error_handler_real($error_level, $message, $filename, $line, $context) { +function _drupal_error_handler_real($error_level, $message, $filename, $line) { if ($error_level & error_reporting()) { $types = drupal_error_levels(); list($severity_msg, $severity_level) = $types[$error_level]; diff --git a/frontend/drupal/includes/menu.inc b/frontend/drupal/includes/menu.inc index 22e6dba97..0f38d6f50 100644 --- a/frontend/drupal/includes/menu.inc +++ b/frontend/drupal/includes/menu.inc @@ -317,7 +317,7 @@ define('MENU_PREFERRED_LINK', '1cf698d64d1aa4b83907cf6ed55db3a7f8e92c91'); * actually exists. This list of 'masks' is built in menu_rebuild(). * * @param $parts - * An array of path parts; for the above example, + * An array of path parts; for the above example, * array('node', '12345', 'edit'). * * @return @@ -2595,7 +2595,7 @@ function menu_get_active_breadcrumb() { // Don't show a link to the current page in the breadcrumb trail. $end = end($active_trail); - if ($item['href'] == $end['href']) { + if (is_array($end) && $item['href'] == $end['href']) { array_pop($active_trail); } diff --git a/frontend/drupal/includes/session.inc b/frontend/drupal/includes/session.inc index 11c77c4c1..a4ce54b7d 100644 --- a/frontend/drupal/includes/session.inc +++ b/frontend/drupal/includes/session.inc @@ -284,6 +284,20 @@ function drupal_session_start() { // Save current session data before starting it, as PHP will destroy it. $session_data = isset($_SESSION) ? $_SESSION : NULL; + // Apply any overrides to the session cookie params. + $params = $original_params = session_get_cookie_params(); + // PHP settings for samesite will be handled by _drupal_cookie_params(). + unset($params['samesite']); + $params = _drupal_cookie_params($params); + if ($params !== $original_params) { + if (\PHP_VERSION_ID >= 70300) { + session_set_cookie_params($params); + } + else { + session_set_cookie_params($params['lifetime'], $params['path'], $params['domain'], $params['secure'], $params['httponly']); + } + } + session_start(); drupal_session_started(TRUE); @@ -323,7 +337,14 @@ function drupal_session_commit() { $insecure_session_name = substr(session_name(), 1); $params = session_get_cookie_params(); $expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0; - setcookie($insecure_session_name, $_COOKIE[$insecure_session_name], $expire, $params['path'], $params['domain'], FALSE, $params['httponly']); + $options = array( + 'expires' => $expire, + 'path' => $params['path'], + 'domain' => $params['domain'], + 'secure' => FALSE, + 'httponly' => $params['httponly'], + ); + drupal_setcookie($insecure_session_name, $_COOKIE[$insecure_session_name], $options); } } // Write the session data. @@ -365,7 +386,14 @@ function drupal_session_regenerate() { // $params['lifetime'] seconds from the current request. If it is not set, // it will expire when the browser is closed. $expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0; - setcookie($insecure_session_name, $session_id, $expire, $params['path'], $params['domain'], FALSE, $params['httponly']); + $options = array( + 'expires' => $expire, + 'path' => $params['path'], + 'domain' => $params['domain'], + 'secure' => FALSE, + 'httponly' => $params['httponly'], + ); + drupal_setcookie($insecure_session_name, $session_id, $options); $_COOKIE[$insecure_session_name] = $session_id; } @@ -380,7 +408,14 @@ function drupal_session_regenerate() { if (isset($old_session_id)) { $params = session_get_cookie_params(); $expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0; - setcookie(session_name(), session_id(), $expire, $params['path'], $params['domain'], $params['secure'], $params['httponly']); + $options = array( + 'expires' => $expire, + 'path' => $params['path'], + 'domain' => $params['domain'], + 'secure' => $params['secure'], + 'httponly' => $params['httponly'], + ); + drupal_setcookie(session_name(), session_id(), $options); $fields = array('sid' => session_id()); if ($is_https) { $fields['ssid'] = session_id(); @@ -488,7 +523,14 @@ function _drupal_session_delete_cookie($name, $secure = NULL) { if ($secure !== NULL) { $params['secure'] = $secure; } - setcookie($name, '', REQUEST_TIME - 3600, $params['path'], $params['domain'], $params['secure'], $params['httponly']); + $options = array( + 'expires' => REQUEST_TIME - 3600, + 'path' => $params['path'], + 'domain' => $params['domain'], + 'secure' => $params['secure'], + 'httponly' => $params['httponly'], + ); + drupal_setcookie($name, '', $options); unset($_COOKIE[$name]); } } diff --git a/frontend/drupal/misc/ajax.js b/frontend/drupal/misc/ajax.js index 79a4e9eb6..a809f5738 100644 --- a/frontend/drupal/misc/ajax.js +++ b/frontend/drupal/misc/ajax.js @@ -408,7 +408,7 @@ Drupal.ajax.prototype.beforeSend = function (xmlhttprequest, options) { // Insert progressbar or throbber. if (this.progress.type == 'bar') { - var progressBar = new Drupal.progressBar('ajax-progress-' + this.element.id, eval(this.progress.update_callback), this.progress.method, eval(this.progress.error_callback)); + var progressBar = new Drupal.progressBar('ajax-progress-' + this.element.id, $.noop, this.progress.method, $.noop); if (this.progress.message) { progressBar.setProgress(-1, this.progress.message); } diff --git a/frontend/drupal/modules/aggregator/aggregator.info b/frontend/drupal/modules/aggregator/aggregator.info index 9fe93e0c4..dac556160 100644 --- a/frontend/drupal/modules/aggregator/aggregator.info +++ b/frontend/drupal/modules/aggregator/aggregator.info @@ -7,7 +7,7 @@ files[] = aggregator.test configure = admin/config/services/aggregator/settings stylesheets[all][] = aggregator.css -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/aggregator/tests/aggregator_test.info b/frontend/drupal/modules/aggregator/tests/aggregator_test.info index 1fc2daa31..840f68f53 100644 --- a/frontend/drupal/modules/aggregator/tests/aggregator_test.info +++ b/frontend/drupal/modules/aggregator/tests/aggregator_test.info @@ -5,7 +5,7 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/block/block.info b/frontend/drupal/modules/block/block.info index 484a4a0f2..95907c03b 100644 --- a/frontend/drupal/modules/block/block.info +++ b/frontend/drupal/modules/block/block.info @@ -6,7 +6,7 @@ core = 7.x files[] = block.test configure = admin/structure/block -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/block/tests/block_test.info b/frontend/drupal/modules/block/tests/block_test.info index 5b1b0d2d6..1a0b512fb 100644 --- a/frontend/drupal/modules/block/tests/block_test.info +++ b/frontend/drupal/modules/block/tests/block_test.info @@ -5,7 +5,7 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/block/tests/themes/block_test_theme/block_test_theme.info b/frontend/drupal/modules/block/tests/themes/block_test_theme/block_test_theme.info index a10950ad4..87a8d6262 100644 --- a/frontend/drupal/modules/block/tests/themes/block_test_theme/block_test_theme.info +++ b/frontend/drupal/modules/block/tests/themes/block_test_theme/block_test_theme.info @@ -13,7 +13,7 @@ regions[footer] = Footer regions[highlighted] = Highlighted regions[help] = Help -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/blog/blog.info b/frontend/drupal/modules/blog/blog.info index 9f626f82a..80ca6e702 100644 --- a/frontend/drupal/modules/blog/blog.info +++ b/frontend/drupal/modules/blog/blog.info @@ -5,7 +5,7 @@ version = VERSION core = 7.x files[] = blog.test -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/book/book.info b/frontend/drupal/modules/book/book.info index 3a611bc4f..5f9d340aa 100644 --- a/frontend/drupal/modules/book/book.info +++ b/frontend/drupal/modules/book/book.info @@ -7,7 +7,7 @@ files[] = book.test configure = admin/content/book/settings stylesheets[all][] = book.css -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/book/book.test b/frontend/drupal/modules/book/book.test index 81f4524ac..448dc23a9 100644 --- a/frontend/drupal/modules/book/book.test +++ b/frontend/drupal/modules/book/book.test @@ -101,7 +101,7 @@ class BookTestCase extends DrupalWebTestCase { // Check that book pages display along with the correct outlines and // previous/next links. - $this->checkBookNode($book, array($nodes[0], $nodes[3], $nodes[4]), FALSE, FALSE, $nodes[0], array()); + $this->checkBookNode($book, array($nodes[0], $nodes[3], $nodes[4]), FALSE, FALSE, $nodes[0]); $this->checkBookNode($nodes[0], array($nodes[1], $nodes[2]), $book, $book, $nodes[1], array($book)); $this->checkBookNode($nodes[1], NULL, $nodes[0], $nodes[0], $nodes[2], array($book, $nodes[0])); $this->checkBookNode($nodes[2], NULL, $nodes[1], $nodes[0], $nodes[3], array($book, $nodes[0])); @@ -124,7 +124,7 @@ class BookTestCase extends DrupalWebTestCase { // First we must set $this->book to the second book, so that the // correct regex will be generated for testing the outline. $this->book = $other_book; - $this->checkBookNode($other_book, array($node), FALSE, FALSE, $node, array()); + $this->checkBookNode($other_book, array($node), FALSE, FALSE, $node); $this->checkBookNode($node, NULL, $other_book, $other_book, FALSE, array($other_book)); } @@ -144,9 +144,9 @@ class BookTestCase extends DrupalWebTestCase { * @param $next * (optional) Next link node. Defaults to FALSE. * @param $breadcrumb - * The nodes that should be displayed in the breadcrumb. + * (optional) The nodes that should be displayed in the breadcrumb. */ - function checkBookNode($node, $nodes, $previous = FALSE, $up = FALSE, $next = FALSE, array $breadcrumb) { + function checkBookNode($node, $nodes = NULL, $previous = FALSE, $up = FALSE, $next = FALSE, array $breadcrumb = array()) { // $number does not use drupal_static as it should not be reset // since it uniquely identifies each call to checkBookNode(). static $number = 0; diff --git a/frontend/drupal/modules/color/color.info b/frontend/drupal/modules/color/color.info index 23be4ace9..3906b758f 100644 --- a/frontend/drupal/modules/color/color.info +++ b/frontend/drupal/modules/color/color.info @@ -5,7 +5,7 @@ version = VERSION core = 7.x files[] = color.test -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/comment/comment.info b/frontend/drupal/modules/comment/comment.info index 68c6c2393..e7fcca1dc 100644 --- a/frontend/drupal/modules/comment/comment.info +++ b/frontend/drupal/modules/comment/comment.info @@ -9,7 +9,7 @@ files[] = comment.test configure = admin/content/comment stylesheets[all][] = comment.css -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/contact/contact.info b/frontend/drupal/modules/contact/contact.info index b6d67dd50..d12f98cb1 100644 --- a/frontend/drupal/modules/contact/contact.info +++ b/frontend/drupal/modules/contact/contact.info @@ -6,7 +6,7 @@ core = 7.x files[] = contact.test configure = admin/structure/contact -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/contextual/contextual.info b/frontend/drupal/modules/contextual/contextual.info index c881710cf..165551cba 100644 --- a/frontend/drupal/modules/contextual/contextual.info +++ b/frontend/drupal/modules/contextual/contextual.info @@ -5,7 +5,7 @@ version = VERSION core = 7.x files[] = contextual.test -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/dashboard/dashboard.info b/frontend/drupal/modules/dashboard/dashboard.info index f4c2db68a..fd30f8401 100644 --- a/frontend/drupal/modules/dashboard/dashboard.info +++ b/frontend/drupal/modules/dashboard/dashboard.info @@ -7,7 +7,7 @@ files[] = dashboard.test dependencies[] = block configure = admin/dashboard/customize -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/dblog/dblog.info b/frontend/drupal/modules/dblog/dblog.info index da068eb78..5ca79fca0 100644 --- a/frontend/drupal/modules/dblog/dblog.info +++ b/frontend/drupal/modules/dblog/dblog.info @@ -5,7 +5,7 @@ version = VERSION core = 7.x files[] = dblog.test -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/field/field.info b/frontend/drupal/modules/field/field.info index c8c4585e7..48bd97047 100644 --- a/frontend/drupal/modules/field/field.info +++ b/frontend/drupal/modules/field/field.info @@ -11,7 +11,7 @@ dependencies[] = field_sql_storage required = TRUE stylesheets[all][] = theme/field.css -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/field/modules/field_sql_storage/field_sql_storage.info b/frontend/drupal/modules/field/modules/field_sql_storage/field_sql_storage.info index ed407fe88..9ce8bf4e9 100644 --- a/frontend/drupal/modules/field/modules/field_sql_storage/field_sql_storage.info +++ b/frontend/drupal/modules/field/modules/field_sql_storage/field_sql_storage.info @@ -7,7 +7,7 @@ dependencies[] = field files[] = field_sql_storage.test required = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/field/modules/field_sql_storage/field_sql_storage.module b/frontend/drupal/modules/field/modules/field_sql_storage/field_sql_storage.module index 842893ad7..deb08d0da 100644 --- a/frontend/drupal/modules/field/modules/field_sql_storage/field_sql_storage.module +++ b/frontend/drupal/modules/field/modules/field_sql_storage/field_sql_storage.module @@ -434,6 +434,81 @@ function field_sql_storage_field_storage_load($entity_type, $entities, $age, $fi } } +/** + * Callback for array_filter(). + */ +function _field_sql_storage_write_compare_filter_callback($value) { + return NULL !== $value && '' !== $value; +} + +/** + * Cleanup field values for later values comparison. + * + * @param array $field + * Field info as returned by field_info_field_by_id(). + * + * @param array $array + * Field values to cleanup. + * + * @return array + * Filtered values. + */ +function _field_sql_storage_write_compare_filter($field, $array) { + foreach ($array as $language => $items) { + if (empty($items)) { + unset($array[$language]); + } + else { + foreach ($items as $delta => $item) { + // This should not happen but some modules provide invalid data to the + // field API. + if (!is_array($item)) { + continue; + } + // Let's start by pruning empty values and non storable values. + $array[$language][$delta] = array_filter(array_intersect_key($item, $field['columns']), '_field_sql_storage_write_compare_filter_callback'); + // Ordering is important because for widget elements and loaded columns + // from database order might differ and give false positives on field + // value change, especially with complex fields such as image fields. + ksort($array[$language][$delta]); + } + } + } + return $array; +} + +/** + * Compare a single field value for both entities and tell us if it changed. + * + * @param array $field + * Loaded field structure. + * @param object $entity1 + * First entity to compare. + * @param object $entity2 + * Second entity to compare. + * + * @return bool + * True if field value changed, false otherwise. + */ +function _field_sql_storage_write_compare($field, $entity1, $entity2) { + $field_name = $field['field_name']; + if (empty($entity1->$field_name) && empty($entity2->$field_name)) { + // Both are empty we can safely assume that it did not change. + return FALSE; + } + if (!isset($entity1->$field_name) || !isset($entity2->$field_name)) { + // One of them is missing but not the other the value changed. + return TRUE; + } + // We need to proceed to deep array comparison, but we cannot do it naively: + // in most cases the field values come from the edit form, and some Form API + // widget values that are not field columns may be present. We need to clean + // up both original and new field values before comparison. + $items1 = _field_sql_storage_write_compare_filter($field, (array) $entity1->$field_name); + $items2 = _field_sql_storage_write_compare_filter($field, (array) $entity2->$field_name); + return $items1 != $items2; +} + /** * Implements hook_field_storage_write(). */ @@ -443,8 +518,29 @@ function field_sql_storage_field_storage_write($entity_type, $entity, $op, $fiel $vid = $id; } + // Check if the given entity is a new revision or not. In case of a new + // revision creation, we cannot skip any field. + if (!empty($vid) && !empty($entity->original)) { + list(, $original_vid) = entity_extract_ids($entity_type, $entity->original); + if (NULL === $original_vid) { + $original_vid = $id; + } + $is_new_revision = $original_vid != $vid; + } + else { + $is_new_revision = FALSE; + } + + // Allow this optimization to be optional. + $skip_unchanged_fields = variable_get('field_sql_storage_skip_writing_unchanged_fields', FALSE); + foreach ($fields as $field_id) { $field = field_info_field_by_id($field_id); + + if ($skip_unchanged_fields && !$is_new_revision && !empty($entity->original) && !_field_sql_storage_write_compare($field, $entity, $entity->original)) { + continue; + } + $field_name = $field['field_name']; $table_name = _field_sql_storage_tablename($field); $revision_name = _field_sql_storage_revision_tablename($field); diff --git a/frontend/drupal/modules/field/modules/field_sql_storage/field_sql_storage.test b/frontend/drupal/modules/field/modules/field_sql_storage/field_sql_storage.test index b2eb50652..e46677be9 100644 --- a/frontend/drupal/modules/field/modules/field_sql_storage/field_sql_storage.test +++ b/frontend/drupal/modules/field/modules/field_sql_storage/field_sql_storage.test @@ -281,6 +281,69 @@ class FieldSqlStorageTestCase extends DrupalWebTestCase { $this->assertEqual($count, 1, 'NULL field translation is wiped.'); } + /** + * Tests the expected return values of _field_sql_storage_write_compare(). + */ + public function testFieldCompareDataModification() { + $langcode = LANGUAGE_NONE; + $field_info = field_info_field($this->field_name); + + // Make sure we have 2 sample field values that are unique. + $value1 = 0; + $value2 = 0; + while ($value1 == $value2) { + $value1 = mt_rand(); + $value2 = (string) mt_rand(); + } + + // Create the 2 entities to compare. + $entity = field_test_create_stub_entity(); + $entity->{$this->field_name}[$langcode][]['value'] = $value1; + $entity1 = clone $entity; + $entity2 = clone $entity; + + // Make sure that it correctly compares identical entities. + $this->assert(!_field_sql_storage_write_compare($field_info, $entity1, $entity2), 'The entities are identical.'); + + // Compare to an empty object. + $this->assert(_field_sql_storage_write_compare($field_info, $entity1, new stdClass()), 'The entity is not the same as an empty object.'); + + // Change one of the values. + $entity2->{$this->field_name}[$langcode][0]['value'] = $value2; + $this->assert(_field_sql_storage_write_compare($field_info, $entity1, $entity2), 'The values are not the same.'); + + // Reset $entity2. + $entity2 = clone $entity; + + // Duplicate the value on one of the entities. + $entity1->{$this->field_name}[$langcode][]['value'] = $value1; + $this->assert(_field_sql_storage_write_compare($field_info, $entity1, $entity2), 'The fields do not have the same number of values.'); + + // Add a second value to both entities. + $entity2->{$this->field_name}[$langcode][]['value'] = $value2; + $this->assert(_field_sql_storage_write_compare($field_info, $entity1, $entity2), 'The values are not the same.'); + + // Replace the array containing the value with the actual value. + $entity2->{$this->field_name}[$langcode] = $entity2->{$this->field_name}[$langcode][0]; + $this->assert(_field_sql_storage_write_compare($field_info, $entity1, $entity2), 'The array to hold field values is replaced by the value.'); + + // Null one value. + $entity2->{$this->field_name}[$langcode] = NULL; + $this->assert(_field_sql_storage_write_compare($field_info, $entity1, $entity2), 'One field is NULL and the other is not.'); + + // Null both values. + $entity1->{$this->field_name}[$langcode] = NULL; + $this->assert(!_field_sql_storage_write_compare($field_info, $entity1, $entity2), 'Both fields are NULL.'); + + // Unset one of the fields. + unset($entity2->{$this->field_name}); + $this->assert(_field_sql_storage_write_compare($field_info, $entity1, $entity2), 'One field structure is unset.'); + + // Unset both of the fields. + unset($entity1->{$this->field_name}); + $this->assert(!_field_sql_storage_write_compare($field_info, $entity1, $entity2), 'Both field structures are unset.'); + } + /** * Test trying to update a field with data. */ diff --git a/frontend/drupal/modules/field/modules/list/list.info b/frontend/drupal/modules/field/modules/list/list.info index bc867c0ba..92bbf7233 100644 --- a/frontend/drupal/modules/field/modules/list/list.info +++ b/frontend/drupal/modules/field/modules/list/list.info @@ -7,7 +7,7 @@ dependencies[] = field dependencies[] = options files[] = tests/list.test -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/field/modules/list/tests/list_test.info b/frontend/drupal/modules/field/modules/list/tests/list_test.info index f344a39f9..5854cb3a1 100644 --- a/frontend/drupal/modules/field/modules/list/tests/list_test.info +++ b/frontend/drupal/modules/field/modules/list/tests/list_test.info @@ -5,7 +5,7 @@ package = Testing version = VERSION hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/field/modules/number/number.info b/frontend/drupal/modules/field/modules/number/number.info index 545ccb105..54ea521ec 100644 --- a/frontend/drupal/modules/field/modules/number/number.info +++ b/frontend/drupal/modules/field/modules/number/number.info @@ -6,7 +6,7 @@ core = 7.x dependencies[] = field files[] = number.test -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/field/modules/options/options.info b/frontend/drupal/modules/field/modules/options/options.info index 8e42a49a4..d3e47aa9e 100644 --- a/frontend/drupal/modules/field/modules/options/options.info +++ b/frontend/drupal/modules/field/modules/options/options.info @@ -6,7 +6,7 @@ core = 7.x dependencies[] = field files[] = options.test -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/field/modules/text/text.info b/frontend/drupal/modules/field/modules/text/text.info index a1145184b..3b7a36f0e 100644 --- a/frontend/drupal/modules/field/modules/text/text.info +++ b/frontend/drupal/modules/field/modules/text/text.info @@ -7,7 +7,7 @@ dependencies[] = field files[] = text.test required = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/field/tests/field_test.info b/frontend/drupal/modules/field/tests/field_test.info index 242fe55a5..324e12093 100644 --- a/frontend/drupal/modules/field/tests/field_test.info +++ b/frontend/drupal/modules/field/tests/field_test.info @@ -6,7 +6,7 @@ files[] = field_test.entity.inc version = VERSION hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/field/tests/field_test.storage.inc b/frontend/drupal/modules/field/tests/field_test.storage.inc index 03eae4a6f..7a6201d31 100644 --- a/frontend/drupal/modules/field/tests/field_test.storage.inc +++ b/frontend/drupal/modules/field/tests/field_test.storage.inc @@ -240,111 +240,6 @@ function field_test_field_storage_delete_revision($entity_type, $entity, $fields _field_test_storage_data($data); } -/** - * Implements hook_field_storage_query(). - */ -function field_test_field_storage_query($field_id, $conditions, $count, &$cursor = NULL, $age) { - $data = _field_test_storage_data(); - - $load_current = $age == FIELD_LOAD_CURRENT; - - $field = field_info_field_by_id($field_id); - $field_columns = array_keys($field['columns']); - - $field_data = $data[$field['id']]; - $sub_table = $load_current ? 'current' : 'revisions'; - // We need to sort records by entity type and entity id. - usort($field_data[$sub_table], '_field_test_field_storage_query_sort_helper'); - - // Initialize results array. - $return = array(); - $entity_count = 0; - $rows_count = 0; - $rows_total = count($field_data[$sub_table]); - $skip = $cursor; - $skipped = 0; - - foreach ($field_data[$sub_table] as $row) { - if ($count != FIELD_QUERY_NO_LIMIT && $entity_count >= $count) { - break; - } - - if ($row->field_id == $field['id']) { - $match = TRUE; - $condition_deleted = FALSE; - // Add conditions. - foreach ($conditions as $condition) { - @list($column, $value, $operator) = $condition; - if (empty($operator)) { - $operator = is_array($value) ? 'IN' : '='; - } - switch ($operator) { - case '=': - $match = $match && $row->{$column} == $value; - break; - case '<>': - case '<': - case '<=': - case '>': - case '>=': - eval('$match = $match && ' . $row->{$column} . ' ' . $operator . ' '. $value); - break; - case 'IN': - $match = $match && in_array($row->{$column}, $value); - break; - case 'NOT IN': - $match = $match && !in_array($row->{$column}, $value); - break; - case 'BETWEEN': - $match = $match && $row->{$column} >= $value[0] && $row->{$column} <= $value[1]; - break; - case 'STARTS_WITH': - case 'ENDS_WITH': - case 'CONTAINS': - // Not supported. - $match = FALSE; - break; - } - // Track condition on 'deleted'. - if ($column == 'deleted') { - $condition_deleted = TRUE; - } - } - - // Exclude deleted data unless we have a condition on it. - if (!$condition_deleted && $row->deleted) { - $match = FALSE; - } - - if ($match) { - if (!isset($skip) || $skipped >= $skip) { - $cursor++; - // If querying all revisions and the entity type has revisions, we need - // to key the results by revision_ids. - $entity_type = entity_get_info($row->type); - $id = ($load_current || empty($entity_type['entity keys']['revision'])) ? $row->entity_id : $row->revision_id; - - if (!isset($return[$row->type][$id])) { - $return[$row->type][$id] = entity_create_stub_entity($row->type, array($row->entity_id, $row->revision_id, $row->bundle)); - $entity_count++; - } - } - else { - $skipped++; - } - } - } - $rows_count++; - - // The query is complete if we walked the whole array. - if ($count != FIELD_QUERY_NO_LIMIT && $rows_count >= $rows_total) { - $cursor = FIELD_QUERY_COMPLETE; - } - } - - return $return; -} - /** * Sort helper for field_test_field_storage_query(). * diff --git a/frontend/drupal/modules/field_ui/field_ui.info b/frontend/drupal/modules/field_ui/field_ui.info index e66bd97a7..555d69283 100644 --- a/frontend/drupal/modules/field_ui/field_ui.info +++ b/frontend/drupal/modules/field_ui/field_ui.info @@ -6,7 +6,7 @@ core = 7.x dependencies[] = field files[] = field_ui.test -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/file/file.field.inc b/frontend/drupal/modules/file/file.field.inc index fc1a1df20..ddb4f841f 100644 --- a/frontend/drupal/modules/file/file.field.inc +++ b/frontend/drupal/modules/file/file.field.inc @@ -593,7 +593,7 @@ function file_field_widget_uri($field, $instance, $data = array()) { /** * The #value_callback for the file_generic field element. */ -function file_field_widget_value($element, $input = FALSE, $form_state) { +function file_field_widget_value($element, $input = FALSE, $form_state = array()) { if ($input) { // Checkboxes lose their value when empty. // If the display field is present make sure its unchecked value is saved. @@ -955,17 +955,14 @@ function theme_file_upload_help($variables) { if (isset($upload_validators['file_validate_image_resolution'])) { $max = $upload_validators['file_validate_image_resolution'][0]; $min = $upload_validators['file_validate_image_resolution'][1]; - if ($min && $max && $min == $max) { - $descriptions[] = t('Images must be exactly !size pixels.', array('!size' => '' . $max . '')); - } - elseif ($min && $max) { - $descriptions[] = t('Images must be between !min and !max pixels.', array('!min' => '' . $min . '', '!max' => '' . $max . '')); + if ($min && $max) { + $descriptions[] = t('Images must be at least !min pixels. Images larger than !max pixels will be resized.', array('!min' => '' . $min . '', '!max' => '' . $max . '')); } elseif ($min) { - $descriptions[] = t('Images must be larger than !min pixels.', array('!min' => '' . $min . '')); + $descriptions[] = t('Images must be at least !min pixels.', array('!min' => '' . $min . '')); } elseif ($max) { - $descriptions[] = t('Images must be smaller than !max pixels.', array('!max' => '' . $max . '')); + $descriptions[] = t('Images larger than !max pixels will be resized.', array('!max' => '' . $max . '')); } } diff --git a/frontend/drupal/modules/file/file.info b/frontend/drupal/modules/file/file.info index fe0ae6dd2..92deebac9 100644 --- a/frontend/drupal/modules/file/file.info +++ b/frontend/drupal/modules/file/file.info @@ -6,7 +6,7 @@ core = 7.x dependencies[] = field files[] = tests/file.test -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/file/tests/file_module_test.info b/frontend/drupal/modules/file/tests/file_module_test.info index 040561e7d..b134008d4 100644 --- a/frontend/drupal/modules/file/tests/file_module_test.info +++ b/frontend/drupal/modules/file/tests/file_module_test.info @@ -5,7 +5,7 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/filter/filter.info b/frontend/drupal/modules/filter/filter.info index bcd85882c..2a71c6c3c 100644 --- a/frontend/drupal/modules/filter/filter.info +++ b/frontend/drupal/modules/filter/filter.info @@ -7,7 +7,7 @@ files[] = filter.test required = TRUE configure = admin/config/content/formats -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/forum/forum.info b/frontend/drupal/modules/forum/forum.info index 99253aa64..514e2557a 100644 --- a/frontend/drupal/modules/forum/forum.info +++ b/frontend/drupal/modules/forum/forum.info @@ -9,7 +9,7 @@ files[] = forum.test configure = admin/structure/forum stylesheets[all][] = forum.css -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/help/help.info b/frontend/drupal/modules/help/help.info index c410c63ea..62d626758 100644 --- a/frontend/drupal/modules/help/help.info +++ b/frontend/drupal/modules/help/help.info @@ -5,7 +5,7 @@ version = VERSION core = 7.x files[] = help.test -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/image/image.info b/frontend/drupal/modules/image/image.info index f427a74b8..6a9abc05e 100644 --- a/frontend/drupal/modules/image/image.info +++ b/frontend/drupal/modules/image/image.info @@ -7,7 +7,7 @@ dependencies[] = file files[] = image.test configure = admin/config/media/image-styles -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/image/image.test b/frontend/drupal/modules/image/image.test index 0c26ffa84..22edcaa06 100644 --- a/frontend/drupal/modules/image/image.test +++ b/frontend/drupal/modules/image/image.test @@ -1022,7 +1022,7 @@ class ImageFieldDisplayTestCase extends ImageFieldTestCase { $this->drupalGet('node/add/article'); $this->assertText(t('Files must be less than 50 KB.'), 'Image widget max file size is displayed on article form.'); $this->assertText(t('Allowed file types: ' . $test_image_extension . '.'), 'Image widget allowed file types displayed on article form.'); - $this->assertText(t('Images must be between 10x10 and 100x100 pixels.'), 'Image widget allowed resolution displayed on article form.'); + $this->assertText(t('Images must be at least 10x10 pixels. Images larger than 100x100 pixels will be resized.'), 'Image widget allowed resolution displayed on article form.'); // We have to create the article first and then edit it because the alt // and title fields do not display until the image has been attached. diff --git a/frontend/drupal/modules/image/tests/image_module_test.info b/frontend/drupal/modules/image/tests/image_module_test.info index 300bfec19..8c4c7cd91 100644 --- a/frontend/drupal/modules/image/tests/image_module_test.info +++ b/frontend/drupal/modules/image/tests/image_module_test.info @@ -6,7 +6,7 @@ core = 7.x files[] = image_module_test.module hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/locale/locale.info b/frontend/drupal/modules/locale/locale.info index aeec19f6d..a48d54d2d 100644 --- a/frontend/drupal/modules/locale/locale.info +++ b/frontend/drupal/modules/locale/locale.info @@ -6,7 +6,7 @@ core = 7.x files[] = locale.test configure = admin/config/regional/language -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/locale/tests/locale_test.info b/frontend/drupal/modules/locale/tests/locale_test.info index 1337147f6..7a682fc2d 100644 --- a/frontend/drupal/modules/locale/tests/locale_test.info +++ b/frontend/drupal/modules/locale/tests/locale_test.info @@ -5,7 +5,7 @@ package = Testing version = VERSION hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/menu/menu.info b/frontend/drupal/modules/menu/menu.info index af1c1cdb0..db77373d1 100644 --- a/frontend/drupal/modules/menu/menu.info +++ b/frontend/drupal/modules/menu/menu.info @@ -6,7 +6,7 @@ core = 7.x files[] = menu.test configure = admin/structure/menu -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/node/node.info b/frontend/drupal/modules/node/node.info index 58746503a..9e166f964 100644 --- a/frontend/drupal/modules/node/node.info +++ b/frontend/drupal/modules/node/node.info @@ -9,7 +9,7 @@ required = TRUE configure = admin/structure/types stylesheets[all][] = node.css -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/node/tests/node_access_test.info b/frontend/drupal/modules/node/tests/node_access_test.info index 06ecbee92..e023592cf 100644 --- a/frontend/drupal/modules/node/tests/node_access_test.info +++ b/frontend/drupal/modules/node/tests/node_access_test.info @@ -5,7 +5,7 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/node/tests/node_test.info b/frontend/drupal/modules/node/tests/node_test.info index ef4f350e2..b2917e831 100644 --- a/frontend/drupal/modules/node/tests/node_test.info +++ b/frontend/drupal/modules/node/tests/node_test.info @@ -5,7 +5,7 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/node/tests/node_test_exception.info b/frontend/drupal/modules/node/tests/node_test_exception.info index be07e9821..8bf4c8a14 100644 --- a/frontend/drupal/modules/node/tests/node_test_exception.info +++ b/frontend/drupal/modules/node/tests/node_test_exception.info @@ -5,7 +5,7 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/openid/openid.inc b/frontend/drupal/modules/openid/openid.inc index a1da1d0b5..4ca747164 100644 --- a/frontend/drupal/modules/openid/openid.inc +++ b/frontend/drupal/modules/openid/openid.inc @@ -142,7 +142,8 @@ function _openid_xrds_parse($raw_xml) { // For PHP version >= 5.2.11, we can use this function to protect against // malicious doctype declarations and other unexpected entity loading. // However, we will not rely on it, and reject any XML with a DOCTYPE. - $disable_entity_loader = function_exists('libxml_disable_entity_loader'); + // libxml_disable_entity_loader() is deprecated in PHP >= 8.0. + $disable_entity_loader = function_exists('libxml_disable_entity_loader') && PHP_VERSION_ID < 80000; if ($disable_entity_loader) { $load_entities = libxml_disable_entity_loader(TRUE); } diff --git a/frontend/drupal/modules/openid/openid.info b/frontend/drupal/modules/openid/openid.info index ec4003c5c..c5cb0c3c4 100644 --- a/frontend/drupal/modules/openid/openid.info +++ b/frontend/drupal/modules/openid/openid.info @@ -5,7 +5,7 @@ package = Core core = 7.x files[] = openid.test -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/openid/openid.module b/frontend/drupal/modules/openid/openid.module index a52dbc3de..f2c1b8d54 100644 --- a/frontend/drupal/modules/openid/openid.module +++ b/frontend/drupal/modules/openid/openid.module @@ -743,7 +743,7 @@ function openid_association_request($public) { return $request; } -function openid_authentication_request($claimed_id, $identity, $return_to = '', $assoc_handle = '', $service) { +function openid_authentication_request($claimed_id, $identity, $return_to, $assoc_handle, $service) { global $base_url; module_load_include('inc', 'openid'); diff --git a/frontend/drupal/modules/openid/tests/openid_test.info b/frontend/drupal/modules/openid/tests/openid_test.info index f45d60dee..8bd7fd891 100644 --- a/frontend/drupal/modules/openid/tests/openid_test.info +++ b/frontend/drupal/modules/openid/tests/openid_test.info @@ -6,7 +6,7 @@ core = 7.x dependencies[] = openid hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/overlay/overlay.info b/frontend/drupal/modules/overlay/overlay.info index fe783baa6..f45f8daed 100644 --- a/frontend/drupal/modules/overlay/overlay.info +++ b/frontend/drupal/modules/overlay/overlay.info @@ -4,7 +4,7 @@ package = Core version = VERSION core = 7.x -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/path/path.info b/frontend/drupal/modules/path/path.info index 3a3458fda..6d4d607d3 100644 --- a/frontend/drupal/modules/path/path.info +++ b/frontend/drupal/modules/path/path.info @@ -6,7 +6,7 @@ core = 7.x files[] = path.test configure = admin/config/search/path -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/php/php.info b/frontend/drupal/modules/php/php.info index 5ae31e002..5fd7ce9b8 100644 --- a/frontend/drupal/modules/php/php.info +++ b/frontend/drupal/modules/php/php.info @@ -5,7 +5,7 @@ version = VERSION core = 7.x files[] = php.test -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/poll/poll.info b/frontend/drupal/modules/poll/poll.info index 9fc8f3d08..bb82be47d 100644 --- a/frontend/drupal/modules/poll/poll.info +++ b/frontend/drupal/modules/poll/poll.info @@ -6,7 +6,7 @@ core = 7.x files[] = poll.test stylesheets[all][] = poll.css -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/profile/profile.info b/frontend/drupal/modules/profile/profile.info index a08fa71e7..dcc03f38d 100644 --- a/frontend/drupal/modules/profile/profile.info +++ b/frontend/drupal/modules/profile/profile.info @@ -11,7 +11,7 @@ configure = admin/config/people/profile ; See user_system_info_alter(). hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/rdf/rdf.info b/frontend/drupal/modules/rdf/rdf.info index 0c7c621df..50a5b0171 100644 --- a/frontend/drupal/modules/rdf/rdf.info +++ b/frontend/drupal/modules/rdf/rdf.info @@ -5,7 +5,7 @@ version = VERSION core = 7.x files[] = rdf.test -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/rdf/tests/rdf_test.info b/frontend/drupal/modules/rdf/tests/rdf_test.info index 62a772930..2593a3d94 100644 --- a/frontend/drupal/modules/rdf/tests/rdf_test.info +++ b/frontend/drupal/modules/rdf/tests/rdf_test.info @@ -6,7 +6,7 @@ core = 7.x hidden = TRUE dependencies[] = blog -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/search/search.info b/frontend/drupal/modules/search/search.info index 609c8f952..e91d74c7e 100644 --- a/frontend/drupal/modules/search/search.info +++ b/frontend/drupal/modules/search/search.info @@ -8,7 +8,7 @@ files[] = search.test configure = admin/config/search/settings stylesheets[all][] = search.css -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/search/tests/search_embedded_form.info b/frontend/drupal/modules/search/tests/search_embedded_form.info index 759a04d7b..c9696727a 100644 --- a/frontend/drupal/modules/search/tests/search_embedded_form.info +++ b/frontend/drupal/modules/search/tests/search_embedded_form.info @@ -5,7 +5,7 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/search/tests/search_extra_type.info b/frontend/drupal/modules/search/tests/search_extra_type.info index 79f463f50..b0091f8eb 100644 --- a/frontend/drupal/modules/search/tests/search_extra_type.info +++ b/frontend/drupal/modules/search/tests/search_extra_type.info @@ -5,7 +5,7 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/search/tests/search_node_tags.info b/frontend/drupal/modules/search/tests/search_node_tags.info index 3b46a9a24..861bca5c5 100644 --- a/frontend/drupal/modules/search/tests/search_node_tags.info +++ b/frontend/drupal/modules/search/tests/search_node_tags.info @@ -5,7 +5,7 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/shortcut/shortcut.info b/frontend/drupal/modules/shortcut/shortcut.info index 80ee46040..467bf751c 100644 --- a/frontend/drupal/modules/shortcut/shortcut.info +++ b/frontend/drupal/modules/shortcut/shortcut.info @@ -6,7 +6,7 @@ core = 7.x files[] = shortcut.test configure = admin/config/user-interface/shortcut -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/drupal_web_test_case.php b/frontend/drupal/modules/simpletest/drupal_web_test_case.php index c426ba53a..f212b0eb5 100644 --- a/frontend/drupal/modules/simpletest/drupal_web_test_case.php +++ b/frontend/drupal/modules/simpletest/drupal_web_test_case.php @@ -1690,8 +1690,14 @@ class DrupalWebTestCase extends DrupalTestCase { $this->fail('Failed to drop all prefixed tables.'); } + // In PHP 8 some tests encounter problems when shutdown code tries to + // access the database connection after it's been explicitly closed, for + // example the destructor of DrupalCacheArray. We avoid this by not fully + // destroying the test database connection. + $close = \PHP_VERSION_ID < 80000; + // Get back to the original connection. - Database::removeConnection('default'); + Database::removeConnection('default', $close); Database::renameConnection('simpletest_original_default', 'default'); // Restore original shutdown callbacks array to prevent original @@ -3084,7 +3090,7 @@ class DrupalWebTestCase extends DrupalTestCase { * @return * TRUE on pass, FALSE on fail. */ - protected function assertTextHelper($text, $message = '', $group, $not_exists) { + protected function assertTextHelper($text, $message, $group, $not_exists) { if ($this->plainTextContent === FALSE) { $this->plainTextContent = filter_xss($this->drupalGetContent(), array()); } @@ -3150,7 +3156,7 @@ class DrupalWebTestCase extends DrupalTestCase { * @return * TRUE on pass, FALSE on fail. */ - protected function assertUniqueTextHelper($text, $message = '', $group, $be_unique) { + protected function assertUniqueTextHelper($text, $message, $group, $be_unique) { if ($this->plainTextContent === FALSE) { $this->plainTextContent = filter_xss($this->drupalGetContent(), array()); } @@ -3256,7 +3262,7 @@ class DrupalWebTestCase extends DrupalTestCase { * @param $callback * The name of the theme function to invoke; e.g. 'links' for theme_links(). * @param $variables - * (optional) An array of variables to pass to the theme function. + * An array of variables to pass to the theme function. * @param $expected * The expected themed output string. * @param $message @@ -3272,7 +3278,7 @@ class DrupalWebTestCase extends DrupalTestCase { * @return * TRUE on pass, FALSE on fail. */ - protected function assertThemeOutput($callback, array $variables = array(), $expected, $message = '', $group = 'Other') { + protected function assertThemeOutput($callback, array $variables, $expected, $message = '', $group = 'Other') { $output = theme($callback, $variables); $this->verbose('Variables:' . '
' .  check_plain(var_export($variables, TRUE)) . '
' . '
' . 'Result:' . '
' .  check_plain(var_export($output, TRUE)) . '
' diff --git a/frontend/drupal/modules/simpletest/simpletest.info b/frontend/drupal/modules/simpletest/simpletest.info index b8576f271..474a8fb9b 100644 --- a/frontend/drupal/modules/simpletest/simpletest.info +++ b/frontend/drupal/modules/simpletest/simpletest.info @@ -58,7 +58,7 @@ 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 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/simpletest.test b/frontend/drupal/modules/simpletest/simpletest.test index 5d1c718c1..80b841d58 100644 --- a/frontend/drupal/modules/simpletest/simpletest.test +++ b/frontend/drupal/modules/simpletest/simpletest.test @@ -164,13 +164,16 @@ class SimpleTestFunctionalTest extends DrupalWebTestCase { $this->pass(t('Test ID is @id.', array('@id' => $this->testId))); // Generates a warning. - $i = 1 / 0; + $a = ''; + foreach ($a as $b) { + + } // Call an assert function specific to that class. $this->assertNothing(); - // Generates a warning inside a PHP function. - array_key_exists(NULL, NULL); + // Generates 3 warnings inside a PHP function. + simplexml_load_string(''); debug('Foo', 'Debug'); } @@ -195,19 +198,21 @@ class SimpleTestFunctionalTest extends DrupalWebTestCase { $this->assertAssertion(t('Invalid permission %permission.', array('%permission' => $this->invalid_permission)), 'Role', 'Fail', 'simpletest.test', 'SimpleTestFunctionalTest->stubTest()'); // Check that a warning is caught by simpletest. - $this->assertAssertion('Division by zero', 'Warning', 'Fail', 'simpletest.test', 'SimpleTestFunctionalTest->stubTest()'); + // The exact error message differs between PHP versions so we check only + // the presense of the 'foreach' statement. + $this->assertAssertion('foreach()', 'Warning', 'Fail', 'simpletest.test', 'SimpleTestFunctionalTest->stubTest()'); // Check that the backtracing code works for specific assert function. $this->assertAssertion('This is nothing.', 'Other', 'Pass', 'simpletest.test', 'SimpleTestFunctionalTest->stubTest()'); // Check that errors that occur inside PHP internal functions are correctly reported. // The exact error message differs between PHP versions so we check only - // the function name 'array_key_exists'. - $this->assertAssertion('array_key_exists', 'Warning', 'Fail', 'simpletest.test', 'SimpleTestFunctionalTest->stubTest()'); + // the function name 'simplexml_load_string'. + $this->assertAssertion('simplexml_load_string', 'Warning', 'Fail', 'simpletest.test', 'SimpleTestFunctionalTest->stubTest()'); $this->assertAssertion("Debug: 'Foo'", 'Debug', 'Fail', 'simpletest.test', 'SimpleTestFunctionalTest->stubTest()'); - $this->assertEqual('6 passes, 5 fails, 2 exceptions, and 1 debug message', $this->childTestResults['summary'], 'Stub test summary is correct'); + $this->assertEqual('6 passes, 5 fails, 4 exceptions, and 1 debug message', $this->childTestResults['summary'], 'Stub test summary is correct'); $this->test_ids[] = $test_id = $this->getTestIdFromResults(); $this->assertTrue($test_id, 'Found test ID in results.'); diff --git a/frontend/drupal/modules/simpletest/tests/actions_loop_test.info b/frontend/drupal/modules/simpletest/tests/actions_loop_test.info index 81783afea..908bed8d6 100644 --- a/frontend/drupal/modules/simpletest/tests/actions_loop_test.info +++ b/frontend/drupal/modules/simpletest/tests/actions_loop_test.info @@ -5,7 +5,7 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/tests/ajax_forms_test.info b/frontend/drupal/modules/simpletest/tests/ajax_forms_test.info index 944f31c1e..01aaa0404 100644 --- a/frontend/drupal/modules/simpletest/tests/ajax_forms_test.info +++ b/frontend/drupal/modules/simpletest/tests/ajax_forms_test.info @@ -5,7 +5,7 @@ package = Testing version = VERSION hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/tests/ajax_test.info b/frontend/drupal/modules/simpletest/tests/ajax_test.info index 860f681b3..a6a924674 100644 --- a/frontend/drupal/modules/simpletest/tests/ajax_test.info +++ b/frontend/drupal/modules/simpletest/tests/ajax_test.info @@ -5,7 +5,7 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/tests/batch_test.info b/frontend/drupal/modules/simpletest/tests/batch_test.info index a8e87a5ce..24f8bf779 100644 --- a/frontend/drupal/modules/simpletest/tests/batch_test.info +++ b/frontend/drupal/modules/simpletest/tests/batch_test.info @@ -5,7 +5,7 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/tests/boot_test_1.info b/frontend/drupal/modules/simpletest/tests/boot_test_1.info index 41c56572d..d09ef328c 100644 --- a/frontend/drupal/modules/simpletest/tests/boot_test_1.info +++ b/frontend/drupal/modules/simpletest/tests/boot_test_1.info @@ -5,7 +5,7 @@ package = Testing version = VERSION hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/tests/boot_test_2.info b/frontend/drupal/modules/simpletest/tests/boot_test_2.info index 10b332413..ab2ca587f 100644 --- a/frontend/drupal/modules/simpletest/tests/boot_test_2.info +++ b/frontend/drupal/modules/simpletest/tests/boot_test_2.info @@ -5,7 +5,7 @@ package = Testing version = VERSION hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/tests/bootstrap.test b/frontend/drupal/modules/simpletest/tests/bootstrap.test index effd04bf2..61caf53ca 100644 --- a/frontend/drupal/modules/simpletest/tests/bootstrap.test +++ b/frontend/drupal/modules/simpletest/tests/bootstrap.test @@ -453,13 +453,13 @@ class BootstrapGetFilenameTestCase extends DrupalUnitTestCase { /** * Skips handling of "file not found" errors. */ - public function fileNotFoundErrorHandler($error_level, $message, $filename, $line, $context) { + public function fileNotFoundErrorHandler($error_level, $message, $filename, $line) { // 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); + _drupal_error_handler($error_level, $message, $filename, $line); } } @@ -569,13 +569,13 @@ class BootstrapGetFilenameWebTestCase extends DrupalWebTestCase { /** * Skips handling of "file not found" errors. */ - public function fileNotFoundErrorHandler($error_level, $message, $filename, $line, $context) { + public function fileNotFoundErrorHandler($error_level, $message, $filename, $line) { // 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); + _drupal_error_handler($error_level, $message, $filename, $line); } /** diff --git a/frontend/drupal/modules/simpletest/tests/common.test b/frontend/drupal/modules/simpletest/tests/common.test index 6145546f7..ff3996763 100644 --- a/frontend/drupal/modules/simpletest/tests/common.test +++ b/frontend/drupal/modules/simpletest/tests/common.test @@ -2633,8 +2633,8 @@ class DrupalErrorCollectionUnitTest extends DrupalWebTestCase { $this->assertEqual(count($this->collectedErrors), 3, 'Three errors were collected'); if (count($this->collectedErrors) == 3) { - $this->assertError($this->collectedErrors[0], 'Notice', 'error_test_generate_warnings()', 'error_test.module', 'Undefined variable: bananas'); - $this->assertError($this->collectedErrors[1], 'Warning', 'error_test_generate_warnings()', 'error_test.module', 'Division by zero'); + $this->assertError($this->collectedErrors[0], 'Notice', 'error_test_generate_warnings()', 'error_test.module', 'Object of class stdClass could not be converted to int'); + $this->assertError($this->collectedErrors[1], 'Warning', 'error_test_generate_warnings()', 'error_test.module', \PHP_VERSION_ID < 80000 ? 'Invalid argument supplied for foreach()' : 'foreach() argument must be of type array|object, string given'); $this->assertError($this->collectedErrors[2], 'User warning', 'error_test_generate_warnings()', 'error_test.module', 'Drupal is awesome'); } else { diff --git a/frontend/drupal/modules/simpletest/tests/common_test.info b/frontend/drupal/modules/simpletest/tests/common_test.info index b22b08328..44d709b81 100644 --- a/frontend/drupal/modules/simpletest/tests/common_test.info +++ b/frontend/drupal/modules/simpletest/tests/common_test.info @@ -7,7 +7,7 @@ stylesheets[all][] = common_test.css stylesheets[print][] = common_test.print.css hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/tests/common_test_cron_helper.info b/frontend/drupal/modules/simpletest/tests/common_test_cron_helper.info index 79cb6dbeb..a0f668b80 100644 --- a/frontend/drupal/modules/simpletest/tests/common_test_cron_helper.info +++ b/frontend/drupal/modules/simpletest/tests/common_test_cron_helper.info @@ -5,7 +5,7 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/tests/database_test.info b/frontend/drupal/modules/simpletest/tests/database_test.info index 758aeb72e..87cd43f9d 100644 --- a/frontend/drupal/modules/simpletest/tests/database_test.info +++ b/frontend/drupal/modules/simpletest/tests/database_test.info @@ -5,7 +5,7 @@ package = Testing version = VERSION hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/tests/database_test.test b/frontend/drupal/modules/simpletest/tests/database_test.test index 04be5c85b..0f0d2c320 100644 --- a/frontend/drupal/modules/simpletest/tests/database_test.test +++ b/frontend/drupal/modules/simpletest/tests/database_test.test @@ -3755,21 +3755,36 @@ class DatabaseTransactionTestCase extends DatabaseTestCase { $transaction = db_transaction(); $this->insertRow('row'); $this->executeDDLStatement(); - // Rollback the outer transaction. + + set_error_handler(array($this, 'rollBackWithoutTransactionErrorHandler')); try { + // Rollback the outer transaction. $transaction->rollback(); - unset($transaction); - // @TODO: an exception should be triggered here, but is not, because - // "ROLLBACK" fails silently in MySQL if there is no transaction active. - // $this->fail(t('Rolling back a transaction containing DDL should fail.')); + // @see \DatabaseConnection_mysql::rollback() + if (PHP_VERSION_ID >= 80000) { + $this->fail('Rolling back a transaction containing DDL should produce a warning.'); + } } - catch (DatabaseTransactionNoActiveException $e) { - $this->pass('Rolling back a transaction containing DDL should fail.'); + catch (Exception $e) { + $this->assertEqual('Rollback attempted when there is no active transaction.', $e->getMessage()); } + restore_error_handler(); + unset($transaction); $this->assertRowPresent('row'); } } + /** + * Special handling of "rollback without transaction" errors. + */ + public function rollBackWithoutTransactionErrorHandler($error_level, $message, $filename, $line) { + // Throw an exception if this is a "rollback without transaction" error. + if (strpos($message, 'Rollback attempted when there is no active transaction.') !== FALSE ) { + throw new Exception('Rollback attempted when there is no active transaction.'); + } + _drupal_error_handler($error_level, $message, $filename, $line); + } + /** * Insert a single row into the testing table. */ diff --git a/frontend/drupal/modules/simpletest/tests/drupal_autoload_test/drupal_autoload_test.info b/frontend/drupal/modules/simpletest/tests/drupal_autoload_test/drupal_autoload_test.info index 6820a3517..37f8b035a 100644 --- a/frontend/drupal/modules/simpletest/tests/drupal_autoload_test/drupal_autoload_test.info +++ b/frontend/drupal/modules/simpletest/tests/drupal_autoload_test/drupal_autoload_test.info @@ -7,7 +7,7 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/tests/drupal_system_listing_compatible_test/drupal_system_listing_compatible_test.info b/frontend/drupal/modules/simpletest/tests/drupal_system_listing_compatible_test/drupal_system_listing_compatible_test.info index 0fb3d57b1..d88417ed3 100644 --- a/frontend/drupal/modules/simpletest/tests/drupal_system_listing_compatible_test/drupal_system_listing_compatible_test.info +++ b/frontend/drupal/modules/simpletest/tests/drupal_system_listing_compatible_test/drupal_system_listing_compatible_test.info @@ -5,7 +5,7 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/tests/drupal_system_listing_incompatible_test/drupal_system_listing_incompatible_test.info b/frontend/drupal/modules/simpletest/tests/drupal_system_listing_incompatible_test/drupal_system_listing_incompatible_test.info index d015d3fb0..d966bd21b 100644 --- a/frontend/drupal/modules/simpletest/tests/drupal_system_listing_incompatible_test/drupal_system_listing_incompatible_test.info +++ b/frontend/drupal/modules/simpletest/tests/drupal_system_listing_incompatible_test/drupal_system_listing_incompatible_test.info @@ -5,7 +5,7 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/tests/entity_cache_test.info b/frontend/drupal/modules/simpletest/tests/entity_cache_test.info index a95eaf5f2..a15be7285 100644 --- a/frontend/drupal/modules/simpletest/tests/entity_cache_test.info +++ b/frontend/drupal/modules/simpletest/tests/entity_cache_test.info @@ -6,7 +6,7 @@ core = 7.x dependencies[] = entity_cache_test_dependency hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/tests/entity_cache_test_dependency.info b/frontend/drupal/modules/simpletest/tests/entity_cache_test_dependency.info index 35ea7039d..72522da34 100644 --- a/frontend/drupal/modules/simpletest/tests/entity_cache_test_dependency.info +++ b/frontend/drupal/modules/simpletest/tests/entity_cache_test_dependency.info @@ -5,7 +5,7 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/tests/entity_crud_hook_test.info b/frontend/drupal/modules/simpletest/tests/entity_crud_hook_test.info index 17198cd0c..f381ce040 100644 --- a/frontend/drupal/modules/simpletest/tests/entity_crud_hook_test.info +++ b/frontend/drupal/modules/simpletest/tests/entity_crud_hook_test.info @@ -5,7 +5,7 @@ package = Testing version = VERSION hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/tests/entity_query_access_test.info b/frontend/drupal/modules/simpletest/tests/entity_query_access_test.info index 9a5d7249a..62e5fee13 100644 --- a/frontend/drupal/modules/simpletest/tests/entity_query_access_test.info +++ b/frontend/drupal/modules/simpletest/tests/entity_query_access_test.info @@ -5,7 +5,7 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/tests/error.test b/frontend/drupal/modules/simpletest/tests/error.test index f946e82f1..5b56d52d9 100644 --- a/frontend/drupal/modules/simpletest/tests/error.test +++ b/frontend/drupal/modules/simpletest/tests/error.test @@ -22,13 +22,13 @@ class DrupalErrorHandlerTestCase extends DrupalWebTestCase { function testErrorHandler() { $error_notice = array( '%type' => 'Notice', - '!message' => 'Undefined variable: bananas', + '!message' => 'Object of class stdClass could not be converted to int', '%function' => 'error_test_generate_warnings()', '%file' => drupal_realpath('modules/simpletest/tests/error_test.module'), ); $error_warning = array( '%type' => 'Warning', - '!message' => 'Division by zero', + '!message' => \PHP_VERSION_ID < 80000 ? 'Invalid argument supplied for foreach()' : 'foreach() argument must be of type array|object, string given', '%function' => 'error_test_generate_warnings()', '%file' => drupal_realpath('modules/simpletest/tests/error_test.module'), ); @@ -113,4 +113,3 @@ class DrupalErrorHandlerTestCase extends DrupalWebTestCase { $this->assertNoRaw($message, format_string('Did not find error message: !message.', array('!message' => $message))); } } - diff --git a/frontend/drupal/modules/simpletest/tests/error_test.info b/frontend/drupal/modules/simpletest/tests/error_test.info index 37425c3c8..58c54e331 100644 --- a/frontend/drupal/modules/simpletest/tests/error_test.info +++ b/frontend/drupal/modules/simpletest/tests/error_test.info @@ -5,7 +5,7 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/tests/error_test.module b/frontend/drupal/modules/simpletest/tests/error_test.module index d062cb067..43d0d0c61 100644 --- a/frontend/drupal/modules/simpletest/tests/error_test.module +++ b/frontend/drupal/modules/simpletest/tests/error_test.module @@ -40,9 +40,13 @@ function error_test_generate_warnings($collect_errors = FALSE) { // Tell Drupal error reporter to send errors to Simpletest or not. define('SIMPLETEST_COLLECT_ERRORS', $collect_errors); // This will generate a notice. - $monkey_love = $bananas; + $notice = new \stdClass(); + $notice == 1 ? 1 : 0; // This will generate a warning. - $awesomely_big = 1/0; + $a = ''; + foreach ($a as $b) { + + } // This will generate a user error. trigger_error("Drupal is awesome", E_USER_WARNING); return ""; diff --git a/frontend/drupal/modules/simpletest/tests/file_test.info b/frontend/drupal/modules/simpletest/tests/file_test.info index 0bb6d1db6..f1f287751 100644 --- a/frontend/drupal/modules/simpletest/tests/file_test.info +++ b/frontend/drupal/modules/simpletest/tests/file_test.info @@ -6,7 +6,7 @@ core = 7.x files[] = file_test.module hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/tests/filter_test.info b/frontend/drupal/modules/simpletest/tests/filter_test.info index 96d316805..cf51e69ec 100644 --- a/frontend/drupal/modules/simpletest/tests/filter_test.info +++ b/frontend/drupal/modules/simpletest/tests/filter_test.info @@ -5,7 +5,7 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/tests/form.test b/frontend/drupal/modules/simpletest/tests/form.test index 49b561a63..23af5f4c7 100644 --- a/frontend/drupal/modules/simpletest/tests/form.test +++ b/frontend/drupal/modules/simpletest/tests/form.test @@ -240,14 +240,14 @@ class FormsTestCase extends DrupalWebTestCase { $values = drupal_json_decode($this->drupalPost(NULL, array('required_checkbox' => 1), t('Submit'))); $expected_values = array( 'disabled_checkbox_on' => 'disabled_checkbox_on', - 'disabled_checkbox_off' => '', + 'disabled_checkbox_off' => 0, 'checkbox_on' => 'checkbox_on', - 'checkbox_off' => '', + 'checkbox_off' => 0, 'zero_checkbox_on' => '0', - 'zero_checkbox_off' => '', + 'zero_checkbox_off' => 0, ); foreach ($expected_values as $widget => $expected_value) { - $this->assertEqual($values[$widget], $expected_value, format_string('Checkbox %widget returns expected value (expected: %expected, got: %value)', array( + $this->assertIdentical($values[$widget], $expected_value, format_string('Checkbox %widget returns expected value (expected: %expected, got: %value)', array( '%widget' => var_export($widget, TRUE), '%expected' => var_export($expected_value, TRUE), '%value' => var_export($values[$widget], TRUE), diff --git a/frontend/drupal/modules/simpletest/tests/form_test.info b/frontend/drupal/modules/simpletest/tests/form_test.info index 346acca29..c3f8ba901 100644 --- a/frontend/drupal/modules/simpletest/tests/form_test.info +++ b/frontend/drupal/modules/simpletest/tests/form_test.info @@ -5,7 +5,7 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/tests/image_test.info b/frontend/drupal/modules/simpletest/tests/image_test.info index b3e168036..009cffe85 100644 --- a/frontend/drupal/modules/simpletest/tests/image_test.info +++ b/frontend/drupal/modules/simpletest/tests/image_test.info @@ -5,7 +5,7 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/tests/menu_test.info b/frontend/drupal/modules/simpletest/tests/menu_test.info index 4973c75b7..80fa7d0fc 100644 --- a/frontend/drupal/modules/simpletest/tests/menu_test.info +++ b/frontend/drupal/modules/simpletest/tests/menu_test.info @@ -5,7 +5,7 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/tests/module_test.info b/frontend/drupal/modules/simpletest/tests/module_test.info index 8c1eb098d..8fb3bf27d 100644 --- a/frontend/drupal/modules/simpletest/tests/module_test.info +++ b/frontend/drupal/modules/simpletest/tests/module_test.info @@ -5,7 +5,7 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/tests/path_test.info b/frontend/drupal/modules/simpletest/tests/path_test.info index ab1a36b56..0de0cf6e1 100644 --- a/frontend/drupal/modules/simpletest/tests/path_test.info +++ b/frontend/drupal/modules/simpletest/tests/path_test.info @@ -5,7 +5,7 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/tests/psr_0_test/psr_0_test.info b/frontend/drupal/modules/simpletest/tests/psr_0_test/psr_0_test.info index 2e2e8e508..efc8711bd 100644 --- a/frontend/drupal/modules/simpletest/tests/psr_0_test/psr_0_test.info +++ b/frontend/drupal/modules/simpletest/tests/psr_0_test/psr_0_test.info @@ -5,7 +5,7 @@ core = 7.x hidden = TRUE package = Testing -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/tests/psr_4_test/psr_4_test.info b/frontend/drupal/modules/simpletest/tests/psr_4_test/psr_4_test.info index a5270a7f7..ab7ec3eea 100644 --- a/frontend/drupal/modules/simpletest/tests/psr_4_test/psr_4_test.info +++ b/frontend/drupal/modules/simpletest/tests/psr_4_test/psr_4_test.info @@ -5,7 +5,7 @@ core = 7.x hidden = TRUE package = Testing -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/tests/requirements1_test.info b/frontend/drupal/modules/simpletest/tests/requirements1_test.info index bc85ee845..31be1944b 100644 --- a/frontend/drupal/modules/simpletest/tests/requirements1_test.info +++ b/frontend/drupal/modules/simpletest/tests/requirements1_test.info @@ -5,7 +5,7 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/tests/requirements2_test.info b/frontend/drupal/modules/simpletest/tests/requirements2_test.info index b023c44e9..792456984 100644 --- a/frontend/drupal/modules/simpletest/tests/requirements2_test.info +++ b/frontend/drupal/modules/simpletest/tests/requirements2_test.info @@ -7,7 +7,7 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/tests/session.test b/frontend/drupal/modules/simpletest/tests/session.test index 893d03e9f..5632d44e4 100644 --- a/frontend/drupal/modules/simpletest/tests/session.test +++ b/frontend/drupal/modules/simpletest/tests/session.test @@ -244,6 +244,187 @@ class SessionTestCase extends DrupalWebTestCase { $this->assertResponse(403, 'An empty session ID is not allowed.'); } + /** + * Test absence of SameSite attribute on session cookies by default. + */ + function testNoSameSiteCookieAttributeDefault() { + $user = $this->drupalCreateUser(array('access content')); + $this->sessionReset($user->uid); + if (\PHP_VERSION_ID < 70300) { + $this->drupalLogin($user); + } + else { + // PHP often defaults to an empty value for session.cookie_samesite but + // that may vary, so we set an explicit empty value. + // Send our own login POST so that we can pass a custom header to trigger + // session_test.module to call ini_set('session.cookie_samesite', $value) + $headers[] = 'X-Session-Cookie-Ini-Set: *EMPTY*'; + $edit = array( + 'name' => $user->name, + 'pass' => $user->pass_raw, + ); + $this->drupalPost('user', $edit, t('Log in'), array(), $headers); + } + $this->assertFalse(preg_match('/SameSite=/i', $this->drupalGetHeader('Set-Cookie', TRUE)), 'Session cookie has no SameSite attribute (default).'); + } + + /** + * Test SameSite attribute = None by default on Secure session cookies. + */ + function testSameSiteCookieAttributeNoneSecure() { + $user = $this->drupalCreateUser(array('access content')); + $this->sessionReset($user->uid); + $headers = array(); + if (\PHP_VERSION_ID >= 70300) { + // Send our own login POST so that we can pass a custom header to trigger + // session_test.module to call ini_set('session.cookie_samesite', $value) + $headers[] = 'X-Session-Cookie-Ini-Set: None'; + } + // Test HTTPS session handling by altering the form action to submit the + // login form through https.php, which creates a mock HTTPS request. + $this->drupalGet('user'); + $form = $this->xpath('//form[@id="user-login"]'); + $form[0]['action'] = $this->httpsUrl('user'); + $edit = array('name' => $user->name, 'pass' => $user->pass_raw); + $this->drupalPost(NULL, $edit, t('Log in'), array(), $headers); + $this->assertTrue(preg_match('/SameSite=None/i', $this->drupalGetHeader('Set-Cookie', TRUE)), 'Session cookie is set as SameSite=None.'); + } + + /** + * Test SameSite attribute = None on session cookies. + */ + function testSameSiteCookieAttributeNone() { + variable_set('samesite_cookie_value', 'None'); + $user = $this->drupalCreateUser(array('access content')); + $this->sessionReset($user->uid); + $this->drupalLogin($user); + $this->assertTrue(preg_match('/SameSite=None/i', $this->drupalGetHeader('Set-Cookie', TRUE)), 'Session cookie is set as SameSite=None.'); + } + + /** + * Test SameSite attribute = Lax on session cookies. + */ + function testSameSiteCookieAttributeLax() { + variable_set('samesite_cookie_value', 'Lax'); + $user = $this->drupalCreateUser(array('access content')); + $this->sessionReset($user->uid); + $this->drupalLogin($user); + $this->assertTrue(preg_match('/SameSite=Lax/i', $this->drupalGetHeader('Set-Cookie', TRUE)), 'Session cookie is set as SameSite=Lax.'); + } + + /** + * Test SameSite attribute = Strict on session cookies. + */ + function testSameSiteCookieAttributeStrict() { + variable_set('samesite_cookie_value', 'Strict'); + $user = $this->drupalCreateUser(array('access content')); + $this->sessionReset($user->uid); + $this->drupalLogin($user); + $this->assertTrue(preg_match('/SameSite=Strict/i', $this->drupalGetHeader('Set-Cookie', TRUE)), 'Session cookie is set as SameSite=Strict.'); + } + + /** + * Test disabling the samesite attribute on session cookies via $conf + */ + function testSameSiteCookieAttributeDisabledViaConf() { + $user = $this->drupalCreateUser(array('access content')); + $this->sessionReset($user->uid); + variable_set('samesite_cookie_value', FALSE); + if (\PHP_VERSION_ID < 70300) { + // There is no session.cookie_samesite in earlier PHP versions. + $this->drupalLogin($user); + } + else { + // Send our own login POST so that we can pass a custom header to trigger + // session_test.module to call ini_set('session.cookie_samesite', $value) + $headers[] = 'X-Session-Cookie-Ini-Set: Lax'; + $edit = array( + 'name' => $user->name, + 'pass' => $user->pass_raw, + ); + $this->drupalPost('user', $edit, t('Log in'), array(), $headers); + } + $this->assertFalse(preg_match('/SameSite=/i', $this->drupalGetHeader('Set-Cookie', TRUE)), 'Session cookie has no SameSite attribute (conf).'); + } + + /** + * Test disabling the samesite attribute on session cookies via php ini + */ + function testSameSiteCookieAttributeDisabledViaPhpIni() { + if (\PHP_VERSION_ID < 70300) { + // There is no session.cookie_samesite in earlier PHP versions. + $this->pass('This test is only for PHP 7.3 and later.'); + return; + } + $user = $this->drupalCreateUser(array('access content')); + // Send our own login POST so that we can pass a custom header to trigger + // session_test.module to call ini_set('session.cookie_samesite', $value) + $headers[] = 'X-Session-Cookie-Ini-Set: *EMPTY*'; + $edit = array( + 'name' => $user->name, + 'pass' => $user->pass_raw, + ); + $this->drupalPost('user', $edit, t('Log in'), array(), $headers); + $this->assertFalse(preg_match('/SameSite=/i', $this->drupalGetHeader('Set-Cookie', TRUE)), 'Session cookie has no SameSite attribute (ini).'); + } + + /** + * Test that a PHP setting for session.cookie_samesite is not overridden by + * the default value in Drupal, without a samesite_cookie_value variable. + */ + function testSamesiteCookiePhpSettingLax() { + if (\PHP_VERSION_ID < 70300) { + // There is no session.cookie_samesite in earlier PHP versions. + $this->pass('This test is only for PHP 7.3 and later.'); + return; + } + $user = $this->drupalCreateUser(array('access content')); + // Send our own login POST so that we can pass a custom header to trigger + // session_test.module to call ini_set('session.cookie_samesite', $value) + $headers[] = 'X-Session-Cookie-Ini-Set: Lax'; + $edit = array( + 'name' => $user->name, + 'pass' => $user->pass_raw, + ); + $this->drupalPost('user', $edit, t('Log in'), array(), $headers); + $this->assertTrue(preg_match('/SameSite=Lax/i', $this->drupalGetHeader('Set-Cookie', TRUE)), 'Session cookie is set as SameSite=Lax.'); + } + + /** + * Test overriding the PHP setting for session.cookie_samesite with the + * samesite_cookie_value variable. + */ + function testSamesiteCookieOverrideLaxToStrict() { + if (\PHP_VERSION_ID < 70300) { + // There is no session.cookie_samesite in earlier PHP versions. + $this->pass('This test is only for PHP 7.3 and later.'); + return; + } + variable_set('samesite_cookie_value', 'Strict'); + $user = $this->drupalCreateUser(array('access content')); + // Send our own login POST so that we can pass a custom header to trigger + // session_test.module to call ini_set('session.cookie_samesite', $value) + $headers[] = 'X-Session-Cookie-Ini-Set: Lax'; + $edit = array( + 'name' => $user->name, + 'pass' => $user->pass_raw, + ); + $this->drupalPost('user', $edit, t('Log in'), array(), $headers); + $this->assertTrue(preg_match('/SameSite=Strict/i', $this->drupalGetHeader('Set-Cookie', TRUE)), 'Session cookie is set as SameSite=Strict.'); + } + + /** + * Test SameSite attribute = Lax on set-cookie header on logout. + */ + function testSamesiteCookieLogoutLax() { + variable_set('samesite_cookie_value', 'Lax'); + $user = $this->drupalCreateUser(array('access content')); + $this->sessionReset($user->uid); + $this->drupalLogin($user); + $this->drupalGet('user/logout'); + $this->assertTrue(preg_match('/SameSite=Lax/i', $this->drupalGetHeader('Set-Cookie', TRUE)), 'Session cookie deletion includes SameSite=Lax.'); + } + /** * Reset the cookie file so that it refers to the specified user. * @@ -285,6 +466,20 @@ class SessionTestCase extends DrupalWebTestCase { $this->assertIdentical($this->drupalGetHeader('X-Session-Empty'), '0', 'Session was not empty.'); } } + + /** + * Builds a URL for submitting a mock HTTPS request to HTTP test environments. + * + * @param $url + * A Drupal path such as 'user'. + * + * @return + * An absolute URL. + */ + protected function httpsUrl($url) { + global $base_url; + return $base_url . '/modules/simpletest/tests/https.php?q=' . $url; + } } /** diff --git a/frontend/drupal/modules/simpletest/tests/session_test.info b/frontend/drupal/modules/simpletest/tests/session_test.info index 4a04c5b8c..7b66b3892 100644 --- a/frontend/drupal/modules/simpletest/tests/session_test.info +++ b/frontend/drupal/modules/simpletest/tests/session_test.info @@ -5,7 +5,7 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/tests/session_test.module b/frontend/drupal/modules/simpletest/tests/session_test.module index 689ff099a..b2d31079b 100644 --- a/frontend/drupal/modules/simpletest/tests/session_test.module +++ b/frontend/drupal/modules/simpletest/tests/session_test.module @@ -64,6 +64,19 @@ function session_test_menu() { return $items; } +/** + * It's very unusual to do anything outside of a function / hook, but in this + * case we want to simulate a given session.cookie_samesite setting in php.ini + * or via ini_set() in settings.php. This would almost never be a good idea + * outside of a test scenario. + */ +if (isset($_SERVER['HTTP_X_SESSION_COOKIE_INI_SET'])) { + if (in_array($_SERVER['HTTP_X_SESSION_COOKIE_INI_SET'], array('None', 'Lax', 'Strict', '*EMPTY*'))) { + $value = ($_SERVER['HTTP_X_SESSION_COOKIE_INI_SET'] == '*EMPTY*') ? '' : $_SERVER['HTTP_X_SESSION_COOKIE_INI_SET']; + ini_set('session.cookie_samesite', $value); + } +} + /** * Implements hook_boot(). */ diff --git a/frontend/drupal/modules/simpletest/tests/system_dependencies_test.info b/frontend/drupal/modules/simpletest/tests/system_dependencies_test.info index d7242e282..eeedee4ab 100644 --- a/frontend/drupal/modules/simpletest/tests/system_dependencies_test.info +++ b/frontend/drupal/modules/simpletest/tests/system_dependencies_test.info @@ -6,7 +6,7 @@ core = 7.x hidden = TRUE dependencies[] = _missing_dependency -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/tests/system_incompatible_core_version_dependencies_test.info b/frontend/drupal/modules/simpletest/tests/system_incompatible_core_version_dependencies_test.info index 1680c1ade..f1b38e520 100644 --- a/frontend/drupal/modules/simpletest/tests/system_incompatible_core_version_dependencies_test.info +++ b/frontend/drupal/modules/simpletest/tests/system_incompatible_core_version_dependencies_test.info @@ -6,7 +6,7 @@ core = 7.x hidden = TRUE dependencies[] = system_incompatible_core_version_test -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/tests/system_incompatible_core_version_test.info b/frontend/drupal/modules/simpletest/tests/system_incompatible_core_version_test.info index 1f70929de..ce6cd080b 100644 --- a/frontend/drupal/modules/simpletest/tests/system_incompatible_core_version_test.info +++ b/frontend/drupal/modules/simpletest/tests/system_incompatible_core_version_test.info @@ -5,7 +5,7 @@ version = VERSION core = 5.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/tests/system_incompatible_module_version_dependencies_test.info b/frontend/drupal/modules/simpletest/tests/system_incompatible_module_version_dependencies_test.info index 94721cbc5..f18384529 100644 --- a/frontend/drupal/modules/simpletest/tests/system_incompatible_module_version_dependencies_test.info +++ b/frontend/drupal/modules/simpletest/tests/system_incompatible_module_version_dependencies_test.info @@ -7,7 +7,7 @@ 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 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/tests/system_incompatible_module_version_test.info b/frontend/drupal/modules/simpletest/tests/system_incompatible_module_version_test.info index ce11959b5..51390e80a 100644 --- a/frontend/drupal/modules/simpletest/tests/system_incompatible_module_version_test.info +++ b/frontend/drupal/modules/simpletest/tests/system_incompatible_module_version_test.info @@ -5,7 +5,7 @@ version = 1.0 core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/tests/system_project_namespace_test.info b/frontend/drupal/modules/simpletest/tests/system_project_namespace_test.info index 371999d87..111c3fb24 100644 --- a/frontend/drupal/modules/simpletest/tests/system_project_namespace_test.info +++ b/frontend/drupal/modules/simpletest/tests/system_project_namespace_test.info @@ -6,7 +6,7 @@ core = 7.x hidden = TRUE dependencies[] = drupal:filter -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/tests/system_test.info b/frontend/drupal/modules/simpletest/tests/system_test.info index 3e6f87f82..7221a8d6e 100644 --- a/frontend/drupal/modules/simpletest/tests/system_test.info +++ b/frontend/drupal/modules/simpletest/tests/system_test.info @@ -6,7 +6,7 @@ core = 7.x files[] = system_test.module hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/tests/taxonomy_test.info b/frontend/drupal/modules/simpletest/tests/taxonomy_test.info index f3a4a4bd8..4b8696bd4 100644 --- a/frontend/drupal/modules/simpletest/tests/taxonomy_test.info +++ b/frontend/drupal/modules/simpletest/tests/taxonomy_test.info @@ -6,7 +6,7 @@ core = 7.x hidden = TRUE dependencies[] = taxonomy -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/tests/theme_test.info b/frontend/drupal/modules/simpletest/tests/theme_test.info index 4a929db16..318c76873 100644 --- a/frontend/drupal/modules/simpletest/tests/theme_test.info +++ b/frontend/drupal/modules/simpletest/tests/theme_test.info @@ -5,7 +5,7 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/tests/themes/test_basetheme/test_basetheme.info b/frontend/drupal/modules/simpletest/tests/themes/test_basetheme/test_basetheme.info index 62d1a72dc..e4338553e 100644 --- a/frontend/drupal/modules/simpletest/tests/themes/test_basetheme/test_basetheme.info +++ b/frontend/drupal/modules/simpletest/tests/themes/test_basetheme/test_basetheme.info @@ -6,7 +6,7 @@ hidden = TRUE settings[basetheme_only] = base theme value settings[subtheme_override] = base theme value -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/tests/themes/test_subtheme/test_subtheme.info b/frontend/drupal/modules/simpletest/tests/themes/test_subtheme/test_subtheme.info index d3fd66caa..188521739 100644 --- a/frontend/drupal/modules/simpletest/tests/themes/test_subtheme/test_subtheme.info +++ b/frontend/drupal/modules/simpletest/tests/themes/test_subtheme/test_subtheme.info @@ -6,7 +6,7 @@ hidden = TRUE settings[subtheme_override] = subtheme value -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/tests/themes/test_theme/test_theme.info b/frontend/drupal/modules/simpletest/tests/themes/test_theme/test_theme.info index a09c4f190..470df24fd 100644 --- a/frontend/drupal/modules/simpletest/tests/themes/test_theme/test_theme.info +++ b/frontend/drupal/modules/simpletest/tests/themes/test_theme/test_theme.info @@ -17,7 +17,7 @@ stylesheets[all][] = system.base.css settings[theme_test_setting] = default value -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/tests/themes/test_theme_nyan_cat/test_theme_nyan_cat.info b/frontend/drupal/modules/simpletest/tests/themes/test_theme_nyan_cat/test_theme_nyan_cat.info index 758c9b553..1c23ececb 100644 --- a/frontend/drupal/modules/simpletest/tests/themes/test_theme_nyan_cat/test_theme_nyan_cat.info +++ b/frontend/drupal/modules/simpletest/tests/themes/test_theme_nyan_cat/test_theme_nyan_cat.info @@ -4,7 +4,7 @@ core = 7.x hidden = TRUE engine = nyan_cat -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/tests/update_script_test.info b/frontend/drupal/modules/simpletest/tests/update_script_test.info index 6ecea7912..3084effa1 100644 --- a/frontend/drupal/modules/simpletest/tests/update_script_test.info +++ b/frontend/drupal/modules/simpletest/tests/update_script_test.info @@ -5,7 +5,7 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/tests/update_test_1.info b/frontend/drupal/modules/simpletest/tests/update_test_1.info index a9cc0c0f9..fa2eb75fe 100644 --- a/frontend/drupal/modules/simpletest/tests/update_test_1.info +++ b/frontend/drupal/modules/simpletest/tests/update_test_1.info @@ -5,7 +5,7 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/tests/update_test_2.info b/frontend/drupal/modules/simpletest/tests/update_test_2.info index a9cc0c0f9..fa2eb75fe 100644 --- a/frontend/drupal/modules/simpletest/tests/update_test_2.info +++ b/frontend/drupal/modules/simpletest/tests/update_test_2.info @@ -5,7 +5,7 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/tests/update_test_3.info b/frontend/drupal/modules/simpletest/tests/update_test_3.info index a9cc0c0f9..fa2eb75fe 100644 --- a/frontend/drupal/modules/simpletest/tests/update_test_3.info +++ b/frontend/drupal/modules/simpletest/tests/update_test_3.info @@ -5,7 +5,7 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/tests/upgrade/upgrade.locale.test b/frontend/drupal/modules/simpletest/tests/upgrade/upgrade.locale.test index 2a3056b06..f7b0038d6 100644 --- a/frontend/drupal/modules/simpletest/tests/upgrade/upgrade.locale.test +++ b/frontend/drupal/modules/simpletest/tests/upgrade/upgrade.locale.test @@ -124,7 +124,7 @@ class LocaleUpgradePathTestCase extends UpgradePathTestCase { /** * Asserts that a page exists and is in the specified language. */ - public function assertPageInLanguage($path = NULL, $langcode) { + public function assertPageInLanguage($path, $langcode) { if (isset($path)) { $this->drupalGet($path); } diff --git a/frontend/drupal/modules/simpletest/tests/url_alter_test.info b/frontend/drupal/modules/simpletest/tests/url_alter_test.info index 3adee0fad..e2775bb4d 100644 --- a/frontend/drupal/modules/simpletest/tests/url_alter_test.info +++ b/frontend/drupal/modules/simpletest/tests/url_alter_test.info @@ -5,7 +5,7 @@ package = Testing version = VERSION hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/simpletest/tests/xmlrpc_test.info b/frontend/drupal/modules/simpletest/tests/xmlrpc_test.info index 326b82dd1..c83dae688 100644 --- a/frontend/drupal/modules/simpletest/tests/xmlrpc_test.info +++ b/frontend/drupal/modules/simpletest/tests/xmlrpc_test.info @@ -5,7 +5,7 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/statistics/statistics.info b/frontend/drupal/modules/statistics/statistics.info index 0ad418980..f55e71530 100644 --- a/frontend/drupal/modules/statistics/statistics.info +++ b/frontend/drupal/modules/statistics/statistics.info @@ -6,7 +6,7 @@ core = 7.x files[] = statistics.test configure = admin/config/system/statistics -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/syslog/syslog.info b/frontend/drupal/modules/syslog/syslog.info index 4ee8581dd..45ee371c7 100644 --- a/frontend/drupal/modules/syslog/syslog.info +++ b/frontend/drupal/modules/syslog/syslog.info @@ -6,7 +6,7 @@ core = 7.x files[] = syslog.test configure = admin/config/development/logging -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/system/system.info b/frontend/drupal/modules/system/system.info index 8b4ba3a38..6cff0a625 100644 --- a/frontend/drupal/modules/system/system.info +++ b/frontend/drupal/modules/system/system.info @@ -12,7 +12,7 @@ files[] = system.test required = TRUE configure = admin/config/system -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/system/system.tar.inc b/frontend/drupal/modules/system/system.tar.inc index 0af6275b4..7a0719e58 100644 --- a/frontend/drupal/modules/system/system.tar.inc +++ b/frontend/drupal/modules/system/system.tar.inc @@ -280,7 +280,7 @@ class Archive_Tar { $this->_close(); // ----- Look for a local copy to delete - if ($this->_temp_tarname != '') { + if ($this->_temp_tarname != '' && (bool) preg_match('/^tar[[:alnum:]]*\.tmp$/', $this->_temp_tarname)) { @drupal_unlink($this->_temp_tarname); } } diff --git a/frontend/drupal/modules/system/tests/cron_queue_test.info b/frontend/drupal/modules/system/tests/cron_queue_test.info index 4c33802bc..c2c263643 100644 --- a/frontend/drupal/modules/system/tests/cron_queue_test.info +++ b/frontend/drupal/modules/system/tests/cron_queue_test.info @@ -5,7 +5,7 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/system/tests/system_cron_test.info b/frontend/drupal/modules/system/tests/system_cron_test.info index 4a79c5461..36a21de0f 100644 --- a/frontend/drupal/modules/system/tests/system_cron_test.info +++ b/frontend/drupal/modules/system/tests/system_cron_test.info @@ -5,7 +5,7 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/taxonomy/taxonomy.info b/frontend/drupal/modules/taxonomy/taxonomy.info index 16b3b361d..d696969d7 100644 --- a/frontend/drupal/modules/taxonomy/taxonomy.info +++ b/frontend/drupal/modules/taxonomy/taxonomy.info @@ -8,7 +8,7 @@ files[] = taxonomy.module files[] = taxonomy.test configure = admin/structure/taxonomy -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/toolbar/toolbar.info b/frontend/drupal/modules/toolbar/toolbar.info index f847f2a0a..0ffdee0ea 100644 --- a/frontend/drupal/modules/toolbar/toolbar.info +++ b/frontend/drupal/modules/toolbar/toolbar.info @@ -4,7 +4,7 @@ core = 7.x package = Core version = VERSION -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/tracker/tracker.info b/frontend/drupal/modules/tracker/tracker.info index 02fdaab62..eb4f6db49 100644 --- a/frontend/drupal/modules/tracker/tracker.info +++ b/frontend/drupal/modules/tracker/tracker.info @@ -6,7 +6,7 @@ version = VERSION core = 7.x files[] = tracker.test -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/translation/tests/translation_test.info b/frontend/drupal/modules/translation/tests/translation_test.info index af85ecc89..b5e861514 100644 --- a/frontend/drupal/modules/translation/tests/translation_test.info +++ b/frontend/drupal/modules/translation/tests/translation_test.info @@ -5,7 +5,7 @@ package = Testing version = VERSION hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/translation/translation.info b/frontend/drupal/modules/translation/translation.info index 8b81011da..4362b7d44 100644 --- a/frontend/drupal/modules/translation/translation.info +++ b/frontend/drupal/modules/translation/translation.info @@ -6,7 +6,7 @@ version = VERSION core = 7.x files[] = translation.test -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/trigger/tests/trigger_test.info b/frontend/drupal/modules/trigger/tests/trigger_test.info index 4af57113d..1b1099e44 100644 --- a/frontend/drupal/modules/trigger/tests/trigger_test.info +++ b/frontend/drupal/modules/trigger/tests/trigger_test.info @@ -4,7 +4,7 @@ package = Testing core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/trigger/trigger.info b/frontend/drupal/modules/trigger/trigger.info index dccfe4a68..7d18ab017 100644 --- a/frontend/drupal/modules/trigger/trigger.info +++ b/frontend/drupal/modules/trigger/trigger.info @@ -6,7 +6,7 @@ core = 7.x files[] = trigger.test configure = admin/structure/trigger -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/update/tests/aaa_update_test.info b/frontend/drupal/modules/update/tests/aaa_update_test.info index 1e591277f..290880308 100644 --- a/frontend/drupal/modules/update/tests/aaa_update_test.info +++ b/frontend/drupal/modules/update/tests/aaa_update_test.info @@ -4,7 +4,7 @@ package = Testing core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/update/tests/bbb_update_test.info b/frontend/drupal/modules/update/tests/bbb_update_test.info index ca8c12c72..55eb983b4 100644 --- a/frontend/drupal/modules/update/tests/bbb_update_test.info +++ b/frontend/drupal/modules/update/tests/bbb_update_test.info @@ -4,7 +4,7 @@ package = Testing core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/update/tests/ccc_update_test.info b/frontend/drupal/modules/update/tests/ccc_update_test.info index 30b134133..981b5b381 100644 --- a/frontend/drupal/modules/update/tests/ccc_update_test.info +++ b/frontend/drupal/modules/update/tests/ccc_update_test.info @@ -4,7 +4,7 @@ package = Testing core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/update/tests/themes/update_test_admintheme/update_test_admintheme.info b/frontend/drupal/modules/update/tests/themes/update_test_admintheme/update_test_admintheme.info index db08879db..06efabaa2 100644 --- a/frontend/drupal/modules/update/tests/themes/update_test_admintheme/update_test_admintheme.info +++ b/frontend/drupal/modules/update/tests/themes/update_test_admintheme/update_test_admintheme.info @@ -3,7 +3,7 @@ description = Test theme which is used as admin theme. core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/update/tests/themes/update_test_basetheme/update_test_basetheme.info b/frontend/drupal/modules/update/tests/themes/update_test_basetheme/update_test_basetheme.info index 409ab59cf..dc984ae9a 100644 --- a/frontend/drupal/modules/update/tests/themes/update_test_basetheme/update_test_basetheme.info +++ b/frontend/drupal/modules/update/tests/themes/update_test_basetheme/update_test_basetheme.info @@ -3,7 +3,7 @@ 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 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/update/tests/themes/update_test_subtheme/update_test_subtheme.info b/frontend/drupal/modules/update/tests/themes/update_test_subtheme/update_test_subtheme.info index 757210739..afdbc931c 100644 --- a/frontend/drupal/modules/update/tests/themes/update_test_subtheme/update_test_subtheme.info +++ b/frontend/drupal/modules/update/tests/themes/update_test_subtheme/update_test_subtheme.info @@ -4,7 +4,7 @@ core = 7.x base theme = update_test_basetheme hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/update/tests/update_test.info b/frontend/drupal/modules/update/tests/update_test.info index bbd89205a..0844cdd68 100644 --- a/frontend/drupal/modules/update/tests/update_test.info +++ b/frontend/drupal/modules/update/tests/update_test.info @@ -5,7 +5,7 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/update/update.info b/frontend/drupal/modules/update/update.info index 2799f33a3..95b1c78d2 100644 --- a/frontend/drupal/modules/update/update.info +++ b/frontend/drupal/modules/update/update.info @@ -6,7 +6,7 @@ core = 7.x files[] = update.test configure = admin/reports/updates/settings -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/user/tests/user_flood_test.info b/frontend/drupal/modules/user/tests/user_flood_test.info index 4500f0882..845418525 100644 --- a/frontend/drupal/modules/user/tests/user_flood_test.info +++ b/frontend/drupal/modules/user/tests/user_flood_test.info @@ -5,7 +5,7 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/user/tests/user_form_test.info b/frontend/drupal/modules/user/tests/user_form_test.info index 3ee56197b..0cfa543cb 100644 --- a/frontend/drupal/modules/user/tests/user_form_test.info +++ b/frontend/drupal/modules/user/tests/user_form_test.info @@ -5,7 +5,7 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/user/tests/user_form_test.module b/frontend/drupal/modules/user/tests/user_form_test.module index 2af15cb83..4379bd1a5 100644 --- a/frontend/drupal/modules/user/tests/user_form_test.module +++ b/frontend/drupal/modules/user/tests/user_form_test.module @@ -80,3 +80,42 @@ function user_form_test_user_account_submit($form, &$form_state) { // test for bugs that can be triggered in contributed modules. $form_state['rebuild'] = TRUE; } + +/** + * Implements hook_form_FORM_ID_alter(). + */ +function user_form_test_form_user_pass_reset_alter(&$form, &$form_state) { + // An unaltered form has no form values; the uid/timestmap/"confirm" are in + // the URL arguments. (If for some reason a form_alter method needs the + // hash, it can be retrieved from $form['#action'].) + if (!is_numeric(arg(2)) || !is_numeric(arg(3)) || !is_string(arg(4)) || arg(4) !== 'confirm') { + throw new Exception("Something unexpected changed in the user_pass_reset form; we are not getting the UID/timestamp/'confirm' passed anymore."); + } + // Use the variable system to communicate to the test code, since we don't + // share a session with it. + variable_set('user_test_pass_reset_form_build_' . arg(2), TRUE); + + $form['#submit'][] = 'user_form_test_form_user_pass_reset_submit'; + // We must cache the form to ensure the form builder (user_pass_reset()) is + // skipped when processing the submitted form, otherwise we get redirected + // already during form build. + $form_state['cache'] = TRUE; +} + +/** + * Submit function for user_pass_reset(). + */ +function user_form_test_form_user_pass_reset_submit($form, &$form_state) { + // On submit, the hash is in arg(4). + if (!is_numeric(arg(2)) || !is_numeric(arg(3)) || !is_string(arg(4)) || strlen(arg(4)) < 32) { + throw new Exception("Something unexpected changed in the user_pass_reset form; we are not getting the UID/timestamp/hash passed anymore."); + } + variable_set('user_test_pass_reset_form_submit_' . arg(2), TRUE); + // Because the form does no further processing and has no redirect set, + // drupal_redirect_form() will redirect back to ourselves + // (user/reset/UID/TIMESTAMP/HASH/login); we will be logged in and redirected + // while the form is built again. FYI: we cannot set $form_state['rebuild'] + // to get around the first redirect without further hacks, because then the + // form won't pass the hash. (Our original $form_state['build_info']['args'] + // contains "confirm" for the 3rd argument.) +} diff --git a/frontend/drupal/modules/user/tests/user_session_test.info b/frontend/drupal/modules/user/tests/user_session_test.info index 4a9152602..ca482bdb5 100644 --- a/frontend/drupal/modules/user/tests/user_session_test.info +++ b/frontend/drupal/modules/user/tests/user_session_test.info @@ -5,7 +5,7 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/user/user.admin.inc b/frontend/drupal/modules/user/user.admin.inc index 6ca330b01..0db7efb1a 100644 --- a/frontend/drupal/modules/user/user.admin.inc +++ b/frontend/drupal/modules/user/user.admin.inc @@ -61,7 +61,7 @@ function user_filter_form() { if ($type == 'permission') { // Merge arrays of module permissions into one. // Slice past the first element '[any]' whose value is not an array. - $options = call_user_func_array('array_merge', array_slice($filters[$type]['options'], 1)); + $options = call_user_func_array('array_merge', array_values(array_slice($filters[$type]['options'], 1))); $value = $options[$value]; } else { @@ -1050,4 +1050,3 @@ function user_admin_role_delete_confirm_submit($form, &$form_state) { drupal_set_message(t('The role has been deleted.')); $form_state['redirect'] = 'admin/people/permissions/roles'; } - diff --git a/frontend/drupal/modules/user/user.info b/frontend/drupal/modules/user/user.info index 01b772a10..4bed910d2 100644 --- a/frontend/drupal/modules/user/user.info +++ b/frontend/drupal/modules/user/user.info @@ -9,7 +9,7 @@ required = TRUE configure = admin/config/people stylesheets[all][] = user.css -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/modules/user/user.pages.inc b/frontend/drupal/modules/user/user.pages.inc index 6f997a62e..937f7a31e 100644 --- a/frontend/drupal/modules/user/user.pages.inc +++ b/frontend/drupal/modules/user/user.pages.inc @@ -134,10 +134,25 @@ function user_pass_submit($form, &$form_state) { /** * Menu callback; process one time login link and redirects to the user page on success. + * + * In order to never disclose password reset hashes via referrer headers or + * web browser history, this function always issues a redirect when a valid + * password reset hash is in the URL. */ function user_pass_reset($form, &$form_state, $uid, $timestamp, $hashed_pass, $action = NULL) { global $user; + // Check for a reset hash in the session. Immediately remove it (to prevent it + // from being reused, for example if this page is visited again via the + // browser history) and store it for later use. + if (isset($_SESSION['pass_reset_hash'])) { + $session_reset_hash = $_SESSION['pass_reset_hash']; + unset($_SESSION['pass_reset_hash']); + } + else { + $session_reset_hash = NULL; + } + // When processing the one-time login link, we have to make sure that a user // isn't already logged in. if ($user->uid) { @@ -182,7 +197,36 @@ function user_pass_reset($form, &$form_state, $uid, $timestamp, $hashed_pass, $a drupal_set_message(t('You have tried to use a one-time login link that has expired. Please request a new one using the form below.'), 'error'); drupal_goto('user/password'); } - elseif ($account->uid && $timestamp >= $account->login && $timestamp <= $current && $hashed_pass == user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid)) { + // Validate the reset hash from the URL or from the session. + $is_valid = FALSE; + if ($account->uid && $timestamp >= $account->login && $timestamp <= $current) { + // If the reset hash in the URL is valid, put it in the session and + // redirect to the same URL but with the hash replaced by an invalid + // one ("confirm"). This prevents disclosing the hash via referrer + // headers or web browser history. + if ($hashed_pass == user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid)) { + if ($action === 'login') { + // The 'login' action redirects directly to the user edit form. + $is_valid = TRUE; + } + else { + $_SESSION['pass_reset_hash'] = $hashed_pass; + $args = arg(); + foreach ($args as &$arg) { + if ($arg == $hashed_pass) { + $arg = 'confirm'; + } + } + $path = implode('/', $args); + drupal_goto($path, array('query' => drupal_get_query_parameters())); + } + } + // If the reset hash from the session is valid, use that. + elseif ($session_reset_hash && $session_reset_hash == user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid)) { + $is_valid = TRUE; + } + } + if ($is_valid) { // First stage is a confirmation form, then login if ($action == 'login') { // Set the new user. @@ -204,7 +248,11 @@ function user_pass_reset($form, &$form_state, $uid, $timestamp, $hashed_pass, $a $form['help'] = array('#markup' => '

' . t('This login can be used only once.') . '

'); $form['actions'] = array('#type' => 'actions'); $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Log in')); - $form['#action'] = url("user/reset/$uid/$timestamp/$hashed_pass/login"); + $form['#action'] = url("user/reset/$uid/$timestamp/$session_reset_hash/login"); + // Prevent the browser from storing this page so that the token will + // not be visible in the form action if the back button is used to + // revisit this page. + drupal_add_http_header('Cache-Control', 'no-store'); return $form; } } diff --git a/frontend/drupal/modules/user/user.test b/frontend/drupal/modules/user/user.test index 4c16b531c..ec2f90d6f 100644 --- a/frontend/drupal/modules/user/user.test +++ b/frontend/drupal/modules/user/user.test @@ -480,6 +480,10 @@ class UserLoginTestCase extends DrupalWebTestCase { class UserPasswordResetTestCase extends DrupalWebTestCase { protected $profile = 'standard'; + function setUp() { + parent::setUp('user_form_test'); + } + public static function getInfo() { return array( 'name' => 'Reset password', @@ -491,20 +495,38 @@ class UserPasswordResetTestCase extends DrupalWebTestCase { /** * Retrieves password reset email and extracts the login link. */ - public function getResetURL() { + public function getResetURL($bypass_form = FALSE) { // Assume the most recent email. $_emails = $this->drupalGetMails(); $email = end($_emails); $urls = array(); preg_match('#.+user/reset/.+#', $email['body'], $urls); - return $urls[0]; + return $urls[0] . ($bypass_form ? '/login' : ''); + } + + /** + * Generates login link. + */ + public function generateResetURL($account, $bypass_form = FALSE) { + return user_pass_reset_url($account) . ($bypass_form ? '/login' : ''); + } + + /** + * Turns a password reset URL into a 'confirm' URL. + */ + public function getConfirmURL($reset_url) { + // Last part is always the hash; replace with "confirm". + $parts = explode('/', $reset_url); + array_pop($parts); + array_push($parts, 'confirm'); + return implode('/', $parts); } /** * Tests password reset functionality. */ - function testUserPasswordReset() { + function testUserPasswordReset($use_direct_login_link = FALSE) { // Create a user. $account = $this->drupalCreateUser(); $this->drupalLogin($account); @@ -540,11 +562,19 @@ class UserPasswordResetTestCase extends DrupalWebTestCase { ); field_create_instance($instance); - $resetURL = $this->getResetURL(); + variable_del("user_test_pass_reset_form_submit_{$account->uid}"); + $resetURL = $this->getResetURL($use_direct_login_link); $this->drupalGet($resetURL); // Check successful login. - $this->drupalPost(NULL, NULL, t('Log in')); + if (!$use_direct_login_link) { + $this->assertUrl($this->getConfirmURL($resetURL), array(), 'The user is redirected to the reset password confirm form.'); + $this->drupalPost(NULL, NULL, t('Log in')); + // The form was fully processed before redirecting. + $form_submit_handled = variable_get("user_test_pass_reset_form_submit_{$account->uid}", FALSE); + $this->assertTrue($form_submit_handled, 'A custom submit handler executed.'); + } + $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.'); // Make sure the Ajax request from uploading a file does not invalidate the // reset token. @@ -559,6 +589,16 @@ class UserPasswordResetTestCase extends DrupalWebTestCase { $edit = array('pass[pass1]' => $password, 'pass[pass2]' => $password); $this->drupalPost(NULL, $edit, t('Save')); $this->assertText(t('The changes have been saved.'), 'Forgotten password changed.'); + + // Ensure blocked and deleted accounts can't access the direct login link. + $this->drupalLogout(); + $reset_url = $this->generateResetURL($account, $use_direct_login_link); + user_save($account, array('status' => 0)); + $this->drupalGet($reset_url); + $this->assertResponse(403); + user_delete($account->uid); + $this->drupalGet($reset_url); + $this->assertResponse(403); } /** @@ -642,21 +682,51 @@ class UserPasswordResetTestCase extends DrupalWebTestCase { /** * Test user password reset while logged in. */ - function testUserPasswordResetLoggedIn() { + function testUserPasswordResetLoggedIn($use_direct_login_link = FALSE) { + $another_account = $this->drupalCreateUser(); $account = $this->drupalCreateUser(); $this->drupalLogin($account); // Make sure the test account has a valid password. user_save($account, array('pass' => user_password())); + // Try to use the login link while logged in as a different user. // Generate one time login link. - $reset_url = user_pass_reset_url($account); + $reset_url = $this->generateResetURL($another_account, $use_direct_login_link); $this->drupalGet($reset_url); + $this->assertRaw(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' => $account->name, '%resetting_user' => $another_account->name, '!logout' => url('user/logout')) + )); - $this->assertText('Reset password'); - $this->drupalPost(NULL, NULL, t('Log in')); + // Test the link for a deleted user while logged in. + user_delete($another_account->uid); + $this->drupalGet($reset_url); + $this->assertText('The one-time login link you clicked is invalid.'); + // Generate a one time login link for the logged-in user. + $fapi_action = $use_direct_login_link ? 'build' : 'submit'; + variable_del("user_test_pass_reset_form_{$fapi_action}_{$account->uid}"); + $reset_url = $this->generateResetURL($account, $use_direct_login_link); + $this->drupalGet($reset_url); + if ($use_direct_login_link) { + // The form is never fully built; user is logged out (session destroyed) + // and redirected to the same URL, then logged in again and redirected + // during form build. + $form_built = variable_get("user_test_pass_reset_form_build_{$account->uid}", FALSE); + $this->assertTrue(!$form_built, 'The password reset form was never fully built.'); + } + else { + $this->assertUrl($this->getConfirmURL($reset_url), array(), 'The user is redirected to the reset password confirm form.'); + $this->assertText('Reset password'); + $this->drupalPost(NULL, NULL, t('Log in')); + // The form was fully processed before redirecting. + $form_submit_handled = variable_get("user_test_pass_reset_form_submit_{$account->uid}", FALSE); + $this->assertTrue($form_submit_handled, 'A custom submit handler executed.'); + } $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.'); + // The user can change the forgotten password on the page they are + // redirected to. $pass = user_password(); $edit = array( 'pass[pass1]' => $pass, @@ -667,6 +737,14 @@ class UserPasswordResetTestCase extends DrupalWebTestCase { $this->assertText('The changes have been saved.'); } + /** + * Test direct login link that bypasses the password reset form. + */ + function testUserDirectLogin() { + $this->testUserPasswordReset(TRUE); + $this->testUserPasswordResetLoggedIn(TRUE); + } + /** * Attempts login using an expired password reset link. */ @@ -770,7 +848,7 @@ class UserPasswordResetTestCase extends DrupalWebTestCase { $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->assertUrl($this->getConfirmURL($reset_url), array(), 'The user is redirected to the reset password confirm form.'); $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.'); } diff --git a/frontend/drupal/profiles/minimal/minimal.info b/frontend/drupal/profiles/minimal/minimal.info index c9a70a1db..bd8f3f6f5 100644 --- a/frontend/drupal/profiles/minimal/minimal.info +++ b/frontend/drupal/profiles/minimal/minimal.info @@ -5,7 +5,7 @@ core = 7.x dependencies[] = block dependencies[] = dblog -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/profiles/standard/standard.info b/frontend/drupal/profiles/standard/standard.info index bcda8923e..2e10e0d05 100644 --- a/frontend/drupal/profiles/standard/standard.info +++ b/frontend/drupal/profiles/standard/standard.info @@ -24,7 +24,7 @@ dependencies[] = field_ui dependencies[] = file dependencies[] = rdf -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/profiles/testing/modules/drupal_system_listing_compatible_test/drupal_system_listing_compatible_test.info b/frontend/drupal/profiles/testing/modules/drupal_system_listing_compatible_test/drupal_system_listing_compatible_test.info index 6ef5373c4..79d3e5bcf 100644 --- a/frontend/drupal/profiles/testing/modules/drupal_system_listing_compatible_test/drupal_system_listing_compatible_test.info +++ b/frontend/drupal/profiles/testing/modules/drupal_system_listing_compatible_test/drupal_system_listing_compatible_test.info @@ -6,7 +6,7 @@ core = 7.x hidden = TRUE files[] = drupal_system_listing_compatible_test.test -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/profiles/testing/modules/drupal_system_listing_incompatible_test/drupal_system_listing_incompatible_test.info b/frontend/drupal/profiles/testing/modules/drupal_system_listing_incompatible_test/drupal_system_listing_incompatible_test.info index fb319ecb0..91e34f6c6 100644 --- a/frontend/drupal/profiles/testing/modules/drupal_system_listing_incompatible_test/drupal_system_listing_incompatible_test.info +++ b/frontend/drupal/profiles/testing/modules/drupal_system_listing_incompatible_test/drupal_system_listing_incompatible_test.info @@ -8,7 +8,7 @@ version = VERSION core = 6.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/profiles/testing/testing.info b/frontend/drupal/profiles/testing/testing.info index fe1a2e863..bf2a85a18 100644 --- a/frontend/drupal/profiles/testing/testing.info +++ b/frontend/drupal/profiles/testing/testing.info @@ -4,7 +4,7 @@ version = VERSION core = 7.x hidden = TRUE -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/sites/all/modules/breakpoints/LICENSE.txt b/frontend/drupal/sites/all/modules/breakpoints/LICENSE.txt new file mode 100644 index 000000000..d159169d1 --- /dev/null +++ b/frontend/drupal/sites/all/modules/breakpoints/LICENSE.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/frontend/drupal/sites/all/modules/breakpoints/README.txt b/frontend/drupal/sites/all/modules/breakpoints/README.txt new file mode 100644 index 000000000..5e5b8879c --- /dev/null +++ b/frontend/drupal/sites/all/modules/breakpoints/README.txt @@ -0,0 +1,76 @@ +# SUMMARY + +**Breakpoints** are where a responsive design adjusts in order to display correctly on different devices. The Breakpoints module standardizes the use of breakpoints, and enables modules and themes to expose or use each others' breakpoints. The Breakpoint module keeps track of the height, width, and resolution breakpoints. + +Breakpoints itself does not natively use the breakpoints entered in order to provide additional functionality to the site; as a result, some theming or development knowledge is required in order to harness the power of breakpoints. + +# REQUIREMENTS + +## Required Modules + +The following module is required for Breakpoints to work. + +* [Chaos tool suite](https://www.drupal.org/project/ctools) (ctools) + +## Recommended Modules + +Breakpoints also works hand-in-hand with a number of other modules for responsive solutions, which is recommended. These modules include: + +* [Picture](https://www.drupal.org/project/picture) +* [Breakpoint Panels](https://www.drupal.org/project/breakpoint_panels) + +# INSTALLATION + +1. Unzip the contents of the module in your sites/all/modules folder. +2. Enable the module from your sites admin/modules. + +# CONFIGURATION + +Breakpoints can be configured in two ways: from the module configuration page or in the theme.info file. Once configured, breakpoints can be exported to themes or to responsive styles, + +Configuring breakpoints from the module configuration page provides several options that allows them to be used quickly, including exporting breakpoints or creating image styles to match breakpoints. To configure a breakpoint from the module configuration page: + +1. Navigate to admin/config/media/breakpoints on your Drupal installation. +2. Enter a human-readible name for your new breakpoint under "Name". +3. Enter a CSS @media breakpoint at which you want the breakpoint to trigger under "Breakpoint". For example, "min-width: 750px". +4. Select multipliers if you wish to support Retina displays under "Multipliers". +5. Click "Save". + +*Note*: breakpoints made from the user interface will have "customuser" prepended to the name given to the breakpoint. + +Breakpoints should be joined into groups for use with responsive styles or for organization purposes. To create a group: + +1. Click on the "Add new group" from the main Breakpoint page. +2. Enter a name for the group under "Group Name". +3. Select the breakpoints you wish to add to the group from the select list. +4. Click "Save". + +A theme can define breakpoints in the theme.info file by adding a name for the breakpoint and a valid media query, like so: + +``` +breakpoints[mobile] = (min-width: 0px) +breakpoints[narrow] = (min-width: 560px) +breakpoints[wide] = (min-width: 851px) +breakpoints[tv] = only screen and (min-width: 3456px) +``` + +*Note*: breakpoints made from a .info file will not be read until the theme has been reloaded, either on the theme page by clicking "Reload theme" or by disabling and enabling the theme. + +# USAGE + +The main usage of Breakpoints comes entirely outside of the module itself. Because breakpoints defined either in the .info file or in the Breakpoints UI are available with a call to the breakpoints module, developers and themers can call on breakpoints in template.php (or in other modules). Modules such as [Picture](https://www.drupal.org/project/picture) and [Breakpoint Panels](https://www.drupal.org/project/breakpoint_panels) can use Breakpoints natively to create responsive pictures or panels, respectively. Use of Breakpoints with those modules, and others, can be found in their respective documentation. + +Aside from the ability to store breakpoints, the Breakpoints module also can convert stored Breakpoints to valid theme.info file format, using the following steps: + +1. Navigate to /admin/config/media/breakpoints. +2. Select a group of breakpoints from the tabs at the top of the screen. +3. Click "Export breakpoints to theme". + +The results in the textbox on the resulting screen can then be copied to a theme.info file, where they will be automatically implemented on any profile running the respective theme. + +*Note*: from this same screen, breakpoints may also be fully exported in a feature-ready format using the "Export nodes" link. + +# CONTACT + +Project: https://www.drupal.org/project/breakpoints +Issues: https://www.drupal.org/project/issues/breakpoints?categories=All \ No newline at end of file diff --git a/frontend/drupal/sites/all/modules/breakpoints/breakpoints.admin.inc b/frontend/drupal/sites/all/modules/breakpoints/breakpoints.admin.inc new file mode 100644 index 000000000..b94473ec0 --- /dev/null +++ b/frontend/drupal/sites/all/modules/breakpoints/breakpoints.admin.inc @@ -0,0 +1,1225 @@ + t('Copy/Paste the following inside your theme.info file.'), + '#type' => 'textarea', + '#default_value' => $form_state['exported_breakpoints'], + ); + } + // Global is the same as no group name + $global = FALSE; + if ($breakpoint_group_name == '' || $breakpoint_group_name == 'global') { + $breakpoint_group_name = ''; + $global = TRUE; + } + + $form_state['group_name'] = $breakpoint_group_name; + + $settings = breakpoints_settings(); + $multipliers = array(); + if (isset($settings->multipliers) && !empty($settings->multipliers)) { + $multipliers = drupal_map_assoc(array_values($settings->multipliers)); + if (array_key_exists('1x', $multipliers)) { + unset($multipliers['1x']); + } + } + + if ($global) { + $form['info'] = array( + '#type' => 'markup', + '#markup' => t("You can manage all your breakpoints on this screen, if one of your themes has breakpoints defined inside the .info file they will be shown here."), + ); + + $info = array(); + $info[] = t("To create a new breakpoint, you have to enter a name and a media query (ex. (min-width: 15em))."); + $info[] = t("All breakpoints can be enabled or disabled so they cannot be used by other modules."); + $info[] = t("For each breakpoint you can define what multipliers have to be available (needed to support 'retina' displays)."); + $info[] = t("Breakpoints you created yourself can be deleted."); + $info[] = t("You can group multiple breakpoints in a group by using '!add', so other modules can easily interact with them.", array('!add' => l(t('Add a new group'), 'admin/config/media/breakpoints/groups/add'))); + $info[] = t("If you do not see the breakpoint group for your theme, make sure your theme is enabled and !clear_cache or click the \"Scan this theme for breakpoints\" button on the bottom of the settings page of your theme.", array('!clear_cache' => l(t('clear your cache'), 'admin/config/development/performance'))); + + $form['more_info'] = array( + '#type' => 'container', + '#theme' => 'item_list', + '#items' => $info, + ); + } + else { + $form['info'] = array( + '#type' => 'markup', + '#markup' => t("You can manage the breakpoints of this group here."), + ); + $info = array(); + $info[] = t("You can change the order of the breakpoints inside this group."); + $info[] = t("You can enable multipliers for each breakpoint, but this will also affect other groups."); + + $form['more_info'] = array( + '#type' => 'container', + '#theme' => 'item_list', + '#items' => $info, + ); + } + + $form['#attached']['css'][] = drupal_get_path('module', 'breakpoints') . '/css/breakpoints.admin.css'; + $form['breakpoints'] = array( + '#type' => 'container', + '#tree' => TRUE, + '#theme' => 'breakpoints_admin_breakpoints_table', + '#multipliers' => $multipliers, + '#group_name' => $breakpoint_group_name, + ); + + $breakpoints = array(); + $breakpoint_group = breakpoints_breakpoint_group_load($breakpoint_group_name); + if ($global) { + $breakpoints = breakpoints_breakpoint_load_all(); + } + else { + $weight = 0; + foreach ($breakpoint_group->breakpoints as $breakpoint_name) { + $breakpoint = breakpoints_breakpoint_load_by_fullkey($breakpoint_name); + if ($breakpoint && isset($breakpoint->machine_name)) { + $breakpoint->global_weight = $breakpoint->weight; + $breakpoint->weight = $weight++; + $breakpoints[$breakpoint_name] = $breakpoint; + } + } + } + + foreach ($breakpoints as $key => $breakpoint) { + if ($breakpoint->source_type == BREAKPOINTS_SOURCE_TYPE_THEME) { + $bp_group = breakpoints_breakpoint_group_load($breakpoint->source); + if ($bp_group->overridden && variable_get('breakpoints_hide_overridden_breakpoints', 1) && $global) { + continue; + } + } + $form['breakpoints'][$key] = array( + '#breakpoint_data' => $breakpoint, + 'name' => array( + '#type' => 'textfield', + '#default_value' => $breakpoint->name, + '#disabled' => TRUE, + '#size' => 20, + ), + 'breakpoint' => array( + '#type' => 'textfield', + '#default_value' => $breakpoint->breakpoint, + '#disabled' => $breakpoint->source_type === BREAKPOINTS_SOURCE_TYPE_THEME || !$global, + '#size' => empty($multipliers) ? 60 : 30, + '#maxlength' => 255, + ), + 'weight' => array( + '#type' => 'textfield', + '#size' => 4, + '#default_value' => isset($breakpoint->weight) ? $breakpoint->weight : 0, + '#attributes' => array('class' => array('breakpoints-weight')), + ), + ); + // Add multipliers checkboxes if needed. + if (!empty($multipliers)) { + $form['breakpoints'][$key]['multipliers'] = array( + '#type' => 'checkboxes', + '#default_value' => (isset($breakpoint->multipliers) && is_array($breakpoint->multipliers)) ? $breakpoint->multipliers : array(), + '#options' => $multipliers, + '#disabled' => $breakpoint->source_type === BREAKPOINTS_SOURCE_TYPE_THEME || !$global, + ); + } + // Add global weight if needed. + $form['breakpoints'][$key]['global_weight'] = array( + '#type' => 'value', + '#value' => isset($breakpoint->global_weight) ? $breakpoint->global_weight : $breakpoint->weight, + ); + } + + if ($global) { + // Add empty row + $form['breakpoints']['new'] = array( + 'name' => array( + '#type' => 'textfield', + '#default_value' => '', + '#size' => 20, + '#maxlength' => 255, + ), + 'machine_name' => array( + '#type' => 'machine_name', + '#size' => '64', + '#title' => t('Machine name'), + '#default_value' => '', + '#machine_name' => array( + 'exists' => 'breakpoints_breakpoint_name_exists', + 'source' => array('breakpoints', 'new', 'name'), + ), + '#required' => FALSE, + '#maxlength' => 255, + ), + 'breakpoint' => array( + '#type' => 'textfield', + '#default_value' => '', + '#size' => empty($multipliers) ? 60 : 30, + '#maxlength' => 255, + ), + 'weight' => array( + '#type' => 'textfield', + '#size' => 4, + '#default_value' => 0, + '#attributes' => array('class' => array('breakpoints-weight')), + ), + ); + // Add multipliers checkboxes if needed. + if (!empty($multipliers)) { + $form['breakpoints']['new']['multipliers'] = array( + '#type' => 'checkboxes', + '#default_value' => array(), + '#options' => $multipliers, + ); + } + } + + // Buttons + $form['buttons'] = array( + '#type' => 'container', + ); + + // Submit button + $form['buttons']['submit'] = array( + '#type' => 'submit', + '#value' => t('Save'), + ); + + if (!$global) { + switch ($breakpoint_group->type) { + case BREAKPOINTS_SOURCE_TYPE_THEME: + if (!$breakpoint_group->overridden) { + $form['buttons']['override'] = array( + '#type' => 'submit', + '#value' => t('Override theme breakpoints'), + '#submit' => array('breakpoints_admin_breakpoints_submit_override'), + ); + $form['buttons']['reload'] = array( + '#type' => 'submit', + '#value' => t('Reload theme breakpoints'), + '#submit' => array('breakpoints_admin_breakpoints_submit_reload'), + ); + $form['buttons']['duplicate'] = array( + '#type' => 'markup', + '#markup' => l( + t('Duplicate group'), 'admin/config/media/breakpoints/groups/' . $breakpoint_group_name . '/duplicate', + array( + 'query' => drupal_get_destination(), + 'attributes' => array('class' => array('breakpoints-group-operations-link', 'breakpoints-group-operations-duplicate-link')), + ) + ), + ); + } + else { + $form['buttons']['exporttotheme'] = array( + '#type' => 'submit', + '#value' => t('Export breakpoints to theme'), + '#submit' => array('breakpoints_admin_breakpoints_submit_exporttotheme'), + ); + $form['buttons']['revert'] = array( + '#type' => 'submit', + '#value' => t('Revert theme breakpoints'), + '#submit' => array('breakpoints_admin_breakpoints_submit_revert'), + ); + $form['buttons']['editlink'] = array( + '#type' => 'markup', + '#markup' => l( + t('Edit group breakpoints'), 'admin/config/media/breakpoints/groups/' . $breakpoint_group_name . '/edit', + array( + 'query' => drupal_get_destination(), + 'attributes' => array('class' => array('breakpoints-group-operations-link', 'breakpoints-group-operations-edit-link')), + ) + ), + ); + $form['buttons']['duplicate'] = array( + '#type' => 'markup', + '#markup' => l( + t('Duplicate group'), 'admin/config/media/breakpoints/groups/' . $breakpoint_group_name . '/duplicate', + array( + 'query' => drupal_get_destination(), + 'attributes' => array('class' => array('breakpoints-group-operations-link', 'breakpoints-group-operations-duplicate-link')), + ) + ), + ); + } + break; + case BREAKPOINTS_SOURCE_TYPE_MODULE: + $form['buttons']['exporttotheme'] = array( + '#type' => 'submit', + '#value' => t('Export breakpoints to theme'), + '#submit' => array('breakpoints_admin_breakpoints_submit_exporttotheme'), + ); + $form['buttons']['editlink'] = array( + '#type' => 'markup', + '#markup' => l( + t('Edit group breakpoints'), 'admin/config/media/breakpoints/groups/' . $breakpoint_group_name . '/edit', + array( + 'query' => drupal_get_destination(), + 'attributes' => array('class' => array('breakpoints-group-operations-link', 'breakpoints-group-operations-edit-link')), + ) + ), + ); + $form['buttons']['duplicate'] = array( + '#type' => 'markup', + '#markup' => l( + t('Duplicate group'), 'admin/config/media/breakpoints/groups/' . $breakpoint_group_name . '/duplicate', + array( + 'query' => drupal_get_destination(), + 'attributes' => array('class' => array('breakpoints-group-operations-link', 'breakpoints-group-operations-duplicate-link')), + ) + ), + ); + break; + case BREAKPOINTS_SOURCE_TYPE_CUSTOM: + $form['buttons']['exporttotheme'] = array( + '#type' => 'submit', + '#value' => t('Export breakpoints to theme'), + '#submit' => array('breakpoints_admin_breakpoints_submit_exporttotheme'), + ); + $form['buttons']['editlink'] = array( + '#type' => 'markup', + '#markup' => l( + t('Edit group breakpoints'), 'admin/config/media/breakpoints/groups/' . $breakpoint_group_name . '/edit', + array( + 'query' => drupal_get_destination(), + 'attributes' => array('class' => array('breakpoints-group-operations-link', 'breakpoints-group-operations-edit-link')), + ) + ), + ); + $form['buttons']['duplicate'] = array( + '#type' => 'markup', + '#markup' => l( + t('Duplicate group'), 'admin/config/media/breakpoints/groups/' . $breakpoint_group_name . '/duplicate', + array( + 'query' => drupal_get_destination(), + 'attributes' => array('class' => array('breakpoints-group-operations-link', 'breakpoints-group-operations-duplicate-link')), + ) + ), + ); + $form['buttons']['deletelink'] = array( + '#type' => 'markup', + '#markup' => l( + t('Delete this group'), 'admin/config/media/breakpoints/groups/' . $breakpoint_group_name . '/delete', + array( + 'query' => array('destination' => 'admin/config/media/breakpoints/groups'), + 'attributes' => array('class' => array('breakpoints-group-operations-link', 'breakpoints-group-operations-delete-link')), + ) + ), + ); + break; + } + } + + return $form; +} + +/** + * Theme form as table. + */ +function theme_breakpoints_admin_breakpoints_table($variables) { + drupal_add_css(drupal_get_path('module', 'breakpoints') . '/css/breakpoints.admin.css'); + $form = $variables['form']; + $global = empty($form['#group_name']); + // Rows. + $rows = array(); + foreach (element_children($form) as $key) { + + if ($key != 'new') { + $row = _breakpoints_admin_breakpoints_table_row($form[$key], $key, $global); + $breakpoint = $form[$key]['#breakpoint_data']; + $class = 'breakpoints-status-' . ($breakpoint->status ? 'enabled' : 'disabled'); + } + else { + $row = _breakpoints_admin_breakpoints_table_new_row($form[$key]); + $class = 'breakpoints-status-new'; + } + + $rows[] = array( + 'data' => $row, + 'class' => array('draggable', $class), + ); + } + + // Header. + $header = array(); + $header[] = array('data' => t('Name'), 'colspan' => 2); + $header[] = t('Breakpoint, @media ...'); + $header[] = t('Multipliers'); + $header[] = t('Source'); + $header[] = t('Status'); + if ($global) { + $header[] = array('data' => t('Operations'), 'colspan' => 3); + } + $header[] = t('Weight'); + + $output = theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('id' => 'resp-img-breakpoints'))); + drupal_add_tabledrag('resp-img-breakpoints', 'order', 'sibling', 'breakpoints-weight'); + return $output; +} + +/** + * Helper callback for theme_breakpoints_admin_breakpoints_table(). + */ +function _breakpoints_admin_breakpoints_table_row(&$element, $key, $global) { + $row = array(); + $link_attributes = array( + 'attributes' => array( + 'class' => array('image-style-link'), + ), + ); + $breakpoint = $element['#breakpoint_data']; + $element['weight']['#attributes']['class'] = array('breakpoints-weight'); + + $row[] = drupal_render($element['name']); + $row[] = ''; + $row[] = drupal_render($element['breakpoint']); + $row[] = drupal_render($element['multipliers']); + $row[] = $breakpoint->source . ' (' . $breakpoint->source_type . ')'; + $row[] = $breakpoint->status ? t('Enabled') : t('Disabled'); + + if ($global) { + $row[] = l($breakpoint->status ? t('Disable') : t('Enable'), 'admin/config/media/breakpoints/' . ($breakpoint->status ? 'disable' : 'enable') . '/' . $key, $link_attributes); + $row[] = $breakpoint->source_type == BREAKPOINTS_SOURCE_TYPE_CUSTOM ? l(t('Delete'), 'admin/config/media/breakpoints/delete/' . $key, $link_attributes) : ''; + $row[] = l(t('Export'), 'admin/config/media/breakpoints/export/' . $key, $link_attributes); + } + + $row[] = drupal_render($element['weight']); + return $row; +} + +/** + * Helper callback for theme_breakpoints_admin_breakpoints_table(). + */ +function _breakpoints_admin_breakpoints_table_new_row(&$element) { + $row = array(); + $row[] = drupal_render($element['name']); + $row[] = drupal_render($element['machine_name']); + $row[] = drupal_render($element['breakpoint']); + $row[] = drupal_render($element['multipliers']); + $row[] = ''; + $row[] = ''; + $row[] = ''; + $row[] = ''; + $row[] = ''; + $row[] = drupal_render($element['weight']); + return $row; +} + +/** + * Admin form validation. + */ +function breakpoints_admin_breakpoints_validate($form, &$form_state) { + if (strpos($form_state['triggering_element']['#id'], 'remove') === FALSE) { + $breakpoints = isset($form_state['values']['breakpoints']) ? $form_state['values']['breakpoints'] : array(); + if (!empty($breakpoints)) { + foreach ($breakpoints as $key => $breakpointdata) { + if (!empty($breakpointdata['name'])) { + // Breakpoint is required. + if ($key == 'new') { + if (empty($breakpointdata['machine_name'])) { + form_set_error('breakpoints][' . $key . '][machine_name', 'Machine name field is required'); + } + } + } + } + } + } +} + +/** + * Admin form submit. + */ +function breakpoints_admin_breakpoints_submit($form, &$form_state) { + $breakpoints = $form_state['values']['breakpoints']; + $group_name = $form_state['group_name']; + $global_group = $group_name == ''; + $group = breakpoints_breakpoint_group_empty_object(); + if (!$global_group) { + // sort by weight, needed to store the right order in a group + uasort($breakpoints, '_breakpoints_sort_by_weight_array'); + } + $saved_breakpoints = array(); + if (!empty($breakpoints)) { + foreach ($breakpoints as $breakpointname => $breakpointdata) { + if (!empty($breakpointdata['name'])) { + $breakpoint = breakpoints_breakpoint_load_by_fullkey($breakpointname); + if ($breakpoint && $breakpointname != 'new') { + // only save the weight when on the global screen. + if ($global_group) { + $breakpoint->weight = $breakpointdata['weight']; + } + else { + $breakpoint->weight = $breakpointdata['global_weight']; + } + $breakpoint->breakpoint = $breakpointdata['breakpoint']; + $breakpoint->multipliers = isset($breakpointdata['multipliers']) ? $breakpointdata['multipliers'] : array(); + breakpoints_breakpoint_save($breakpoint); + $saved_breakpoints[] = $breakpointname; + } + else { + $breakpoint = new stdClass(); + $breakpoint->name = $breakpointdata['name']; + $breakpoint->breakpoint = $breakpointdata['breakpoint']; + $breakpoint->source = 'user'; + $breakpoint->source_type = 'custom'; + $breakpoint->weight = $breakpointdata['weight']; + $breakpoint->status = TRUE; + $breakpoint->multipliers = isset($breakpointdata['multipliers']) ? $breakpointdata['multipliers'] : array(); + $breakpoint->machine_name = 'custom.user.' . $breakpointdata['machine_name']; + breakpoints_breakpoint_save($breakpoint); + $saved_breakpoints[] = breakpoints_breakpoint_config_name($breakpoint); + } + } + } + if (!$global_group) { + $group = breakpoints_breakpoint_group_load($group_name); + if ($group) { + $group->breakpoints = $saved_breakpoints; + breakpoints_breakpoint_group_save($group); + } + } + } +} + +/** + * Admin form submit - Override theme breakpoints. + */ +function breakpoints_admin_breakpoints_submit_override($form, &$form_state) { + $group_name = $form_state['group_name']; + $group = breakpoints_breakpoint_group_empty_object(); + $global_group = $group_name == ''; + if (!$global_group) { + $group = breakpoints_breakpoint_group_load($group_name); + if ($group) { + breakpoints_breakpoints_group_override($group); + } + } +} + +/** + * Admin form submit - Revert theme breakpoints. + */ +function breakpoints_admin_breakpoints_submit_revert($form, &$form_state) { + $group_name = $form_state['group_name']; + $global_group = $group_name == ''; + if (!$global_group) { + $group = breakpoints_breakpoint_group_load($group_name); + if ($group) { + breakpoints_breakpoints_group_revert($group); + } + } +} + +/** + * Admin form submit - Reload theme breakpoints. + */ +function breakpoints_admin_breakpoints_submit_reload($form, &$form_state) { + $group_name = $form_state['group_name']; + $global_group = $group_name == ''; + if (!$global_group) { + $group = breakpoints_breakpoint_group_load($group_name); + if ($group) { + breakpoints_breakpoints_group_reload($group); + } + } +} + +/** + * Admin form submit - Export breakpoints to theme. + */ +function breakpoints_admin_breakpoints_submit_exporttotheme($form, &$form_state) { + $group_name = $form_state['group_name']; + $global_group = $group_name == ''; + if (!$global_group) { + $group = breakpoints_breakpoint_group_load($group_name); + if ($group) { + $breakpoints = breakpoints_breakpoints_group_exporttotheme($group); + if ($breakpoints) { + $export = array(); + foreach ($breakpoints as $breakpoint_name => $breakpoint) { + $export[] = 'breakpoints[' . $breakpoint_name . '] = ' . $breakpoint; + } + $form_state['exported_breakpoints'] = implode("\n", $export); + $form_state['rebuild'] = TRUE; + } + } + } +} + +/** + * Page callback. + */ +function breakpoints_admin_breakpoint_actions_page($group_name, $action, $breakpoint) { + if (in_array($action, array('enable', 'disable', 'delete', 'export'))) { + return drupal_get_form('breakpoints_admin_breakpoint_actions_form', $group_name, $action, $breakpoint); + } + return MENU_NOT_FOUND; +} + +/** + * Admin action form: enable, disable, delete, export + */ +function breakpoints_admin_breakpoint_actions_form($form, &$form_state, $group_name, $action, $breakpoint) { + switch ($action) { + case 'enable': + case 'disable': + case 'delete': + $form_state['group_name'] = $group_name; + $form_state['action'] = $action; + $form_state['breakpoint'] = $breakpoint; + $question = t('Are you sure you want to %action %breakpoint', array( + '%action' => $action, + '%breakpoint' => $breakpoint, + )); + if (!empty($group_name)) { + $path = 'admin/config/media/breakpoints/groups/' . $group_name; + } + else { + $path = 'admin/config/media/breakpoints'; + } + $form = confirm_form($form, $question, $path, ''); + break; + case 'export': + $form = drupal_get_form('breakpoints_admin_breakpoint_export_form', $breakpoint); + break; + } + return $form; +} + +/** + * Admin action form submit + */ +function breakpoints_admin_breakpoint_actions_form_submit($form, &$form_state) { + $group_name = $form_state['group_name']; + $action = $form_state['action']; + $breakpoint = $form_state['breakpoint']; + switch ($action) { + case 'delete': + breakpoints_breakpoint_delete_by_fullkey($breakpoint); + break; + case 'enable': + case 'disable': + breakpoints_breakpoint_toggle_status($breakpoint); + break; + } + if (!empty($group_name)) { + $form_state['redirect'] = 'admin/config/media/breakpoints/groups/' . $group_name; + } + else { + $form_state['redirect'] = 'admin/config/media/breakpoints'; + } +} + +function breakpoints_add_style_form($form, &$form_state) { + module_load_include('inc', 'image', 'image.admin'); + $form['style'] = array( + '#title' => t('Image style'), + '#type' => 'select', + '#options' => array_filter(image_style_options(FALSE), '_breakpoints_filter_styles'), + '#required' => TRUE, + '#description' => t('This image style will be cloned to create the responsive style'), + ); + + $form['base_name'] = array( + '#type' => 'textfield', + '#size' => '64', + '#title' => t('Image style base name'), + '#description' => t('The name is used in URLs for generated images. Use only lowercase alphanumeric characters, underscores (_), and hyphens (-).'), + '#element_validate' => array('image_style_name_validate'), + '#required' => TRUE, + ); + $breakpoints = breakpoints_breakpoint_load_all_active(); + if (isset($breakpoints) && !empty($breakpoints)) { + $options = array(); + foreach ($breakpoints as $breakpoint) { + foreach ($breakpoint->multipliers as $multiplier) { + $options[str_replace('.', '_', $breakpoint->machine_name . '_' . $multiplier)] = $breakpoint->name . ' [' . $breakpoint->breakpoint . ', multiplier:' . $multiplier . ']'; + } + } + $form['breakpoints'] = array( + '#title' => t('breakpoints'), + '#type' => 'checkboxes', + '#options' => $options, + '#default_value' => drupal_map_assoc(array_keys($options)), + '#description' => t('Select the breakpoints to create an image style for'), + '#required' => TRUE, + ); + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Create'), + ); + } + else { + $form['redirect_link'] = array( + '#markup' => t("You need to create a breakpoint first before creating a responsive style. ") . l('Click here', 'admin/config/media/breakpoints') . t(" to continue with configuring breakpoints."), + ); + } + return $form; +} + +function breakpoints_add_style_form_validate($form, &$form_state) { + foreach (array_filter($form_state['values']['breakpoints']) as $breakpoint) { + if (drupal_strlen($form_state['values']['base_name'] . $breakpoint) > 64) { + form_set_error( + 'breakpoints', + t( + 'Could not generate image styles because the generated name @name is longer than the maximum allowed length of 64 characters for image style names (@length characters).', + array('@name' => $form_state['values']['base_name'] . $breakpoint, '@length' => drupal_strlen($form_state['values']['base_name'] . $breakpoint)) + ) + ); + } + if (image_style_load($form_state['values']['base_name'] . $breakpoint)) { + form_set_error('breakpoints', t('An image style with the name @name already exists', array('@name' => $form_state['values']['base_name'] . $breakpoint))); + } + } +} + +function breakpoints_add_style_form_submit($form, &$form_state) { + $base = image_style_load($form_state['values']['style']); + if (!isset($base['effects'])) { + $base['effects'] = array(); + } + foreach (array_filter($form_state['values']['breakpoints']) as $breakpoint) { + $new_style = array( + 'name' => $form_state['values']['base_name'] . $breakpoint, + ); + $style = image_style_save($new_style); + if ($style) { + foreach ($base['effects'] as $effect) { + $effect['isid'] = $style['isid']; + $effect['data']['style_name'] = $new_style['name']; + unset($effect['ieid']); + image_effect_save($effect); + } + } + } + $form_state['redirect'] = 'admin/config/media/image-styles'; + drupal_set_message(t('The new styles have been created')); +} + +function breakpoints_admin_breakpoint_group_edit_form($form, &$form_state, $machine_name = '') { + $form = array(); + $group = breakpoints_breakpoint_group_load($machine_name); + $breakpoints = breakpoints_breakpoint_load_all(); + if (empty($breakpoints)) { + return breakpoints_admin_breakpoint_group_edit_form_no_breakpoints(); + } + + $form_state['#breakpoint_group'] = $group; + $is_new = $machine_name == ''; + $form_state['#is_new'] = $is_new; + + $form['name'] = array( + '#type' => 'textfield', + '#size' => '64', + '#title' => t('group name'), + '#required' => TRUE, + '#default_value' => isset($group->name) ? $group->name : '', + '#disabled' => !$is_new, + ); + + $form['machine_name'] = array( + '#type' => 'machine_name', + '#size' => '64', + '#title' => t('Machine name'), + '#required' => TRUE, + '#default_value' => isset($group->machine_name) ? $group->machine_name : '', + '#disabled' => !$is_new, + '#machine_name' => array( + 'exists' => 'breakpoints_breakpoint_group_name_exists', + ), + ); + + foreach ($breakpoints as $breakpoint_name => $breakpoint) { + $options[$breakpoint_name] = $breakpoint->name . ' [' . $breakpoint->breakpoint . ']'; + } + + $form['breakpoints'] = array( + '#title' => 'Select the breakpoints you want to use in this group', + '#type' => 'checkboxes', + '#options' => $options, + '#default_value' => isset($group->breakpoints) ? drupal_map_assoc($group->breakpoints) : array(), + '#required' => TRUE, + ); + + // Buttons + $form['buttons'] = array( + '#type' => 'container', + ); + + // Submit button + $form['buttons']['submit'] = array( + '#type' => 'submit', + '#value' => t('Save'), + ); + + if (!$is_new && $group->type == BREAKPOINTS_SOURCE_TYPE_CUSTOM) { + $form['buttons']['deletelink'] = array( + '#type' => 'markup', + '#markup' => l(t('Delete this group'), 'admin/config/media/breakpoints/groups/' . $group->machine_name . '/delete', array( + 'query' => drupal_get_destination(), + )), + ); + } + + return $form; +} + +function breakpoints_admin_breakpoint_group_edit_form_no_breakpoints() { + $form = array(); + + $form['info'] = array( + '#type' => 'markup', + '#markup' => t("There're no breakpoints defined, you'll have to !create them first.", array('!create' => l(t('create'), 'admin/config/media/breakpoints'))), + ); + + return $form; +} + +function breakpoints_admin_breakpoint_group_edit_form_validate($form, &$form_state) { + $name = $form_state['values']['machine_name']; + $label = $form_state['values']['name']; +} + +function breakpoints_admin_breakpoint_group_edit_form_submit($form, &$form_state) { + $machine_name = $form_state['values']['machine_name']; + $name = $form_state['values']['name']; + $breakpoints = array(); + foreach ($form_state['values']['breakpoints'] as $breakpoint => $status) { + if ($status) { + $breakpoints[] = $breakpoint; + } + } + $is_new = $form_state['#is_new']; + + if ($is_new) { + $new_group = breakpoints_breakpoint_group_empty_object(); + $new_group->machine_name = $machine_name; + $new_group->name = $name; + $new_group->type = BREAKPOINTS_SOURCE_TYPE_CUSTOM; + $new_group->breakpoints = $breakpoints; + breakpoints_breakpoint_group_save($new_group); + menu_rebuild(); + $form_state['redirect'] = 'admin/config/media/breakpoints/groups/' . $machine_name; + drupal_set_message(t('The new group have been created')); + } + else { + $existing_group = breakpoints_breakpoint_group_load($machine_name); + $existing_group->breakpoints = $breakpoints; + breakpoints_breakpoint_group_save($existing_group); + } +} + +/** + * Delete a group. + */ +function breakpoints_admin_breakpoint_group_delete_form($form, &$form_state, $machine_name) { + $group = breakpoints_breakpoint_group_load($machine_name); + $form_state['machine_name'] = $machine_name; + $question = t('Are you sure you want to delete %group', array( + '%group' => $group->name, + )); + $path = 'admin/config/media/breakpoints/groups'; + return confirm_form($form, $question, $path, ''); +} + +/** + * Delete a group. + */ +function breakpoints_admin_breakpoint_group_delete_form_submit($form, &$form_state) { + $machine_name = $form_state['machine_name']; + breakpoints_breakpoint_group_delete_by_name($machine_name); + menu_rebuild(); + $form_state['redirect'] = 'admin/config/media/breakpoints'; +} + +/** + * Export a group. + */ +function breakpoints_admin_breakpoint_group_export_form($form, &$form_state, $machine_name) { + // Create the export code textarea. + ctools_include('export'); + $group = breakpoints_breakpoint_group_load($machine_name); + if (!$group || !$machine_name) { + $group = new stdClass(); + } + $group_export = ctools_export_crud_export('breakpoint_group', $group); + + $form['group_export'] = array( + '#type' => 'textarea', + '#title' => t('Breakpoint group code'), + '#rows' => count(explode("\n", $group_export)), + '#default_value' => $group_export, + ); + + $breakpoints_export = NULL; + if (isset($group->breakpoints)) { + foreach ($group->breakpoints as $breakpoint) { + if (!is_array($breakpoint) && !is_object($breakpoint)) { + $breakpoint = breakpoints_breakpoint_load_by_fullkey($breakpoint); + } + $breakpoints_export .= ctools_export_object('breakpoints', $breakpoint); + $breakpoints_export .= "\n"; + } + } + + $form['breakpoints_export'] = array( + '#type' => 'textarea', + '#title' => t('Breakpoints code'), + '#rows' => count(explode("\n", $breakpoints_export)), + '#default_value' => $breakpoints_export, + ); + + return $form; +} + +/** + * Import a breakpoint group. + */ +function breakpoints_admin_breakpoint_group_import_form($form, &$form_state) { + $form['import'] = array( + '#type' => 'textarea', + '#rows' => 10, + ); + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Import') + ); + return $form; +} + +/** + * Validate a breakpoint group import. + */ +function breakpoints_admin_breakpoint_group_import_form_validate($form, &$form_state) { + ctools_include('export'); + $code = $form_state['values']['import']; + $group = ctools_export_crud_import('breakpoint_group', $code); + if (!breakpoints_breakpoint_group_validate($group)) { + form_set_error('import', t('Not a valid group object')); + return; + } + if (breakpoints_breakpoint_group_name_exists($group->machine_name)) { + form_set_error('import', t('A group with machine name %name already exists', array('%name' => $group->machine_name))); + return; + } + foreach ($group->breakpoints as $key => $breakpoint) { + // check if the breakpoint is a fully loaded object. + if (is_array($breakpoint) || is_object($breakpoint)) { + if (!breakpoints_breakpoint_validate($breakpoint)) { + form_set_error('import', t('The breakpoint group contains an invalid breakpoint.')); + return; + } + } + } + // Manually imported groups are the same as custom made groups. + $group->type = BREAKPOINTS_SOURCE_TYPE_CUSTOM; + form_set_value($form['import'], $group, $form_state); +} + +/** + * Import breakpoint group. + */ +function breakpoints_admin_breakpoint_group_import_form_submit($form, &$form_state) { + $group = $form_state['values']['import']; + + foreach ($group->breakpoints as $key => $breakpoint) { + // check if the breakpoint is a fully loaded object. + if (is_array($breakpoint) || is_object($breakpoint)) { + $breakpoint = (object)$breakpoint; + // If the breakpoints exist, only overwrite the custom ones. + if ($existing_breakpoint = breakpoints_breakpoint_load_by_fullkey($breakpoint->machine_name)) { + if ($breakpoint->source_type == BREAKPOINTS_SOURCE_TYPE_CUSTOM) { + $breakpoint = (object)array_merge((array)$existing_breakpoint, (array)$breakpoint); + breakpoints_breakpoint_save($breakpoint); + } + } + else { + breakpoints_breakpoint_save($breakpoint); + } + $group->breakpoints[$key] = $breakpoint->machine_name; + } + } + if (breakpoints_breakpoint_group_save($group)) { + drupal_set_message(t('Group %group saved.', array('%group' => $group->name))); + $form_state['redirect'] = 'admin/config/media/breakpoints/groups/' . $group->machine_name; + } + else { + drupal_set_message(t('Something went wrong, we could not save the group', 'error')); + } +} + +/** + * Export a breakpoint. + */ +function breakpoints_admin_breakpoint_export_form($form, $form_state, $fullkey) { + // Create the export code textarea. + ctools_include('export'); + $breakpoint = breakpoints_breakpoint_load_by_fullkey($fullkey); + if (!$breakpoint) { + $breakpoint = new stdClass(); + } + $export = ctools_export_object('breakpoints', $breakpoint); + + $form['export'] = array( + '#type' => 'textarea', + '#title' => t('Breakpoint code'), + '#rows' => 20, + '#default_value' => $export, + ); + return $form; +} + +/** + * Import breakpoint. + */ +function breakpoints_admin_breakpoint_import_form($form, $form_state) { + $form['import'] = array( + '#type' => 'textarea', + '#rows' => 10, + ); + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Import') + ); + return $form; +} + +/** + * Validate a breakpoint import. + */ +function breakpoints_admin_breakpoint_import_form_validate($form, &$form_state) { + ctools_include('export'); + $code = $form_state['values']['import']; + $breakpoint = ctools_export_crud_import('breakpoints', $code); + if (!breakpoints_breakpoint_validate($breakpoint)) { + form_set_error('import', t('Not a valid breakpoint object')); + } + else { + if (breakpoints_breakpoint_machine_name_exists($breakpoint->machine_name)) { + form_set_error('import', t('A breakpoint with machine name %name already exists', array('%name' => $breakpoint->machine_name))); + } + else { + form_set_value($form['import'], $breakpoint, $form_state); + } + } +} + +/** + * Import breakpoint. + */ +function breakpoints_admin_breakpoint_import_form_submit($form, &$form_state) { + $breakpoint = $form_state['values']['import']; + if (breakpoints_breakpoint_save($breakpoint)) { + drupal_set_message(t('Breakpoint %breakpoint saved.', array('%breakpoint' => $breakpoint->name))); + $form_state['redirect'] = 'admin/config/media/breakpoints/'; + } + else { + drupal_set_message(t('Something went wrong, we could not save the breakpoint'), 'error'); + } +} + +/** + * Multipliers administration form. + */ +function breakpoints_multipliers_form($form, &$form_state) { + $settings = breakpoints_settings(); + $multipliers = drupal_map_assoc($settings->multipliers); + if (isset($multipliers['1x'])) { + unset($multipliers['1x']); + } + $form['multipliers'] = array( + '#type' => 'container', + '#tree' => TRUE, + '#theme' => 'breakpoints_multipliers_table_form', + ); + $form['multipliers']['1x'] = array( + '#markup' => '1x', + ); + foreach ($multipliers as $multiplier) { + $form['multipliers'][$multiplier] = array( + '#type' => 'textfield', + '#title' => '', + '#required' => FALSE, + '#default_value' => $multiplier, + ); + } + $form['multipliers']['new'] = array( + '#type' => 'textfield', + '#title' => '', + '#required' => FALSE, + '#default_value' => '', + '#description' => t('Multiplier like 1.5x, 2x.'), + ); + + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Save'), + ); + return $form; +} + +function theme_breakpoints_multipliers_table_form($element) { + $form = $element['form']; + $header = array(t('Label'), t('Operations')); + foreach (element_children($form) as $multiplier) { + $row = array(); + $row[] = drupal_render($form[$multiplier]); + $row[] = in_array($multiplier, array('new', '1x')) ? '' : l(t('Delete'), 'admin/config/media/breakpoints/multipliers/' . $multiplier . '/delete'); + $rows[] = $row; + } + return theme('table', array('header' => $header, 'rows' => $rows)); +} + +function breakpoints_multipliers_form_validate($form, $form_state) { + $multipliers = $form_state['values']['multipliers']; + $saved_multipliers = array( + '1x' => '1x', + ); + foreach ($multipliers as $form_key => $multiplier) { + if ($multiplier == '' && $form_key != 'new') { + form_set_error('multipliers][' . $form_key, t('Label is required.')); + } + if ($multiplier != '') { + if (isset($saved_multipliers[$multiplier])) { + form_set_error('multipliers][' . $form_key, t('Label must be unique.')); + } + $saved_multipliers[$multiplier] = $multiplier; + } + } +} + +function breakpoints_multipliers_form_submit($form, &$form_state) { + $multipliers = array_values(array_filter($form_state['values']['multipliers'])); + array_unshift($multipliers, '1x'); + breakpoints_settings_save($multipliers); + drupal_set_message(t('Multiplier settings are saved.')); +} + +function breakpoints_admin_multiplier_delete_form($form, &$form_state, $multiplier) { + $path = 'admin/config/media/breakpoints/multipliers'; + if ($multiplier == '1x') { + $form['multiplier'] = array( + '#markup' => t('Multiplier %multiplier can not be deleted! !link', array('%multiplier' => $multiplier, '!link' => l(t('Back to overview page.'), $path))) + ); + return $form; + } + $form['multiplier'] = array( + '#type' => 'value', + '#value' => $multiplier, + ); + return confirm_form($form, t('Are you sure you want to delete multiplier %multiplier', array('%multiplier' => $multiplier)), $path); +} + +function breakpoints_admin_multiplier_delete_form_submit($form, &$form_state) { + $settings = breakpoints_settings(); + $multiplier = $form_state['values']['multiplier']; + $multipliers = drupal_map_assoc($settings->multipliers); + if (isset($multipliers[$multiplier])) { + unset($multipliers[$multiplier]); + } + breakpoints_settings_save(array_values($multipliers)); + drupal_set_message(t('Multiplier %multiplier was deleted', array('%multiplier' => $multiplier))); + $form_state['redirect'] = 'admin/config/media/breakpoints/multipliers'; +} + +function breakpoints_admin_settings_form($form, &$form_state) { + $form['breakpoints_hide_overridden_breakpoints'] = array( + '#type' => 'checkbox', + '#description' => t('When overriding breakpoints defined by a theme, hide them on the overview page'), + '#title' => t('Hide overridden breakpoints'), + '#default_value' => variable_get('breakpoints_hide_overridden_breakpoints', 1), + ); + return system_settings_form($form); +} + +/** + * Duplicate group form. + */ +function breakpoints_admin_breakpoint_group_duplicate_form($form, &$form_state, $breakpoint_group_name) { + $form = array(); + + $src_group = breakpoints_breakpoint_group_load($breakpoint_group_name); + $form_state['#src_group'] = $src_group; + + $form['#attached']['css'][] = drupal_get_path('module', 'breakpoints') . '/css/breakpoints.admin.css'; + $form['name'] = array( + '#type' => 'textfield', + '#size' => '64', + '#title' => t('New group name'), + '#required' => TRUE, + '#default_value' => t('Duplicate of') . ' ' . $src_group->name, + ); + + $form['machine_name'] = array( + '#type' => 'machine_name', + '#size' => '64', + '#title' => t('Machine name'), + '#required' => TRUE, + '#default_value' => '', + '#machine_name' => array( + 'exists' => 'breakpoints_breakpoint_group_name_exists', + ), + ); + + // Buttons + $form['buttons'] = array( + '#type' => 'container', + ); + + // Submit button + $form['buttons']['submit'] = array( + '#type' => 'submit', + '#value' => t('Save'), + ); + + $form['buttons']['cancellink'] = array( + '#type' => 'markup', + '#markup' => l( + t('Cancel'), 'admin/config/media/breakpoints/groups/' . $src_group->machine_name, + array( + 'attributes' => array('class' => array('breakpoints-group-operations-link', 'breakpoints-group-operations-cancel-link')), + ) + ), + ); + + return $form; +} + +/** + * Duplicate group form validate. + */ +function breakpoints_admin_breakpoint_group_duplicate_form_validate($form, &$form_state) { +} + +/** + * Duplicate group form submit. + */ +function breakpoints_admin_breakpoint_group_duplicate_form_submit($form, &$form_state) { + $machine_name = $form_state['values']['machine_name']; + $name = $form_state['values']['name']; + $src_group = $form_state['#src_group']; + if ($src_group) { + breakpoints_breakpoints_group_duplicate($src_group, $name, $machine_name); + // Clear the Ctools export API cache. + ctools_include('export'); + ctools_export_load_object_reset('breakpoint_group'); + menu_rebuild(); + $form_state['redirect'] = 'admin/config/media/breakpoints/groups/' . $machine_name; + drupal_set_message(t('The new group have been created')); + } +} diff --git a/frontend/drupal/sites/all/modules/breakpoints/breakpoints.info b/frontend/drupal/sites/all/modules/breakpoints/breakpoints.info new file mode 100644 index 000000000..4ab67b9f0 --- /dev/null +++ b/frontend/drupal/sites/all/modules/breakpoints/breakpoints.info @@ -0,0 +1,13 @@ +name = Breakpoints +description = Manage breakpoints +core = 7.x +dependencies[] = ctools +files[] = breakpoints.module +files[] = breakpoints.test +configure = admin/config/media/breakpoints +; Information added by Drupal.org packaging script on 2018-03-08 +version = "7.x-1.6" +core = "7.x" +project = "breakpoints" +datestamp = "1520497694" + diff --git a/frontend/drupal/sites/all/modules/breakpoints/breakpoints.install b/frontend/drupal/sites/all/modules/breakpoints/breakpoints.install new file mode 100644 index 000000000..b06a639cb --- /dev/null +++ b/frontend/drupal/sites/all/modules/breakpoints/breakpoints.install @@ -0,0 +1,186 @@ + 'Breakpoints', + 'fields' => array( + 'id' => array( + 'type' => 'serial', + 'not null' => TRUE, + 'description' => 'The internal identifier for a responsive images suffix', + 'no export' => TRUE, // Do not export database-only keys. + ), + 'machine_name' => array( + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'description' => 'The machine name of the breakpoint.', + ), + 'name' => array( + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'description' => 'The name of the breakpoint.', + ), + 'breakpoint' => array( + 'type' => 'varchar', + 'length' => 255, + 'description' => 'media query', + 'not null' => TRUE, + 'default' => '', + ), + 'source' => array( + 'type' => 'varchar', + 'length' => 255, + 'description' => 'name of theme, module', + 'not null' => TRUE, + 'default' => '', + ), + 'source_type' => array( + 'type' => 'varchar', + 'length' => 255, + 'description' => 'is breakpoint defined by theme, module or custom', + 'not null' => TRUE, + 'default' => '', + ), + 'status' => array( + 'type' => 'int', + 'description' => 'enabled or disabled', + 'not null' => TRUE, + 'default' => 1, + ), + 'weight' => array( + 'type' => 'int', + 'description' => 'weight', + 'not null' => TRUE, + 'default' => 0, + ), + 'multipliers' => array( + 'type' => 'blob', + 'description' => 'all enabled multipliers', + 'not null' => TRUE, + 'serialize' => TRUE, + ), + ), + 'primary key' => array('id'), + 'unique keys' => array( + 'machine_name' => array('machine_name'), + ), + // CTools exportable object definition + 'export' => array( + 'key' => 'machine_name', + 'key name' => 'machine_name', + 'primary key' => 'id', + 'identifier' => 'breakpoint', + 'admin_title' => 'label', + 'default hook' => 'default_breakpoints', + 'api' => array( + 'owner' => 'breakpoints', + 'api' => 'default_breakpoints', + 'minimum_version' => 1, + 'current_version' => 1, + ), + 'load all callback' => '_breakpoints_breakpoint_load_all_callback', + ), + ); + $schema['breakpoint_group'] = array( + 'description' => 'Breakpoint group', + 'fields' => array( + 'id' => array( + 'type' => 'serial', + 'not null' => TRUE, + 'description' => 'The internal identifier for a responsive images suffix', + 'no export' => TRUE, // Do not export database-only keys. + ), + 'machine_name' => array( + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'description' => 'The machine name of the breakpoint.', + ), + 'name' => array( + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'description' => 'The name of the breakpoint.', + ), + 'breakpoints' => array( + 'type' => 'blob', + 'description' => 'breakpoints', + 'not null' => TRUE, + 'serialize' => TRUE, + // we do not export options saved in this column, we export the fully loaded objects. + 'export callback' => 'breakpoint_group_export_breakpoints', + ), + 'type' => array( + 'type' => 'varchar', + 'length' => 255, + 'description' => 'theme, module or custom', + 'not null' => TRUE, + 'default' => '', + ), + 'overridden' => array( + 'type' => 'int', + 'description' => 'Boolean indicating if this group is overriden', + 'not null' => TRUE, + 'default' => 0, + ), + ), + 'primary key' => array('id'), + 'unique keys' => array( + 'machine_name' => array('machine_name'), + ), + // CTools exportable object definition + 'export' => array( + 'key' => 'machine_name', + 'key name' => 'breakpoint group machine_name', + 'primary key' => 'id', + 'identifier' => 'breakpoint_group', + 'admin_title' => 'label', + 'default hook' => 'default_breakpoint_group', + 'export type string' => 'ctools_type', + 'api' => array( + 'owner' => 'breakpoints', + 'api' => 'default_breakpoint_group', + 'minimum_version' => 1, + 'current_version' => 1, + ), + 'load all callback' => 'breakpoints_breakpoint_group_load_all', + ), + ); + return $schema; +} + +/** + * Add the 'overridden' column to the breakpoint_group table. + */ +function breakpoints_update_7101(&$sandbox) { + db_add_field( + 'breakpoint_group', + 'overridden', + array( + 'type' => 'int', + 'description' => 'Boolean indicating if this group is overriden', + 'not null' => TRUE, + 'default' => 0, + ) + ); +} + +/** + * Add the '1x' multiplier to all breakpoints. + */ +function breakpoints_update_7102(&$sandbox) { + $breakpoints = breakpoints_breakpoint_load_all(); + foreach ($breakpoints as $breakpoint) { + breakpoints_breakpoint_save($breakpoint); + } +} diff --git a/frontend/drupal/sites/all/modules/breakpoints/breakpoints.module b/frontend/drupal/sites/all/modules/breakpoints/breakpoints.module new file mode 100644 index 000000000..8504d4a81 --- /dev/null +++ b/frontend/drupal/sites/all/modules/breakpoints/breakpoints.module @@ -0,0 +1,970 @@ + array( + 'title' => t('Administer Breakpoints'), + 'description' => t('Administer all breakpoints'), + ), + ); +} + +/** + * Implements hook_ctools_plugin_directory(). + */ +function breakpoints_ctools_plugin_directory($module, $plugin) { + if ($module == 'ctools' && $plugin == 'export_ui') { + return 'plugins/' . $plugin; + } +} + +/** + * Implements hook_ctools_plugin_api(). + */ +function breakpoints_ctools_plugin_api($owner, $api) { + return array('version' => 1); +} + +/** + * Implements hook_enable(). + * Import breakpoints from all enabled themes. + */ +function breakpoints_enable() { + $themes = list_themes(); + foreach ($themes as $theme_key => $theme) { + if (!$theme->status) { + unset($themes[$theme_key]); + } + } + breakpoints_themes_enabled(array_keys($themes)); +} + +/** + * Implements hook_themes_enabled(); + * Import breakpoints from all new enabled themes. + * Do not use breakpoints_breakpoints_group_reload_from_theme as is clears the cache. + */ +function breakpoints_themes_enabled($theme_list, $rebuild_menu = TRUE) { + $themes = list_themes(); + $updated = FALSE; + $new_breakpoint_group = FALSE; + foreach ($theme_list as $theme_key) { + if (isset($themes[$theme_key]->info['breakpoints'])) { + $updated = TRUE; + $weight = 0; + $theme_settings = $themes[$theme_key]->info['breakpoints']; + $multipliers = isset($themes[$theme_key]->info['multipliers']) ? $themes[$theme_key]->info['multipliers'] : array(); + $settings = breakpoints_settings(); + $current_multipliers = drupal_map_assoc($settings->multipliers); + $breakpoint_group = breakpoints_breakpoint_group_load($theme_key); + if(!$breakpoint_group) { + // Build a group for each theme + $new_breakpoint_group = TRUE; + $breakpoint_group = breakpoints_breakpoint_group_empty_object(); + $breakpoint_group->machine_name = $theme_key; + $breakpoint_group->name = $themes[$theme_key]->info['name']; + $breakpoint_group->type = BREAKPOINTS_SOURCE_TYPE_THEME; + } + foreach ($theme_settings as $name => $media_query) { + $breakpoint = breakpoints_breakpoint_load($name, $theme_key, 'theme'); + if (!$breakpoint) { + $breakpoint = breakpoints_breakpoint_empty_object(); + $breakpoint->name = $name; + $breakpoint->source = $theme_key; + $breakpoint->source_type = 'theme'; + $breakpoint->theme = ''; + $breakpoint->status = TRUE; + $breakpoint->weight = $weight++; + $breakpoint->machine_name = breakpoints_breakpoint_config_name($breakpoint); + } + $breakpoint->breakpoint = $media_query; + $breakpoint->multipliers = isset($multipliers[$name]) ? drupal_map_assoc($multipliers[$name]) : array(); + $current_multipliers += drupal_map_assoc($breakpoint->multipliers); + breakpoints_breakpoint_save($breakpoint); + $breakpoint_group->breakpoints[] = $breakpoint->machine_name; + } + breakpoints_settings_save($current_multipliers); + breakpoints_breakpoint_group_save($breakpoint_group); + if($new_breakpoint_group) { + $message_text = 'The breakpoints from theme %theme are imported and a new group is created.'; + } else { + $message_text = 'The breakpoints from theme %theme are imported and an existing group was updated.'; + } + $message = t($message_text, array( + '%theme' => $themes[$theme_key]->info['name'], + '@url' => url('admin/config/media/breakpoints/groups/' . $theme_key), + )); + drupal_set_message($message, 'status'); + } + } + if ($rebuild_menu && $updated) { + variable_set('menu_rebuild_needed', TRUE); + } +} + +/** + * Implements hook_themes_disabled(); + * Remove breakpoints from all disabled themes. + */ +function breakpoints_themes_disabled($theme_list) { + $themes = list_themes(); + foreach ($theme_list as $theme_key) { + $breakpoints = breakpoints_breakpoint_load_all_theme($theme_key); + foreach ($breakpoints as $breakpoint) { + breakpoints_breakpoint_delete($breakpoint, $theme_key); + } + breakpoints_breakpoint_group_delete_by_name($theme_key); + } + variable_set('menu_rebuild_needed', TRUE); +} + +/** + * Implements hook_menu(). + */ +function breakpoints_menu() { + $items = array(); + + // @todo: link to all breakpoints and a list of all groups + // cf theme settings page + $items['admin/config/media/breakpoints'] = array( + 'title' => 'Breakpoints', + 'description' => 'Manage breakpoints', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('breakpoints_admin_breakpoints'), + 'access arguments' => array('administer breakpoints'), + 'file' => 'breakpoints.admin.inc', + ); + + $items['admin/config/media/breakpoints/create_style'] = array( + 'title' => 'Add responsive style', + 'description' => 'Add a responsive image style', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('breakpoints_add_style_form'), + 'access arguments' => array('administer breakpoints'), + 'file' => 'breakpoints.admin.inc', + 'type' => MENU_LOCAL_TASK, + 'weight' => 30, + ); + + $items['admin/config/media/breakpoints/multipliers'] = array( + 'title' => 'Multipliers', + 'description' => 'Manage multipliers', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('breakpoints_multipliers_form'), + 'access arguments' => array('administer breakpoints'), + 'file' => 'breakpoints.admin.inc', + 'type' => MENU_LOCAL_TASK, + 'weight' => 20, + ); + + $items['admin/config/media/breakpoints/settings'] = array( + 'title' => 'Settings', + 'description' => 'Manage breakpoint settings', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('breakpoints_admin_settings_form'), + 'access arguments' => array('administer breakpoints'), + 'file' => 'breakpoints.admin.inc', + 'type' => MENU_LOCAL_TASK, + 'weight' => 30, + ); + + $items['admin/config/media/breakpoints/multipliers/%/delete'] = array( + 'title' => '', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('breakpoints_admin_multiplier_delete_form', 5), + 'type' => MENU_CALLBACK, + 'access arguments' => array('administer breakpoints'), + 'file' => 'breakpoints.admin.inc', + 'weight' => 15, + ); + + $items['admin/config/media/breakpoints/groups'] = array( + 'title' => 'Groups', + 'type' => MENU_DEFAULT_LOCAL_TASK, + 'weight' => 10, + ); + + $items['admin/config/media/breakpoints/groups/global'] = array( + 'title' => 'All breakpoints', + 'type' => MENU_DEFAULT_LOCAL_TASK, + 'weight' => -1, + ); + + $items['admin/config/media/breakpoints/groups/add'] = array( + 'title' => 'Add a new group', + 'page arguments' => array('breakpoints_admin_breakpoint_group_edit_form'), + 'type' => MENU_LOCAL_TASK, + 'access arguments' => array('administer breakpoints'), + 'file' => 'breakpoints.admin.inc', + 'weight' => 99, + ); + + $items['admin/config/media/breakpoints/groups/import'] = array( + 'title' => 'Import a new group', + 'page arguments' => array('breakpoints_admin_breakpoint_group_import_form'), + 'type' => MENU_LOCAL_TASK, + 'access arguments' => array('administer breakpoints'), + 'file' => 'breakpoints.admin.inc', + 'weight' => 99, + ); + + $items['admin/config/media/breakpoints/groups/import-breakpoint'] = array( + 'title' => 'Import a new breakpoint', + 'page arguments' => array('breakpoints_admin_breakpoint_import_form'), + 'type' => MENU_LOCAL_TASK, + 'access arguments' => array('administer breakpoints'), + 'file' => 'breakpoints.admin.inc', + 'weight' => 99, + ); + + $items['admin/config/media/breakpoints/%/%'] = array( + 'title' => '', + 'page callback' => 'breakpoints_admin_breakpoint_actions_page', + 'page arguments' => array('', 4, 5), + 'type' => MENU_CALLBACK, + 'access arguments' => array('administer breakpoints'), + 'file' => 'breakpoints.admin.inc', + 'weight' => 15, + ); + + $breakpoint_groups = breakpoints_breakpoint_group_load_all(); + foreach ($breakpoint_groups as $breakpoint_group_name => $breakpoint_group) { + if (!empty($breakpoint_group->machine_name)) { + $items['admin/config/media/breakpoints/groups/' . $breakpoint_group->machine_name] = array( + 'title' => $breakpoint_group->name, + 'page arguments' => array('breakpoints_admin_breakpoints', $breakpoint_group->machine_name), + 'type' => MENU_LOCAL_TASK, + 'access arguments' => array('administer breakpoints'), + 'file' => 'breakpoints.admin.inc', + 'weight' => 15, + ); + $items['admin/config/media/breakpoints/groups/' . $breakpoint_group->machine_name . '/edit'] = array( + 'title' => 'Edit ' . $breakpoint_group->name, + 'page arguments' => array('breakpoints_admin_breakpoint_group_edit_form', $breakpoint_group->machine_name), + 'type' => MENU_CALLBACK, + 'access arguments' => array('administer breakpoints'), + 'file' => 'breakpoints.admin.inc', + 'weight' => 15, + ); + $items['admin/config/media/breakpoints/groups/' . $breakpoint_group->machine_name . '/delete'] = array( + 'title' => 'Delete ' . $breakpoint_group->name, + 'page arguments' => array('breakpoints_admin_breakpoint_group_delete_form', $breakpoint_group->machine_name), + 'type' => MENU_CALLBACK, + 'access arguments' => array('administer breakpoints'), + 'file' => 'breakpoints.admin.inc', + 'weight' => 15, + ); + + $items['admin/config/media/breakpoints/groups/' . $breakpoint_group->machine_name . '/export'] = array( + 'title' => 'Export ' . $breakpoint_group->name, + 'page arguments' => array('breakpoints_admin_breakpoint_group_export_form', $breakpoint_group->machine_name), + 'type' => MENU_LOCAL_ACTION, + 'access arguments' => array('administer breakpoints'), + 'file' => 'breakpoints.admin.inc', + 'weight' => 15, + ); + + $items['admin/config/media/breakpoints/groups/' . $breakpoint_group->machine_name . '/duplicate'] = array( + 'title' => 'Duplicate ' . $breakpoint_group->name, + 'page arguments' => array('breakpoints_admin_breakpoint_group_duplicate_form', $breakpoint_group->machine_name), + 'type' => MENU_CALLBACK, + 'access arguments' => array('administer breakpoints'), + 'file' => 'breakpoints.admin.inc', + 'weight' => 15, + ); + + $items['admin/config/media/breakpoints/groups/' . $breakpoint_group->machine_name . '/%/%'] = array( + 'title' => '', + 'page arguments' => array('breakpoints_admin_breakpoint_actions_form', $breakpoint_group->machine_name, 6, 7), + 'type' => MENU_CALLBACK, + 'access arguments' => array('administer breakpoints'), + 'file' => 'breakpoints.admin.inc', + 'weight' => 15, + ); + } + } + + return $items; +} + +/** + * Load general settings. + */ +function breakpoints_settings() { + $config = new StdClass; + $config->multipliers = variable_get('breakpoints_multipliers', array('1x', '1.5x', '2x')); + return $config; +} + +/** + * Save general settings. + */ +function breakpoints_settings_save($multipliers) { + variable_set('breakpoints_multipliers', $multipliers); +} + +/** + * Sort breakpoints by weight. + */ +function _breakpoints_sort_by_weight($a, $b) { + if (isset($a->weight) && isset($b->weight)) { + if ($a->weight == $b->weight) { + if (isset($a->source_type) && $a->source_type == BREAKPOINTS_SOURCE_TYPE_CUSTOM) { + return -1; + } + if (isset($b->source_type) && $b->source_type == BREAKPOINTS_SOURCE_TYPE_CUSTOM) { + return 1; + } + return 0; + } + return ($a->weight < $b->weight) ? -1 : 1; + } + return 0; +} + +/** + * Sort breakpoints by weight. + */ +function _breakpoints_sort_by_weight_array($a, $b) { + return _breakpoints_sort_by_weight((object) $a, (object) $b); +} + +/** + * Construct config name. + */ +function breakpoints_breakpoint_config_name($breakpoints_breakpoint) { + if (is_string($breakpoints_breakpoint)) { + return $breakpoints_breakpoint; + } + else { + return drupal_strtolower('breakpoints' + . '.' . $breakpoints_breakpoint->source_type + . '.' . $breakpoints_breakpoint->source + . '.' . $breakpoints_breakpoint->name); + } +} + +/** + * Load a single breakpoint. + */ +function breakpoints_breakpoint_load($name, $source, $source_type) { + $key = drupal_strtolower('breakpoints.' . $source_type . '.' . $source . '.' . $name); + return breakpoints_breakpoint_load_by_fullkey($key); +} + +/** + * Load a single breakpoint using the full config key. + */ +function breakpoints_breakpoint_load_by_fullkey($machine_name = NULL) { + $breakpoints = &drupal_static(__FUNCTION__); + + if (!isset($breakpoints)) { + $breakpoints = _breakpoints_breakpoint_load_all_callback(); + } + + if ($machine_name) { + $breakpoint = isset($breakpoints[$machine_name]) ? $breakpoints[$machine_name] : FALSE; + return $breakpoint; + } + else { + return $breakpoints; + } +} + +/** + * CTools export 'load all callback' CRUD callback for breakpoints. + */ +function _breakpoints_breakpoint_load_all_callback($reset = FALSE) { + if (!$reset && $cache = cache_get('breakpoints:breakpoints')) { + return $cache->data; + } + else { + ctools_include('export'); + $breakpoints = ctools_export_load_object('breakpoints', 'all'); + cache_set('breakpoints:breakpoints', $breakpoints); + return $breakpoints; + } +} + +/** + * Load all breakpoints. + */ +function breakpoints_breakpoint_load_all($theme_key = '') { + $breakpoints_user = breakpoints_breakpoint_load_all_custom(); + $breakpoints_module = breakpoints_breakpoint_load_all_module(); + $breakpoints_theme = breakpoints_breakpoint_load_all_theme($theme_key); + $breakpoints = array_merge($breakpoints_theme, $breakpoints_module, $breakpoints_user); + uasort($breakpoints, '_breakpoints_sort_by_weight'); + return $breakpoints; +} + +/** + * Load all enabled breakpoints. + */ +function breakpoints_breakpoint_load_all_active($theme_key = '') { + $breakpoints = breakpoints_breakpoint_load_all($theme_key); + $enabled = array(); + if (!empty($breakpoints)) { + foreach ($breakpoints as $breakpoint_name => $breakpoint) { + if ($breakpoint->status) { + $enabled[$breakpoint_name] = $breakpoint; + } + } + } + return $enabled; +} + +/** + * Load all breakpoints by source type. + */ +function _breakpoints_breakpoint_load_all_by_type($source_type, $source = '') { + $breakpoints = breakpoints_breakpoint_load_by_fullkey(); + foreach ($breakpoints as $machine_name => $breakpoint) { + if ($breakpoint->source_type != $source_type) { + unset($breakpoints[$machine_name]); + continue; + } + if ($source != '' && $breakpoint->source != $source) { + unset($breakpoints[$machine_name]); + } + } + return $breakpoints; +} + +/** + * Load all custom breakpoints. + */ +function breakpoints_breakpoint_load_all_custom() { + $breakpoints = _breakpoints_breakpoint_load_all_by_type(BREAKPOINTS_SOURCE_TYPE_CUSTOM); + return $breakpoints; +} + +/** + * Load all user defined breakpoints. + */ +function breakpoints_breakpoint_load_all_module() { + return _breakpoints_breakpoint_load_all_by_type(BREAKPOINTS_SOURCE_TYPE_MODULE); +} + +/** + * Load all breakpoints from the theme. + */ +function breakpoints_breakpoint_load_all_theme($theme_key = '') { + return _breakpoints_breakpoint_load_all_by_type(BREAKPOINTS_SOURCE_TYPE_THEME, $theme_key); +} + +/** + * Empty breakpoint object. + */ +function breakpoints_breakpoint_empty_object() { + return (object) breakpoints_breakpoint_empty_array(); +} + +/** + * Empty breakpoint array. + */ +function breakpoints_breakpoint_empty_array() { + return array( + 'name' => '', + 'machine_name' => '', + 'breakpoint' => '', + 'source' => '', + 'source_type' => '', + 'status' => TRUE, + 'weight' => 0, + 'multipliers' => array(), + ); +} + +/** + * Save a single breakpoint. + */ +function breakpoints_breakpoint_save(&$breakpoint) { + ctools_include('export'); + + if (!isset($breakpoint->machine_name) || empty($breakpoint->machine_name)) { + $breakpoint->machine_name = breakpoints_breakpoint_config_name($breakpoint); + } + $update = (isset($breakpoint->id) && is_numeric($breakpoint->id)) ? array('id') : array(); + // Remove unused multipliers. + $breakpoint->multipliers = array_filter($breakpoint->multipliers); + if (is_null($breakpoint->multipliers)){ + $breakpoint->multipliers = array(); + } + // Add the '1x' multiplier. + $breakpoint->multipliers = array_merge($breakpoint->multipliers, array('1x' => '1x')); + $result = drupal_write_record('breakpoints', $breakpoint, $update); + breakpoints_breakpoint_reset(); + return $result; +} + +/** + * Delete a single breakpoint. + */ +function breakpoints_breakpoint_delete($breakpoint) { + $name = breakpoints_breakpoint_config_name($breakpoint); + return breakpoints_breakpoint_delete_by_fullkey($name); +} + +/** + * Delete a single breakpoint. + */ +function breakpoints_breakpoint_delete_by_fullkey($key) { + if (!empty($key)) { + $sql = "DELETE FROM {breakpoints} where machine_name = :key"; + db_query($sql, array(':key' => $key)); + } + breakpoints_breakpoint_reset(); +} + +/** + * Clears the breakpoint caches. + */ +function breakpoints_breakpoint_reset() { + ctools_include('export'); + ctools_export_load_object_reset('breakpoints'); + drupal_static_reset('breakpoints_breakpoint_load_by_fullkey'); + cache_clear_all('breakpoints:breakpoints', 'cache'); +} + +/** + * Toggle status of a single breakpoint. + */ +function breakpoints_breakpoint_toggle_status($machine_name) { + $breakpoint = breakpoints_breakpoint_load_by_fullkey($machine_name); + if ($breakpoint) { + $breakpoint->status = !$breakpoint->status; + breakpoints_breakpoint_save($breakpoint); + } +} + +/** + * Check if a breakpoint name already exists. + */ +function breakpoints_breakpoint_name_exists($machine_name) { + $breakpoints = breakpoints_breakpoint_load_all_custom(); + $fullkey = 'custom.user.' . $machine_name; + return array_key_exists($fullkey, $breakpoints); +} + +/** + * Check if a breakpoint machine name already exists. + */ +function breakpoints_breakpoint_machine_name_exists($machine_name) { + // Just try to load the breakpoint object, we profit from ctool's cache mechanism, + // better that doing a query to the db every time this function is called. + return (bool) breakpoints_breakpoint_load_by_fullkey($machine_name); +} + +/** + * Empty breakpoint group object. + */ +function breakpoints_breakpoint_group_empty_object() { + return (object) breakpoints_breakpoint_group_empty_array(); +} + +/** + * Empty breakpoint group array. + */ +function breakpoints_breakpoint_group_empty_array() { + return array( + 'machine_name' => '', + 'name' => '', + 'breakpoints' => array(), + 'type' => 'custom', + ); +} + +/** + * Check if a group name already exists. + */ +function breakpoints_breakpoint_group_name_exists($machine_name) { + // Check for reserved words. + if ($machine_name == 'global' || $machine_name == 'add') { + return TRUE; + } + // Check if group name is used before. + $group_check = breakpoints_breakpoint_group_load($machine_name); + if ($group_check && isset($group_check->machine_name) && !empty($group_check->machine_name)) { + return TRUE; + } + return FALSE; +} + +/** + * Load all breakpoint groups. + */ +function breakpoints_breakpoint_group_load_all($reset = FALSE) { + $groups = &drupal_static(__FUNCTION__); + + if (!isset($groups)) { + if (!$reset && $cache = cache_get('breakpoints:groups')) { + return $cache->data; + } + else { + ctools_include('export'); + $groups = ctools_export_load_object('breakpoint_group', 'all'); + cache_set('breakpoints:groups', $groups); + } + } + + return $groups; +} + +/** + * Load a single breakpoint group. + */ +function breakpoints_breakpoint_group_load($name = NULL) { + $groups = breakpoints_breakpoint_group_load_all(); + + if ($name) { + $group = isset($groups[$name]) ? $groups[$name] : FALSE; + return $group; + } + else { + return $groups; + } +} + +/** + * Validate a single breakpoint group. + */ +function breakpoints_breakpoint_group_validate($group) { + if (!is_object($group)) { + return FALSE; + } + foreach (array('machine_name', 'name', 'breakpoints', 'type') as $property) { + if (!property_exists($group, $property)) { + return FALSE; + } + } + return TRUE; +} + +/** + * Validate a single breakpoint. + */ +function breakpoints_breakpoint_validate($breakpoint) { + if (!is_object($breakpoint)) { + return FALSE; + } + foreach (array_keys(breakpoints_breakpoint_empty_array()) as $property) { + if (!property_exists($breakpoint, $property)) { + return FALSE; + } + } + return TRUE; +} + +/** + * Save a single breakpoint group. + */ +function breakpoints_breakpoint_group_save(&$breakpoint_group) { + ctools_include('export'); + + $update = (isset($breakpoint_group->id) && is_numeric($breakpoint_group->id)) ? array('id') : array(); + $result = drupal_write_record('breakpoint_group', $breakpoint_group, $update); + // rebuild menu if we add a new group + if (empty($update)) { + variable_set('menu_rebuild_needed', TRUE); + } + breakpoints_breakpoint_group_reset(); + + return $result; +} + +/** + * Delete a single breakpoint group. + */ +function breakpoints_breakpoint_group_delete($breakpoint_group) { + $name = $breakpoint_group->machine_name; + breakpoints_breakpoint_group_delete_by_fullkey($name); +} + +/** + * Delete a single breakpoint group by fullkey. + */ +function breakpoints_breakpoint_group_delete_by_name($machine_name) { + $name = $machine_name; + breakpoints_breakpoint_group_delete_by_fullkey($name); +} + +/** + * Delete a single breakpoint group by fullkey. + */ +function breakpoints_breakpoint_group_delete_by_fullkey($key) { + if (!empty($key)) { + $sql = "DELETE FROM {breakpoint_group} where machine_name = :key"; + db_query($sql, array(':key' => $key)); + } + variable_set('menu_rebuild_needed', TRUE); + breakpoints_breakpoint_group_reset(); +} + +/** + * Clears the breakpoint group caches. + */ +function breakpoints_breakpoint_group_reset() { + ctools_include('export'); + ctools_export_load_object_reset('breakpoint_group'); + drupal_static_reset('breakpoints_breakpoint_group_load_all'); + cache_clear_all('breakpoints:groups', 'cache'); +} + +/** + * Implements hook_theme(). + */ +function breakpoints_theme() { + return array( + 'breakpoints_admin_breakpoints_table' => array( + 'render element' => 'form', + 'theme_key' => NULL, + ), + 'breakpoints_multipliers_table_form' => array( + 'render element' => 'form', + 'theme_key' => NULL, + ) + ); +} + +/** + * Export callback. + * @see breakpoints_schema() + */ +function breakpoint_group_export_breakpoints($object, $field, $value, $indent) { + ctools_include('export'); + $export = array(); + if (isset($object->breakpoints)) { + foreach ($object->breakpoints as $breakpoint) { + if (!is_array($breakpoint) && !is_object($breakpoint)) { + $breakpoint = breakpoints_breakpoint_load_by_fullkey($breakpoint); + } + $export[] = $breakpoint->machine_name; + } + } + return ctools_var_export($export, $indent); +} + +/** + * Reload the breakpoints as defined by the group. + */ +function breakpoints_breakpoints_group_reload(&$group, $force = TRUE) { + switch ($group->type) { + case BREAKPOINTS_SOURCE_TYPE_THEME: + // delete all breakpoints defined by this theme. + $breakpoints = breakpoints_breakpoint_load_all_theme($group->machine_name); + foreach ($breakpoints as $breakpoint) { + breakpoints_breakpoint_delete($breakpoint); + } + + // Reload all breakpoints from theme.info. + $reloaded_group = breakpoints_breakpoints_group_reload_from_theme($group->machine_name); + + // Reset the breakpoints for this group. + if ($force) { + $group->breakpoints = $reloaded_group->breakpoints; + breakpoints_breakpoint_group_save($group); + } + break; + } +} + +function breakpoints_breakpoints_group_reload_from_theme($theme_key) { + // Clear caches so theme.info is fresh. + system_rebuild_theme_data(); + drupal_theme_rebuild(); + + $themes = list_themes(); + if (isset($themes[$theme_key]->info['breakpoints'])) { + $weight = 0; + $theme_settings = $themes[$theme_key]->info['breakpoints']; + $multipliers = isset($themes[$theme_key]->info['multipliers']) ? $themes[$theme_key]->info['multipliers'] : array(); + $settings = breakpoints_settings(); + $current_multipliers = drupal_map_assoc($settings->multipliers); + // Build a group for each theme + $breakpoint_group = breakpoints_breakpoint_group_empty_object(); + $breakpoint_group->machine_name = $theme_key; + $breakpoint_group->name = $themes[$theme_key]->info['name']; + $breakpoint_group->type = BREAKPOINTS_SOURCE_TYPE_THEME; + foreach ($theme_settings as $name => $media_query) { + $breakpoint = breakpoints_breakpoint_load($name, $theme_key, 'theme'); + if (!$breakpoint) { + $breakpoint = breakpoints_breakpoint_empty_object(); + $breakpoint->name = $name; + $breakpoint->source = $theme_key; + $breakpoint->source_type = 'theme'; + $breakpoint->theme = ''; + $breakpoint->status = TRUE; + $breakpoint->weight = $weight++; + $breakpoint->machine_name = breakpoints_breakpoint_config_name($breakpoint); + } + $breakpoint->breakpoint = $media_query; + $breakpoint->multipliers = isset($multipliers[$name]) ? drupal_map_assoc($multipliers[$name]) : array(); + $current_multipliers += drupal_map_assoc($breakpoint->multipliers); + breakpoints_breakpoint_save($breakpoint); + $breakpoint_group->breakpoints[] = $breakpoint->machine_name; + } + breakpoints_settings_save($current_multipliers); + return $breakpoint_group; + } +} + +/** + * Revert the breakpoints of a group. + */ +function breakpoints_breakpoints_group_revert(&$group) { + breakpoints_breakpoints_group_reload($group); + $group->overridden = 0; + breakpoints_breakpoint_group_save($group); +} + +/** + * Duplicate a group. + */ +function breakpoints_breakpoints_group_duplicate($group, $new_name, $new_machine_name) { + $new_group = breakpoints_breakpoint_group_empty_object(); + $new_group->machine_name = $new_machine_name; + $new_group->name = $new_name; + $new_group->type = BREAKPOINTS_SOURCE_TYPE_CUSTOM; + $new_group->breakpoints = $group->breakpoints; + breakpoints_breakpoint_group_save($new_group); + return $new_group; +} + +/** + * Override the breakpoints of a group. + */ +function breakpoints_breakpoints_group_override($group) { + foreach ($group->breakpoints as $key => $breakpoint) { + $breakpoint = breakpoints_breakpoint_load_by_fullkey($breakpoint); + $old_breakpoint = clone $breakpoint; + if ($breakpoint->source_type == BREAKPOINTS_SOURCE_TYPE_THEME && $breakpoint->source == $group->machine_name) { + unset($breakpoint->id); + $breakpoint->machine_name = 'custom.' . $breakpoint->source . '.' . str_replace('-', '_', drupal_clean_css_identifier($breakpoint->name)); + $breakpoint->source_type = BREAKPOINTS_SOURCE_TYPE_CUSTOM; + + // make sure it doesn't already exists. + if (breakpoints_breakpoint_load_by_fullkey($breakpoint->machine_name) === FALSE) { + breakpoints_breakpoint_save($breakpoint); + } + + // Add to the group and delete old breakpoint. + $group->breakpoints[$key] = $breakpoint->machine_name; + breakpoints_breakpoint_delete($old_breakpoint, $group->machine_name); + } + } + $group->overridden = 1; + breakpoints_breakpoint_group_save($group); +} + +/** + * Export breakpoints ready for theme.info inclusion. + */ +function breakpoints_breakpoints_group_exporttotheme(&$group) { + $export = array(); + foreach ($group->breakpoints as $breakpoint_name) { + $breakpoint = breakpoints_breakpoint_load_by_fullkey($breakpoint_name); + if ($breakpoint && $breakpoint->status) { + $export[$breakpoint->name] = $breakpoint->breakpoint; + } + } + return $export; +} + +/** + * array_filter callback. + */ +function _breakpoints_filter_styles($var) { + static $exists = NULL; + if (is_null($exists)) { + $exists = module_exists('resp_img') && defined('RESP_IMG_STYLE_PREFIX'); + } + if (!$exists) { + return TRUE; + } + return strpos(is_array($var) ? $var['name'] : $var, RESP_IMG_STYLE_PREFIX) !== 0; +} + +/** + * Implements hook_permission(). + */ +function breakpoints_form_system_theme_settings_alter(&$form, &$form_state) { + if (isset($form_state['build_info']['args'][0])) { + $form['actions']['rescan_breakpoints'] = array( + '#type' => 'submit', + '#value' => t('Scan this theme for breakpoints'), + '#submit' => array('breakpoints_form_system_theme_settings_alter_submit'), + ); + } +} + +function breakpoints_form_system_theme_settings_alter_submit(&$form, &$form_state) { + $theme = $form_state['build_info']['args'][0]; + $group = breakpoints_breakpoint_group_load($theme); + if ($group) { + breakpoints_breakpoints_group_reload($group); + } + else { + breakpoints_themes_enabled(array($theme)); + } +} + +/** + * Implements hook_features_export(). + */ +function breakpoint_group_features_export($data, &$export, $module_name = '') { + features_include(); + $pipe = ctools_component_features_export('breakpoint_group', $data, $export, $module_name); + foreach ($data as $group_name) { + $group = breakpoints_breakpoint_group_load($group_name); + foreach ($group->breakpoints as $breakpoint_name) { + $breakpoint = breakpoints_breakpoint_load_by_fullkey($breakpoint_name); + if ($breakpoint->source_type != BREAKPOINTS_SOURCE_TYPE_THEME) { + $pipe['breakpoints'][] = $breakpoint_name; + } + } + } + return $pipe; +} + +/** + * Implements hook_flush_caches(). + */ +function breakpoints_flush_caches() { + if (variable_get('breakpoints_flush_caches_disabled', FALSE)) { + return; + } + + // hook_flush_caches gets invoked with both drupal_flush_all_caches and system_cron + // To avoid frequent menu/theme flushes, we must backtrace caller. + $backtrace = debug_backtrace(); + if (isset($backtrace[3]) && ($backtrace[3]['function'] == 'drupal_flush_all_caches') && empty($backtrace[3]['args'])) { + + $themes = list_themes(); + foreach ($themes as $theme_key => $theme) { + if ($theme->status) { + $group = breakpoints_breakpoint_group_load($theme_key); + if ($group) { + breakpoints_breakpoints_group_reload($group, FALSE); + } + else { + breakpoints_themes_enabled(array($theme_key), FALSE); + } + } + } + } +} diff --git a/frontend/drupal/sites/all/modules/breakpoints/breakpoints.test b/frontend/drupal/sites/all/modules/breakpoints/breakpoints.test new file mode 100644 index 000000000..b75a479e8 --- /dev/null +++ b/frontend/drupal/sites/all/modules/breakpoints/breakpoints.test @@ -0,0 +1,913 @@ + $breakpoint->name); + $properties = array('name', 'breakpoint', 'source', 'source_type', 'status', 'weight', 'multipliers'); + if ($in_database) { + $properties[] = 'id'; + } + $assert_group = t('Breakpoints API'); + // Verify text format database record. + $db_breakpoint = db_select('breakpoints', 'b') + ->fields('b') + ->condition('machine_name', $breakpoint->machine_name) + ->execute() + ->fetchObject(); + $db_breakpoint->multipliers = unserialize($db_breakpoint->multipliers); + foreach ($properties as $property) { + $this->assertEqual($db_breakpoint->{$property}, $breakpoint->{$property}, t('Database: Proper ' . $property . ' for breakpoint %breakpoint.', $t_args), $assert_group); + } + + // Verify breakpoints_breakpoint_load_by_fullkey(). + $load_breakpoint = breakpoints_breakpoint_load_by_fullkey($breakpoint->machine_name); + foreach ($properties as $property) { + $this->assertEqual($load_breakpoint->{$property}, $breakpoint->{$property}, t('breakpoints_breakpoint_load_by_fullkey: Proper ' . $property . ' for breakpoint %breakpoint.', $t_args), $assert_group); + } + } +} + +/** + * Tests for breakpoints CRUD operations. + */ +class BreakpointsCRUDTestCase extends BreakpointsTestCase { + + public static function getInfo() { + return array( + 'name' => 'Breakpoints CRUD operations', + 'description' => 'Test creation, loading, updating, deleting of breakpoints.', + 'group' => 'Breakpoints', + ); + } + + /** + * Test CRUD operations for breakpoints. + */ + function testBreakpointsCRUD() { + // Add a breakpoint with minimum data only. + $breakpoint = new stdClass(); + $breakpoint->disabled = FALSE; + $breakpoint->api_version = 1; + $breakpoint->name = 'Custom'; + $breakpoint->breakpoint = '(min-width: 600px)'; + $breakpoint->source = 'user'; + $breakpoint->source_type = 'custom'; + $breakpoint->status = 1; + $breakpoint->weight = 0; + $breakpoint->multipliers = array( + '1.5x' => 0, + '2x' => 0, + ); + breakpoints_breakpoint_save($breakpoint); + $this->verifyBreakpoint($breakpoint); + + // Update the breakpoint. + $breakpoint->weight = 1; + $breakpoint->multipliers['2x'] = 1; + breakpoints_breakpoint_save($breakpoint); + $this->verifyBreakpoint($breakpoint); + + // Disable the breakpoint. + $breakpoint->status = 0; + breakpoints_breakpoint_save($breakpoint); + $this->verifyBreakpoint($breakpoint); + $breakpoints = breakpoints_breakpoint_load_all_active(); + $this->assertFalse(isset($breakpoints[$breakpoint->machine_name]), t('breakpoints_breakpoint_load_all_active: Disabled breakpoints aren\'t loaded.'), t('Breakpoints API')); + + // Delete the breakpoint. + breakpoints_breakpoint_delete($breakpoint); + $db_breakpoint = db_select('breakpoints', 'b')->fields('b')->condition('machine_name', $breakpoint->machine_name)->execute()->fetchObject(); + $this->assertFalse($db_breakpoint, t('Database: Deleted breakpoint no longer exists'), t('Breakpoints API')); + $this->assertFalse(breakpoints_breakpoint_load_by_fullkey($breakpoint->machine_name), t('breakpoints_breakpoint_load_by_fullkey: Loading a deleted breakpoint returns false.'), t('Breakpoints API')); + } +} + +/** + * Tests for breakpoints admin interface. + */ +class BreakpointsAdminTestCase extends BreakpointsTestCase { + + public static function getInfo() { + return array( + 'name' => 'Breakpoints administration functionality', + 'description' => 'Thoroughly test the administrative interface of the breakpoints module.', + 'group' => 'Breakpoints', + ); + } + + function setUp() { + parent::setUp(); + + // Create user. + $this->admin_user = $this->drupalCreateUser(array( + 'administer breakpoints', + )); + + $this->drupalLogin($this->admin_user); + } + + /** + * Test breakpoint administration functionality + */ + function testBreakpointAdmin() { + // Add breakpoint. + $this->drupalGet('admin/config/media/breakpoints'); + $name = $this->randomName(); + $mediaquery = '(min-width: 600px)'; + $edit = array( + 'breakpoints[new][name]' => $name, + 'breakpoints[new][machine_name]' => drupal_strtolower($name), + 'breakpoints[new][breakpoint]' => $mediaquery, + ); + + $this->drupalPost(NULL, $edit, t('Save')); + + $machine_name = BREAKPOINTS_SOURCE_TYPE_CUSTOM . '.user.' . drupal_strtolower($name); + // Verify the breakpoint was saved and verify default weight of the breakpoint. + $this->drupalGet('admin/config/media/breakpoints'); + $this->assertFieldByName("breakpoints[$machine_name][weight]", 0, t('Breakpoint weight was saved.')); + + // Change the weight of the breakpoint. + $edit = array( + "breakpoints[$machine_name][weight]" => 5, + ); + $this->drupalPost(NULL, $edit, t('Save')); + $this->assertFieldByName("breakpoints[$machine_name][weight]", 5, t('Breakpoint weight was saved.')); + + // Submit the form. + $this->drupalGet('admin/config/media/breakpoints'); + $this->drupalPost(NULL, array(), t('Save')); + + // Verify that the custom weight of the breakpoint has been retained. + $this->drupalGet('admin/config/media/breakpoints'); + $this->assertFieldByName("breakpoints[$machine_name][weight]", 5, t('Breakpoint weight was retained.')); + + // Change the multipliers of the breakpoint. + $edit = array( + "breakpoints[$machine_name][multipliers][1.5x]" => "1.5x", + ); + $this->drupalPost(NULL, $edit, t('Save')); + $id = drupal_clean_css_identifier('edit-breakpoints-' . $machine_name . '-multipliers-'); + $this->assertFieldChecked($id . '15x', t('Breakpoint multipliers were saved.')); + $this->assertNoFieldChecked($id . '2x', t('Breakpoint multipliers were saved.')); + + // Submit the form. + $this->drupalGet('admin/config/media/breakpoints'); + $this->drupalPost(NULL, array(), t('Save')); + + // Verify that the custom weight of the breakpoint has been retained. + $this->drupalGet('admin/config/media/breakpoints'); + $this->assertFieldChecked($id . '15x', t('Breakpoint multipliers were retained.')); + $this->assertNoFieldChecked($id . '2x', t('Breakpoint multipliers were retained.')); + + // Disable breakpoint. + $this->assertLinkByHref('admin/config/media/breakpoints/disable/' . $machine_name); + $this->drupalGet('admin/config/media/breakpoints/disable/' . $machine_name); + $this->drupalPost(NULL, array(), t('Confirm')); + + // Verify that the breakpoint is disabled. + $this->assertLinkByHref('admin/config/media/breakpoints/enable/' . $machine_name, 0, t('Breakpoint was disabled.')); + + // Attempt to create a breakpoint with the same machine name as the disabled + // breakpoint but with a different human readable name. + $edit = array( + 'breakpoints[new][name]' => 'New Breakpoint', + 'breakpoints[new][machine_name]' => drupal_strtolower($name), + 'breakpoints[new][breakpoint]' => $mediaquery, + 'breakpoints[new][multipliers][1.5x]' => 0, + 'breakpoints[new][multipliers][2x]' => 0, + ); + $this->drupalPost(NULL, $edit, t('Save')); + $this->assertText('The machine-readable name is already in use. It must be unique.'); + + // Delete breakpoint. + $this->assertLinkByHref('admin/config/media/breakpoints/delete/' . $machine_name); + $this->drupalGet('admin/config/media/breakpoints/delete/' . $machine_name); + $this->drupalPost(NULL, array(), t('Confirm')); + + // Verify that deleted breakpoint no longer exists. + $this->drupalGet('admin/config/media/breakpoints'); + $this->assertNoFieldByName('breakpoints[' . $machine_name . '][name]', '', t('Deleted breakpoint no longer exists')); + } + + /** + * Test breakpoint export/import functionality. + */ + function testBreakpointExportImport() { + $breakpoint = new stdClass(); + $breakpoint->disabled = FALSE; + $breakpoint->api_version = 1; + $breakpoint->machine_name = 'custom.user.test'; + $breakpoint->name = 'test'; + $breakpoint->breakpoint = '(min-width: 600px)'; + $breakpoint->source = 'user'; + $breakpoint->source_type = 'custom'; + $breakpoint->status = 1; + $breakpoint->weight = 0; + $breakpoint->multipliers = array( + '1.5x' => 0, + '2x' => 0, + ); + + // Import a breakpoint; + $importstring = array(); + $importstring[] = '$breakpoint = new stdClass();'; + $importstring[] = '$breakpoint->disabled = FALSE; /* Edit this to true to make a default breakpoint disabled initially */'; + $importstring[] = '$breakpoint->api_version = 1;'; + $importstring[] = '$breakpoint->machine_name = \'custom.user.test\';'; + $importstring[] = '$breakpoint->name = \'test\';'; + $importstring[] = '$breakpoint->breakpoint = \'(min-width: 600px)\';'; + $importstring[] = '$breakpoint->source = \'user\';'; + $importstring[] = '$breakpoint->source_type = \'custom\';'; + $importstring[] = '$breakpoint->status = 1;'; + $importstring[] = '$breakpoint->weight = 0;'; + $importstring[] = '$breakpoint->multipliers = array('; + $importstring[] = ' \'1.5x\' => 0,'; + $importstring[] = ' \'2x\' => 0,'; + $importstring[] = ');'; + + $this->drupalGet('admin/config/media/breakpoints/groups/import-breakpoint'); + $edit = array( + "import" => implode("\n", $importstring), + ); + $this->drupalPost(NULL, $edit, t('Import')); + + // Verify the breakpoint was imported. + $this->drupalGet('admin/config/media/breakpoints'); + $this->assertField('breakpoints[' . $breakpoint->machine_name . '][name]', t('Breakpoint imported correctly.')); + + // Verify the breakpoint is in the database, is loadable and has the correct data. + $this->verifyBreakpoint($breakpoint, FALSE); + + // Verify the breakpoint exports correctly. + $this->drupalGet('admin/config/media/breakpoints/export/' . $breakpoint->machine_name); + foreach ($importstring as $importline) { + $importline = trim($importline); + if (!empty($importline)) { + // Text in a textarea is htmlencoded. + $this->assertRaw(check_plain($importline)); + } + } + } +} + +/** + * Base class for Breakpoint Group tests. + */ +abstract class BreakpointGroupTestCase extends DrupalWebTestCase { + function setUp() { + $modules = func_get_args(); + if (isset($modules[0]) && is_array($modules[0])) { + $modules = $modules[0]; + } + array_unshift($modules, 'breakpoints'); + parent::setUp($modules); + } + + /** + * Verify that a breakpoint is properly stored. + */ + function verifyBreakpointGroup($group, $in_database = TRUE) { + $t_args = array('%group' => $group->name); + $properties = array('name', 'machine_name', 'breakpoints'); + if ($in_database) { + $properties[] = 'id'; + } + $assert_group = t('Breakpoints API'); + // Verify text format database record. + $db_group = db_select('breakpoint_group', 'bg') + ->fields('bg') + ->condition('machine_name', $group->machine_name) + ->execute() + ->fetchObject(); + $db_group->breakpoints = unserialize($db_group->breakpoints); + foreach ($properties as $property) { + $this->assertEqual($db_group->{$property}, $group->{$property}, t('Database: Proper ' . $property . ' for breakpoint group %group.', $t_args), $assert_group); + } + + // Verify breakpoints_breakpoint_group_load(). + $load_group = breakpoints_breakpoint_group_load($group->machine_name); + foreach ($properties as $property) { + $this->assertEqual($load_group->{$property}, $group->{$property}, t('breakpoints_breakpoint_group_load: Proper ' . $property . ' for breakpoint group %group.', $t_args), $assert_group); + } + } +} + +/** + * Tests for breakpoint group CRUD operations. + */ +class BreakpointGroupCRUDTestCase extends BreakpointGroupTestCase { + + public static function getInfo() { + return array( + 'name' => 'Breakpoint Group CRUD operations', + 'description' => 'Test creation, loading, updating, deleting of breakpoint groups.', + 'group' => 'Breakpoints', + ); + } + + /** + * Test CRUD operations for breakpoint groups. + */ + function testBreakpointGroupCRUD() { + // Add breakpoints. + $breakpoints = array(); + for ($i = 0; $i <= 3; $i++) { + $breakpoint = new stdClass(); + $breakpoint->disabled = FALSE; + $breakpoint->api_version = 1; + $breakpoint->name = $this->randomName(); + $width = ($i + 1) * 200; + $breakpoint->breakpoint = "(min-width: {$width}px)"; + $breakpoint->source = 'user'; + $breakpoint->source_type = 'custom'; + $breakpoint->status = 1; + $breakpoint->weight = $i; + $breakpoint->multipliers = array( + '1.5x' => 0, + '2x' => 0, + ); + breakpoints_breakpoint_save($breakpoint); + $breakpoints[$breakpoint->machine_name] = $breakpoint; + } + // Add a breakpoint group with minimum data only. + $group = new stdClass(); + $group->name = $this->randomName(); + $group->machine_name = drupal_strtolower($group->name); + $group->breakpoints = array(); + breakpoints_breakpoint_group_save($group); + $this->verifyBreakpointGroup($group); + + // Update the breakpoint group. + $group->breakpoints = array_keys($breakpoints); + breakpoints_breakpoint_group_save($group); + $this->verifyBreakpointGroup($group); + + // Delete the breakpoint group. + breakpoints_breakpoint_group_delete($group); + $db_group = db_select('breakpoint_group', 'bg')->fields('bg')->condition('machine_name', $group->machine_name)->execute()->fetchObject(); + $this->assertFalse($db_group, t('Database: Deleted breakpoint group no longer exists'), t('Breakpoints API')); + $this->assertFalse(breakpoints_breakpoint_group_load($group->machine_name), t('breakpoints_breakpoint_group_load: Loading a deleted breakpoint group returns false.'), t('Breakpoints API')); + } +} + +/** + * Tests for breakpoint groups admin interface. + */ +class BreakpointGroupAdminTestCase extends BreakpointGroupTestCase { + + public static function getInfo() { + return array( + 'name' => 'Breakpoint Group administration functionality', + 'description' => 'Thoroughly test the administrative interface of the breakpoints module.', + 'group' => 'Breakpoints', + ); + } + + function setUp() { + parent::setUp(); + + // Create user. + $this->admin_user = $this->drupalCreateUser(array( + 'administer breakpoints', + )); + + $this->drupalLogin($this->admin_user); + } + + /** + * Test breakpoint administration functionality + */ + function testBreakpointGroupAdmin() { + // Add breakpoints. + $breakpoints = array(); + for ($i = 0; $i <= 3; $i++) { + $breakpoint = new stdClass(); + $breakpoint->disabled = FALSE; + $breakpoint->api_version = 1; + $breakpoint->name = $this->randomName(); + $width = ($i + 1) * 200; + $breakpoint->breakpoint = "(min-width: {$width}px)"; + $breakpoint->source = 'user'; + $breakpoint->source_type = 'custom'; + $breakpoint->status = 1; + $breakpoint->weight = $i; + $breakpoint->multipliers = array( + '1.5x' => 0, + '2x' => 0, + ); + breakpoints_breakpoint_save($breakpoint); + $breakpoints[$breakpoint->machine_name] = $breakpoint; + } + // Add breakpoint group. + $this->drupalGet('admin/config/media/breakpoints/groups/add'); + $name = $this->randomName(); + $machine_name = drupal_strtolower($name); + $breakpoint = reset($breakpoints); + $edit = array( + 'name' => $name, + 'machine_name' => $machine_name, + 'breakpoints[' . $breakpoint->machine_name . ']' => $breakpoint->machine_name, + ); + + $this->drupalPost(NULL, $edit, t('Save')); + + // Verify the breakpoint was saved. + $this->drupalGet('admin/config/media/breakpoints/groups/' . $machine_name); + $this->assertResponse(200, t('Breakpoint group was saved.')); + + // Verify the breakpoint was attached to the group. + $this->assertField('breakpoints[' . $breakpoint->machine_name . '][name]', t('The Breakpoint was added.')); + + // Add breakpoints to the breakpoint group. + $this->drupalGet('admin/config/media/breakpoints/groups/' . $machine_name . '/edit'); + $edit = array(); + foreach ($breakpoints as $key => $breakpoint) { + $edit['breakpoints[' . $key . ']'] = $key; + } + $this->drupalPost(NULL, $edit, t('Save')); + + // Verify the breakpoints were attached to the group. + $this->drupalGet('admin/config/media/breakpoints/groups/' . $machine_name); + foreach ($breakpoints as $key => $breakpoint) { + $this->assertField('breakpoints[' . $key . '][name]', t('The Breakpoint was added.')); + } + + // Change the order breakpoints of the breakpoints within the breakpoint group. + $breakpoint = end($breakpoints); + $edit = array( + "breakpoints[{$breakpoint->machine_name}][weight]" => 0, + ); + $this->drupalPost(NULL, $edit, t('Save')); + $this->assertFieldByName("breakpoints[{$breakpoint->machine_name}][weight]", 0, t('Breakpoint weight was saved.')); + + // Submit the form. + $this->drupalGet('admin/config/media/breakpoints'); + $this->drupalPost(NULL, array(), t('Save')); + + // Verify that the custom weight of the breakpoint has been retained. + $this->drupalGet('admin/config/media/breakpoints/groups/' . $machine_name); + $this->assertFieldByName("breakpoints[{$breakpoint->machine_name}][weight]", 0, t('Breakpoint weight was retained.')); + + // Verify that the weight has only changed within the group. + $this->drupalGet('admin/config/media/breakpoints'); + $this->assertFieldByName("breakpoints[{$breakpoint->machine_name}][weight]", $breakpoint->weight, t('Breakpoint weight has only changed within the group.')); + + // Change the multipliers of the breakpoint within the group. + $edit = array( + "breakpoints[{$breakpoint->machine_name}][multipliers][1.5x]" => "1.5x", + ); + $this->drupalPost(NULL, $edit, t('Save')); + $id = drupal_clean_css_identifier('edit-breakpoints-' . $breakpoint->machine_name . '-multipliers-'); + $this->assertFieldChecked($id . '15x', t('Breakpoint multipliers were saved.')); + $this->assertNoFieldChecked($id . '2x', t('Breakpoint multipliers were saved.')); + + // Submit the form. + $this->drupalGet('admin/config/media/breakpoints/groups/' . $machine_name); + $this->drupalPost(NULL, array(), t('Save')); + + // Verify that the multipliers of the breakpoint has been retained. + $this->drupalGet('admin/config/media/breakpoints/groups/' . $machine_name); + $this->assertFieldChecked($id . '15x', t('Breakpoint multipliers were retained.')); + $this->assertNoFieldChecked($id . '2x', t('Breakpoint multipliers were retained.')); + + // Verify that the multipliers only changed within the group. + $this->drupalGet('admin/config/media/breakpoints'); + $this->assertFieldChecked($id . '15x', t('Breakpoint multipliers were retained.')); + $this->assertNoFieldChecked($id . '2x', t('Breakpoint multipliers were retained.')); + + // Attempt to create a breakpoint group of the same machine name as the disabled + // breakpoint but with a different human readable name. + // Add breakpoint group. + $this->drupalGet('admin/config/media/breakpoints/groups/add'); + $breakpoint = reset($breakpoints); + $edit = array( + 'name' => $this->randomName(), + 'machine_name' => $machine_name, + 'breakpoints[' . $breakpoint->machine_name . ']' => $breakpoint->machine_name, + ); + + $this->drupalPost(NULL, $edit, t('Save')); + $this->assertText('The machine-readable name is already in use. It must be unique.'); + + // Delete breakpoint. + $this->drupalGet('admin/config/media/breakpoints/groups/' . $machine_name . '/delete'); + $this->drupalPost(NULL, array(), t('Confirm')); + + // Verify that deleted breakpoint no longer exists. + $this->drupalGet('admin/config/media/breakpoints/groups/' . $machine_name); + $this->assertResponse(404, t('Breakpoint group was deleted.')); + } + + /** + * Test breakpoint group export/import functionality. + */ + function testBreakpointGroupExportImport() { + /** + * Breakpoints. + */ + $breakpoints = array(); + $breakpoint = new stdClass(); + $breakpoint->disabled = FALSE; + $breakpoint->api_version = 1; + $breakpoint->machine_name = 'custom.user.mobile'; + $breakpoint->name = 'mobile'; + $breakpoint->breakpoint = '(min-width: 0px)'; + $breakpoint->source = 'user'; + $breakpoint->source_type = 'custom'; + $breakpoint->status = 1; + $breakpoint->weight = 4; + $breakpoint->multipliers = array( + '1.5x' => 0, + '2x' => 0, + ); + + $breakpoints[$breakpoint->machine_name] = $breakpoint; + + $breakpoint = new stdClass(); + $breakpoint->disabled = FALSE; + $breakpoint->api_version = 1; + $breakpoint->machine_name = 'custom.user.narrow'; + $breakpoint->name = 'narrow'; + $breakpoint->breakpoint = '(min-width: 560px)'; + $breakpoint->source = 'user'; + $breakpoint->source_type = 'custom'; + $breakpoint->status = 1; + $breakpoint->weight = 5; + $breakpoint->multipliers = array( + '1.5x' => 0, + '2x' => 0, + ); + + $breakpoints[$breakpoint->machine_name] = $breakpoint; + + $breakpoint = new stdClass(); + $breakpoint->disabled = FALSE; + $breakpoint->api_version = 1; + $breakpoint->machine_name = 'custom.user.wide'; + $breakpoint->name = 'wide'; + $breakpoint->breakpoint = '(min-width: 851px)'; + $breakpoint->source = 'user'; + $breakpoint->source_type = 'custom'; + $breakpoint->status = 1; + $breakpoint->weight = 6; + $breakpoint->multipliers = array( + '1.5x' => 0, + '2x' => 0, + ); + + $breakpoints[$breakpoint->machine_name] = $breakpoint; + + $breakpoint = new stdClass(); + $breakpoint->disabled = FALSE; + $breakpoint->api_version = 1; + $breakpoint->machine_name = 'custom.user.tv'; + $breakpoint->name = 'tv'; + $breakpoint->breakpoint = 'only screen and (min-width: 3456px)'; + $breakpoint->source = 'user'; + $breakpoint->source_type = 'custom'; + $breakpoint->status = 1; + $breakpoint->weight = 7; + $breakpoint->multipliers = array( + '1.5x' => 0, + '2x' => 0, + ); + + $breakpoints[$breakpoint->machine_name] = $breakpoint; + + /** + * Breakpoint group. + */ + $breakpoint_group = new stdClass(); + $breakpoint_group->disabled = FALSE; /* Edit this to true to make a default breakpoint_group disabled initially */ + $breakpoint_group->api_version = 1; + $breakpoint_group->machine_name = 'customgroup'; + $breakpoint_group->name = 'Customgroup'; + $breakpoint_group->breakpoints = array_keys($breakpoints); + $breakpoint_group->type = 'custom'; + $breakpoint_group->overridden = 0; + + $importstring = array(); + $importstring[] = '/**'; + $importstring[] = ' * Breakpoints.'; + $importstring[] = ' */'; + $importstring[] = '$breakpoints = array();'; + $importstring[] = '$breakpoint = new stdClass();'; + $importstring[] = '$breakpoint->disabled = FALSE; /* Edit this to true to make a default breakpoint disabled initially */'; + $importstring[] = '$breakpoint->api_version = 1;'; + $importstring[] = '$breakpoint->machine_name = \'custom.user.mobile\';'; + $importstring[] = '$breakpoint->name = \'mobile\';'; + $importstring[] = '$breakpoint->breakpoint = \'(min-width: 0px)\';'; + $importstring[] = '$breakpoint->source = \'user\';'; + $importstring[] = '$breakpoint->source_type = \'custom\';'; + $importstring[] = '$breakpoint->status = 1;'; + $importstring[] = '$breakpoint->weight = 4;'; + $importstring[] = '$breakpoint->multipliers = array('; + $importstring[] = ' \'1.5x\' => 0,'; + $importstring[] = ' \'2x\' => 0,'; + $importstring[] = ');'; + $importstring[] = ''; + $importstring[] = '$breakpoints[] = $breakpoint;'; + $importstring[] = ''; + $importstring[] = '$breakpoint = new stdClass();'; + $importstring[] = '$breakpoint->disabled = FALSE; /* Edit this to true to make a default breakpoint disabled initially */'; + $importstring[] = '$breakpoint->api_version = 1;'; + $importstring[] = '$breakpoint->machine_name = \'custom.user.narrow\';'; + $importstring[] = '$breakpoint->name = \'narrow\';'; + $importstring[] = '$breakpoint->breakpoint = \'(min-width: 560px)\';'; + $importstring[] = '$breakpoint->source = \'user\';'; + $importstring[] = '$breakpoint->source_type = \'custom\';'; + $importstring[] = '$breakpoint->status = 1;'; + $importstring[] = '$breakpoint->weight = 5;'; + $importstring[] = '$breakpoint->multipliers = array('; + $importstring[] = ' \'1.5x\' => 0,'; + $importstring[] = ' \'2x\' => 0,'; + $importstring[] = ');'; + $importstring[] = ''; + $importstring[] = '$breakpoints[] = $breakpoint;'; + $importstring[] = ''; + $importstring[] = '$breakpoint = new stdClass();'; + $importstring[] = '$breakpoint->disabled = FALSE; /* Edit this to true to make a default breakpoint disabled initially */'; + $importstring[] = '$breakpoint->api_version = 1;'; + $importstring[] = '$breakpoint->machine_name = \'custom.user.wide\';'; + $importstring[] = '$breakpoint->name = \'wide\';'; + $importstring[] = '$breakpoint->breakpoint = \'(min-width: 851px)\';'; + $importstring[] = '$breakpoint->source = \'user\';'; + $importstring[] = '$breakpoint->source_type = \'custom\';'; + $importstring[] = '$breakpoint->status = 1;'; + $importstring[] = '$breakpoint->weight = 6;'; + $importstring[] = '$breakpoint->multipliers = array('; + $importstring[] = ' \'1.5x\' => 0,'; + $importstring[] = ' \'2x\' => 0,'; + $importstring[] = ');'; + $importstring[] = ''; + $importstring[] = '$breakpoints[] = $breakpoint;'; + $importstring[] = ''; + $importstring[] = '$breakpoint = new stdClass();'; + $importstring[] = '$breakpoint->disabled = FALSE; /* Edit this to true to make a default breakpoint disabled initially */'; + $importstring[] = '$breakpoint->api_version = 1;'; + $importstring[] = '$breakpoint->machine_name = \'custom.user.tv\';'; + $importstring[] = '$breakpoint->name = \'tv\';'; + $importstring[] = '$breakpoint->breakpoint = \'only screen and (min-width: 3456px)\';'; + $importstring[] = '$breakpoint->source = \'user\';'; + $importstring[] = '$breakpoint->source_type = \'custom\';'; + $importstring[] = '$breakpoint->status = 1;'; + $importstring[] = '$breakpoint->weight = 7;'; + $importstring[] = '$breakpoint->multipliers = array('; + $importstring[] = ' \'1.5x\' => 0,'; + $importstring[] = ' \'2x\' => 0,'; + $importstring[] = ');'; + $importstring[] = ''; + $importstring[] = '$breakpoints[] = $breakpoint;'; + $importstring[] = ''; + $importstring[] = '/**'; + $importstring[] = ' * Breakpoint group.'; + $importstring[] = ' */'; + $importstring[] = '$breakpoint_group = new stdClass();'; + $importstring[] = '$breakpoint_group->disabled = FALSE; /* Edit this to true to make a default breakpoint_group disabled initially */'; + $importstring[] = '$breakpoint_group->api_version = 1;'; + $importstring[] = '$breakpoint_group->machine_name = \'customgroup\';'; + $importstring[] = '$breakpoint_group->name = \'Customgroup\';'; + $importstring[] = '$breakpoint_group->breakpoints = $breakpoints;'; + $importstring[] = '$breakpoint_group->type = \'custom\';'; + $importstring[] = '$breakpoint_group->overridden = 0;'; + + $this->drupalGet('admin/config/media/breakpoints/groups/import'); + $edit = array( + "import" => implode("\n", $importstring), + ); + $this->drupalPost(NULL, $edit, t('Import')); + + // Verify the breakpoint group was imported. + $this->drupalGet('admin/config/media/breakpoints/groups/' . $breakpoint_group->machine_name); + $this->assertResponse(200, t('Breakpoint group imported correctly')); + + // Verify the breakpoint group is in the database, is loadable and has the correct data. + $this->verifyBreakpointGroup($breakpoint_group, FALSE); + + // Verify the breakpoint group exports correctly. + $this->drupalGet('admin/config/media/breakpoints/groups/' . $breakpoint_group->machine_name . '/export'); + foreach ($importstring as $importline) { + $importline = trim($importline); + if (!empty($importline)) { + // Text in a textarea is htmlencoded. + $this->assertRaw(check_plain($importline)); + } + } + } +} + +/** + * Test breakpoints provided by themes. + */ +class BreakpointsThemeTestCase extends BreakpointGroupTestCase { + public static function getInfo() { + return array( + 'name' => 'Breakpoint Theme functionality', + 'description' => 'Thoroughly test the breakpoints provided by a theme.', + 'group' => 'Breakpoints', + ); + } + + public function setUp() { + parent::setUp('breakpoints_theme_test'); + theme_enable(array('breakpoints_test_theme')); + // Create user. + $this->admin_user = $this->drupalCreateUser(array( + 'administer breakpoints', + )); + + $this->drupalLogin($this->admin_user); + } + + /** + * Test the breakpoints provided by a theme. + */ + public function testThemeBreakpoints() { + // Verify the breakpoint group for breakpoints_test_theme was created. + $breakpoint_group = new stdClass(); + $breakpoint_group->disabled = FALSE; /* Edit this to true to make a default breakpoint_group disabled initially */ + $breakpoint_group->api_version = 1; + $breakpoint_group->machine_name = 'breakpoints_test_theme'; + $breakpoint_group->name = 'Breakpoints test theme'; + $breakpoint_group->breakpoints = array( + 'breakpoints.theme.breakpoints_test_theme.mobile', + 'breakpoints.theme.breakpoints_test_theme.narrow', + 'breakpoints.theme.breakpoints_test_theme.wide', + 'breakpoints.theme.breakpoints_test_theme.tv', + ); + $breakpoint_group->type = 'theme'; + $breakpoint_group->overridden = 0; + $this->verifyBreakpointGroup($breakpoint_group, FALSE); + + // Override the breakpoints. + $this->drupalGet('admin/config/media/breakpoints/groups/' . $breakpoint_group->machine_name); + $this->drupalPost(NULL, array(), t('Override theme breakpoints')); + + // Clear CTools cache, since drupalGet and drupalPost are different requests than the request + // this test is running in, the group object is still in the static cache, so we need to clear + // it manually. + ctools_export_load_object_reset('breakpoint_group'); + + // Verify the group is overridden. + $breakpoint_group->breakpoints = array( + 'custom.breakpoints_test_theme.mobile', + 'custom.breakpoints_test_theme.narrow', + 'custom.breakpoints_test_theme.wide', + 'custom.breakpoints_test_theme.tv', + ); + $breakpoint_group->overridden = 1; + $this->verifyBreakpointGroup($breakpoint_group, FALSE); + + // Verify there is no override button for this group anymore. + $this->drupalGet('admin/config/media/breakpoints/groups/' . $breakpoint_group->machine_name); + $this->assertNoFieldById('edit-override'); + } +} + +/** + * Test breakpoint multipliers. + */ +class BreakpointMultipliersTestCase extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Breakpoint Multiplier functionality', + 'description' => 'Thoroughly test the breakpoint multiplier functionality (CRUD).', + 'group' => 'Breakpoints', + ); + } + + public function setUp() { + parent::setUp('breakpoints', 'breakpoints_theme_test'); + // Enable our test theme so we have breakpoints to test on. + theme_enable(array('breakpoints_test_theme')); + // Create user. + $this->admin_user = $this->drupalCreateUser(array( + 'administer breakpoints', + )); + + $this->drupalLogin($this->admin_user); + } + + /** + * Test breakpoints multipliers functionality. + */ + public function testBreakpointMultipliers() { + // Verify the default multipliers are visible. + $this->drupalGet('admin/config/media/breakpoints/multipliers'); + $settings = breakpoints_settings(); + foreach ($settings->multipliers as $multiplier) { + $this->assertRaw($multiplier, t('Default multiplier %multiplier found', array('%multiplier' => $multiplier))); + if ($multiplier != '1x') { + $this->assertFieldByName('multipliers[' . $multiplier . ']', $multiplier); + } + } + + // Verify the '1x' multiplier can't be deleted. + $this->drupalGet('admin/config/media/breakpoints/multipliers/1x/delete'); + $this->assertText(t('Multiplier 1x can not be deleted!'), t('Multiplier 1x can not be deleted')); + $this->assertNoFieldById('edit-submit'); + + // Add a multiplier. + $new_multiplier = drupal_strtolower($this->randomName()); + $this->drupalGet('admin/config/media/breakpoints/multipliers'); + $edit = array( + 'multipliers[new]' => $new_multiplier, + ); + $this->drupalPost(NULL, $edit, t('Save')); + + // Verify the multiplier was added to the database. + $settings = breakpoints_settings(); + $this->assertTrue(in_array($new_multiplier, $settings->multipliers), t('Multiplier %multiplier was added.', array('%multiplier' => $new_multiplier))); + + // Verify the new multiplier is visible on the multiplier overview page. + $this->assertFieldByName('multipliers[' . $new_multiplier . ']', $new_multiplier); + + // Update a multiplier. + $updated_multiplier = drupal_strtolower($this->randomName()); + $edit = array( + 'multipliers[' . $new_multiplier . ']' => $updated_multiplier, + ); + $this->drupalPost(NULL, $edit, t('Save')); + + // Verify the multiplier was updated in the database. + $settings = breakpoints_settings(); + $this->assertFalse(in_array($new_multiplier, $settings->multipliers), t('Multiplier %multiplier was updated.', array('%multiplier' => $updated_multiplier))); + $this->assertTrue(in_array($updated_multiplier, $settings->multipliers), t('Multiplier %multiplier was updated.', array('%multiplier' => $updated_multiplier))); + + // Verify the updated multiplier is visible on the multiplier overview page. + $this->assertNoFieldByName('multipliers[' . $new_multiplier . ']'); + $this->assertFieldByName('multipliers[' . $updated_multiplier . ']', $updated_multiplier); + $new_multiplier = $updated_multiplier; + + // Verify the default multipliers are visible on the global breakpoints page. + $this->drupalGet('admin/config/media/breakpoints'); + foreach (breakpoints_breakpoint_load_all() as $breakpoint) { + foreach ($settings->multipliers as $multiplier) { + if ($multiplier != '1x') { + $this->assertFieldByName('breakpoints[' . $breakpoint->machine_name . '][multipliers][' . $multiplier . ']'); + } + else { + // Multiplier 1x can not be disabled for any breakpoint. + $this->assertNoFieldByName('breakpoints[' . $breakpoint->machine_name . '][multipliers][' . $multiplier . ']'); + } + } + } + + // Enable a multiplier for a breakpoint and verify if it's enabled on all pages. + $edit = array( + 'breakpoints[breakpoints.theme.breakpoints_test_theme.narrow][multipliers][1.5x]' => 1, + 'breakpoints[breakpoints.theme.breakpoints_test_theme.narrow][multipliers][' . $new_multiplier . ']' => 1, + ); + $this->drupalPost(NULL, $edit, t('Save')); + + // Verify the checkbox for the enabled multipliers is checked on the global breakpoints page. + $this->assertFieldChecked('edit-breakpoints-breakpointsthemebreakpoints-test-themenarrow-multipliers-15x'); + $this->assertFieldChecked('edit-breakpoints-breakpointsthemebreakpoints-test-themenarrow-multipliers-' . drupal_clean_css_identifier($new_multiplier)); + + // Verify the checkbox for the enabled multipliers is checked on the breakpoints page of a group. + $this->drupalGet('admin/config/media/breakpoints/groups/breakpoints_test_theme'); + $this->assertFieldChecked('edit-breakpoints-breakpointsthemebreakpoints-test-themenarrow-multipliers-15x'); + $this->assertFieldChecked('edit-breakpoints-breakpointsthemebreakpoints-test-themenarrow-multipliers-' . drupal_clean_css_identifier($new_multiplier)); + + // Delete a multiplier. + $this->drupalGet('admin/config/media/breakpoints/multipliers/' . $new_multiplier . '/delete'); + $this->drupalPost(NULL, array(), t('Confirm')); + $this->assertText('Multiplier ' . $new_multiplier . ' was deleted'); + + // Verify the deleted multiplier is no longer visible on the multiplier overview page. + $this->drupalGet('admin/config/media/breakpoints/multipliers'); + $this->assertNoFieldByName('multipliers[' . $new_multiplier . ']'); + + // Verify the deleted multiplier is deleted from the database. + $settings = breakpoints_settings(); + $this->assertFalse(in_array($new_multiplier, $settings->multipliers), t('Multiplier %multiplier was deleted.', array('%multiplier' => $new_multiplier))); + + // Verify the deleted multiplier is no longer visible on the breakpoints page. + $this->drupalGet('admin/config/media/breakpoints'); + foreach (breakpoints_breakpoint_load_all() as $breakpoint) { + $this->assertNoFieldByName('breakpoints[' . $breakpoint->machine_name . '][multipliers][' . $new_multiplier . ']'); + } + } +} diff --git a/frontend/drupal/sites/all/modules/breakpoints/css/breakpoints.admin.css b/frontend/drupal/sites/all/modules/breakpoints/css/breakpoints.admin.css new file mode 100644 index 000000000..1cfe33f33 --- /dev/null +++ b/frontend/drupal/sites/all/modules/breakpoints/css/breakpoints.admin.css @@ -0,0 +1,11 @@ +tr.odd.breakpoints-status-disabled { + background: none repeat scroll 0 0 #ee7777; +} +tr.even.breakpoints-status-disabled { + background: none repeat scroll 0 0 #ff9999; +} + +a.breakpoints-group-operations-link { + margin-bottom: 1em; + margin-right: 1em; +} diff --git a/frontend/drupal/sites/all/modules/breakpoints/plugins/export_ui/breakpoints.inc b/frontend/drupal/sites/all/modules/breakpoints/plugins/export_ui/breakpoints.inc new file mode 100644 index 000000000..b5ba05295 --- /dev/null +++ b/frontend/drupal/sites/all/modules/breakpoints/plugins/export_ui/breakpoints.inc @@ -0,0 +1,7 @@ + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/frontend/drupal/sites/all/modules/picture/README.txt b/frontend/drupal/sites/all/modules/picture/README.txt new file mode 100644 index 000000000..70d572b9e --- /dev/null +++ b/frontend/drupal/sites/all/modules/picture/README.txt @@ -0,0 +1,22 @@ +-- SUMMARY -- + +Picture element + +Hide image +---------- + +If you use the '- empty image -' option, you have to add the following +to your theme css to completely hide the image, otherwise it will +still take some space. + +img[width="1"][height="1"] { + display: none; +} + +Warning: +For now this CSS will not work until https://drupal.org/node/2280471 +gets fixed. It is postponed until the upstream issues +https://github.com/ResponsiveImagesCG/picture-element/issues/50 and +https://github.com/ResponsiveImagesCG/picture-element/issues/85 +are fixed. Meanwhile you can hide the image with css using breakpoints. See +https://drupal.org/node/2280471 for more info. diff --git a/frontend/drupal/sites/all/modules/picture/ckeditor/plugins/plugin.js b/frontend/drupal/sites/all/modules/picture/ckeditor/plugins/plugin.js new file mode 100644 index 000000000..eb5731afc --- /dev/null +++ b/frontend/drupal/sites/all/modules/picture/ckeditor/plugins/plugin.js @@ -0,0 +1,268 @@ +/** + * @file Plugin to support responsive images with the Picture module and + * the CKEditor module. + */ +( function(){ + CKEDITOR.plugins.add('picture_ckeditor', + { + init : function(editor) + { + + // Used later to ensure the required features have been enabled in the + // Advanced Content Filter. + features = { + 'imageSize': { 'requiredContent': 'img[data-picture-mapping]' }, + 'imageAlign': { 'requiredContent': 'img[data-picture-align]' } + }; + + // If we have image2, enable the more advanced functionality + if (CKEDITOR.config.plugins.indexOf('image2') != -1) { + // CKEditor's normal alignment will be removed below, we need to + // provide replacement classes based on Picture data + CKEDITOR.addCss('img[data-picture-align="right"] { float: right; }'); + CKEDITOR.addCss('img[data-picture-align="left"] { float: left; }'); + CKEDITOR.addCss('img[data-picture-align="center"] { display: block; margin-left: auto; margin-right: auto; }'); + CKEDITOR.addCss('span[data-cke-display-name="image"] { display: block; }'); + } + + // Else, we need check if regular image is installed + else if (CKEDITOR.config.plugins.indexOf('image') != -1) { + CKEDITOR.config.disableObjectResizing = true; + } + + // When opening a dialog, a 'definition' is created for it. For + // each editor instance the 'dialogDefinition' event is then + // fired. We can use this event to make customizations to the + // definition of existing dialogs. + CKEDITOR.on('dialogDefinition', function(event) { + // Take the dialog name. + if ((event.editor != editor)) return; + var dialogName = event.data.name; + // The definition holds the structured data that is used to eventually + // build the dialog and we can use it to customize just about anything. + // In Drupal terms, it's sort of like CKEditor's version of a Forms API and + // what we're doing here is a bit like a hook_form_alter. + var dialogDefinition = event.data.definition; + + + if (dialogName == 'image2') { + var infoTab = dialogDefinition.getContents('info'); + // UpdatePreview is copied from ckeditor image plugin. + var updatePreview = function(dialog) { + // Don't load before onShow. + if (!dialog.originalElement || !dialog.preview) { + return 1; + } + + // Read attributes and update imagePreview. + dialog.commitContent(PREVIEW, dialog.preview); + return 0; + }; + // Add the select list for choosing the image width. + infoTab.add({ + type: 'select', + id: 'imageSize', + label: Drupal.settings.picture.label, + items: Drupal.settings.picture.mappings, + 'default': 'not_set', + requiredContent: features.imageSize.requiredContent, + setup: function(widget) { + mapping = widget.parts.image.getAttribute('data-picture-mapping'); + this.setValue(mapping ? mapping : 'not_set'); + }, + // Create a custom data-picture-mapping attribute. + commit: function(widget) { + widget.parts.image.setAttribute('data-picture-mapping', this.getValue()); + }, + validate: function() { + if (this.getValue() == 'not_set') { + var message = 'Please make a selection from ' + Drupal.settings.picture.label; + alert(message); + return false; + } else { + return true; + } + } + } + ); + + // Alignment's inline styling should be deprecated in favor of + // picture's data attributes + infoTab.remove('alignment'); + + // Picture breaks captions + infoTab.remove('hasCaption'); + + // Add a select widget to choose image alignment. + infoTab.add({ + type: 'select', + id: 'imageAlign', + label: 'Image Alignment', + items: [ [ 'Not Set', '' ], [ 'Left', 'left'], + [ 'Right', 'right' ], [ 'Center', 'center'] ], + requiredContent: features.imageAlign.requiredContent, + setup: function(widget) { + alignment = widget.parts.image.getAttribute('data-picture-align'); + this.setValue(alignment ? alignment : ''); + }, + // Creates a custom data-picture-align attribute since working with classes + // is more difficult. If we used classes, then we'd have to search for + // exisiting alignment classes and remove them before adding a new one. + // With the custom attribute we can always just overwrite it's value. + commit: function(widget) { + if (this.getValue() || this.isChanged()) { + widget.parts.image.setAttribute('data-picture-align', this.getValue()); + } + } + + }, + // Position before imageSize. + 'imageSize' + ); + } + // Resources for the following: + // Download: https://github.com/ckeditor/ckeditor-dev + // See /plugins/image/dialogs/image.js + // and refer to http://docs.ckeditor.com/#!/api/CKEDITOR.dialog.definition + // Visit: file:///[path_to_ckeditor-dev]/plugins/devtools/samples/devtools.html + // for an excellent way to find machine names for dialog elements. + else if (dialogName == 'image') { + dialogDefinition.removeContents('Link'); + var infoTab = dialogDefinition.getContents('info'); + var altText = infoTab.get('txtAlt'); + var IMAGE = 1, + LINK = 2, + PREVIEW = 4, + CLEANUP = 8; + // UpdatePreview is copied from ckeditor image plugin. + var updatePreview = function(dialog) { + // Don't load before onShow. + if (!dialog.originalElement || !dialog.preview) { + return 1; + } + + // Read attributes and update imagePreview. + dialog.commitContent(PREVIEW, dialog.preview); + return 0; + }; + // Add the select list for choosing the image width. + infoTab.add({ + type: 'select', + id: 'imageSize', + label: Drupal.settings.picture.label, + items: Drupal.settings.picture.mappings, + 'default': 'not_set', + onChange: function() { + var dialog = this.getDialog(); + var element = dialog.originalElement; + element.setAttribute('data-picture-mapping', this.getValue()); + updatePreview(this.getDialog()); + }, + setup: function(type, element) { + if (type == IMAGE) { + var value = element.getAttribute('data-picture-mapping'); + this.setValue(value); + } + }, + // Create a custom data-picture-mapping attribute. + commit: function(type, element) { + if (type == IMAGE) { + if (this.getValue() || this.isChanged()) { + element.setAttribute('data-picture-mapping', this.getValue()); + } + } else if (type == PREVIEW) { + element.setAttribute('data-picture-mapping', this.getValue()); + } else if (type == CLEANUP) { + element.setAttribute('data-picture-mapping', ''); + } + }, + validate: function() { + if (this.getValue() == 'not_set') { + var message = 'Please make a selection from ' + Drupal.settings.picture.label; + alert(message); + return false; + } else { + return true; + } + } + }, + // Position before preview. + 'htmlPreview' + ); + + // Put a title attribute field on the main 'info' tab. + infoTab.add( { + type: 'text', + id: 'txtTitle', + label: 'The title attribute is used as a tooltip when the mouse hovers over the image.', + onChange: function() { + updatePreview(this.getDialog()); + }, + setup: function(type, element) { + if (type == IMAGE) + this.setValue(element.getAttribute('title')); + }, + commit: function(type, element) { + if (type == IMAGE) { + if (this.getValue() || this.isChanged()) + element.setAttribute('title', this.getValue()); + } else if (type == PREVIEW) { + element.setAttribute('title', this.getValue()); + } else if (type == CLEANUP) { + element.removeAttribute('title'); + } + } + }, + // Position before the imageSize select box. + 'htmlPreview' + ); + + // Add a select widget to choose image alignment. + infoTab.add({ + type: 'select', + id: 'imageAlign', + label: 'Image Alignment', + items: [ [ 'Not Set', '' ], [ 'Left', 'left'], + [ 'Right', 'right' ], [ 'Center', 'center'] ], + 'default': '', + onChange: function() { + updatePreview(this.getDialog()); + }, + setup: function(type, element) { + if (type == IMAGE) { + var value = element.getAttribute('data-picture-align'); + this.setValue(value); + } + }, + // Creates a custom data-picture-align attribute since working with classes + // is more difficult. If we used classes, then we'd have to search for + // exisiting alignment classes and remove them before adding a new one. + // With the custom attribute we can always just overwrite it's value. + commit: function(type, element) { + if (type == IMAGE) { + if (this.getValue() || this.isChanged()) { + element.setAttribute('data-picture-align', this.getValue()); + } + } else if (type == PREVIEW) { + element.setAttribute('data-picture-align', this.getValue()); + } else if (type == CLEANUP) { + element.setAttribute('data-picture-align', ''); + } + } + + }, + // Position before imageSize. + 'imageSize' + ); + + // Improve the alt field label. Copied from Drupal's image field. + altText.label = 'The alt attribute may be used by search engines, and screen readers.'; + + // Remove a bunch of extraneous fields. These properties will be set in + // the theme or module CSS. + infoTab.remove('basic'); + } + }); + } + }); +})(); diff --git a/frontend/drupal/sites/all/modules/picture/ctools/plugins/export_ui/picture_mapping.inc b/frontend/drupal/sites/all/modules/picture/ctools/plugins/export_ui/picture_mapping.inc new file mode 100644 index 000000000..7beee01b8 --- /dev/null +++ b/frontend/drupal/sites/all/modules/picture/ctools/plugins/export_ui/picture_mapping.inc @@ -0,0 +1,56 @@ + 'picture_mapping', + 'access' => 'administer pictures', + 'menu' => array( + 'menu prefix' => 'admin/config/media', + 'menu item' => 'picture', + 'menu title' => 'Picture mappings', + 'menu description' => 'Manage picture mappings.', + ), + 'title singular' => t('picture mapping'), + 'title plural' => t('picture mappings'), + 'title singular proper' => t('Picture mapping'), + 'title plural proper' => t('Picture mappings'), + 'form' => array( + 'settings' => 'picture_mapping_form', + ), + 'use wizard' => TRUE, + 'form info' => array( + 'free trail' => FALSE, + 'order' => array( + 'pick_breakpoint_group' => t('Breakpoint group'), + 'configure' => t('Configure picture mappings'), + ), + 'edit order' => array( + 'configure' => t('Configure the matching engine'), + ), + 'clone order' => array( + 'configure' => t('Configure the matching engine'), + ), + 'import order' => array( + 'import' => t('Import code'), + ), + 'forms' => array( + 'pick_breakpoint_group' => array( + 'form id' => 'picture_select_breakpoint_group_form', + ), + ), + ), + ); +} + +/** + * Define this Export UI plugin. + */ +$plugin = picture_picture_mapping_ctools_export_ui(); diff --git a/frontend/drupal/sites/all/modules/picture/flexslider_picture/flexslider_picture.info b/frontend/drupal/sites/all/modules/picture/flexslider_picture/flexslider_picture.info new file mode 100644 index 000000000..c0e14173e --- /dev/null +++ b/frontend/drupal/sites/all/modules/picture/flexslider_picture/flexslider_picture.info @@ -0,0 +1,15 @@ +name = FlexSlider Picture +description = Integrates the Picture module with the FlexSlider module for a truly responsive slider. +package = Picture +core = 7.x + +dependencies[] = picture +dependencies[] = flexslider (2.x) +dependencies[] = flexslider_fields (2.x) + +; Information added by Drupal.org packaging script on 2015-10-13 +version = "7.x-2.13" +core = "7.x" +project = "picture" +datestamp = "1444740240" + diff --git a/frontend/drupal/sites/all/modules/picture/flexslider_picture/flexslider_picture.install b/frontend/drupal/sites/all/modules/picture/flexslider_picture/flexslider_picture.install new file mode 100644 index 000000000..d96d3f78f --- /dev/null +++ b/frontend/drupal/sites/all/modules/picture/flexslider_picture/flexslider_picture.install @@ -0,0 +1,159 @@ + 'Saves which flexslider optionsets use picture mappings and which use image styles.', + 'export' => array( + 'key' => 'flexslider_optionset', + 'identifier' => 'flexslider_picture_optionset', + 'api' => array( + 'owner' => 'flexslider_picture', + 'api' => 'flexslider_picture_optionset', + 'minimum_version' => 1, + 'current_version' => 1, + ), + ), + 'fields' => array( + 'id' => array( + 'type' => 'serial', + 'not null' => TRUE, + 'description' => 'The internal identifier.', + 'no export' => TRUE, + ), + 'flexslider_optionset' => array( + 'description' => 'The machine-readable option set name.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + ), + 'imagestyle_type' => array( + 'description' => 'One of image_style or picture_mapping.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + ), + 'mapping' => array( + 'description' => 'The picture mapping for this optionset.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => FALSE, + ), + 'fallback' => array( + 'description' => 'The style machine name.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + ), + 'primary key' => array('id'), + 'indexes' => array( + 'imagestyle_type' => array('imagestyle_type'), + ), + ); + return $schema; +} + +/** + * Implements hook_install(). + */ +function flexslider_picture_install() { + // For each existing flexslider config, add a record in + // {flexslider_picture_optionset}. + $q = db_select('flexslider_optionset', 'f'); + $q->addField('f', 'name', 'flexslider_optionset'); + $q->addExpression("'image_style'", 'imagestyle_type'); + $q->addExpression("''", 'mapping'); + $q->addExpression("''", 'fallback'); + db_insert('flexslider_picture_optionset')->fields(array( + 'flexslider_optionset', + 'imagestyle_type', + 'mapping', + 'fallback', + ))->from($q)->execute(); +} + +/** + * Implements hook_schema_alter(). + */ +function flexslider_picture_schema_alter(&$schema) { + $schema['flexslider_optionset']['join']['flexslider_picture'] = array( + 'table' => 'flexslider_picture_optionset', + 'left_key' => 'name', + 'right_key' => 'flexslider_optionset', + 'callback' => 'flexslider_picture_join_callback', + 'load' => array( + 'imagestyle_type', + 'mapping', + 'fallback', + ), + 'fields' => array( + 'imagestyle_type' => array( + 'description' => 'One of image_style or picture_mapping.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + ), + 'mapping' => array( + 'description' => 'The picture mapping for this optionset.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => FALSE, + ), + 'fallback' => array( + 'description' => 'The style machine name.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + ), + ); +} + +/** + * Add the 'fallback' column to the {flexslider_picture_optionset} table. + */ +function flexslider_picture_update_7001() { + if (!db_field_exists('flexslider_picture_optionset', 'fallback')) { + db_add_field( + 'flexslider_picture_optionset', + 'fallback', + array( + 'description' => 'The style machine name.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ) + ); + } +} + +/** + * Add flexslider_picture_optionsets for existing flexslider optionsets. + */ +function flexslider_picture_update_7201() { + // For each existing flexslider config, add a record in + // {flexslider_picture_optionset}. + $q = db_select('flexslider_optionset', 'f'); + $q->addField('f', 'name', 'flexslider_optionset'); + $q->addExpression("'image_style'", 'imagestyle_type'); + $q->addExpression("''", 'mapping'); + $q->addExpression("''", 'fallback'); + db_insert('flexslider_picture_optionset')->fields(array( + 'flexslider_optionset', + 'imagestyle_type', + 'mapping', + 'fallback', + ))->from($q)->execute(); +} diff --git a/frontend/drupal/sites/all/modules/picture/flexslider_picture/flexslider_picture.module b/frontend/drupal/sites/all/modules/picture/flexslider_picture/flexslider_picture.module new file mode 100644 index 000000000..6a410b013 --- /dev/null +++ b/frontend/drupal/sites/all/modules/picture/flexslider_picture/flexslider_picture.module @@ -0,0 +1,249 @@ + array( + 'label' => t('flexslider'), + 'field types' => array('image', 'media'), + 'settings' => array( + 'optionset' => 'default', + 'image_style' => '', + 'caption' => FALSE, + ), + ), + ); +} + +/** + * Implements hook_field_formatter_settings_form(). + * + * Provides display settings form within the manage display page of + * an image field with formatter flexslider. + */ +function flexslider_picture_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) { + return flexslider_fields_field_formatter_settings_form($field, $instance, $view_mode, $form, $form_state); +} + +/** + * Implements hook_field_formatter_settings_summary(). + * + * Displays the summary of the options of a flexslider formatted image field. + */ +function flexslider_picture_field_formatter_settings_summary($field, $instance, $view_mode) { + return flexslider_fields_field_formatter_settings_summary($field, $instance, $view_mode); +} + +/** + * Implements hook_field_formatter_view(). + * + * Prepares a renderable array of images and adds the neccessary JS and CSS. + */ +function flexslider_picture_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) { + $element = flexslider_fields_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display); + $optionsets = array(); + foreach (element_children($element) as $child) { + if (!isset($optionsets[$element[$child]['#settings']['optionset']])) { + $optionsets[$element[$child]['#settings']['optionset']] = flexslider_optionset_load($element[$child]['#settings']['optionset']); + } + $optionset = $optionsets[$element[$child]['#settings']['optionset']]; + if ($optionset && isset($optionset->imagestyle_type) && !empty($optionset->imagestyle_type) && $optionset->imagestyle_type == 'picture_mapping') { + $element[$child]['#attached'] = array( + 'library' => array( + array('picture', 'matchmedia'), + array('picture', 'picturefill'), + array('picture', 'picture.ajax'), + ), + ); + } + } + return $element; +} + +/** + * Implements hook_form_FORM_ID_alter(). + */ +function flexslider_picture_form_ctools_export_ui_edit_item_form_alter(&$form, &$form_state) { + if ($form_state['plugin']['schema'] == 'flexslider_optionset') { + $form['image_style']['#tree'] = TRUE; + $form['image_style']['#weight'] = 0; + $form['title']['#weight'] = -1; + $form['image_style']['imagestyle_type'] = array( + '#type' => 'radios', + '#title' => t('Image type'), + '#description' => t( + 'Choosing picture mapping will give you a truly responsive slider. + Flexslider is responsive because it will resize the images for smaller screens, but it will still load the large image. + With picture mappings, a smaller image will be loaded for smaller screens (and thus less data transfer).'), + '#options' => array( + 'image_style' => t('Image Style'), + 'picture_mapping' => t('Picture Mapping'), + ), + '#weight' => -2, + '#default_value' => (isset($form_state['item']->imagestyle_type) && !empty($form_state['item']->imagestyle_type)) ? $form_state['item']->imagestyle_type : 'image_style', + ); + + $options = picture_get_mapping_options(); + $form['image_style']['mapping'] = array( + '#title' => t('Normal picuture mapping'), + '#states' => array( + 'visible' => array( + ':input[name="image_style[imagestyle_type]"]' => array('value' => 'picture_mapping'), + ), + ), + '#weight' => -1, + ); + if (!empty($options)) { + $form['image_style']['mapping'] += array( + '#type' => 'select', + '#description' => t('Picture mapping for the main stage images.'), + '#options' => $options, + '#default_value' => (!empty($form_state['item']->mapping)) ? $form_state['item']->mapping : '', + ); + } + else { + $form['image_style']['mapping'] += array( + '#type' => 'item', + '#markup' => t("There're no picture mappings defined, you'll have to !create them first.", array('!create' => l(t('create'), 'admin/config/media/picture'))), + ); + } + $image_styles = image_style_options(FALSE); + $form['image_style']['fallback'] = array( + '#title' => t('Fallback image style'), + '#description' => t('The fallback image style to use'), + '#type' => 'select', + '#empty_option' => t('Automatic'), + '#options' => $image_styles + array( + PICTURE_EMPTY_IMAGE => t('Empty image'), + PICTURE_ORIGINAL_IMAGE => t('Original image'), + ), + '#states' => array( + 'visible' => array( + ':input[name="image_style[imagestyle_type]"]' => array('value' => 'picture_mapping'), + ), + ), + '#default_value' => (!empty($form_state['item']->fallback)) ? $form_state['item']->fallback : '', + ); + + // Colorbox support. + $form['image_style']['colorboxEnabled'] = array( + '#title' => t('Enable colorbox'), + '#type' => 'checkbox', + '#default_value' => (!empty($form_state['item']->options['colorboxEnabled'])) ? $form_state['item']->options['colorboxEnabled'] : '', + ); + $form['image_style']['colorboxImageStyle'] = array( + '#title' => t('Colorbox group'), + '#type' => 'select', + '#default_value' => (!empty($form_state['item']->options['colorboxImageStyle'])) ? $form_state['item']->options['colorboxImageStyle'] : '', + '#required' => FALSE, + '#options' => $options, + '#states' => array( + 'visible' => array( + ':input[name$="image_style[colorboxEnabled]"]' => array('checked' => TRUE), + ), + ), + ); + + $form['image_style']['colorboxFallbackImageStyle'] = array( + '#title' => t('Colorbox fallback image style'), + '#type' => 'select', + '#default_value' => (!empty($form_state['item']->options['colorboxFallbackImageStyle'])) ? $form_state['item']->options['colorboxFallbackImageStyle'] : '', + '#required' => FALSE, + '#empty_option' => t('Automatic'), + '#options' => $image_styles + array( + PICTURE_EMPTY_IMAGE => t('Empty image'), + PICTURE_ORIGINAL_IMAGE => t('Original image'), + ), + '#states' => array( + 'visible' => array( + ':input[name$="image_style[colorboxEnabled]"]' => array('checked' => TRUE), + ), + ), + ); + + $form['#submit'][] = '_flexslider_picture_ctools_export_ui_edit_item_form_submit'; + } +} + +/** + * Submit callback. + */ +function _flexslider_picture_ctools_export_ui_edit_item_form_submit($form, &$form_state) { + $fields = array_intersect_key($form_state['values']['image_style'], drupal_map_assoc(array( + 'imagestyle_type', + 'mapping', + 'fallback', + ))); + $fields['flexslider_optionset'] = $form_state['values']['name']; + db_merge('flexslider_picture_optionset') + ->key(array('flexslider_optionset' => $form_state['values']['name'])) + ->fields($fields) + ->execute(); + + // Add the colorbox options into the generic options field. + // @todo Should we move it into our own table or how likely is it that + // flexslider itself will add colorbox support and thus add "similar" options? + $optionset = &$form_state['optionset']; + $optionset->options['colorboxEnabled'] = !empty($form_state['values']['image_style']['colorboxEnabled']); + $optionset->options['colorboxImageStyle'] = !empty($form_state['values']['image_style']['colorboxImageStyle']) ? $form_state['values']['image_style']['colorboxImageStyle'] : ''; + $optionset->options['colorboxFallbackImageStyle'] = !empty($form_state['values']['image_style']['colorboxFallbackImageStyle']) ? $form_state['values']['image_style']['colorboxFallbackImageStyle'] : ''; +} + +/** + * Join callback. + * + * @see flexslider_picture_schema_alter() + */ +function flexslider_picture_join_callback(&$query, $schema, $join_schema) { + $tables = &$query->getTables(); + foreach ($tables as &$table) { + if ($table['table'] == 'flexslider_picture_optionset') { + $table['join type'] = 'LEFT OUTER'; + break; + } + } +} + +/** + * Implements hook_flexslider_default_presets_alter(). + * + * Mashed up the default presets of flexslider and picture. + */ +function flexslider_picture_flexslider_default_presets_alter(&$defaults) { + foreach ($defaults as $name => &$settings) { + $default_preset = ctools_get_default_object('flexslider_picture_optionset', $name); + if (!empty($default_preset)) { + $settings->imagestyle_type = $default_preset->imagestyle_type; + $settings->mapping = $default_preset->mapping; + $settings->fallback = $default_preset->fallback; + } + } +} diff --git a/frontend/drupal/sites/all/modules/picture/flexslider_picture/theme/flexslider_picture.theme.inc b/frontend/drupal/sites/all/modules/picture/flexslider_picture/theme/flexslider_picture.theme.inc new file mode 100644 index 000000000..fcbfdfeb9 --- /dev/null +++ b/frontend/drupal/sites/all/modules/picture/flexslider_picture/theme/flexslider_picture.theme.inc @@ -0,0 +1,147 @@ +imagestyle_type) && !empty($optionset->imagestyle_type) && $optionset->imagestyle_type == 'picture_mapping'); + + if ($vars['picture_formatter_enabled']) { + $items = &$vars['items']; + + // Get the breakpoints based on the mapping. + $fallback_image_style = $optionset->fallback; + $mappings = picture_mapping_load($optionset->mapping); + if (!$mappings) { + trigger_error(check_plain("Unable to load picture mapping {$optionset->mapping}."), E_USER_ERROR); + return; + } + $breakpoint_styles = picture_get_mapping_breakpoints($mappings, $fallback_image_style); + + // If colorbox is enabled build additional configuration. + if (!empty($optionset->options['colorboxEnabled'])) { + // Add additional necessary scripts and styles. + drupal_add_js(drupal_get_path('module', 'picture') . '/picture_colorbox.js'); + drupal_add_css(drupal_get_path('module', 'picture') . '/picture_colorbox.css'); + if (!variable_get('colorbox_inline', 0)) { + drupal_add_js(drupal_get_path('module', 'colorbox') . '/js/colorbox_inline.js'); + } + + $colorbox_fallback_image_style = $optionset->options['colorboxFallbackImageStyle']; + $mappings = picture_mapping_load($optionset->options['colorboxImageStyle']); + if (!$mappings) { + trigger_error(check_plain("Unable to load picture mapping {$optionset->options['colorboxImageStyle']}."), E_USER_ERROR); + $optionset->options['colorboxEnabled'] = FALSE; + } + else { + $colorbox_breakpoints = picture_get_mapping_breakpoints($mappings, $colorbox_fallback_image_style); + // Grouping ID for the colorbox gallery. Use more_entropy to ensure the + // php function works on every environment (cygwin). + $colorbox_group_id = uniqid('flexgroup', TRUE); + } + } + + // Prepare the item slides to be passed to render(). + foreach ($items as &$item) { + if (isset($item['item'])) { + $item['slide'] = array( + '#theme' => 'picture', + '#style_name' => $fallback_image_style, + '#uri' => $item['item']['uri'], + '#height' => $item['item']['height'], + '#width' => $item['item']['width'], + '#alt' => $item['item']['alt'], + '#title' => $item['item']['title'], + '#breakpoints' => $breakpoint_styles, + ); + + // If colorbox is enabled change the theming function and add settings. + if (!empty($optionset->options['colorboxEnabled'])) { + $item['slide'] = array( + '#theme' => 'picture_formatter_colorbox', + '#item' => $item['item'], + '#image_style' => $fallback_image_style, + '#path' => $item['item']['uri'], + '#colorbox_image_style' => $colorbox_fallback_image_style, + '#colorbox' => $colorbox_breakpoints, + '#colorbox_group_id' => $colorbox_group_id, + '#width' => $item['item']['width'], + '#height' => $item['item']['height'], + '#alt' => $item['item']['alt'], + '#title' => $item['item']['title'], + '#breakpoints' => $breakpoint_styles, + ); + } + if (!isset($item['thumb'])) { + $item['thumb'] = image_style_url($fallback_image_style, $item['item']['uri']); + } + } + } + } +} + +/** + * Theme list item. + */ +function template_process_flexslider_picture_list_item(&$vars) { + // Call the default process function first. + template_process_flexslider_list_item($vars); + + $settings = $vars['settings']; + $attributes = &$vars['settings']['attributes']; + if (isset($settings['optionset']->options['controlNav']) and $settings['optionset']->options['controlNav'] === "thumbnails" && !isset($attributes['data-thumb']) || empty($attributes['data-thumb'])) { + if (isset($vars['thumb'])) { + $attributes['data-thumb'] = $vars['thumb']; + } + else { + $src = array(); + preg_match("", $vars['item'], $src); + + if (!empty($src[1])) { + $attributes['data-thumb'] = $src[1]; + } + } + } +} + +/** + * Theme callback. + */ +function theme_flexslider_picture_list(&$vars) { + if (!empty($vars['picture_formatter_enabled'])) { + // Reference configuration variables. + $attributes = &$vars['settings']['attributes']; + $type = &$vars['settings']['type']; + $output = ''; + + // Build the list. + if (!empty($vars['items'])) { + $output .= "<$type" . drupal_attributes($attributes) . '>'; + foreach ($vars['items'] as $item) { + $slide = render($item['slide']); + $output .= theme('flexslider_list_item', array( + 'item' => $slide, + 'thumb' => isset($item['thumb']) ? $item['thumb'] : NULL, + 'settings' => $vars['settings'], + 'caption' => isset($item['caption']) ? $item['caption'] : '', + )); + } + $output .= ""; + } + + return $output; + } + // If this isn't a picture optionset use the default theming. + return theme_flexslider_list($vars); +} diff --git a/frontend/drupal/sites/all/modules/picture/includes/PictureMapping.php b/frontend/drupal/sites/all/modules/picture/includes/PictureMapping.php new file mode 100644 index 000000000..94a7fd72d --- /dev/null +++ b/frontend/drupal/sites/all/modules/picture/includes/PictureMapping.php @@ -0,0 +1,397 @@ + $info) { + if (isset($data->{$field})) { + $this->{$field} = !empty($info['serialize']) && is_string($data->{$field}) ? unserialize($data->{$field}) : $data->{$field}; + } + else { + $this->{$field} = NULL; + } + unset($data->{$field}); + } + + if (isset($schema['join'])) { + foreach ($schema['join'] as $join) { + $join_schema = ctools_export_get_schema($join['table']); + if (!empty($join['load'])) { + foreach ($join['load'] as $field) { + $info = $join_schema['fields'][$field]; + $this->{$field} = empty($info['serialize']) ? $data->{$field} : unserialize($data->{$field}); + unset($data->field); + } + } + } + } + + foreach ((array) $data as $field => $val) { + $this->{$field} = $val; + } + $this->loadBreakpointGroup(); + $this->loadAllMappings(); + } + + /** + * Save the picture mapping. + * + * @return false||int + * If the record insert or update failed, returns FALSE. If it succeeded, + * returns SAVED_NEW or SAVED_UPDATED, depending on the operation performed. + */ + public function save() { + $update = array(); + $this->cleanMappings(); + $data = $this->toArray(); + if (isset($this->id) && $this->id) { + $update = array('id'); + $data['id'] = $this->id; + } + $return = drupal_write_record('picture_mapping', $data, $update); + module_load_include('info.inc', 'field'); + field_info_cache_clear(); + $this->setValues(ctools_export_get_schema('picture_mapping'), $data); + $this->loadBreakpointGroup(); + return $return; + } + + /** + * Returns an array of all property values. + * + * @return mixed[] + * An array of property values, keyed by property name. + */ + public function toArray() { + return array( + 'machine_name' => $this->machine_name, + 'label' => $this->label, + 'breakpoint_group' => $this->breakpoint_group && is_object($this->breakpoint_group) ? $this->breakpoint_group->machine_name : $this->breakpoint_group, + 'mapping' => $this->mapping, + ); + } + + /** + * Create a duplicate. + * + * @return PictureMapping + * The duplicate. + */ + public function createDuplicate() { + $clone = clone $this; + $clone->id = NULL; + $clone->machine_name = $this->machine_name . '_clone'; + $clone->label = t('Clone of !label', array('!label' => check_plain($this->label))); + $clone->mapping = $this->mapping; + return $clone; + } + + /** + * Loads the breakpoint group. + */ + protected function loadBreakpointGroup() { + if ($this->breakpoint_group && !is_object($this->breakpoint_group)) { + $breakpoint_group = breakpoints_breakpoint_group_load($this->breakpoint_group); + $this->breakpoint_group = $breakpoint_group; + } + } + + /** + * Loads all mappings and removes non-existing ones. + */ + protected function loadAllMappings() { + $loaded_mappings = $this->mapping; + $all_mappings = array(); + if ($breakpoint_group = $this->breakpoint_group) { + $breakpoints = $breakpoint_group->breakpoints; + foreach ($breakpoints as $breakpoint_id) { + $breakpoint = breakpoints_breakpoint_load_by_fullkey($breakpoint_id); + if ($breakpoint) { + // Get the mapping for the default multiplier. + $all_mappings[$breakpoint_id]['1x'] = ''; + if (isset($loaded_mappings[$breakpoint->machine_name]['1x'])) { + $all_mappings[$breakpoint_id]['1x'] = $loaded_mappings[$breakpoint->machine_name]['1x']; + } + + // Get the mapping for the other multipliers. + if (isset($breakpoint->multipliers) && !empty($breakpoint->multipliers)) { + foreach ($breakpoint->multipliers as $multiplier => $status) { + if ($status) { + $all_mappings[$breakpoint_id][$multiplier] = ''; + if (isset($loaded_mappings[$breakpoint->machine_name][$multiplier])) { + $all_mappings[$breakpoint_id][$multiplier] = $loaded_mappings[$breakpoint->machine_name][$multiplier]; + } + } + } + } + } + } + } + $this->mapping = $all_mappings; + } + + /** + * Clean mappings. + */ + protected function cleanMappings() { + foreach ($this->mapping as $breakpoint => $multipliers) { + foreach ($multipliers as $multiplier => $mapping_definition) { + switch ($mapping_definition['mapping_type']) { + case '_none': + unset($mapping_definition['image_style']); + unset($mapping_definition['sizes']); + unset($mapping_definition['sizes_image_styles']); + break; + + case 'image_style': + unset($mapping_definition['sizes']); + unset($mapping_definition['sizes_image_styles']); + break; + + case 'sizes': + unset($mapping_definition['image_style']); + $mapping_definition['sizes_image_styles'] = array_filter($mapping_definition['sizes_image_styles']); + break; + } + $this->mapping[$breakpoint][$multiplier] = $mapping_definition; + } + } + } + + /** + * Check if there are mappings. + * + * @return bool + * TRUE if this PictureMapping has mappings, FALSE otherwise. + */ + public function hasMappings() { + $mapping_found = FALSE; + foreach ($this->mapping as $multipliers) { + foreach ($multipliers as $mapping_definition) { + if (!PictureMapping::isEmptyMappingDefinition($mapping_definition)) { + $mapping_found = TRUE; + break 2; + } + } + } + return $mapping_found; + } + + /** + * Check if a mapping definition is empty. + * + * @return bool + * TRUE if this mapping definition is considered empty, FALSE otherwise. + */ + public static function isEmptyMappingDefinition($mapping_definition) { + if (!empty($mapping_definition) && isset($mapping_definition['mapping_type'])) { + switch ($mapping_definition['mapping_type']) { + case 'sizes': + if ($mapping_definition['sizes'] && array_filter($mapping_definition['sizes_image_styles'])) { + return FALSE; + } + break; + + case 'image_style': + if ($mapping_definition['image_style']) { + return FALSE; + } + break; + } + } + return TRUE; + } + + /** + * Get the machine name. + * + * @return string + * The machine name. + */ + public function getMachineName() { + return $this->machine_name; + } + + /** + * Set the machine name. + */ + public function setMachineName($machine_name) { + $this->machine_name = $machine_name; + } + + /** + * Get the picture mappings. + * + * @return array + * The mappings. + */ + public function getMappings() { + return $this->mapping; + } + + /** + * Set the picture mappings. + */ + public function setMappings($mappings) { + $this->mapping = $mappings; + } + + /** + * Set the label. + */ + public function setLabel($label) { + $this->label = $label; + } + + /** + * Get the label. + * + * @return string + * The label. + */ + public function label() { + return $this->label; + } + + /** + * Set the breakpoint group. + */ + public function setBreakpointGroup($breakpoint_group) { + if (!$this->getBreakpointGroup() || $breakpoint_group != $this->getBreakpointGroup()->name) { + $this->breakpoint_group = $breakpoint_group; + $this->loadBreakpointGroup(); + $this->loadAllMappings(); + } + } + + /** + * Get the breakpoint group. + * + * @return object + * The breakpoint group object. + */ + public function getBreakpointGroup() { + $this->loadBreakpointGroup(); + return $this->breakpoint_group; + } + + /** + * Is utilized for reading data from inaccessible properties. + */ + public function __get($name) { + switch ($name) { + case 'machine_name': + return $this->getMachineName(); + + case 'label': + return $this->label(); + + case 'mapping': + return $this->getMappings(); + + case 'breakpoint_group': + if ($this->isExporting) { + return $this->breakpoint_group; + } + return $this->getBreakpointGroup(); + + default: + return $this->{$name}; + } + } + + /** + * Is run when writing data to inaccessible properties. + */ + public function __set($name, $value) { + switch ($name) { + case 'machine_name': + $this->setMachineName($value); + break; + + case 'label': + $this->setLabel($value); + break; + + case 'mapping': + $this->setMappings($value); + break; + + case 'breakpoint_group': + $this->setBreakpointGroup($value); + break; + + default: + $this->{$name} = $value; + break; + } + } + + /** + * Is triggered by calling isset() or empty() on inaccessible properties. + */ + public function __isset($name) { + return isset($this->{$name}); + } + + /** + * Export this PictureMapping. + * + * @return string + * The export string. + */ + public function export($indent = '') { + $this->cleanMappings(); + $this->breakpoint_group = $this->getBreakpointGroup() ? $this->getBreakpointGroup()->machine_name : $this->breakpoint_group; + $this->isExporting = TRUE; + $export = ctools_export_object('picture_mapping', $this, $indent); + $this->isExporting = TRUE; + $this->loadBreakpointGroup(); + return $export; + } + +} diff --git a/frontend/drupal/sites/all/modules/picture/lazysizes/lazysizes.js b/frontend/drupal/sites/all/modules/picture/lazysizes/lazysizes.js new file mode 100644 index 000000000..6c34c372f --- /dev/null +++ b/frontend/drupal/sites/all/modules/picture/lazysizes/lazysizes.js @@ -0,0 +1,547 @@ +(function(window, factory) { + var lazySizes = factory(window, window.document); + window.lazySizes = lazySizes; + if(typeof module == 'object' && module.exports){ + module.exports = lazySizes; + } else if (typeof define == 'function' && define.amd) { + define(lazySizes); + } +}(window, function(window, document) { + 'use strict'; + /*jshint eqnull:true */ + if(!document.getElementsByClassName){return;} + + var lazySizesConfig; + + var docElem = document.documentElement; + + var addEventListener = window.addEventListener; + + var setTimeout = window.setTimeout; + + var rAF = window.requestAnimationFrame || setTimeout; + + var regPicture = /^picture$/i; + + var loadEvents = ['load', 'error', 'lazyincluded', '_lazyloaded']; + + var hasClass = function(ele, cls) { + var reg = new RegExp('(\\s|^)'+cls+'(\\s|$)'); + return ele.className.match(reg) && reg; + }; + + var addClass = function(ele, cls) { + if (!hasClass(ele, cls)){ + ele.className += ' '+cls; + } + }; + + var removeClass = function(ele, cls) { + var reg; + if ((reg = hasClass(ele,cls))) { + ele.className = ele.className.replace(reg, ' '); + } + }; + + var addRemoveLoadEvents = function(dom, fn, add){ + var action = add ? 'addEventListener' : 'removeEventListener'; + if(add){ + addRemoveLoadEvents(dom, fn); + } + loadEvents.forEach(function(evt){ + dom[action](evt, fn); + }); + }; + + var triggerEvent = function(elem, name, detail, noBubbles, noCancelable){ + var event = document.createEvent('CustomEvent'); + + event.initCustomEvent(name, !noBubbles, !noCancelable, detail || {}); + + event.details = event.detail; + + elem.dispatchEvent(event); + return event; + }; + + var updatePolyfill = function (el, full){ + var polyfill; + if(!window.HTMLPictureElement){ + if( ( polyfill = (window.picturefill || window.respimage || lazySizesConfig.pf) ) ){ + polyfill({reevaluate: true, elements: [el]}); + } else if(full && full.src){ + el.src = full.src; + } + } + }; + + var getCSS = function (elem, style){ + return getComputedStyle(elem, null)[style]; + }; + + var getWidth = function(elem, parent, width){ + width = width || elem.offsetWidth; + + while(width < lazySizesConfig.minSize && parent && !elem._lazysizesWidth){ + width = parent.offsetWidth; + parent = parent.parentNode; + } + + return width; + }; + + var throttle = function(fn){ + var running; + var lastTime = 0; + var Date = window.Date; + var run = function(){ + running = false; + lastTime = Date.now(); + fn(); + }; + var afterAF = function(){ + //Todo: test setImmediate + setTimeout(run); + }; + var getAF = function(){ + rAF(afterAF); + }; + + return function(){ + if(running){ + return; + } + var delay = lazySizesConfig.throttle - (Date.now() - lastTime); + + running = true; + + if(delay < 9){ + delay = 9; + } + setTimeout(getAF, delay); + }; + }; + + var loader = (function(){ + var lazyloadElems, preloadElems, isCompleted, resetPreloadingTimer, loadMode; + + var eLvW, elvH, eLtop, eLleft, eLright, eLbottom; + + var defaultExpand, preloadExpand; + + var regImg = /^img$/i; + var regIframe = /^iframe$/i; + + var supportScroll = ('onscroll' in window) && !(/glebot/.test(navigator.userAgent)); + + var shrinkExpand = 0; + var currentExpand = 0; + + var isLoading = 0; + var lowRuns = 1; + + var resetPreloading = function(e){ + isLoading--; + if(e && e.target){ + addRemoveLoadEvents(e.target, resetPreloading); + } + + if(!e || isLoading < 0 || !e.target){ + isLoading = 0; + } + }; + + var isNestedVisible = function(elem, elemExpand){ + var outerRect; + var parent = elem; + var visible = getCSS(elem, 'visibility') != 'hidden'; + + eLtop -= elemExpand; + eLbottom += elemExpand; + eLleft -= elemExpand; + eLright += elemExpand; + + while(visible && (parent = parent.offsetParent)){ + visible = ((getCSS(parent, 'opacity') || 1) > 0); + + if(visible && getCSS(parent, 'overflow') != 'visible'){ + outerRect = parent.getBoundingClientRect(); + visible = eLright > outerRect.left && + eLleft < outerRect.right && + eLbottom > outerRect.top - 1 && + eLtop < outerRect.bottom + 1 + ; + } + } + + return visible; + }; + + var checkElements = function() { + var eLlen, i, rect, autoLoadElem, loadedSomething, elemExpand, elemNegativeExpand, elemExpandVal, beforeExpandVal; + + if((loadMode = lazySizesConfig.loadMode) && isLoading < 8 && (eLlen = lazyloadElems.length)){ + + i = 0; + + lowRuns++; + + if(currentExpand < preloadExpand && isLoading < 1 && lowRuns > 3 && loadMode > 2){ + currentExpand = preloadExpand; + lowRuns = 0; + } else if(currentExpand != defaultExpand && loadMode > 1 && lowRuns > 2 && isLoading < 6){ + currentExpand = defaultExpand; + } else { + currentExpand = shrinkExpand; + } + + for(; i < eLlen; i++){ + + if(!lazyloadElems[i] || lazyloadElems[i]._lazyRace){continue;} + + if(!supportScroll){unveilElement(lazyloadElems[i]);continue;} + + if(!(elemExpandVal = lazyloadElems[i].getAttribute('data-expand')) || !(elemExpand = elemExpandVal * 1)){ + elemExpand = currentExpand; + } + + if(beforeExpandVal !== elemExpand){ + eLvW = innerWidth + elemExpand; + elvH = innerHeight + elemExpand; + elemNegativeExpand = elemExpand * -1; + beforeExpandVal = elemExpand; + } + + rect = lazyloadElems[i].getBoundingClientRect(); + + if ((eLbottom = rect.bottom) >= elemNegativeExpand && + (eLtop = rect.top) <= elvH && + (eLright = rect.right) >= elemNegativeExpand && + (eLleft = rect.left) <= eLvW && + (eLbottom || eLright || eLleft || eLtop) && + ((isCompleted && isLoading < 3 && !elemExpandVal && (loadMode < 3 || lowRuns < 4)) || isNestedVisible(lazyloadElems[i], elemExpand))){ + unveilElement(lazyloadElems[i], rect.width); + loadedSomething = true; + } else if(!loadedSomething && isCompleted && !autoLoadElem && + isLoading < 3 && lowRuns < 4 && loadMode > 2 && + (preloadElems[0] || lazySizesConfig.preloadAfterLoad) && + (preloadElems[0] || (!elemExpandVal && ((eLbottom || eLright || eLleft || eLtop) || lazyloadElems[i].getAttribute(lazySizesConfig.sizesAttr) != 'auto')))){ + autoLoadElem = preloadElems[0] || lazyloadElems[i]; + } + } + + if(autoLoadElem && !loadedSomething){ + unveilElement(autoLoadElem); + } + } + }; + + var throttledCheckElements = throttle(checkElements); + + var switchLoadingClass = function(e){ + addClass(e.target, lazySizesConfig.loadedClass); + removeClass(e.target, lazySizesConfig.loadingClass); + addRemoveLoadEvents(e.target, switchLoadingClass); + }; + + var changeIframeSrc = function(elem, src){ + try { + elem.contentWindow.location.replace(src); + } catch(e){ + elem.setAttribute('src', src); + } + }; + + var rafBatch = (function(){ + var isRunning; + var batch = []; + var runBatch = function(){ + while(batch.length){ + (batch.shift())(); + } + isRunning = false; + }; + return function(fn){ + batch.push(fn); + if(!isRunning){ + isRunning = true; + rAF(runBatch); + } + }; + })(); + + var unveilElement = function (elem, width){ + var sources, i, len, sourceSrcset, src, srcset, parent, isPicture, event, firesLoad, customMedia; + + var isImg = regImg.test(elem.nodeName); + + //allow using sizes="auto", but don't use. it's invalid. Use data-sizes="auto" or a valid value for sizes instead (i.e.: sizes="80vw") + var sizes = elem.getAttribute(lazySizesConfig.sizesAttr) || elem.getAttribute('sizes'); + var isAuto = sizes == 'auto'; + + if( (isAuto || !isCompleted) && isImg && (elem.src || elem.srcset) && !elem.complete && !hasClass(elem, lazySizesConfig.errorClass)){return;} + + elem._lazyRace = true; + isLoading++; + + rafBatch(function lazyUnveil(){ + + if(elem._lazyRace){ + delete elem._lazyRace; + } + + removeClass(elem, lazySizesConfig.lazyClass); + + if(!(event = triggerEvent(elem, 'lazybeforeunveil')).defaultPrevented){ + + if(sizes){ + if(isAuto){ + autoSizer.updateElem(elem, true, width); + addClass(elem, lazySizesConfig.autosizesClass); + } else { + elem.setAttribute('sizes', sizes); + } + } + + srcset = elem.getAttribute(lazySizesConfig.srcsetAttr); + src = elem.getAttribute(lazySizesConfig.srcAttr); + + if(isImg) { + parent = elem.parentNode; + isPicture = parent && regPicture.test(parent.nodeName || ''); + } + + firesLoad = event.detail.firesLoad || (('src' in elem) && (srcset || src || isPicture)); + + event = {target: elem}; + + if(firesLoad){ + addRemoveLoadEvents(elem, resetPreloading, true); + clearTimeout(resetPreloadingTimer); + resetPreloadingTimer = setTimeout(resetPreloading, 2500); + + addClass(elem, lazySizesConfig.loadingClass); + addRemoveLoadEvents(elem, switchLoadingClass, true); + } + + if(isPicture){ + sources = parent.getElementsByTagName('source'); + for(i = 0, len = sources.length; i < len; i++){ + if( (customMedia = lazySizesConfig.customMedia[sources[i].getAttribute('data-media') || sources[i].getAttribute('media')]) ){ + sources[i].setAttribute('media', customMedia); + } + sourceSrcset = sources[i].getAttribute(lazySizesConfig.srcsetAttr); + if(sourceSrcset){ + sources[i].setAttribute('srcset', sourceSrcset); + } + } + } + + if(srcset){ + elem.setAttribute('srcset', srcset); + } else if(src){ + if(regIframe.test(elem.nodeName)){ + changeIframeSrc(elem, src); + } else { + elem.setAttribute('src', src); + } + } + + if(srcset || isPicture){ + updatePolyfill(elem, {src: src}); + } + } + + if( !firesLoad || elem.complete ){ + if(firesLoad){ + resetPreloading(event); + } else { + isLoading--; + } + switchLoadingClass(event); + } + }); + }; + + var onload = function(){ + var scrollTimer; + var afterScroll = function(){ + lazySizesConfig.loadMode = 3; + throttledCheckElements(); + }; + + isCompleted = true; + lowRuns += 8; + + lazySizesConfig.loadMode = 3; + + addEventListener('scroll', function(){ + if(lazySizesConfig.loadMode == 3){ + lazySizesConfig.loadMode = 2; + } + clearTimeout(scrollTimer); + scrollTimer = setTimeout(afterScroll, 99); + }, true); + }; + + return { + _: function(){ + + lazyloadElems = document.getElementsByClassName(lazySizesConfig.lazyClass); + preloadElems = document.getElementsByClassName(lazySizesConfig.lazyClass + ' ' + lazySizesConfig.preloadClass); + + defaultExpand = lazySizesConfig.expand; + preloadExpand = Math.round(defaultExpand * lazySizesConfig.expFactor); + + addEventListener('scroll', throttledCheckElements, true); + + addEventListener('resize', throttledCheckElements, true); + + if(window.MutationObserver){ + new MutationObserver( throttledCheckElements ).observe( docElem, {childList: true, subtree: true, attributes: true} ); + } else { + docElem.addEventListener('DOMNodeInserted', throttledCheckElements, true); + docElem.addEventListener('DOMAttrModified', throttledCheckElements, true); + setInterval(throttledCheckElements, 999); + } + + addEventListener('hashchange', throttledCheckElements, true); + + //, 'fullscreenchange' + ['focus', 'mouseover', 'click', 'load', 'transitionend', 'animationend', 'webkitAnimationEnd'].forEach(function(name){ + document.addEventListener(name, throttledCheckElements, true); + }); + + if(!(isCompleted = /d$|^c/.test(document.readyState))){ + addEventListener('load', onload); + document.addEventListener('DOMContentLoaded', throttledCheckElements); + } else { + onload(); + } + + throttledCheckElements(); + }, + checkElems: throttledCheckElements, + unveil: unveilElement + }; + })(); + + + var autoSizer = (function(){ + var autosizesElems; + + var sizeElement = function (elem, dataAttr, width){ + var sources, i, len, event; + var parent = elem.parentNode; + + if(parent){ + width = getWidth(elem, parent, width); + event = triggerEvent(elem, 'lazybeforesizes', {width: width, dataAttr: !!dataAttr}); + + if(!event.defaultPrevented){ + width = event.detail.width; + + if(width && width !== elem._lazysizesWidth){ + elem._lazysizesWidth = width; + width += 'px'; + elem.setAttribute('sizes', width); + + if(regPicture.test(parent.nodeName || '')){ + sources = parent.getElementsByTagName('source'); + for(i = 0, len = sources.length; i < len; i++){ + sources[i].setAttribute('sizes', width); + } + } + + if(!event.detail.dataAttr){ + updatePolyfill(elem, event.detail); + } + } + } + } + }; + + var updateElementsSizes = function(){ + var i; + var len = autosizesElems.length; + if(len){ + i = 0; + + for(; i < len; i++){ + sizeElement(autosizesElems[i]); + } + } + }; + + var throttledUpdateElementsSizes = throttle(updateElementsSizes); + + return { + _: function(){ + autosizesElems = document.getElementsByClassName(lazySizesConfig.autosizesClass); + addEventListener('resize', throttledUpdateElementsSizes); + }, + checkElems: throttledUpdateElementsSizes, + updateElem: sizeElement + }; + })(); + + var init = function(){ + if(!init.i){ + init.i = true; + autoSizer._(); + loader._(); + } + }; + + (function(){ + var prop; + var lazySizesDefaults = { + lazyClass: 'lazyload', + loadedClass: 'lazyloaded', + loadingClass: 'lazyloading', + preloadClass: 'lazypreload', + errorClass: 'lazyerror', + autosizesClass: 'lazyautosizes', + srcAttr: 'data-src', + srcsetAttr: 'data-srcset', + sizesAttr: 'data-sizes', + //preloadAfterLoad: false, + minSize: 40, + customMedia: {}, + init: true, + expFactor: 2, + expand: 359, + loadMode: 2, + throttle: 125 + }; + + lazySizesConfig = window.lazySizesConfig || window.lazysizesConfig || {}; + + for(prop in lazySizesDefaults){ + if(!(prop in lazySizesConfig)){ + lazySizesConfig[prop] = lazySizesDefaults[prop]; + } + } + + window.lazySizesConfig = lazySizesConfig; + + setTimeout(function(){ + if(lazySizesConfig.init){ + init(); + } + }); + })(); + + return { + cfg: lazySizesConfig, + autoSizer: autoSizer, + loader: loader, + init: init, + uP: updatePolyfill, + aC: addClass, + rC: removeClass, + hC: hasClass, + fire: triggerEvent, + gW: getWidth + }; +})); diff --git a/frontend/drupal/sites/all/modules/picture/lazysizes/lazysizes.min.js b/frontend/drupal/sites/all/modules/picture/lazysizes/lazysizes.min.js new file mode 100644 index 000000000..ff1926d1b --- /dev/null +++ b/frontend/drupal/sites/all/modules/picture/lazysizes/lazysizes.min.js @@ -0,0 +1,2 @@ +/*! lazysizes - v1.1.3-RC1 - Licensed MIT */ +!function(a,b){var c=b(a,a.document);a.lazySizes=c,"object"==typeof module&&module.exports?module.exports=c:"function"==typeof define&&define.amd&&define(c)}(window,function(a,b){"use strict";if(b.getElementsByClassName){var c,d=b.documentElement,e=a.addEventListener,f=a.setTimeout,g=a.requestAnimationFrame||f,h=/^picture$/i,i=["load","error","lazyincluded","_lazyloaded"],j=function(a,b){var c=new RegExp("(\\s|^)"+b+"(\\s|$)");return a.className.match(c)&&c},k=function(a,b){j(a,b)||(a.className+=" "+b)},l=function(a,b){var c;(c=j(a,b))&&(a.className=a.className.replace(c," "))},m=function(a,b,c){var d=c?"addEventListener":"removeEventListener";c&&m(a,b),i.forEach(function(c){a[d](c,b)})},n=function(a,c,d,e,f){var g=b.createEvent("CustomEvent");return g.initCustomEvent(c,!e,!f,d||{}),g.details=g.detail,a.dispatchEvent(g),g},o=function(b,d){var e;a.HTMLPictureElement||((e=a.picturefill||a.respimage||c.pf)?e({reevaluate:!0,elements:[b]}):d&&d.src&&(b.src=d.src))},p=function(a,b){return getComputedStyle(a,null)[b]},q=function(a,b,d){for(d=d||a.offsetWidth;da&&(a=9),f(k,a)}}},s=function(){var i,q,s,u,v,w,x,y,z,A,B,C,D,E=/^img$/i,F=/^iframe$/i,G="onscroll"in a&&!/glebot/.test(navigator.userAgent),H=0,I=0,J=0,K=1,L=function(a){J--,a&&a.target&&m(a.target,L),(!a||0>J||!a.target)&&(J=0)},M=function(a,b){var c,d=a,e="hidden"!=p(a,"visibility");for(y-=b,B+=b,z-=b,A+=b;e&&(d=d.offsetParent);)e=(p(d,"opacity")||1)>0,e&&"visible"!=p(d,"overflow")&&(c=d.getBoundingClientRect(),e=A>c.left&&zc.top-1&&yJ&&(a=i.length)){for(b=0,K++,D>I&&1>J&&K>3&&v>2?(I=D,K=0):I=I!=C&&v>1&&K>2&&6>J?C:H;a>b;b++)i[b]&&!i[b]._lazyRace&&(G?((j=i[b].getAttribute("data-expand"))&&(g=1*j)||(g=I),k!==g&&(w=innerWidth+g,x=innerHeight+g,h=-1*g,k=g),d=i[b].getBoundingClientRect(),(B=d.bottom)>=h&&(y=d.top)<=x&&(A=d.right)>=h&&(z=d.left)<=w&&(B||A||z||y)&&(s&&3>J&&!j&&(3>v||4>K)||M(i[b],g))?(S(i[b],d.width),f=!0):!f&&s&&!e&&3>J&&4>K&&v>2&&(q[0]||c.preloadAfterLoad)&&(q[0]||!j&&(B||A||z||y||"auto"!=i[b].getAttribute(c.sizesAttr)))&&(e=q[0]||i[b])):S(i[b]));e&&!f&&S(e)}},O=r(N),P=function(a){k(a.target,c.loadedClass),l(a.target,c.loadingClass),m(a.target,P)},Q=function(a,b){try{a.contentWindow.location.replace(b)}catch(c){a.setAttribute("src",b)}},R=function(){var a,b=[],c=function(){for(;b.length;)b.shift()();a=!1};return function(d){b.push(d),a||(a=!0,g(c))}}(),S=function(a,b){var d,e,g,i,p,q,r,v,w,x,y,z=E.test(a.nodeName),A=a.getAttribute(c.sizesAttr)||a.getAttribute("sizes"),B="auto"==A;(!B&&s||!z||!a.src&&!a.srcset||a.complete||j(a,c.errorClass))&&(a._lazyRace=!0,J++,R(function(){if(a._lazyRace&&delete a._lazyRace,l(a,c.lazyClass),!(w=n(a,"lazybeforeunveil")).defaultPrevented){if(A&&(B?(t.updateElem(a,!0,b),k(a,c.autosizesClass)):a.setAttribute("sizes",A)),q=a.getAttribute(c.srcsetAttr),p=a.getAttribute(c.srcAttr),z&&(r=a.parentNode,v=r&&h.test(r.nodeName||"")),x=w.detail.firesLoad||"src"in a&&(q||p||v),w={target:a},x&&(m(a,L,!0),clearTimeout(u),u=f(L,2500),k(a,c.loadingClass),m(a,P,!0)),v)for(d=r.getElementsByTagName("source"),e=0,g=d.length;g>e;e++)(y=c.customMedia[d[e].getAttribute("data-media")||d[e].getAttribute("media")])&&d[e].setAttribute("media",y),i=d[e].getAttribute(c.srcsetAttr),i&&d[e].setAttribute("srcset",i);q?a.setAttribute("srcset",q):p&&(F.test(a.nodeName)?Q(a,p):a.setAttribute("src",p)),(q||v)&&o(a,{src:p})}(!x||a.complete)&&(x?L(w):J--,P(w))}))},T=function(){var a,b=function(){c.loadMode=3,O()};s=!0,K+=8,c.loadMode=3,e("scroll",function(){3==c.loadMode&&(c.loadMode=2),clearTimeout(a),a=f(b,99)},!0)};return{_:function(){i=b.getElementsByClassName(c.lazyClass),q=b.getElementsByClassName(c.lazyClass+" "+c.preloadClass),C=c.expand,D=Math.round(C*c.expFactor),e("scroll",O,!0),e("resize",O,!0),a.MutationObserver?new MutationObserver(O).observe(d,{childList:!0,subtree:!0,attributes:!0}):(d.addEventListener("DOMNodeInserted",O,!0),d.addEventListener("DOMAttrModified",O,!0),setInterval(O,999)),e("hashchange",O,!0),["focus","mouseover","click","load","transitionend","animationend","webkitAnimationEnd"].forEach(function(a){b.addEventListener(a,O,!0)}),(s=/d$|^c/.test(b.readyState))?T():(e("load",T),b.addEventListener("DOMContentLoaded",O)),O()},checkElems:O,unveil:S}}(),t=function(){var a,d=function(a,b,c){var d,e,f,g,i=a.parentNode;if(i&&(c=q(a,i,c),g=n(a,"lazybeforesizes",{width:c,dataAttr:!!b}),!g.defaultPrevented&&(c=g.detail.width,c&&c!==a._lazysizesWidth))){if(a._lazysizesWidth=c,c+="px",a.setAttribute("sizes",c),h.test(i.nodeName||""))for(d=i.getElementsByTagName("source"),e=0,f=d.length;f>e;e++)d[e].setAttribute("sizes",c);g.detail.dataAttr||o(a,g.detail)}},f=function(){var b,c=a.length;if(c)for(b=0;c>b;b++)d(a[b])},g=r(f);return{_:function(){a=b.getElementsByClassName(c.autosizesClass),e("resize",g)},checkElems:g,updateElem:d}}(),u=function(){u.i||(u.i=!0,t._(),s._())};return function(){var b,d={lazyClass:"lazyload",loadedClass:"lazyloaded",loadingClass:"lazyloading",preloadClass:"lazypreload",errorClass:"lazyerror",autosizesClass:"lazyautosizes",srcAttr:"data-src",srcsetAttr:"data-srcset",sizesAttr:"data-sizes",minSize:40,customMedia:{},init:!0,expFactor:2,expand:359,loadMode:2,throttle:125};c=a.lazySizesConfig||a.lazysizesConfig||{};for(b in d)b in c||(c[b]=d[b]);a.lazySizesConfig=c,f(function(){c.init&&u()})}(),{cfg:c,autoSizer:t,loader:s,init:u,uP:o,aC:k,rC:l,hC:j,fire:n,gW:q}}}); \ No newline at end of file diff --git a/frontend/drupal/sites/all/modules/picture/lazysizes/plugins/aspectratio/ls.aspectratio.css b/frontend/drupal/sites/all/modules/picture/lazysizes/plugins/aspectratio/ls.aspectratio.css new file mode 100644 index 000000000..8435eed96 --- /dev/null +++ b/frontend/drupal/sites/all/modules/picture/lazysizes/plugins/aspectratio/ls.aspectratio.css @@ -0,0 +1,3 @@ +picture img { + width: 100%; +} \ No newline at end of file diff --git a/frontend/drupal/sites/all/modules/picture/lazysizes/plugins/aspectratio/ls.aspectratio.js b/frontend/drupal/sites/all/modules/picture/lazysizes/plugins/aspectratio/ls.aspectratio.js new file mode 100644 index 000000000..9c754ce6f --- /dev/null +++ b/frontend/drupal/sites/all/modules/picture/lazysizes/plugins/aspectratio/ls.aspectratio.js @@ -0,0 +1,204 @@ +(function(window, document){ + 'use strict'; + + if(!window.addEventListener){return;} + + var imageRatio, extend$, $; + + var regPicture = /^picture$/i; + var aspectRatioAttr = 'data-aspectratio'; + var aspectRatioSel = 'img[' + aspectRatioAttr + ']'; + + var matchesMedia = function(media){ + if(window.matchMedia){ + matchesMedia = function(media){ + return !media || (matchMedia(media) || {}).matches; + }; + } else if(window.Modernizr && Modernizr.mq){ + return !media || Modernizr.mq(media); + } else { + return !media; + } + return matchesMedia(media); + }; + + var addClass = function(elem, className){ + if($){ + $(elem).addClass(className); + } else if(window.lazySizes){ + lazySizes.aC(elem, className); + } else { + elem.classList.add(className); + } + }; + + var removeClass = function(elem, className){ + if($){ + $(elem).removeClass(className); + } else if(window.lazySizes){ + lazySizes.rC(elem, className); + } else { + elem.classList.remove(className); + } + }; + + function AspectRatio(){ + this.ratioElems = document.getElementsByClassName('lazyaspectratio'); + this._setupEvents(); + this.processImages(); + } + + AspectRatio.prototype = { + _setupEvents: function(){ + var module = this; + + var addRemoveAspectRatio = function(elem){ + if(elem.naturalWidth < 36){ + module.addAspectRatio(elem, true); + } else { + module.removeAspectRatio(elem, true); + } + }; + var onload = function(){ + module.processImages(); + }; + + document.addEventListener('load', function(e){ + if(e.target.getAttribute && e.target.getAttribute(aspectRatioAttr)){ + addRemoveAspectRatio(e.target); + } + }, true); + + addEventListener('resize', (function(){ + var timer; + var resize = function(){ + var i, len; + for(i = 0, len = module.ratioElems.length; i < len; i++){ + addRemoveAspectRatio(module.ratioElems[i]); + } + }; + + return function(){ + clearTimeout(timer); + timer = setTimeout(resize, 33); + }; + })()); + + document.addEventListener('DOMContentLoaded', onload); + + addEventListener('load', onload); + }, + processImages: function(context){ + var elements, i; + + if(!context){ + context = document; + } + + if('length' in context && !context.nodeName){ + elements = context; + } else { + elements = context.querySelectorAll(aspectRatioSel); + } + + for(i = 0; i < elements.length; i++){ + if(elements[i].naturalWidth > 36){ + this.removeAspectRatio(elements[i]); + continue; + } + this.addAspectRatio(elements[i]); + } + }, + getSelectedRatio: function(img){ + var i, len, sources, customMedia, ratio; + var parent = img.parentNode; + if(parent && regPicture.test(parent.nodeName || '')){ + sources = parent.getElementsByTagName('source'); + + for(i = 0, len = sources.length; i < len; i++){ + customMedia = sources[i].getAttribute('data-media') || sources[i].getAttribute('media'); + + if(window.lazySizesConfig && lazySizesConfig.customMedia[customMedia]){ + customMedia = lazySizesConfig.customMedia[customMedia]; + } + + if(matchesMedia(customMedia)){ + ratio = sources[i].getAttribute(aspectRatioAttr); + break; + } + } + } + + return ratio || img.getAttribute(aspectRatioAttr) || ''; + }, + parseRatio: (function(){ + var regRatio = /^\s*([+\d\.]+)(\s*[\/x]\s*([+\d\.]+))?\s*$/; + var ratioCache = {}; + return function(ratio){ + + if(!ratioCache[ratio] && ratio.match(regRatio)){ + if(RegExp.$3){ + ratioCache[ratio] = RegExp.$1 / RegExp.$3; + } else { + ratioCache[ratio] = RegExp.$1 * 1; + } + } + + return ratioCache[ratio]; + }; + })(), + addAspectRatio: function(img, notNew){ + var ratio; + var width = img.offsetWidth; + + if(!notNew){ + addClass(img, 'lazyaspectratio'); + } + + if(width < 36){ + if(width && window.console){ + console.log('Define width of image, so we can calculate the height'); + } + return; + } + + ratio = this.getSelectedRatio(img); + ratio = this.parseRatio(ratio); + + if(ratio){ + img.style.height = (width / ratio) + 'px'; + } + }, + removeAspectRatio: function(img){ + removeClass(img, 'lazyaspectratio'); + img.style.height = ''; + img.removeAttribute(aspectRatioAttr); + } + }; + + extend$ = function(){ + $ = window.jQuery || window.Zepto || window.shoestring || window.$; + if($ && $.fn && !$.fn.imageRatio && $.fn.filter && $.fn.add && $.fn.find){ + $.fn.imageRatio = function(){ + imageRatio.processImages(this.find(aspectRatioSel).add(this.filter(aspectRatioSel))); + return this; + }; + } else { + $ = false; + } + }; + + extend$(); + setTimeout(extend$); + + imageRatio = new AspectRatio(); + + window.imageRatio = imageRatio; + + if(typeof module == 'object' && module.exports){ + module.exports = imageRatio; + } else if (typeof define == 'function' && define.amd) { + define(imageRatio); + } + +})(window, document); diff --git a/frontend/drupal/sites/all/modules/picture/lazysizes/plugins/aspectratio/ls.aspectratio.min.js b/frontend/drupal/sites/all/modules/picture/lazysizes/plugins/aspectratio/ls.aspectratio.min.js new file mode 100644 index 000000000..1f619cfed --- /dev/null +++ b/frontend/drupal/sites/all/modules/picture/lazysizes/plugins/aspectratio/ls.aspectratio.min.js @@ -0,0 +1,2 @@ +/*! lazysizes - v1.1.3-RC1 - Licensed MIT */ +!function(a,b){"use strict";function c(){this.ratioElems=b.getElementsByClassName("lazyaspectratio"),this._setupEvents(),this.processImages()}if(a.addEventListener){var d,e,f,g=/^picture$/i,h="data-aspectratio",i="img["+h+"]",j=function(b){return a.matchMedia?(j=function(a){return!a||(matchMedia(a)||{}).matches})(b):a.Modernizr&&Modernizr.mq?!b||Modernizr.mq(b):!b},k=function(b,c){f?f(b).addClass(c):a.lazySizes?lazySizes.aC(b,c):b.classList.add(c)},l=function(b,c){f?f(b).removeClass(c):a.lazySizes?lazySizes.rC(b,c):b.classList.remove(c)};c.prototype={_setupEvents:function(){var a=this,c=function(b){b.naturalWidth<36?a.addAspectRatio(b,!0):a.removeAspectRatio(b,!0)},d=function(){a.processImages()};b.addEventListener("load",function(a){a.target.getAttribute&&a.target.getAttribute(h)&&c(a.target)},!0),addEventListener("resize",function(){var b,d=function(){var b,d;for(b=0,d=a.ratioElems.length;d>b;b++)c(a.ratioElems[b])};return function(){clearTimeout(b),b=setTimeout(d,33)}}()),b.addEventListener("DOMContentLoaded",d),addEventListener("load",d)},processImages:function(a){var c,d;a||(a=b),c="length"in a&&!a.nodeName?a:a.querySelectorAll(i);for(d=0;d36?this.removeAspectRatio(c[d]):this.addAspectRatio(c[d])},getSelectedRatio:function(b){var c,d,e,f,i,k=b.parentNode;if(k&&g.test(k.nodeName||""))for(e=k.getElementsByTagName("source"),c=0,d=e.length;d>c;c++)if(f=e[c].getAttribute("data-media")||e[c].getAttribute("media"),a.lazySizesConfig&&lazySizesConfig.customMedia[f]&&(f=lazySizesConfig.customMedia[f]),j(f)){i=e[c].getAttribute(h);break}return i||b.getAttribute(h)||""},parseRatio:function(){var a=/^\s*([+\d\.]+)(\s*[\/x]\s*([+\d\.]+))?\s*$/,b={};return function(c){return!b[c]&&c.match(a)&&(RegExp.$3?b[c]=RegExp.$1/RegExp.$3:b[c]=1*RegExp.$1),b[c]}}(),addAspectRatio:function(b,c){var d,e=b.offsetWidth;return c||k(b,"lazyaspectratio"),36>e?void(e&&a.console&&console.log("Define width of image, so we can calculate the height")):(d=this.getSelectedRatio(b),d=this.parseRatio(d),void(d&&(b.style.height=e/d+"px")))},removeAspectRatio:function(a){l(a,"lazyaspectratio"),a.style.height="",a.removeAttribute(h)}},e=function(){f=a.jQuery||a.Zepto||a.shoestring||a.$,f&&f.fn&&!f.fn.imageRatio&&f.fn.filter&&f.fn.add&&f.fn.find?f.fn.imageRatio=function(){return d.processImages(this.find(i).add(this.filter(i))),this}:f=!1},e(),setTimeout(e),d=new c,a.imageRatio=d,"object"==typeof module&&module.exports?module.exports=d:"function"==typeof define&&define.amd&&define(d)}}(window,document); \ No newline at end of file diff --git a/frontend/drupal/sites/all/modules/picture/picture.admin.inc b/frontend/drupal/sites/all/modules/picture/picture.admin.inc new file mode 100644 index 000000000..c5c4749e9 --- /dev/null +++ b/frontend/drupal/sites/all/modules/picture/picture.admin.inc @@ -0,0 +1,431 @@ +machine_name] = $group->name; + } + if ($options) { + $form['breakpoint_group'] = array( + '#title' => t('Breakpoint group'), + '#type' => 'select', + '#required' => TRUE, + '#description' => t('Select a breakpoint group.'), + '#options' => $options, + '#default_value' => $form_state['item']->getBreakpointGroup() ? $form_state['item']->getBreakpointGroup()->machine_name : NULL, + ); + } + else { + $form['breakpoint_group'] = array( + '#title' => t('Breakpoint group'), + '#type' => 'item', + '#markup' => t( + 'There are no breakpoint groups. !create_link.', + array('!create_link' => l(t('Create a breakpoint group'), 'admin/config/media/breakpoints/groups/add')) + ), + ); + } + + return $form; +} + +/** + * Validate breakpoint group. + */ +function picture_select_breakpoint_group_form_validate($form, &$form_state) { + if (!isset($form_state['values']['breakpoint_group']) || empty($form_state['values']['breakpoint_group'])) { + form_error($form['breakpoint_group'], t('No breakpoint group was selected.')); + } +} + +/** + * Submit callback. + */ +function picture_select_breakpoint_group_form_submit($form, &$form_state) { + $form_state['item']->setBreakpointGroup($form_state['values']['breakpoint_group']); +} + +/** + * Build picture mapping form. + */ +function picture_mapping_form(&$form, &$form_state) { + $picture_mapping = $form_state['item']; + $form['info']['label']['#required'] = TRUE; + $form['info']['machine_name']['#title'] = t('Machine name'); + $groups = breakpoints_breakpoint_group_load_all(); + $options = array(); + foreach ($groups as $group) { + $options[$group->machine_name] = $group->name; + } + $form['breakpoint_group'] = array( + '#title' => t('Breakpoint group'), + '#type' => 'select', + '#required' => TRUE, + '#description' => t('Select a breakpoint group.'), + '#options' => $options, + '#default_value' => $picture_mapping->getBreakpointGroup()->machine_name, + '#ajax' => array( + 'callback' => 'picture_mapping_form_change_breakpoint_group', + 'wrapper' => 'picture-mapping-container', + 'method' => 'replace', + 'effect' => 'fade', + ), + ); + + $form['mapping'] = array( + '#type' => 'container', + '#attributes' => array( + 'id' => 'picture-mapping-container', + ), + '#tree' => TRUE, + ); + + $image_styles = image_style_options(TRUE) + array( + PICTURE_EMPTY_IMAGE => t('Empty image'), + PICTURE_ORIGINAL_IMAGE => t('Original image'), + ); + foreach ($picture_mapping->getMappings() as $breakpoint_id => $mapping) { + foreach ($mapping as $multiplier => $mapping_definition) { + $breakpoint = breakpoints_breakpoint_load_by_fullkey($breakpoint_id); + $label = $multiplier . ' ' . $breakpoint->name . ' [' . $breakpoint->breakpoint . ']'; + $form['mapping'][$breakpoint_id][$multiplier] = array( + '#type' => 'fieldset', + '#title' => check_plain($label), + ); + $form['mapping'][$breakpoint_id][$multiplier]['mapping_type'] = array( + '#title' => t('Type'), + '#type' => 'radios', + '#options' => array( + '_none' => t('Do not use this breakpoint'), + 'image_style' => t('Use image styles'), + 'sizes' => t('Use the sizes attribute'), + ), + '#default_value' => isset($mapping_definition['mapping_type']) ? $mapping_definition['mapping_type'] : '_none', + ); + $form['mapping'][$breakpoint_id][$multiplier]['image_style'] = array( + '#type' => 'select', + '#title' => t('Image style'), + '#options' => $image_styles, + '#maxlength' => 1024, + '#default_value' => isset($mapping_definition['image_style']) ? $mapping_definition['image_style'] : '', + '#description' => t('Select an image style for this breakpoint.'), + '#states' => array( + 'visible' => array( + ':input[name="mapping[' . $breakpoint_id . '][' . $multiplier . '][mapping_type]"]' => array('value' => 'image_style'), + ), + ), + ); + $form['mapping'][$breakpoint_id][$multiplier]['sizes'] = array( + '#type' => 'textfield', + '#title' => t('Sizes'), + '#default_value' => isset($mapping_definition['sizes']) ? $mapping_definition['sizes'] : '', + '#description' => t('Enter the value for the sizes attribute (e.g. "(min-width:700px) 700px, 100vw").'), + '#states' => array( + 'visible' => array( + ':input[name="mapping[' . $breakpoint_id . '][' . $multiplier . '][mapping_type]"]' => array('value' => 'sizes'), + ), + ), + ); + $form['mapping'][$breakpoint_id][$multiplier]['sizes_image_styles'] = array( + '#title' => t('Image styles'), + '#type' => 'checkboxes', + '#options' => array_diff_key($image_styles, array('' => '')), + '#default_value' => isset($mapping_definition['sizes_image_styles']) ? $mapping_definition['sizes_image_styles'] : array(), + '#states' => array( + 'visible' => array( + ':input[name="mapping[' . $breakpoint_id . '][' . $multiplier . '][mapping_type]"]' => array('value' => 'sizes'), + ), + ), + ); + + } + } + $form['#tree'] = FALSE; + return $form; +} + +/** + * Helper function for the form. + */ +function picture_mapping_form_change_breakpoint_group($form, &$form_state) { + return $form['mapping']; +} + +/** + * Validate callback. + */ +function picture_mapping_form_validate($form, &$form_state) { + $picture_mapping = $form_state['item']; + if ($form_state['triggering_element']['#type'] != 'submit') { + $picture_mapping->setBreakpointGroup($form_state['values']['breakpoint_group']); + $form_state['rebuild'] = TRUE; + return; + } + $picture_mapping->setLabel($form_state['values']['label']); + $picture_mapping->setMachineName($form_state['values']['machine_name']); + $picture_mapping->setMappings($form_state['values']['mapping']); + // Make sure at least one mapping is defined. + if (!$picture_mapping->hasMappings()) { + form_error($form['mapping'], t('Please select at least one mapping.')); + } +} + +/** + * Chooses which picture groups are available in the CKEditor image dialog. + */ +function picture_admin_settings() { + $form = array(); + $picture_mappings = picture_get_mapping_options(); + + // Check if picture mappings have been configured before proceeding. + if ($picture_mappings) { + // Create a settings form. + $form['description'] = array( + '#type' => 'item', + '#title' => t('Choose which picture mappings will be available in the CKEditor image dialog.'), + '#description' => t('See picture_wysiwyg.css for an example of how to style these images in your theme using the selectors suggested below.'), + ); + + // Retrieve pre-existing settings. + $ckeditor_mappings = variable_get('picture_ckeditor_mappings', array()); + + // Loop through each picture mapping and place a checkbox and weight. + foreach ($picture_mappings as $machine_name => $display_name) { + $form[$machine_name] = array( + '#type' => 'fieldset', + '#title' => t('@name picture mapping', array('@name' => $display_name)), + ); + $form[$machine_name]['enabled'] = array( + '#type' => 'checkbox', + '#default_value' => isset($ckeditor_mappings[$machine_name]) ? $ckeditor_mappings[$machine_name]['enabled'] : 0, + '#title' => t('Include @name picture mapping in the CKEditor image dialog', array('@name' => $display_name)), + ); + $form[$machine_name]['css'] = array( + '#type' => 'item', + '#markup' => t('Consider using the selector span[data-picture-mapping="@machine_name"] in your theme CSS.', array( + '@machine_name' => $machine_name, + )), + ); + $form[$machine_name]['weight'] = array( + '#type' => 'select', + '#title' => t('Weight'), + '#options' => drupal_map_assoc(array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)), + '#default_value' => isset($ckeditor_mappings[$machine_name]) ? $ckeditor_mappings[$machine_name]['weight'] : 1, + '#description' => t('Control the sort order of picture mappings in the CKEditor "size" drop-down. Higher weights sink to the bottom of the list.'), + ); + $form[$machine_name]['fallback'] = array( + '#type' => 'select', + '#title' => t('Fallback image style'), + '#options' => drupal_map_assoc(array_keys(image_styles())) + array( + PICTURE_EMPTY_IMAGE => t('Empty image'), + ), + '#default_value' => isset($ckeditor_mappings[$machine_name]) ? $ckeditor_mappings[$machine_name]['fallback'] : NULL, + ); + $form[$machine_name]['lazyload'] = array( + '#title' => t('Picture lazyload'), + '#description' => t('Image will be rendered when it appears in viewport, helps to optimize page load speed.'), + '#type' => 'checkbox', + '#default_value' => isset($ckeditor_mappings[$machine_name]['lazyload']) ? $ckeditor_mappings[$machine_name]['lazyload'] : 0, + ); + + $form[$machine_name]['lazyload_aspect_ratio'] = array( + '#title' => t('Keep aspect ratio'), + '#type' => 'checkbox', + '#description' => t('Preserve the space for the image being lazyloaded to avoid layout reflows.
Image ratio is defined per breakpoint, make sure all images from srcset have the same ratio.
Output example: !example', + array('!example' => htmlentities('')) + ), + '#default_value' => isset($ckeditor_mappings[$machine_name]['lazyload_aspect_ratio']) ? $ckeditor_mappings[$machine_name]['lazyload_aspect_ratio'] : 0, + '#states' => array( + 'visible' => array( + ':input[name="article_responsive_image[lazyload]"]' => array('checked' => TRUE), + ':input[name="article_responsive_image[fallback]"]' => array('value' => PICTURE_EMPTY_IMAGE), + ), + ), + ); + } + $form['#tree'] = TRUE; + $form['ckeditor_label'] = array( + '#type' => 'textfield', + '#title' => t('Label in the CKEditor image dialog'), + '#description' => t('This sets the label for the drop-down select box containing these picture mappings in the CKEditor image dialog'), + '#default_value' => variable_get('picture_ckeditor_label', 'Image size (required)'), + ); + } + else { + $form['info'] = array( + '#markup' => t('No picture mappings found. !create', array('!create' => l(t('Create one now.'), 'admin/config/media/picture/add'))), + ); + } + if (module_exists('file_entity') && function_exists('file_type_load_all') && !variable_get('picture_updated_to_file_entity_2', FALSE)) { + $form['picture_updated_to_file_entity_2'] = array( + '#type' => 'checkbox', + '#title' => t('Update the file formatter to version 2 of the File Entity module'), + '#default_value' => variable_get('picture_updated_to_file_entity_2', FALSE), + ); + } + + $form['picture_js_scope'] = array( + '#title' => t('Picture JavaScript scope'), + '#description' => t("For performance reasons, JS should be loaded in the footer. However, you can move the Picture module's JS to the header so that it loads before JS from other modules like Views Slideshow."), + '#type' => 'radios', + '#options' => array( + 'header' => t('Header'), + 'footer' => t('Footer'), + ), + '#default_value' => variable_get('picture_js_scope', 'footer'), + ); + + $form['picture_polyfill_version'] = array( + '#title' => t('Picture polyfill to use'), + '#description' => t('For performance reasons, use the minified version.'), + '#type' => 'radios', + '#options' => array( + 'min' => t('Minified polyfill'), + 'dev' => t('Development polyfill'), + 'own' => t('Use custom polyfill'), + ), + '#default_value' => variable_get('picture_polyfill_version', 'min'), + ); + + $form['picture_fallback_method'] = array( + '#title' => t('Picture fallback method'), + '#description' => t('Use the fallback method, the defualt is to use srcset since it avoids double downloads, but src is the right method and will also work if the browser does not support javascript.'), + '#type' => 'radios', + '#options' => array( + 'src' => t('src fallback'), + 'srcset' => t('srcset fallback'), + ), + '#default_value' => variable_get('picture_fallback_method', 'srcset'), + ); + + $form['picture_img_sizes_output_method'] = array( + '#title' => t('"Image with sizes" output method'), + '#description' => t('"srcset on Image tag" creates the correct markup, however "picture element" will prevent duplicate downloads.'), + '#type' => 'radios', + '#options' => array( + 'img_srcset' => t('srcset on Image tag'), + 'picture_element' => t('Picture element'), + ), + '#default_value' => variable_get('picture_img_sizes_output_method', 'picture_element'), + ); + + $form['submit'] = array( + '#type' => 'submit', + '#value' => 'Save', + ); + return $form; +} + +/** + * Validate handler for the picture_ckeditor_settings form. + * + * It checks that a fallback image style is selected for every + * picture mapping that has been enabled for the CKEditor image dialog. + */ +function picture_admin_settings_validate($form, &$form_state) { + $picture_mappings = picture_mapping_load_all(); + if ($picture_mappings) { + foreach ($picture_mappings as $picture_mapping) { + $machine_name = $picture_mapping->getMachineName(); + if ($form_state['values'][$machine_name]['enabled'] == 1) { + if (empty($form_state['values'][$machine_name]['fallback'])) { + form_set_error($machine_name . '][fallback', t('Please choose a fallback image style for this picture mapping')); + } + } + } + } +} + +/** + * Submit handler, places chosen picture groups into the variables table. + */ +function picture_admin_settings_submit($form, &$form_state) { + $picture_mappings = picture_mapping_load_all(); + $ckeditor_mappings = array(); + if ($picture_mappings) { + // Loop each picture mapping and record the settings. + foreach ($picture_mappings as $picture_mapping) { + $machine_name = $picture_mapping->getMachineName(); + $ckeditor_mappings[$machine_name]['enabled'] = $form_state['values'][$machine_name]['enabled']; + $ckeditor_mappings[$machine_name]['weight'] = $form_state['values'][$machine_name]['weight']; + $ckeditor_mappings[$machine_name]['fallback'] = $form_state['values'][$machine_name]['fallback']; + $ckeditor_mappings[$machine_name]['lazyload'] = $form_state['values'][$machine_name]['lazyload']; + $ckeditor_mappings[$machine_name]['lazyload_aspect_ratio'] = $form_state['values'][$machine_name]['lazyload_aspect_ratio']; + } + + uasort($ckeditor_mappings, 'picture_compare_weights'); + variable_set('picture_ckeditor_mappings', $ckeditor_mappings); + variable_set('picture_ckeditor_label', $form_state['values']['ckeditor_label']); + } + + if (isset($form_state['values']['picture_updated_to_file_entity_2']) && $form_state['values']['picture_updated_to_file_entity_2']) { + variable_set('picture_updated_to_file_entity_2', _picture_update_to_file_entity_2()); + } + + variable_set('picture_js_scope', $form_state['values']['picture_js_scope']); + variable_set('picture_polyfill_version', $form_state['values']['picture_polyfill_version']); + variable_set('picture_fallback_method', $form_state['values']['picture_fallback_method']); + variable_set('picture_img_sizes_output_method', $form_state['values']['picture_img_sizes_output_method']); + drupal_set_message(t('Your settings have been saved')); +} + +/** + * Update picture module to version 2 of File Entity module. + */ +function _picture_update_to_file_entity_2() { + // File Entity is not installed or the installed version is not version 2. + if (!module_exists('file_entity') || !function_exists('file_type_load_all')) { + return FALSE; + } + // Picture was already updated. + if (variable_get('picture_updated_to_file_entity_2', FALSE)) { + return TRUE; + } + $entity_info = entity_get_info('file'); + $types = file_type_load_all(); + foreach ($types as $file_type) { + $view_modes = array('default' => array('label' => t('Default'))) + $entity_info['view modes']; + foreach (array_keys($view_modes) as $view_mode) { + $current_displays = file_displays_load($file_type->type, $view_mode, TRUE); + if (isset($current_displays['file_picture']) && $current_displays['file_picture']->status) { + if (!isset($current_displays['file_field_picture']) || !$current_displays['file_field_picture']->status) { + $display = file_display_new($file_type->type, $view_mode, 'file_field_picture'); + $display->settings = $current_displays['file_picture']->settings; + unset($display->settings['alt']); + unset($display->settings['title']); + $display->settings['image_link'] = ''; + $display->settings['colorbox'] = isset($display->settings['picture_mapping']) ? $display->settings['picture_mapping'] : $display->settings['picture_group']; + $display->settings['picture_mapping'] = isset($display->settings['picture_mapping']) ? $display->settings['picture_mapping'] : $display->settings['picture_group']; + unset($display->settings['picture_group']); + $display->status = 1; + file_display_save($display); + } + } + elseif (isset($current_displays['file_field_picture'])) { + $display = $current_displays['file_field_picture']; + $display->settings['picture_mapping'] = isset($display->settings['picture_mapping']) ? $display->settings['picture_mapping'] : $display->settings['picture_group']; + unset($display->settings['picture_group']); + file_display_save($display); + } + } + } + return TRUE; +} + +/** + * Sort picture groups for the CKEditor image dialog. + */ +function picture_compare_weights($a, $b) { + if ($a['weight'] == $b['weight']) { + return 0; + } + return ($a['weight'] < $b['weight']) ? -1 : 1; +} diff --git a/frontend/drupal/sites/all/modules/picture/picture.drush.inc b/frontend/drupal/sites/all/modules/picture/picture.drush.inc new file mode 100644 index 000000000..dc51505ed --- /dev/null +++ b/frontend/drupal/sites/all/modules/picture/picture.drush.inc @@ -0,0 +1,71 @@ + 'Flush all derived images for a given picture mapping.', + 'arguments' => array( + 'name' => 'A picture mapping machine name. If not provided, user may choose from a list of names.', + ), + 'examples' => array( + 'drush picture-mapping-flush' => 'Pick a picture mapping and then delete its image derivatives.', + 'drush picture-mapping-flush example_mapping' => 'Delete all image derivatives used by the example_mapping picture mapping.', + ), + 'aliases' => array('pmf'), + ); + + return $items; +} + +function drush_picture_mapping_flush($name = NULL) { + if (empty($name)) { + $choices = drupal_map_assoc(array_keys(picture_get_mapping_options())); + if ($choice = drush_choice($choices, dt("Choose a picture mapping to flush."))) { + $name = $choice; + } + else { + return; + } + } + + /** @var PictureMapping $mapping */ + $mapping = picture_mapping_load($name); + if (!$mapping) { + return drush_set_error(dt('Picture mapping !name not recognized.', array('!name' => $name))); + } + + $image_styles = array(); + foreach ($mapping->getMappings() as $breakpoint_name => $multipliers) { + $breakpoint = breakpoints_breakpoint_load_by_fullkey($breakpoint_name); + if ($breakpoint) { + foreach ($multipliers as $multiplier => $mapping_definition) { + switch ($mapping_definition['mapping_type']) { + case 'sizes': + $image_styles = array_merge($image_styles, array_filter($mapping_definition['sizes_image_styles'])); + break; + + case 'image_style': + $image_styles[] = $mapping_definition['image_style']; + break; + } + } + } + } + + if (!empty($image_styles)) { + $image_styles = array_unique($image_styles); + foreach ($image_styles as $image_style) { + drush_invoke('image-flush', array($image_style)); + } + } + else { + drush_log(dt('No image styles found for picture mapping !name', array('!name' => $name))); + } +} diff --git a/frontend/drupal/sites/all/modules/picture/picture.file_entity_1.inc b/frontend/drupal/sites/all/modules/picture/picture.file_entity_1.inc new file mode 100644 index 000000000..da136b8f9 --- /dev/null +++ b/frontend/drupal/sites/all/modules/picture/picture.file_entity_1.inc @@ -0,0 +1,198 @@ + t('Picture'), + 'default settings' => array( + 'picture_group' => '', + 'fallback_image_style' => '', + 'lazyload' => '', + 'lazyload_aspect_ratio' => '', + 'alt' => '', + 'title' => '', + ), + 'view callback' => 'picture_file_formatter_picture_view', + 'settings callback' => 'picture_file_formatter_picture_settings', + ); + + return $formatters; +} + +/** + * View callback for hook_file_formatter_info(). + */ +function picture_file_formatter_picture_view($file, $display, $langcode) { + // Prevent PHP notices when trying to read empty files. + // @see http://drupal.org/node/681042 + if (!$file->filesize) { + return; + } + + // Do not bother proceeding if this file does not have an image mime type. + if (strpos($file->filemime, 'image/') !== 0) { + return; + } + + $scheme = file_uri_scheme($file->uri); + $wrappers = file_get_stream_wrappers(STREAM_WRAPPERS_READ); + $readable = !empty($wrappers[$scheme]); + if ($readable) { + $fallback_image_style = ''; + $group_name = $display['settings']['picture_group']; + $mappings = picture_mapping_load($group_name); + if (!$mappings) { + trigger_error(check_plain("Unable to load picture mapping $group_name."), E_USER_ERROR); + return; + } + $breakpoint_styles = picture_get_mapping_breakpoints($mappings, $fallback_image_style); + + if (isset($display['settings']['fallback_image_style']) && !empty($display['settings']['fallback_image_style'])) { + $fallback_image_style = $display['settings']['fallback_image_style']; + } + if (isset($file->override) && isset($file->override['wysiwyg']) && $file->override['wysiwyg']) { + return array( + '#theme' => 'image_style', + '#style_name' => $fallback_image_style, + '#path' => $file->uri, + ); + } + $replace_options = array( + 'clear' => 1, + 'sanitize' => 0, + ); + $dimensions = array( + 'width' => '', + 'height' => '', + ); + if (isset($file->image_dimensions['width'])) { + $dimensions['width'] = $file->image_dimensions['width']; + } + elseif (isset($file->metadata['width'])) { + $dimensions['width'] = $file->metadata['width']; + } + if (isset($file->image_dimensions['height'])) { + $dimensions['height'] = $file->image_dimensions['height']; + } + elseif (isset($file->metadata['height'])) { + $dimensions['height'] = $file->metadata['height']; + } + + $libraries = array( + array('picture', 'picturefill_head'), + array('picture', 'picturefill'), + array('picture', 'picture.ajax'), + ); + if (!empty($display['settings']['lazyload'])) { + $libraries[] = array('picture', 'lazysizes'); + + if (!empty($display['settings']['lazyload_aspect_ratio'])) { + $libraries[] = array('picture', 'lazysizes_aspect_ratio'); + }; + + }; + + + $element = array( + '#theme' => 'picture_formatter', + '#attached' => array( + 'library' => $libraries + ), + '#item' => array( + 'style_name' => $fallback_image_style, + 'path' => $file->uri, + 'uri' => $file->uri, + 'alt' => token_replace($display['settings']['alt'], array('file' => $file), $replace_options), + 'title' => token_replace($display['settings']['title'], array('file' => $file), $replace_options), + ) + $dimensions, + '#image_style' => $fallback_image_style, + '#breakpoints' => $breakpoint_styles, + '#path' => '', + '#lazyload' => !empty($display['settings']['lazyload']), + '#lazyload_aspect_ratio' => !empty($display['settings']['lazyload_aspect_ratio']), + ); + + return $element; + } +} + +/** + * Settings callback for hook_file_formatter_info(). + */ +function picture_file_formatter_picture_settings($form, &$form_state, $settings) { + $picture_group_options = array(); + $picture_mappings = picture_mapping_load_all(); + if ($picture_mappings && !empty($picture_mappings)) { + foreach ($picture_mappings as $machine_name => $picture_mapping) { + $breakpoint_group = $picture_mapping->getBreakpointGroup(); + if ($breakpoint_group) { + $picture_group_options[$machine_name] = $picture_mapping->label; + } + } + } + + $element['picture_group'] = array( + '#title' => t('Picture group'), + '#type' => 'select', + '#default_value' => $settings['picture_group'], + '#required' => TRUE, + '#options' => $picture_group_options, + ); + + $image_styles = image_style_options(FALSE); + $element['fallback_image_style'] = array( + '#title' => t('Fallback image style'), + '#type' => 'select', + '#default_value' => $settings['fallback_image_style'], + '#empty_option' => t('Automatic'), + '#options' => $image_styles + array( + PICTURE_EMPTY_IMAGE => t('Empty image'), + ), + ); + + $element['lazyload'] = array( + '#title' => t('Picture lazyload'), + '#type' => 'checkbox', + '#description' => t('Image will be rendered when it appears in viewport, helps to optimize page load speed.'), + '#default_value' => !empty($settings['lazyload']), + ); + + $element['lazyload_aspect_ratio'] = array( + '#title' => t('Keep aspect ratio'), + '#type' => 'checkbox', + '#description' => t('Preserve the space for the image being lazyloaded to avoid layout reflows.
Image ratio is defined per breakpoint, make sure all images from srcset have the same ratio.
Output example: !example', + array('!example' => htmlentities('')) + ), + '#default_value' => !empty($settings['lazyload_aspect_ratio']), + '#states' => array( + 'visible' => array( + ':input[name="displays[file_picture][settings][lazyload]"]' => array('checked' => TRUE), + ':input[name="displays[file_picture][settings][fallback_image_style]"]' => array('value' => PICTURE_EMPTY_IMAGE), + ) + ) + ); + + $element['alt'] = array( + '#title' => t('Alt attribute'), + '#description' => t('The text to use as value for the img tag alt attribute.'), + '#type' => 'textfield', + '#default_value' => $settings['alt'], + ); + + // Allow the setting of the title attribute. + $element['title'] = array( + '#title' => t('Title attribute'), + '#description' => t('The text to use as value for the img tag title attribute.'), + '#type' => 'textfield', + '#default_value' => $settings['title'], + ); + + return $element; +} diff --git a/frontend/drupal/sites/all/modules/picture/picture.info b/frontend/drupal/sites/all/modules/picture/picture.info new file mode 100644 index 000000000..d67959887 --- /dev/null +++ b/frontend/drupal/sites/all/modules/picture/picture.info @@ -0,0 +1,17 @@ +name = Picture +description = Picture element +core = 7.x +dependencies[] = ctools +dependencies[] = image +dependencies[] = breakpoints +files[] = includes/PictureMapping.php +configure = admin/config/media/picture +package = Picture +stylesheets[all][] = picture_wysiwyg.css + +; Information added by Drupal.org packaging script on 2015-10-13 +version = "7.x-2.13" +core = "7.x" +project = "picture" +datestamp = "1444740240" + diff --git a/frontend/drupal/sites/all/modules/picture/picture.install b/frontend/drupal/sites/all/modules/picture/picture.install new file mode 100644 index 000000000..79bc62344 --- /dev/null +++ b/frontend/drupal/sites/all/modules/picture/picture.install @@ -0,0 +1,243 @@ + 'Responsible images and styles mappings to breakpoints', + 'fields' => array( + 'id' => array( + 'type' => 'serial', + 'not null' => TRUE, + 'description' => 'The internal identifier for this mapping', + 'no export' => TRUE, + ), + 'label' => array( + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + 'description' => 'The machine name of the mapping', + ), + 'machine_name' => array( + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'description' => 'The machine name of the mapping', + ), + 'breakpoint_group' => array( + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'description' => 'The group this mapping belongs to', + ), + 'mapping' => array( + 'type' => 'blob', + 'not null' => TRUE, + 'description' => 'The mappings linked to the breakpoints group', + 'serialize' => TRUE, + ), + ), + 'primary key' => array('id'), + 'export' => array( + 'key' => 'machine_name', + 'key name' => 'machine_name', + 'primary key' => 'id', + 'identifier' => 'picture_mapping', + 'admin_title' => 'label', + 'default hook' => 'default_picture_mapping', + 'object factory' => 'picture_mapping_object_factory', + 'create callback' => 'picture_mapping_create', + 'save callback' => 'picture_mapping_save', + 'export callback' => 'picture_mapping_export', + 'api' => array( + 'owner' => 'picture', + 'api' => 'default_picture_mapping', + 'minimum_version' => 2, + 'current_version' => 2, + ), + ), + ); + return $schema; +} + +/** + * Implements hook_install(). + */ +function picture_install() { + module_load_include('admin.inc', 'picture'); + variable_set('picture_updated_to_file_entity_2', _picture_update_to_file_entity_2()); +} + +/** + * Implements hook_uninstall(). + */ +function picture_uninstall() { + variable_del('picture_ckeditor_mappings'); + variable_del('picture_ckeditor_label'); + variable_del('picture_updated_to_file_entity_2'); + variable_del('picture_js_scope'); + variable_del('picture_polyfill_version'); + variable_del('picture_fallback_method'); + // Delete field formatter settings. + ctools_include('export'); + $entity_info = entity_get_info('file'); + $file_types = file_type_load_all(); + $view_modes = array_keys($entity_info['view modes']); + $view_modes[] = 'default'; + $formatters = array( + 'file_picture', + 'file_field_picture', + 'file_picture_sizes_formatter', + 'file_field_picture_sizes_formatter' + ); + foreach ($file_types as $file_type) { + foreach ($view_modes as $view_mode) { + foreach ($formatters as $formatter) { + ctools_export_crud_delete('file_display', $file_type->type . '__' . $view_mode . '__' . $formatter); + } + } + } + file_info_cache_clear(); +} + +/** + * Implements hook_requirements(). + */ +function picture_requirements($phase) { + $requirements = array(); + if (module_exists('colorbox')) { + $t = get_t(); + $info = system_get_info('module', 'colorbox'); + $requirements['picture_colorbox'] = array( + 'title' => $t('Picture: Colorbox module version.'), + 'value' => $info['version'], + 'severity' => REQUIREMENT_OK, + ); + if (version_compare($info['version'], '7.x-2.5') < 0) { + $requirements['picture_colorbox']['description'] = $t('The Colorbox module must be version 7.x-2.5 or higher in order to work with the Picture module.'); + $requirements['picture_colorbox']['severity'] = REQUIREMENT_ERROR; + } + } + return $requirements; +} + +/** + * Update from 7.x-1.x. + */ +function picture_update_7200() { + module_load_include('php', 'picture', 'includes/PictureMapping'); + db_add_field('picture_mapping', 'label', array( + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + 'description' => 'The machine name of the mapping', + )); + variable_set('picture_ckeditor_mappings', variable_get('picture_ckeditor_groups', array())); + variable_del('picture_ckeditor_groups'); + // Force a schema reset. + drupal_get_schema('picture_mapping', TRUE); + + // Get old picture mappings from code. + ctools_include('export'); + $schema = ctools_export_get_schema('picture_mapping'); + $export = $schema['export']; + $export['api'] = array( + 'owner' => 'picture', + 'api' => 'default_picture_mapping', + 'minimum_version' => 1, + 'current_version' => 1, + ); + $code_mappings = _ctools_export_get_defaults('picture_mapping', $export); + // Get old picture mappings from database. + $db_mappings = ctools_export_crud_load_all('picture_mapping'); + + $all_mappings = array_merge($db_mappings, $code_mappings); + // Update mappings. + foreach ($all_mappings as $picture_mapping) { + if (!($picture_mapping instanceof PictureMapping)) { + $old_mapping = $picture_mapping; + $picture_mapping = new PictureMapping(); + $picture_mapping->setValues($schema, $old_mapping); + } + $mapping = $picture_mapping->getMappings(); + $picture_mapping->setLabel($picture_mapping->getMachineName()); + if (isset($picture_mapping->api_version)) { + $picture_mapping->api_version = 2; + } + $new_mapping = array(); + foreach ($mapping as $breakpoint => $multipliers) { + foreach ($multipliers as $multiplier => $image_style) { + $new_mapping[$breakpoint][$multiplier] = array( + 'mapping_type' => 'image_style', + 'image_style' => $image_style, + 'sizes' => '', + 'sizes_image_styles' => array(), + ); + } + } + $picture_mapping->setMappings($new_mapping); + $picture_mapping->save(); + } +} + +/** + * Update to the new file display formatter provided by file_entity.module. + */ +function picture_update_7201() { + module_load_include('admin.inc', 'picture'); + variable_set('picture_updated_to_file_entity_2', _picture_update_to_file_entity_2()); +} + +/** + * Update field formatters to the new colorbox settings. + */ +function picture_update_7202() { + $fields = field_read_fields(array('type' => 'image')); + foreach ($fields as $field) { + $instances = field_read_instances(array('field_id' => $field['id'])); + foreach ($instances as $instance) { + foreach ($instance['display'] as $view_mode => $view_mode_settings) { + if ($view_mode_settings['type'] == 'picture' && isset($instance['display'][$view_mode]['settings']['colorbox'])) { + $colorbox_group = $instance['display'][$view_mode]['settings']['colorbox']; + $instance['display'][$view_mode]['settings']['colorbox_settings'] = array( + 'colorbox_group' => $colorbox_group, + 'colorbox_gallery' => 'none', + 'colorbox_gallery_custom' => '', + 'colorbox_caption' => 'none', + 'colorbox_caption_custom' => '', + 'colorbox_multivalue_index' => NULL, + ); + } + } + field_update_instance($instance); + } + } +} + +/** + * Update ckeditor settings to the new plugin path. + */ +function picture_update_7203() { + if (module_exists('ckeditor')) { + module_load_include('inc', 'ckeditor', 'includes/ckeditor.admin'); + $result = db_select('ckeditor_settings', 'ck') + ->fields('ck') + ->execute() + ->fetchAll(); + foreach ($result as $res) { + $res->settings = ckeditor_admin_values_to_settings(unserialize($res->settings)); + db_update('ckeditor_settings') + ->fields(array('settings' => $res->settings)) + ->condition('name', $res->name) + ->execute(); + } + } +} diff --git a/frontend/drupal/sites/all/modules/picture/picture.js b/frontend/drupal/sites/all/modules/picture/picture.js new file mode 100644 index 000000000..0de7470b6 --- /dev/null +++ b/frontend/drupal/sites/all/modules/picture/picture.js @@ -0,0 +1,42 @@ +if (typeof Drupal !== 'undefined' && typeof jQuery !== 'undefined') { + // Only load if Drupal and jQuery are defined. + (function ($) { + Drupal.behaviors.picture = { + attach: function (context) { + // Don't load if there's native picture element support. + if (!('HTMLPictureElement' in window)) { + // Ensure we always pass a raw DOM element to picture fill, otherwise it + // will fallback to the document scope and maybe handle to much. + var imgs = $(context).find('img'); + if (imgs.length) { + window.picturefill({ + elements: imgs.get() + }); + } + } + // If this is an opened colorbox ensure the content dimensions are set + // properly. colorbox.js of the colorbox modules sets #cboxLoadedContent + // as context. + if (context === '#cboxLoadedContent' && $(context).find('picture').length) { + // Try to resize right away. + $.colorbox.resize(); + // Make sure the colorbox resizes always when the image is changed. + $('img', context).once('colorbox-lazy-load', function(){ + $(this).load(function(){ + // Ensure there's no max-width / max-height otherwise we won't get + // the proper values. We could use naturalWeight / naturalHeight + // but that's not supported by '; + $output .= '

' . t('The Picture module provides an image formatter and breakpoint mappings to output responsive images using the HTML5 picture tag. For more information, see the online documentation for the Picture module.', array('!picture' => 'https://drupal.org/documentation/modules/picture')) . '

'; + $output .= '

' . t('Uses') . '

'; + $output .= '
'; + $output .= '
' . t('Defining picture mappings') . '
'; + $output .= '
' . t('By creating picture mappings you define the image styles that are being used to output images at certain breakpoints. On the Picture mappings page, click Add picture mapping to create a new mapping. First chose a label and a breakpoint group and click Save. After that you can choose the image styles that will be used for each breakpoint. Image styles can be defined on the Image styles page that is provided by the Image module. Breakpoints are defined in the info files of the theme or you can create custom breakpoints. See the help page of the Breakpoint module for more information.', array( + '!picture_mapping' => url('admin/config/media/picture'), + '!image_styles' => url('admin/config/media/image-styles'), + '!image_help' => url('admin/help/image'), + '!breakpoint_help' => url('admin/help/breakpoints'), + )) . '
'; + $output .= '
' . t('Using picture mappings in Image fields') . '
'; + $output .= '
' . t('After defining responsive image mappings, you can use them in the display settings for your Image fields, so that the site displays responsive images using the HTML5 picture tag. Open the Manage display page for the entity type (content type, taxonomy vocabulary, etc.) that the Image field is attached to. Choose the format Picture, click the Edit icon, and select one of the picture mappings that you have created. For general information on how to manage fields and their display see the help page of the Field UI module.', array('!field_ui' => url('admin/help/field_ui'))) . '
'; + $output .= '
'; + break; + + case 'admin/config/media/picture': + $output .= '

' . t('A picture mapping associates an image style with each breakpoint defined by your theme.') . '

'; + break; + + } + return $output; +} + +/** + * Implements hook_flush_caches(). + */ +function picture_flush_caches() { + // After update.php, caches are flushed. Check if file_entity has updated to + // version 2. + module_load_include('admin.inc', 'picture'); + variable_set('picture_updated_to_file_entity_2', _picture_update_to_file_entity_2()); +} + +/** + * Implements hook_modules_enabled(). + */ +function picture_modules_enabled($modules) { + if (in_array('file_entity', $modules)) { + module_load_include('admin.inc', 'picture'); + module_load_include('module', 'file_entity'); + variable_set('picture_updated_to_file_entity_2', _picture_update_to_file_entity_2()); + } +} + +/** + * Implements hook_permission(). + */ +function picture_permission() { + return array( + 'administer pictures' => array( + 'title' => t('Administer Pictures'), + 'description' => t('Administer Pictures'), + ), + ); +} + +/** + * Implements hook_menu(). + */ +function picture_menu() { + $items = array(); + $items['admin/config/media/picture/settings'] = array( + 'title' => 'Settings', + 'type' => MENU_LOCAL_TASK, + 'description' => 'Picture settings (CKEditor, File Entity, ...)', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('picture_admin_settings'), + 'access arguments' => array('administer pictures'), + 'file' => 'picture.admin.inc', + 'weight' => 0, + ); + return $items; +} + +/** + * Load mappings. + */ +function picture_mapping_load($name) { + ctools_include('export'); + if ($name) { + $mappings = ctools_export_load_object('picture_mapping', 'names', array($name)); + $mapping = isset($mappings[$name]) ? $mappings[$name] : FALSE; + return $mapping; + } + return FALSE; +} + +/** + * Load all mappings. + */ +function picture_mapping_load_all() { + ctools_include('export'); + return ctools_export_load_object('picture_mapping'); +} + +/** + * Save mappings. + */ +function picture_mapping_save(PictureMapping $mapping) { + return $mapping->save(); +} + +/** + * Implements hook_library(). + */ +function picture_library() { + $libraries = array(); + switch (variable_get('picture_polyfill_version', 'min')) { + case 'min': + $libraries['picturefill_head'] = array( + 'title' => t('Picturefill head fix'), + 'version' => '2.3.1', + 'js' => array( + 'document.createElement( "picture" );' => array( + 'type' => 'inline', + 'weight' => -10, + 'group' => JS_DEFAULT, + 'scope' => 'header', + 'need_jquery' => FALSE, + ), + ), + ); + + $libraries['picturefill'] = array( + 'title' => t('Picturefill'), + 'website' => 'https://github.com/scottjehl/picturefill', + 'version' => '2.3.1', + 'js' => array( + drupal_get_path('module', 'picture') . '/picturefill2/picturefill.min.js' => array( + 'type' => 'file', + 'weight' => -10, + 'group' => JS_DEFAULT, + 'scope' => variable_get('picture_js_scope', 'footer'), + 'need_jquery' => FALSE, + 'async' => TRUE, + ), + ), + ); + + $libraries['picture.ajax'] = array( + 'title' => t('Ajax support for picture'), + 'version' => VERSION, + 'js' => array( + drupal_get_path('module', 'picture') . '/picture.min.js' => array( + 'type' => 'file', + 'weight' => -10, + 'group' => JS_DEFAULT, + 'scope' => variable_get('picture_js_scope', 'footer'), + 'need_jquery' => FALSE, + 'async' => TRUE, + ), + ), + ); + + $libraries['lazysizes'] = array( + 'title' => t('Lazyload for picture element'), + 'version' => '1.0.1', + 'js' => array( + drupal_get_path('module', 'picture') . '/lazysizes/lazysizes.min.js' => array( + 'type' => 'file', + // file has async, put before all files to prevent JS concatenation breaking + 'weight' => -20, + 'group' => JS_LIBRARY, + 'need_jquery' => FALSE, + 'async' => TRUE, + ), + ), + ); + + $libraries['lazysizes_aspect_ratio'] = array( + 'title' => t('Aspect ratio plugin for lazysizes'), + 'version' => '1.0.1', + 'js' => array( + drupal_get_path('module', 'picture') . '/lazysizes/plugins/aspectratio/ls.aspectratio.min.js' => array( + 'type' => 'file', + 'weight' => -10, + 'group' => JS_LIBRARY, + 'need_jquery' => FALSE, + 'async' => TRUE, + ), + ), + 'css' => array( + drupal_get_path('module', 'picture') . '/lazysizes/plugins/aspectratio/ls.aspectratio.css' => array( + 'type' => 'file', + 'media' => 'screen', + ), + ), + ); + break; + + case 'dev': + $libraries['picturefill_head'] = array( + 'title' => t('Picturefill head fix'), + 'version' => '2.3.1', + 'js' => array( + 'document.createElement( "picture" );' => array( + 'type' => 'inline', + 'weight' => -10, + 'group' => JS_DEFAULT, + 'scope' => 'header', + 'need_jquery' => FALSE, + ), + ), + ); + + $libraries['picturefill'] = array( + 'title' => t('Picturefill'), + 'website' => 'https://github.com/scottjehl/picturefill', + 'version' => '2.3.1', + 'js' => array( + drupal_get_path('module', 'picture') . '/picturefill2/picturefill.js' => array( + 'type' => 'file', + 'weight' => -10, + 'group' => JS_DEFAULT, + 'scope' => variable_get('picture_js_scope', 'footer'), + 'need_jquery' => FALSE, + 'async' => TRUE, + ), + ), + ); + + $libraries['picture.ajax'] = array( + 'title' => t('Ajax support for picture'), + 'version' => VERSION, + 'js' => array( + drupal_get_path('module', 'picture') . '/picture.js' => array( + 'type' => 'file', + 'weight' => -10, + 'group' => JS_DEFAULT, + 'scope' => variable_get('picture_js_scope', 'footer'), + 'need_jquery' => FALSE, + 'async' => TRUE, + ), + ), + ); + + $libraries['lazysizes'] = array( + 'title' => t('Lazyload for picture element'), + 'version' => '1.0.1', + 'js' => array( + drupal_get_path('module', 'picture') . '/lazysizes/lazysizes.js' => array( + 'type' => 'file', + // file has async, put before all files to prevent JS concatenation breaking + 'weight' => -20, + 'group' => JS_LIBRARY, + 'need_jquery' => FALSE, + 'async' => TRUE, + ), + ), + ); + + + $libraries['lazysizes_aspect_ratio'] = array( + 'title' => t('Aspect ratio plugin for lazysizes'), + 'version' => '1.0.1', + 'js' => array( + drupal_get_path('module', 'picture') . '/lazysizes/plugins/aspectratio/ls.aspectratio.js' => array( + 'type' => 'file', + 'weight' => -10, + 'group' => JS_LIBRARY, + 'need_jquery' => FALSE, + 'async' => TRUE, + ), + ), + 'css' => array( + drupal_get_path('module', 'picture') . '/lazysizes/plugins/aspectratio/ls.aspectratio.css' => array( + 'type' => 'file', + 'media' => 'screen', + ), + ), + ); + break; + } + + return $libraries; +} + +/** + * Implements hook_ctools_plugin_directory(). + * + * Lets CTools know which plugin APIs are implemented by picture module. + */ +function picture_ctools_plugin_directory($owner, $plugin_type) { + // Load the export_ui plugin. + if ($owner == 'ctools' && $plugin_type == 'export_ui') { + return 'ctools/plugins/export_ui'; + } +} + +/** + * Implements hook_theme(). + */ +function picture_theme() { + return array( + 'picture' => array( + 'variables' => array( + 'style_name' => NULL, + 'path' => NULL, + 'uri' => NULL, + 'width' => NULL, + 'height' => NULL, + 'alt' => '', + 'title' => NULL, + 'attributes' => array(), + 'breakpoints' => array(), + 'timestamp' => NULL, + 'lazyload' => NULL, + 'lazyload_aspect_ratio' => NULL, + ), + ), + 'picture_formatter' => array( + 'variables' => array( + 'item' => NULL, + 'path' => NULL, + 'image_style' => NULL, + 'breakpoints' => array(), + 'lazyload' => NULL, + 'lazyload_aspect_ratio' => NULL, + ), + ), + 'picture_formatter_colorbox' => array( + 'variables' => array( + 'item' => NULL, + 'path' => NULL, + 'image_style' => NULL, + 'breakpoints' => array(), + 'colorbox_group' => array(), + 'colorbox_image_style' => NULL, + 'colorbox_group_id' => NULL, + 'colorbox_caption' => NULL, + ), + ), + 'picture_source' => array( + 'variables' => array( + 'srcset' => NULL, + 'media' => NULL, + 'mime_type' => NULL, + 'sizes' => NULL, + 'lazyload' => NULL, + 'lazyload_aspect_ratio' => NULL, + ), + ), + 'image_srcset' => array( + 'variables' => array( + 'uri' => NULL, + 'path' => NULL, + 'width' => NULL, + 'height' => NULL, + 'alt' => '', + 'title' => NULL, + 'attributes' => array(), + 'srcset' => array(), + 'sizes' => NULL, + ), + ), + 'picture_sizes_formatter' => array( + 'variables' => array( + 'item' => NULL, + 'path' => NULL, + 'image_styles' => array(), + 'fallback_image_style' => NULL, + 'sizes' => NULL, + ), + ), + ); +} + +/** + * Implements hook_field_formatter_info(). + */ +function picture_field_formatter_info() { + $formatters = array(); + $mappings = array_keys(picture_get_mapping_options()); + if ($mappings) { + $formatters['picture'] = array( + 'label' => t('Picture'), + 'field types' => array('image'), + 'settings' => array( + 'picture_mapping' => reset($mappings), + 'fallback_image_style' => '', + 'lazyload' => '', + 'lazyload_aspect_ratio' => '', + 'image_link' => '', + 'colorbox_settings' => array( + 'colorbox_group' => '', + 'colorbox_gallery' => 'post', + 'colorbox_gallery_custom' => '', + 'colorbox_caption' => 'auto', + 'colorbox_caption_custom' => '', + 'colorbox_multivalue_index' => NULL, + ), + ), + ); + } + $formatters['picture_sizes_formatter'] = array( + 'label' => t('Image with sizes'), + 'field types' => array('image'), + 'settings' => array( + 'sizes' => '', + 'image_styles' => array(), + 'fallback_image_style' => '', + 'lazyload' => '', + 'lazyload_aspect_ratio' => '', + 'image_link' => '', + 'colorbox_settings' => array( + 'colorbox_group' => '', + 'colorbox_gallery' => 'post', + 'colorbox_gallery_custom' => '', + 'colorbox_caption' => 'auto', + 'colorbox_caption_custom' => '', + 'colorbox_multivalue_index' => NULL, + ), + ), + ); + + return $formatters; +} + +/** + * Implements hook_field_formatter_settings_form(). + */ +function picture_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) { + $display = $instance['display'][$view_mode]; + $function = "picture_field_formatter_settings_{$display['type']}_form"; + $settings = $display['settings']; + return $function($field, $instance, $settings); +} + +/** + * Helper function. + * + * @see picture_field_formatter_settings_form() + */ +function picture_field_formatter_settings_picture_form($field, $instance, $settings) { + $options = picture_get_mapping_options(); + if ($options) { + $element['picture_mapping'] = array( + '#title' => t('Picture mapping'), + '#type' => 'select', + '#default_value' => isset($settings['picture_mapping']) ? $settings['picture_mapping'] : '', + '#required' => TRUE, + '#options' => picture_get_mapping_options(), + ); + } + // No picture mappings. + else { + $element['picture_mapping'] = array( + '#title' => t('Picture mapping'), + '#type' => 'item', + '#markup' => t( + 'There are no picture groups defined. !create_link.', + array('!create_link' => l(t('Create a picture mapping'), 'admin/config/media/picture/add')) + ), + ); + } + + $image_styles = image_style_options(FALSE); + $element['fallback_image_style'] = array( + '#title' => t('Fallback image style'), + '#type' => 'select', + '#default_value' => $settings['fallback_image_style'], + '#empty_option' => t('Automatic'), + '#options' => $image_styles + array( + PICTURE_EMPTY_IMAGE => t('Empty image'), + PICTURE_ORIGINAL_IMAGE => t('Original image'), + ), + ); + + $element['lazyload'] = array( + '#title' => t('Picture lazyload'), + '#description' => t('Image will be rendered when it appears in viewport, helps to optimize page load speed.'), + '#type' => 'checkbox', + '#default_value' => $settings['lazyload'] ? $settings['lazyload'] : FALSE , + ); + + $element['lazyload_aspect_ratio'] = array( + '#title' => t('Keep aspect ratio'), + '#type' => 'checkbox', + '#description' => t('Preserve the space for the image being lazyloaded to avoid layout reflows.
Image ratio is defined per breakpoint, make sure all images from srcset have the same ratio.
Output example: !example', + array('!example' => htmlentities('')) + ), + '#default_value' => !empty($settings['lazyload_aspect_ratio']), + '#states' => array( + 'visible' => array( + ':input[name="fields[field_image][settings_edit_form][settings][lazyload]"]' => array('checked' => TRUE), + ':input[name="fields[field_image][settings_edit_form][settings][fallback_image_style]"]' => array('value' => PICTURE_EMPTY_IMAGE), + ) + ) + ); + + $link_types = picture_link_types($instance); + + $element['image_link'] = array( + '#title' => t('Link image to'), + '#type' => 'select', + '#default_value' => $settings['image_link'], + '#empty_option' => t('Nothing'), + '#options' => $link_types, + '#attributes' => array( + 'class' => array('picture-image-link'), + ), + ); + + if (module_exists('colorbox')) { + // Settings for the colorbox option. + $element['colorbox_settings'] = array( + '#type' => 'fieldset', + '#tree' => TRUE, + '#title' => t('Colorbox settings'), + '#collapsed' => FALSE, + '#collapsible' => TRUE, + '#states' => array( + 'visible' => array( + ':input[name$="' . $field['field_name'] . '][settings_edit_form][settings][image_link]"].picture-image-link' => array('value' => 'colorbox'), + ), + ), + ); + + $element['colorbox_settings']['colorbox_group'] = array( + '#title' => t('Colorbox picture mapping'), + '#type' => 'select', + '#default_value' => $settings['colorbox_settings']['colorbox_group'], + '#required' => FALSE, + '#options' => picture_get_mapping_options(), + ); + + $gallery = array( + 'post' => t('Per post gallery'), + 'page' => t('Per page gallery'), + 'field_post' => t('Per field in post gallery'), + 'field_page' => t('Per field in page gallery'), + 'custom' => t('Custom'), + 'none' => t('No gallery'), + ); + $element['colorbox_settings']['colorbox_gallery'] = array( + '#title' => t('Gallery (image grouping)'), + '#type' => 'select', + '#default_value' => $settings['colorbox_settings']['colorbox_gallery'], + '#options' => $gallery, + '#description' => t('How Colorbox should group the image galleries.'), + '#attributes' => array( + 'class' => array('picture-colorbox-gallery'), + ), + ); + $element['colorbox_settings']['colorbox_gallery_custom'] = array( + '#title' => t('Custom gallery'), + '#type' => 'textfield', + '#maxlength' => 32, + '#default_value' => $settings['colorbox_settings']['colorbox_gallery_custom'], + '#description' => t('All images on a page with the same gallery value (rel attribute) will be grouped together. It must only contain lowercase letters, numbers, hyphen and underscores.'), + '#element_validate' => array('colorbox_gallery_custom_validate'), + '#required' => FALSE, + '#states' => array( + 'visible' => array( + ':input[name$="[settings][colorbox_settings][colorbox_gallery]"].picture-colorbox-gallery' => array('value' => 'custom'), + ), + ), + ); + + $caption = array( + 'auto' => t('Automatic'), + 'title' => t('Title text'), + 'alt' => t('Alt text'), + 'node_title' => t('Content title'), + 'custom' => t('Custom (with tokens)'), + 'none' => t('None'), + ); + $element['colorbox_settings']['colorbox_caption'] = array( + '#title' => t('Caption'), + '#type' => 'select', + '#default_value' => $settings['colorbox_settings']['colorbox_caption'], + '#options' => $caption, + '#description' => t('Automatic will use the first none empty value of the title, the alt text and the content title.'), + '#attributes' => array( + 'class' => array('picture-colorbox-caption'), + ), + ); + $element['colorbox_settings']['colorbox_caption_custom'] = array( + '#title' => t('Custom caption'), + '#type' => 'textfield', + '#default_value' => $settings['colorbox_settings']['colorbox_caption_custom'], + '#states' => array( + 'visible' => array( + ':input[name$="[settings][colorbox_settings][colorbox_caption]"].picture-colorbox-caption' => array('value' => 'custom'), + ), + ), + ); + + // Allow users to hide or set a custom recursion limit. + // The module token_tweaks sets a global recursion limit that can not be + // bypassed. + if (module_exists('token') && $recursion_limit = min(variable_get('token_tree_recursion_limit', 3), variable_get('colorbox_token_recursion_limit', 3))) { + $element['colorbox_settings']['colorbox_token'] = array( + '#type' => 'fieldset', + '#title' => t('Replacement patterns'), + '#theme' => 'token_tree', + '#token_types' => array($instance['entity_type'], 'file'), + '#recursion_limit' => $recursion_limit, + '#dialog' => TRUE, + '#states' => array( + 'visible' => array( + ':input[name$="[settings][colorbox_settings][colorbox_caption]"].picture-colorbox-caption' => array('value' => 'custom'), + ), + ), + ); + } + else { + $element['colorbox_settings']['colorbox_token'] = array( + '#type' => 'fieldset', + '#title' => t('Replacement patterns'), + '#description' => '' . t('For token support the token module must be installed.', array('@token_url' => 'http://drupal.org/project/token')) . '', + '#states' => array( + 'visible' => array( + ':input[name$="[settings][colorbox_settings][colorbox_caption]"].picture-colorbox-caption' => array('value' => 'custom'), + ), + ), + ); + } + } + + return $element; +} + +/** + * Helper function. + * + * @see picture_field_formatter_settings_form() + */ +function picture_field_formatter_settings_picture_sizes_formatter_form($field, $instance, $settings) { + $element = array(); + if ($instance['entity_type'] == 'file') { + if (empty($settings['sizes'])) { + $settings['sizes'] = '(min-width: 0px)'; + } + $styles = array_filter($settings['image_styles']); + if (empty($styles)) { + $settings['image_styles'][PICTURE_EMPTY_IMAGE] = PICTURE_EMPTY_IMAGE; + } + } + $element['sizes'] = array( + '#title' => t('Sizes'), + '#type' => 'textfield', + '#maxlength' => 1024, + '#description' => t( + 'The value of the sizes attribute. See !link for more information.', + array( + '!link' => l(t('the spec'), 'http://www.whatwg.org/specs/web-apps/current-work/multipage/embedded-content.html#introduction-3:viewport-based-selection-2'), + ) + ), + '#default_value' => $settings['sizes'], + '#required' => TRUE, + ); + + $image_styles = image_style_options(FALSE); + $image_styles[PICTURE_EMPTY_IMAGE] = t('Empty image'); + $image_styles[PICTURE_ORIGINAL_IMAGE] = t('Original image'); + + $element['image_styles'] = array( + '#title' => t('Image styles'), + '#type' => 'checkboxes', + '#default_value' => $settings['image_styles'], + '#options' => $image_styles, + '#required' => TRUE, + ); + + $element['fallback_image_style'] = array( + '#title' => t('Fallback image style'), + '#type' => 'select', + '#default_value' => $settings['fallback_image_style'], + '#options' => $image_styles + array( + PICTURE_EMPTY_IMAGE => t('Empty image'), + PICTURE_ORIGINAL_IMAGE => t('Original image'), + ), + '#required' => TRUE, + ); + + $link_types = picture_link_types($instance); + unset($link_types['colorbox']); + + $element['image_link'] = array( + '#title' => t('Link image to'), + '#type' => 'select', + '#default_value' => $settings['image_link'], + '#empty_option' => t('Nothing'), + '#options' => $link_types, + '#attributes' => array( + 'class' => array('picture-image-link'), + ), + ); + + return $element; +} + +/** + * Returns a list of picture mappings for use in a select list. + */ +function picture_get_mapping_options() { + $picture_mapping_options = array(); + $picture_mappings = picture_mapping_load_all(); + if ($picture_mappings && !empty($picture_mappings)) { + foreach ($picture_mappings as $picture_mapping) { + // Exclude old mappings. + if ($picture_mapping instanceof PictureMapping && (!isset($picture_mapping->disabled) || !$picture_mapping->disabled) && $picture_mapping->hasMappings()) { + $picture_mapping_options[$picture_mapping->getMachineName()] = $picture_mapping->label(); + } + } + } + return $picture_mapping_options; +} + +/** + * Implements hook_field_formatter_settings_summary(). + */ +function picture_field_formatter_settings_summary($field, $instance, $view_mode) { + $display = $instance['display'][$view_mode]; + $function = "picture_field_formatter_settings_{$display['type']}_summary"; + $settings = $display['settings']; + return $function($field, $instance, $settings); +} + +/** + * Helper function. + * + * @see picture_field_formatter_settings_summary() + */ +function picture_field_formatter_settings_picture_summary($field, $instance, $settings) { + $summary = array(); + + $picture_mapping = picture_mapping_load($settings['picture_mapping']); + if ($picture_mapping) { + $summary[] = t('Picture mapping: @picture_mapping', array('@picture_mapping' => $picture_mapping->label())); + $image_styles = image_style_options(FALSE); + unset($image_styles['']); + if (isset($image_styles[$settings['fallback_image_style']])) { + $summary[] = t('Fallback Image style: @style', array('@style' => $image_styles[$settings['fallback_image_style']])); + } + else { + $summary[] = t('Automatic fallback'); + } + + $link_types = picture_link_types($instance); + // Display this setting only if image is linked. + if (isset($link_types[$settings['image_link']])) { + $summary[] = filter_xss_admin($link_types[$settings['image_link']]); + } + } + else { + $summary[] = t('Select a responsive image mapping.'); + } + + // Display the settings for the colorbox if chosen. + if ($settings['image_link'] == 'colorbox') { + $summary[] = t('Colorbox Group: @setting', array('@setting' => $settings['colorbox_settings']['colorbox_group'])); + $gallery = array( + 'post' => t('Per post gallery'), + 'page' => t('Per page gallery'), + 'field_post' => t('Per field in post gallery'), + 'field_page' => t('Per field in page gallery'), + 'custom' => t('Custom'), + 'none' => t('No gallery'), + ); + $summary[] = t('Colorbox Gallery: @setting', array('@setting' => $gallery[$settings['colorbox_settings']['colorbox_gallery']])); + if ($settings['colorbox_settings']['colorbox_gallery'] == 'custom') { + $summary[] = '→ ' . t('Colorbox Gallery Custom: @setting', array('@setting' => $settings['colorbox_settings']['colorbox_gallery_custom'])); + } + $caption = array( + 'auto' => t('Automatic'), + 'title' => t('Title text'), + 'alt' => t('Alt text'), + 'node_title' => t('Content title'), + 'custom' => t('Custom (with tokens)'), + 'none' => t('None'), + ); + $summary[] = t('Colorbox Caption: @setting', array('@setting' => $caption[$settings['colorbox_settings']['colorbox_caption']])); + if ($settings['colorbox_settings']['colorbox_caption'] == 'custom') { + $summary[] = '→ ' . t('Colorbox Caption Custom: @setting', array('@setting' => $settings['colorbox_settings']['colorbox_caption_custom'])); + } + } + + return implode('
', $summary); +} + +/** + * Helper function. + * + * @see picture_field_formatter_settings_form() + */ +function picture_field_formatter_settings_picture_sizes_formatter_summary($field, $instance, $settings) { + $summary = array(); + $summary[] = t('Sizes: @sizes', array('@sizes' => $settings['sizes'])); + + $image_styles = image_style_options(FALSE); + unset($image_styles['']); + $image_styles[PICTURE_EMPTY_IMAGE] = t('Empty image'); + $image_styles[PICTURE_ORIGINAL_IMAGE] = t('Original image'); + $selected_styles = array_filter($settings['image_styles']); + $summary[] = t( + 'Image styles: @styles', + array( + '@styles' => implode(', ', array_intersect_key($image_styles, $selected_styles)), + ) + ); + + $summary[] = t('Fallback image style: @style', array('@style' => $image_styles[$settings['fallback_image_style']])); + + $link_types = picture_link_types($instance); + // Display this setting only if image is linked. + if (isset($link_types[$settings['image_link']])) { + $summary[] = filter_xss_admin($link_types[$settings['image_link']]); + } + + return implode('
', $summary); +} + +/** + * Implements hook_field_formatter_view(). + */ +function picture_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) { + $function = "picture_field_formatter_{$display['type']}_view"; + return $function($entity_type, $entity, $field, $instance, $langcode, $items, $display); +} + +/** + * Helper function. + * + * @see picture_field_formatter_view() + */ +function picture_field_formatter_picture_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) { + $element = array(); + $settings = $display['settings']; + // Check if the formatter involves a link. + $image_link = $display['settings']['image_link']; + if ($image_link == 'content') { + if (isset($entity->referencing_entity) && isset($entity->referencing_entity_type)) { + $uri = entity_uri($entity->referencing_entity_type, $entity->referencing_entity); + } + else { + $uri = entity_uri($entity_type, $entity); + } + } + elseif ($image_link == 'file') { + $link_file = TRUE; + } + elseif ($image_link) { + if (isset($entity->$image_link)) { + // Support for field translations. + $language = field_language($entity_type, $entity, $field['field_name']); + $link_field = $entity->$image_link; + + if (isset($link_field[$language])) { + $link_values = $link_field[$language]; + } + } + elseif (image_style_load($image_link)) { + $link_file = $image_link; + } + } + $fallback_image_style = $settings['fallback_image_style']; + + // Support old display settings from 7.x-1.x. + $mapping_name = isset($display['settings']['picture_mapping']) && !empty($display['settings']['picture_mapping']) ? $display['settings']['picture_mapping'] : $display['settings']['picture_group']; + + // If we haven't saved a picture mapping for this field previously and + // this isn't a field formatter screen (i.e. panels), + // load a default mapping. + $picture_mapping = FALSE; + if (isset($mapping_name) && !empty($mapping_name)) { + $picture_mapping = picture_mapping_load($mapping_name); + if (!$picture_mapping) { + trigger_error(check_plain("Unable to load picture mapping $mapping_name."), E_USER_ERROR); + return $element; + } + } + else { + $all_mappings = picture_mapping_load_all(); + $picture_mapping = reset($all_mappings); + if (!$picture_mapping) { + trigger_error("No picture mappings have been defined yet.", E_USER_ERROR); + return $element; + } + } + $breakpoint_styles = picture_get_mapping_breakpoints($picture_mapping, $fallback_image_style); + + // Assume regular display. + $formatter = 'picture_formatter'; + $colorbox_breakpoints = array(); + $colorbox_fallback_image_style = ''; + + // Check for colorbox link. + if (module_exists('colorbox') && $display['settings']['image_link'] == 'colorbox') { + $formatter = 'picture_formatter_colorbox'; + $mappings = picture_mapping_load($display['settings']['colorbox_settings']['colorbox_group']); + if (!$mappings) { + trigger_error(check_plain("Unable to load picture mapping {$display['settings']['colorbox_settings']['colorbox_group']}."), E_USER_ERROR); + return $element; + } + $colorbox_breakpoints = picture_get_mapping_breakpoints($mappings, $colorbox_fallback_image_style); + } + + foreach ($items as $delta => $item) { + if (isset($link_file)) { + $uri = array( + 'path' => $link_file === TRUE ? file_create_url($item['uri']) : image_style_url($link_file, $item['uri']), + 'options' => array(), + ); + } + // Handle multiple link with image values. + if (isset($link_values)) { + if (isset($link_values[$delta]['url'])) { + $uri = array( + 'path' => $link_values[$delta]['url'], + 'options' => array('attributes' => $link_values[$delta]['attributes']), + ); + // Handle query fragment if there is any. + if (!empty($link_values[$delta]['query'])) { + $uri['options']['query'] = $link_values[$delta]['query']; + } + } + // If there are more image values than link values unset the link. + else { + unset($uri); + } + } + + $libraries = array( + array('picture', 'picturefill_head'), + array('picture', 'picturefill'), + array('picture', 'picture.ajax'), + ); + if (!empty($settings['lazyload'])) { + $libraries[] = array('picture', 'lazysizes'); + + if (!empty($display['settings']['lazyload_aspect_ratio'])) { + $libraries[] = array('picture', 'lazysizes_aspect_ratio'); + }; + + }; + $element[$delta] = array( + '#theme' => $formatter, + '#attached' => array( + 'library' => $libraries, + ), + '#item' => $item, + '#image_style' => $fallback_image_style, + '#breakpoints' => $breakpoint_styles, + '#path' => isset($uri) ? $uri : '', + '#colorbox_group' => $colorbox_breakpoints, + '#colorbox_image_style' => $colorbox_fallback_image_style, + '#lazyload' => !empty($settings['lazyload']), + '#lazyload_aspect_ratio' => !empty($settings['lazyload_aspect_ratio']), + ); + + // Add css and js for colorbox. + if ($formatter == 'picture_formatter_colorbox') { + $element[$delta]['#attached']['css'][drupal_get_path('module', 'picture') . '/picture_colorbox.css'] = array('type' => 'file'); + $element[$delta]['#attached']['js'][drupal_get_path('module', 'picture') . '/picture_colorbox.js'] = array('type' => 'file'); + if (!variable_get('colorbox_inline', 0)) { + $element[$delta]['#attached']['js'][drupal_get_path('module', 'colorbox') . '/js/colorbox_inline.js'] = array('type' => 'file'); + } + + $colorbox_settings = $display['settings']['colorbox_settings']; + + // Add the group ID. + list($id) = entity_extract_ids($entity_type, $entity); + $entity_id = !empty($id) ? $entity_type . '-' . $id : 'entity-id'; + // If this is a file entity field check for the referencing entity to do + // the proper grouping. + if (isset($entity->referencing_entity) && isset($entity->referencing_field)) { + // Because we don't have the entity type we use some "magic" to get a + // unique entity identifier. + $entity_id = spl_object_hash($entity->referencing_entity); + $colorbox_group_field = $entity->referencing_field; + } + else { + $colorbox_group_field = $field['field_name']; + } + switch ($colorbox_settings['colorbox_gallery']) { + case 'post': + $gallery_id = 'gallery-' . $entity_id; + break; + + case 'page': + $gallery_id = 'gallery-all'; + break; + + case 'field_post': + $gallery_id = 'gallery-' . $entity_id . '-' . $colorbox_group_field; + break; + + case 'field_page': + $gallery_id = 'gallery-' . $colorbox_group_field; + break; + + case 'custom': + $gallery_id = $colorbox_settings['colorbox_gallery_custom']; + break; + + default: + $gallery_id = ''; + } + $element[$delta]['#colorbox_group_id'] = $gallery_id; + + // Add the caption. + $entity_title = entity_label($entity_type, $entity); + + switch ($colorbox_settings['colorbox_caption']) { + case 'auto': + // If the title is empty use alt or the entity title in that order. + if (!empty($item['title'])) { + $caption = $item['title']; + } + elseif (!empty($item['alt'])) { + $caption = $item['alt']; + } + elseif (!empty($entity_title)) { + $caption = $entity_title; + } + else { + $caption = ''; + } + break; + + case 'title': + $caption = $item['title']; + break; + + case 'alt': + $caption = $item['alt']; + break; + + case 'node_title': + $caption = $entity_title; + break; + + case 'custom': + $caption = token_replace($colorbox_settings['colorbox_caption_custom'], array($entity_type => $entity, 'file' => (object) $item), array('clear' => TRUE)); + break; + + default: + $caption = ''; + } + + // Shorten the caption for the example styles or when caption shortening + // is active. + $colorbox_style = variable_get('colorbox_style', 'default'); + $trim_length = variable_get('colorbox_caption_trim_length', 75); + if (((strpos($colorbox_style, 'colorbox/example') !== FALSE) || variable_get('colorbox_caption_trim', 0)) && (drupal_strlen($caption) > $trim_length)) { + $caption = drupal_substr($caption, 0, $trim_length - 5) . '...'; + } + + $element[$delta]['#colorbox_caption'] = $caption; + } + } + return $element; +} + +/** + * Helper function. + * + * @see picture_field_formatter_view() + */ +function picture_field_formatter_picture_sizes_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) { + $elements = array(); + $settings = $display['settings']; + // Check if the formatter involves a link. + $image_link = $display['settings']['image_link']; + if ($image_link == 'content') { + $uri = entity_uri($entity_type, $entity); + } + elseif ($image_link == 'file') { + $link_file = TRUE; + } + elseif ($image_link) { + if (isset($entity->$image_link)) { + // Support for field translations. + $language = field_language($entity_type, $entity, $field['field_name']); + $link_field = $entity->$image_link; + + if (isset($link_field[$language])) { + $link_values = $link_field[$language]; + } + } + elseif (image_style_load($image_link)) { + $link_file = $image_link; + } + } + + foreach ($items as $delta => $item) { + if (isset($link_file)) { + $uri = array( + 'path' => $link_file === TRUE ? file_create_url($item['uri']) : image_style_url($link_file, $item['uri']), + 'options' => array(), + ); + } + // Handle multiple link with image values. + if (isset($link_values)) { + if (isset($link_values[$delta]['url'])) { + $uri = array( + 'path' => $link_values[$delta]['url'], + 'options' => array('attributes' => $link_values[$delta]['attributes']), + ); + // Handle query fragment if there is any. + if (!empty($link_values[$delta]['query'])) { + $uri['options']['query'] = $link_values[$delta]['query']; + } + } + // If there are more image values than link values unset the link. + else { + unset($uri); + } + } + $elements[$delta] = array( + '#theme' => 'picture_sizes_formatter', + '#item' => $item, + '#image_styles' => array_filter($settings['image_styles']), + '#fallback_image_style' => $settings['fallback_image_style'], + '#sizes' => $settings['sizes'], + '#path' => isset($uri) ? $uri : '', + ); + + } + return $elements; +} + +/** + * Theme picture formatter. + */ +function theme_picture_formatter($variables) { + if (!isset($variables['breakpoints']) || empty($variables['breakpoints'])) { + $image_formatter = array( + '#theme' => 'image_formatter', + '#item' => $variables['item'], + '#image_style' => $variables['image_style'], + '#path' => $variables['path'], + ); + return drupal_render($image_formatter); + } + $item = $variables['item']; + + $responsive_image = array( + '#theme' => 'picture', + '#width' => isset($item['width']) ? $item['width'] : NULL, + '#height' => isset($item['height']) ? $item['height'] : NULL, + '#style_name' => $variables['image_style'], + '#breakpoints' => $variables['breakpoints'], + '#uri' => $item['uri'], + '#alt' => isset($item['alt']) ? $item['alt'] : '', + '#attributes' => isset($item['attributes']) ? $item['attributes'] : NULL, + '#timestamp' => isset($item['timestamp']) ? $item['timestamp'] : NULL, + '#lazyload' => !empty($variables['lazyload']), + '#lazyload_aspect_ratio' => !empty($variables['lazyload_aspect_ratio']), + ); + if (isset($item['title']) && drupal_strlen($item['title']) != 0) { + $responsive_image['#title'] = $item['title']; + } + if (!empty($item['class'])) { + $class = (!empty($responsive_image['#attributes']['class'])) ? $responsive_image['#attributes']['class'] : array(); + if (!is_array($class)) { + $class = array($class); + } + $responsive_image['#attributes']['class'] = $class + $item['class']; + } + if (is_array($variables['path']) && isset($variables['path']['path'])) { + $path = $variables['path']['path']; + $options = isset($variables['path']['options']) ? $variables['path']['options'] : array(); + $options['html'] = TRUE; + return l(drupal_render($responsive_image), $path, $options); + } + + return drupal_render($responsive_image); +} + +/** + * Theme pictue sizes formatter. + */ +function theme_picture_sizes_formatter($variables) { + $output_method = variable_get('picture_img_sizes_output_method', 'picture_element'); + $variables['image_style'] = $variables['fallback_image_style']; + + if ($output_method == 'picture_element') { + $variables['breakpoints'][BREAKPOINTS_SOURCE_TYPE_MODULE . '.picture.empty_srcset']['1x'] = array( + 'mapping_type' => 'sizes', + 'sizes' => $variables['sizes'], + 'sizes_image_styles' => $variables['image_styles'], + 'lazyload' => !empty($variables['lazyload']), + 'lazyload_aspect_ratio' => !empty($variables['lazyload_aspect_ratio']), + ); + return theme('picture_formatter', $variables); + } + + if ($output_method == 'img_srcset') { + $image = $variables['item']; + + // Map image attributes. + $variables['uri'] = _picture_image_style_url($variables['fallback_image_style'], $image['uri']); + + if (!empty($image['alt'])) { + $variables['alt'] = $image['alt']; + } + if (!empty($image['title'])) { + $variables['title'] = $image['title']; + } + + // Set up image source options. + $variables['srcset'] = array(); + // Get the dimensions of the original image. + $image_info = image_get_info($image['uri']); + foreach ($variables['image_styles'] as $image_style) { + $image_style_uri = _picture_image_style_url($image_style, $image['uri']); + $dimensions = array( + 'width' => $image_info['width'], + 'height' => $image_info['height'], + ); + // Let the the image style transform the dimensions. This way we avoid the + // performance cost of actually having to generate the derivative. The + // image style can transform the dimensions without the derivative having + // to exist. + $dimensions = picture_get_image_dimensions($image_style, $dimensions); + + $variables['srcset'][] = array( + 'uri' => $image_style_uri, + 'width' => $dimensions['width'] . 'w', + ); + } + return theme('image_srcset', $variables); + } +} + +/** + * Theme function to add support for colorbox. + */ +function theme_picture_formatter_colorbox($variables) { + if (!isset($variables['breakpoints']) || empty($variables['breakpoints'])) { + return theme('image_formatter', $variables); + } + + $item = $variables['item']; + + // Do not output an empty 'title' attribute. + if (isset($item['title']) && drupal_strlen($item['title']) == 0) { + unset($item['title']); + } + + $item['style_name'] = $variables['image_style']; + $item['breakpoints'] = $variables['breakpoints']; + + if (!isset($item['path']) && isset($variables['uri'])) { + $item['path'] = $variables['uri']; + } + $output = theme('picture', $item); + + // If an error happened skip further processing. + if (empty($output)) { + return $output; + } + + if (isset($variables['colorbox_group'])) { + $item['breakpoints'] = $variables['colorbox_group']; + $item['style_name'] = $variables['colorbox_image_style']; + $id = 'picture-colorbox-' . user_password(); + $colorbox = '
' . theme('picture', $item) . '
'; + + $options = array( + 'attributes' => array('class' => array('colorbox-inline')), + 'query' => array( + 'maxWidth' => '80%', + 'maxHeight' => '80%', + 'inline' => 'true', + ), + 'fragment' => $id, + 'html' => TRUE, + ); + + if (!empty($variables['colorbox_group_id'])) { + $options['attributes']['rel'] = $variables['colorbox_group_id']; + } + if (!empty($variables['colorbox_caption'])) { + $options['query']['title'] = $variables['colorbox_caption']; + } + + // Do not load picture automatically. + $colorbox = str_replace('', '', $colorbox); + $colorbox = str_replace(' srcset="', ' data-srcset="', $colorbox); + + $output = l($output, current_path(), $options) . $colorbox; + } + return $output; +} + +/** + * Returns HTML for a picture. + * + * @param array $variables + * An associative array containing: + * - uri: Either the path of the image file (relative to base_path()) or a + * full URL. + * - width: The width of the image (if known). + * - height: The height of the image (if known). + * - alt: The alternative text for text-based browsers. + * - breakpoints: An array containing breakpoints. + * - attributes: An array containing attributes. + * + * @ingroup themeable + */ +function theme_picture(array $variables) { + + $image_styles = image_styles(); + // Make sure that width and height are proper values. + // If they exists we'll output them. + // @see http://www.w3.org/community/respimg/2012/06/18/florians-compromise/ + if (isset($variables['width']) && empty($variables['width'])) { + unset($variables['width']); + unset($variables['height']); + } + elseif (isset($variables['height']) && empty($variables['height'])) { + unset($variables['width']); + unset($variables['height']); + } + if (isset($variables['metadata']['width']) && isset($variables['metadata']['height'])) { + $variables['width'] = $variables['metadata']['width']; + $variables['height'] = $variables['metadata']['height']; + } + + $sources = array(); + $output = array(); + // If we know width and height we use them, if not we should get image info. + if (!empty($variables['width']) && !empty($variables['height'])) { + $dimensions = array( + 'width' => $variables['width'], + 'height' => $variables['height'], + ); + } + else { + if (($image_info = image_get_info($variables['uri'])) === FALSE) { + watchdog('Picture', 'Unable to load image: %image', array('%image' => $variables['uri'])); + // If the image couldn't be loaded return nothing. + return NULL; + } + $dimensions = array( + 'width' => $image_info['width'], + 'height' => $image_info['height'], + ); + } + + // All breakpoints and multipliers. + foreach ($variables['breakpoints'] as $breakpoint_name => $multipliers) { + $breakpoint = breakpoints_breakpoint_load_by_fullkey($breakpoint_name); + if ($breakpoint) { + $sizes = array(); + $srcset = array(); + + foreach ($multipliers as $multiplier => $mapping_definition) { + switch ($mapping_definition['mapping_type']) { + case 'sizes': + foreach (array_filter($mapping_definition['sizes_image_styles']) as $image_style_name) { + $dimensions_clone = $dimensions; + picture_get_image_dimensions($image_style_name, $dimensions_clone); + // Get mime type. + // $derivative_mime_type = $mime_type; + // $image_style->transformMimeType($derivative_mime_type); + // $derivative_mime_types[] = $derivative_mime_type; + $srcset[$dimensions_clone['width']] = _picture_image_style_url($image_style_name, $variables['uri'], $variables['timestamp']) . ' ' . $dimensions_clone['width'] . 'w'; + $sizes = array_merge(explode(',', $mapping_definition['sizes']), $sizes); + } + break; + + case 'image_style': + // Get mime type (not implemented in Drupal 7. + // $derivative_mime_type = $mime_type; + // $image_style->transformMimeType($derivative_mime_type); + // $derivative_mime_types[] = $derivative_mime_type; + $srcset[$multiplier] = _picture_image_style_url($mapping_definition['image_style'], $variables['uri'], $variables['timestamp']) . ' ' . $multiplier; + break; + } + } + + // Sort srcset. + ksort($srcset); + + $aspect_ratio = ''; + if (!empty($variables['lazyload_aspect_ratio']) && !empty($image_styles[$mapping_definition['image_style']])) { + $dimensions = array( + 'width' => $variables['width'], + 'height' => $variables['height'], + ); + $dimensions = picture_get_image_dimensions($mapping_definition['image_style'], $dimensions); + // store as a string, JS plugin will do calculation + $aspect_ratio = $dimensions['width'] . '/' . $dimensions['height']; + } + + $sources[] = array( + '#theme' => 'picture_source', + '#srcset' => implode(', ', array_unique($srcset)), + '#media' => $breakpoint->breakpoint, + '#sizes' => implode(', ', array_unique($sizes)), + '#lazyload' => !empty($variables['lazyload']), + '#lazyload_aspect_ratio' => $aspect_ratio, + ); + } + } + $attributes = array(); + foreach (array('alt', 'title', 'attributes') as $key) { + $field = sprintf('field_file_image_%s_text', $key); + if (isset($variables[$key]) && !empty($variables[$key])) { + if ($key == 'attributes') { + $attributes += $variables[$key]; + } + else { + $attributes[$key] = $variables[$key]; + } + } + elseif (isset($variables[$field]) && is_array($variables[$field]) && isset($variables[$field][LANGUAGE_NONE][0]['safe_value'])) { + if ($key == 'attributes') { + $attributes += $variables[$field][LANGUAGE_NONE][0]['safe_value']; + } + else { + $attributes[$key] = $variables[$field][LANGUAGE_NONE][0]['safe_value']; + } + } + } + if (!empty($sources)) { + $output[] = ' ''))) . '>'; + $output[] = ''; + $output = array_merge($output, array_map('drupal_render', $sources)); + $output[] = ''; + $src = _picture_image_style_url($variables['style_name'], $variables['uri'], $variables['timestamp']); + + if (!empty($variables['lazyload'])) { + if (!isset($attributes['class'])) { + $attributes['class'] = array(); + } + elseif (!is_array($attributes['class'])) { + $attributes['class'] = (array)$attributes['class']; + } + $attributes['class'][] = 'lazyload'; + } + + if (!empty($variables['lazyload_aspect_ratio'])) { + // img tag has to have data-aspectratio attribute even if its dummy placeholder + $attributes['data-aspectratio'] = ''; + } + + if (variable_get('picture_fallback_method', 'src') === 'src') { + $min_ie9_fallback = array( + '#theme' => 'image_srcset', + '#uri' => $src, + '#alt' => isset($attributes['alt']) ? $attributes['alt'] : '', + '#title' => isset($attributes['title']) ? $attributes['title'] : '', + '#attributes' => array_diff_key($attributes, array('alt' => '', 'title' => '')), + ); + $output[] = drupal_render($min_ie9_fallback); + } + else { + // Fallback image for < IE9. + $min_ie9_fallback = array( + '#theme' => 'image_srcset', + '#uri' => $src, + '#alt' => isset($attributes['alt']) ? $attributes['alt'] : '', + '#title' => isset($attributes['title']) ? $attributes['title'] : '', + '#attributes' => array_diff_key($attributes, array('alt' => '', 'title' => '')), + ); + $output[] = ''; + + // Fallback image for > IE8. + $srcset = array( + 'uri' => $src, + ); + $dimensions_clone = $dimensions; + picture_get_image_dimensions($variables['style_name'], $dimensions_clone); + if (!empty($dimensions_clone['width'])) { + $srcset['width'] = $dimensions_clone['width'] . 'w'; + } + $plus_ie8_fallback = array( + '#theme' => 'image_srcset', + '#srcset' => array($srcset), + '#alt' => isset($attributes['alt']) ? $attributes['alt'] : '', + '#title' => isset($attributes['title']) ? $attributes['title'] : '', + '#attributes' => array_diff_key($attributes, array('alt' => '', 'title' => '')), + ); + $output[] = ''; + $output[] = drupal_render($plus_ie8_fallback); + $output[] = ''; + } + + $output[] = ''; + + return implode("\n", $output); + } +} + +/** + * Returns HTML for an image. + * + * @param array $variables + * An associative array containing: + * - path: Either the path of the image file (relative to base_path()) or a + * full URL. + * - srcset: Array of multiple URI's and sizes/multipliers. + * - sizes: The value for the sizes attribute. + * - width: The width of the image (if known). + * - height: The height of the image (if known). + * - alt: The alternative text for text-based browsers. HTML 4 and XHTML 1.0 + * always require an alt attribute. The HTML 5 draft allows the alt + * attribute to be omitted in some cases. Therefore, this variable defaults + * to an empty string, but can be set to NULL for the attribute to be + * omitted. Usually, neither omission nor an empty string satisfies + * accessibility requirements, so it is strongly encouraged for code + * calling theme('image') to pass a meaningful value for this variable. + * - http://www.w3.org/TR/REC-html40/struct/objects.html#h-13.8 + * - http://www.w3.org/TR/xhtml1/dtds.html + * - http://dev.w3.org/html5/spec/Overview.html#alt + * - title: The title text is displayed when the image is hovered in some + * popular browsers. + * - attributes: Associative array of attributes to be placed in the img tag. + */ +function theme_image_srcset(array $variables) { + // Sizes is mandatory, so make sure the key is set, default is 100vw. + if (!isset($variables['sizes']) || empty($variables['sizes'])) { + $attributes['sizes'] = '100vw'; + } + + // Make sure we have an array. + $attributes = isset($variables['attributes']) ? $variables['attributes'] : array(); + + if (isset($variables['uri']) && !empty($variables['uri'])) { + // TODO: remove when https://www.drupal.org/node/1342504 is fixed. + if (strpos($variables['uri'], 'data:') === 0) { + $attributes['src'] = $variables['uri']; + } + else { + $attributes['src'] = file_create_url($variables['uri']); + } + } + if (isset($variables['srcset']) && !empty($variables['srcset'])) { + $srcsets = array(); + foreach ($variables['srcset'] as $src) { + // URI is mandatory. + if (isset($src['uri']) && !empty($src['uri'])) { + $key = ''; + // TODO: remove when https://www.drupal.org/node/1342504 is fixed. + if (strpos($src['uri'], 'data:') === 0) { + $source = $src['uri']; + } + else { + $source = file_create_url($src['uri']); + } + if (isset($src['width']) && !empty($src['width'])) { + $source .= ' ' . $src['width']; + $key = (int) drupal_substr($src['width'], 0, -1); + } + elseif (isset($src['multiplier']) && !empty($src['multiplier'])) { + $source .= ' ' . $src['multiplier']; + $key = (int) ((float) drupal_substr($src['multiplier'], -1) * 100); + } + $srcsets[$key] = $source; + } + } + ksort($srcsets); + $attributes['srcset'] = implode(', ', $srcsets); + } + foreach (array('width', 'height', 'alt', 'title', 'sizes') as $key) { + if (isset($variables[$key])) { + $attributes[$key] = $variables[$key]; + } + } + if (isset($variables['path']) && !empty($variables['path'])) { + return l('', $variables['path']['path'], array('html' => TRUE) + $variables['path']['options']); + } + return ''; +} + +/** + * Returns HTML for a source tag. + * + * @param array $variables + * An associative array containing: + * - media: The media query to use. + * - src: Either the path of the image file (relative to base_path()) or a + * full URL. + * - dimensions: The width and height of the image (if known). + * + * @ingroup themeable + */ +function theme_picture_source(array $variables) { + + if (!empty($variables['lazyload'])) { + $attributes['data-srcset'] = $variables['srcset']; + if (!empty($variables['lazyload_aspect_ratio'])) { + $attributes['data-aspectratio'] = $variables['lazyload_aspect_ratio']; + } + } + else { + $attributes['srcset'] = $variables['srcset']; + } + + if (isset($variables['media']) && !empty($variables['media'])) { + $attributes['media'] = $variables['media']; + } + + if (isset($variables['mime_type']) && !empty($variables['mime_type'])) { + $attributes['type'] = $variables['mime_type']; + } + if (isset($variables['sizes']) && !empty($variables['sizes'])) { + $attributes['sizes'] = $variables['sizes']; + } + return ''; +} + +/** + * Determines the dimensions of an image. + * + * @param string $image_style_name + * The name of the style to be used to alter the original image. + * @param array $dimensions + * Dimensions to be modified - an array with components width and height, in + * pixels. + */ +function picture_get_image_dimensions($image_style_name, array &$dimensions) { + if ($image_style_name == PICTURE_EMPTY_IMAGE) { + $dimensions = array( + 'width' => 1, + 'height' => 1, + ); + } + elseif ($image_style_name == PICTURE_ORIGINAL_IMAGE) { + // NOOP. + } + else { + image_style_transform_dimensions($image_style_name, $dimensions); + } + + return $dimensions; +} + +/** + * Implements hook_filter_info(). + */ +function picture_filter_info() { + $filters = array(); + $filters['picture'] = array( + 'title' => t('Make images responsive with the picture module'), + 'description' => t('Replace img tags with markup that contains media width breakpoints. The appropriate image file size will be chosen.'), + 'process callback' => '_picture_filter_process', + 'tips callback' => '_picture_filter_tips', + 'type' => 'FILTER_TYPE_TRANSFORM_REVERSIBLE', + ); + + return $filters; +} + +/** + * Process callback for inline image filter. + */ +function _picture_filter_process($text, $filter) { + + // Find all img tags with a data-picture-mapping attribute. + preg_match_all('//i', $text, $images1); + preg_match_all('//i', $text, $images2); + $images = array_merge($images1[0], $images2[0]); + if (!empty($images)) { + foreach ($images as $image) { + // Create the render array expected by theme_picture_formatter. + $image_render_array = _picture_filter_prepare_image($image); + if (!$image_render_array) { + continue; + } + + // Get the responsive markup for this image. + $new_markup = drupal_render($image_render_array); + + // Replace the original img tag with the responsive markup. + $text = str_replace($image, $new_markup, $text); + } + } + return $text; +} + +/** + * Prepares a Render Array for theme_picture_formatter(). + * + * It is similar to picture_field_formatter_view() + * with modifications for inline images. + * + * @param string $image + * An img tag. + * + * @see picture_field_formatter_view() + */ +function _picture_filter_prepare_image($image) { + // Make sure the closing tag is right. + $image = str_replace('/>', '>', $image); + $image = str_replace('>', ' />', $image); + $image = str_replace(" ", '', $image); + $image = htmlspecialchars($image); + + // Parse the tag as xml. + $xml = simplexml_load_string('' . html_entity_decode($image, ENT_QUOTES, "utf-8") . ''); + if (isset($xml->img[0]) && is_object($xml->img[0])) { + $attributes = array(); + foreach ($xml->img[0]->attributes() as $a => $b) { + $attributes[$a] = (string) $b; + } + } + + $fallback_image_style = ''; + if (isset($attributes['data-picture-mapping'])) { + $mapping_id = $attributes['data-picture-mapping']; + } + elseif (isset($attributes['data-picture-group'])) { + $mapping_id = $attributes['data-picture-group']; + } + else { + $mapping_id = null; + } + $mappings = picture_mapping_load($mapping_id); + // Make sure we have valid mappings. + if (empty($mappings)) { + return FALSE; + } + $breakpoint_styles = picture_get_mapping_breakpoints($mappings, $fallback_image_style); + + // Make sure we have a src attribute. + if (!isset($attributes['src'])) { + return FALSE; + } + $src = $attributes['src']; + unset($attributes['src']); + $alt = isset($attributes['alt']) ? $attributes['alt'] : ''; + unset($attributes['alt']); + $title = isset($attributes['title']) ? $attributes['title'] : ''; + unset($attributes['title']); + + // Make sure we have map src to uri. + $uri = picture_image_uri($src); + if (!$uri) { + return FALSE; + } + + $image_info = image_get_info($uri); + if (!$image_info) { + // It's not an image. + return FALSE; + } + $picture_mappings = variable_get('picture_ckeditor_mappings', array()); + $image_render_array = array( + '#theme' => 'picture', + '#style_name' => $picture_mappings[$mapping_id]['fallback'], + '#uri' => $uri, + '#width' => $image_info['width'], + '#height' => $image_info['height'], + '#alt' => $alt, + '#title' => $title, + '#attributes' => array( + 'data-picture-mapping' => $mapping_id, + ) + $attributes, + '#breakpoints' => $breakpoint_styles, + '#lazyload' => isset($picture_mappings[$mapping_id]['lazyload']) ? $picture_mappings[$mapping_id]['lazyload'] : FALSE, + '#lazyload_aspect_ratio' => isset($picture_mappings[$mapping_id]['lazyload_aspect_ratio']) ? $picture_mappings[$mapping_id]['lazyload_aspect_ratio'] : FALSE, + ); + return $image_render_array; +} + +/** + * Implements callback_filter_tips(). + */ +function _picture_filter_tips($filter, $format, $long = FALSE) { + return t('Images with a data-picture-mapping attribute will be responsive, with a file size appropriate for the browser width.'); +} + +/** + * Implements hook_page_build(). + * + * Add the image processing javascript to every page. This allows these scripts + * to get included in aggregation, which is probably good since there will be + * pictures needing this javascript on most pages. The library does not get + * added twice, even if it's attached to multiple fields that are also being + * displayed with responsive images. Maybe this should check that the + * page is not an admin theme page? + */ +function picture_page_build(&$page) { + drupal_add_library('picture', 'picturefill_head', TRUE); + drupal_add_library('picture', 'picturefill', TRUE); + drupal_add_library('picture', 'picture.ajax', TRUE); + + // Integrate with the WYSIWYG module, and the CKEditor module. + $picture_mappings = picture_get_mapping_options(); + $ckeditor_mappings = variable_get('picture_ckeditor_mappings', array()); + $mappings = array(); + + // CKEditor library expects an array of options formatted as + // ['Display name', 'machine_name']. + foreach ($ckeditor_mappings as $machine_name => $parameters) { + if (array_key_exists($machine_name, $picture_mappings)) { + if ($parameters['enabled'] == 1) { + $mappings[] = array($picture_mappings[$machine_name], $machine_name); + if (!empty($ckeditor_mappings[$machine_name]['lazyload'])) { + drupal_add_library('picture', 'lazysizes'); + + if (!empty($ckeditor_mappings[$machine_name]['lazyload_aspect_ratio'])) { + drupal_add_library('picture', 'lazysizes_aspect_ratio'); + }; + + }; + } + } + } + + if (!empty($mappings)) { + $mappings[] = array('Not Set', 'not_set'); + drupal_add_js(array( + 'picture' => array( + 'mappings' => $mappings, + 'label' => variable_get('picture_ckeditor_label', 'Image size (required)'), + )), 'setting'); + } +} + +/** + * Helper function to figure out the uri of an image source. + * + * @param string $src + * Image src starting with http://, https://, or root relative /. + */ +function picture_image_uri($src) { + foreach (module_implements('picture_image_uri') as $module) { + $function = $module . '_picture_image_uri'; + if ($uri = $function($src)) { + $uri = file_stream_wrapper_uri_normalize($uri); + return urldecode($uri); + } + } + return FALSE; +} + +/** + * Implements hook_picture_image_uri(). + */ +function picture_picture_image_uri($src) { + global $base_path; + + $uri = ''; + // Prepare the src by removing http:// or https://. + $src = parse_url($src, PHP_URL_PATH); + // Remove leading or trailing slashes. + $src = trim($src, '/'); + + // List all visible stream wrappers. + $visible_stream_wrappers = array_intersect_key(file_get_stream_wrappers(STREAM_WRAPPERS_WRITE), file_get_stream_wrappers(STREAM_WRAPPERS_VISIBLE)); + $needles = array(); + $matches = array(); + foreach ($visible_stream_wrappers as $scheme => $data) { + $class = file_stream_wrapper_get_class($scheme); + $stream_wrapper = new $class(); + // Trim leading or trailing slashes since the Directory could be root + // relative. + if (method_exists($stream_wrapper, 'getDirectoryPath')) { + $needles[$scheme] = trim($base_path . $stream_wrapper->getDirectoryPath(), '/'); + + // Check whether the file stream directory is at the beginning of + // the image src. Use === since strpos could return false. + if (!empty($needles[$scheme]) && strpos($src, $needles[$scheme]) === 0) { + $matches[$scheme] = $needles[$scheme]; + } + } + } + + // No file stream wrapper is starting with the image src. + // So it's not a public file. + if (empty($matches)) { + // Check for managed/private file with relative path like system/files/.. + $src_exploded = explode('/', $src); + if ($src_exploded[0] == 'system' && $src_exploded[1] == 'files') { + // Managed/private file recognized. + // Check for image style derivatives. + if ($src_exploded[2] == 'styles') { + // Image style recognized. + $unwanted_part = 'system/files/styles/' . $src_exploded[3] . '/private/'; + $uri = str_replace($unwanted_part, 'private://', $src); + $uri = file_stream_wrapper_uri_normalize($uri); + return urldecode($uri); + } + else { + // No image style recognized; must be an original. + $unwanted_part = 'system/files/'; + $uri = str_replace($unwanted_part, 'private://', $src); + $uri = file_stream_wrapper_uri_normalize($uri); + return urldecode($uri); + } + } + else { + // Can't figure out the Drupal uri. + return FALSE; + } + } + + // If one file scheme directory is a subdirectory of another file + // scheme directory, choose the longer one. This issue is possible with + // the following scenario: + // public file dir: /sites/default/files/ + // private file dir: /sites/default/files/private/ + // image src: /sites/default/files/private/the-image.jpg + // In this example, the intended scheme would be 'private'. + if (empty($matches)) { + // Can't figure out the Drupal uri. + return FALSE; + } + + // Find the length of each matching directory path. + $lengths = array_map('strlen', $matches); + + // Determine the key of the longest one. + $the_scheme = array_search(max($lengths), $lengths); + + // Construct the Drupal uri. + $uri = $the_scheme . '://' . str_replace($matches[$the_scheme], '', $src); + return $uri; +} + +/** + * Implements hook_wysiwyg_plugin(). + * + * Modify the CKEditor image dialog for use with the picture module. + */ +function picture_wysiwyg_plugin($editor, $version) { + if ($editor == 'ckeditor') { + return array( + 'picture_ckeditor' => array( + 'path' => drupal_get_path('module', 'picture') . '/ckeditor/plugins/', + 'load' => TRUE, + 'extensions' => array('picture_ckeditor' => t('Responsive images with the Picture Module')), + 'url' => 'https://www.drupal.org/project/picture', + ), + ); + } +} + +/** + * Implements hook_ckeditor_plugin(). + * + * Modify the CKEditor image dialog for use with the picture module. + */ +function picture_ckeditor_plugin() { + return array( + 'picture_ckeditor' => array( + 'name' => 'picture_ckeditor', + 'desc' => t('Support responsive images with the Picture module.'), + 'buttons' => FALSE, + 'path' => drupal_get_path('module', 'picture') . '/ckeditor/plugins/', + 'default' => 't', + ), + ); +} + +/** + * Wrapper around image_style_url() so we can return an empty image. + */ +function _picture_image_style_url($style_name, $path, $timestamp = NULL) { + if ($style_name == PICTURE_EMPTY_IMAGE) { + return 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; + } + if ($style_name == PICTURE_ORIGINAL_IMAGE) { + $url = file_create_url($path); + } + else { + $url = image_style_url($style_name, $path); + } + + if (!empty($timestamp)) { + $url .= (strpos($url, '?') !== FALSE ? '&' : '?') . drupal_http_build_query(array('timestamp' => $timestamp)); + } + return $url; +} + +/** + * Implements hook_wysiwyg_editor_settings_alter(). + */ +function picture_wysiwyg_editor_settings_alter(&$settings, $context) { + if ($context['profile']->editor == 'ckeditor') { + if (!isset($settings['extraAllowedContent'])) { + $settings['extraAllowedContent'] = array( + 'img[src,title,alt,style,width,height,class,hspace,vspace,view_mode,format,fid,data-picture-mapping,data-picture-group,data-picture-align]', + ); + } + else { + // @todo: try finding the img entry and add data- attributes if needed. + } + } +} + +/** + * Returns a list with the image styles of a mapping configuration. + * + * @param object $picture_mapping + * The mapping configuration. + * @param string $fallback_image_style + * Reference to access the evaluated fallback image style. + * + * @return array + * List with the image styles of a mapping configuration. + * The array has following structure: + * array( + * breakpoint_name => array( + * multiplier => array( + * mapping_type => _none|sizes|image_style, + * image_style => image_style_name, + * sizes => '(min-width: 700px) 700px, 100vw', + * sizes_image_styles => array( + * thumbnail => thumbnail, + * medium => medium, + * large => large, + * ), + * ), + * ), + * ); + */ +function picture_get_mapping_breakpoints(PictureMapping $picture_mapping, &$fallback_image_style = NULL) { + $breakpoint_styles = array(); + $image_styles = array(); + foreach ($picture_mapping->getMappings() as $breakpoint_name => $multipliers) { + // Make sure there are multipliers. + if (!empty($multipliers)) { + $breakpoint = breakpoints_breakpoint_load_by_fullkey($breakpoint_name); + if ($breakpoint && $breakpoint->status) { + // Determine the enabled multipliers. + $multipliers = array_intersect_key($multipliers, $breakpoint->multipliers); + foreach ($multipliers as $multiplier => $mapping_definition) { + // Make sure the multiplier still exists. + if (!PictureMapping::isEmptyMappingDefinition($mapping_definition)) { + if ($mapping_definition['mapping_type'] == 'image_style') { + $image_styles[] = $mapping_definition['image_style']; + } + elseif ($mapping_definition['mapping_type'] == 'sizes') { + $mapping_definition['sizes_image_styles'] = array_filter($mapping_definition['sizes_image_styles']); + $image_styles = array_merge($image_styles, $mapping_definition['sizes_image_styles']); + } + if (!isset($breakpoint_styles[$breakpoint_name])) { + $breakpoint_styles[$breakpoint_name] = array(); + } + $breakpoint_styles[$breakpoint_name][$multiplier] = $mapping_definition; + } + } + } + } + } + + // Check if the user defined a custom fallback image style. + if (!$fallback_image_style) { + $fallback_image_style = end($image_styles); + } + return $breakpoint_styles; +} + +/** + * Helper function to compute the list of possible link types. + */ +function picture_link_types($instance) { + $link_types = array( + 'content' => t('Content'), + 'file' => t('File'), + ); + + if (module_exists('colorbox')) { + $link_types['colorbox'] = t('Colorbox'); + } + + // If the link module is installed, also allow any link fields to be used. + foreach (field_info_fields() as $field_key => $field_info) { + if ($field_info['type'] == 'link_field') { + $field_instance = field_info_instance($instance['entity_type'], $field_info['field_name'], $instance['bundle']); + if ($field_instance) { + $link_types[$field_key] = "$field_instance[label] ($field_info[field_name])"; + } + } + } + + // Allow linking to a specific image style. + $image_styles = image_style_options(FALSE); + foreach ($image_styles as $image_style => $label) { + $link_types[$image_style] = t('Image style: @style', array('@style' => $label)); + } + return $link_types; +} + +/** + * Picture mapping factory. + */ +function picture_mapping_object_factory($schema, $data) { + if (!class_exists('PictureMapping')) { + module_load_include('php', 'picture', 'includes/PictureMapping'); + } + $mapping = new PictureMapping(); + $mapping->setValues($schema, $data); + return $mapping; +} + +/** + * Create picture mapping. + */ +function picture_mapping_create($set_defaults) { + $schema = ctools_export_get_schema('picture_mapping'); + $data = ctools_export_new_object('picture_mapping', $set_defaults); + return picture_mapping_object_factory($schema, $data); +} + +/** + * Export picture mapping. + */ +function picture_mapping_export(PictureMapping $mapping, $indent) { + return $mapping->export($indent); +} + +/** + * Implements hook_ctools_plugin_api(). + */ +function picture_ctools_plugin_api($module = NULL, $api = NULL) { + if ($module == "breakpoints" && $api == "default_breakpoints") { + return array("version" => "1"); + } +} + +/** + * Implements hook_default_breakpoints(). + */ +function picture_default_breakpoints() { + $export = array(); + + $breakpoint = new stdClass(); + $breakpoint->disabled = FALSE; /* Edit this to true to make a default breakpoint disabled initially */ + $breakpoint->api_version = 1; + $breakpoint->machine_name = BREAKPOINTS_SOURCE_TYPE_MODULE . '.picture.empty_srcset'; + $breakpoint->name = 'Empty srcset'; + $breakpoint->breakpoint = ''; + $breakpoint->source = 'picture'; + $breakpoint->source_type = BREAKPOINTS_SOURCE_TYPE_MODULE; + $breakpoint->status = 1; + $breakpoint->weight = 0; + $breakpoint->multipliers = array( + '1x' => '1x', + ); + $export[BREAKPOINTS_SOURCE_TYPE_MODULE . '.picture.empty_srcset'] = $breakpoint; + + return $export; +} diff --git a/frontend/drupal/sites/all/modules/picture/picture_colorbox.css b/frontend/drupal/sites/all/modules/picture/picture_colorbox.css new file mode 100644 index 000000000..329e6b15f --- /dev/null +++ b/frontend/drupal/sites/all/modules/picture/picture_colorbox.css @@ -0,0 +1,10 @@ +.picture-colorbox-container { + max-width: 100%; + max-height: 100%; + overflow: hidden; + height: 100%; +} +.picture-colorbox-container img { + max-width: 100%; + max-height: 100%; +} diff --git a/frontend/drupal/sites/all/modules/picture/picture_colorbox.js b/frontend/drupal/sites/all/modules/picture/picture_colorbox.js new file mode 100644 index 000000000..219b2a1b8 --- /dev/null +++ b/frontend/drupal/sites/all/modules/picture/picture_colorbox.js @@ -0,0 +1,39 @@ +(function ($) { + Drupal.behaviors.pictureLazyloadPictures = { + attach: function (context, settings) { + $(context).bind('cbox_load', function () { + var href = $.colorbox.element()[0].hash; + + if (href.search('#picture-colorbox-') === 0) { + href = '.' + href.substr(1, href.length) + ', #' + href.substr(1, href.length); + } + var $target = $(href); + $('span[lazyload]', $target).replaceWith(function () { + var picture = $('
').append($(this).clone()).html(); + + picture = picture.replace(//ig, ''); + picture = picture.replace(/ data-srcset="/ig, ' srcset="'); + $('img', picture).load(function() { + // Ensure there's no max-width / max-height otherwise we won't get + // the proper values. We could use naturalWeight / naturalHeight + // but that's not supported by -1 === false && ( parseFloat( length ) > 0 || length.indexOf( "calc(" ) > -1 )) ) { + return false; + } + + /** + * If length is specified in `vw` units, use `%` instead since the div we’re measuring + * is injected at the top of the document. + * + * TODO: maybe we should put this behind a feature test for `vw`? The risk of doing this is possible browser inconsistancies with vw vs % + */ + length = length.replace( "vw", "%" ); + + // Create a cached element for getting length value widths + if ( !pf.lengthEl ) { + pf.lengthEl = doc.createElement( "div" ); + + // Positioning styles help prevent padding/margin/width on `html` or `body` from throwing calculations off. + pf.lengthEl.style.cssText = "border:0;display:block;font-size:1em;left:0;margin:0;padding:0;position:absolute;visibility:hidden"; + + // Add a class, so that everyone knows where this element comes from + pf.lengthEl.className = "helper-from-picturefill-js"; + } + + pf.lengthEl.style.width = "0px"; + + try { + pf.lengthEl.style.width = length; + } catch ( e ) {} + + doc.body.appendChild(pf.lengthEl); + + cssValue = pf.lengthEl.offsetWidth; + + if ( cssValue <= 0 ) { + cssValue = false; + } + + doc.body.removeChild( pf.lengthEl ); + + return cssValue; + }; + + pf.detectTypeSupport = function( type, typeUri ) { + // based on Modernizr's lossless img-webp test + // note: asynchronous + var image = new w.Image(); + image.onerror = function() { + pf.types[ type ] = false; + picturefill(); + }; + image.onload = function() { + pf.types[ type ] = image.width === 1; + picturefill(); + }; + image.src = typeUri; + + return "pending"; + }; + // container of supported mime types that one might need to qualify before using + pf.types = pf.types || {}; + + pf.initTypeDetects = function() { + // Add support for standard mime types + pf.types[ "image/jpeg" ] = true; + pf.types[ "image/gif" ] = true; + pf.types[ "image/png" ] = true; + pf.types[ "image/svg+xml" ] = doc.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#Image", "1.1"); + pf.types[ "image/webp" ] = pf.detectTypeSupport("image/webp", "data:image/webp;base64,UklGRh4AAABXRUJQVlA4TBEAAAAvAAAAAAfQ//73v/+BiOh/AAA="); + }; + + pf.verifyTypeSupport = function( source ) { + var type = source.getAttribute( "type" ); + // if type attribute exists, return test result, otherwise return true + if ( type === null || type === "" ) { + return true; + } else { + var pfType = pf.types[ type ]; + // if the type test is a function, run it and return "pending" status. The function will rerun picturefill on pending elements once finished. + if ( typeof pfType === "string" && pfType !== "pending") { + pf.types[ type ] = pf.detectTypeSupport( type, pfType ); + return "pending"; + } else if ( typeof pfType === "function" ) { + pfType(); + return "pending"; + } else { + return pfType; + } + } + }; + + // Parses an individual `size` and returns the length, and optional media query + pf.parseSize = function( sourceSizeStr ) { + var match = /(\([^)]+\))?\s*(.+)/g.exec( sourceSizeStr ); + return { + media: match && match[1], + length: match && match[2] + }; + }; + + // Takes a string of sizes and returns the width in pixels as a number + pf.findWidthFromSourceSize = function( sourceSizeListStr ) { + // Split up source size list, ie ( max-width: 30em ) 100%, ( max-width: 50em ) 50%, 33% + // or (min-width:30em) calc(30% - 15px) + var sourceSizeList = pf.trim( sourceSizeListStr ).split( /\s*,\s*/ ), + winningLength; + + for ( var i = 0, len = sourceSizeList.length; i < len; i++ ) { + // Match ? length, ie ( min-width: 50em ) 100% + var sourceSize = sourceSizeList[ i ], + // Split "( min-width: 50em ) 100%" into separate strings + parsedSize = pf.parseSize( sourceSize ), + length = parsedSize.length, + media = parsedSize.media; + + if ( !length ) { + continue; + } + // if there is no media query or it matches, choose this as our winning length + if ( (!media || pf.matchesMedia( media )) && + // pass the length to a method that can properly determine length + // in pixels based on these formats: http://dev.w3.org/csswg/css-values-3/#length-value + (winningLength = pf.getWidthFromLength( length )) ) { + break; + } + } + + //if we have no winningLength fallback to 100vw + return winningLength || Math.max(w.innerWidth || 0, doc.documentElement.clientWidth); + }; + + pf.parseSrcset = function( srcset ) { + /** + * A lot of this was pulled from Boris Smus’ parser for the now-defunct WHATWG `srcset` + * https://github.com/borismus/srcset-polyfill/blob/master/js/srcset-info.js + * + * 1. Let input (`srcset`) be the value passed to this algorithm. + * 2. Let position be a pointer into input, initially pointing at the start of the string. + * 3. Let raw candidates be an initially empty ordered list of URLs with associated + * unparsed descriptors. The order of entries in the list is the order in which entries + * are added to the list. + */ + var candidates = []; + + while ( srcset !== "" ) { + srcset = srcset.replace( /^\s+/g, "" ); + + // 5. Collect a sequence of characters that are not space characters, and let that be url. + var pos = srcset.search(/\s/g), + url, descriptor = null; + + if ( pos !== -1 ) { + url = srcset.slice( 0, pos ); + + var last = url.slice(-1); + + // 6. If url ends with a U+002C COMMA character (,), remove that character from url + // and let descriptors be the empty string. Otherwise, follow these substeps + // 6.1. If url is empty, then jump to the step labeled descriptor parser. + + if ( last === "," || url === "" ) { + url = url.replace( /,+$/, "" ); + descriptor = ""; + } + srcset = srcset.slice( pos + 1 ); + + // 6.2. Collect a sequence of characters that are not U+002C COMMA characters (,), and + // let that be descriptors. + if ( descriptor === null ) { + var descpos = srcset.indexOf( "," ); + if ( descpos !== -1 ) { + descriptor = srcset.slice( 0, descpos ); + srcset = srcset.slice( descpos + 1 ); + } else { + descriptor = srcset; + srcset = ""; + } + } + } else { + url = srcset; + srcset = ""; + } + + // 7. Add url to raw candidates, associated with descriptors. + if ( url || descriptor ) { + candidates.push({ + url: url, + descriptor: descriptor + }); + } + } + return candidates; + }; + + pf.parseDescriptor = function( descriptor, sizesattr ) { + // 11. Descriptor parser: Let candidates be an initially empty source set. The order of entries in the list + // is the order in which entries are added to the list. + var sizes = sizesattr || "100vw", + sizeDescriptor = descriptor && descriptor.replace( /(^\s+|\s+$)/g, "" ), + widthInCssPixels = pf.findWidthFromSourceSize( sizes ), + resCandidate; + + if ( sizeDescriptor ) { + var splitDescriptor = sizeDescriptor.split(" "); + + for (var i = splitDescriptor.length - 1; i >= 0; i--) { + var curr = splitDescriptor[ i ], + lastchar = curr && curr.slice( curr.length - 1 ); + + if ( ( lastchar === "h" || lastchar === "w" ) && !pf.sizesSupported ) { + resCandidate = parseFloat( ( parseInt( curr, 10 ) / widthInCssPixels ) ); + } else if ( lastchar === "x" ) { + var res = curr && parseFloat( curr, 10 ); + resCandidate = res && !isNaN( res ) ? res : 1; + } + } + } + return resCandidate || 1; + }; + + /** + * Takes a srcset in the form of url/ + * ex. "images/pic-medium.png 1x, images/pic-medium-2x.png 2x" or + * "images/pic-medium.png 400w, images/pic-medium-2x.png 800w" or + * "images/pic-small.png" + * Get an array of image candidates in the form of + * {url: "/foo/bar.png", resolution: 1} + * where resolution is http://dev.w3.org/csswg/css-values-3/#resolution-value + * If sizes is specified, resolution is calculated + */ + pf.getCandidatesFromSourceSet = function( srcset, sizes ) { + var candidates = pf.parseSrcset( srcset ), + formattedCandidates = []; + + for ( var i = 0, len = candidates.length; i < len; i++ ) { + var candidate = candidates[ i ]; + + formattedCandidates.push({ + url: candidate.url, + resolution: pf.parseDescriptor( candidate.descriptor, sizes ) + }); + } + return formattedCandidates; + }; + + /** + * if it's an img element and it has a srcset property, + * we need to remove the attribute so we can manipulate src + * (the property's existence infers native srcset support, and a srcset-supporting browser will prioritize srcset's value over our winning picture candidate) + * this moves srcset's value to memory for later use and removes the attr + */ + pf.dodgeSrcset = function( img ) { + if ( img.srcset ) { + img[ pf.ns ].srcset = img.srcset; + img.srcset = ""; + img.setAttribute( "data-pfsrcset", img[ pf.ns ].srcset ); + } + }; + + // Accept a source or img element and process its srcset and sizes attrs + pf.processSourceSet = function( el ) { + var srcset = el.getAttribute( "srcset" ), + sizes = el.getAttribute( "sizes" ), + candidates = []; + + // if it's an img element, use the cached srcset property (defined or not) + if ( el.nodeName.toUpperCase() === "IMG" && el[ pf.ns ] && el[ pf.ns ].srcset ) { + srcset = el[ pf.ns ].srcset; + } + + if ( srcset ) { + candidates = pf.getCandidatesFromSourceSet( srcset, sizes ); + } + return candidates; + }; + + pf.backfaceVisibilityFix = function( picImg ) { + // See: https://github.com/scottjehl/picturefill/issues/332 + var style = picImg.style || {}, + WebkitBackfaceVisibility = "webkitBackfaceVisibility" in style, + currentZoom = style.zoom; + + if (WebkitBackfaceVisibility) { + style.zoom = ".999"; + + WebkitBackfaceVisibility = picImg.offsetWidth; + + style.zoom = currentZoom; + } + }; + + pf.setIntrinsicSize = (function() { + var urlCache = {}; + var setSize = function( picImg, width, res ) { + if ( width ) { + picImg.setAttribute( "width", parseInt(width / res, 10) ); + } + }; + return function( picImg, bestCandidate ) { + var img; + if ( !picImg[ pf.ns ] || w.pfStopIntrinsicSize ) { + return; + } + if ( picImg[ pf.ns ].dims === undefined ) { + picImg[ pf.ns].dims = picImg.getAttribute("width") || picImg.getAttribute("height"); + } + if ( picImg[ pf.ns].dims ) { return; } + + if ( bestCandidate.url in urlCache ) { + setSize( picImg, urlCache[bestCandidate.url], bestCandidate.resolution ); + } else { + img = doc.createElement( "img" ); + img.onload = function() { + urlCache[bestCandidate.url] = img.width; + + //IE 10/11 don't calculate width for svg outside document + if ( !urlCache[bestCandidate.url] ) { + try { + doc.body.appendChild( img ); + urlCache[bestCandidate.url] = img.width || img.offsetWidth; + doc.body.removeChild( img ); + } catch(e){} + } + + if ( picImg.src === bestCandidate.url ) { + setSize( picImg, urlCache[bestCandidate.url], bestCandidate.resolution ); + } + picImg = null; + img.onload = null; + img = null; + }; + img.src = bestCandidate.url; + } + }; + })(); + + pf.applyBestCandidate = function( candidates, picImg ) { + var candidate, + length, + bestCandidate; + + candidates.sort( pf.ascendingSort ); + + length = candidates.length; + bestCandidate = candidates[ length - 1 ]; + + for ( var i = 0; i < length; i++ ) { + candidate = candidates[ i ]; + if ( candidate.resolution >= pf.getDpr() ) { + bestCandidate = candidate; + break; + } + } + + if ( bestCandidate ) { + + bestCandidate.url = pf.makeUrl( bestCandidate.url ); + + if ( picImg.src !== bestCandidate.url ) { + if ( pf.restrictsMixedContent() && bestCandidate.url.substr(0, "http:".length).toLowerCase() === "http:" ) { + if ( window.console !== undefined ) { + console.warn( "Blocked mixed content image " + bestCandidate.url ); + } + } else { + picImg.src = bestCandidate.url; + // currentSrc attribute and property to match + // http://picture.responsiveimages.org/#the-img-element + if ( !pf.curSrcSupported ) { + picImg.currentSrc = picImg.src; + } + + pf.backfaceVisibilityFix( picImg ); + } + } + + pf.setIntrinsicSize(picImg, bestCandidate); + } + }; + + pf.ascendingSort = function( a, b ) { + return a.resolution - b.resolution; + }; + + /** + * In IE9, elements get removed if they aren't children of + * video elements. Thus, we conditionally wrap source elements + * using + * and must account for that here by moving those source elements + * back into the picture element. + */ + pf.removeVideoShim = function( picture ) { + var videos = picture.getElementsByTagName( "video" ); + if ( videos.length ) { + var video = videos[ 0 ], + vsources = video.getElementsByTagName( "source" ); + while ( vsources.length ) { + picture.insertBefore( vsources[ 0 ], video ); + } + // Remove the video element once we're finished removing its children + video.parentNode.removeChild( video ); + } + }; + + /** + * Find all `img` elements, and add them to the candidate list if they have + * a `picture` parent, a `sizes` attribute in basic `srcset` supporting browsers, + * a `srcset` attribute at all, and they haven’t been evaluated already. + */ + pf.getAllElements = function() { + var elems = [], + imgs = doc.getElementsByTagName( "img" ); + + for ( var h = 0, len = imgs.length; h < len; h++ ) { + var currImg = imgs[ h ]; + + if ( currImg.parentNode.nodeName.toUpperCase() === "PICTURE" || + ( currImg.getAttribute( "srcset" ) !== null ) || currImg[ pf.ns ] && currImg[ pf.ns ].srcset !== null ) { + elems.push( currImg ); + } + } + return elems; + }; + + pf.getMatch = function( img, picture ) { + var sources = picture.childNodes, + match; + + // Go through each child, and if they have media queries, evaluate them + for ( var j = 0, slen = sources.length; j < slen; j++ ) { + var source = sources[ j ]; + + // ignore non-element nodes + if ( source.nodeType !== 1 ) { + continue; + } + + // Hitting the `img` element that started everything stops the search for `sources`. + // If no previous `source` matches, the `img` itself is evaluated later. + if ( source === img ) { + return match; + } + + // ignore non-`source` nodes + if ( source.nodeName.toUpperCase() !== "SOURCE" ) { + continue; + } + // if it's a source element that has the `src` property set, throw a warning in the console + if ( source.getAttribute( "src" ) !== null && typeof console !== undefined ) { + console.warn("The `src` attribute is invalid on `picture` `source` element; instead, use `srcset`."); + } + + var media = source.getAttribute( "media" ); + + // if source does not have a srcset attribute, skip + if ( !source.getAttribute( "srcset" ) ) { + continue; + } + + // if there's no media specified, OR w.matchMedia is supported + if ( ( !media || pf.matchesMedia( media ) ) ) { + var typeSupported = pf.verifyTypeSupport( source ); + + if ( typeSupported === true ) { + match = source; + break; + } else if ( typeSupported === "pending" ) { + return false; + } + } + } + + return match; + }; + + function picturefill( opt ) { + var elements, + element, + parent, + firstMatch, + candidates, + options = opt || {}; + + elements = options.elements || pf.getAllElements(); + + // Loop through all elements + for ( var i = 0, plen = elements.length; i < plen; i++ ) { + element = elements[ i ]; + parent = element.parentNode; + firstMatch = undefined; + candidates = undefined; + + // immediately skip non-`img` nodes + if ( element.nodeName.toUpperCase() !== "IMG" ) { + continue; + } + + // expando for caching data on the img + if ( !element[ pf.ns ] ) { + element[ pf.ns ] = {}; + } + + // if the element has already been evaluated, skip it unless + // `options.reevaluate` is set to true ( this, for example, + // is set to true when running `picturefill` on `resize` ). + if ( !options.reevaluate && element[ pf.ns ].evaluated ) { + continue; + } + + // if `img` is in a `picture` element + if ( parent && parent.nodeName.toUpperCase() === "PICTURE" ) { + + // IE9 video workaround + pf.removeVideoShim( parent ); + + // return the first match which might undefined + // returns false if there is a pending source + // TODO the return type here is brutal, cleanup + firstMatch = pf.getMatch( element, parent ); + + // if any sources are pending in this picture due to async type test(s) + // remove the evaluated attr and skip for now ( the pending test will + // rerun picturefill on this element when complete) + if ( firstMatch === false ) { + continue; + } + } else { + firstMatch = undefined; + } + + // Cache and remove `srcset` if present and we’re going to be doing `picture`/`srcset`/`sizes` polyfilling to it. + if ( ( parent && parent.nodeName.toUpperCase() === "PICTURE" ) || + ( !pf.sizesSupported && ( element.srcset && regWDesc.test( element.srcset ) ) ) ) { + pf.dodgeSrcset( element ); + } + + if ( firstMatch ) { + candidates = pf.processSourceSet( firstMatch ); + pf.applyBestCandidate( candidates, element ); + } else { + // No sources matched, so we’re down to processing the inner `img` as a source. + candidates = pf.processSourceSet( element ); + + if ( element.srcset === undefined || element[ pf.ns ].srcset ) { + // Either `srcset` is completely unsupported, or we need to polyfill `sizes` functionality. + pf.applyBestCandidate( candidates, element ); + } // Else, resolution-only `srcset` is supported natively. + } + + // set evaluated to true to avoid unnecessary reparsing + element[ pf.ns ].evaluated = true; + } + } + + /** + * Sets up picture polyfill by polling the document and running + * the polyfill every 250ms until the document is ready. + * Also attaches picturefill on resize + */ + function runPicturefill() { + pf.initTypeDetects(); + picturefill(); + var intervalId = setInterval( function() { + // When the document has finished loading, stop checking for new images + // https://github.com/ded/domready/blob/master/ready.js#L15 + picturefill(); + + if ( /^loaded|^i|^c/.test( doc.readyState ) ) { + clearInterval( intervalId ); + return; + } + }, 250 ); + + var resizeTimer; + var handleResize = function() { + picturefill({ reevaluate: true }); + }; + function checkResize() { + clearTimeout(resizeTimer); + resizeTimer = setTimeout( handleResize, 60 ); + } + + if ( w.addEventListener ) { + w.addEventListener( "resize", checkResize, false ); + } else if ( w.attachEvent ) { + w.attachEvent( "onresize", checkResize ); + } + } + + runPicturefill(); + + /* expose methods for testing */ + picturefill._ = pf; + + expose( picturefill ); + +} )( window, window.document, new window.Image() ); diff --git a/frontend/drupal/sites/all/modules/picture/picturefill2/picturefill.min.js b/frontend/drupal/sites/all/modules/picture/picturefill2/picturefill.min.js new file mode 100644 index 000000000..26b1d3640 --- /dev/null +++ b/frontend/drupal/sites/all/modules/picture/picturefill2/picturefill.min.js @@ -0,0 +1,4 @@ +/*! Picturefill - v2.3.1 - 2015-04-09 +* http://scottjehl.github.io/picturefill +* Copyright (c) 2015 https://github.com/scottjehl/picturefill/blob/master/Authors.txt; Licensed MIT */ +window.matchMedia||(window.matchMedia=function(){"use strict";var a=window.styleMedia||window.media;if(!a){var b=document.createElement("style"),c=document.getElementsByTagName("script")[0],d=null;b.type="text/css",b.id="matchmediajs-test",c.parentNode.insertBefore(b,c),d="getComputedStyle"in window&&window.getComputedStyle(b,null)||b.currentStyle,a={matchMedium:function(a){var c="@media "+a+"{ #matchmediajs-test { width: 1px; } }";return b.styleSheet?b.styleSheet.cssText=c:b.textContent=c,"1px"===d.width}}}return function(b){return{matches:a.matchMedium(b||"all"),media:b||"all"}}}()),function(a,b,c){"use strict";function d(b){"object"==typeof module&&"object"==typeof module.exports?module.exports=b:"function"==typeof define&&define.amd&&define("picturefill",function(){return b}),"object"==typeof a&&(a.picturefill=b)}function e(a){var b,c,d,e,f,i=a||{};b=i.elements||g.getAllElements();for(var j=0,k=b.length;k>j;j++)if(c=b[j],d=c.parentNode,e=void 0,f=void 0,"IMG"===c.nodeName.toUpperCase()&&(c[g.ns]||(c[g.ns]={}),i.reevaluate||!c[g.ns].evaluated)){if(d&&"PICTURE"===d.nodeName.toUpperCase()){if(g.removeVideoShim(d),e=g.getMatch(c,d),e===!1)continue}else e=void 0;(d&&"PICTURE"===d.nodeName.toUpperCase()||!g.sizesSupported&&c.srcset&&h.test(c.srcset))&&g.dodgeSrcset(c),e?(f=g.processSourceSet(e),g.applyBestCandidate(f,c)):(f=g.processSourceSet(c),(void 0===c.srcset||c[g.ns].srcset)&&g.applyBestCandidate(f,c)),c[g.ns].evaluated=!0}}function f(){function c(){clearTimeout(d),d=setTimeout(h,60)}g.initTypeDetects(),e();var d,f=setInterval(function(){return e(),/^loaded|^i|^c/.test(b.readyState)?void clearInterval(f):void 0},250),h=function(){e({reevaluate:!0})};a.addEventListener?a.addEventListener("resize",c,!1):a.attachEvent&&a.attachEvent("onresize",c)}if(a.HTMLPictureElement)return void d(function(){});b.createElement("picture");var g=a.picturefill||{},h=/\s+\+?\d+(e\d+)?w/;g.ns="picturefill",function(){g.srcsetSupported="srcset"in c,g.sizesSupported="sizes"in c,g.curSrcSupported="currentSrc"in c}(),g.trim=function(a){return a.trim?a.trim():a.replace(/^\s+|\s+$/g,"")},g.makeUrl=function(){var a=b.createElement("a");return function(b){return a.href=b,a.href}}(),g.restrictsMixedContent=function(){return"https:"===a.location.protocol},g.matchesMedia=function(b){return a.matchMedia&&a.matchMedia(b).matches},g.getDpr=function(){return a.devicePixelRatio||1},g.getWidthFromLength=function(a){var c;if(!a||a.indexOf("%")>-1!=!1||!(parseFloat(a)>0||a.indexOf("calc(")>-1))return!1;a=a.replace("vw","%"),g.lengthEl||(g.lengthEl=b.createElement("div"),g.lengthEl.style.cssText="border:0;display:block;font-size:1em;left:0;margin:0;padding:0;position:absolute;visibility:hidden",g.lengthEl.className="helper-from-picturefill-js"),g.lengthEl.style.width="0px";try{g.lengthEl.style.width=a}catch(d){}return b.body.appendChild(g.lengthEl),c=g.lengthEl.offsetWidth,0>=c&&(c=!1),b.body.removeChild(g.lengthEl),c},g.detectTypeSupport=function(b,c){var d=new a.Image;return d.onerror=function(){g.types[b]=!1,e()},d.onload=function(){g.types[b]=1===d.width,e()},d.src=c,"pending"},g.types=g.types||{},g.initTypeDetects=function(){g.types["image/jpeg"]=!0,g.types["image/gif"]=!0,g.types["image/png"]=!0,g.types["image/svg+xml"]=b.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#Image","1.1"),g.types["image/webp"]=g.detectTypeSupport("image/webp","data:image/webp;base64,UklGRh4AAABXRUJQVlA4TBEAAAAvAAAAAAfQ//73v/+BiOh/AAA=")},g.verifyTypeSupport=function(a){var b=a.getAttribute("type");if(null===b||""===b)return!0;var c=g.types[b];return"string"==typeof c&&"pending"!==c?(g.types[b]=g.detectTypeSupport(b,c),"pending"):"function"==typeof c?(c(),"pending"):c},g.parseSize=function(a){var b=/(\([^)]+\))?\s*(.+)/g.exec(a);return{media:b&&b[1],length:b&&b[2]}},g.findWidthFromSourceSize=function(c){for(var d,e=g.trim(c).split(/\s*,\s*/),f=0,h=e.length;h>f;f++){var i=e[f],j=g.parseSize(i),k=j.length,l=j.media;if(k&&(!l||g.matchesMedia(l))&&(d=g.getWidthFromLength(k)))break}return d||Math.max(a.innerWidth||0,b.documentElement.clientWidth)},g.parseSrcset=function(a){for(var b=[];""!==a;){a=a.replace(/^\s+/g,"");var c,d=a.search(/\s/g),e=null;if(-1!==d){c=a.slice(0,d);var f=c.slice(-1);if((","===f||""===c)&&(c=c.replace(/,+$/,""),e=""),a=a.slice(d+1),null===e){var g=a.indexOf(",");-1!==g?(e=a.slice(0,g),a=a.slice(g+1)):(e=a,a="")}}else c=a,a="";(c||e)&&b.push({url:c,descriptor:e})}return b},g.parseDescriptor=function(a,b){var c,d=b||"100vw",e=a&&a.replace(/(^\s+|\s+$)/g,""),f=g.findWidthFromSourceSize(d);if(e)for(var h=e.split(" "),i=h.length-1;i>=0;i--){var j=h[i],k=j&&j.slice(j.length-1);if("h"!==k&&"w"!==k||g.sizesSupported){if("x"===k){var l=j&&parseFloat(j,10);c=l&&!isNaN(l)?l:1}}else c=parseFloat(parseInt(j,10)/f)}return c||1},g.getCandidatesFromSourceSet=function(a,b){for(var c=g.parseSrcset(a),d=[],e=0,f=c.length;f>e;e++){var h=c[e];d.push({url:h.url,resolution:g.parseDescriptor(h.descriptor,b)})}return d},g.dodgeSrcset=function(a){a.srcset&&(a[g.ns].srcset=a.srcset,a.srcset="",a.setAttribute("data-pfsrcset",a[g.ns].srcset))},g.processSourceSet=function(a){var b=a.getAttribute("srcset"),c=a.getAttribute("sizes"),d=[];return"IMG"===a.nodeName.toUpperCase()&&a[g.ns]&&a[g.ns].srcset&&(b=a[g.ns].srcset),b&&(d=g.getCandidatesFromSourceSet(b,c)),d},g.backfaceVisibilityFix=function(a){var b=a.style||{},c="webkitBackfaceVisibility"in b,d=b.zoom;c&&(b.zoom=".999",c=a.offsetWidth,b.zoom=d)},g.setIntrinsicSize=function(){var c={},d=function(a,b,c){b&&a.setAttribute("width",parseInt(b/c,10))};return function(e,f){var h;e[g.ns]&&!a.pfStopIntrinsicSize&&(void 0===e[g.ns].dims&&(e[g.ns].dims=e.getAttribute("width")||e.getAttribute("height")),e[g.ns].dims||(f.url in c?d(e,c[f.url],f.resolution):(h=b.createElement("img"),h.onload=function(){if(c[f.url]=h.width,!c[f.url])try{b.body.appendChild(h),c[f.url]=h.width||h.offsetWidth,b.body.removeChild(h)}catch(a){}e.src===f.url&&d(e,c[f.url],f.resolution),e=null,h.onload=null,h=null},h.src=f.url)))}}(),g.applyBestCandidate=function(a,b){var c,d,e;a.sort(g.ascendingSort),d=a.length,e=a[d-1];for(var f=0;d>f;f++)if(c=a[f],c.resolution>=g.getDpr()){e=c;break}e&&(e.url=g.makeUrl(e.url),b.src!==e.url&&(g.restrictsMixedContent()&&"http:"===e.url.substr(0,"http:".length).toLowerCase()?void 0!==window.console&&console.warn("Blocked mixed content image "+e.url):(b.src=e.url,g.curSrcSupported||(b.currentSrc=b.src),g.backfaceVisibilityFix(b))),g.setIntrinsicSize(b,e))},g.ascendingSort=function(a,b){return a.resolution-b.resolution},g.removeVideoShim=function(a){var b=a.getElementsByTagName("video");if(b.length){for(var c=b[0],d=c.getElementsByTagName("source");d.length;)a.insertBefore(d[0],c);c.parentNode.removeChild(c)}},g.getAllElements=function(){for(var a=[],c=b.getElementsByTagName("img"),d=0,e=c.length;e>d;d++){var f=c[d];("PICTURE"===f.parentNode.nodeName.toUpperCase()||null!==f.getAttribute("srcset")||f[g.ns]&&null!==f[g.ns].srcset)&&a.push(f)}return a},g.getMatch=function(a,b){for(var c,d=b.childNodes,e=0,f=d.length;f>e;e++){var h=d[e];if(1===h.nodeType){if(h===a)return c;if("SOURCE"===h.nodeName.toUpperCase()){null!==h.getAttribute("src")&&void 0!==typeof console&&console.warn("The `src` attribute is invalid on `picture` `source` element; instead, use `srcset`.");var i=h.getAttribute("media");if(h.getAttribute("srcset")&&(!i||g.matchesMedia(i))){var j=g.verifyTypeSupport(h);if(j===!0){c=h;break}if("pending"===j)return!1}}}}return c},f(),e._=g,d(e)}(window,window.document,new window.Image); diff --git a/frontend/drupal/themes/bartik/bartik.info b/frontend/drupal/themes/bartik/bartik.info index bd5a9935c..7cae615b3 100644 --- a/frontend/drupal/themes/bartik/bartik.info +++ b/frontend/drupal/themes/bartik/bartik.info @@ -34,7 +34,7 @@ regions[footer] = Footer settings[shortcut_module_link] = 0 -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/themes/garland/garland.info b/frontend/drupal/themes/garland/garland.info index 64077f10c..8f05df631 100644 --- a/frontend/drupal/themes/garland/garland.info +++ b/frontend/drupal/themes/garland/garland.info @@ -7,7 +7,7 @@ stylesheets[all][] = style.css stylesheets[print][] = print.css settings[garland_width] = fluid -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/themes/seven/seven.info b/frontend/drupal/themes/seven/seven.info index 8e14429b4..0de4cdf22 100644 --- a/frontend/drupal/themes/seven/seven.info +++ b/frontend/drupal/themes/seven/seven.info @@ -13,7 +13,7 @@ regions[page_bottom] = Page bottom regions[sidebar_first] = First sidebar regions_hidden[] = sidebar_first -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/frontend/drupal/themes/stark/stark.info b/frontend/drupal/themes/stark/stark.info index 4eec4dc06..b66f128d5 100644 --- a/frontend/drupal/themes/stark/stark.info +++ b/frontend/drupal/themes/stark/stark.info @@ -5,7 +5,7 @@ version = VERSION core = 7.x stylesheets[all][] = layout.css -; Information added by Drupal.org packaging script on 2021-01-20 -version = "7.78" +; Information added by Drupal.org packaging script on 2021-04-21 +version = "7.80" project = "drupal" -datestamp = "1611162699" +datestamp = "1619021862" diff --git a/sql/update2.sql b/sql/update2.sql index f504c1957..5664870a1 100644 --- a/sql/update2.sql +++ b/sql/update2.sql @@ -9491,3 +9491,7 @@ UPDATE misc SET value='21.03.30' WHERE what="version"; ALTER TABLE srv_anketa DROP COLUMN old_email_style; UPDATE misc SET value='21.03.30' WHERE what="version"; + +# Drupal verzija je bila posodobljena in vpišemo na 1ka.si in te +UPDATE misc SET value='7.80' WHERE what="drupal version"; +UPDATE misc SET value='21.05.24' WHERE what="version"; \ No newline at end of file