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 <mflaschen@wikimedia.org>
Co-Author: Timo Tijhof <krinklemail@gmail.com>
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
$wgVariantArticlePath, $wgActionPaths, $wgVersion,
$wgEnableAPI, $wgEnableWriteAPI, $wgDBname,
$wgSitename, $wgFileExtensions, $wgExtensionAssetsPath,
- $wgCookiePrefix, $wgResourceLoaderMaxQueryLength,
+ $wgCookiePrefix, $wgCookieDomain, $wgCookiePath,
+ $wgCookieExpiration, $wgResourceLoaderMaxQueryLength,
$wgResourceLoaderStorageEnabled, $wgResourceLoaderStorageVersion,
$wgSearchType;
'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() ),
"mw.Notification_",
"mw.user",
"mw.util",
- "mw.plugin.*"
+ "mw.plugin.*",
+ "mw.cookie"
]
},
{
'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' ),
'position' => 'top', // For $wgPreloadJavaScriptMwUtil
'targets' => array( 'desktop', 'mobile' ),
),
+ 'mediawiki.cookie' => array(
+ 'scripts' => 'resources/src/mediawiki/mediawiki.cookie.js',
+ 'dependencies' => array(
+ 'jquery.cookie',
+ ),
+ ),
/* MediaWiki Action */
--- /dev/null
+( 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 <samsmith@wikimedia.org>
+ * @author Matthew Flaschen <mflaschen@wikimedia.org>
+ * @author Timo Tijhof <krinklemail@gmail.com>
+ *
+ * @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 ) );
'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',
'mediawiki.special.recentchanges',
'mediawiki.language',
'mediawiki.cldr',
+ 'mediawiki.cookie',
'test.mediawiki.qunit.testrunner',
),
)
--- /dev/null
+( 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 ) );