From 7df30fefbb32222c8285a89683756e67fa6edbf7 Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 11 Jan 2023 18:12:29 +1100 Subject: [PATCH 01/12] feat: add file beforeFind triggers, file ACL, file cleanup, file references --- spec/CloudCode.spec.js | 58 ++++++++++++ spec/FilesController.spec.js | 8 +- spec/ParseFile.spec.js | 130 ++++++++++++-------------- spec/helper.js | 4 +- src/Config.js | 13 +++ src/Controllers/DatabaseController.js | 52 +++++++++++ src/Controllers/FilesController.js | 111 ++++++++++++++++------ src/Controllers/SchemaController.js | 46 +++++++++ src/Options/Definitions.js | 13 +++ src/Options/docs.js | 2 + src/Options/index.js | 6 ++ src/ParseServer.js | 15 +++ src/RestQuery.js | 44 +++++---- src/RestWrite.js | 19 ++-- src/Routers/FilesRouter.js | 120 +++++++++++++++++++++--- src/Routers/UsersRouter.js | 2 +- src/rest.js | 29 ++++-- 17 files changed, 519 insertions(+), 153 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index c02999ad51..d7b3371bba 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -3812,6 +3812,64 @@ describe('saveFile hooks', () => { ); } }); + + it('can run find hooks', async () => { + const user = new Parse.User(); + user.setUsername('triggeruser'); + user.setPassword('triggeruser'); + await user.signUp(); + const base64 = 'V29ya2luZyBhdCBQYXJzZSBpcyBncmVhdCE='; + const file = new Parse.File('myfile.txt', { base64 }); + await file.save({ sessionToken: user.getSessionToken() }); + const hooks = { + beforeFind(req) { + expect(req.user.id).toEqual(user.id); + expect(req.file instanceof Parse.File).toBeTrue(); + expect(req.file._name).toEqual(file._name); + expect(req.file._url).toEqual(file._url); + }, + afterFind(req) { + expect(req.user.id).toEqual(user.id); + expect(req.file instanceof Parse.File).toBeTrue(); + expect(req.file._name).toEqual(file._name); + expect(req.file._url).toEqual(file._url); + }, + }; + for (const key in hooks) { + spyOn(hooks, key).and.callThrough(); + Parse.Cloud[key](Parse.File, hooks[key]); + } + const response = await request({ url: file.url() }); + expect(response.text).toEqual('Working at Parse is great!'); + for (const key in hooks) { + expect(hooks[key]).toHaveBeenCalled(); + } + }); + + fit('can clean up files', async () => { + const server = await reconfigureServer(); + const base64 = 'V29ya2luZyBhdCBQYXJzZSBpcyBncmVhdCE='; + const file = new Parse.File('myfile.txt', { base64 }); + const obj = await new Parse.Object('TestObject').save({ file }); + await Promise.all([ + (async () => { + const objects = await new Parse.Query('_FileObject').find({ useMasterKey: true }); + expect(objects.length).toBe(1); + })(), + (async () => { + const objects = await new Parse.Query('_FileReference').find({ useMasterKey: true }); + expect(objects.length).toBe(1); + })(), + ]); + await obj.destroy(); + await new Promise(resolve => setTimeout(resolve, 1000)); + const references = await new Parse.Query('_FileReference').find({ useMasterKey: true }); + expect(references.length).toBe(0); + await server.cleanupFiles(); + await new Promise(resolve => setTimeout(resolve, 1000)); + const objects = await new Parse.Query('_FileObject').find({ useMasterKey: true }); + expect(objects.length).toBe(0); + }); }); describe('sendEmail', () => { diff --git a/spec/FilesController.spec.js b/spec/FilesController.spec.js index 8fee5aca2f..9c6c1f2c2c 100644 --- a/spec/FilesController.spec.js +++ b/spec/FilesController.spec.js @@ -21,11 +21,11 @@ const mockAdapter = { // Small additional tests to improve overall coverage describe('FilesController', () => { - it('should properly expand objects', done => { + it('should properly expand objects', async () => { const config = Config.get(Parse.applicationId); const gridFSAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse'); const filesController = new FilesController(gridFSAdapter); - const result = filesController.expandFilesInObject(config, function () {}); + const result = await filesController.expandFilesInObject(config, function () {}); expect(result).toBeUndefined(); @@ -37,10 +37,8 @@ describe('FilesController', () => { const anObject = { aFile: fullFile, }; - filesController.expandFilesInObject(config, anObject); + await filesController.expandFilesInObject(config, anObject); expect(anObject.aFile.url).toEqual('http://an.url'); - - done(); }); it_only_db('mongo')('should pass databaseOptions to GridFSBucketAdapter', async () => { diff --git a/spec/ParseFile.spec.js b/spec/ParseFile.spec.js index ed21304d39..6a9a49fb4a 100644 --- a/spec/ParseFile.spec.js +++ b/spec/ParseFile.spec.js @@ -265,7 +265,7 @@ describe('Parse.File testing', () => { ok(objectAgain.get('file') instanceof Parse.File); }); - it('autosave file in object', async done => { + fit('autosave file in object', async () => { let file = new Parse.File('hello.txt', data, 'text/plain'); ok(!file.url()); const object = new Parse.Object('TestObject'); @@ -276,7 +276,60 @@ describe('Parse.File testing', () => { ok(file.name()); ok(file.url()); notEqual(file.name(), 'hello.txt'); - done(); + await Promise.all([ + (async () => { + const fileObjects = await new Parse.Query('_FileObject').find({ + useMasterKey: true, + json: true, + }); + expect(fileObjects.length).toBe(1); + const fileObject = fileObjects[0]; + expect(fileObject.className).toBe('_FileObject'); + expect(fileObject.file).toEqual({ + __type: 'File', + name: file.name(), + url: file.url().split('?token')[0], + }); + expect(fileObject.ACL).toEqual({ + '*': { + read: true, + }, + }); + })(), + (async () => { + const fileReferences = await new Parse.Query('_FileReference').find({ + useMasterKey: true, + json: true, + }); + expect(fileReferences.length).toBe(1); + const fileReference = fileReferences[0]; + expect(fileReference.className).toBe('_FileReference'); + expect(fileReference.file).toEqual({ + __type: 'Pointer', + className: '_FileObject', + objectId: fileReference.file.objectId, + }); + expect(fileReference.referenceId).toBe(objectAgain.id); + expect(fileReference.referenceClass).toBe('TestObject'); + })(), + (async () => { + const fileSessions = await new Parse.Query('_FileSession').find({ + useMasterKey: true, + json: true, + }); + expect(fileSessions.length).toBe(3); + const fileSession = fileSessions[2]; + expect(fileSession.file).toEqual({ + __type: 'Pointer', + className: '_FileObject', + objectId: fileSession.file.objectId, + }); + expect(fileSession.token).toBe(file.url().split('?token=')[1]); + expect(fileSession.master).toBe(false); + expect(new Date(fileSession.expiry.iso) instanceof Date).toBeTrue(); + })(), + ]); + expect(file.url()).toContain('?token='); }); it('autosave file in object in object', async done => { @@ -390,7 +443,7 @@ describe('Parse.File testing', () => { }); }); - it('supports array of files', done => { + it('supports array of files', async () => { const file = { __type: 'File', url: 'http://meep.meep', @@ -399,19 +452,12 @@ describe('Parse.File testing', () => { const files = [file, file]; const obj = new Parse.Object('FilesArrayTest'); obj.set('files', files); - obj - .save() - .then(() => { - const query = new Parse.Query('FilesArrayTest'); - return query.first(); - }) - .then(result => { - const filesAgain = result.get('files'); - expect(filesAgain.length).toEqual(2); - expect(filesAgain[0].name()).toEqual('meep'); - expect(filesAgain[0].url()).toEqual('http://meep.meep'); - done(); - }); + await obj.save(); + const result = await new Parse.Query('FilesArrayTest').first(); + const filesAgain = result.get('files'); + expect(filesAgain.length).toEqual(2); + expect(filesAgain[0].name()).toEqual('meep'); + expect(filesAgain[0].url()).toEqual('http://meep.meep'); }); it('validates filename characters', done => { @@ -486,58 +532,6 @@ describe('Parse.File testing', () => { }); }); - it('creates correct url for old files hosted on files.parsetfss.com', done => { - const file = { - __type: 'File', - url: 'http://irrelevant.elephant/', - name: 'tfss-123.txt', - }; - const obj = new Parse.Object('OldFileTest'); - obj.set('oldfile', file); - obj - .save() - .then(() => { - const query = new Parse.Query('OldFileTest'); - return query.first(); - }) - .then(result => { - const fileAgain = result.get('oldfile'); - expect(fileAgain.url()).toEqual('http://files.parsetfss.com/test/tfss-123.txt'); - done(); - }) - .catch(e => { - jfail(e); - done(); - }); - }); - - it('creates correct url for old files hosted on files.parse.com', done => { - const file = { - __type: 'File', - url: 'http://irrelevant.elephant/', - name: 'd6e80979-a128-4c57-a167-302f874700dc-123.txt', - }; - const obj = new Parse.Object('OldFileTest'); - obj.set('oldfile', file); - obj - .save() - .then(() => { - const query = new Parse.Query('OldFileTest'); - return query.first(); - }) - .then(result => { - const fileAgain = result.get('oldfile'); - expect(fileAgain.url()).toEqual( - 'http://files.parse.com/test/d6e80979-a128-4c57-a167-302f874700dc-123.txt' - ); - done(); - }) - .catch(e => { - jfail(e); - done(); - }); - }); - it('supports files in objects without urls', done => { const file = { __type: 'File', diff --git a/spec/helper.js b/spec/helper.js index 445a137ac5..0965873be7 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -82,7 +82,7 @@ on_db( ); let logLevel; -let silent = true; +let silent = false; if (process.env.VERBOSE) { silent = false; logLevel = 'verbose'; @@ -112,6 +112,8 @@ const defaultConfiguration = { enableForPublic: true, enableForAnonymousUser: true, enableForAuthenticatedUser: true, + enableLegacyAccess: false, + tokenValidityDuration: 5 * 60, }, push: { android: { diff --git a/src/Config.js b/src/Config.js index bd7c6f21af..1e4cfc2cfc 100644 --- a/src/Config.js +++ b/src/Config.js @@ -445,6 +445,19 @@ export class Config { } else if (typeof fileUpload.enableForAuthenticatedUser !== 'boolean') { throw 'fileUpload.enableForAuthenticatedUser must be a boolean value.'; } + if (fileUpload.enableLegacyAccess === undefined) { + fileUpload.enableLegacyAccess = FileUploadOptions.enableLegacyAccess.default; + } else if (typeof fileUpload.enableLegacyAccess !== 'boolean') { + throw 'fileUpload.enableLegacyAccess must be a boolean value.'; + } + if (fileUpload.tokenValidityDuration === undefined) { + fileUpload.tokenValidityDuration = FileUploadOptions.tokenValidityDuration.default; + } else if ( + typeof fileUpload.tokenValidityDuration !== 'number' || + fileUpload.tokenValidityDuration <= 0 + ) { + throw 'fileUpload.tokenValidityDuration must be a positive number.'; + } } static validateIps(field, masterKeyIps) { diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 3aa4da160e..afe98c7e7c 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -1699,9 +1699,30 @@ class DatabaseController { ...SchemaController.defaultColumns._Idempotency, }, }; + const requiredFileObjectFields = { + fields: { + ...SchemaController.defaultColumns._Default, + ...SchemaController.defaultColumns._FileObject, + }, + }; + const requiredFileSessionFields = { + fields: { + ...SchemaController.defaultColumns._Default, + ...SchemaController.defaultColumns._FileSession, + }, + }; + const requiredFileReferenceFields = { + fields: { + ...SchemaController.defaultColumns._Default, + ...SchemaController.defaultColumns._FileReference, + }, + }; await this.loadSchema().then(schema => schema.enforceClassExists('_User')); await this.loadSchema().then(schema => schema.enforceClassExists('_Role')); await this.loadSchema().then(schema => schema.enforceClassExists('_Idempotency')); + await this.loadSchema().then(schema => schema.enforceClassExists('_FileObject')); + await this.loadSchema().then(schema => schema.enforceClassExists('_FileSession')); + await this.loadSchema().then(schema => schema.enforceClassExists('_FileReference')); await this.adapter.ensureUniqueness('_User', requiredUserFields, ['username']).catch(error => { logger.warn('Unable to ensure uniqueness for usernames: ', error); @@ -1745,6 +1766,37 @@ class DatabaseController { throw error; }); + await this.adapter + .ensureUniqueness('_FileObject', requiredFileObjectFields, ['file']) + .catch(error => { + logger.warn('Unable to ensure uniqueness for file object: ', error); + throw error; + }); + + await this.adapter + .ensureUniqueness('_FileObject', requiredFileObjectFields, ['file']) + .catch(error => { + logger.warn('Unable to ensure uniqueness for file object: ', error); + throw error; + }); + + await this.adapter + .ensureUniqueness('_FileSession', requiredFileSessionFields, ['token']) + .catch(error => { + logger.warn('Unable to ensure uniqueness for file object: ', error); + throw error; + }); + + await this.adapter + .ensureUniqueness('_FileReference', requiredFileReferenceFields, [ + 'referenceId', + 'referenceClass', + ]) + .catch(error => { + logger.warn('Unable to ensure uniqueness for file object: ', error); + throw error; + }); + const isMongoAdapter = this.adapter instanceof MongoStorageAdapter; const isPostgresAdapter = this.adapter instanceof PostgresStorageAdapter; if (isMongoAdapter || isPostgresAdapter) { diff --git a/src/Controllers/FilesController.js b/src/Controllers/FilesController.js index aaff8511fe..5ad4dad5ba 100644 --- a/src/Controllers/FilesController.js +++ b/src/Controllers/FilesController.js @@ -4,11 +4,9 @@ import AdaptableController from './AdaptableController'; import { validateFilename, FilesAdapter } from '../Adapters/Files/FilesAdapter'; import path from 'path'; import mime from 'mime'; -const Parse = require('parse').Parse; - -const legacyFilesRegex = new RegExp( - '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}-.*' -); +import { Parse } from 'parse/node'; +import RestQuery from '../RestQuery'; +import { randomString } from '../cryptoUtils'; export class FilesController extends AdaptableController { getFileData(config, filename) { @@ -55,39 +53,90 @@ export class FilesController extends AdaptableController { * with the current mount point and app id. * Object may be a single object or list of REST-format objects. */ - expandFilesInObject(config, object) { + async expandFilesInObject(config, object, className, auth, op) { if (object instanceof Array) { - object.map(obj => this.expandFilesInObject(config, obj)); - return; + return Promise.all( + object.map(obj => this.expandFilesInObject(config, obj, className, auth, op)) + ); } if (typeof object !== 'object') { return; } - for (const key in object) { - const fileObject = object[key]; - if (fileObject && fileObject['__type'] === 'File') { - if (fileObject['url']) { - continue; - } - const filename = fileObject['name']; - // all filenames starting with "tfss-" should be from files.parsetfss.com - // all filenames starting with a "-" seperated UUID should be from files.parse.com - // all other filenames have been migrated or created from Parse Server - if (config.fileKey === undefined) { - fileObject['url'] = this.adapter.getFileLocation(config, filename); - } else { - if (filename.indexOf('tfss-') === 0) { - fileObject['url'] = - 'http://files.parsetfss.com/' + config.fileKey + '/' + encodeURIComponent(filename); - } else if (legacyFilesRegex.test(filename)) { - fileObject['url'] = - 'http://files.parse.com/' + config.fileKey + '/' + encodeURIComponent(filename); - } else { - fileObject['url'] = this.adapter.getFileLocation(config, filename); + await Promise.all( + Object.keys(object).map(async key => { + const fileObject = object[key]; + if (fileObject && fileObject['__type'] === 'File') { + const filename = fileObject['name']; + if (!fileObject.url) { + fileObject.url = this.adapter.getFileLocation(config, filename); + } + if (className.charAt(0) !== '_' && op !== 'delete') { + const file = new Parse.File(filename); + file._url = fileObject.url; + const files = await new RestQuery( + config, + auth, + '_FileObject', + { file: file.toJSON() }, + { limit: 1 } + ).execute(); + if (files.results.length === 0 && !config.fileUpload.enableLegacyAccess) { + delete object[key]; + return; + } + const [token] = await Promise.all([ + this.createFileSession(config, auth, files.results[0].objectId), + (async () => { + try { + const refFile = Parse.Object.extend('_FileObject').createWithoutData( + files.results[0].objectId + ); + const reference = await new Parse.Query('_FileReference') + .equalTo({ + file: refFile, + referenceId: object.objectId, + referenceClass: className, + }) + .first({ useMasterKey: true }); + if (!reference) { + const fileReference = new Parse.Object('_FileReference'); + fileReference.set({ + file: Parse.Object.extend('_FileObject').createWithoutData( + files.results[0].objectId + ), + referenceId: object.objectId, + referenceClass: className, + }); + await fileReference.save(null, { useMasterKey: true }); + } + } catch (e) { + /* */ + } + })(), + ]); + fileObject['url'] = `${fileObject['url']}?token=${token}`; } } - } - } + }) + ); + } + + async createFileSession(config, auth, objectId) { + const fileObj = Parse.Object.extend('_FileObject').createWithoutData(objectId); + const token = randomString(32); + const expiry = new Date(); + expiry.setTime(expiry.getTime() + config.fileUpload.tokenValidityDuration * 1000); + const fileSession = new Parse.Object('_FileSession'); + fileSession.set({ + file: fileObj, + token, + expiry, + master: auth?.isMaster, + sessionToken: auth?.user?.getSessionToken(), + installationId: auth?.installationId, + }); + await fileSession.save(null, { useMasterKey: true }); + return token; } expectedAdapterType() { diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index 62757d251d..e6b88f68e9 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -148,6 +148,22 @@ const defaultColumns: { [string]: SchemaFields } = Object.freeze({ reqId: { type: 'String' }, expire: { type: 'Date' }, }, + _FileObject: { + file: { type: 'File' }, + }, + _FileSession: { + file: { type: 'Pointer', targetClass: '_FileObject' }, + master: { type: 'Boolean' }, + sessionToken: { type: 'String' }, + installationId: { type: 'String' }, + token: { type: 'String' }, + expiry: { type: 'Date' }, + }, + _FileReference: { + file: { type: 'Pointer', targetClass: '_FileObject' }, + referenceId: { type: 'String' }, + referenceClass: { type: 'String' }, + }, }); // fields required for read or write operations on their respective classes. @@ -174,6 +190,9 @@ const systemClasses = Object.freeze([ '_JobSchedule', '_Audience', '_Idempotency', + '_FileObject', + '_FileSession', + '_FileReference', ]); const volatileClasses = Object.freeze([ @@ -185,6 +204,9 @@ const volatileClasses = Object.freeze([ '_JobSchedule', '_Audience', '_Idempotency', + '_FileObject', + '_FileSession', + '_FileReference', ]); // Anything that start with role @@ -654,6 +676,27 @@ const _IdempotencySchema = convertSchemaToAdapterSchema( classLevelPermissions: {}, }) ); +const _FileSchema = convertSchemaToAdapterSchema( + injectDefaultSchema({ + className: '_FileObject', + fields: defaultColumns._FileObject, + classLevelPermissions: {}, + }) +); +const _FileSessionSchema = convertSchemaToAdapterSchema( + injectDefaultSchema({ + className: '_FileSession', + fields: defaultColumns._FileSession, + classLevelPermissions: {}, + }) +); +const _FileReferencechema = convertSchemaToAdapterSchema( + injectDefaultSchema({ + className: '_FileReference', + fields: defaultColumns._FileReference, + classLevelPermissions: {}, + }) +); const VolatileClassesSchemas = [ _HooksSchema, _JobStatusSchema, @@ -663,6 +706,9 @@ const VolatileClassesSchemas = [ _GraphQLConfigSchema, _AudienceSchema, _IdempotencySchema, + _FileSchema, + _FileSessionSchema, + _FileReferencechema, ]; const dbTypeMatchesObjectType = (dbType: SchemaField | string, objectType: SchemaField) => { diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index f7a4f822d7..36027864c3 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -955,6 +955,19 @@ module.exports.FileUploadOptions = { action: parsers.booleanParser, default: false, }, + enableLegacyAccess: { + env: 'PARSE_SERVER_FILE_UPLOAD_ENABLE_LEGACY_ACCESS', + help: + 'Is true if files that do not have a corresponding _FileObject should be publicly accessable.', + action: parsers.booleanParser, + default: false, + }, + tokenValidityDuration: { + env: 'PARSE_SERVER_FILE_UPLOAD_TOKEN_VALIDITY_DURATION', + help: 'Duration of the file token in seconds', + action: parsers.numberParser('tokenValidityDuration'), + default: 300, + }, }; module.exports.DatabaseOptions = { enableSchemaHooks: { diff --git a/src/Options/docs.js b/src/Options/docs.js index b0378d327e..3347f36656 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -219,6 +219,8 @@ * @property {Boolean} enableForAnonymousUser Is true if file upload should be allowed for anonymous users. * @property {Boolean} enableForAuthenticatedUser Is true if file upload should be allowed for authenticated users. * @property {Boolean} enableForPublic Is true if file upload should be allowed for anyone, regardless of user authentication. + * @property {Boolean} enableLegacyAccess Is true if files that do not have a corresponding _FileObject should be publicly accessable. + * @property {Number} tokenValidityDuration Duration of the file token in seconds */ /** diff --git a/src/Options/index.js b/src/Options/index.js index 661d062de6..d8e101b258 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -537,6 +537,12 @@ export interface FileUploadOptions { /* Is true if file upload should be allowed for anyone, regardless of user authentication. :DEFAULT: false */ enableForPublic: ?boolean; + /* Is true if files that do not have a corresponding _FileObject should be publicly accessable. + :DEFAULT: false */ + enableLegacyAccess: ?boolean; + /* Duration of the file token in seconds + :DEFAULT: 300 */ + tokenValidityDuration: ?number; } export interface DatabaseOptions { diff --git a/src/ParseServer.js b/src/ParseServer.js index 355d9178ca..94b6d5f300 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -153,6 +153,21 @@ class ParseServer { return this._app; } + cleanupFiles() { + new Parse.Query('_FileObject').each( + async obj => { + const file = obj.get('file'); + const reference = await new Parse.Query('_FileReference').equalTo('file', obj).first(); + if (!reference) { + console.log(`Deleting orphaned file ${file.url()}`); + await file.destroy({ useMasterKey: true }); + } + }, + { useMasterKey: true } + ); + console.log('Beginning to cleanup files...'); + } + handleShutdown() { const promises = []; const { adapter: databaseAdapter } = this.config.databaseController; diff --git a/src/RestQuery.js b/src/RestQuery.js index f936a5a7a8..3ccf3ea23f 100644 --- a/src/RestQuery.js +++ b/src/RestQuery.js @@ -643,10 +643,10 @@ RestQuery.prototype.replaceEquality = function () { // Returns a promise for whether it was successful. // Populates this.response with an object that only has 'results'. -RestQuery.prototype.runFind = function (options = {}) { +RestQuery.prototype.runFind = async function (options = {}) { if (this.findOptions.limit === 0) { this.response = { results: [] }; - return Promise.resolve(); + return; } const findOptions = Object.assign({}, this.findOptions); if (this.keys) { @@ -657,24 +657,32 @@ RestQuery.prototype.runFind = function (options = {}) { if (options.op) { findOptions.op = options.op; } - return this.config.database - .find(this.className, this.restWhere, findOptions, this.auth) - .then(results => { - if (this.className === '_User' && !findOptions.explain) { - for (var result of results) { - this.cleanResultAuthData(result); - } - } + const results = await this.config.database.find( + this.className, + this.restWhere, + findOptions, + this.auth + ); + if (this.className === '_User' && !findOptions.explain) { + for (const result of results) { + this.cleanResultAuthData(result); + } + } - this.config.filesController.expandFilesInObject(this.config, results); + await this.config.filesController.expandFilesInObject( + this.config, + results, + this.className, + this.auth, + findOptions.op + ); - if (this.redirectClassName) { - for (var r of results) { - r.className = this.redirectClassName; - } - } - this.response = { results: results }; - }); + if (this.redirectClassName) { + for (const r of results) { + r.className = this.redirectClassName; + } + } + this.response = { results: results }; }; // Returns a promise for whether it was successful. diff --git a/src/RestWrite.js b/src/RestWrite.js index 3a8385e52a..4b2e84246b 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -314,7 +314,12 @@ RestWrite.prototype.runBeforeLoginTrigger = async function (userData) { const extraData = { className: this.className }; // Expand file objects - this.config.filesController.expandFilesInObject(this.config, userData); + await this.config.filesController.expandFilesInObject( + this.config, + userData, + this.className, + this.auth + ); const user = triggers.inflate(extraData, userData); @@ -1343,11 +1348,13 @@ RestWrite.prototype.handleInstallation = function () { // If we short-circuited the object response - then we need to make sure we expand all the files, // since this might not have a query, meaning it won't return the full result back. // TODO: (nlutsenko) This should die when we move to per-class based controllers on _Session/_User -RestWrite.prototype.expandFilesForExistingObjects = function () { - // Check whether we have a short-circuited response - only then run expansion. - if (this.response && this.response.response) { - this.config.filesController.expandFilesInObject(this.config, this.response.response); - } +RestWrite.prototype.expandFilesForExistingObjects = async function () { + await this.config.filesController.expandFilesInObject( + this.config, + this.data, + this.className, + this.auth + ); }; RestWrite.prototype.runDatabaseOperation = function () { diff --git a/src/Routers/FilesRouter.js b/src/Routers/FilesRouter.js index e911d772a4..ff0af71d4f 100644 --- a/src/Routers/FilesRouter.js +++ b/src/Routers/FilesRouter.js @@ -5,6 +5,8 @@ import Parse from 'parse/node'; import Config from '../Config'; import mime from 'mime'; import logger from '../logger'; +import Auth from '../Auth'; +import RestQuery from '../RestQuery'; const triggers = require('../triggers'); const http = require('http'); const Utils = require('../Utils'); @@ -67,7 +69,7 @@ export class FilesRouter { return router; } - getHandler(req, res) { + async getHandler(req, res) { const config = Config.get(req.params.appId); if (!config) { res.status(403); @@ -75,6 +77,7 @@ export class FilesRouter { res.json({ code: err.code, error: err.message }); return; } + const token = req.param('token'); const filesController = config.filesController; const filename = req.params.filename; const contentType = mime.getType(filename); @@ -84,20 +87,57 @@ export class FilesRouter { res.set('Content-Type', 'text/plain'); res.end('File not found.'); }); - } else { - filesController - .getFileData(config, filename) - .then(data => { - res.status(200); - res.set('Content-Type', contentType); - res.set('Content-Length', data.length); - res.end(data); - }) - .catch(() => { - res.status(404); - res.set('Content-Type', 'text/plain'); - res.end('File not found.'); + return; + } + try { + const fileSession = await new Parse.Query('_FileSession') + .equalTo('token', token) + .first({ useMasterKey: true }); + if (!fileSession) { + throw 'File not found'; + } + let auth = new Auth.Auth({ + config: req.config, + isMaster: false, + }); + if (fileSession.get('master')) { + auth = new Auth.Auth({ + config: req.config, + installationId: fileSession.get('installationId'), + isMaster: true, + }); + } else if (fileSession.get('sessionToken')) { + auth = await Auth.getAuthForSessionToken({ + config: req.config, + installationId: fileSession.get('installationId'), + sessionToken: await fileSession.get('sessionToken'), }); + } + const fileObject = new Parse.File(filename); + fileObject._url = filesController.adapter.getFileLocation(req.config, filename); + fileObject.contentType = contentType; + const triggerResult = await triggers.maybeRunFileTrigger( + triggers.Types.beforeFind, + { file: fileObject }, + config, + auth + ); + const data = await filesController.getFileData(config, triggerResult.file.name()); + fileObject._data = data; + await triggers.maybeRunFileTrigger( + triggers.Types.afterFind, + { file: fileObject }, + config, + auth + ); + res.status(200); + res.set('Content-Type', contentType); + res.set('Content-Length', data.length); + res.end(data); + } catch (e) { + res.status(404); + res.set('Content-Type', 'text/plain'); + res.end(e || 'File not found.'); } } @@ -126,7 +166,7 @@ export class FilesRouter { return; } const filesController = config.filesController; - const { filename } = req.params; + const { filename, acl } = req.params; const contentType = req.get('Content-type'); if (!req.body || !req.body.length) { @@ -218,6 +258,23 @@ export class FilesRouter { url: createFileResult.url, name: createFileResult.name, }; + const fileObj = new Parse.Object('_FileObject'); + const file = new Parse.File(fileObject.file._name); + file._url = fileObject.file._url; + fileObj.set('file', file); + fileObj.setACL( + new Parse.ACL( + acl || + user?.id || { + '*': { + read: true, + }, + } + ) + ); + await fileObj.save(null, { useMasterKey: true }); + const token = await filesController.createFileSession(config, req.auth, fileObj.id); + saveResult.url = saveResult.url + '?token=' + token; } // run afterSaveFile trigger await triggers.maybeRunFileTrigger(triggers.Types.afterSave, fileObject, config, req.auth); @@ -242,6 +299,18 @@ export class FilesRouter { const file = new Parse.File(filename); file._url = filesController.adapter.getFileLocation(req.config, filename); const fileObject = { file, fileSize: null }; + const files = await new RestQuery( + req.config, + req.auth, + '_FileObject', + { file: file.toJSON() }, + { limit: 1 } + ).execute(); + if (files.results.length === 0 && !req.config.fileUpload.enableLegacyAccess) { + const error = new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'File not found.'); + next(error); + return; + } await triggers.maybeRunFileTrigger( triggers.Types.beforeDelete, fileObject, @@ -257,6 +326,27 @@ export class FilesRouter { req.config, req.auth ); + (async () => { + if (!files.results[0]?.objectId) { + return; + } + const fileObj = Parse.Object.extend('_FileObject').createWithoutData( + files.results[0].objectId + ); + new Parse.Query('_FileReference').equalTo({ file: fileObj }).each( + obj => { + obj.destroy(null, { useMasterKey: true }); + }, + { useMasterKey: true } + ); + new Parse.Query('_FileSession').equalTo({ file: fileObj }).each( + obj => { + obj.destroy(null, { useMasterKey: true }); + }, + { useMasterKey: true } + ); + await fileObj.destroy({ useMasterKey: true }); + })(); res.status(200); // TODO: return useful JSON here? res.end(); diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index a0c0039c47..6421e2acfe 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -246,7 +246,7 @@ export class UsersRouter extends ClassesRouter { // Remove hidden properties. UsersRouter.removeHiddenProperties(user); - req.config.filesController.expandFilesInObject(req.config, user); + await req.config.filesController.expandFilesInObject(req.config, user); // Before login trigger; throws if failure await maybeRunTrigger( diff --git a/src/rest.js b/src/rest.js index e1e53668a6..fa6b7ca0b9 100644 --- a/src/rest.js +++ b/src/rest.js @@ -152,14 +152,27 @@ function del(config, auth, className, objectId, context) { } } - return config.database.destroy( - className, - { - objectId: objectId, - }, - options, - schemaController - ); + return Promise.all([ + config.database.destroy( + className, + { + objectId: objectId, + }, + options, + schemaController + ), + config.database + .destroy( + '_FileReference', + { + referenceId: objectId, + referenceClass: className, + }, + {}, + schemaController + ) + .catch(e => e), + ]); }) .then(() => { // Notify LiveQuery server if possible From 729fd977bca25cb92f354efd794e4ea045c44d16 Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 11 Jan 2023 18:20:02 +1100 Subject: [PATCH 02/12] Update CloudCode.spec.js --- spec/CloudCode.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index d7b3371bba..2b20c7d5d9 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -3846,7 +3846,7 @@ describe('saveFile hooks', () => { } }); - fit('can clean up files', async () => { + it('can clean up files', async () => { const server = await reconfigureServer(); const base64 = 'V29ya2luZyBhdCBQYXJzZSBpcyBncmVhdCE='; const file = new Parse.File('myfile.txt', { base64 }); From c7f5b75328502613486c1de4025d69ae289dd5f8 Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 11 Jan 2023 18:20:10 +1100 Subject: [PATCH 03/12] Update ParseFile.spec.js --- spec/ParseFile.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/ParseFile.spec.js b/spec/ParseFile.spec.js index 6a9a49fb4a..0aa3dd1cf1 100644 --- a/spec/ParseFile.spec.js +++ b/spec/ParseFile.spec.js @@ -265,7 +265,7 @@ describe('Parse.File testing', () => { ok(objectAgain.get('file') instanceof Parse.File); }); - fit('autosave file in object', async () => { + it('autosave file in object', async () => { let file = new Parse.File('hello.txt', data, 'text/plain'); ok(!file.url()); const object = new Parse.Object('TestObject'); From 3ce5cb5661050e5cd63dc3a507e0a8ed2a374d70 Mon Sep 17 00:00:00 2001 From: dblythy Date: Tue, 11 Apr 2023 17:35:44 +1000 Subject: [PATCH 04/12] wip --- spec/CloudCode.spec.js | 23 +++++++++++++++-------- spec/ParseFile.spec.js | 8 ++++++++ spec/helper.js | 4 ++-- src/Controllers/DatabaseController.js | 7 ------- src/Controllers/FilesController.js | 8 ++++++++ src/Options/index.js | 2 +- src/Routers/FilesRouter.js | 16 +++++++++------- 7 files changed, 43 insertions(+), 25 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 2b20c7d5d9..8bb69b9778 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -3683,7 +3683,7 @@ describe('saveFile hooks', () => { await file.save({ useMasterKey: true }); }); - it('beforeDeleteFile should call with fileObject', async () => { + fit('beforeDeleteFile should call with fileObject', async () => { await reconfigureServer({ filesAdapter: mockAdapter }); Parse.Cloud.beforeDelete(Parse.File, req => { expect(req.file).toBeInstanceOf(Parse.File); @@ -3695,7 +3695,7 @@ describe('saveFile hooks', () => { await file.destroy({ useMasterKey: true }); }); - it('beforeDeleteFile should throw error', async done => { + fit('beforeDeleteFile should throw error', async done => { await reconfigureServer({ filesAdapter: mockAdapter }); Parse.Cloud.beforeDelete(Parse.File, () => { throw new Error('some error message'); @@ -3709,7 +3709,7 @@ describe('saveFile hooks', () => { } }); - it('afterDeleteFile should call with fileObject', async done => { + fit('afterDeleteFile should call with fileObject', async done => { await reconfigureServer({ filesAdapter: mockAdapter }); Parse.Cloud.beforeDelete(Parse.File, req => { expect(req.file).toBeInstanceOf(Parse.File); @@ -3764,8 +3764,7 @@ describe('saveFile hooks', () => { } }); - it('legacy hooks', async () => { - await reconfigureServer({ filesAdapter: mockAdapter }); + fit('legacy hooks', async () => { const logger = require('../lib/logger').logger; const logSpy = spyOn(logger, 'warn').and.callFake(() => {}); const triggers = { @@ -3813,7 +3812,15 @@ describe('saveFile hooks', () => { } }); - it('can run find hooks', async () => { + fit('can run find hooks', async () => { + await reconfigureServer({ + fileUpload: { + enableForPublic: true, + enableForAnonymousUser: true, + enableForAuthenticatedUser: true, + enableLegacyAccess: true, + }, + }); const user = new Parse.User(); user.setUsername('triggeruser'); user.setPassword('triggeruser'); @@ -3826,13 +3833,13 @@ describe('saveFile hooks', () => { expect(req.user.id).toEqual(user.id); expect(req.file instanceof Parse.File).toBeTrue(); expect(req.file._name).toEqual(file._name); - expect(req.file._url).toEqual(file._url); + expect(req.file._url).toEqual(file._url.split('?')[0]); }, afterFind(req) { expect(req.user.id).toEqual(user.id); expect(req.file instanceof Parse.File).toBeTrue(); expect(req.file._name).toEqual(file._name); - expect(req.file._url).toEqual(file._url); + expect(req.file._url).toEqual(file._url.split('?')[0]); }, }; for (const key in hooks) { diff --git a/spec/ParseFile.spec.js b/spec/ParseFile.spec.js index 0aa3dd1cf1..e26e23e698 100644 --- a/spec/ParseFile.spec.js +++ b/spec/ParseFile.spec.js @@ -266,6 +266,14 @@ describe('Parse.File testing', () => { }); it('autosave file in object', async () => { + await reconfigureServer({ + fileUpload: { + enableForPublic: true, + enableForAnonymousUser: true, + enableForAuthenticatedUser: true, + enableLegacyAccess: true, + }, + }); let file = new Parse.File('hello.txt', data, 'text/plain'); ok(!file.url()); const object = new Parse.Object('TestObject'); diff --git a/spec/helper.js b/spec/helper.js index 0965873be7..988281deae 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -82,7 +82,7 @@ on_db( ); let logLevel; -let silent = false; +let silent = true; if (process.env.VERBOSE) { silent = false; logLevel = 'verbose'; @@ -112,7 +112,7 @@ const defaultConfiguration = { enableForPublic: true, enableForAnonymousUser: true, enableForAuthenticatedUser: true, - enableLegacyAccess: false, + enableLegacyAccess: true, tokenValidityDuration: 5 * 60, }, push: { diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index afe98c7e7c..6df1c1abba 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -1773,13 +1773,6 @@ class DatabaseController { throw error; }); - await this.adapter - .ensureUniqueness('_FileObject', requiredFileObjectFields, ['file']) - .catch(error => { - logger.warn('Unable to ensure uniqueness for file object: ', error); - throw error; - }); - await this.adapter .ensureUniqueness('_FileSession', requiredFileSessionFields, ['token']) .catch(error => { diff --git a/src/Controllers/FilesController.js b/src/Controllers/FilesController.js index 5ad4dad5ba..4885bdd53b 100644 --- a/src/Controllers/FilesController.js +++ b/src/Controllers/FilesController.js @@ -136,6 +136,14 @@ export class FilesController extends AdaptableController { installationId: auth?.installationId, }); await fileSession.save(null, { useMasterKey: true }); + + clearTimeout(this.clearExpiredFileSessions); + this.clearExpiredFileSessions = setTimeout(() => { + new Parse.Query('_FileSession') + .lessThan('expiry', new Date()) + .each(session => session.destroy({ useMasterKey: true }), { useMasterKey: true }); + }, 5000); + return token; } diff --git a/src/Options/index.js b/src/Options/index.js index d8e101b258..d2d90d05d4 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -538,7 +538,7 @@ export interface FileUploadOptions { :DEFAULT: false */ enableForPublic: ?boolean; /* Is true if files that do not have a corresponding _FileObject should be publicly accessable. - :DEFAULT: false */ + :DEFAULT: true */ enableLegacyAccess: ?boolean; /* Duration of the file token in seconds :DEFAULT: 300 */ diff --git a/src/Routers/FilesRouter.js b/src/Routers/FilesRouter.js index ff0af71d4f..1f54441014 100644 --- a/src/Routers/FilesRouter.js +++ b/src/Routers/FilesRouter.js @@ -97,24 +97,28 @@ export class FilesRouter { throw 'File not found'; } let auth = new Auth.Auth({ - config: req.config, + config, isMaster: false, }); if (fileSession.get('master')) { auth = new Auth.Auth({ - config: req.config, + config, installationId: fileSession.get('installationId'), isMaster: true, }); } else if (fileSession.get('sessionToken')) { auth = await Auth.getAuthForSessionToken({ - config: req.config, + config, installationId: fileSession.get('installationId'), sessionToken: await fileSession.get('sessionToken'), }); } const fileObject = new Parse.File(filename); - fileObject._url = filesController.adapter.getFileLocation(req.config, filename); + const conf = { ...config }; + if (!conf.mount) { + conf.mount = conf.serverURL; + } + fileObject._url = filesController.adapter.getFileLocation(conf, filename); fileObject.contentType = contentType; const triggerResult = await triggers.maybeRunFileTrigger( triggers.Types.beforeFind, @@ -307,9 +311,7 @@ export class FilesRouter { { limit: 1 } ).execute(); if (files.results.length === 0 && !req.config.fileUpload.enableLegacyAccess) { - const error = new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'File not found.'); - next(error); - return; + throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'File not found.'); } await triggers.maybeRunFileTrigger( triggers.Types.beforeDelete, From 1d446760b10e8491e3530e84e13b8222086aed6b Mon Sep 17 00:00:00 2001 From: dblythy Date: Tue, 11 Apr 2023 17:54:33 +1000 Subject: [PATCH 05/12] wip --- spec/CloudCode.spec.js | 3 ++- spec/ParseFile.spec.js | 2 +- src/Routers/FilesRouter.js | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 8bb69b9778..2f73005da8 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -3765,6 +3765,7 @@ describe('saveFile hooks', () => { }); fit('legacy hooks', async () => { + await reconfigureServer({ filesAdapter: mockAdapter }); const logger = require('../lib/logger').logger; const logSpy = spyOn(logger, 'warn').and.callFake(() => {}); const triggers = { @@ -3818,7 +3819,7 @@ describe('saveFile hooks', () => { enableForPublic: true, enableForAnonymousUser: true, enableForAuthenticatedUser: true, - enableLegacyAccess: true, + enableLegacyAccess: false, }, }); const user = new Parse.User(); diff --git a/spec/ParseFile.spec.js b/spec/ParseFile.spec.js index e26e23e698..1e93e44363 100644 --- a/spec/ParseFile.spec.js +++ b/spec/ParseFile.spec.js @@ -271,7 +271,7 @@ describe('Parse.File testing', () => { enableForPublic: true, enableForAnonymousUser: true, enableForAuthenticatedUser: true, - enableLegacyAccess: true, + enableLegacyAccess: false, }, }); let file = new Parse.File('hello.txt', data, 'text/plain'); diff --git a/src/Routers/FilesRouter.js b/src/Routers/FilesRouter.js index 1f54441014..a6fdbd41a2 100644 --- a/src/Routers/FilesRouter.js +++ b/src/Routers/FilesRouter.js @@ -116,7 +116,7 @@ export class FilesRouter { const fileObject = new Parse.File(filename); const conf = { ...config }; if (!conf.mount) { - conf.mount = conf.serverURL; + conf.mount = conf.publicServerURL || conf.serverURL; } fileObject._url = filesController.adapter.getFileLocation(conf, filename); fileObject.contentType = contentType; From 86a689ecad5f8fd10d4821f7e0b1f253ff2f7bdd Mon Sep 17 00:00:00 2001 From: dblythy Date: Tue, 11 Apr 2023 18:00:09 +1000 Subject: [PATCH 06/12] Update FilesRouter.js --- src/Routers/FilesRouter.js | 42 +++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/Routers/FilesRouter.js b/src/Routers/FilesRouter.js index a6fdbd41a2..1732e8d897 100644 --- a/src/Routers/FilesRouter.js +++ b/src/Routers/FilesRouter.js @@ -328,27 +328,27 @@ export class FilesRouter { req.config, req.auth ); - (async () => { - if (!files.results[0]?.objectId) { - return; - } - const fileObj = Parse.Object.extend('_FileObject').createWithoutData( - files.results[0].objectId - ); - new Parse.Query('_FileReference').equalTo({ file: fileObj }).each( - obj => { - obj.destroy(null, { useMasterKey: true }); - }, - { useMasterKey: true } - ); - new Parse.Query('_FileSession').equalTo({ file: fileObj }).each( - obj => { - obj.destroy(null, { useMasterKey: true }); - }, - { useMasterKey: true } - ); - await fileObj.destroy({ useMasterKey: true }); - })(); + const fileId = files.results[0]?.objectId; + if (fileId) { + const fileObj = Parse.Object.extend('_FileObject').createWithoutData(fileId); + fileObj + .destroy({ useMasterKey: true }) + .then(() => { + new Parse.Query('_FileReference').equalTo({ file: fileObj }).each( + obj => { + obj.destroy(null, { useMasterKey: true }); + }, + { useMasterKey: true } + ); + new Parse.Query('_FileSession').equalTo({ file: fileObj }).each( + obj => { + obj.destroy(null, { useMasterKey: true }); + }, + { useMasterKey: true } + ); + }) + .catch(e => e); + } res.status(200); // TODO: return useful JSON here? res.end(); From 62b59cf8d2021d8423800b06bc1a09ae60a75d8c Mon Sep 17 00:00:00 2001 From: dblythy Date: Tue, 11 Apr 2023 18:09:23 +1000 Subject: [PATCH 07/12] Update CloudCode.spec.js --- spec/CloudCode.spec.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 2f73005da8..731b73ca83 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -3683,7 +3683,7 @@ describe('saveFile hooks', () => { await file.save({ useMasterKey: true }); }); - fit('beforeDeleteFile should call with fileObject', async () => { + it('beforeDeleteFile should call with fileObject', async () => { await reconfigureServer({ filesAdapter: mockAdapter }); Parse.Cloud.beforeDelete(Parse.File, req => { expect(req.file).toBeInstanceOf(Parse.File); @@ -3695,7 +3695,7 @@ describe('saveFile hooks', () => { await file.destroy({ useMasterKey: true }); }); - fit('beforeDeleteFile should throw error', async done => { + it('beforeDeleteFile should throw error', async done => { await reconfigureServer({ filesAdapter: mockAdapter }); Parse.Cloud.beforeDelete(Parse.File, () => { throw new Error('some error message'); @@ -3709,7 +3709,7 @@ describe('saveFile hooks', () => { } }); - fit('afterDeleteFile should call with fileObject', async done => { + it('afterDeleteFile should call with fileObject', async done => { await reconfigureServer({ filesAdapter: mockAdapter }); Parse.Cloud.beforeDelete(Parse.File, req => { expect(req.file).toBeInstanceOf(Parse.File); @@ -3764,7 +3764,7 @@ describe('saveFile hooks', () => { } }); - fit('legacy hooks', async () => { + it('legacy hooks', async () => { await reconfigureServer({ filesAdapter: mockAdapter }); const logger = require('../lib/logger').logger; const logSpy = spyOn(logger, 'warn').and.callFake(() => {}); @@ -3813,7 +3813,7 @@ describe('saveFile hooks', () => { } }); - fit('can run find hooks', async () => { + it('can run find hooks', async () => { await reconfigureServer({ fileUpload: { enableForPublic: true, From 3886372347be4726f9ffaac64b210fc45648417e Mon Sep 17 00:00:00 2001 From: dblythy Date: Tue, 11 Apr 2023 18:10:41 +1000 Subject: [PATCH 08/12] Update Definitions.js --- src/Options/Definitions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index e2ceb07cb4..3411b7b5b8 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -972,7 +972,7 @@ module.exports.FileUploadOptions = { help: 'Is true if files that do not have a corresponding _FileObject should be publicly accessable.', action: parsers.booleanParser, - default: false, + default: true, }, tokenValidityDuration: { env: 'PARSE_SERVER_FILE_UPLOAD_TOKEN_VALIDITY_DURATION', From a0c274b0a0bb85655a5d5621baf2d305b644af89 Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 12 Apr 2023 13:37:00 +1000 Subject: [PATCH 09/12] fix tests --- spec/ParseUser.spec.js | 31 ++++++++++--------------------- src/RestWrite.js | 14 ++++++++------ 2 files changed, 18 insertions(+), 27 deletions(-) diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index 4d3beaf349..df3915cbb5 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -1361,30 +1361,19 @@ describe('Parse.User testing', () => { .catch(done.fail); }); - it('log in with provider with files', done => { + it('log in with provider with files', async () => { const provider = getMockFacebookProvider(); Parse.User._registerAuthenticationProvider(provider); const file = new Parse.File('yolo.txt', [1, 2, 3], 'text/plain'); - file - .save() - .then(file => { - const user = new Parse.User(); - user.set('file', file); - return user._linkWith('facebook', {}); - }) - .then(user => { - expect(user._isLinked('facebook')).toBeTruthy(); - return Parse.User._logInWith('facebook', {}); - }) - .then(user => { - const fileAgain = user.get('file'); - expect(fileAgain.name()).toMatch(/yolo.txt$/); - expect(fileAgain.url()).toMatch(/yolo.txt$/); - }) - .then(() => { - done(); - }) - .catch(done.fail); + await file.save(); + let user = new Parse.User(); + user.set('file', file); + await user._linkWith('facebook', {}); + expect(user._isLinked('facebook')).toBeTruthy(); + user = await Parse.User._logInWith('facebook', {}); + const fileAgain = user.get('file'); + expect(fileAgain.name()).toMatch(/yolo.txt$/); + expect(fileAgain.url()).toMatch(/yolo.txt$/); }); it('log in with provider twice', async done => { diff --git a/src/RestWrite.js b/src/RestWrite.js index 4b2e84246b..17753fea8a 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -1349,12 +1349,14 @@ RestWrite.prototype.handleInstallation = function () { // since this might not have a query, meaning it won't return the full result back. // TODO: (nlutsenko) This should die when we move to per-class based controllers on _Session/_User RestWrite.prototype.expandFilesForExistingObjects = async function () { - await this.config.filesController.expandFilesInObject( - this.config, - this.data, - this.className, - this.auth - ); + if (this.response?.response) { + await this.config.filesController.expandFilesInObject( + this.config, + this.response.response, + this.className, + this.auth + ); + } }; RestWrite.prototype.runDatabaseOperation = function () { From b799706b6a720a347dad2997627970f47ce41bd6 Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 17 May 2023 14:09:50 +1000 Subject: [PATCH 10/12] wip --- spec/CloudCode.spec.js | 3 ++- spec/helper.js | 2 +- src/Controllers/FilesController.js | 2 +- src/RestWrite.js | 12 ++++++++++++ src/Routers/UsersRouter.js | 2 +- 5 files changed, 17 insertions(+), 4 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 731b73ca83..cb054be237 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -3854,11 +3854,12 @@ describe('saveFile hooks', () => { } }); - it('can clean up files', async () => { + fit('can clean up files', async () => { const server = await reconfigureServer(); const base64 = 'V29ya2luZyBhdCBQYXJzZSBpcyBncmVhdCE='; const file = new Parse.File('myfile.txt', { base64 }); const obj = await new Parse.Object('TestObject').save({ file }); + await new Promise(resolve => setTimeout(resolve, 1000)); await Promise.all([ (async () => { const objects = await new Parse.Query('_FileObject').find({ useMasterKey: true }); diff --git a/spec/helper.js b/spec/helper.js index 7aada7ae11..29e62b0f55 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -82,7 +82,7 @@ on_db( ); let logLevel; -let silent = true; +let silent = false; if (process.env.VERBOSE) { silent = false; logLevel = 'verbose'; diff --git a/src/Controllers/FilesController.js b/src/Controllers/FilesController.js index 4885bdd53b..8ec66e3e12 100644 --- a/src/Controllers/FilesController.js +++ b/src/Controllers/FilesController.js @@ -54,7 +54,7 @@ export class FilesController extends AdaptableController { * Object may be a single object or list of REST-format objects. */ async expandFilesInObject(config, object, className, auth, op) { - if (object instanceof Array) { + if (Array.isArray(object)) { return Promise.all( object.map(obj => this.expandFilesInObject(config, obj, className, auth, op)) ); diff --git a/src/RestWrite.js b/src/RestWrite.js index 17753fea8a..d4572e0c8f 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -150,6 +150,9 @@ RestWrite.prototype.execute = function () { .then(() => { return this.runAfterSaveTrigger(); }) + .then(() => { + return this.buildFileReferences(); + }) .then(() => { return this.cleanUserAuthData(); }) @@ -1359,6 +1362,15 @@ RestWrite.prototype.expandFilesForExistingObjects = async function () { } }; +RestWrite.prototype.buildFileReferences = async function () { + await this.config.filesController.expandFilesInObject( + this.config, + this.data, + this.className, + this.auth + ); +}; + RestWrite.prototype.runDatabaseOperation = function () { if (this.response) { return; diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index e14b388821..50dd29417e 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -246,7 +246,7 @@ export class UsersRouter extends ClassesRouter { // Remove hidden properties. UsersRouter.removeHiddenProperties(user); - await req.config.filesController.expandFilesInObject(req.config, user); + await req.config.filesController.expandFilesInObject(req.config, user, '_User', req.auth); // Before login trigger; throws if failure await maybeRunTrigger( From 7ecf5db6d518a79f5ed5f75a218b863315b7c5b7 Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 17 May 2023 14:13:30 +1000 Subject: [PATCH 11/12] Update ParseServer.js --- src/ParseServer.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ParseServer.js b/src/ParseServer.js index c23bb55059..4189fdae9b 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -158,7 +158,9 @@ class ParseServer { new Parse.Query('_FileObject').each( async obj => { const file = obj.get('file'); - const reference = await new Parse.Query('_FileReference').equalTo('file', obj).first(); + const reference = await new Parse.Query('_FileReference') + .equalTo('file', obj) + .first({ useMasterKey: true }); if (!reference) { console.log(`Deleting orphaned file ${file.url()}`); await file.destroy({ useMasterKey: true }); From d6792241446474e4a041c0fc5fd8693d5901479d Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 17 May 2023 14:16:16 +1000 Subject: [PATCH 12/12] Update CloudCode.spec.js --- spec/CloudCode.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index cb054be237..ef6f9928a8 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -3854,7 +3854,7 @@ describe('saveFile hooks', () => { } }); - fit('can clean up files', async () => { + it('can clean up files', async () => { const server = await reconfigureServer(); const base64 = 'V29ya2luZyBhdCBQYXJzZSBpcyBncmVhdCE='; const file = new Parse.File('myfile.txt', { base64 });