Skip to content
38 changes: 38 additions & 0 deletions src/__tests__/ParseObject-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ const ParseObject = require('../ParseObject').default;
const ParseOp = require('../ParseOp');
const RESTController = require('../RESTController');
const SingleInstanceStateController = require('../SingleInstanceStateController');
const encode = require('../encode').default;
const unsavedChildren = require('../unsavedChildren').default;

const mockXHR = require('./test_helpers/mockXHR');
Expand Down Expand Up @@ -3855,4 +3856,41 @@ describe('ParseObject pin', () => {
});
CoreManager.set('ALLOW_CUSTOM_OBJECT_ID', false);
});

it('handles unsaved circular references', async () => {
const xhrs = [];
RESTController._setXHR(function () {
const xhr = {
setRequestHeader: jest.fn(),
open: jest.fn(),
send: jest.fn(),
status: 200,
readyState: 4,
};
xhrs.push(xhr);
return xhr;
});

const a = {};
const b = {};
a.b = b;
b.a = a;

const object = new ParseObject('Test');
object.set('a', a);
expect(() => {
object.save();
}).toThrowError(
'Maximum recursive calls exceeded in traverse function. Potential infinite recursion detected.'
);
});

it('throws error for infinite recursion', () => {
const circularObject = {};
circularObject.circularReference = circularObject;

expect(() => {
encode(circularObject, false, false, [], false);
}).toThrowError('Maximum recursive calls exceeded in encode function. Potential infinite recursion detected.');
});
});
25 changes: 21 additions & 4 deletions src/encode.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,30 @@ import ParseObject from './ParseObject';
import { Op } from './ParseOp';
import ParseRelation from './ParseRelation';

const MAX_RECURSIVE_CALLS = 999;

function encode(
value: mixed,
disallowObjects: boolean,
forcePointers: boolean,
seen: Array<mixed>,
offline: boolean
offline: boolean,
counter: number = 0
): any {
counter++;

if (counter > MAX_RECURSIVE_CALLS) {
const message = 'Maximum recursive calls exceeded in encode function. Potential infinite recursion detected.';
console.error(message);
console.error('Value causing potential infinite recursion:', value);
console.error('Disallow objects:', disallowObjects);
console.error('Force pointers:', forcePointers);
console.error('Seen:', seen);
console.error('Offline:', offline);

throw new Error(message);
}

if (value instanceof ParseObject) {
if (disallowObjects) {
throw new Error('Parse Objects not allowed here');
Expand Down Expand Up @@ -67,14 +84,14 @@ function encode(

if (Array.isArray(value)) {
return value.map(v => {
return encode(v, disallowObjects, forcePointers, seen, offline);
return encode(v, disallowObjects, forcePointers, seen, offline, counter);
});
}

if (value && typeof value === 'object') {
const output = {};
for (const k in value) {
output[k] = encode(value[k], disallowObjects, forcePointers, seen, offline);
output[k] = encode(value[k], disallowObjects, forcePointers, seen, offline, counter);
}
return output;
}
Expand All @@ -89,5 +106,5 @@ export default function (
seen?: Array<mixed>,
offline?: boolean
): any {
return encode(value, !!disallowObjects, !!forcePointers, seen || [], offline);
return encode(value, !!disallowObjects, !!forcePointers, seen || [], offline, 0);
}
22 changes: 18 additions & 4 deletions src/unsavedChildren.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import ParseFile from './ParseFile';
import ParseObject from './ParseObject';
import ParseRelation from './ParseRelation';

const MAX_RECURSIVE_CALLS = 999;

type EncounterMap = {
objects: { [identifier: string]: ParseObject | boolean },
files: Array<ParseFile>,
Expand Down Expand Up @@ -48,8 +50,20 @@ function traverse(
obj: ParseObject,
encountered: EncounterMap,
shouldThrow: boolean,
allowDeepUnsaved: boolean
allowDeepUnsaved: boolean,
counter: number = 0
) {
counter++;

if (counter > MAX_RECURSIVE_CALLS) {
const message = 'Maximum recursive calls exceeded in traverse function. Potential infinite recursion detected.';
console.error(message);
console.error('Object causing potential infinite recursion:', obj);
console.error('Encountered objects:', encountered);

throw new Error(message);
}

if (obj instanceof ParseObject) {
if (!obj.id && shouldThrow) {
throw new Error('Cannot create a pointer to an unsaved Object.');
Expand All @@ -60,7 +74,7 @@ function traverse(
const attributes = obj.attributes;
for (const attr in attributes) {
if (typeof attributes[attr] === 'object') {
traverse(attributes[attr], encountered, !allowDeepUnsaved, allowDeepUnsaved);
traverse(attributes[attr], encountered, !allowDeepUnsaved, allowDeepUnsaved, counter);
}
}
}
Expand All @@ -78,13 +92,13 @@ function traverse(
if (Array.isArray(obj)) {
obj.forEach(el => {
if (typeof el === 'object') {
traverse(el, encountered, shouldThrow, allowDeepUnsaved);
traverse(el, encountered, shouldThrow, allowDeepUnsaved, counter);
}
});
}
for (const k in obj) {
if (typeof obj[k] === 'object') {
traverse(obj[k], encountered, shouldThrow, allowDeepUnsaved);
traverse(obj[k], encountered, shouldThrow, allowDeepUnsaved, counter);
}
}
}