diff --git a/module/VuFind/src/VuFind/Cart.php b/module/VuFind/src/VuFind/Cart.php index 51ab9cf25235e51d709abda9e429110f6071cd11..a881d3cffb1851cc24b13238e48cd8621f6dd839 100644 --- a/module/VuFind/src/VuFind/Cart.php +++ b/module/VuFind/src/VuFind/Cart.php @@ -337,6 +337,16 @@ class Cart return $this->cookieManager->getPath(); } + /** + * Get cookie SameSite attribute (null if unset). + * + * @return string + */ + public function getCookieSameSite() + { + return $this->cookieManager->getSameSite(); + } + /** * Process parameters and return the cart content. * diff --git a/themes/bootstrap3/js/cart.js b/themes/bootstrap3/js/cart.js index 656ba06f6756d8931d91ae795aa539fe1e93ac24..f391561e5b305eed03db38eaae0c6e701d6e11e9 100644 --- a/themes/bootstrap3/js/cart.js +++ b/themes/bootstrap3/js/cart.js @@ -1,4 +1,4 @@ -/*global Cookies, VuFind */ +/*global VuFind */ /*exported cartFormHandler */ VuFind.register('cart', function Cart() { @@ -7,6 +7,7 @@ VuFind.register('cart', function Cart() { var _COOKIE_DELIM = "\t"; var _COOKIE_DOMAIN = false; var _COOKIE_PATH = '/'; + var _COOKIE_SAMESITE = 'Lax'; function setDomain(domain) { _COOKIE_DOMAIN = domain; @@ -16,6 +17,14 @@ VuFind.register('cart', function Cart() { _COOKIE_PATH = path; } + function setCookieSameSite(sameSite) { + _COOKIE_SAMESITE = sameSite; + } + + function _getCookieParams() { + return { path: _COOKIE_PATH, domain: _COOKIE_DOMAIN, SameSite: _COOKIE_SAMESITE }; + } + function _uniqueArray(op) { var ret = []; for (var i = 0; i < op.length; i++) { @@ -27,14 +36,14 @@ VuFind.register('cart', function Cart() { } function _getItems() { - var items = Cookies.getItem(_COOKIE); + var items = window.Cookies.get(_COOKIE); if (items) { return items.split(_COOKIE_DELIM); } return []; } function _getSources() { - var items = Cookies.getItem(_COOKIE_SOURCES); + var items = window.Cookies.get(_COOKIE_SOURCES); if (items) { return items.split(_COOKIE_DELIM); } @@ -96,11 +105,11 @@ VuFind.register('cart', function Cart() { // Add source to source cookie cartItems[cartItems.length] = String.fromCharCode(65 + cartSources.length) + id; cartSources[cartSources.length] = source; - Cookies.setItem(_COOKIE_SOURCES, cartSources.join(_COOKIE_DELIM), false, _COOKIE_PATH, _COOKIE_DOMAIN); + window.Cookies.set(_COOKIE_SOURCES, cartSources.join(_COOKIE_DELIM), _getCookieParams()); } else { cartItems[cartItems.length] = String.fromCharCode(65 + sIndex) + id; } - Cookies.setItem(_COOKIE, _uniqueArray(cartItems).join(_COOKIE_DELIM), false, _COOKIE_PATH, _COOKIE_DOMAIN); + window.Cookies.set(_COOKIE, _uniqueArray(cartItems).join(_COOKIE_DELIM), _getCookieParams()); updateCount(); return true; } @@ -135,11 +144,11 @@ VuFind.register('cart', function Cart() { } } if (cartItems.length > 0) { - Cookies.setItem(_COOKIE, _uniqueArray(cartItems).join(_COOKIE_DELIM), false, _COOKIE_PATH, _COOKIE_DOMAIN); - Cookies.setItem(_COOKIE_SOURCES, _uniqueArray(cartSources).join(_COOKIE_DELIM), false, _COOKIE_PATH, _COOKIE_DOMAIN); + window.Cookies.set(_COOKIE, _uniqueArray(cartItems).join(_COOKIE_DELIM), _getCookieParams()); + window.Cookies.set(_COOKIE_SOURCES, _uniqueArray(cartSources).join(_COOKIE_DELIM), _getCookieParams()); } else { - Cookies.removeItem(_COOKIE, _COOKIE_PATH, _COOKIE_DOMAIN); - Cookies.removeItem(_COOKIE_SOURCES, _COOKIE_PATH, _COOKIE_DOMAIN); + window.Cookies.remove(_COOKIE, _getCookieParams()); + window.Cookies.remove(_COOKIE_SOURCES, _getCookieParams()); } updateCount(); return true; @@ -249,6 +258,7 @@ VuFind.register('cart', function Cart() { hasItem: hasItem, removeItem: removeItem, setCookiePath: setCookiePath, + setCookieSameSite: setCookieSameSite, setDomain: setDomain, updateCount: updateCount, // Init diff --git a/themes/bootstrap3/js/vendor/cookies.js b/themes/bootstrap3/js/vendor/cookies.js deleted file mode 100644 index e0d8a44404e6aa0d0ad943a943ea4b4fa04a190b..0000000000000000000000000000000000000000 --- a/themes/bootstrap3/js/vendor/cookies.js +++ /dev/null @@ -1,64 +0,0 @@ -// https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie/Simple_document.cookie_framework -/*\ -|*| -|*| :: cookies.js :: -|*| -|*| A complete cookies reader/writer framework with full unicode support. -|*| -|*| Revision #1 - September 4, 2014 -|*| -|*| https://developer.mozilla.org/en-US/docs/Web/API/document.cookie -|*| https://developer.mozilla.org/User:fusionchess -|*| -|*| This framework is released under the GNU Public License, version 3 or later. -|*| http://www.gnu.org/licenses/gpl-3.0-standalone.html -|*| -|*| Syntaxes: -|*| -|*| * Cookies.setItem(name, value[, end[, path[, domain[, secure]]]]) -|*| * Cookies.getItem(name) -|*| * Cookies.removeItem(name[, path[, domain]]) -|*| * Cookies.hasItem(name) -|*| * Cookies.keys() -|*| -\*/ - -var Cookies = { - getItem: function (sKey) { - if (!sKey) { return null; } - return decodeURIComponent(document.cookie.replace(new RegExp("(?:(?:^|.*;)\\s*" + encodeURIComponent(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=\\s*([^;]*).*$)|^.*$"), "$1")) || null; - }, - setItem: function (sKey, sValue, vEnd, sPath, sDomain, bSecure) { - if (!sKey || /^(?:expires|max\-age|path|domain|secure)$/i.test(sKey)) { return false; } - var sExpires = ""; - if (vEnd) { - switch (vEnd.constructor) { - case Number: - sExpires = vEnd === Infinity ? "; expires=Fri, 31 Dec 9999 23:59:59 GMT" : "; max-age=" + vEnd; - break; - case String: - sExpires = "; expires=" + vEnd; - break; - case Date: - sExpires = "; expires=" + vEnd.toUTCString(); - break; - } - } - document.cookie = encodeURIComponent(sKey) + "=" + encodeURIComponent(sValue) + sExpires + (sDomain ? "; domain=" + sDomain : "") + (sPath ? "; path=" + sPath : "") + (bSecure ? "; secure" : ""); - return true; - }, - removeItem: function (sKey, sPath, sDomain) { - if (!this.hasItem(sKey)) { return false; } - document.cookie = encodeURIComponent(sKey) + "=; expires=Thu, 01 Jan 1970 00:00:00 GMT" + (sDomain ? "; domain=" + sDomain : "") + (sPath ? "; path=" + sPath : ""); - return true; - }, - hasItem: function (sKey) { - if (!sKey) { return false; } - return (new RegExp("(?:^|;\\s*)" + encodeURIComponent(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=")).test(document.cookie); - }, - keys: function () { - var aKeys = document.cookie.replace(/((?:^|\s*;)[^\=]+)(?=;|$)|^\s*|\s*(?:\=[^;]*)?(?:\1|$)/g, "").split(/\s*(?:\=[^;]*)?;\s*/); - for (var nLen = aKeys.length, nIdx = 0; nIdx < nLen; nIdx++) { aKeys[nIdx] = decodeURIComponent(aKeys[nIdx]); } - return aKeys; - } -}; \ No newline at end of file diff --git a/themes/bootstrap3/js/vendor/js.cookie.js b/themes/bootstrap3/js/vendor/js.cookie.js new file mode 100644 index 0000000000000000000000000000000000000000..80a755124a41e345a59860bf1785e127ab444a97 --- /dev/null +++ b/themes/bootstrap3/js/vendor/js.cookie.js @@ -0,0 +1,163 @@ +/*! + * JavaScript Cookie v2.2.1 + * https://github.com/js-cookie/js-cookie + * + * Copyright 2006, 2015 Klaus Hartl & Fagner Brack + * Released under the MIT license + */ +;(function (factory) { + var registeredInModuleLoader; + if (typeof define === 'function' && define.amd) { + define(factory); + registeredInModuleLoader = true; + } + if (typeof exports === 'object') { + module.exports = factory(); + registeredInModuleLoader = true; + } + if (!registeredInModuleLoader) { + var OldCookies = window.Cookies; + var api = window.Cookies = factory(); + api.noConflict = function () { + window.Cookies = OldCookies; + return api; + }; + } +}(function () { + function extend () { + var i = 0; + var result = {}; + for (; i < arguments.length; i++) { + var attributes = arguments[ i ]; + for (var key in attributes) { + result[key] = attributes[key]; + } + } + return result; + } + + function decode (s) { + return s.replace(/(%[0-9A-Z]{2})+/g, decodeURIComponent); + } + + function init (converter) { + function api() {} + + function set (key, value, attributes) { + if (typeof document === 'undefined') { + return; + } + + attributes = extend({ + path: '/' + }, api.defaults, attributes); + + if (typeof attributes.expires === 'number') { + attributes.expires = new Date(new Date() * 1 + attributes.expires * 864e+5); + } + + // We're using "expires" because "max-age" is not supported by IE + attributes.expires = attributes.expires ? attributes.expires.toUTCString() : ''; + + try { + var result = JSON.stringify(value); + if (/^[\{\[]/.test(result)) { + value = result; + } + } catch (e) {} + + value = converter.write ? + converter.write(value, key) : + encodeURIComponent(String(value)) + .replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g, decodeURIComponent); + + key = encodeURIComponent(String(key)) + .replace(/%(23|24|26|2B|5E|60|7C)/g, decodeURIComponent) + .replace(/[\(\)]/g, escape); + + var stringifiedAttributes = ''; + for (var attributeName in attributes) { + if (!attributes[attributeName]) { + continue; + } + stringifiedAttributes += '; ' + attributeName; + if (attributes[attributeName] === true) { + continue; + } + + // Considers RFC 6265 section 5.2: + // ... + // 3. If the remaining unparsed-attributes contains a %x3B (";") + // character: + // Consume the characters of the unparsed-attributes up to, + // not including, the first %x3B (";") character. + // ... + stringifiedAttributes += '=' + attributes[attributeName].split(';')[0]; + } + + return (document.cookie = key + '=' + value + stringifiedAttributes); + } + + function get (key, json) { + if (typeof document === 'undefined') { + return; + } + + var jar = {}; + // To prevent the for loop in the first place assign an empty array + // in case there are no cookies at all. + var cookies = document.cookie ? document.cookie.split('; ') : []; + var i = 0; + + for (; i < cookies.length; i++) { + var parts = cookies[i].split('='); + var cookie = parts.slice(1).join('='); + + if (!json && cookie.charAt(0) === '"') { + cookie = cookie.slice(1, -1); + } + + try { + var name = decode(parts[0]); + cookie = (converter.read || converter)(cookie, name) || + decode(cookie); + + if (json) { + try { + cookie = JSON.parse(cookie); + } catch (e) {} + } + + jar[name] = cookie; + + if (key === name) { + break; + } + } catch (e) {} + } + + return key ? jar[key] : jar; + } + + api.set = set; + api.get = function (key) { + return get(key, false /* read as raw */); + }; + api.getJSON = function (key) { + return get(key, true /* read as json */); + }; + api.remove = function (key, attributes) { + set(key, '', extend(attributes, { + expires: -1 + })); + }; + + api.defaults = {}; + + api.withConverter = init; + + return api; + } + + return init(function () {}); +})); diff --git a/themes/bootstrap3/templates/layout/layout.phtml b/themes/bootstrap3/templates/layout/layout.phtml index 4ddf4ca1b35667f7addb114b36e5c21002d38503..e10c16ce2108bbcfd1184aa2218ad8c7e4a2578d 100644 --- a/themes/bootstrap3/templates/layout/layout.phtml +++ b/themes/bootstrap3/templates/layout/layout.phtml @@ -68,7 +68,7 @@ // Deal with cart stuff: $cart = $this->cart(); if ($cart->isActive()) { - $this->headScript()->appendFile("vendor/cookies.js"); + $this->headScript()->appendFile("vendor/js.cookie.js"); $this->headScript()->appendFile("cart.js"); $domain = $cart->getCookieDomain(); if (!empty($domain)) { @@ -82,6 +82,12 @@ 'VuFind.cart.setCookiePath("' . $cookiePath . '");' ); } + $cookieSameSite = $cart->getCookieSameSite(); + if (null !== $cookieSameSite) { + $this->headScript()->appendScript( + 'VuFind.cart.setCookieSameSite("' . $cookieSameSite . '");' + ); + } $this->jsTranslations()->addStrings( [ 'addBookBag' => 'Add to Book Bag',