sanitize.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. /**
  2. * Module dependencies
  3. */
  4. var _ = require('@sailshq/lodash');
  5. var Readable = require('stream').Readable;
  6. /**
  7. * Rebuild a value to make it JSON-compatible (plus some extra affordances)
  8. * This is used when validating/coercing an array or dictionary (and its contents)
  9. * against `example: {}` or `example: []`.
  10. *
  11. * It is also used elsewhere throughout rttc.
  12. *
  13. * @param {Anything} val
  14. * @param {Boolean?} allowNull
  15. * @param {Boolean?} dontStringifyFunctions
  16. * @param {Boolean?} allowNaNAndFriends
  17. * @param {Boolean?} doRunToJSONMethods
  18. * ^^^^^^^^^^^^^^^^^^
  19. * (only applies to certain things -- see https://trello.com/c/5SkpUlhI/402-make-customtojson-work-with-actions2#comment-5a3b6e7b43107b7a2938e7bd)
  20. */
  21. module.exports = function rebuildSanitized(val, allowNull, dontStringifyFunctions, allowNaNAndFriends, doRunToJSONMethods) {
  22. // Does not `allowNull` by default.
  23. // Never allows `undefined` at the top level (or inside- but that check is below in stringifySafe)
  24. if (_.isUndefined(val)) {
  25. if (allowNull) { return null; }
  26. else { return undefined; }
  27. }
  28. return _rebuild(val, allowNull, dontStringifyFunctions, allowNaNAndFriends, doRunToJSONMethods);
  29. };
  30. // (note that the recursive validation will not penetrate deeper into this
  31. // object, so we don't have to worry about this function running more than once
  32. // and doing unnecessary extra deep copies at each successive level)
  33. // If dictionary:
  34. // ==============================================================================
  35. // Sanitize a dictionary provided for a generic dictionary example (`example: {}`)
  36. // The main recursive validation function will not descend into this dictionary because
  37. // it's already met the minimum requirement of being an object. So we need to deep clone
  38. // the provided value for safety; and in the process ensure that it meets the basic minimum
  39. // quality requirements (i.e. no dictionaries within have any keys w/ invalid values)
  40. // If array:
  41. // ==============================================================================
  42. // Sanitize an array provided for a generic array example (`example: []`)
  43. // The main recursive validation function will not descend into this array because
  44. // it's already met the minimum requirement of being `_.isArray()`. So we need to
  45. // deep clone the provided value for safety; and in the process ensure that it meets
  46. // the basic minimum quality requirements (i.e. no dictionaries within have any keys w/
  47. // invalid values)
  48. //
  49. // We also don't include invalid items in the rebuilt array.
  50. //
  51. // (NOTE: `example: ['===']` won't make it here because it will be picked up
  52. // by the recursive validation. And so it's different-- it will contain
  53. // the original items, and therefore may contain dictionaries w/ keys w/ invalid values)
  54. function _rebuild(val, allowNull, dontStringifyFunctions, allowNaNAndFriends, doRunToJSONMethods) {
  55. var stack = [];
  56. var keys = [];
  57. // This was modified from @isaacs' json-stringify-safe
  58. // (see https://github.com/isaacs/json-stringify-safe/commit/02cfafd45f06d076ac4bf0dd28be6738a07a72f9#diff-c3fcfbed30e93682746088e2ce1a4a24)
  59. var cycleReplacer = function(unused, value) {
  60. if (stack[0] === value) { return '[Circular ~]'; }
  61. return '[Circular ~.' + keys.slice(0, stack.indexOf(value)).join('.') + ']';
  62. };
  63. function _recursivelyRebuildAndSanitize (val, key) {
  64. // Handle circle jerks
  65. if (stack.length > 0) {
  66. var self = this;
  67. var thisPos = stack.indexOf(self);
  68. ~thisPos ? stack.splice(thisPos + 1) : stack.push(self);
  69. ~thisPos ? keys.splice(thisPos, Infinity, key) : keys.push(key);
  70. if (~stack.indexOf(val)) {
  71. val = cycleReplacer.call(self, key, val);
  72. }
  73. }
  74. else { stack.push(val); }
  75. // Rebuild and strip undefineds/nulls
  76. if (_.isArray(val)) {
  77. return _.reduce(val,function (memo, item, i) {
  78. if (!_.isUndefined(item) && (allowNull || !_.isNull(item))) {
  79. memo.push(_recursivelyRebuildAndSanitize.call(val, item, i));
  80. }
  81. return memo;
  82. }, []);
  83. }
  84. // Serialize errors, regexps, dates, and functions to strings:
  85. else if (_.isError(val)){
  86. if (doRunToJSONMethods && _.isFunction(val.toJSON)) {
  87. val = val.toJSON();
  88. } else {
  89. val = val.stack;
  90. }
  91. }
  92. else if (_.isRegExp(val)){
  93. val = val.toString();
  94. }
  95. else if (_.isDate(val)){
  96. val = val.toJSON();
  97. }
  98. else if (_.isFunction(val)){
  99. if (!dontStringifyFunctions) {
  100. val = val.toString();
  101. }
  102. }
  103. else if (!_.isObject(val)) {
  104. // Coerce NaN, Infinity, and -Infinity to 0:
  105. // (unless "allowNaNAndFriends" is enabled)
  106. if (!allowNaNAndFriends) {
  107. if (_.isNaN(val)) {
  108. val = 0;
  109. }
  110. else if (val === Infinity) {
  111. val = 0;
  112. }
  113. else if (val === -Infinity) {
  114. val = 0;
  115. }
  116. }
  117. // Always coerce -0 to +0 regardless.
  118. if (val === 0) {
  119. val = 0;
  120. }
  121. }
  122. else if (_.isObject(val)) {
  123. // Reject readable streams out of hand
  124. if (val instanceof Readable) {
  125. return null;
  126. }
  127. // Reject buffers out of hand
  128. if (val instanceof Buffer) {
  129. return null;
  130. }
  131. // Reject `RttcRefPlaceholders` out of hand
  132. // (this is a special case so there is a placeholder value that ONLY validates stricly against the "ref" type)
  133. // (note that like anything else, RttcRefPlaceholders nested inside of a JSON/generic dict/generic array get sanitized into JSON-compatible things)
  134. if (_.isObject(val.constructor) && val.constructor.name === 'RttcRefPlaceholder') {
  135. return null;
  136. }
  137. // Run its .toJSON() method and use the result, if appropriate.
  138. if (doRunToJSONMethods && _.isFunction(val.toJSON)) {
  139. return val.toJSON();
  140. }//•
  141. return _.reduce(_.keys(val),function (memo, key) {
  142. var subVal = val[key];
  143. if (!_.isUndefined(subVal) && (allowNull || !_.isNull(subVal))) {
  144. memo[key] = _recursivelyRebuildAndSanitize.call(val, subVal, key);
  145. }
  146. return memo;
  147. }, {});
  148. }
  149. return val;
  150. }
  151. // Pass in the empty string for the top-level "key"
  152. // to satisfy Mr. isaac's replacer
  153. return _recursivelyRebuildAndSanitize(val, '');
  154. }