UNPKG

15.4 kBJavaScriptView Raw
1"use strict";
2
3Object.defineProperty(exports, "__esModule", {
4 value: true
5});
6
7var _MapStorage = require("./MapStorage");
8
9var _MapStorage2 = _interopRequireDefault(_MapStorage);
10
11var _GenericError = require("../error/GenericError");
12
13var _GenericError2 = _interopRequireDefault(_GenericError);
14
15var _Request = require("../router/Request");
16
17var _Request2 = _interopRequireDefault(_Request);
18
19var _Response = require("../router/Response");
20
21var _Response2 = _interopRequireDefault(_Response);
22
23var _Window = require("../window/Window");
24
25var _Window2 = _interopRequireDefault(_Window);
26
27function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
28
29/**
30 * Implementation note: This is the largest possible safe value that has been
31 * tested, used to represent "infinity".
32 *
33 * @const
34 * @type {Date}
35 */
36const MAX_EXPIRE_DATE = new Date('Sat Sep 13 275760 00:00:00 GMT+0000 (UTC)');
37/**
38 * Separator used to separate cookie declarations in the {@code Cookie} HTTP
39 * header or the return value of the {@code document.cookie} property.
40 *
41 * @const
42 * @type {string}
43 */
44
45const COOKIE_SEPARATOR = '; ';
46/**
47 * Storage of cookies, mirroring the cookies to the current request / response
48 * at the server side and the {@code document.cookie} property at the client
49 * side. The storage caches the cookies internally.
50 */
51
52class CookieStorage extends _MapStorage2.default {
53 static get $dependencies() {
54 return [_Window2.default, _Request2.default, _Response2.default];
55 }
56 /**
57 * Initializes the cookie storage.
58 *
59 * @param {Window} window The window utility.
60 * @param {Request} request The current HTTP request.
61 * @param {Response} response The current HTTP response.
62 * @example
63 * cookie.set('cookie', 'value', { expires: 10 }); // cookie expires
64 * // after 10s
65 * cookie.set('cookie'); // delete cookie
66 *
67 */
68
69
70 constructor(window, request, response) {
71 super();
72 /**
73 * The window utility used to determine whether the IMA is being run
74 * at the client or at the server.
75 *
76 * @type {Window}
77 */
78
79 this._window = window;
80 /**
81 * The current HTTP request. This field is used at the server side.
82 *
83 * @type {Request}
84 */
85
86 this._request = request;
87 /**
88 * The current HTTP response. This field is used at the server side.
89 *
90 * @type {Response}
91 */
92
93 this._response = response;
94 /**
95 * The overriding cookie attribute values.
96 *
97 * @type {{
98 * path: string,
99 * secure: boolean,
100 * httpOnly: boolean,
101 * domain: string,
102 * expires: ?(number|Date),
103 * maxAge: ?number
104 * }}
105 */
106
107 this._options = {
108 path: '/',
109 expires: null,
110 maxAge: null,
111 secure: false,
112 httpOnly: false,
113 domain: ''
114 };
115 /**
116 * Transform encode and decode functions for cookie value.
117 *
118 * @type {{
119 * encode: function(string): string,
120 * decode: function(string): string
121 * }}
122 */
123
124 this._transformFunction = {
125 encode: value => value,
126 decode: value => value
127 };
128 }
129 /**
130 * @inheritdoc
131 * @param {{
132 * path: string=,
133 * secure: boolean=,
134 * httpOnly: boolean=,
135 * domain: string=,
136 * expires: ?(number|Date)=,
137 * maxAge: ?number=
138 * }} options
139 * @param {{
140 * encode: function(string): string=,
141 * decode: function(string): string=
142 * }} transformFunction
143 */
144
145
146 init(options = {}, transformFunction = {}) {
147 this._transformFunction = Object.assign(this._transformFunction, transformFunction);
148 this._options = Object.assign(this._options, options);
149
150 this._parse();
151
152 return this;
153 }
154 /**
155 * @inheritdoc
156 */
157
158
159 has(name) {
160 return super.has(name);
161 }
162 /**
163 * @inheritdoc
164 */
165
166
167 get(name) {
168 if (super.has(name)) {
169 return super.get(name).value;
170 } else {
171 return undefined;
172 }
173 }
174 /**
175 * @inheritdoc
176 * @param {string} name The key identifying the storage entry.
177 * @param {*} value The storage entry value.
178 * @param {{
179 * maxAge: number=,
180 * expires: (string|Date)=,
181 * domain: string=,
182 * path: string=,
183 * httpOnly: boolean=,
184 * secure: boolean=
185 * }=} options The cookie options. The {@code maxAge} is the maximum
186 * age in seconds of the cookie before it will be deleted, the
187 * {@code expires} is an alternative to that, specifying the moment
188 * at which the cookie will be discarded. The {@code domain} and
189 * {@code path} specify the cookie's domain and path. The
190 * {@code httpOnly} and {@code secure} flags set the flags of the
191 * same name of the cookie.
192 */
193
194
195 set(name, value, options = {}) {
196 options = Object.assign({}, this._options, options);
197
198 if (value === undefined) {
199 // Deletes the cookie
200 options.maxAge = 0;
201 options.expires = this._getExpirationAsDate(-1);
202 } else {
203 this._recomputeCookieMaxAgeAndExpires(options);
204 }
205
206 value = this._sanitizeCookieValue(value + '');
207
208 if (this._window.isClient()) {
209 document.cookie = this._generateCookieString(name, value, options);
210 } else {
211 this._response.setCookie(name, value, options);
212 }
213
214 super.set(name, {
215 value,
216 options
217 });
218 return this;
219 }
220 /**
221 * Deletes the cookie identified by the specified name.
222 *
223 * @param {string} name Name identifying the cookie.
224 * @param {{
225 * domain: string=,
226 * path: string=,
227 * httpOnly: boolean=,
228 * secure: boolean=
229 * }=} options The cookie options. The {@code domain} and
230 * {@code path} specify the cookie's domain and path. The
231 * {@code httpOnly} and {@code secure} flags set the flags of the
232 * same name of the cookie.
233 * @return {Storage} This storage.
234 */
235
236
237 delete(name, options = {}) {
238 if (this.has(name)) {
239 this.set(name, undefined, options);
240 super.delete(name);
241 }
242
243 return this;
244 }
245 /**
246 * @inheritdoc
247 */
248
249
250 clear() {
251 for (let cookieName of super.keys()) {
252 this.delete(cookieName);
253 }
254
255 return super.clear();
256 }
257 /**
258 * @inheritdoc
259 */
260
261
262 keys() {
263 return super.keys();
264 }
265 /**
266 * @inheritdoc
267 */
268
269
270 size() {
271 return super.size();
272 }
273 /**
274 * Returns all cookies in this storage serialized to a string compatible
275 * with the {@code Cookie} HTTP header.
276 *
277 * @return {string} All cookies in this storage serialized to a string
278 * compatible with the {@code Cookie} HTTP header.
279 */
280
281
282 getCookiesStringForCookieHeader() {
283 let cookieStrings = [];
284
285 for (let cookieName of super.keys()) {
286 let cookieItem = super.get(cookieName);
287 cookieStrings.push(this._generateCookieString(cookieName, cookieItem.value, {}));
288 }
289
290 return cookieStrings.join(COOKIE_SEPARATOR);
291 }
292 /**
293 * Parses cookies from the provided {@code Set-Cookie} HTTP header value.
294 *
295 * The parsed cookies will be set to the internal storage, and the current
296 * HTTP response (via the {@code Set-Cookie} HTTP header) if at the server
297 * side, or the browser (via the {@code document.cookie} property).
298 *
299 * @param {string} setCookieHeader The value of the {@code Set-Cookie} HTTP
300 * header.
301 */
302
303
304 parseFromSetCookieHeader(setCookieHeader) {
305 let cookie = this._extractCookie(setCookieHeader);
306
307 if (cookie.name !== null) {
308 this.set(cookie.name, cookie.value, cookie.options);
309 }
310 }
311 /**
312 * Parses cookies from a cookie string and sets the parsed cookies to the
313 * internal storage.
314 *
315 * The method obtains the cookie string from the request's {@code Cookie}
316 * HTTP header when used at the server side, and the {@code document.cookie}
317 * property at the client side.
318 */
319
320
321 _parse() {
322 let cookiesString = this._window.isClient() ? document.cookie : this._request.getCookieHeader();
323 let cookiesArray = cookiesString ? cookiesString.split(COOKIE_SEPARATOR) : [];
324
325 for (let i = 0; i < cookiesArray.length; i++) {
326 let cookie = this._extractCookie(cookiesArray[i]);
327
328 if (cookie.name !== null) {
329 cookie.options = Object.assign({}, this._options, cookie.options);
330 super.set(cookie.name, {
331 value: this._sanitizeCookieValue(cookie.value),
332 options: cookie.options
333 });
334 }
335 }
336 }
337 /**
338 * Creates a copy of the provided word (or text) that has its first
339 * character converted to lower case.
340 *
341 * @param {string} word The word (or any text) that should have its first
342 * character converted to lower case.
343 * @return {string} A copy of the provided string with its first character
344 * converted to lower case.
345 */
346
347
348 _firstLetterToLowerCase(word) {
349 return word.charAt(0).toLowerCase() + word.substring(1);
350 }
351 /**
352 * Generates a string representing the specified cookied, usable either
353 * with the {@code document.cookie} property or the {@code Set-Cookie} HTTP
354 * header.
355 *
356 * (Note that the {@code Cookie} HTTP header uses a slightly different
357 * syntax.)
358 *
359 * @param {string} name The cookie name.
360 * @param {(boolean|number|string)} value The cookie value, will be
361 * converted to string.
362 * @param {{
363 * path: string=,
364 * domain: string=,
365 * expires: Date=,
366 * maxAge: Number=,
367 * secure: boolean=
368 * }} options Cookie attributes. Only the attributes listed in the
369 * type annotation of this field are supported. For documentation
370 * and full list of cookie attributes see
371 * http://tools.ietf.org/html/rfc2965#page-5
372 * @return {string} A string representing the cookie. Setting this string
373 * to the {@code document.cookie} property will set the cookie to
374 * the browser's cookie storage.
375 */
376
377
378 _generateCookieString(name, value, options) {
379 let cookieString = name + '=' + this._transformFunction.encode(value);
380
381 cookieString += options.domain ? ';Domain=' + options.domain : '';
382 cookieString += options.path ? ';Path=' + options.path : '';
383 cookieString += options.expires ? ';Expires=' + options.expires.toUTCString() : '';
384 cookieString += options.maxAge ? ';Max-Age=' + options.maxAge : '';
385 cookieString += options.httpOnly ? ';HttpOnly' : '';
386 cookieString += options.secure ? ';Secure' : '';
387 return cookieString;
388 }
389 /**
390 * Converts the provided cookie expiration to a {@code Date} instance.
391 *
392 * @param {(number|string|Date)} expiration Cookie expiration in seconds
393 * from now, or as a string compatible with the {@code Date}
394 * constructor.
395 * @return {Date} Cookie expiration as a {@code Date} instance.
396 */
397
398
399 _getExpirationAsDate(expiration) {
400 if (expiration instanceof Date) {
401 return expiration;
402 }
403
404 if (typeof expiration === 'number') {
405 return expiration === Infinity ? MAX_EXPIRE_DATE : new Date(Date.now() + expiration * 1000);
406 }
407
408 return expiration ? new Date(expiration) : MAX_EXPIRE_DATE;
409 }
410 /**
411 * Extract cookie name, value and options from cookie string.
412 *
413 * @param {string} cookieString The value of the {@code Set-Cookie} HTTP
414 * header.
415 * @return {{
416 * name: ?string,
417 * value: ?string,
418 * options: Object<string, (boolean|Date)>
419 * }}
420 */
421
422
423 _extractCookie(cookieString) {
424 let cookieOptions = {};
425 let cookieName = null;
426 let cookieValue = null;
427 let cookiePairs = cookieString.split(COOKIE_SEPARATOR.trim());
428 cookiePairs.forEach((pair, index) => {
429 let [name, value] = this._extractNameAndValue(pair, index);
430
431 if (index === 0) {
432 cookieName = name;
433 cookieValue = value;
434 } else {
435 cookieOptions[name] = value;
436 }
437 });
438 return {
439 name: cookieName,
440 value: cookieValue,
441 options: cookieOptions
442 };
443 }
444 /**
445 * Extract name and value for defined pair and pair index.
446 *
447 * @param {string} pair
448 * @param {number} pairIndex
449 * @return {Array<?(boolean|string|Date)>}
450 */
451
452
453 _extractNameAndValue(pair, pairIndex) {
454 let separatorIndexEqual = pair.indexOf('=');
455 let name = '';
456 let value = null;
457
458 if (pairIndex === 0 && separatorIndexEqual < 0) {
459 return [null, null];
460 }
461
462 if (separatorIndexEqual < 0) {
463 name = pair.trim();
464 value = true;
465 } else {
466 name = pair.substring(0, separatorIndexEqual).trim();
467 value = this._transformFunction.decode(pair.substring(separatorIndexEqual + 1).trim()); // erase quoted values
468
469 if ('"' === value[0]) {
470 value = value.slice(1, -1);
471 }
472
473 if (name === 'Expires') {
474 value = this._getExpirationAsDate(value);
475 }
476
477 if (name === 'Max-Age') {
478 name = 'maxAge';
479 value = parseInt(value, 10);
480 }
481 }
482
483 if (pairIndex !== 0) {
484 name = this._firstLetterToLowerCase(name);
485 }
486
487 return [name, value];
488 }
489 /**
490 * Sanitize cookie value by rules in
491 * (@see http://tools.ietf.org/html/rfc6265#section-4r.1.1). Erase all
492 * invalid characters from cookie value.
493 *
494 * @param {string} value Cookie value
495 * @return {string} Sanitized value
496 */
497
498
499 _sanitizeCookieValue(value) {
500 let sanitizedValue = '';
501
502 for (let keyChar = 0; keyChar < value.length; keyChar++) {
503 let charCode = value.charCodeAt(keyChar);
504 let char = value[keyChar];
505 let isValid = charCode >= 33 && charCode <= 126 && char !== '"' && char !== ';' && char !== '\\';
506
507 if (isValid) {
508 sanitizedValue += char;
509 } else {
510 if ($Debug) {
511 throw new _GenericError2.default(`Invalid char ${char} code ${charCode} in ${value}. ` + `Dropping the invalid character from the cookie's ` + `value.`, {
512 value,
513 charCode,
514 char
515 });
516 }
517 }
518 }
519
520 return sanitizedValue;
521 }
522 /**
523 * Recomputes cookie's attributes maxAge and expires between each other.
524 *
525 * @param {{
526 * path: string=,
527 * domain: string=,
528 * expires: Date=,
529 * maxAge: Number=,
530 * secure: boolean=
531 * }} options Cookie attributes. Only the attributes listed in the
532 * type annotation of this field are supported. For documentation
533 * and full list of cookie attributes see
534 * http://tools.ietf.org/html/rfc2965#page-5
535 */
536
537
538 _recomputeCookieMaxAgeAndExpires(options) {
539 if (options.maxAge || options.expires) {
540 options.expires = this._getExpirationAsDate(options.maxAge || options.expires);
541 }
542
543 if (!options.maxAge && options.expires) {
544 options.maxAge = Math.floor((options.expires.valueOf() - Date.now()) / 1000);
545 }
546 }
547
548}
549
550exports.default = CookieStorage;
551
552typeof $IMA !== 'undefined' && $IMA !== null && $IMA.Loader && $IMA.Loader.register('ima/storage/CookieStorage', [], function (_export, _context) {
553 'use strict';
554 return {
555 setters: [],
556 execute: function () {
557 _export('default', exports.default);
558 }
559 };
560});