UNPKG

apostrophe

Version:

Apostrophe is a user-friendly content management system. You'll need more than this core module. See apostrophenow.org to get started.

427 lines (392 loc) • 12.7 kB
var _ = require('lodash'); var sanitize = require('validator').sanitize; var moment = require('moment'); /** * sanitize * @augments Augments the apos object with methods that sanitize user input, * with a strong bias toward supplying a reasonable value. Also related convenience * methods which manipulate the results or provide the underpinnings for the * sanitization methods and may be used directly. */ module.exports = function(self) { // Simple string sanitization so junk submissions can't crash the app. Converts numbers to strings. // Converts other non-string types to the empty string. // Returns the value of `def` if the string is empty. self.sanitizeString = function(s, def) { if (typeof(s) !== 'string') { if (typeof(s) === 'number') { s += ''; } else { s = ''; } } s = s.trim(); if (def !== undefined) { if (s === '') { s = def; } } return s; }; // Sanitize a URL via apos.fixUrl, which can figure out how to fix // a variety of common errors in entering URLs. self.sanitizeUrl = function(s, def) { s = self.sanitizeString(s, def); // Allow the default to be undefined, null, false, etc. if (s === def) { return s; } s = self.fixUrl(s); if (s === null) { return def; } return s; }; // Fix common errors in URLs. // // Accepts valid http, https and ftp absolute and relative URLs, as well as mailto: URLs. If the URL smells like // it starts with a domain name, supplies an http:// prefix. // // There is a browser-side version in editor.js which is kept in sync. self.fixUrl = function(href) { if (href.match(/^(((https?|ftp)\:\/\/)|mailto\:|\#|([^\/\.]+)?\/|[^\/\.]+$)/)) { // All good - no change required return href; } else if (href.match(/^[^\/\.]+\.[^\/\.]+/)) { // Smells like a domain name. Educated guess: they left off http:// return 'http://' + href; } else { return null; } }; // Sanitize a select element. If the value is not one of the choices, returns `def`. // choices may be either an array of allowable values or an array of objects // with `value` properties. self.sanitizeSelect = function(s, choices, def) { if (!choices.length) { return def; } if (typeof(choices[0]) === 'object') { if (_.find(choices, function(choice) { return choice.value === s; })) { return s; } return def; } if (!_.contains(choices, s)) { return def; } return s; }; // Sanitize a boolean value: // // Accepts true, 'true', 't', '1', 1 as `true`. // // Accepts everything else as false. // // If nothing is submitted the default (def) is returned. // // If def is undefined the default is `false`. self.sanitizeBoolean = function(b, def) { if (b === true) { return true; } if (b === false) { return false; } b = self.sanitizeString(b, def); if (b === def) { if (b === undefined) { return false; } return b; } b = b.toLowerCase().charAt(0); if (b === '') { 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. // // false, true and null are accepted as synonyms for '0', '1' and 'any'. // // '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." // // 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.convertBooleanFilterCriteria = function(name, options, criteria, def) { if (def === undefined) { def = 'any'; } // Consume special options then remove them, turning the rest into mongo criteria if (def === undefined) { def = 'any'; } var value = (options[name] === undefined) ? def : options[name]; if ((value === 'any') || (value === null)) { // Don't care, show all } else if ((!value) || (value === '0')) { // Must be absent or false. Hooray for $ne criteria[name] = { $ne: true }; } else { // Must be true criteria[name] = true; } }; // Sanitize an integer. The value is clamped to be between `min` and `max` if // those arguments are not undefined. self.sanitizeInteger = 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 ((min !== undefined) && (i < min)) { i = min; } if ((max !== undefined) && (i > max)) { i = max; } return i; }; // Sanitize a floating point number. The value is clamped to be between `min` and `max` if // those arguments are not undefined. self.sanitizeFloat = function(i, def, min, max) { if (def === undefined) { def = 0; } if (typeof(i) === 'number') { i = Math.floor(i); } else { try { i = parseFloat(i, 10); if (isNaN(i)) { i = def; } } catch (e) { i = def; } } if ((min !== undefined) && (i < min)) { i = min; } if ((max !== undefined) && (i > max)) { i = max; } return i; }; // pad an integer with leading zeroes, creating a string self.padInteger = function(i, places) { var s = i + ''; while (s.length < places) { s = '0' + s; } return s; }; // 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. self.sanitizeDate = function(date, def) { var components; function returnDefault() { if (def === undefined) { def = moment().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 self.padInteger(new Date().getYear() + 1900, 4) + '-' + self.padInteger(components[0], 2) + '-' + self.padInteger(components[1], 2); } else if (components.length === 3) { // Convert mm/dd/yyyy to yyyy-mm-dd if (components[2] < 100) { components[2] += 1000; } 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 self.padInteger(new Date().getYear() + 1900, 4) + '-' + 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 { date = new Date(date); if (isNaN(date.getTime())) { return returnDefault(); } return self.padInteger(date.getYear() + 1900, 4) + '-' + self.padInteger(date.getMonth() + 1, 2) + '-' + self.padInteger(date.getDay(), 2); } catch (e) { return returnDefault(); } }; // 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 moment(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.sanitizeTime = function(time, def) { time = self.sanitizeString(time).toLowerCase(); time = time.trim(); var components = time.match(/^(\d+)(:(\d+))?(:(\d+))?\s*(am|pm)?$/); if (components) { var hours = parseInt(components[1], 10); var minutes = (components[3] !== undefined) ? parseInt(components[3], 10) : 0; var seconds = (components[5] !== undefined) ? parseInt(components[5], 10) : 0; var ampm = components[6]; if ((hours === 12) && (ampm === 'am')) { hours -= 12; } else if ((hours === 12) && (ampm === 'pm')) { // Leave it be } else if (ampm === 'pm') { 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 moment().format('HH:mm'); } }; // Requires a time in HH:MM or HH:MM:ss format. Returns // an object with hours, minutes and seconds properties. // See apos.sanitizeTime for an easy way to get a time into the // appropriate input format. self.parseTime = function(time) { var components = time.match(/^(\d\d):(\d\d)(:(\d\d))$/); return { hours: time[1], minutes: time[2], seconds: time[3] || 0 }; }; // 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 moment(date).format('HH:mm:ss'); }; // Sanitize tags. Tags should be submitted as an array of strings. // This method ensures the array is an array and the items in the // array are strings. This method may also be used to sanitize // an array of IDs. // // If a filterTag function is passed as an option when initializing // Apostrophe, then all tags are passed through it (as individual // strings, one per call) and the return value is used instead. This // is useful if you wish to force all-uppercase or all-lowercase // tags for a particular project. Be sure to update any existing // database entries for tags before making such a change. self.sanitizeTags = function(tags) { if (!Array.isArray(tags)) { return []; } tags = _.map(tags, function(tag) { if (typeof(tag) === 'number') { tag = tag.toString(); } return tag; }); tags = _.filter(tags, function(tag) { return (typeof(tag) === 'string'); }); if (self.filterTag) { tags = _.map(tags, self.filterTag); } return tags; }; // Sanitize an id. IDs must consist solely of upper and lower case // letters and numbers, digits, and underscores. self.sanitizeId = function(s, def) { var id = self.sanitizeString(s, def); if (id === def) { return id; } if (!id.match(/^[A-Za-z0-9\_]+$/)) { 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.sanitizeIds = function(ids) { if (!Array.isArray(ids)) { return []; } var result = []; _.each(ids, function(id) { id = self.sanitizeId(id); if (id === undefined) { return; } result.push(id); }); return result; }; // Sanitize an array of strings, simply to ensure they are strings. // Empty strings are allowed. self.sanitizeStrings = function(strings) { if (!Array.isArray(strings)) { return []; } return _.map(strings, function(s) { return self.sanitizeString(s); }); }; };