Skip to content

Commit 27d7484

Browse files
committed
add mw.cookie (relies on jquery.cookie)
1 parent 2a2ac17 commit 27d7484

File tree

7 files changed

+284
-5
lines changed

7 files changed

+284
-5
lines changed

README.md

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
## mock-mediawiki
22
![Node.js CI](https://github.com/wikimedia-gadgets/mock-mediawiki/workflows/test/badge.svg)
33
[![NPM version](https://img.shields.io/npm/v/mock-mediawiki.svg)](https://www.npmjs.com/package/mock-mediawiki)
4+
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com)
45

56
Honest MediaWiki JS interface mocking in Node.js.
67

@@ -55,8 +56,3 @@ For mw.language, [convertGrammar specialisations](https://github.com/wikimedia/m
5556

5657
Please file an issue if anything doesn't work.
5758

58-
### To-do
59-
60-
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com)
61-
62-
- [ ] Add mw.cookie

index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ require("./lib/mediawiki.api/user");
3232
require("./lib/mediawiki.api/watch");
3333
require("./lib/mediawiki.storage");
3434
require("./lib/mediawiki.template");
35+
require("./lib/jquery.cookie/jquery.cookie");
36+
require("./lib/mediawiki.cookie/mediawiki.cookie");
3537
require("./lib/CLDRPluralRuleParser/CLDRPluralRuleParser");
3638
mw.libs.pluralRuleParser = window.pluralRuleParser;
3739
require("./lib/mediawiki.cldr/index");

lib/jquery.cookie/jquery.cookie.js

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*!
2+
* jQuery Cookie Plugin v1.3.1
3+
* https://github.com/carhartl/jquery-cookie
4+
*
5+
* Copyright 2013 Klaus Hartl
6+
* Released under the MIT license
7+
*
8+
* Patched for MediaWiki to handle SameSite flag.
9+
*/
10+
(function ($, document, undefined) {
11+
12+
var pluses = /\+/g;
13+
14+
function raw(s) {
15+
return s;
16+
}
17+
18+
function decoded(s) {
19+
try {
20+
return unRfc2068(decodeURIComponent(s.replace(pluses, ' ')));
21+
} catch(e) {
22+
// If the cookie cannot be decoded this should not throw an error.
23+
// See T271838.
24+
return '';
25+
}
26+
}
27+
28+
function unRfc2068(value) {
29+
if (value.indexOf('"') === 0) {
30+
// This is a quoted cookie as according to RFC2068, unescape
31+
value = value.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
32+
}
33+
return value;
34+
}
35+
36+
function fromJSON(value) {
37+
return config.json ? JSON.parse(value) : value;
38+
}
39+
40+
var config = $.cookie = function (key, value, options) {
41+
42+
// write
43+
if (value !== undefined) {
44+
options = $.extend({}, config.defaults, options);
45+
46+
if (value === null) {
47+
options.expires = -1;
48+
}
49+
50+
if (typeof options.expires === 'number') {
51+
var days = options.expires, t = options.expires = new Date();
52+
t.setDate(t.getDate() + days);
53+
}
54+
55+
value = config.json ? JSON.stringify(value) : String(value);
56+
57+
return (document.cookie = [
58+
encodeURIComponent(key), '=', config.raw ? value : encodeURIComponent(value),
59+
options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE
60+
options.path ? '; path=' + options.path : '',
61+
options.domain ? '; domain=' + options.domain : '',
62+
options.secure ? '; secure' : '',
63+
// PATCH: handle SameSite flag --tgr
64+
options.sameSite ? '; samesite=' + options.sameSite : ''
65+
].join(''));
66+
}
67+
68+
// read
69+
var decode = config.raw ? raw : decoded;
70+
var cookies = document.cookie.split('; ');
71+
var result = key ? null : {};
72+
for (var i = 0, l = cookies.length; i < l; i++) {
73+
var parts = cookies[i].split('=');
74+
var name = decode(parts.shift());
75+
var cookie = decode(parts.join('='));
76+
77+
if (key && key === name) {
78+
result = fromJSON(cookie);
79+
break;
80+
}
81+
82+
if (!key) {
83+
result[name] = fromJSON(cookie);
84+
}
85+
}
86+
87+
return result;
88+
};
89+
90+
config.defaults = {};
91+
92+
$.removeCookie = function (key, options) {
93+
if ($.cookie(key) !== null) {
94+
$.cookie(key, null, options);
95+
return true;
96+
}
97+
return false;
98+
};
99+
100+
})(jQuery, document);

lib/mediawiki.cookie/config.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"prefix": "testwiki",
3+
"domain": "",
4+
"path": "/",
5+
"expires": 2592000,
6+
"sameSiteLegacy": true
7+
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
'use strict';
2+
3+
var config = require( './config.json' ),
4+
defaults = {
5+
prefix: config.prefix,
6+
domain: config.domain,
7+
path: config.path,
8+
expires: config.expires,
9+
secure: false,
10+
sameSite: '',
11+
sameSiteLegacy: config.sameSiteLegacy
12+
};
13+
14+
/**
15+
* Manage cookies in a way that is syntactically and functionally similar
16+
* to the `WebRequest#getCookie` and `WebResponse#setcookie` methods in PHP.
17+
*
18+
* @author Sam Smith <samsmith@wikimedia.org>
19+
* @author Matthew Flaschen <mflaschen@wikimedia.org>
20+
*
21+
* @class mw.cookie
22+
* @singleton
23+
*/
24+
mw.cookie = {
25+
26+
/**
27+
* Set or delete a cookie.
28+
*
29+
* **Note:** If explicitly passing `null` or `undefined` for an options key,
30+
* that will override the default. This is natural in JavaScript, but noted
31+
* here because it is contrary to MediaWiki's `WebResponse#setcookie()` method
32+
* in PHP.
33+
*
34+
* When using this for persistent storage of identifiers (e.g. for tracking
35+
* sessions), be aware that persistence may vary slightly across browsers and
36+
* browser versions, and can be affected by a number of factors such as
37+
* storage limits (cookie eviction) and session restore features.
38+
*
39+
* Without an expiry, this creates a session cookie. In a browser, session cookies persist
40+
* for the lifetime of the browser *process*. Including across tabs, page views, and windows,
41+
* until the browser itself is *fully* closed, or until the browser clears all storage for
42+
* a given website. An exception to this is if the user evokes a "restore previous
43+
* session" feature that some browsers have.
44+
*
45+
* @param {string} key
46+
* @param {string|null} value Value of cookie. If `value` is `null` then this method will
47+
* instead remove a cookie by name of `key`.
48+
* @param {Object|Date|number} [options] Options object, or expiry date
49+
* @param {Date|number|null} [options.expires=wgCookieExpiration] The expiry date of the cookie,
50+
* or lifetime in seconds. If `options.expires` is null or 0, then a session cookie is set.
51+
* @param {string} [options.prefix=wgCookiePrefix] The prefix of the key
52+
* @param {string} [options.domain=wgCookieDomain] The domain attribute of the cookie
53+
* @param {string} [options.path=wgCookiePath] The path attribute of the cookie
54+
* @param {boolean} [options.secure=false] Whether or not to include the secure attribute.
55+
* (Does **not** use the wgCookieSecure configuration variable)
56+
* @param {string} [options.sameSite=''] The SameSite flag of the cookie ('None' / 'Lax'
57+
* / 'Strict', case-insensitive; default is to omit the flag, which results in Lax on
58+
* modern browsers). Set to None AND set secure=true if the cookie needs to be visible on
59+
* cross-domain requests.
60+
* @param {boolean} [options.sameSiteLegacy=$wgUseSameSiteLegacyCookies] If true, sameSite=None
61+
* cookies will also be sent as a non-SameSite cookie with an 'ss0-' prefix, to work around
62+
* old browsers interpreting the standard differently.
63+
*/
64+
set: function ( key, value, options ) {
65+
var prefix, date, sameSiteLegacy;
66+
67+
// The 'options' parameter may be a shortcut for the expiry.
68+
if ( arguments.length > 2 && ( !options || options instanceof Date || typeof options === 'number' ) ) {
69+
options = { expires: options };
70+
}
71+
// Apply defaults
72+
options = $.extend( {}, defaults, options );
73+
74+
// Don't pass invalid option to $.cookie
75+
prefix = options.prefix;
76+
delete options.prefix;
77+
78+
if ( !options.expires ) {
79+
// Session cookie (null or zero)
80+
// Normalize to absent (undefined) for $.cookie.
81+
delete options.expires;
82+
} else if ( typeof options.expires === 'number' ) {
83+
// Lifetime in seconds
84+
date = new Date();
85+
date.setTime( Number( date ) + ( options.expires * 1000 ) );
86+
options.expires = date;
87+
}
88+
89+
sameSiteLegacy = options.sameSiteLegacy;
90+
delete options.sameSiteLegacy;
91+
92+
if ( value !== null ) {
93+
value = String( value );
94+
}
95+
96+
$.cookie( prefix + key, value, options );
97+
if ( sameSiteLegacy && options.sameSite && options.sameSite.toLowerCase() === 'none' ) {
98+
// Make testing easy by not changing the object passed to the first $.cookie call
99+
options = $.extend( {}, options );
100+
delete options.sameSite;
101+
$.cookie( prefix + 'ss0-' + key, value, options );
102+
}
103+
},
104+
105+
/**
106+
* Get the value of a cookie.
107+
*
108+
* @param {string} key
109+
* @param {string} [prefix=wgCookiePrefix] The prefix of the key. If `prefix` is
110+
* `undefined` or `null`, then `wgCookiePrefix` is used
111+
* @param {Mixed} [defaultValue=null]
112+
* @return {string|null|Mixed} If the cookie exists, then the value of the
113+
* cookie, otherwise `defaultValue`
114+
*/
115+
get: function ( key, prefix, defaultValue ) {
116+
var result;
117+
118+
if ( prefix === undefined || prefix === null ) {
119+
prefix = defaults.prefix;
120+
}
121+
122+
// Was defaultValue omitted?
123+
if ( arguments.length < 3 ) {
124+
defaultValue = null;
125+
}
126+
127+
result = $.cookie( prefix + key );
128+
129+
return result !== null ? result : defaultValue;
130+
},
131+
132+
/**
133+
* Get the value of a SameSite=None cookie, using the legacy ss0- cookie if needed.
134+
*
135+
* @param {string} key
136+
* @param {string} [prefix=wgCookiePrefix] The prefix of the key. If `prefix` is
137+
* `undefined` or `null`, then `wgCookiePrefix` is used
138+
* @param {Mixed} [defaultValue=null]
139+
* @return {string|null|Mixed} If the cookie exists, then the value of the
140+
* cookie, otherwise `defaultValue`
141+
*/
142+
getCrossSite: function ( key, prefix, defaultValue ) {
143+
var value;
144+
145+
value = this.get( key, prefix, null );
146+
if ( value === null ) {
147+
value = this.get( 'ss0-' + key, prefix, null );
148+
}
149+
if ( value === null ) {
150+
value = defaultValue;
151+
}
152+
return value;
153+
}
154+
};
155+
156+
if ( window.QUnit ) {
157+
module.exports = {
158+
setDefaults: function ( value ) {
159+
var prev = defaults;
160+
defaults = value;
161+
return prev;
162+
}
163+
};
164+
}

tests/jest.test.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ describe('test', function () {
6060
mw.loader.addStyleTag('body { font-size: 10px; }');
6161
}, 10000);
6262

63+
test('cookie', () => {
64+
mw.cookie.set('key', 'value');
65+
expect(mw.cookie.get('key')).toBe('value');
66+
});
67+
6368
test('api', async () => {
6469
let api = new mw.Api({
6570
ajax: {

tests/mocha.test.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ describe('test', function () {
6363
mw.loader.addStyleTag('body { font-size: 10px; }');
6464
});
6565

66+
it('cookie', () => {
67+
mw.cookie.set('key', 'value');
68+
assert.strictEqual(mw.cookie.get('key'),'value');
69+
});
70+
6671
it('api with login', async () => {
6772
if (process.env.WMF_USERNAME && process.env.WMF_PASSWORD) {
6873
let api = new mw.Api({

0 commit comments

Comments
 (0)