Implement mediawiki.cookie module
authorSam Smith <git@samsmith.io>
Tue, 25 Mar 2014 15:18:09 +0000 (15:18 +0000)
committerMatthew Flaschen <mflaschen@wikimedia.org>
Wed, 14 May 2014 04:52:41 +0000 (00:52 -0400)
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>

RELEASE-NOTES-1.23
includes/resourceloader/ResourceLoaderStartUpModule.php
maintenance/jsduck/categories.json
resources/Resources.php
resources/src/mediawiki/mediawiki.cookie.js [new file with mode: 0644]
tests/qunit/QUnitTestResources.php
tests/qunit/suites/resources/mediawiki/mediawiki.cookie.test.js [new file with mode: 0644]

index 231903c..622d96e 100644 (file)
@@ -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
index d0f0541..d4359c5 100644 (file)
@@ -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() ),
index 2d45645..93abf87 100644 (file)
@@ -25,7 +25,8 @@
                                        "mw.Notification_",
                                        "mw.user",
                                        "mw.util",
-                                       "mw.plugin.*"
+                                       "mw.plugin.*",
+                                       "mw.cookie"
                                ]
                        },
                        {
index 668dc14..767c9cf 100644 (file)
@@ -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 (file)
index 0000000..657edf3
--- /dev/null
@@ -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 <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 ) );
index e861967..f48397f 100644 (file)
@@ -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 (file)
index 0000000..c9653da
--- /dev/null
@@ -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 ) );