Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,18 @@ module.exports = [
curly: ["error", "all"],
"block-spacing": ["error", "always"],
"no-unused-vars": "off",
"no-console": "warn"
"no-console": "warn",
"no-restricted-syntax": [
"error",
{
selector: "BinaryExpression[operator='instanceof'][right.name='Date']",
message: "Use Utils.isDate() instead of instanceof Date (cross-realm safe).",
},
{
selector: "BinaryExpression[operator='instanceof'][right.name='RegExp']",
message: "Use Utils.isRegExp() instead of instanceof RegExp (cross-realm safe).",
},
]
},
},
];
154 changes: 154 additions & 0 deletions spec/Utils.spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const Utils = require('../lib/Utils');
const { createSanitizedError, createSanitizedHttpError } = require("../lib/Error")
const vm = require('vm');

describe('Utils', () => {
describe('encodeForUrl', () => {
Expand Down Expand Up @@ -287,4 +288,157 @@ describe('Utils', () => {
expect(error.message).toBe('Detailed error message');
});
});

describe('isDate', () => {
it('should return true for a Date', () => {
expect(Utils.isDate(new Date())).toBe(true);
});
it('should return true for a cross-realm Date', () => {
const crossRealmDate = vm.runInNewContext('new Date()');
// eslint-disable-next-line no-restricted-syntax -- intentional: proving instanceof fails cross-realm
expect(crossRealmDate instanceof Date).toBe(false);
expect(Utils.isDate(crossRealmDate)).toBe(true);
});
it('should return false for non-Date values', () => {
expect(Utils.isDate(null)).toBe(false);
expect(Utils.isDate(undefined)).toBe(false);
expect(Utils.isDate('2021-01-01')).toBe(false);
expect(Utils.isDate(123)).toBe(false);
expect(Utils.isDate({})).toBe(false);
});
});

describe('isRegExp', () => {
it('should return true for a RegExp', () => {
expect(Utils.isRegExp(/test/)).toBe(true);
expect(Utils.isRegExp(new RegExp('test'))).toBe(true);
});
it('should return true for a cross-realm RegExp', () => {
const crossRealmRegExp = vm.runInNewContext('/test/');
expect(crossRealmRegExp instanceof RegExp).toBe(false);
expect(Utils.isRegExp(crossRealmRegExp)).toBe(true);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
it('should return false for non-RegExp values', () => {
expect(Utils.isRegExp(null)).toBe(false);
expect(Utils.isRegExp(undefined)).toBe(false);
expect(Utils.isRegExp('/test/')).toBe(false);
expect(Utils.isRegExp({})).toBe(false);
});
});

describe('isMap', () => {
it('should return true for a Map', () => {
expect(Utils.isMap(new Map())).toBe(true);
});
it('should return true for a cross-realm Map', () => {
const crossRealmMap = vm.runInNewContext('new Map()');
expect(crossRealmMap instanceof Map).toBe(false);
expect(Utils.isMap(crossRealmMap)).toBe(true);
});
it('should return false for non-Map values', () => {
expect(Utils.isMap(null)).toBe(false);
expect(Utils.isMap(undefined)).toBe(false);
expect(Utils.isMap({})).toBe(false);
expect(Utils.isMap(new Set())).toBe(false);
});
});

describe('isSet', () => {
it('should return true for a Set', () => {
expect(Utils.isSet(new Set())).toBe(true);
});
it('should return true for a cross-realm Set', () => {
const crossRealmSet = vm.runInNewContext('new Set()');
expect(crossRealmSet instanceof Set).toBe(false);
expect(Utils.isSet(crossRealmSet)).toBe(true);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
it('should return false for non-Set values', () => {
expect(Utils.isSet(null)).toBe(false);
expect(Utils.isSet(undefined)).toBe(false);
expect(Utils.isSet({})).toBe(false);
expect(Utils.isSet(new Map())).toBe(false);
});
});

describe('isNativeError', () => {
it('should return true for an Error', () => {
expect(Utils.isNativeError(new Error('test'))).toBe(true);
});
it('should return true for Error subclasses', () => {
expect(Utils.isNativeError(new TypeError('test'))).toBe(true);
expect(Utils.isNativeError(new RangeError('test'))).toBe(true);
});
it('should return true for a cross-realm Error', () => {
const crossRealmError = vm.runInNewContext('new Error("test")');
expect(crossRealmError instanceof Error).toBe(false);
expect(Utils.isNativeError(crossRealmError)).toBe(true);
});
it('should return false for non-Error values', () => {
expect(Utils.isNativeError(null)).toBe(false);
expect(Utils.isNativeError(undefined)).toBe(false);
expect(Utils.isNativeError({ message: 'fake' })).toBe(false);
expect(Utils.isNativeError('error')).toBe(false);
});
});

describe('isPromise', () => {
it('should return true for a Promise', () => {
expect(Utils.isPromise(Promise.resolve())).toBe(true);
});
it('should return true for a cross-realm Promise', () => {
const crossRealmPromise = vm.runInNewContext('Promise.resolve()');
expect(crossRealmPromise instanceof Promise).toBe(false);
expect(Utils.isPromise(crossRealmPromise)).toBe(true);
});
it('should return true for a thenable', () => {
expect(Utils.isPromise({ then: () => {} })).toBe(true);
});
it('should return false for non-Promise values', () => {
expect(Utils.isPromise(null)).toBe(false);
expect(Utils.isPromise(undefined)).toBe(false);
expect(Utils.isPromise({})).toBe(false);
expect(Utils.isPromise(42)).toBe(false);
});
it('should return false for plain objects when Object.prototype.then is polluted', () => {
Object.prototype.then = () => {};
try {
expect(Utils.isPromise({})).toBe(false);
expect(Utils.isPromise({ a: 1 })).toBe(false);
} finally {
delete Object.prototype.then;
}
});
it('should return true for real thenables even when Object.prototype.then is polluted', () => {
Object.prototype.then = () => {};
try {
expect(Utils.isPromise({ then: () => {} })).toBe(true);
Comment thread
mtrezza marked this conversation as resolved.
expect(Utils.isPromise(Promise.resolve())).toBe(true);
} finally {
delete Object.prototype.then;
}
});
});

describe('isObject', () => {
it('should return true for plain objects', () => {
expect(Utils.isObject({})).toBe(true);
expect(Utils.isObject({ a: 1 })).toBe(true);
});
it('should return true for a cross-realm object', () => {
const crossRealmObj = vm.runInNewContext('({ a: 1 })');
expect(crossRealmObj instanceof Object).toBe(false);
expect(Utils.isObject(crossRealmObj)).toBe(true);
});
it('should return true for arrays and other objects', () => {
expect(Utils.isObject([])).toBe(true);
expect(Utils.isObject(new Date())).toBe(true);
});
it('should return false for non-object values', () => {
expect(Utils.isObject(null)).toBe(false);
expect(Utils.isObject(undefined)).toBe(false);
expect(Utils.isObject(42)).toBe(false);
expect(Utils.isObject('string')).toBe(false);
expect(Utils.isObject(true)).toBe(false);
});
});
});
11 changes: 11 additions & 0 deletions spec/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,17 @@ module.exports = [
curly: ["error", "all"],
"block-spacing": ["error", "always"],
"no-unused-vars": "off",
"no-restricted-syntax": [
"error",
{
selector: "BinaryExpression[operator='instanceof'][right.name='Date']",
message: "Use Utils.isDate() instead of instanceof Date (cross-realm safe).",
},
{
selector: "BinaryExpression[operator='instanceof'][right.name='RegExp']",
message: "Use Utils.isRegExp() instead of instanceof RegExp (cross-realm safe).",
},
],
},
},
];
4 changes: 2 additions & 2 deletions src/Adapters/Storage/Mongo/MongoStorageAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { format as formatUrl, parse as parseUrl } from '../../../vendor/mongodbUrl';
import type { QueryOptions, QueryType, SchemaType, StorageClass } from '../StorageAdapter';
import { StorageAdapter } from '../StorageAdapter';
import Utils from '../../../Utils';
import MongoCollection from './MongoCollection';
import MongoSchemaCollection from './MongoSchemaCollection';
import {
Expand All @@ -18,7 +19,6 @@ import Parse from 'parse/node';
import _ from 'lodash';
import defaults, { ParseServerDatabaseOptions } from '../../../defaults';
import logger from '../../../logger';
import Utils from '../../../Utils';

// @flow-disable-next
const mongodb = require('mongodb');
Expand Down Expand Up @@ -1110,7 +1110,7 @@ export class MongoStorageAdapter implements StorageAdapter {
* @returns {any} The original value if not convertible to Date, or a Date object if it is.
*/
_convertToDate(value: any): any {
if (value instanceof Date) {
if (Utils.isDate(value)) {
return value;
}
if (typeof value === 'string') {
Expand Down
16 changes: 8 additions & 8 deletions src/Adapters/Storage/Mongo/MongoTransform.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ const transformKeyValueForUpdate = (className, restKey, restValue, parseFormatSc
};

const isRegex = value => {
return value && value instanceof RegExp;
return value && Utils.isRegExp(value);
};

const isStartsWithRegex = value => {
Expand Down Expand Up @@ -189,7 +189,7 @@ const transformInteriorValue = restValue => {
var value = transformInteriorAtom(restValue);
if (value !== CannotTransform) {
if (value && typeof value === 'object') {
if (value instanceof Date) {
if (Utils.isDate(value)) {
return value;
}
if (value instanceof Array) {
Expand Down Expand Up @@ -218,7 +218,7 @@ const transformInteriorValue = restValue => {
const valueAsDate = value => {
if (typeof value === 'string') {
return new Date(value);
} else if (value instanceof Date) {
} else if (Utils.isDate(value)) {
return value;
}
return false;
Expand Down Expand Up @@ -565,7 +565,7 @@ function CannotTransform() {}

const transformInteriorAtom = atom => {
// TODO: check validity harder for the __type-defined types
if (typeof atom === 'object' && atom && !(atom instanceof Date) && atom.__type === 'Pointer') {
if (typeof atom === 'object' && atom && !Utils.isDate(atom) && atom.__type === 'Pointer') {
return {
__type: 'Pointer',
className: atom.className,
Expand Down Expand Up @@ -606,7 +606,7 @@ function transformTopLevelAtom(atom, field) {
case 'function':
throw new Parse.Error(Parse.Error.INVALID_JSON, `cannot transform value: ${atom}`);
case 'object':
if (atom instanceof Date) {
if (Utils.isDate(atom)) {
// Technically dates are not rest format, but, it seems pretty
// clear what they should be transformed to, so let's just do it.
return atom;
Expand Down Expand Up @@ -1057,7 +1057,7 @@ const nestedMongoObjectToNestedParseObject = mongoObject => {
return mongoObject.map(nestedMongoObjectToNestedParseObject);
}

if (mongoObject instanceof Date) {
if (Utils.isDate(mongoObject)) {
return Parse._encode(mongoObject);
}

Expand All @@ -1076,7 +1076,7 @@ const nestedMongoObjectToNestedParseObject = mongoObject => {
if (
Object.prototype.hasOwnProperty.call(mongoObject, '__type') &&
mongoObject.__type == 'Date' &&
mongoObject.iso instanceof Date
Utils.isDate(mongoObject.iso)
) {
mongoObject.iso = mongoObject.iso.toJSON();
return mongoObject;
Expand Down Expand Up @@ -1120,7 +1120,7 @@ const mongoObjectToParseObject = (className, mongoObject, schema) => {
return mongoObject.map(nestedMongoObjectToNestedParseObject);
}

if (mongoObject instanceof Date) {
if (Utils.isDate(mongoObject)) {
return Parse._encode(mongoObject);
}

Expand Down
4 changes: 2 additions & 2 deletions src/Adapters/Storage/Postgres/PostgresStorageAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -1696,7 +1696,7 @@ export class PostgresStorageAdapter implements StorageAdapter {
updatePatterns.push(`$${index}:name = $${index + 1}`);
values.push(fieldName, toPostgresValue(fieldValue));
index += 2;
} else if (fieldValue instanceof Date) {
} else if (Utils.isDate(fieldValue)) {
updatePatterns.push(`$${index}:name = $${index + 1}`);
values.push(fieldName, fieldValue);
index += 2;
Expand Down Expand Up @@ -2057,7 +2057,7 @@ export class PostgresStorageAdapter implements StorageAdapter {
if (object[fieldName] === null) {
delete object[fieldName];
}
if (object[fieldName] instanceof Date) {
if (Utils.isDate(object[fieldName])) {
object[fieldName] = {
__type: 'Date',
iso: object[fieldName].toISOString(),
Expand Down
3 changes: 2 additions & 1 deletion src/Config.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
} from './Options/Definitions';
import ParseServer from './cloud-code/Parse.Server';
import Deprecator from './Deprecator/Deprecator';
import Utils from './Utils';

function removeTrailingSlash(str) {
if (!str) {
Expand Down Expand Up @@ -420,7 +421,7 @@ export class Config {
if (passwordPolicy.validatorPattern) {
if (typeof passwordPolicy.validatorPattern === 'string') {
passwordPolicy.validatorPattern = new RegExp(passwordPolicy.validatorPattern);
} else if (!(passwordPolicy.validatorPattern instanceof RegExp)) {
} else if (!Utils.isRegExp(passwordPolicy.validatorPattern)) {
throw 'passwordPolicy.validatorPattern must be a regex string or RegExp object.';
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/Controllers/ParseGraphQLController.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import requiredParameter from '../../lib/requiredParameter';
import Utils from '../Utils';
import DatabaseController from './DatabaseController';
import CacheController from './CacheController';

Expand Down Expand Up @@ -306,7 +307,7 @@ const isValidSimpleObject = function (obj): boolean {
typeof obj === 'object' &&
!Array.isArray(obj) &&
obj !== null &&
obj instanceof Date !== true &&
Utils.isDate(obj) !== true &&
obj instanceof Promise !== true
);
};
Expand Down
9 changes: 5 additions & 4 deletions src/GraphQL/loaders/defaultGraphQLTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
} from 'graphql';
import { toGlobalId } from 'graphql-relay';
import GraphQLUpload from 'graphql-upload/GraphQLUpload.js';
import Utils from '../../Utils';

class TypeValidationError extends Error {
constructor(value, type) {
Expand Down Expand Up @@ -149,7 +150,7 @@ const parseDateIsoValue = value => {
if (!isNaN(date)) {
return date;
}
} else if (value instanceof Date) {
} else if (Utils.isDate(value)) {
return value;
}

Expand All @@ -160,7 +161,7 @@ const serializeDateIso = value => {
if (typeof value === 'string') {
return value;
}
if (value instanceof Date) {
if (Utils.isDate(value)) {
return value.toISOString();
}

Expand All @@ -179,7 +180,7 @@ const DATE = new GraphQLScalarType({
name: 'Date',
description: 'The Date scalar type is used in operations and types that involve dates.',
parseValue(value) {
if (typeof value === 'string' || value instanceof Date) {
if (typeof value === 'string' || Utils.isDate(value)) {
return {
__type: 'Date',
iso: parseDateIsoValue(value),
Expand All @@ -194,7 +195,7 @@ const DATE = new GraphQLScalarType({
throw new TypeValidationError(value, 'Date');
},
serialize(value) {
if (typeof value === 'string' || value instanceof Date) {
if (typeof value === 'string' || Utils.isDate(value)) {
return serializeDateIso(value);
} else if (typeof value === 'object' && value.__type === 'Date' && value.iso) {
return serializeDateIso(value.iso);
Expand Down
Loading
Loading