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' ) ;
var 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' ;
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 ) {
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 ( ) ;
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 ) => {
roles . push ( this . auth . user . id ) ;
this . runOptions . acl = this . runOptions . acl . concat ( roles ) ;
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 ,
'This user is not allowed to access ' +
'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 ;
2016-12-07 15:17:05 -08:00
const updatedObject = triggers . inflate ( extraData , this . originalData ) ;
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 ) ;
}
2016-03-11 23:03:47 -05:00
updatedObject . set ( this . sanitizedData ( ) ) ;
2016-01-28 10:58:12 -08:00
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 ) {
this . data . objectId = cryptoUtils . newObjectId ( ) ;
}
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 ,
'bad or missing username' ) ;
}
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 ,
'password is required' ) ;
}
}
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 ,
'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 ,
'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 (
2016-01-28 10:58:12 -08:00
this . className ,
2016-03-10 18:59:19 -05:00
{ '$or' : query } , { } )
}
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 ) => {
2016-03-10 18:59:19 -05:00
results = r ;
if ( results . length > 1 ) {
// More than 1 user with the passed id's
throw new Parse . Error ( Parse . Error . ACCOUNT _ALREADY _LINKED ,
2016-01-28 10:58:12 -08: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 ) {
if ( ! this . query ) {
// Login with auth data
delete results [ 0 ] . password ;
2016-12-07 15:17:05 -08:00
const userResult = results [ 0 ] ;
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
2016-07-09 00:49:46 -04:00
// Determine if authData was updated
2016-12-07 15:17:05 -08:00
const mutatedAuthData = { } ;
2016-07-09 00:49:46 -04:00
Object . keys ( authData ) . forEach ( ( provider ) => {
2016-12-07 15:17:05 -08:00
const providerData = authData [ provider ] ;
const userAuthData = userResult . authData [ provider ] ;
2016-07-09 00:49:46 -04:00
if ( ! _ . isEqual ( providerData , userAuthData ) ) {
mutatedAuthData [ provider ] = providerData ;
}
} ) ;
2016-03-22 21:17:53 -04:00
this . response = {
2016-07-09 00:49:46 -04:00
response : userResult ,
2016-03-22 21:17:53 -04:00
location : this . location ( )
} ;
2016-07-09 00:49:46 -04:00
2017-05-07 12:55:30 -04:00
// If we didn't change the auth data, just keep going
if ( Object . keys ( mutatedAuthData ) . length === 0 ) {
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 ( ( ) => {
2016-07-09 00:49:46 -04:00
// Assign the new authData in the response
2016-11-24 15:47:41 -05:00
Object . keys ( mutatedAuthData ) . forEach ( ( provider ) => {
2016-07-09 00:49:46 -04:00
this . response . response . authData [ provider ] = mutatedAuthData [ provider ] ;
} ) ;
// Run the DB update directly, as 'master'
// Just update the authData part
return this . config . database . update ( this . className , { objectId : this . data . objectId } , { authData : mutatedAuthData } , { } ) ;
2017-05-07 12:55:30 -04:00
} ) ;
2016-03-22 21:17:53 -04:00
} else if ( this . query && this . query . objectId ) {
// Trying to update auth data but users
// are different
if ( results [ 0 ] . objectId !== this . query . objectId ) {
throw new Parse . Error ( Parse . Error . ACCOUNT _ALREADY _LINKED ,
'this auth is already used' ) ;
}
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
}
2016-05-22 09:59:36 -07:00
if ( this . query ) {
// 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
}
if ( this . query && ! this . auth . isMaster ) {
2016-01-28 10:58:12 -08:00
this . storage [ 'clearSessions' ] = true ;
2016-07-13 07:18:24 -04:00
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
}
2016-11-29 22:31:52 +05:30
// We updated the email, send a new validation
this . storage [ 'sendVerificationEmail' ] = true ;
this . config . userController . setEmailVerifyToken ( this . data ) ;
} ) ;
} ;
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 ;
}
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
var token = 'r:' + cryptoUtils . newToken ( ) ;
var expiresAt = this . config . generateSessionExpiresAt ( ) ;
var sessionData = {
sessionToken : token ,
user : {
_ _type : 'Pointer' ,
className : '_User' ,
objectId : this . objectId ( )
} ,
createdWith : {
'action' : 'signup' ,
'authProvider' : this . storage [ 'authProvider' ] || 'password'
} ,
restricted : false ,
installationId : this . auth . installationId ,
expiresAt : Parse . _encode ( expiresAt )
} ;
if ( this . response && this . response . response ) {
this . response . response . sessionToken = token ;
}
2016-05-22 09:59:36 -07:00
var create = new RestWrite ( this . config , Auth . master ( this . config ) , '_Session' , null , sessionData ) ;
2016-04-20 11:57:38 -04:00
return create . execute ( ) ;
}
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 )
. then ( this . handleFollowup . bind ( this ) ) ;
}
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 ( )
2016-02-02 09:59:20 -05: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.
// Does nothing if this isn't an installation object.
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 ,
'Session token required.' ) ;
}
// 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.' ) ;
}
if ( ! this . query && ! this . auth . isMaster ) {
2016-02-12 02:02:55 +01:00
var token = 'r:' + cryptoUtils . newToken ( ) ;
2016-04-02 11:36:47 -04:00
var expiresAt = this . config . generateSessionExpiresAt ( ) ;
2016-01-28 10:58:12 -08:00
var sessionData = {
sessionToken : token ,
user : {
_ _type : 'Pointer' ,
className : '_User' ,
objectId : this . auth . user . id
} ,
createdWith : {
'action' : 'create'
} ,
restricted : true ,
2016-02-02 22:50:43 -05:00
expiresAt : Parse . _encode ( expiresAt )
2016-01-28 10:58:12 -08:00
} ;
for ( var key in this . data ) {
if ( key == 'objectId' ) {
continue ;
}
sessionData [ key ] = this . data [ key ] ;
}
2016-05-22 09:59:36 -07:00
var create = new RestWrite ( this . config , Auth . master ( this . config ) , '_Session' , null , sessionData ) ;
2016-01-28 10:58:12 -08:00
return create . execute ( ) . then ( ( results ) => {
if ( ! results . response ) {
throw new Parse . Error ( Parse . Error . INTERNAL _SERVER _ERROR ,
'Error creating session.' ) ;
}
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 ,
'at least one ID field (deviceToken, installationId) ' +
'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 ,
2016-01-28 10:58:12 -08: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 ,
2016-01-28 10:58:12 -08:00
'installationId may not be changed in this ' +
'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 ,
2016-01-28 10:58:12 -08:00
'deviceToken may not be changed in this ' +
'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 ,
2016-01-28 10:58:12 -08:00
'deviceType may not be changed in this ' +
'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 ,
'deviceType must be specified in this operation' ) ;
}
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 ,
'Must specify installationId when deviceToken ' +
'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 &&
! this . auth . couldUpdateUserId ( this . query . objectId ) ) {
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
if ( this . className === '_User' && this . data . ACL ) {
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 )
. 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 )
2016-06-10 20:27:21 -07:00
. catch ( error => {
if ( this . className !== '_User' || error . code !== Parse . Error . DUPLICATE _VALUE ) {
throw error ;
}
// 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.
// TODO: See if we can later do this without additional queries by using named indexes.
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 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-05-23 17:13:32 -07:00
. then ( response => {
response . objectId = this . data . objectId ;
response . createdAt = this . data . createdAt ;
2016-08-17 15:26:42 +02:00
2016-06-27 06:19:37 +03:00
if ( this . responseShouldHaveUsername ) {
response . username = this . data . username ;
}
2016-08-05 18:23:54 +05:30
this . _updateResponseWithData ( response , this . data ) ;
2016-05-23 17:13:32 -07:00
this . response = {
status : 201 ,
response ,
location : this . location ( )
} ;
} ) ;
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.
2016-12-07 15:17:05 -08:00
const updatedObject = triggers . inflate ( extraData , this . originalData ) ;
2016-03-11 23:03:47 -05:00
updatedObject . set ( this . sanitizedData ( ) ) ;
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
2016-08-17 15:26:42 +02:00
return triggers . maybeRunTrigger ( triggers . Types . afterSave , this . auth , updatedObject , originalObject , this . config ) ;
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/' :
'/classes/' + this . className + '/' ) ;
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 ) ;
}
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 ] ;
const responseValue = response [ fieldName ] ;
2016-07-15 09:24:53 -04:00
response [ fieldName ] = responseValue || 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 ;