From 7c585b5a33d57469d19e9b176f14b84403488b73 Mon Sep 17 00:00:00 2001 From: Sam Smith Date: Tue, 25 Mar 2014 15:18:09 +0000 Subject: [PATCH] Implement mediawiki.cookie module Provides functonality similar to WebRequest#getCookie and WebResponse#setcookie. Wraps $.cookie and automatically takes care of wgCookiePrefix etc. Bug: 49156 Change-Id: I217ef258aecf1acd335e2cea56ae08b22541c7d4 Co-Author: Matthew Flaschen Co-Author: Timo Tijhof --- RELEASE-NOTES-1.23 | 3 + .../ResourceLoaderStartUpModule.php | 6 +- maintenance/jsduck/categories.json | 3 +- resources/Resources.php | 7 + resources/src/mediawiki/mediawiki.cookie.js | 126 +++++++++++++ tests/qunit/QUnitTestResources.php | 2 + .../mediawiki/mediawiki.cookie.test.js | 172 ++++++++++++++++++ 7 files changed, 317 insertions(+), 2 deletions(-) create mode 100644 resources/src/mediawiki/mediawiki.cookie.js create mode 100644 tests/qunit/suites/resources/mediawiki/mediawiki.cookie.test.js diff --git a/RELEASE-NOTES-1.23 b/RELEASE-NOTES-1.23 index 231903ce6b..622d96e373 100644 --- a/RELEASE-NOTES-1.23 +++ b/RELEASE-NOTES-1.23 @@ -156,6 +156,9 @@ production. in StartProfiler.php instead of using this. * (bug 63444) Made it possible to change the indent string (default: 4 spaces) used by FormatJson::encode(). +* (bug 49156) Added the mediawiki.cookie ResourceLoader module, which wraps + jQuery.cookie so that getting/setting a cookie is syntactically and functionally + similar to using the WebRequest#getCookie/WebResponse#setcookie methods. === Bug fixes in 1.23 === * (bug 41759) The "updated since last visit" markers (on history pages, recent diff --git a/includes/resourceloader/ResourceLoaderStartUpModule.php b/includes/resourceloader/ResourceLoaderStartUpModule.php index d0f05416f8..d4359c5707 100644 --- a/includes/resourceloader/ResourceLoaderStartUpModule.php +++ b/includes/resourceloader/ResourceLoaderStartUpModule.php @@ -48,7 +48,8 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { $wgVariantArticlePath, $wgActionPaths, $wgVersion, $wgEnableAPI, $wgEnableWriteAPI, $wgDBname, $wgSitename, $wgFileExtensions, $wgExtensionAssetsPath, - $wgCookiePrefix, $wgResourceLoaderMaxQueryLength, + $wgCookiePrefix, $wgCookieDomain, $wgCookiePath, + $wgCookieExpiration, $wgResourceLoaderMaxQueryLength, $wgResourceLoaderStorageEnabled, $wgResourceLoaderStorageVersion, $wgSearchType; @@ -104,6 +105,9 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { 'wgExtensionAssetsPath' => $wgExtensionAssetsPath, // MediaWiki sets cookies to have this prefix by default 'wgCookiePrefix' => $wgCookiePrefix, + 'wgCookieDomain' => $wgCookieDomain, + 'wgCookiePath' => $wgCookiePath, + 'wgCookieExpiration' => $wgCookieExpiration, 'wgResourceLoaderMaxQueryLength' => $wgResourceLoaderMaxQueryLength, 'wgCaseSensitiveNamespaces' => $caseSensitiveNamespaces, 'wgLegalTitleChars' => Title::convertByteClassToUnicodeClass( Title::legalChars() ), diff --git a/maintenance/jsduck/categories.json b/maintenance/jsduck/categories.json index 2d45645113..93abf87b2e 100644 --- a/maintenance/jsduck/categories.json +++ b/maintenance/jsduck/categories.json @@ -25,7 +25,8 @@ "mw.Notification_", "mw.user", "mw.util", - "mw.plugin.*" + "mw.plugin.*", + "mw.cookie" ] }, { diff --git a/resources/Resources.php b/resources/Resources.php index 668dc14ee3..767c9cfd58 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -240,6 +240,7 @@ return array( 'jquery.colorUtil' => array( 'scripts' => 'resources/src/jquery/jquery.colorUtil.js', ), + // Use mediawiki.cookie in new code, rather than jquery.cookie. 'jquery.cookie' => array( 'scripts' => 'resources/lib/jquery/jquery.cookie.js', 'targets' => array( 'desktop', 'mobile' ), @@ -911,6 +912,12 @@ return array( 'position' => 'top', // For $wgPreloadJavaScriptMwUtil 'targets' => array( 'desktop', 'mobile' ), ), + 'mediawiki.cookie' => array( + 'scripts' => 'resources/src/mediawiki/mediawiki.cookie.js', + 'dependencies' => array( + 'jquery.cookie', + ), + ), /* MediaWiki Action */ diff --git a/resources/src/mediawiki/mediawiki.cookie.js b/resources/src/mediawiki/mediawiki.cookie.js new file mode 100644 index 0000000000..657edf35d5 --- /dev/null +++ b/resources/src/mediawiki/mediawiki.cookie.js @@ -0,0 +1,126 @@ +( function ( mw, $ ) { + 'use strict'; + + /** + * Provides an API for getting and setting cookies that is + * syntactically and functionally similar to the server-side cookie + * API (`WebRequest#getCookie` and `WebResponse#setcookie`). + * + * @author Sam Smith + * @author Matthew Flaschen + * @author Timo Tijhof + * + * @class mw.cookie + * @singleton + */ + mw.cookie = { + + /** + * Sets or deletes a cookie. + * + * While this is natural in JavaScript, contrary to `WebResponse#setcookie` in PHP, the + * default values for the `options` properties only apply if that property isn't set + * already in your options object (e.g. passing `{ secure: null }` or `{ secure: undefined }` + * overrides the default value for `options.secure`). + * + * @param {string} key + * @param {string|null} value Value of cookie. If `value` is `null` then this method will + * instead remove a cookie by name of `key`. + * @param {Object|Date} [options] Options object, or expiry date + * @param {Date|boolean} [options.expires=wgCookieExpiration] The expiry date of the cookie. + * + * Default cookie expiration is based on `wgCookieExpiration`. If `wgCookieExpiration` is + * 0, a session cookie is set (expires when the browser is closed). For non-zero values of + * `wgCookieExpiration`, the cookie expires `wgCookieExpiration` seconds from now. + * + * If options.expires is null, then a session cookie is set. + * @param {string} [options.prefix=wgCookiePrefix] The prefix of the key + * @param {string} [options.domain=wgCookieDomain] The domain attribute of the cookie + * @param {string} [options.path=wgCookiePath] The path attribute of the cookie + * @param {boolean} [options.secure=false] Whether or not to include the secure attribute. + * (Does **not** use the wgCookieSecure configuration variable) + */ + set: function ( key, value, options ) { + var config, defaultOptions, date; + + // wgCookieSecure is not used for now, since 'detect' could not work with + // ResourceLoaderStartUpModule, as module cache is not fragmented by protocol. + config = mw.config.get( [ + 'wgCookiePrefix', + 'wgCookieDomain', + 'wgCookiePath', + 'wgCookieExpiration' + ] ); + + defaultOptions = { + prefix: config.wgCookiePrefix, + domain: config.wgCookieDomain, + path: config.wgCookiePath, + secure: false + }; + + // Options argument can also be a shortcut for the expiry + // Expiry can be a Date or null + if ( $.type( options ) !== 'object' ) { + // Also takes care of options = undefined, in which case we also don't need $.extend() + defaultOptions.expires = options; + options = defaultOptions; + } else { + options = $.extend( defaultOptions, options ); + } + + // $.cookie makes session cookies when expiry is omitted, + // however our default is to expire wgCookieExpiration seconds from now. + // Note: If wgCookieExpiration is 0, that is considered a special value indicating + // all cookies should be session cookies by default. + if ( options.expires === undefined && config.wgCookieExpiration !== 0 ) { + date = new Date(); + date.setTime( Number( date ) + ( config.wgCookieExpiration * 1000 ) ); + options.expires = date; + } else if ( options.expires === null ) { + // $.cookie makes a session cookie when expires is omitted + delete options.expires; + } + + // Process prefix + key = options.prefix + key; + delete options.prefix; + + // Process value + if ( value !== null ) { + value = String( value ); + } + + // Other options are handled by $.cookie + $.cookie( key, value, options ); + }, + + /** + * Gets the value of a cookie. + * + * @param {string} key + * @param {string} [prefix=wgCookiePrefix] The prefix of the key. If `prefix` is + * `undefined` or `null`, then `wgCookiePrefix` is used + * @param {Mixed} [defaultValue=null] + * @return {string} If the cookie exists, then the value of the + * cookie, otherwise `defaultValue` + */ + get: function ( key, prefix, defaultValue ) { + var result; + + if ( prefix === undefined || prefix === null ) { + prefix = mw.config.get( 'wgCookiePrefix' ); + } + + // Was defaultValue omitted? + if ( arguments.length < 3 ) { + defaultValue = null; + } + + result = $.cookie( prefix + key ); + + return result !== null ? result : defaultValue; + } + }; + +} ( mediaWiki, jQuery ) ); diff --git a/tests/qunit/QUnitTestResources.php b/tests/qunit/QUnitTestResources.php index e861967e11..f48397f6c4 100644 --- a/tests/qunit/QUnitTestResources.php +++ b/tests/qunit/QUnitTestResources.php @@ -76,6 +76,7 @@ return array( 'tests/qunit/suites/resources/mediawiki.special/mediawiki.special.recentchanges.test.js', 'tests/qunit/suites/resources/mediawiki/mediawiki.language.test.js', 'tests/qunit/suites/resources/mediawiki/mediawiki.cldr.test.js', + 'tests/qunit/suites/resources/mediawiki/mediawiki.cookie.test.js', ), 'dependencies' => array( 'jquery.accessKeyLabel', @@ -107,6 +108,7 @@ return array( 'mediawiki.special.recentchanges', 'mediawiki.language', 'mediawiki.cldr', + 'mediawiki.cookie', 'test.mediawiki.qunit.testrunner', ), ) diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.cookie.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.cookie.test.js new file mode 100644 index 0000000000..c9653dabc0 --- /dev/null +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.cookie.test.js @@ -0,0 +1,172 @@ +( function ( mw, $ ) { + + var NOW = 9012, // miliseconds + DEFAULT_DURATION = 5678, // seconds + expiryDate = new Date(); + + expiryDate.setTime( NOW + ( DEFAULT_DURATION * 1000 ) ); + + QUnit.module( 'mediawiki.cookie', QUnit.newMwEnvironment( { + setup: function () { + this.stub( $, 'cookie' ).returns( null ); + + this.sandbox.useFakeTimers( NOW ); + }, + config: { + wgCookiePrefix: 'mywiki', + wgCookieDomain: 'example.org', + wgCookiePath: '/path', + wgCookieExpiration: DEFAULT_DURATION + } + } ) ); + + QUnit.test( 'set( key, value )', 7, function ( assert ) { + var call; + + // Simple case + mw.cookie.set( 'foo', 'bar' ); + + call = $.cookie.lastCall.args; + assert.strictEqual( call[ 0 ], 'mywikifoo' ); + assert.strictEqual( call[ 1 ], 'bar' ); + assert.deepEqual( call[ 2 ], { + expires: expiryDate, + domain: 'example.org', + path: '/path', + secure: false + } ); + + mw.cookie.set( 'foo', null ); + call = $.cookie.lastCall.args; + assert.strictEqual( call[ 1 ], null, 'null removes cookie' ); + + mw.cookie.set( 'foo', undefined ); + call = $.cookie.lastCall.args; + assert.strictEqual( call[ 1 ], 'undefined', 'undefined is value' ); + + mw.cookie.set( 'foo', false ); + call = $.cookie.lastCall.args; + assert.strictEqual( call[ 1 ], 'false', 'false is a value' ); + + mw.cookie.set( 'foo', 0 ); + call = $.cookie.lastCall.args; + assert.strictEqual( call[ 1 ], '0', '0 is value' ); + } ); + + QUnit.test( 'set( key, value, expires )', 5, function ( assert ) { + var date, options; + + date = new Date(); + date.setTime( 1234 ); + + mw.cookie.set( 'foo', 'bar' ); + options = $.cookie.lastCall.args[ 2 ]; + assert.deepEqual( options.expires, expiryDate, 'Default cookie expiration is used' ); + + mw.cookie.set( 'foo', 'bar', date ); + options = $.cookie.lastCall.args[ 2 ]; + assert.strictEqual( options.expires, date, 'Custom expiration date' ); + + mw.cookie.set( 'foo', 'bar', null ); + options = $.cookie.lastCall.args[ 2 ]; + assert.strictEqual( options.expires, undefined, 'Expiry null forces session cookie' ); + + // Per DefaultSettings.php, when wgCookieExpiration is 0, the default should + // be session cookies + mw.config.set( 'wgCookieExpiration', 0 ); + + mw.cookie.set( 'foo', 'bar' ); + options = $.cookie.lastCall.args[ 2 ]; + assert.strictEqual( options.expires, undefined, 'wgCookieExpiration=0 results in session cookies by default' ); + + mw.cookie.set( 'foo', 'bar', date ); + options = $.cookie.lastCall.args[ 2 ]; + assert.strictEqual( options.expires, date, 'Custom expiration when default is session cookies' ); + } ); + + QUnit.test( 'set( key, value, options )', 4, function ( assert ) { + var date, call; + + mw.cookie.set( 'foo', 'bar', { + prefix: 'myPrefix', + domain: 'myDomain', + path: 'myPath', + secure: true + } ); + + call = $.cookie.lastCall.args; + assert.strictEqual( call[0], 'myPrefixfoo' ); + assert.deepEqual( call[ 2 ], { + expires: expiryDate, + domain: 'myDomain', + path: 'myPath', + secure: true + }, 'Options (without expires)' ); + + date = new Date(); + date.setTime( 1234 ); + + mw.cookie.set( 'foo', 'bar', { + expires: date, + prefix: 'myPrefix', + domain: 'myDomain', + path: 'myPath', + secure: true + } ); + + call = $.cookie.lastCall.args; + assert.strictEqual( call[0], 'myPrefixfoo' ); + assert.deepEqual( call[ 2 ], { + expires: date, + domain: 'myDomain', + path: 'myPath', + secure: true + }, 'Options (incl. expires)' ); + } ); + + QUnit.test( 'get( key ) - no values', 6, function ( assert ) { + var key, value; + + mw.cookie.get( 'foo' ); + + key = $.cookie.lastCall.args[ 0 ]; + assert.strictEqual( key, 'mywikifoo', 'Default prefix' ); + + mw.cookie.get( 'foo', undefined ); + key = $.cookie.lastCall.args[ 0 ]; + assert.strictEqual( key, 'mywikifoo', 'Use default prefix for undefined' ); + + mw.cookie.get( 'foo', null ); + key = $.cookie.lastCall.args[ 0 ]; + assert.strictEqual( key, 'mywikifoo', 'Use default prefix for null' ); + + mw.cookie.get( 'foo', '' ); + key = $.cookie.lastCall.args[ 0 ]; + assert.strictEqual( key, 'foo', 'Don\'t use default prefix for empty string' ); + + value = mw.cookie.get( 'foo' ); + assert.strictEqual( value, null, 'Return null by default' ); + + value = mw.cookie.get( 'foo', null, 'bar' ); + assert.strictEqual( value, 'bar', 'Custom default value' ); + } ); + + QUnit.test( 'get( key ) - with value', 1, function ( assert ) { + var value; + + $.cookie.returns( 'bar' ); + + value = mw.cookie.get( 'foo' ); + assert.strictEqual( value, 'bar', 'Return value of cookie' ); + } ); + + QUnit.test( 'get( key, prefix )', 1, function ( assert ) { + var key; + + mw.cookie.get( 'foo', 'bar' ); + + key = $.cookie.lastCall.args[ 0 ]; + assert.strictEqual( key, 'barfoo' ); + } ); + +} ( mediaWiki, jQuery ) ); -- 2.20.1