2016-01-28 10:58:12 -08:00
// A RestWrite encapsulates everything we need to run an operation
// that writes to the database.
// This could be either a "create" or an "update".
2016-04-18 18:59:57 -07:00
var SchemaController = require ( './Controllers/SchemaController' ) ;
2016-01-28 10:58:12 -08:00
var deepcopy = require ( 'deepcopy' ) ;
2018-02-19 11:15:54 -05:00
const Auth = require ( './Auth' ) ;
2016-02-12 02:02:55 +01:00
var cryptoUtils = require ( './cryptoUtils' ) ;
2016-02-01 11:24:26 -08:00
var passwordCrypto = require ( './password' ) ;
2016-01-28 10:58:12 -08:00
var Parse = require ( 'parse/node' ) ;
var triggers = require ( './triggers' ) ;
2016-07-15 09:24:53 -04:00
var ClientSDK = require ( './ClientSDK' ) ;
2016-05-22 09:59:36 -07:00
import RestQuery from './RestQuery' ;
2016-05-23 17:13:32 -07:00
import _ from 'lodash' ;
2017-10-26 11:28:13 -07:00
import logger from './logger' ;
2016-01-28 10:58:12 -08:00
// query and data are both provided in REST API format. So data
// types are encoded by plain old objects.
// If query is null, this is a "create" and the data in data should be
// created.
// Otherwise this is an "update" - the object matching the query
// should get updated with data.
// RestWrite will handle objectId, createdAt, and updatedAt for
// everything. It also knows to use triggers and special modifications
// for the _User class.
2016-07-12 10:06:13 -04:00
function RestWrite ( config , auth , className , query , data , originalData , clientSDK ) {
2017-10-26 15:35:07 -04:00
if ( auth . isReadOnly ) {
throw new Parse . Error ( Parse . Error . OPERATION _FORBIDDEN , 'Cannot perform a write operation when using readOnlyMasterKey' ) ;
}
2016-01-28 10:58:12 -08:00
this . config = config ;
this . auth = auth ;
this . className = className ;
2016-07-12 10:06:13 -04:00
this . clientSDK = clientSDK ;
2016-01-28 10:58:12 -08:00
this . storage = { } ;
2016-02-18 13:29:26 -05:00
this . runOptions = { } ;
2016-01-28 10:58:12 -08:00
if ( ! query && data . objectId ) {
2016-06-11 00:43:02 -07:00
throw new Parse . Error ( Parse . Error . INVALID _KEY _NAME , 'objectId is an invalid field name.' ) ;
2016-01-28 10:58:12 -08:00
}
2016-03-11 23:03:47 -05:00
2016-01-28 10:58:12 -08:00
// When the operation is complete, this.response may have several
// fields.
// response: the actual data to be returned
// status: the http status code. if not present, treated like a 200
// location: the location header. if not present, no location header
this . response = null ;
// Processing this operation may mutate our data, so we operate on a
// copy
this . query = deepcopy ( query ) ;
this . data = deepcopy ( data ) ;
// We never change originalData, so we do not need a deep copy
this . originalData = originalData ;
// The timestamp we'll use for this whole operation
this . updatedAt = Parse . _encode ( new Date ( ) ) . iso ;
}
// A convenient method to perform all the steps of processing the
// write, in order.
// Returns a promise for a {response, status, location} object.
// status and location are optional.
RestWrite . prototype . execute = function ( ) {
return Promise . resolve ( ) . then ( ( ) => {
2016-02-11 22:16:07 -05:00
return this . getUserAndRoleACL ( ) ;
2016-02-26 22:55:39 +08:00
} ) . then ( ( ) => {
return this . validateClientClassCreation ( ) ;
2016-01-28 10:58:12 -08:00
} ) . then ( ( ) => {
return this . handleInstallation ( ) ;
} ) . then ( ( ) => {
return this . handleSession ( ) ;
2016-02-27 15:54:43 +01:00
} ) . then ( ( ) => {
return this . validateAuthData ( ) ;
2016-01-28 10:58:12 -08:00
} ) . then ( ( ) => {
return this . runBeforeTrigger ( ) ;
2016-09-09 14:41:03 -04:00
} ) . then ( ( ) => {
return this . validateSchema ( ) ;
2016-02-22 19:33:27 -08:00
} ) . then ( ( ) => {
return this . setRequiredFieldsIfNeeded ( ) ;
2016-01-28 10:58:12 -08:00
} ) . then ( ( ) => {
return this . transformUser ( ) ;
2016-02-24 15:15:57 -08:00
} ) . then ( ( ) => {
return this . expandFilesForExistingObjects ( ) ;
2017-11-11 09:41:23 -05:00
} ) . then ( ( ) => {
return this . destroyDuplicatedSessions ( ) ;
2016-01-28 10:58:12 -08:00
} ) . then ( ( ) => {
return this . runDatabaseOperation ( ) ;
2016-04-20 11:57:38 -04:00
} ) . then ( ( ) => {
return this . createSessionTokenIfNeeded ( ) ;
2016-01-28 10:58:12 -08:00
} ) . then ( ( ) => {
return this . handleFollowup ( ) ;
} ) . then ( ( ) => {
return this . runAfterTrigger ( ) ;
2016-03-25 16:11:27 -07:00
} ) . then ( ( ) => {
return this . cleanUserAuthData ( ) ;
2016-01-28 10:58:12 -08:00
} ) . then ( ( ) => {
return this . response ;
2016-06-16 15:39:05 -07:00
} )
2016-01-28 10:58:12 -08:00
} ;
2016-02-11 22:16:07 -05:00
// Uses the Auth object to get the list of roles, adds the user id
RestWrite . prototype . getUserAndRoleACL = function ( ) {
2016-02-16 10:45:43 -05:00
if ( this . auth . isMaster ) {
2016-02-11 22:16:07 -05:00
return Promise . resolve ( ) ;
}
2016-02-16 10:45:43 -05:00
2016-02-18 13:29:26 -05:00
this . runOptions . acl = [ '*' ] ;
2016-02-16 10:45:43 -05:00
2016-02-22 19:33:27 -08:00
if ( this . auth . user ) {
2016-02-16 10:45:43 -05:00
return this . auth . getUserRoles ( ) . then ( ( roles ) => {
2017-09-09 14:02:07 -04:00
this . runOptions . acl = this . runOptions . acl . concat ( roles , [ this . auth . user . id ] ) ;
2016-06-10 20:27:21 -07:00
return ;
2016-02-16 10:45:43 -05:00
} ) ;
2016-06-10 20:27:21 -07:00
} else {
2016-02-11 22:16:07 -05:00
return Promise . resolve ( ) ;
2016-02-16 10:45:43 -05:00
}
2016-02-11 22:16:07 -05:00
} ;
2016-02-26 22:55:39 +08:00
// Validates this operation against the allowClientClassCreation config.
RestWrite . prototype . validateClientClassCreation = function ( ) {
2016-02-27 15:37:34 +08:00
if ( this . config . allowClientClassCreation === false && ! this . auth . isMaster
2016-06-14 00:21:52 +08:00
&& SchemaController . systemClasses . indexOf ( this . className ) === - 1 ) {
return this . config . database . loadSchema ( )
. then ( schemaController => schemaController . hasClass ( this . className ) )
. then ( hasClass => {
if ( hasClass !== true ) {
throw new Parse . Error ( Parse . Error . OPERATION _FORBIDDEN ,
2017-06-20 09:15:26 -07:00
'This user is not allowed to access ' +
2016-06-14 00:21:52 +08:00
'non-existent class: ' + this . className ) ;
}
2016-11-24 15:47:41 -05:00
} ) ;
2016-02-26 22:55:39 +08:00
} else {
return Promise . resolve ( ) ;
}
} ;
2016-01-28 10:58:12 -08:00
// Validates this operation against the schema.
RestWrite . prototype . validateSchema = function ( ) {
2016-03-07 23:07:24 -05:00
return this . config . database . validateObject ( this . className , this . data , this . query , this . runOptions ) ;
2016-01-28 10:58:12 -08:00
} ;
// Runs any beforeSave triggers against this operation.
// Any change leads to our data being mutated.
RestWrite . prototype . runBeforeTrigger = function ( ) {
2016-02-27 15:54:43 +01:00
if ( this . response ) {
return ;
}
2016-03-11 23:03:47 -05:00
2016-02-24 00:05:03 -08:00
// Avoid doing any setup for triggers if there is no 'beforeSave' trigger for this class.
2016-02-24 15:55:11 -05:00
if ( ! triggers . triggerExists ( this . className , triggers . Types . beforeSave , this . config . applicationId ) ) {
2016-02-24 00:05:03 -08:00
return Promise . resolve ( ) ;
}
2016-01-28 10:58:12 -08:00
// Cloud code gets a bit of extra data for its objects
var extraData = { className : this . className } ;
if ( this . query && this . query . objectId ) {
extraData . objectId = this . query . objectId ;
}
2016-02-23 18:01:54 -08:00
let originalObject = null ;
2017-06-14 20:51:41 +02:00
const updatedObject = this . buildUpdatedObject ( extraData ) ;
2016-01-28 10:58:12 -08:00
if ( this . query && this . query . objectId ) {
2016-02-23 18:01:54 -08:00
// This is an update for existing object.
2016-01-28 10:58:12 -08:00
originalObject = triggers . inflate ( extraData , this . originalData ) ;
}
return Promise . resolve ( ) . then ( ( ) => {
2016-04-22 08:20:14 +12:00
return triggers . maybeRunTrigger ( triggers . Types . beforeSave , this . auth , updatedObject , originalObject , this . config ) ;
2016-01-28 10:58:12 -08:00
} ) . then ( ( response ) => {
if ( response && response . object ) {
2016-08-05 18:23:54 +05:30
this . storage . fieldsChangedByTrigger = _ . reduce ( response . object , ( result , value , key ) => {
if ( ! _ . isEqual ( this . data [ key ] , value ) ) {
result . push ( key ) ;
}
return result ;
} , [ ] ) ;
2016-01-28 10:58:12 -08:00
this . data = response . object ;
// We should delete the objectId for an update write
if ( this . query && this . query . objectId ) {
delete this . data . objectId
}
}
} ) ;
} ;
2016-02-22 19:33:27 -08:00
RestWrite . prototype . setRequiredFieldsIfNeeded = function ( ) {
if ( this . data ) {
// Add default fields
this . data . updatedAt = this . updatedAt ;
if ( ! this . query ) {
this . data . createdAt = this . updatedAt ;
2016-03-06 01:32:50 +07:00
// Only assign new objectId if we are creating new object
if ( ! this . data . objectId ) {
2017-06-27 11:22:43 +01:00
this . data . objectId = cryptoUtils . newObjectId ( this . config . objectIdSize ) ;
2016-03-06 01:32:50 +07:00
}
2016-02-22 19:33:27 -08:00
}
}
return Promise . resolve ( ) ;
} ;
2016-01-28 10:58:12 -08:00
// Transforms auth data for a user object.
// Does nothing if this isn't a user object.
// Returns a promise for when we're done if it can't finish this tick.
RestWrite . prototype . validateAuthData = function ( ) {
if ( this . className !== '_User' ) {
return ;
}
if ( ! this . query && ! this . data . authData ) {
2017-03-17 18:57:21 -04:00
if ( typeof this . data . username !== 'string' || _ . isEmpty ( this . data . username ) ) {
2016-01-28 10:58:12 -08:00
throw new Parse . Error ( Parse . Error . USERNAME _MISSING ,
2017-06-20 09:15:26 -07:00
'bad or missing username' ) ;
2016-01-28 10:58:12 -08:00
}
2017-03-17 18:57:21 -04:00
if ( typeof this . data . password !== 'string' || _ . isEmpty ( this . data . password ) ) {
2016-01-28 10:58:12 -08:00
throw new Parse . Error ( Parse . Error . PASSWORD _MISSING ,
2017-06-20 09:15:26 -07:00
'password is required' ) ;
2016-01-28 10:58:12 -08:00
}
}
2016-02-26 05:05:15 -08:00
if ( ! this . data . authData || ! Object . keys ( this . data . authData ) . length ) {
2016-01-28 10:58:12 -08:00
return ;
}
2016-02-04 14:03:39 -05:00
var authData = this . data . authData ;
var providers = Object . keys ( authData ) ;
2016-03-10 18:59:19 -05:00
if ( providers . length > 0 ) {
2016-12-07 15:17:05 -08:00
const canHandleAuthData = providers . reduce ( ( canHandle , provider ) => {
2016-03-11 14:50:33 -05:00
var providerAuthData = authData [ provider ] ;
var hasToken = ( providerAuthData && providerAuthData . id ) ;
return canHandle && ( hasToken || providerAuthData == null ) ;
} , true ) ;
if ( canHandleAuthData ) {
2016-03-10 18:59:19 -05:00
return this . handleAuthData ( authData ) ;
2016-02-04 14:03:39 -05:00
}
2016-01-28 10:58:12 -08:00
}
2016-02-04 14:03:39 -05:00
throw new Parse . Error ( Parse . Error . UNSUPPORTED _SERVICE ,
2017-06-20 09:15:26 -07:00
'This authentication method is unsupported.' ) ;
2016-01-28 10:58:12 -08:00
} ;
2016-03-10 18:59:19 -05:00
RestWrite . prototype . handleAuthDataValidation = function ( authData ) {
2016-12-07 15:17:05 -08:00
const validations = Object . keys ( authData ) . map ( ( provider ) => {
2016-03-10 18:59:19 -05:00
if ( authData [ provider ] === null ) {
return Promise . resolve ( ) ;
2016-01-28 10:58:12 -08:00
}
2016-12-07 15:17:05 -08:00
const validateAuthData = this . config . authDataManager . getValidatorForProvider ( provider ) ;
2016-03-10 18:59:19 -05:00
if ( ! validateAuthData ) {
throw new Parse . Error ( Parse . Error . UNSUPPORTED _SERVICE ,
2017-06-20 09:15:26 -07:00
'This authentication method is unsupported.' ) ;
2016-11-24 15:47:41 -05:00
}
2016-03-10 18:59:19 -05:00
return validateAuthData ( authData [ provider ] ) ;
} ) ;
return Promise . all ( validations ) ;
}
2016-02-04 14:03:39 -05:00
2016-03-10 18:59:19 -05:00
RestWrite . prototype . findUsersWithAuthData = function ( authData ) {
2016-12-07 15:17:05 -08:00
const providers = Object . keys ( authData ) ;
const query = providers . reduce ( ( memo , provider ) => {
2016-03-10 18:59:19 -05:00
if ( ! authData [ provider ] ) {
return memo ;
2016-02-04 14:03:39 -05:00
}
2016-12-07 15:17:05 -08:00
const queryKey = ` authData. ${ provider } .id ` ;
const query = { } ;
2016-03-10 18:59:19 -05:00
query [ queryKey ] = authData [ provider ] . id ;
memo . push ( query ) ;
return memo ;
2016-11-24 15:47:41 -05:00
} , [ ] ) . filter ( ( q ) => {
2016-09-10 23:32:10 +02:00
return typeof q !== 'undefined' ;
2016-03-10 18:59:19 -05:00
} ) ;
2016-03-11 23:03:47 -05:00
2016-03-10 18:59:19 -05:00
let findPromise = Promise . resolve ( [ ] ) ;
if ( query . length > 0 ) {
2016-11-24 15:47:41 -05:00
findPromise = this . config . database . find (
2017-06-20 09:15:26 -07:00
this . className ,
{ '$or' : query } , { } )
2016-03-10 18:59:19 -05:00
}
2016-03-11 23:03:47 -05:00
2016-03-10 18:59:19 -05:00
return findPromise ;
}
2016-01-28 10:58:12 -08:00
2016-03-10 14:27:00 -08:00
2016-03-10 18:59:19 -05:00
RestWrite . prototype . handleAuthData = function ( authData ) {
let results ;
2017-05-07 12:55:30 -04:00
return this . findUsersWithAuthData ( authData ) . then ( ( r ) => {
2018-05-16 15:40:02 -04:00
results = r . filter ( ( user ) => {
return ! this . auth . isMaster && user . ACL && Object . keys ( user . ACL ) . length > 0 ;
} ) ;
2016-03-10 18:59:19 -05:00
if ( results . length > 1 ) {
// More than 1 user with the passed id's
throw new Parse . Error ( Parse . Error . ACCOUNT _ALREADY _LINKED ,
2017-06-20 09:15:26 -07:00
'this auth is already used' ) ;
2016-03-10 18:59:19 -05:00
}
2016-03-11 23:03:47 -05:00
2016-03-11 14:50:33 -05:00
this . storage [ 'authProvider' ] = Object . keys ( authData ) . join ( ',' ) ;
2016-03-11 23:03:47 -05:00
2016-03-22 21:17:53 -04:00
if ( results . length > 0 ) {
2017-05-28 17:50:16 -04:00
const userResult = results [ 0 ] ;
const mutatedAuthData = { } ;
Object . keys ( authData ) . forEach ( ( provider ) => {
const providerData = authData [ provider ] ;
const userAuthData = userResult . authData [ provider ] ;
if ( ! _ . isEqual ( providerData , userAuthData ) ) {
mutatedAuthData [ provider ] = providerData ;
}
} ) ;
const hasMutatedAuthData = Object . keys ( mutatedAuthData ) . length !== 0 ;
2017-07-25 15:26:34 -04:00
let userId ;
if ( this . query && this . query . objectId ) {
userId = this . query . objectId ;
} else if ( this . auth && this . auth . user && this . auth . user . id ) {
userId = this . auth . user . id ;
}
if ( ! userId || userId === userResult . objectId ) { // no user making the call
// OR the user making the call is the right one
2016-03-22 21:17:53 -04:00
// Login with auth data
delete results [ 0 ] . password ;
2016-08-17 15:26:42 +02:00
2016-03-22 21:17:53 -04:00
// need to set the objectId first otherwise location has trailing undefined
2016-07-09 00:49:46 -04:00
this . data . objectId = userResult . objectId ;
2016-08-17 15:26:42 +02:00
2017-08-29 16:34:59 -04:00
if ( ! this . query || ! this . query . objectId ) { // this a login call, no userId passed
this . response = {
response : userResult ,
location : this . location ( )
} ;
}
2017-05-07 12:55:30 -04:00
// If we didn't change the auth data, just keep going
2017-05-28 17:50:16 -04:00
if ( ! hasMutatedAuthData ) {
2017-05-07 12:55:30 -04:00
return ;
}
2016-07-09 00:49:46 -04:00
// We have authData that is updated on login
// that can happen when token are refreshed,
// We should update the token and let the user in
2017-05-07 12:55:30 -04:00
// We should only check the mutated keys
return this . handleAuthDataValidation ( mutatedAuthData ) . then ( ( ) => {
2017-08-29 16:34:59 -04:00
// IF we have a response, we'll skip the database operation / beforeSave / afterSave etc...
// we need to set it up there.
// We are supposed to have a response only on LOGIN with authData, so we skip those
// If we're not logging in, but just updating the current user, we can safely skip that part
if ( this . response ) {
// Assign the new authData in the response
Object . keys ( mutatedAuthData ) . forEach ( ( provider ) => {
this . response . response . authData [ provider ] = mutatedAuthData [ provider ] ;
} ) ;
// Run the DB update directly, as 'master'
// Just update the authData part
// Then we're good for the user, early exit of sorts
return this . config . database . update ( this . className , { objectId : this . data . objectId } , { authData : mutatedAuthData } , { } ) ;
}
2017-05-07 12:55:30 -04:00
} ) ;
2017-07-25 15:26:34 -04:00
} else if ( userId ) {
2016-03-22 21:17:53 -04:00
// Trying to update auth data but users
// are different
2017-07-25 15:26:34 -04:00
if ( userResult . objectId !== userId ) {
2016-03-22 21:17:53 -04:00
throw new Parse . Error ( Parse . Error . ACCOUNT _ALREADY _LINKED ,
2017-06-20 09:15:26 -07:00
'this auth is already used' ) ;
2016-03-22 21:17:53 -04:00
}
2017-05-28 17:50:16 -04:00
// No auth data was mutated, just keep going
if ( ! hasMutatedAuthData ) {
return ;
}
2016-01-28 10:58:12 -08:00
}
2016-04-04 14:27:11 -04:00
}
2017-05-07 12:55:30 -04:00
return this . handleAuthDataValidation ( authData ) ;
2016-03-10 18:59:19 -05:00
} ) ;
2016-02-04 14:03:39 -05:00
}
2016-01-28 10:58:12 -08:00
2016-05-18 12:12:30 +12:00
2016-01-28 10:58:12 -08:00
// The non-third-party parts of User transformation
RestWrite . prototype . transformUser = function ( ) {
2016-11-29 22:31:52 +05:30
var promise = Promise . resolve ( ) ;
2016-02-01 22:20:17 -05:00
if ( this . className !== '_User' ) {
2016-11-29 22:31:52 +05:30
return promise ;
2016-01-28 10:58:12 -08:00
}
2017-05-11 23:14:58 +09:30
if ( ! this . auth . isMaster && "emailVerified" in this . data ) {
const error = ` Clients aren't allowed to manually update email verification. `
throw new Parse . Error ( Parse . Error . OPERATION _FORBIDDEN , error ) ;
}
2017-05-28 20:34:49 -04:00
// Do not cleanup session if objectId is not set
if ( this . query && this . objectId ( ) ) {
2016-05-22 09:59:36 -07:00
// If we're updating a _User object, we need to clear out the cache for that user. Find all their
// session tokens, and remove them from the cache.
2016-11-29 22:31:52 +05:30
promise = new RestQuery ( this . config , Auth . master ( this . config ) , '_Session' , {
user : {
_ _type : "Pointer" ,
className : "_User" ,
objectId : this . objectId ( ) ,
}
} ) . execute ( )
. then ( results => {
results . results . forEach ( session => this . config . cacheController . user . del ( session . sessionToken ) ) ;
} ) ;
2016-03-17 02:06:26 -07:00
}
2016-01-28 10:58:12 -08:00
return promise . then ( ( ) => {
// Transform the password
2017-02-24 17:51:50 +05:30
if ( this . data . password === undefined ) { // ignore only if undefined. should proceed if empty ('')
2016-11-29 22:31:52 +05:30
return Promise . resolve ( ) ;
2016-11-17 22:07:51 +05:30
}
2017-05-16 14:13:09 -04:00
if ( this . query ) {
2016-01-28 10:58:12 -08:00
this . storage [ 'clearSessions' ] = true ;
2017-05-16 14:13:09 -04:00
// Generate a new session only if the user requested
if ( ! this . auth . isMaster ) {
this . storage [ 'generateNewSession' ] = true ;
}
2016-01-28 10:58:12 -08:00
}
2016-11-17 22:07:51 +05:30
2016-11-29 22:31:52 +05:30
return this . _validatePasswordPolicy ( ) . then ( ( ) => {
2016-11-17 22:07:51 +05:30
return passwordCrypto . hash ( this . data . password ) . then ( ( hashedPassword ) => {
this . data . _hashed _password = hashedPassword ;
delete this . data . password ;
} ) ;
2016-01-28 10:58:12 -08:00
} ) ;
} ) . then ( ( ) => {
2016-11-29 22:31:52 +05:30
return this . _validateUserName ( ) ;
} ) . then ( ( ) => {
return this . _validateEmail ( ) ;
} ) ;
} ;
RestWrite . prototype . _validateUserName = function ( ) {
// Check for username uniqueness
if ( ! this . data . username ) {
if ( ! this . query ) {
this . data . username = cryptoUtils . randomString ( 25 ) ;
this . responseShouldHaveUsername = true ;
2016-01-28 10:58:12 -08:00
}
2016-11-29 22:31:52 +05:30
return Promise . resolve ( ) ;
}
// We need to a find to check for duplicate username in case they are missing the unique index on usernames
// TODO: Check if there is a unique index, and if so, skip this query.
return this . config . database . find (
this . className ,
{ username : this . data . username , objectId : { '$ne' : this . objectId ( ) } } ,
{ limit : 1 }
) . then ( results => {
if ( results . length > 0 ) {
throw new Parse . Error ( Parse . Error . USERNAME _TAKEN , 'Account already exists for this username.' ) ;
}
return ;
} ) ;
} ;
RestWrite . prototype . _validateEmail = function ( ) {
if ( ! this . data . email || this . data . email . _ _op === 'Delete' ) {
return Promise . resolve ( ) ;
}
// Validate basic email address format
if ( ! this . data . email . match ( /^.+@.+$/ ) ) {
return Promise . reject ( new Parse . Error ( Parse . Error . INVALID _EMAIL _ADDRESS , 'Email address format is invalid.' ) ) ;
}
// Same problem for email as above for username
return this . config . database . find (
this . className ,
{ email : this . data . email , objectId : { '$ne' : this . objectId ( ) } } ,
{ limit : 1 }
) . then ( results => {
if ( results . length > 0 ) {
throw new Parse . Error ( Parse . Error . EMAIL _TAKEN , 'Account already exists for this email address.' ) ;
2016-01-28 10:58:12 -08:00
}
2017-05-29 21:06:40 -03:00
if (
! this . data . authData ||
! Object . keys ( this . data . authData ) . length ||
Object . keys ( this . data . authData ) . length === 1 && Object . keys ( this . data . authData ) [ 0 ] === 'anonymous'
) {
// We updated the email, send a new validation
this . storage [ 'sendVerificationEmail' ] = true ;
this . config . userController . setEmailVerifyToken ( this . data ) ;
}
2016-11-29 22:31:52 +05:30
} ) ;
} ;
RestWrite . prototype . _validatePasswordPolicy = function ( ) {
if ( ! this . config . passwordPolicy )
return Promise . resolve ( ) ;
return this . _validatePasswordRequirements ( ) . then ( ( ) => {
return this . _validatePasswordHistory ( ) ;
} ) ;
} ;
RestWrite . prototype . _validatePasswordRequirements = function ( ) {
// check if the password conforms to the defined password policy if configured
const policyError = 'Password does not meet the Password Policy requirements.' ;
// check whether the password meets the password strength requirements
if ( this . config . passwordPolicy . patternValidator && ! this . config . passwordPolicy . patternValidator ( this . data . password ) ||
this . config . passwordPolicy . validatorCallback && ! this . config . passwordPolicy . validatorCallback ( this . data . password ) ) {
return Promise . reject ( new Parse . Error ( Parse . Error . VALIDATION _ERROR , policyError ) ) ;
}
// check whether password contain username
if ( this . config . passwordPolicy . doNotAllowUsername === true ) {
if ( this . data . username ) { // username is not passed during password reset
if ( this . data . password . indexOf ( this . data . username ) >= 0 )
return Promise . reject ( new Parse . Error ( Parse . Error . VALIDATION _ERROR , policyError ) ) ;
} else { // retrieve the User object using objectId during password reset
return this . config . database . find ( '_User' , { objectId : this . objectId ( ) } )
. then ( results => {
if ( results . length != 1 ) {
throw undefined ;
}
if ( this . data . password . indexOf ( results [ 0 ] . username ) >= 0 )
return Promise . reject ( new Parse . Error ( Parse . Error . VALIDATION _ERROR , policyError ) ) ;
return Promise . resolve ( ) ;
} ) ;
2016-01-28 10:58:12 -08:00
}
2016-11-29 22:31:52 +05:30
}
return Promise . resolve ( ) ;
} ;
RestWrite . prototype . _validatePasswordHistory = function ( ) {
// check whether password is repeating from specified history
if ( this . query && this . config . passwordPolicy . maxPasswordHistory ) {
return this . config . database . find ( '_User' , { objectId : this . objectId ( ) } , { keys : [ "_password_history" , "_hashed_password" ] } )
. then ( results => {
if ( results . length != 1 ) {
throw undefined ;
}
const user = results [ 0 ] ;
let oldPasswords = [ ] ;
if ( user . _password _history )
oldPasswords = _ . take ( user . _password _history , this . config . passwordPolicy . maxPasswordHistory - 1 ) ;
oldPasswords . push ( user . password ) ;
const newPassword = this . data . password ;
// compare the new password hash with all old password hashes
2016-12-07 15:17:05 -08:00
const promises = oldPasswords . map ( function ( hash ) {
2016-11-29 22:31:52 +05:30
return passwordCrypto . compare ( newPassword , hash ) . then ( ( result ) => {
if ( result ) // reject if there is a match
return Promise . reject ( "REPEAT_PASSWORD" ) ;
return Promise . resolve ( ) ;
} )
} ) ;
// wait for all comparisons to complete
return Promise . all ( promises ) . then ( ( ) => {
return Promise . resolve ( ) ;
} ) . catch ( err => {
if ( err === "REPEAT_PASSWORD" ) // a match was found
return Promise . reject ( new Parse . Error ( Parse . Error . VALIDATION _ERROR , ` New password should not be the same as last ${ this . config . passwordPolicy . maxPasswordHistory } passwords. ` ) ) ;
throw err ;
} ) ;
} ) ;
}
return Promise . resolve ( ) ;
2016-01-28 10:58:12 -08:00
} ;
2016-04-20 11:57:38 -04:00
RestWrite . prototype . createSessionTokenIfNeeded = function ( ) {
if ( this . className !== '_User' ) {
return ;
}
if ( this . query ) {
return ;
}
2017-09-11 11:07:39 -04:00
if ( ! this . storage [ 'authProvider' ] // signup call, with
&& this . config . preventLoginWithUnverifiedEmail // no login without verification
&& this . config . verifyUserEmails ) { // verification is on
return ; // do not create the session token in that case!
}
2016-07-13 07:18:24 -04:00
return this . createSessionToken ( ) ;
}
RestWrite . prototype . createSessionToken = function ( ) {
2016-09-09 15:18:37 -04:00
// cloud installationId from Cloud Code,
// never create session tokens from there.
if ( this . auth . installationId && this . auth . installationId === 'cloud' ) {
return ;
}
2016-04-20 11:57:38 -04:00
2018-02-19 11:15:54 -05:00
const {
sessionData ,
createSession ,
} = Auth . createSession ( this . config , {
userId : this . objectId ( ) ,
2016-04-20 11:57:38 -04:00
createdWith : {
2017-09-11 09:52:18 -04:00
'action' : this . storage [ 'authProvider' ] ? 'login' : 'signup' ,
2016-04-20 11:57:38 -04:00
'authProvider' : this . storage [ 'authProvider' ] || 'password'
} ,
installationId : this . auth . installationId ,
2018-02-19 11:15:54 -05:00
} ) ;
2016-04-20 11:57:38 -04:00
if ( this . response && this . response . response ) {
2018-02-19 11:15:54 -05:00
this . response . response . sessionToken = sessionData . sessionToken ;
2016-04-20 11:57:38 -04:00
}
2017-09-11 09:52:18 -04:00
2018-02-19 11:15:54 -05:00
return createSession ( ) ;
2017-11-11 09:41:23 -05:00
}
RestWrite . prototype . destroyDuplicatedSessions = function ( ) {
// Only for _Session, and at creation time
if ( this . className != '_Session' || this . query ) {
return ;
}
2017-09-11 09:52:18 -04:00
// Destroy the sessions in 'Background'
2017-11-11 09:41:23 -05:00
const {
user ,
installationId ,
sessionToken ,
} = this . data ;
if ( ! user || ! installationId ) {
return ;
}
if ( ! user . objectId ) {
return ;
}
2017-09-11 09:52:18 -04:00
this . config . database . destroy ( '_Session' , {
2017-11-11 09:41:23 -05:00
user ,
installationId ,
sessionToken : { '$ne' : sessionToken } ,
2017-09-11 09:52:18 -04:00
} ) ;
2016-04-20 11:57:38 -04:00
}
2016-01-28 10:58:12 -08:00
// Handles any followup logic
RestWrite . prototype . handleFollowup = function ( ) {
2016-04-22 15:21:50 -07:00
if ( this . storage && this . storage [ 'clearSessions' ] && this . config . revokeSessionOnPasswordReset ) {
2016-01-28 10:58:12 -08:00
var sessionQuery = {
user : {
2016-11-24 15:47:41 -05:00
_ _type : 'Pointer' ,
className : '_User' ,
objectId : this . objectId ( )
}
2016-01-28 10:58:12 -08:00
} ;
delete this . storage [ 'clearSessions' ] ;
2016-07-13 07:18:24 -04:00
return this . config . database . destroy ( '_Session' , sessionQuery )
2017-06-20 09:15:26 -07:00
. then ( this . handleFollowup . bind ( this ) ) ;
2016-07-13 07:18:24 -04:00
}
2016-08-17 15:26:42 +02:00
2016-07-13 07:18:24 -04:00
if ( this . storage && this . storage [ 'generateNewSession' ] ) {
delete this . storage [ 'generateNewSession' ] ;
return this . createSessionToken ( )
2017-06-20 09:15:26 -07:00
. then ( this . handleFollowup . bind ( this ) ) ;
2016-01-28 10:58:12 -08:00
}
2016-03-11 23:03:47 -05:00
2016-02-27 15:24:45 -05:00
if ( this . storage && this . storage [ 'sendVerificationEmail' ] ) {
delete this . storage [ 'sendVerificationEmail' ] ;
// Fire and forget!
this . config . userController . sendVerificationEmail ( this . data ) ;
2016-07-13 07:18:24 -04:00
return this . handleFollowup . bind ( this ) ;
2016-02-27 15:24:45 -05:00
}
2016-01-28 10:58:12 -08:00
} ;
// Handles the _Session class specialness.
2017-09-18 14:53:11 -04:00
// Does nothing if this isn't an _Session object.
2016-01-28 10:58:12 -08:00
RestWrite . prototype . handleSession = function ( ) {
if ( this . response || this . className !== '_Session' ) {
return ;
}
if ( ! this . auth . user && ! this . auth . isMaster ) {
throw new Parse . Error ( Parse . Error . INVALID _SESSION _TOKEN ,
2017-06-20 09:15:26 -07:00
'Session token required.' ) ;
2016-01-28 10:58:12 -08:00
}
// TODO: Verify proper error to throw
if ( this . data . ACL ) {
throw new Parse . Error ( Parse . Error . INVALID _KEY _NAME , 'Cannot set ' +
'ACL on a Session.' ) ;
}
2017-09-18 14:53:11 -04:00
if ( this . query ) {
if ( this . data . user && ! this . auth . isMaster && this . data . user . objectId != this . auth . user . id ) {
throw new Parse . Error ( Parse . Error . INVALID _KEY _NAME ) ;
} else if ( this . data . installationId ) {
throw new Parse . Error ( Parse . Error . INVALID _KEY _NAME ) ;
} else if ( this . data . sessionToken ) {
throw new Parse . Error ( Parse . Error . INVALID _KEY _NAME ) ;
}
}
2016-01-28 10:58:12 -08:00
if ( ! this . query && ! this . auth . isMaster ) {
2018-02-19 11:15:54 -05:00
const additionalSessionData = { } ;
2016-01-28 10:58:12 -08:00
for ( var key in this . data ) {
2017-09-18 14:53:11 -04:00
if ( key === 'objectId' || key === 'user' ) {
2016-01-28 10:58:12 -08:00
continue ;
}
2018-02-19 11:15:54 -05:00
additionalSessionData [ key ] = this . data [ key ] ;
2016-01-28 10:58:12 -08:00
}
2018-02-19 11:15:54 -05:00
const { sessionData , createSession } = Auth . createSession ( this . config , {
userId : this . auth . user . id ,
createdWith : {
action : 'create' ,
} ,
additionalSessionData
} ) ;
return createSession ( ) . then ( ( results ) => {
2016-01-28 10:58:12 -08:00
if ( ! results . response ) {
throw new Parse . Error ( Parse . Error . INTERNAL _SERVER _ERROR ,
2017-06-20 09:15:26 -07:00
'Error creating session.' ) ;
2016-01-28 10:58:12 -08:00
}
sessionData [ 'objectId' ] = results . response [ 'objectId' ] ;
this . response = {
status : 201 ,
location : results . location ,
response : sessionData
} ;
} ) ;
}
} ;
// Handles the _Installation class specialness.
// Does nothing if this isn't an installation object.
// If an installation is found, this can mutate this.query and turn a create
// into an update.
// Returns a promise for when we're done if it can't finish this tick.
RestWrite . prototype . handleInstallation = function ( ) {
if ( this . response || this . className !== '_Installation' ) {
return ;
}
2016-09-24 13:42:36 -04:00
if ( ! this . query && ! this . data . deviceToken && ! this . data . installationId && ! this . auth . installationId ) {
2016-01-28 10:58:12 -08:00
throw new Parse . Error ( 135 ,
2017-06-20 09:15:26 -07:00
'at least one ID field (deviceToken, installationId) ' +
2016-01-28 10:58:12 -08:00
'must be specified in this operation' ) ;
}
// If the device token is 64 characters long, we assume it is for iOS
// and lowercase it.
if ( this . data . deviceToken && this . data . deviceToken . length == 64 ) {
this . data . deviceToken = this . data . deviceToken . toLowerCase ( ) ;
}
// We lowercase the installationId if present
if ( this . data . installationId ) {
this . data . installationId = this . data . installationId . toLowerCase ( ) ;
}
2016-10-19 15:06:19 -04:00
let installationId = this . data . installationId ;
// If data.installationId is not set and we're not master, we can lookup in auth
if ( ! installationId && ! this . auth . isMaster ) {
installationId = this . auth . installationId ;
}
2016-09-24 13:42:36 -04:00
if ( installationId ) {
installationId = installationId . toLowerCase ( ) ;
}
2016-11-28 13:11:54 -05:00
// Updating _Installation but not updating anything critical
2016-12-01 10:24:46 -08:00
if ( this . query && ! this . data . deviceToken
2016-11-28 13:11:54 -05:00
&& ! installationId && ! this . data . deviceType ) {
return ;
}
2016-01-28 10:58:12 -08:00
var promise = Promise . resolve ( ) ;
2016-02-26 00:55:36 -05:00
var idMatch ; // Will be a match on either objectId or installationId
2016-07-19 02:12:29 -04:00
var objectIdMatch ;
var installationIdMatch ;
2016-02-26 00:55:36 -05:00
var deviceTokenMatches = [ ] ;
2016-07-19 02:12:29 -04:00
// Instead of issuing 3 reads, let's do it with one OR.
2016-12-07 15:17:05 -08:00
const orQueries = [ ] ;
2016-01-28 10:58:12 -08:00
if ( this . query && this . query . objectId ) {
2016-07-19 02:12:29 -04:00
orQueries . push ( {
2016-11-24 15:47:41 -05:00
objectId : this . query . objectId
2016-07-19 02:12:29 -04:00
} ) ;
}
2016-09-24 13:42:36 -04:00
if ( installationId ) {
2016-07-19 02:12:29 -04:00
orQueries . push ( {
2016-09-24 13:42:36 -04:00
'installationId' : installationId
2016-07-19 02:12:29 -04:00
} ) ;
}
if ( this . data . deviceToken ) {
orQueries . push ( { 'deviceToken' : this . data . deviceToken } ) ;
}
if ( orQueries . length == 0 ) {
return ;
}
2016-11-24 15:47:41 -05:00
promise = promise . then ( ( ) => {
2016-07-19 02:12:29 -04:00
return this . config . database . find ( '_Installation' , {
2016-11-24 15:47:41 -05:00
'$or' : orQueries
2016-07-19 02:12:29 -04:00
} , { } ) ;
2016-11-24 15:47:41 -05:00
} ) . then ( ( results ) => {
results . forEach ( ( result ) => {
2016-07-19 02:12:29 -04:00
if ( this . query && this . query . objectId && result . objectId == this . query . objectId ) {
objectIdMatch = result ;
}
2016-09-24 13:42:36 -04:00
if ( result . installationId == installationId ) {
2016-07-19 02:12:29 -04:00
installationIdMatch = result ;
}
if ( result . deviceToken == this . data . deviceToken ) {
deviceTokenMatches . push ( result ) ;
}
} ) ;
// Sanity checks when running a query
if ( this . query && this . query . objectId ) {
if ( ! objectIdMatch ) {
throw new Parse . Error ( Parse . Error . OBJECT _NOT _FOUND ,
2017-06-20 09:15:26 -07:00
'Object not found for update.' ) ;
2016-07-19 02:12:29 -04:00
}
2016-10-19 19:54:19 -04:00
if ( this . data . installationId && objectIdMatch . installationId &&
this . data . installationId !== objectIdMatch . installationId ) {
2016-11-24 15:47:41 -05:00
throw new Parse . Error ( 136 ,
2017-06-20 09:15:26 -07:00
'installationId may not be changed in this ' +
2016-01-28 10:58:12 -08:00
'operation' ) ;
2016-11-24 15:47:41 -05:00
}
if ( this . data . deviceToken && objectIdMatch . deviceToken &&
2016-07-19 02:12:29 -04:00
this . data . deviceToken !== objectIdMatch . deviceToken &&
! this . data . installationId && ! objectIdMatch . installationId ) {
2016-11-24 15:47:41 -05:00
throw new Parse . Error ( 136 ,
2017-06-20 09:15:26 -07:00
'deviceToken may not be changed in this ' +
2016-01-28 10:58:12 -08:00
'operation' ) ;
2016-11-24 15:47:41 -05:00
}
if ( this . data . deviceType && this . data . deviceType &&
2016-07-19 02:12:29 -04:00
this . data . deviceType !== objectIdMatch . deviceType ) {
2016-11-24 15:47:41 -05:00
throw new Parse . Error ( 136 ,
2017-06-20 09:15:26 -07:00
'deviceType may not be changed in this ' +
2016-01-28 10:58:12 -08:00
'operation' ) ;
2016-11-24 15:47:41 -05:00
}
2016-01-28 10:58:12 -08:00
}
2016-07-19 02:12:29 -04:00
if ( this . query && this . query . objectId && objectIdMatch ) {
idMatch = objectIdMatch ;
2016-01-28 10:58:12 -08:00
}
2016-07-19 02:12:29 -04:00
2016-09-24 13:42:36 -04:00
if ( installationId && installationIdMatch ) {
2016-07-19 02:12:29 -04:00
idMatch = installationIdMatch ;
2016-01-28 10:58:12 -08:00
}
2016-09-24 13:42:36 -04:00
// need to specify deviceType only if it's new
if ( ! this . query && ! this . data . deviceType && ! idMatch ) {
throw new Parse . Error ( 135 ,
2017-06-20 09:15:26 -07:00
'deviceType must be specified in this operation' ) ;
2016-09-24 13:42:36 -04:00
}
2016-07-19 02:12:29 -04:00
} ) . then ( ( ) => {
2016-02-26 00:55:36 -05:00
if ( ! idMatch ) {
2016-01-28 10:58:12 -08:00
if ( ! deviceTokenMatches . length ) {
return ;
} else if ( deviceTokenMatches . length == 1 &&
2016-09-24 13:42:36 -04:00
( ! deviceTokenMatches [ 0 ] [ 'installationId' ] || ! installationId )
2016-01-28 10:58:12 -08:00
) {
// Single match on device token but none on installationId, and either
// the passed object or the match is missing an installationId, so we
// can just return the match.
return deviceTokenMatches [ 0 ] [ 'objectId' ] ;
} else if ( ! this . data . installationId ) {
throw new Parse . Error ( 132 ,
2017-06-20 09:15:26 -07:00
'Must specify installationId when deviceToken ' +
2016-01-28 10:58:12 -08:00
'matches multiple Installation objects' ) ;
} else {
// Multiple device token matches and we specified an installation ID,
// or a single match where both the passed and matching objects have
// an installation ID. Try cleaning out old installations that match
// the deviceToken, and return nil to signal that a new object should
// be created.
var delQuery = {
'deviceToken' : this . data . deviceToken ,
'installationId' : {
2016-09-24 13:42:36 -04:00
'$ne' : installationId
2016-01-28 10:58:12 -08:00
}
} ;
if ( this . data . appIdentifier ) {
delQuery [ 'appIdentifier' ] = this . data . appIdentifier ;
}
2017-05-09 14:10:38 +02:00
this . config . database . destroy ( '_Installation' , delQuery )
. catch ( err => {
if ( err . code == Parse . Error . OBJECT _NOT _FOUND ) {
// no deletions were made. Can be ignored.
return ;
}
// rethrow the error
throw err ;
} ) ;
2016-01-28 10:58:12 -08:00
return ;
}
} else {
if ( deviceTokenMatches . length == 1 &&
! deviceTokenMatches [ 0 ] [ 'installationId' ] ) {
// Exactly one device token match and it doesn't have an installation
// ID. This is the one case where we want to merge with the existing
// object.
2016-12-07 15:17:05 -08:00
const delQuery = { objectId : idMatch . objectId } ;
2016-01-28 10:58:12 -08:00
return this . config . database . destroy ( '_Installation' , delQuery )
. then ( ( ) => {
return deviceTokenMatches [ 0 ] [ 'objectId' ] ;
2017-05-09 14:10:38 +02:00
} )
. catch ( err => {
if ( err . code == Parse . Error . OBJECT _NOT _FOUND ) {
// no deletions were made. Can be ignored
return ;
}
// rethrow the error
throw err ;
2016-01-28 10:58:12 -08:00
} ) ;
} else {
if ( this . data . deviceToken &&
2016-02-26 00:55:36 -05:00
idMatch . deviceToken != this . data . deviceToken ) {
2016-01-28 10:58:12 -08:00
// We're setting the device token on an existing installation, so
// we should try cleaning out old installations that match this
// device token.
2016-12-07 15:17:05 -08:00
const delQuery = {
2016-01-28 10:58:12 -08:00
'deviceToken' : this . data . deviceToken ,
2016-04-14 15:59:59 -04:00
} ;
// We have a unique install Id, use that to preserve
// the interesting installation
if ( this . data . installationId ) {
delQuery [ 'installationId' ] = {
2016-01-28 10:58:12 -08:00
'$ne' : this . data . installationId
}
2016-04-14 15:59:59 -04:00
} else if ( idMatch . objectId && this . data . objectId
&& idMatch . objectId == this . data . objectId ) {
// we passed an objectId, preserve that instalation
delQuery [ 'objectId' ] = {
'$ne' : idMatch . objectId
}
} else {
// What to do here? can't really clean up everything...
return idMatch . objectId ;
}
2016-01-28 10:58:12 -08:00
if ( this . data . appIdentifier ) {
delQuery [ 'appIdentifier' ] = this . data . appIdentifier ;
}
2017-05-09 14:10:38 +02:00
this . config . database . destroy ( '_Installation' , delQuery )
. catch ( err => {
if ( err . code == Parse . Error . OBJECT _NOT _FOUND ) {
// no deletions were made. Can be ignored.
return ;
}
// rethrow the error
throw err ;
} ) ;
2016-01-28 10:58:12 -08:00
}
// In non-merge scenarios, just return the installation match id
2016-02-26 00:55:36 -05:00
return idMatch . objectId ;
2016-01-28 10:58:12 -08:00
}
}
} ) . then ( ( objId ) => {
if ( objId ) {
this . query = { objectId : objId } ;
delete this . data . objectId ;
delete this . data . createdAt ;
}
// TODO: Validate ops (add/remove on channels, $inc on badge, etc.)
} ) ;
return promise ;
} ;
2016-02-24 15:15:57 -08:00
// If we short-circuted 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 ) ;
}
} ;
2016-01-28 10:58:12 -08:00
RestWrite . prototype . runDatabaseOperation = function ( ) {
if ( this . response ) {
return ;
}
2016-05-18 12:12:30 +12:00
if ( this . className === '_Role' ) {
this . config . cacheController . role . clear ( ) ;
}
2016-01-28 10:58:12 -08:00
if ( this . className === '_User' &&
this . query &&
2018-06-28 16:31:22 -04:00
this . auth . isUnauthenticated ( ) ) {
2016-06-11 00:43:02 -07:00
throw new Parse . Error ( Parse . Error . SESSION _MISSING , ` Cannot modify user ${ this . query . objectId } . ` ) ;
2016-01-28 10:58:12 -08:00
}
2016-03-11 23:03:47 -05:00
2016-02-19 14:07:12 -05:00
if ( this . className === '_Product' && this . data . download ) {
this . data . downloadName = this . data . download . name ;
}
2016-01-28 10:58:12 -08:00
// TODO: Add better detection for ACL, ensuring a user can't be locked from
// their own user record.
if ( this . data . ACL && this . data . ACL [ '*unresolved' ] ) {
throw new Parse . Error ( Parse . Error . INVALID _ACL , 'Invalid ACL.' ) ;
}
if ( this . query ) {
2016-04-08 13:19:41 -04:00
// Force the user to not lockout
// Matched with parse.com
2018-05-16 15:40:02 -04:00
if ( this . className === '_User' && this . data . ACL && this . auth . isMaster !== true ) {
2016-04-08 13:19:41 -04:00
this . data . ACL [ this . query . objectId ] = { read : true , write : true } ;
}
2016-11-21 21:16:38 +05:30
// update password timestamp if user password is being changed
if ( this . className === '_User' && this . data . _hashed _password && this . config . passwordPolicy && this . config . passwordPolicy . maxPasswordAge ) {
this . data . _password _changed _at = Parse . _encode ( new Date ( ) ) ;
}
2016-11-25 23:20:06 +09:00
// Ignore createdAt when update
delete this . data . createdAt ;
2016-11-29 22:31:52 +05:30
let defer = Promise . resolve ( ) ;
// if password history is enabled then save the current password to history
if ( this . className === '_User' && this . data . _hashed _password && this . config . passwordPolicy && this . config . passwordPolicy . maxPasswordHistory ) {
defer = this . config . database . find ( '_User' , { objectId : this . objectId ( ) } , { keys : [ "_password_history" , "_hashed_password" ] } ) . then ( results => {
if ( results . length != 1 ) {
throw undefined ;
}
const user = results [ 0 ] ;
let oldPasswords = [ ] ;
if ( user . _password _history ) {
oldPasswords = _ . take ( user . _password _history , this . config . passwordPolicy . maxPasswordHistory ) ;
}
//n-1 passwords go into history including last password
while ( oldPasswords . length > this . config . passwordPolicy . maxPasswordHistory - 2 ) {
oldPasswords . shift ( ) ;
}
oldPasswords . push ( user . password ) ;
this . data . _password _history = oldPasswords ;
} ) ;
}
return defer . then ( ( ) => {
// Run an update
return this . config . database . update ( this . className , this . query , this . data , this . runOptions )
2017-06-20 09:15:26 -07:00
. then ( response => {
response . updatedAt = this . updatedAt ;
this . _updateResponseWithData ( response , this . data ) ;
this . response = { response } ;
} ) ;
2016-05-23 17:13:32 -07:00
} ) ;
2016-01-28 10:58:12 -08:00
} else {
2016-11-21 21:16:38 +05:30
// Set the default ACL and password timestamp for the new _User
2016-04-08 13:19:41 -04:00
if ( this . className === '_User' ) {
var ACL = this . data . ACL ;
// default public r/w ACL
if ( ! ACL ) {
ACL = { } ;
ACL [ '*' ] = { read : true , write : false } ;
}
// make sure the user is not locked down
2016-02-15 19:40:22 -05:00
ACL [ this . data . objectId ] = { read : true , write : true } ;
this . data . ACL = ACL ;
2016-11-21 21:16:38 +05:30
// password timestamp to be used when password expiry policy is enforced
if ( this . config . passwordPolicy && this . config . passwordPolicy . maxPasswordAge ) {
this . data . _password _changed _at = Parse . _encode ( new Date ( ) ) ;
}
2016-03-11 23:03:47 -05:00
}
2016-04-02 11:36:47 -04:00
2016-01-28 10:58:12 -08:00
// Run a create
2016-02-11 22:16:07 -05:00
return this . config . database . create ( this . className , this . data , this . runOptions )
2017-06-20 09:15:26 -07:00
. catch ( error => {
if ( this . className !== '_User' || error . code !== Parse . Error . DUPLICATE _VALUE ) {
throw error ;
2016-06-10 20:27:21 -07:00
}
2017-09-05 17:51:11 -04:00
// Quick check, if we were able to infer the duplicated field name
if ( error && error . userInfo && error . userInfo . duplicated _field === 'username' ) {
throw new Parse . Error ( Parse . Error . USERNAME _TAKEN , 'Account already exists for this username.' ) ;
}
if ( error && error . userInfo && error . userInfo . duplicated _field === 'email' ) {
throw new Parse . Error ( Parse . Error . EMAIL _TAKEN , 'Account already exists for this email address.' ) ;
}
2017-06-20 09:15:26 -07:00
// If this was a failed user creation due to username or email already taken, we need to
// check whether it was username or email and return the appropriate error.
2017-09-05 17:51:11 -04:00
// Fallback to the original method
2017-06-20 09:15:26 -07:00
// TODO: See if we can later do this without additional queries by using named indexes.
2016-06-10 20:27:21 -07:00
return this . config . database . find (
this . className ,
2017-06-20 09:15:26 -07:00
{ username : this . data . username , objectId : { '$ne' : this . objectId ( ) } } ,
2016-06-10 20:27:21 -07:00
{ limit : 1 }
2017-06-20 09:15:26 -07:00
)
. then ( results => {
if ( results . length > 0 ) {
throw new Parse . Error ( Parse . Error . USERNAME _TAKEN , 'Account already exists for this username.' ) ;
}
return this . config . database . find (
this . className ,
{ email : this . data . email , objectId : { '$ne' : this . objectId ( ) } } ,
{ limit : 1 }
) ;
} )
. then ( results => {
if ( results . length > 0 ) {
throw new Parse . Error ( Parse . Error . EMAIL _TAKEN , 'Account already exists for this email address.' ) ;
}
throw new Parse . Error ( Parse . Error . DUPLICATE _VALUE , 'A duplicate value for a field with unique values was provided' ) ;
} ) ;
2016-06-10 20:27:21 -07:00
} )
2017-06-20 09:15:26 -07:00
. then ( response => {
response . objectId = this . data . objectId ;
response . createdAt = this . data . createdAt ;
if ( this . responseShouldHaveUsername ) {
response . username = this . data . username ;
2016-06-10 20:27:21 -07:00
}
2017-06-20 09:15:26 -07:00
this . _updateResponseWithData ( response , this . data ) ;
this . response = {
status : 201 ,
response ,
location : this . location ( )
} ;
2016-06-10 20:27:21 -07:00
} ) ;
2016-01-28 10:58:12 -08:00
}
} ;
// Returns nothing - doesn't wait for the trigger.
RestWrite . prototype . runAfterTrigger = function ( ) {
2016-03-02 18:23:00 -08:00
if ( ! this . response || ! this . response . response ) {
return ;
}
// Avoid doing any setup for triggers if there is no 'afterSave' trigger for this class.
2016-12-07 15:17:05 -08:00
const hasAfterSaveHook = triggers . triggerExists ( this . className , triggers . Types . afterSave , this . config . applicationId ) ;
const hasLiveQuery = this . config . liveQueryController . hasLiveQuery ( this . className ) ;
2016-03-10 14:27:00 -08:00
if ( ! hasAfterSaveHook && ! hasLiveQuery ) {
2016-03-02 18:23:00 -08:00
return Promise . resolve ( ) ;
}
2016-01-28 10:58:12 -08:00
var extraData = { className : this . className } ;
if ( this . query && this . query . objectId ) {
extraData . objectId = this . query . objectId ;
}
// Build the original object, we only do this for a update write.
2016-03-02 18:20:02 -08:00
let originalObject ;
2016-01-28 10:58:12 -08:00
if ( this . query && this . query . objectId ) {
originalObject = triggers . inflate ( extraData , this . originalData ) ;
}
2016-03-02 18:20:02 -08:00
// Build the inflated object, different from beforeSave, originalData is not empty
// since developers can change data in the beforeSave.
2017-06-14 20:51:41 +02:00
const updatedObject = this . buildUpdatedObject ( extraData ) ;
2016-03-02 18:20:02 -08:00
updatedObject . _handleSaveResponse ( this . response . response , this . response . status || 200 ) ;
2016-03-10 14:27:00 -08:00
// Notifiy LiveQueryServer if possible
this . config . liveQueryController . onAfterSave ( updatedObject . className , updatedObject , originalObject ) ;
// Run afterSave trigger
2017-10-26 11:28:13 -07:00
return triggers . maybeRunTrigger ( triggers . Types . afterSave , this . auth , updatedObject , originalObject , this . config )
. catch ( function ( err ) {
logger . warn ( 'afterSave caught an error' , err ) ;
} )
2016-01-28 10:58:12 -08:00
} ;
// A helper to figure out what location this operation happens at.
RestWrite . prototype . location = function ( ) {
var middle = ( this . className === '_User' ? '/users/' :
2017-06-20 09:15:26 -07:00
'/classes/' + this . className + '/' ) ;
2016-01-28 10:58:12 -08:00
return this . config . mount + middle + this . data . objectId ;
} ;
// A helper to get the object id for this operation.
// Because it could be either on the query or on the data
RestWrite . prototype . objectId = function ( ) {
return this . data . objectId || this . query . objectId ;
} ;
2016-03-11 23:03:47 -05:00
// Returns a copy of the data and delete bad keys (_auth_data, _hashed_password...)
RestWrite . prototype . sanitizedData = function ( ) {
2016-12-07 15:17:05 -08:00
const data = Object . keys ( this . data ) . reduce ( ( data , key ) => {
2016-03-11 23:03:47 -05:00
// Regexp comes from Parse.Object.prototype.validate
if ( ! ( /^[A-Za-z][0-9A-Za-z_]*$/ ) . test ( key ) ) {
delete data [ key ] ;
}
return data ;
} , deepcopy ( this . data ) ) ;
return Parse . _decode ( undefined , data ) ;
}
2017-06-14 20:51:41 +02:00
// Returns an updated copy of the object
RestWrite . prototype . buildUpdatedObject = function ( extraData ) {
const updatedObject = triggers . inflate ( extraData , this . originalData ) ;
Object . keys ( this . data ) . reduce ( function ( data , key ) {
if ( key . indexOf ( "." ) > 0 ) {
// subdocument key with dot notation ('x.y':v => 'x':{'y':v})
const splittedKey = key . split ( "." ) ;
const parentProp = splittedKey [ 0 ] ;
let parentVal = updatedObject . get ( parentProp ) ;
if ( typeof parentVal !== 'object' ) {
parentVal = { } ;
}
parentVal [ splittedKey [ 1 ] ] = data [ key ] ;
updatedObject . set ( parentProp , parentVal ) ;
delete data [ key ] ;
}
return data ;
} , deepcopy ( this . data ) ) ;
updatedObject . set ( this . sanitizedData ( ) ) ;
return updatedObject ;
} ;
2016-03-25 16:11:27 -07:00
RestWrite . prototype . cleanUserAuthData = function ( ) {
if ( this . response && this . response . response && this . className === '_User' ) {
2016-12-07 15:17:05 -08:00
const user = this . response . response ;
2016-03-25 16:11:27 -07:00
if ( user . authData ) {
Object . keys ( user . authData ) . forEach ( ( provider ) => {
if ( user . authData [ provider ] === null ) {
delete user . authData [ provider ] ;
}
} ) ;
if ( Object . keys ( user . authData ) . length == 0 ) {
delete user . authData ;
}
}
}
} ;
2016-08-05 18:23:54 +05:30
RestWrite . prototype . _updateResponseWithData = function ( response , data ) {
if ( _ . isEmpty ( this . storage . fieldsChangedByTrigger ) ) {
return response ;
}
2016-12-07 15:17:05 -08:00
const clientSupportsDelete = ClientSDK . supportsForwardDelete ( this . clientSDK ) ;
2016-08-05 18:23:54 +05:30
this . storage . fieldsChangedByTrigger . forEach ( fieldName => {
2016-12-07 15:17:05 -08:00
const dataValue = data [ fieldName ] ;
2016-07-15 09:24:53 -04:00
2017-10-15 21:15:30 -07:00
if ( ! response . hasOwnProperty ( fieldName ) ) {
response [ fieldName ] = dataValue ;
}
2016-08-17 15:26:42 +02:00
2016-07-15 09:24:53 -04:00
// Strips operations from responses
if ( response [ fieldName ] && response [ fieldName ] . _ _op ) {
delete response [ fieldName ] ;
if ( clientSupportsDelete && dataValue . _ _op == 'Delete' ) {
response [ fieldName ] = dataValue ;
}
}
} ) ;
return response ;
}
2016-02-27 14:46:29 -05:00
export default RestWrite ;
2016-01-28 10:58:12 -08:00
module . exports = RestWrite ;