1 | ;
|
2 |
|
3 | Object.defineProperty(exports, "__esModule", {
|
4 | value: true
|
5 | });
|
6 |
|
7 | var _MapStorage = require("./MapStorage");
|
8 |
|
9 | var _MapStorage2 = _interopRequireDefault(_MapStorage);
|
10 |
|
11 | var _GenericError = require("../error/GenericError");
|
12 |
|
13 | var _GenericError2 = _interopRequireDefault(_GenericError);
|
14 |
|
15 | var _Request = require("../router/Request");
|
16 |
|
17 | var _Request2 = _interopRequireDefault(_Request);
|
18 |
|
19 | var _Response = require("../router/Response");
|
20 |
|
21 | var _Response2 = _interopRequireDefault(_Response);
|
22 |
|
23 | var _Window = require("../window/Window");
|
24 |
|
25 | var _Window2 = _interopRequireDefault(_Window);
|
26 |
|
27 | function _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 | */
|
36 | const 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 |
|
45 | const 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 |
|
52 | class 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 |
|
550 | exports.default = CookieStorage;
|
551 |
|
552 | typeof $IMA !== 'undefined' && $IMA !== null && $IMA.Loader && $IMA.Loader.register('ima/storage/CookieStorage', [], function (_export, _context) {
|
553 | ;
|
554 | return {
|
555 | setters: [],
|
556 | execute: function () {
|
557 | _export('default', exports.default);
|
558 | }
|
559 | };
|
560 | });
|