This repository was archived by the owner on Mar 3, 2026. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathindex.js
More file actions
499 lines (458 loc) · 14.7 KB
/
index.js
File metadata and controls
499 lines (458 loc) · 14.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
const dayjs = require('dayjs');
module.exports = function(options) {
const self = {};
self.options = options || {};
self.filterTag = self.options.filterTag || function(tag) {
tag = tag.trim();
return tag.toLowerCase();
};
self.string = function(s, def) {
if (typeof (s) !== 'string') {
if ((typeof (s) === 'number') || (typeof (s) === 'boolean')) {
s += '';
} else {
s = '';
}
}
s = s.trim();
if (def !== undefined) {
if (s === '') {
s = def;
}
}
return s;
};
self.strings = function(strings) {
if (!Array.isArray(strings)) {
return [];
}
return strings.map(function(s) {
return self.string(s);
});
};
self.integer = function(i, def, min, max) {
if (def === undefined) {
def = 0;
}
if (typeof (i) === 'number') {
i = Math.floor(i);
} else {
try {
i = parseInt(i, 10);
if (isNaN(i)) {
i = def;
}
} catch (e) {
i = def;
}
}
if ((typeof (min) === 'number') && (i < min)) {
i = min;
}
if ((typeof (max) === 'number') && (i > max)) {
i = max;
}
return i;
};
self.padInteger = function(i, places) {
let s = i + '';
while (s.length < places) {
s = '0' + s;
}
return s;
};
self.float = function(i, def, min, max) {
if (def === undefined) {
def = 0;
}
if (!(typeof (i) === 'number')) {
try {
i = parseFloat(i, 10);
if (isNaN(i)) {
i = def;
}
} catch (e) {
i = def;
}
}
if ((typeof (min) === 'number') && (i < min)) {
i = min;
}
if ((typeof (max) === 'number') && (i > max)) {
i = max;
}
return i;
};
self.url = function(s, def, httpsFix) {
s = self.string(s, def);
// Allow the default to be undefined, null, false, etc.
if (s === def) {
return s;
}
s = fixUrl(s);
if (s === null) {
return def;
}
s = naughtyHref(s);
if (s === true) {
return def;
}
return s;
function fixUrl(href) {
if (href.match(/^(((https?|ftp):\/\/)|((mailto|tel|sms):)|#|([^/.]+)?\/|[^/.]+$)/)) {
// All good - no change required
return href;
} else if (href.match(/^[^/.]+\.[^/.]+/)) {
// Smells like a domain name. Educated guess: they left off http://
const protocol = httpsFix ? 'https://' : 'http://';
return protocol + href;
} else {
return null;
}
};
function naughtyHref(href) {
// Browsers ignore character codes of 32 (space) and below in a surprising
// number of situations. Start reading here:
// https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Embedded_tab
// eslint-disable-next-line no-control-regex
href = href.replace(/[\x00-\x20]+/g, '');
// Clobber any comments in URLs, which the browser might
// interpret inside an XML data island, allowing
// a javascript: URL to be snuck through
while (true) {
const firstIndex = href.indexOf('<!--');
if (firstIndex === -1) {
break;
}
const lastIndex = href.indexOf('-->', firstIndex + 4);
if (lastIndex === -1) {
break;
}
href = href.substring(0, firstIndex) + href.substring(lastIndex + 3);
}
// Case insensitive so we don't get faked out by JAVASCRIPT #1
// Allow more characters after the first so we don't get faked
// out by certain schemes browsers accept
const matches = href.match(/^([a-zA-Z]+):/);
if (!matches) {
// No scheme = no way to inject js (right?)
return href;
}
const scheme = matches[1].toLowerCase();
return (![ 'http', 'https', 'ftp', 'mailto', 'tel', 'sms' ].includes(scheme)) ? true : href;
}
};
self.select = function(s, choices, def) {
s = self.string(s);
if (!choices || !choices.length) {
return def;
}
let choice;
if (typeof (choices[0]) === 'object') {
choice = choices.find(function(choice) {
if ((choice.value === null) || (choice.value === undefined)) {
// Don't crash on invalid choices
return false;
}
return choice.value.toString() === s;
});
if (choice != null) {
return choice.value;
}
return def;
}
choice = choices.find(function(choice) {
if ((choice === null) || (choice === undefined)) {
// Don't crash on invalid choices
return false;
}
return choice.toString() === s;
});
if (choice !== undefined) {
return choice;
}
return def;
};
self.boolean = function(b, def) {
if (b === true) {
return true;
}
if (b === false) {
return false;
}
b = self.string(b, def);
if (b === def) {
if (b === undefined) {
return false;
}
return b;
}
b = b.toLowerCase().charAt(0);
if ((b === '') || (b === 'n') || (b === '0') || (b === 'f')) {
return false;
}
if ((b === 't') || (b === 'y') || (b === '1')) {
return true;
}
return false;
};
// Given an `options` object in which options[name] is a string
// set to '0', '1', or 'any', this method adds mongodb criteria
// to the `criteria` object.
//
// '0' or false means "the property must be false or absent," '1' or true
// means "the property must be true," and 'any' or null means "we don't care
// what the property is."
//
// See `booleanOrNull` for additional synonyms accepted for the
// three possible values.
//
// An empty string is considered equivalent to '0'.
//
// This is not the same as apos.sanitizeBoolean which is concerned only with
// true or false and does not address "any."
//
// `def` defaults to `any`.
//
// This method is most often used with REST API parameters and forms.
self.addBooleanFilterToCriteria = function(options, name, criteria, def) {
// if any or null, we aren't changing criteria
if (def === undefined) {
def = null;
}
// allow object or boolean
let value = (typeof (options) === 'object' && options !== null) ? options[name] : options;
value = (value === undefined) ? def : value;
value = self.booleanOrNull(value);
if (value === null) {
// Don't care, show all
} else if (!value) {
// Must be absent or false. Hooray for $ne
criteria[name] = { $ne: true };
} else {
// Must be true
criteria[name] = true;
}
};
// This method is used for tristate filters, i.e. "published,"
// "unpublished", and "show me both".
//
// Accepts `true`, `false`, or `null` and returns them exactly
// as such; if the parameter is none of those or their synonyms
// below, returns `def`.
//
// Also accepts the strings `'yes'` (or starting with y), `'no'` (or
// starting with n), `'true'` (or starting with t), `'false'`
// (or starting with f), `'1'`, `'0'` and the strings `'any'` and
// `'null'`. The string `'null'` must be an exact match, anything
// else starting with `n` is taken as `no` (false).
//
// These various synonyms are useful for string input, such as from
// a user friendly query string.
self.booleanOrNull = function(b, def) {
if (b === true) {
return b;
}
if (b === false) {
return b;
}
if (b === null) {
return b;
}
b = self.string(b, def);
if (b === def) {
if (def === undefined) {
return null;
}
return b;
}
// String 'null' must match as a full string to disambiguate from string 'n' for 'no'
if (b === 'null') {
return null;
}
b = b.toLowerCase().charAt(0);
if ((b === '') || (b === 'n') || (b === '0') || (b === 'f')) {
return false;
}
if ((b === 't') || (b === 'y') || (b === '1')) {
return true;
}
if ((b === 'a')) {
return null;
}
return def;
};
// Accept a user-entered string in YYYY-MM-DD, MM/DD, MM/DD/YY, or MM/DD/YYYY format
// (tolerates missing leading zeroes on MM and DD). Also accepts a Date object.
// Returns YYYY-MM-DD.
//
// The current year is assumed when MM/DD is used. If there is no explicit default
// any unparseable date is returned as today's date.
//
// If the default is explicitly `null` (not `undefined`) then `null` is returned for
// any unparseable date.
//
// The `now` argument can be passed for performance if you prefer to call
// `new Date()` just once before many calls to this method and pass that single
// value to all of them.
self.date = function(date, def, now) {
let components;
function returnDefault() {
if (def === undefined) {
def = dayjs().format('YYYY-MM-DD');
}
return def;
}
if (typeof (date) === 'string') {
if (date.match(/\//)) {
components = date.split('/');
if (components.length === 2) {
// Convert mm/dd to yyyy-mm-dd
return (now || new Date()).getFullYear() + '-' + self.padInteger(components[0], 2) + '-' + self.padInteger(components[1], 2);
} else if (components.length === 3) {
// Convert mm/dd/yy to mm/dd/yyyy
if (components[2] < 100) {
// Add the current century. If the result is more than
// 50 years in the future, assume they meant the
// previous century. Thus in 2015, we find that
// we get the intuitive result for both 1/1/75,
// 1/1/99 and 1/1/25. It's a nasty habit among
// us imprecise humans. -Tom
const d = (now || new Date());
const nowYear = d.getFullYear() % 100;
const nowCentury = d.getFullYear() - nowYear;
let theirYear = parseInt(components[2]) + nowCentury;
if (theirYear - d.getFullYear() > 50) {
theirYear -= 100;
}
components[2] = theirYear;
}
// Convert mm/dd/yyyy to yyyy-mm-dd
return self.padInteger(components[2], 4) + '-' + self.padInteger(components[0], 2) + '-' + self.padInteger(components[1], 2);
} else {
return returnDefault();
}
} else if (date.match(/-/)) {
components = date.split('-');
if (components.length === 2) {
// Convert mm-dd to yyyy-mm-dd
return (now || new Date()).getFullYear() + '-' + self.padInteger(components[0], 2) + '-' + self.padInteger(components[1], 2);
} else if (components.length === 3) {
// Convert yyyy-mm-dd (with questionable padding) to yyyy-mm-dd
return self.padInteger(components[0], 4) + '-' + self.padInteger(components[1], 2) + '-' + self.padInteger(components[2], 2);
} else {
return returnDefault();
}
}
}
try {
if (date === null) {
return returnDefault();
}
date = (now || new Date(date));
if (isNaN(date.getTime())) {
return returnDefault();
}
return date.getFullYear() + '-' + self.padInteger(date.getMonth() + 1, 2) + '-' + self.padInteger(date.getDate(), 2);
} catch (e) {
return returnDefault();
}
};
// This is likely not relevent to you unless you're using Apostrophe
// Given a date object, return a date string in Apostrophe's preferred sortable,
// comparable, JSON-able format, which is YYYY-MM-DD. If `date` is undefined
// the current date is used.
self.formatDate = function(date) {
return dayjs(date).format('YYYY-MM-DD');
};
// Accepts a user-entered string in 12-hour or 24-hour time and returns a string
// in 24-hour time. This method is tolerant of syntax such as `4pm`; minutes and
// seconds are optional.
//
// If `def` is not set the default is the current time.
self.time = function(time, def) {
time = self.string(time).toLowerCase();
time = time.trim();
const components = time.match(/^(\d+)([:|.](\d+))?([:|.](\d+))?\s*(am|pm|AM|PM|a|p|A|M)?$/);
if (components) {
let hours = parseInt(components[1], 10);
const minutes = (components[3] !== undefined) ? parseInt(components[3], 10) : 0;
const seconds = (components[5] !== undefined) ? parseInt(components[5], 10) : 0;
let ampm = (components[6]) ? components[6].toLowerCase() : components[6];
ampm = ampm && ampm.charAt(0);
if ((hours === 12) && (ampm === 'a')) {
hours -= 12;
} else if ((hours === 12) && (ampm === 'p')) {
// Leave it be
} else if (ampm === 'p') {
hours += 12;
}
if ((hours === 24) || (hours === '24')) {
hours = 0;
}
return self.padInteger(hours, 2) + ':' + self.padInteger(minutes, 2) + ':' + self.padInteger(seconds, 2);
} else {
if (def !== undefined) {
return def;
}
return dayjs().format('HH:mm');
}
};
// This is likely not relevent to you unless you're using Apostrophe
// Given a JavaScript Date object, return a time string in
// Apostrophe's preferred sortable, comparable, JSON-able format:
// 24-hour time, with seconds.
//
// If `date` is missing the current time is used.
self.formatTime = function(date) {
return dayjs(date).format('HH:mm:ss');
};
// Sanitize tags. Tags should be submitted as an array of strings,
// or a comma-separated string.
//
// This method ensures the input is an array or string and, if
// an array, that the elements of the array are strings.
//
// If a filterTag function is passed as an option when initializing
// Launder, then all tags are passed through it (as individual
// strings, one per call) and the return value is used instead. You
// may also pass a filterTag when calling this function
self.tags = function(tags, filter) {
if (typeof (tags) === 'string') {
tags = tags.split(/,\s*/);
}
if (!Array.isArray(tags)) {
return [];
}
const strings = tags.map(tag => self.string(tag));
const rewritten = strings.map(filter || self.filterTag);
const filtered = rewritten.filter(tag => tag.length > 0);
return filtered;
};
// Sanitize an id. IDs must consist solely of upper and lower case
// letters, numbers, and digits unless options.idRegExp is set.
self.idRegExp = self.options.idRegExp || /^[A-Za-z0-9_]+$/;
self.id = function(s, def) {
const id = self.string(s, def);
if (id === def) {
return id;
}
if (!id.match(self.idRegExp)) {
return def;
}
return id;
};
// Sanitize an array of IDs. IDs must consist solely of upper and lower case
// letters and numbers, digits, and underscores. Any elements that are not
// IDs are omitted from the final array.
self.ids = function(ids) {
if (!Array.isArray(ids)) {
return [];
}
const result = ids.filter(function(id) {
return (self.id(id) !== undefined);
});
return result;
};
return self;
};