2017-04-01 23:55:55 -04:00
|
|
|
/*
|
|
|
|
* Contains program Server class
|
|
|
|
*
|
2017-06-08 14:46:51 -04:00
|
|
|
* Copyright (C) 2017 R-T Specialty, LLC.
|
2017-04-01 23:55:55 -04:00
|
|
|
*
|
|
|
|
* This file is part of the Liza Data Collection Framework.
|
|
|
|
*
|
|
|
|
* liza is free software: you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU Affero General Public License as
|
|
|
|
* published by the Free Software Foundation, either version 3 of the
|
|
|
|
* License, or (at your option) any later version.
|
|
|
|
*
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*
|
|
|
|
* @todo like Client and Ui, this mammoth did not evolve well and has too
|
|
|
|
* many responsibilities; refactor
|
|
|
|
*/
|
|
|
|
|
|
|
|
const { Class } = require( 'easejs' );
|
|
|
|
const { EventEmitter } = require( 'events' );
|
|
|
|
|
|
|
|
const fs = require( 'fs' );
|
|
|
|
const util = require( 'util' );
|
|
|
|
|
|
|
|
const {
|
|
|
|
bucket: {
|
|
|
|
bucket_filter,
|
|
|
|
QuoteDataBucket,
|
|
|
|
BucketSiblingDescriptor,
|
|
|
|
|
|
|
|
diff: {
|
|
|
|
StdBucketDiffContext,
|
|
|
|
GroupedBucketDiffContext,
|
|
|
|
StdBucketDiffResult,
|
|
|
|
GroupedBucketDiffResult,
|
|
|
|
StdBucketDiff,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|
|
|
|
field: {
|
|
|
|
FieldClassMatcher,
|
|
|
|
},
|
|
|
|
|
|
|
|
server: {
|
|
|
|
encsvc: {
|
|
|
|
QuoteDataBucketCipher,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|
|
|
|
util: {
|
|
|
|
ShallowArrayDiff,
|
|
|
|
},
|
|
|
|
} = require( '..' );
|
|
|
|
|
|
|
|
|
|
|
|
module.exports = Class( 'Server' )
|
|
|
|
.extend( EventEmitter,
|
|
|
|
{
|
|
|
|
'private response': null,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Dao
|
|
|
|
* @type {MongoServerDao}
|
|
|
|
*/
|
|
|
|
'private dao': null,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Stores references to program objects
|
|
|
|
* @type {Object}
|
|
|
|
*/
|
|
|
|
'private programs': {},
|
|
|
|
|
|
|
|
'private quoteFillHooks': [],
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Logger to use
|
|
|
|
* @type {PriorityLog}
|
|
|
|
*/
|
|
|
|
'private logger': null,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Default bucket data for various programs
|
|
|
|
* @type {Object}
|
|
|
|
*/
|
|
|
|
'private _defaultBuckets': {},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Encryption service client
|
|
|
|
* @type {EncryptionService}
|
|
|
|
*/
|
|
|
|
'private _encService': null,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Holds bucket ciphers for each program
|
|
|
|
* @type {Object.<string,QuoteDataBucketCipher>}
|
|
|
|
*/
|
|
|
|
'private _bucketCiphers': {},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Step and program cache
|
|
|
|
*
|
|
|
|
* @type {Store}
|
|
|
|
*/
|
|
|
|
'private _cache': null,
|
|
|
|
|
|
|
|
|
|
|
|
'public __construct': function( response, dao, logger, encsvc )
|
|
|
|
{
|
|
|
|
this.response = response;
|
|
|
|
this.dao = dao;
|
|
|
|
this.logger = logger;
|
|
|
|
this._encService = encsvc;
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
'public init': function( cache, rater )
|
|
|
|
{
|
|
|
|
this._cache = cache;
|
|
|
|
|
|
|
|
this._initDb();
|
|
|
|
this.reload( rater );
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
'public reload': function( rater )
|
|
|
|
{
|
|
|
|
var _self = this;
|
|
|
|
|
|
|
|
rater.init(
|
|
|
|
// log
|
|
|
|
function( msg )
|
|
|
|
{
|
|
|
|
_self.logger.log( _self.logger.PRIORITY_IMPORTANT, msg );
|
|
|
|
},
|
|
|
|
|
|
|
|
// error
|
|
|
|
function( msg, stack )
|
|
|
|
{
|
|
|
|
stack = stack || '<no stack trace>';
|
|
|
|
|
|
|
|
_self.logger.log( _self.logger.PRIORITY_ERROR,
|
|
|
|
"%s\n-!%s",
|
|
|
|
msg,
|
|
|
|
stack.replace( /\n/g, '\n-!' )
|
|
|
|
);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Initializes the database (attempt DAO connection)
|
|
|
|
*
|
|
|
|
* @return undefined
|
|
|
|
*/
|
|
|
|
'private _initDb': function()
|
|
|
|
{
|
|
|
|
var server = this;
|
|
|
|
|
|
|
|
// error listeners
|
|
|
|
this.dao.on( 'connectError', function( err )
|
|
|
|
{
|
|
|
|
server.logger.log( server.logger.PRIORITY_ERROR,
|
|
|
|
"Database connection failure: (%s) %s",
|
|
|
|
err.errno || '-',
|
|
|
|
err.message || ''
|
|
|
|
);
|
|
|
|
|
|
|
|
// attempt to reconnect every 5 seconds
|
|
|
|
setTimeout( function()
|
|
|
|
{
|
|
|
|
server.logger.log( server.logger.PRIORITY_DB,
|
|
|
|
"[Server] Attempting to reconnect to database..."
|
|
|
|
);
|
|
|
|
server.dao.connect();
|
|
|
|
}, 5000 );
|
|
|
|
}).on( 'saveQuoteError', function( err, quote )
|
|
|
|
{
|
|
|
|
server.logger.log( server.logger.PRIORITY_ERROR,
|
|
|
|
"Failed to save quote %d: %s",
|
|
|
|
quote.getId(),
|
|
|
|
err.message || ''
|
|
|
|
);
|
|
|
|
}).on( 'seqError', function( err )
|
|
|
|
{
|
|
|
|
server.logger.log( server.logger.PRIORITY_ERROR,
|
|
|
|
"Sequence error: %s",
|
|
|
|
err
|
|
|
|
);
|
|
|
|
}).on( 'seqInit', function( seq )
|
|
|
|
{
|
|
|
|
server.logger.log( server.logger.PRIORITY_DB,
|
|
|
|
"Initialized default sequence: %s",
|
|
|
|
seq
|
|
|
|
);
|
|
|
|
}).on( 'ready', function()
|
|
|
|
{
|
|
|
|
server.logger.log( server.logger.PRIORITY_DB,
|
|
|
|
"[Server] Connected to database; DAO ready"
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
server.logger.log( server.logger.PRIORITY_DB,
|
|
|
|
"[Server] Connecting to database..."
|
|
|
|
);
|
|
|
|
|
|
|
|
this.dao.init();
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
sendResponse: function( request, quote, data, action )
|
|
|
|
{
|
|
|
|
request.end( this.response.from( quote, data, action ) );
|
|
|
|
return this;
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
sendError: function( request, message, action, btn_caption )
|
|
|
|
{
|
|
|
|
request.end( this.response.error( message, action, btn_caption ) );
|
|
|
|
return this;
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Initializes a quote with any existing quote data
|
|
|
|
*
|
|
|
|
* @return Server self to allow for method chaining
|
|
|
|
*/
|
|
|
|
initQuote: function( quote, program, request, callback, error_callback )
|
|
|
|
{
|
|
|
|
var server = this,
|
|
|
|
quote_id = quote.getId(),
|
|
|
|
session = request.getSession(),
|
|
|
|
agent_id = session.agentId(),
|
|
|
|
agent_name = session.agentName();
|
|
|
|
|
|
|
|
// get the data for this quote
|
|
|
|
this.dao.pullQuote( quote_id, function( quote_data )
|
|
|
|
{
|
|
|
|
var new_quote = false;
|
|
|
|
if ( !( quote_data ) )
|
|
|
|
{
|
|
|
|
quote_data = {};
|
|
|
|
new_quote = true;
|
|
|
|
|
|
|
|
// ensure it's a valid quote id
|
|
|
|
server.dao.getMinQuoteId( function( min_id )
|
|
|
|
{
|
|
|
|
// don't allow before the min quote id
|
|
|
|
if ( quote_id < min_id )
|
|
|
|
{
|
|
|
|
error_callback.call( server );
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
server.dao.getMaxQuoteId( function( max_id )
|
|
|
|
{
|
|
|
|
if ( quote_id > max_id )
|
|
|
|
{
|
|
|
|
// we has a problem
|
|
|
|
error_callback.call( server );
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// we're good
|
|
|
|
init_finish( program );
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// quote is not new; just continue
|
|
|
|
server.getProgram( quote_data.programId )
|
|
|
|
.then( function( quote_program )
|
|
|
|
{
|
|
|
|
init_finish( quote_program );
|
|
|
|
} );
|
|
|
|
}
|
|
|
|
|
|
|
|
function init_finish( quote_program )
|
|
|
|
{
|
|
|
|
// fill in the quote data (with reasonable defaults if the quote
|
|
|
|
// does not yet exist); IMPORTANT: do not set pver to the
|
|
|
|
// current version here; the quote will be repaired if it is not
|
|
|
|
// set
|
|
|
|
quote
|
|
|
|
.setData(
|
|
|
|
server._getDefaultBucket( quote_program, quote_data )
|
|
|
|
)
|
|
|
|
.setQuickSaveData( quote_data.quicksave || {} )
|
|
|
|
.setAgentId( quote_data.agentId || agent_id )
|
|
|
|
.setAgentName( quote_data.agentName || agent_name )
|
|
|
|
.setStartDate(
|
|
|
|
quote_data.getStartDate
|
|
|
|
|| Math.round( new Date().getTime() / 1000 )
|
|
|
|
)
|
|
|
|
.setImported( quote_data.importedInd || false )
|
|
|
|
.setBound( quote_data.boundInd || false )
|
|
|
|
.needsImport( quote_data.importDirty || false )
|
|
|
|
.setCurrentStepId(
|
|
|
|
quote_data.currentStepId
|
|
|
|
|| quote_program.getFirstStepId()
|
|
|
|
)
|
|
|
|
.setTopVisitedStepId(
|
|
|
|
quote_data.topVisitedStepId
|
|
|
|
|| quote_program.getFirstStepId()
|
|
|
|
)
|
|
|
|
// it is important that we set this to top visited to
|
|
|
|
// ensure that (a) they cannot init a quote and skip the
|
|
|
|
// first step and (b) that older quotes without this field
|
|
|
|
// are properly initialized
|
|
|
|
.setTopSavedStepId(
|
|
|
|
quote_data.topSavedStepId
|
|
|
|
|| ( quote.getTopVisitedStepId() - 1 )
|
|
|
|
)
|
|
|
|
.setProgram( quote_program )
|
|
|
|
.setProgramVersion( quote_data.pver || '' )
|
|
|
|
.setExplicitLock(
|
|
|
|
( quote_data.explicitLock || '' ),
|
|
|
|
( quote_data.explicitLockStepId || 0 )
|
|
|
|
)
|
|
|
|
.setError( quote_data.error || '' )
|
|
|
|
.setCreditScoreRef( quote_data.creditScoreRef || 0 )
|
|
|
|
.setLastPremiumDate( quote_data.lastPremDate || 0 )
|
|
|
|
.setRatedDate( quote_data.initialRatedDate || 0 )
|
|
|
|
.on( 'stepChange', function( step_id )
|
|
|
|
{
|
|
|
|
// save the quote state (we don't care if it succeeds or
|
|
|
|
// fails because (a) failures will be automatically
|
|
|
|
// logged and (b) we may not be dealing with a request,
|
|
|
|
// so we may not be able to send a response to the
|
|
|
|
// client)
|
|
|
|
server.dao.saveQuoteState( quote );
|
|
|
|
});
|
|
|
|
|
|
|
|
// if no data was returned, then the quote doesn't exist in the
|
|
|
|
// database
|
|
|
|
if ( new_quote )
|
|
|
|
{
|
|
|
|
// initialize it
|
|
|
|
server.dao.saveQuote( quote, null, null, {
|
|
|
|
agentId: agent_id,
|
|
|
|
agentName: agent_name,
|
|
|
|
agentEntityId: session.agentEntityId(),
|
|
|
|
startDate: quote.getStartDate(),
|
|
|
|
programId: quote.getProgramId(),
|
|
|
|
initialRatedDate: 0,
|
|
|
|
importedInd: ( quote.isImported() ) ? 1 : 0,
|
|
|
|
boundInd: ( quote.isBound() ) ? 1 : 0,
|
|
|
|
importDirty: 0,
|
|
|
|
syncInd: 0,
|
|
|
|
boundInd: 0,
|
|
|
|
notifyInd: 0,
|
|
|
|
syncDate: 0,
|
|
|
|
lastPremDate: 0,
|
|
|
|
internal: ( session.isInternal() ) ? 1: 0,
|
|
|
|
pver: program.version,
|
|
|
|
|
|
|
|
explicitLock: quote.getExplicitLockReason(),
|
|
|
|
explicitLockStepId: quote.getExplicitLockStep(),
|
|
|
|
} );
|
|
|
|
}
|
|
|
|
|
|
|
|
callback.call( server );
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return this;
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
'private _checkQuotePver': function( quote, program, callback )
|
|
|
|
{
|
|
|
|
// note that if program.version is not set, then something is likely
|
|
|
|
// wrong with the build that generates it: always clean in this case to
|
|
|
|
// be safe
|
|
|
|
if ( program.version
|
|
|
|
&& ( quote.getProgramVersion() === program.version )
|
|
|
|
)
|
|
|
|
{
|
|
|
|
callback( false, false );
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
var _self = this;
|
|
|
|
|
|
|
|
this.logger.log( this.logger.PRIORITY_INFO,
|
|
|
|
'Quote %s program version change (%s -> %s); will be scanned.',
|
|
|
|
quote.getId(),
|
|
|
|
quote.getProgramVersion(),
|
|
|
|
program.version
|
|
|
|
);
|
|
|
|
|
|
|
|
// TODO: thread
|
|
|
|
// service any other requests first, and then proceed to cleaning
|
|
|
|
process.nextTick( function()
|
|
|
|
{
|
|
|
|
var nwait = 0,
|
|
|
|
msg = [],
|
|
|
|
handled = false;
|
|
|
|
|
|
|
|
// by default, clear is undefined; event handlers should call the
|
|
|
|
// appropriate function to state whether the quote has been properly
|
|
|
|
// upgraded; if no handlers indicate success, or if any indiciate
|
|
|
|
// failure, then disallow servicing the quote
|
|
|
|
var clear = undefined,
|
|
|
|
event = {
|
|
|
|
good: function()
|
|
|
|
{
|
|
|
|
// if undefined, then we're good, otherwise keep the
|
|
|
|
// existing value (we cannot override bad)
|
|
|
|
clear = ( clear === undefined ) ? true : clear;
|
|
|
|
},
|
|
|
|
|
|
|
|
bad: function( s )
|
|
|
|
{
|
|
|
|
// bad trumps all
|
|
|
|
clear = false;
|
|
|
|
msg.push( s );
|
|
|
|
},
|
|
|
|
|
|
|
|
wait: function()
|
|
|
|
{
|
|
|
|
nwait++;
|
|
|
|
return c;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// trigger the event and let someone (hopefully) take care of this
|
|
|
|
try
|
|
|
|
{
|
|
|
|
_self.emit( 'quotePverUpdate', quote, program, event );
|
|
|
|
}
|
|
|
|
catch ( e )
|
|
|
|
{
|
|
|
|
// ruh roh...
|
|
|
|
event.bad( e.message );
|
|
|
|
nwait = 0;
|
|
|
|
|
|
|
|
// this is an unhandled exception, as far as we're concerned;
|
|
|
|
// re-throw so that we have a stack trace, but do so after we're
|
|
|
|
// done processing
|
|
|
|
process.nextTick( function()
|
|
|
|
{
|
|
|
|
throw e;
|
|
|
|
} );
|
|
|
|
}
|
|
|
|
|
|
|
|
function c()
|
|
|
|
{
|
|
|
|
// do nothing until we're done waiting
|
|
|
|
if ( --nwait > 0 )
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( clear === true )
|
|
|
|
{
|
|
|
|
// clear for version update
|
|
|
|
quote.setProgramVersion( program.version );
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// default message
|
|
|
|
if ( msg.length === 0 )
|
|
|
|
{
|
|
|
|
msg.push( ''+clear );
|
|
|
|
}
|
|
|
|
|
|
|
|
// see comments for clear var above
|
|
|
|
_self.logger.log( _self.logger.PRIORITY_ERROR,
|
|
|
|
'Quote %s scan failed (' + msg.join( '; ' ) + ')',
|
|
|
|
quote.getId()
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( !handled )
|
|
|
|
{
|
|
|
|
handled = true;
|
|
|
|
callback( !clear, true );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// if nothing has requested that we wait, then continue immediately
|
|
|
|
if ( !handled && ( nwait === 0 ) )
|
|
|
|
{
|
|
|
|
handled = true;
|
|
|
|
c();
|
|
|
|
}
|
|
|
|
} );
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Generates default bucket data for the given program
|
|
|
|
*
|
|
|
|
* @return {Object} default bucket data
|
|
|
|
*/
|
|
|
|
'private _getDefaultBucket': function( program, quote_data )
|
|
|
|
{
|
|
|
|
var defaults = program.defaults,
|
|
|
|
bucket = quote_data.data || {},
|
|
|
|
pre = this._defaultBuckets[ program.getId() ];
|
|
|
|
|
|
|
|
// we only want to merge in the defaults if this is the first visit to
|
|
|
|
// the quote
|
|
|
|
if ( quote_data.currentStepId > 0 )
|
|
|
|
{
|
|
|
|
// todo: uncomment later; for now we want older quotes to still work
|
|
|
|
//return bucket;
|
|
|
|
}
|
|
|
|
|
|
|
|
// if we already generated the default bucket data and have no
|
|
|
|
// quote-specific data, return it
|
|
|
|
if ( pre && ( quote_data.data === undefined ) )
|
|
|
|
{
|
|
|
|
return pre;
|
|
|
|
}
|
|
|
|
|
|
|
|
// generate
|
|
|
|
for ( item in program.defaults )
|
|
|
|
{
|
|
|
|
if ( bucket[ item ] === undefined )
|
|
|
|
{
|
|
|
|
bucket[ item ] = [ defaults[ item ] ];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// set as default bucket only if we didn't merge
|
|
|
|
if ( quote_data.data === undefined )
|
|
|
|
{
|
|
|
|
this._defaultBuckets[ program.getId() ] = bucket;
|
|
|
|
}
|
|
|
|
|
|
|
|
return bucket;
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sends a new quote initialization request to the client
|
|
|
|
*
|
|
|
|
* @param {HttpServerRequest} request
|
|
|
|
* @param {Function} quote_new function to create new quote
|
|
|
|
*
|
|
|
|
* @return {Server} self
|
|
|
|
*/
|
|
|
|
sendNewQuote: function( request, quote_new )
|
|
|
|
{
|
|
|
|
var server = this,
|
|
|
|
session = request.getSession();
|
|
|
|
|
|
|
|
function donew( quote_id )
|
|
|
|
{
|
|
|
|
var quote = quote_new( quote_id );
|
|
|
|
server.sendResponse( request, quote, { valid: false } );
|
|
|
|
}
|
|
|
|
|
|
|
|
// should we override the quote id?
|
|
|
|
var rqn;
|
|
|
|
if ( ( rqn = session.getReturnQuoteNumber() ) > 0 )
|
|
|
|
{
|
|
|
|
donew( rqn );
|
|
|
|
|
|
|
|
// we don't need to wait for this to finish, since the next request
|
|
|
|
// won't be for a new quote
|
|
|
|
session.clearReturnQuoteNumber();
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// get the next available quote id
|
|
|
|
this.dao.getNextQuoteId( donew );
|
|
|
|
}
|
|
|
|
|
|
|
|
return this;
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
sendInit: function( request, quote, program, quote_new, prev )
|
|
|
|
{
|
|
|
|
var _self = this,
|
|
|
|
args = arguments;
|
|
|
|
|
|
|
|
this._checkQuotePver( quote, program, function( err, mod )
|
|
|
|
{
|
|
|
|
if ( err )
|
|
|
|
{
|
|
|
|
// return as fatal
|
|
|
|
_self.sendError( request, "Quote sanitization failed" );
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// save the quote updates (but only if it was modified)
|
|
|
|
if ( mod )
|
|
|
|
{
|
|
|
|
_self.dao.saveQuote( quote,
|
|
|
|
function()
|
|
|
|
{
|
|
|
|
_self._processInit.apply( _self, args );
|
|
|
|
},
|
|
|
|
function()
|
|
|
|
{
|
|
|
|
_self.sendError( request,
|
|
|
|
"Quote sanitization failed to commit"
|
|
|
|
);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
_self._processInit.apply( _self, args );
|
|
|
|
}
|
|
|
|
} );
|
|
|
|
|
|
|
|
return this;
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sends /init response
|
|
|
|
*
|
|
|
|
* @param UserRequest request
|
|
|
|
* @param Quote quote
|
|
|
|
* @param {Program} program
|
|
|
|
* @param {Function} quote_new function to create new quote
|
|
|
|
*
|
|
|
|
* @return Server self to allow for method chaining
|
|
|
|
*
|
|
|
|
* @todo generate quote # rather than prompting
|
|
|
|
*/
|
|
|
|
_processInit: function( request, quote, program, quote_new, prev )
|
|
|
|
{
|
|
|
|
var actions = null,
|
|
|
|
valid = true,
|
|
|
|
program_id = program.getId(),
|
|
|
|
session = request.getSession(),
|
|
|
|
internal = session.isInternal();
|
|
|
|
|
|
|
|
// if no quote id was given, simply prompt for one for now
|
|
|
|
if ( quote.getId() == 0 )
|
|
|
|
{
|
|
|
|
this.sendNewQuote( request, quote_new );
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
else if ( quote.getProgramId() !== program_id )
|
|
|
|
{
|
|
|
|
// invalid program; change the program id
|
|
|
|
actions = [ {
|
|
|
|
action: 'setProgram',
|
|
|
|
id: quote.getProgramId(),
|
|
|
|
quoteId: quote.getId(),
|
|
|
|
} ];
|
|
|
|
|
|
|
|
valid = false;
|
|
|
|
}
|
|
|
|
else if ( quote.hasError() )
|
|
|
|
{
|
|
|
|
this.sendError( request, quote.getError() );
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
// ensure that the agent id matches the quote's agent (unless internal)
|
|
|
|
else if ( ( internal === false )
|
|
|
|
&& ( request.getSession().agentId() != quote.getAgentId() )
|
|
|
|
)
|
|
|
|
{
|
|
|
|
// todo: generate a new quote #
|
|
|
|
actions = [ { action: 'quotePrompt' } ];
|
|
|
|
valid = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// don't return any quote data if invalid - we don't want people spying
|
|
|
|
// on the data!
|
|
|
|
if ( valid === false )
|
|
|
|
{
|
|
|
|
this.sendResponse( request, quote, {
|
|
|
|
valid: false,
|
|
|
|
}, actions );
|
|
|
|
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
var data = quote.getBucket().getData() || {};
|
|
|
|
|
|
|
|
// if we're not internal, filter out the internal questions from the
|
|
|
|
// data array to ensure that they can't spy on our internal data
|
|
|
|
if ( request.getSession().isInternal() === false )
|
|
|
|
{
|
|
|
|
for ( id in program.internal )
|
|
|
|
{
|
|
|
|
delete data[ id ];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var bucket = quote.getBucket(),
|
|
|
|
lock = quote.getExplicitLockReason(),
|
|
|
|
lock_step = quote.getExplicitLockStep(),
|
|
|
|
_self = this;
|
|
|
|
|
|
|
|
if ( valid && !lock && prev )
|
|
|
|
{
|
|
|
|
actions = [ {
|
|
|
|
action: 'warning',
|
|
|
|
message: (
|
|
|
|
'Somebody else is currently viewing this quote; it ' +
|
|
|
|
'has been locked and will be read-only until the ' +
|
|
|
|
'other person is finished. Please try again later.' +
|
|
|
|
|
|
|
|
( ( internal )
|
|
|
|
? '<br /><br />N.B.: You are an internal user, so you ' +
|
|
|
|
'may unlock the quote above, but be warned that ' +
|
|
|
|
'concurrent writes may have negative affects on ' +
|
|
|
|
'the integrity of the quote.' +
|
|
|
|
'<br /><br />' +
|
|
|
|
'Currently viewing: ' + prev
|
|
|
|
: ''
|
|
|
|
)
|
|
|
|
)
|
|
|
|
} ];
|
|
|
|
|
|
|
|
lock = 'concurrent-access';
|
|
|
|
}
|
|
|
|
|
|
|
|
// decrypt bucket contents, if necessary, and return
|
|
|
|
this._getBucketCipher( program ).decrypt( bucket, function()
|
|
|
|
{
|
|
|
|
_self.sendResponse( request, quote, {
|
|
|
|
valid: valid,
|
|
|
|
data: bucket.getData() || {},
|
|
|
|
|
|
|
|
currentStepId: quote.getCurrentStepId(),
|
|
|
|
topVisitedStepId: quote.getTopVisitedStepId(),
|
|
|
|
imported: quote.isImported(),
|
|
|
|
bound: quote.isBound(),
|
|
|
|
needsImport: quote.needsImport(),
|
|
|
|
explicitLock: lock,
|
|
|
|
explicitLockStepId: lock_step,
|
|
|
|
agentId: quote.getAgentId(),
|
|
|
|
agentName: quote.getAgentName(),
|
|
|
|
|
|
|
|
quicksave: quote.getQuickSaveData(),
|
|
|
|
|
|
|
|
// set to undefined if not internal so it's not included in the
|
|
|
|
// JSON response
|
|
|
|
internal: ( ( request.getSession().isInternal() === true )
|
|
|
|
? true
|
|
|
|
: undefined
|
|
|
|
),
|
|
|
|
}, actions );
|
|
|
|
} );
|
|
|
|
|
|
|
|
return this;
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sends a step to the client
|
|
|
|
*
|
|
|
|
* @param UserRequest request
|
|
|
|
* @param Quote quote
|
|
|
|
* @param Integer program
|
|
|
|
* @param Integer step_id id of the step
|
|
|
|
*
|
|
|
|
* @return void
|
|
|
|
*/
|
|
|
|
sendStep: function( request, quote, program, step_id, session )
|
|
|
|
{
|
|
|
|
var cur_id = quote.getCurrentStepId(),
|
|
|
|
saved_id = quote.getTopSavedStepId(),
|
|
|
|
program_id = program.id;
|
|
|
|
|
|
|
|
if ( program.steps[ step_id ] === undefined )
|
|
|
|
{
|
|
|
|
this.sendError( request,
|
|
|
|
"Invalid step request; step " + step_id + " does not exist.",
|
|
|
|
[ { action: 'gostep', id: cur_id } ]
|
|
|
|
);
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
var type = program.steps[ step_id ].type;
|
|
|
|
|
|
|
|
// is this a management step? if so, we must be internal
|
|
|
|
if ( ( type === 'manage' ) && ( !session.isInternal() ) )
|
|
|
|
{
|
|
|
|
// we're not internal, so let's send them back to the first step
|
|
|
|
this.sendResponse( request, quote, {}, [
|
|
|
|
{ action: 'gostep', id: program.getFirstStepId() }
|
|
|
|
] );
|
|
|
|
}
|
|
|
|
|
|
|
|
// are they permitted to navigate to this step?
|
|
|
|
if ( step_id > ( quote.getTopVisitedStepId() + 1 ) )
|
|
|
|
{
|
|
|
|
// knock them back to the next step they're able to save
|
|
|
|
var tostep_id = ( quote.getTopSavedStepId() + 1 );
|
|
|
|
|
|
|
|
this.logger.log( this.logger.PRIORITY_ERROR,
|
|
|
|
"Quote " + quote.getId() + " has not yet reached step " +
|
|
|
|
step_id + "; forcing to step " + tostep_id
|
|
|
|
);
|
|
|
|
|
|
|
|
this.sendError( request,
|
|
|
|
"Failed to navigate to step: you have not yet reached " +
|
|
|
|
"the requested step.",
|
|
|
|
[ { action: 'gostep', id: tostep_id } ]
|
|
|
|
);
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// perform forward-validations *on the current step* to ensure that they
|
|
|
|
// cannot leave the quote and then return, requesting a future step (if
|
|
|
|
// permitted), thereby evading client-side forward-validations
|
|
|
|
if ( step_id > cur_id )
|
|
|
|
{
|
|
|
|
if ( this._forwardValidate( quote, program, cur_id ) === false )
|
|
|
|
{
|
|
|
|
this.sendError( request,
|
|
|
|
"The previous step contains errors; please correct them " +
|
|
|
|
"before continuing.",
|
|
|
|
[ { action: 'gostep', id: cur_id } ]
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var server = this;
|
|
|
|
|
|
|
|
this._cache.get( 'step_html' )
|
|
|
|
.then( prog => prog.get( program_id ) )
|
|
|
|
.then( shtml => shtml.get( step_id ) )
|
|
|
|
.then( data =>
|
|
|
|
{
|
|
|
|
// send the step HTML to the client
|
|
|
|
server.sendResponse( request, quote, {
|
|
|
|
html: data,
|
|
|
|
} );
|
|
|
|
} )
|
|
|
|
.catch( err =>
|
|
|
|
{
|
|
|
|
server.logger.log( server.logger.PRIORITY_ERROR,
|
|
|
|
"Failed to load program '%s' step %d: %s",
|
|
|
|
program_id,
|
|
|
|
step_id,
|
|
|
|
err.message
|
|
|
|
);
|
|
|
|
|
|
|
|
server.sendError( request,
|
|
|
|
'The step you requested could not be loaded.'
|
|
|
|
);
|
|
|
|
|
|
|
|
throw err;
|
|
|
|
} );
|
|
|
|
|
|
|
|
return this;
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Step HTML cache miss function
|
|
|
|
*
|
|
|
|
* Load step HTML from disk. This is intended to be used as a
|
|
|
|
* miss function.
|
|
|
|
*
|
|
|
|
* TODO: Extract method
|
|
|
|
*
|
|
|
|
* @param {string} program_id program containing step
|
|
|
|
* @param {number} step_id step to load
|
|
|
|
*
|
|
|
|
* @return {Promise}
|
|
|
|
*/
|
|
|
|
'public loadStepHtml': function( program_id, step_id )
|
|
|
|
{
|
|
|
|
var step_filename =
|
|
|
|
process.env.LV_ROOT_PATH + '/src/_gen/views/scripts/quote/' +
|
|
|
|
program_id + '/steps/' + step_id + '.phtml';
|
|
|
|
|
|
|
|
return new Promise( function( resolve, reject )
|
|
|
|
{
|
|
|
|
fs.readFile( step_filename, 'utf-8', function( err, data )
|
|
|
|
{
|
|
|
|
// we had a problem with the step
|
|
|
|
if ( err !== null )
|
|
|
|
{
|
|
|
|
reject( err );
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
resolve( data );
|
|
|
|
});
|
|
|
|
} );
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Perform forward-validations for a given quote and step
|
|
|
|
*
|
|
|
|
* This check is necessary to ensure that the client-side events are not
|
|
|
|
* bypassed, which is realatively simple to do. For example, one could leave
|
|
|
|
* the quote and return at a future step (so long as the operation is
|
|
|
|
* otherwise permitted), preventing the `forward' event from triggering on
|
|
|
|
* the client (as it is a relative event).
|
|
|
|
*
|
|
|
|
* @param {Quote} quote quote to forward-validate
|
|
|
|
* @param {Program} program program to validate against
|
|
|
|
* @param {number} step_id id of current step (before navigation)
|
|
|
|
*
|
|
|
|
* @return {boolean} validation success/failure
|
|
|
|
*/
|
|
|
|
'private _forwardValidate': function( quote, program, step_id )
|
|
|
|
{
|
|
|
|
var success = false,
|
|
|
|
_self = this;
|
|
|
|
|
|
|
|
// TODO: we need cmatch data to pass to `forward'
|
|
|
|
return true;
|
|
|
|
|
|
|
|
quote.visitData( function( bucket )
|
|
|
|
{
|
|
|
|
try
|
|
|
|
{
|
|
|
|
// forward event returns an object containing failures
|
|
|
|
success = ( program.forward( step_id, bucket, {} ) === null );
|
|
|
|
}
|
|
|
|
catch ( e )
|
|
|
|
{
|
|
|
|
// this should never happen, but in case it does, we need to
|
|
|
|
// make sure the user isn't left hanging with no response from
|
|
|
|
// the server; return gracefully after logging the error
|
|
|
|
_self.logger_log(
|
|
|
|
_self.logger.PRIORITY_ERROR,
|
|
|
|
'Forward-validation error (%s): WEB#%s, step %s',
|
|
|
|
program.id,
|
|
|
|
quote.getId(),
|
|
|
|
step_id
|
|
|
|
);
|
|
|
|
}
|
|
|
|
} );
|
|
|
|
|
|
|
|
// N.B.: defaults to false above
|
|
|
|
return success;
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
visitStep: function( step_id, request, quote )
|
|
|
|
{
|
|
|
|
// update the quote step, if valid
|
|
|
|
if ( step_id <= ( quote.getTopVisitedStepId() + 1 ) )
|
|
|
|
{
|
|
|
|
quote.setCurrentStepId( step_id );
|
|
|
|
}
|
|
|
|
|
|
|
|
this.sendResponse( request, quote, {} );
|
|
|
|
return this;
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
sendProgramJs: function( request, program_id )
|
|
|
|
{
|
|
|
|
var server = this;
|
|
|
|
|
|
|
|
this._cache.get( 'program_js' )
|
|
|
|
.then( progjs => progjs.get( program_id ) )
|
|
|
|
.then( data =>
|
|
|
|
{
|
|
|
|
// send the JS to the client
|
|
|
|
request.setContentType( 'text/javascript' ).end( data );
|
|
|
|
} )
|
|
|
|
.catch( err =>
|
|
|
|
{
|
|
|
|
server.logger.log( server.logger.PRIORITY_ERROR,
|
|
|
|
"Failed to load program '%s' JS: %s",
|
|
|
|
program_id,
|
|
|
|
err
|
|
|
|
);
|
|
|
|
|
|
|
|
server.sendError( request,
|
|
|
|
'Unable to retrieve program data'
|
|
|
|
);
|
|
|
|
|
|
|
|
throw err;
|
|
|
|
} );
|
|
|
|
|
|
|
|
return this;
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Program JS cache miss function
|
|
|
|
*
|
|
|
|
* Loads program JS from disk. This is intended to be used as a
|
|
|
|
* miss function.
|
|
|
|
*
|
|
|
|
* TODO: Extract method
|
|
|
|
*
|
|
|
|
* @param {string} program_id program to load
|
|
|
|
*
|
|
|
|
* @return {Promise}
|
|
|
|
*/
|
|
|
|
'public loadProgramFiles': function( program_id )
|
|
|
|
{
|
|
|
|
var root_path = process.env.LV_ROOT_PATH + '/src/_gen/scripts/program/' + program_id,
|
|
|
|
js_filename = root_path + '/Program.js',
|
|
|
|
inc_filename = root_path + '/include.js',
|
|
|
|
retjs = '';
|
|
|
|
|
|
|
|
return new Promise( function( resolve, reject )
|
|
|
|
{
|
|
|
|
// read both files
|
|
|
|
fs.readFile( js_filename, 'utf8', function( err, data )
|
|
|
|
{
|
|
|
|
if ( err !== null )
|
|
|
|
{
|
|
|
|
reject( err );
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// wrap in closure
|
|
|
|
data = "(function(require,module){" +
|
|
|
|
"var exports=module.exports={};" +
|
|
|
|
data +
|
|
|
|
"\n})(require,modules['program/" + program_id +
|
|
|
|
"/Program']={});"
|
|
|
|
|
|
|
|
retjs = data;
|
|
|
|
|
|
|
|
// read include file
|
|
|
|
fs.readFile( inc_filename, 'utf8', function( err, data )
|
|
|
|
{
|
|
|
|
if ( err === null )
|
|
|
|
{
|
|
|
|
retjs += data;
|
|
|
|
}
|
|
|
|
|
|
|
|
// we have all of our data; return the result
|
|
|
|
resolve( retjs );
|
|
|
|
} );
|
|
|
|
} );
|
|
|
|
} );
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles a quote data post
|
|
|
|
*
|
|
|
|
* This function is called when an HTTP POST is made to save quote data.
|
|
|
|
*
|
|
|
|
* @param Integer step_id id of the step
|
|
|
|
* @param UserRequest request request object
|
|
|
|
* @param Quote quote instance of quote to operate on
|
|
|
|
* @param Program program program associated with the quote
|
|
|
|
*
|
|
|
|
* @param {UserSession} session user session
|
|
|
|
*
|
|
|
|
* @return undefined
|
|
|
|
*/
|
|
|
|
handlePost: function( step_id, request, quote, program, session )
|
|
|
|
{
|
|
|
|
var server = this;
|
|
|
|
|
|
|
|
// do not allow quote modification if locked unless logged in as an
|
|
|
|
// internal user (FS#5772) and the program is unlockable
|
|
|
|
if ( (
|
|
|
|
quote.isLocked()
|
|
|
|
|| ( step_id < quote.getExplicitLockStep() )
|
|
|
|
)
|
|
|
|
&& !( session.isInternal() && program.unlockable )
|
|
|
|
)
|
|
|
|
{
|
|
|
|
server.logger.log( server.logger.PRIORITY_INFO,
|
|
|
|
"Cannot save imported quote: %s",
|
|
|
|
quote.getId()
|
|
|
|
);
|
|
|
|
|
|
|
|
server.sendError( request,
|
|
|
|
"This quote has been locked and can no longer be modified."
|
|
|
|
);
|
|
|
|
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
// are they getting too far ahead of themselves?
|
|
|
|
if ( step_id > ( quote.getTopSavedStepId() + 1 ) )
|
|
|
|
{
|
|
|
|
// knock back to next step that they're able to save
|
|
|
|
var tostep_id = ( quote.getTopSavedStepId() + 1 );
|
|
|
|
|
|
|
|
this.logger.log( this.logger.PRIORITY_ERROR,
|
|
|
|
"Quote " + quote.getId() + " cannot yet save step " +
|
|
|
|
step_id + "; forcing to step " + tostep_id
|
|
|
|
);
|
|
|
|
|
|
|
|
this.sendError( request,
|
|
|
|
"Unable to save step: you have not yet reached " +
|
|
|
|
"the requested step.",
|
|
|
|
[ { action: 'gostep', id: tostep_id, title: 'Go Back' } ]
|
|
|
|
);
|
|
|
|
|
|
|
|
// prohibit save
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
request.getPostData( function( post_data )
|
|
|
|
{
|
|
|
|
// fill the quote with the posted data
|
|
|
|
if ( post_data.data )
|
|
|
|
{
|
|
|
|
try
|
|
|
|
{
|
|
|
|
var filtered = server._sanitizeBucketData(
|
|
|
|
post_data.data, request, program
|
|
|
|
);
|
|
|
|
|
|
|
|
quote.setData( filtered );
|
|
|
|
|
|
|
|
// calculated values (store only)
|
|
|
|
program.initQuote( quote.getBucket(), true );
|
|
|
|
}
|
|
|
|
catch ( err )
|
|
|
|
{
|
|
|
|
server.logger.log( server.logger.PRIORITY_ERROR,
|
|
|
|
"Invalid POST data string (%s): %s",
|
|
|
|
err,
|
|
|
|
post_data.data
|
|
|
|
);
|
|
|
|
|
|
|
|
server.sendError( request,
|
|
|
|
'There was an error saving your data. Please ' +
|
|
|
|
'try again.',
|
|
|
|
[ { action: 'gostep', id: step_id, title: 'Go Back' } ]
|
|
|
|
);
|
|
|
|
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// save the quote
|
|
|
|
server._doQuoteSave( step_id, request, quote, program );
|
|
|
|
});
|
|
|
|
|
|
|
|
return this;
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sanitize the given bucket data
|
|
|
|
*
|
|
|
|
* Ensures that we are storing only "correct" data within our database. This
|
|
|
|
* also strips any unknown bucket values, preventing users from using us as
|
|
|
|
* their own personal database.
|
|
|
|
*/
|
|
|
|
'private _sanitizeBucketData': function(
|
|
|
|
bucket_data, request, program, permit_null
|
|
|
|
)
|
|
|
|
{
|
|
|
|
var data = JSON.parse( bucket_data ),
|
|
|
|
types = program.meta.qtypes,
|
|
|
|
ignore = {};
|
|
|
|
|
|
|
|
// if we're not internal, filter out the internal questions
|
|
|
|
// (so they can't post to them)
|
|
|
|
if ( request.getSession().isInternal() === false )
|
|
|
|
{
|
|
|
|
for ( id in program.internal )
|
|
|
|
{
|
|
|
|
ignore[ id ] = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// return the filtered data
|
|
|
|
bucket_filter.filter( data, types, ignore, permit_null );
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
'private _doQuoteSave': function( step_id, request, quote, program, c )
|
|
|
|
{
|
|
|
|
var server = this;
|
|
|
|
|
|
|
|
// whenever they save, we want to make sure we invalidate the premium,
|
|
|
|
// unless this is a rating step
|
|
|
|
if ( ( program.rateSteps || [] )[ step_id ] !== true )
|
|
|
|
{
|
|
|
|
quote.setLastPremiumDate( 0 );
|
|
|
|
}
|
|
|
|
|
|
|
|
server.quoteFill( quote, step_id,
|
|
|
|
// success
|
|
|
|
function()
|
|
|
|
{
|
|
|
|
// encrypt bucket
|
|
|
|
var bucket = quote.getBucket();
|
|
|
|
server._getBucketCipher( program ).encrypt( bucket, function()
|
|
|
|
{
|
|
|
|
// as a precaution to prevent navigation burps, update the
|
|
|
|
// step if it's greater than the previous
|
|
|
|
if ( step_id > quote.getTopVisitedStepId() )
|
|
|
|
{
|
|
|
|
quote.setCurrentStepId( step_id );
|
|
|
|
}
|
|
|
|
if ( step_id > quote.getTopSavedStepId() )
|
|
|
|
{
|
|
|
|
// only updated by saveQuoteState
|
|
|
|
quote.setTopSavedStepId( step_id );
|
|
|
|
server.dao.saveQuoteState( quote );
|
|
|
|
}
|
|
|
|
|
|
|
|
server.dao.saveQuote( quote,
|
|
|
|
// quote was saved successfully
|
|
|
|
function()
|
|
|
|
{
|
|
|
|
server._postSubmit(
|
|
|
|
request, quote, step_id, program,
|
|
|
|
request.getSession().isInternal()
|
|
|
|
);
|
|
|
|
|
|
|
|
c && c( true );
|
|
|
|
},
|
|
|
|
// failed to save the quote
|
|
|
|
function()
|
|
|
|
{
|
|
|
|
// todo: option to allow them to try again
|
|
|
|
server.sendError( request,
|
|
|
|
'There was a problem saving your quote. ' +
|
|
|
|
'<em>The previous step was not saved!</em>'
|
|
|
|
);
|
|
|
|
|
|
|
|
c && c( false );
|
|
|
|
}
|
|
|
|
);
|
|
|
|
} );
|
|
|
|
},
|
|
|
|
// failure
|
|
|
|
function( failures )
|
|
|
|
{
|
|
|
|
// todo: detailed logging (this shouldn't happen)
|
|
|
|
server.logger.log( server.logger.PRIORITY_ERROR,
|
|
|
|
"Server-side quote data validation failure for " +
|
|
|
|
"quote #%s, program %s, step %d:\n%s",
|
|
|
|
quote.getId(),
|
|
|
|
program.id,
|
|
|
|
step_id,
|
|
|
|
util.inspect( failures )
|
|
|
|
);
|
|
|
|
|
|
|
|
server.sendError( request,
|
|
|
|
'There was a problem with the data you entered. ' +
|
|
|
|
'Please click "Go Back" below to go back to the ' +
|
|
|
|
'previous step and correct the errors.',
|
|
|
|
[
|
|
|
|
{ action: 'gostep', id: step_id },
|
|
|
|
{ action: 'invalidate', errors: failures },
|
|
|
|
],
|
|
|
|
'Go Back'
|
|
|
|
);
|
|
|
|
|
|
|
|
c && c( false );
|
|
|
|
}
|
|
|
|
);
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
'private _postSubmit': function( request, quote, step_id, program, internal )
|
|
|
|
{
|
|
|
|
var server = this,
|
|
|
|
actions = [],
|
|
|
|
bucket = null;
|
|
|
|
|
|
|
|
// XXX
|
|
|
|
quote.visitData( function( b )
|
|
|
|
{
|
|
|
|
bucket = b;
|
|
|
|
} );
|
|
|
|
|
|
|
|
var result = program.postSubmit(
|
|
|
|
step_id,
|
|
|
|
bucket,
|
|
|
|
function( event, question_id, value )
|
|
|
|
{
|
|
|
|
switch ( event )
|
|
|
|
{
|
|
|
|
// kick back to the given step, if they're already past it
|
|
|
|
case 'kickBack':
|
|
|
|
var step_id = +value;
|
|
|
|
|
|
|
|
// clear any fields scheduled to be cleared on kickback
|
|
|
|
var retdata = server._kbclear( program, quote );
|
|
|
|
|
|
|
|
if ( quote.getTopVisitedStepId() > step_id )
|
|
|
|
{
|
|
|
|
quote.setTopVisitedStepId( step_id );
|
|
|
|
|
|
|
|
// knock them back to the step if they're currently
|
|
|
|
// further
|
|
|
|
if ( quote.getCurrentStepId() > step_id )
|
|
|
|
{
|
|
|
|
quote.setCurrentStepId( step_id );
|
|
|
|
actions.push( {
|
|
|
|
action: 'gostep',
|
|
|
|
id: step_id,
|
|
|
|
} );
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
server.dao.mergeBucket( quote, retdata, function()
|
|
|
|
{
|
|
|
|
server.dao.saveQuoteState( quote, function()
|
|
|
|
{
|
|
|
|
// if we're not internal, strip any potential
|
|
|
|
// internal data from the response
|
|
|
|
// XXX: maybe we should do this in
|
|
|
|
// sendResponse() to ensure consistency
|
|
|
|
if ( internal === false )
|
|
|
|
{
|
|
|
|
for ( id in program.internal )
|
|
|
|
{
|
|
|
|
delete retdata[ id ];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// don't send the response until the state
|
|
|
|
// is saved; we don't want a race condition
|
|
|
|
// if they're speeding through steps!
|
|
|
|
finish( retdata );
|
|
|
|
} );
|
|
|
|
} );
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
server.logger.log( server.logger.PRIORITY_ERROR,
|
|
|
|
"Unknown postSubmit event: %s",
|
|
|
|
event
|
|
|
|
);
|
|
|
|
|
|
|
|
finish();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
function finish( data )
|
|
|
|
{
|
|
|
|
data = data || {};
|
|
|
|
server.sendResponse( request, quote, data, actions );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
// if there's no events, then just respond with a generic OK
|
|
|
|
if ( result === false )
|
|
|
|
{
|
|
|
|
server.sendResponse( request, quote );
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
'private _kbclear': function( program, quote )
|
|
|
|
{
|
|
|
|
var set = {};
|
|
|
|
|
|
|
|
for ( var field in program.kbclear )
|
|
|
|
{
|
|
|
|
var data = quote.getDataByName( field ),
|
|
|
|
val = ( program.defaults[ field ] || '' );
|
|
|
|
|
|
|
|
for ( var i in data )
|
|
|
|
{
|
|
|
|
data[ i ] = val;
|
|
|
|
}
|
|
|
|
|
|
|
|
set[ field ] = data;
|
|
|
|
}
|
|
|
|
|
|
|
|
quote.setData( set );
|
|
|
|
|
|
|
|
// return the fields that have changed
|
|
|
|
return set;
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
quoteFill: function( data, step_id, success, failure )
|
|
|
|
{
|
|
|
|
if ( data instanceof Function )
|
|
|
|
{
|
|
|
|
this.quoteFillHooks.push( data );
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
var abort = false,
|
|
|
|
failures = {};
|
|
|
|
|
|
|
|
var event = {
|
|
|
|
abort: function( failure_data )
|
|
|
|
{
|
|
|
|
failures = failure_data;
|
|
|
|
abort = true;
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
var len = this.quoteFillHooks.length;
|
|
|
|
for ( var i = 0; i < len; i++ )
|
|
|
|
{
|
|
|
|
this.quoteFillHooks[i].call( event, data, step_id );
|
|
|
|
|
|
|
|
// if we aborted, there's no need to continue
|
|
|
|
if ( abort )
|
|
|
|
{
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// only call the callback if we did not abort
|
|
|
|
if ( abort )
|
|
|
|
{
|
|
|
|
failure.call( this, failures );
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
success.call( this );
|
|
|
|
}
|
|
|
|
|
|
|
|
return this;
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Lazily loads and returns the requested Program object
|
|
|
|
*
|
|
|
|
* @param String program_id id of the program to retrieve
|
|
|
|
*
|
|
|
|
* @return Server self to allow for method chaining
|
|
|
|
*/
|
|
|
|
'public getProgram': function( program_id )
|
|
|
|
{
|
|
|
|
var _self = this;
|
|
|
|
|
|
|
|
return this._cache.get( 'program' )
|
|
|
|
.then( pcache => pcache.get( program_id ) )
|
|
|
|
.catch( function( e )
|
|
|
|
{
|
|
|
|
// looks like it doesn't exist
|
|
|
|
_self.logger.log( _self.logger.PRIORITY_ERROR,
|
|
|
|
"Program class '%s' could not be loaded: %s",
|
|
|
|
program_id,
|
|
|
|
e.message
|
|
|
|
);
|
|
|
|
|
|
|
|
throw e;
|
|
|
|
} );
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Program object cache miss function
|
|
|
|
*
|
|
|
|
* Instantiates program. This is intended to be used as a miss
|
|
|
|
* function.
|
|
|
|
*
|
|
|
|
* TODO: Extract method
|
|
|
|
*
|
|
|
|
* @param {string} program_id program to instantiate
|
|
|
|
*
|
|
|
|
* @return {Promise}
|
|
|
|
*/
|
|
|
|
'public loadProgram': function( program_id )
|
|
|
|
{
|
|
|
|
var server = this;
|
|
|
|
|
|
|
|
return new Promise( function( resolve, reject )
|
|
|
|
{
|
|
|
|
try
|
|
|
|
{
|
|
|
|
const program_path = 'program/' + program_id + '/Program';
|
|
|
|
|
|
|
|
// node caches modules; make sure it's cleared
|
|
|
|
delete require.cache[
|
|
|
|
require.resolve( program_path )
|
|
|
|
];
|
|
|
|
|
|
|
|
// attempt to load the program class
|
|
|
|
const program_module = require( program_path );
|
|
|
|
const program = program_module();
|
|
|
|
|
|
|
|
// hook ourselves
|
|
|
|
server.quoteFill( function( quote, step_id )
|
|
|
|
{
|
|
|
|
var _self = this;
|
|
|
|
|
|
|
|
// only perform quote validation if the quote is
|
|
|
|
// using this program
|
|
|
|
if ( quote.getProgramId() !== program_id )
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// todo: unnecessary dependency
|
|
|
|
var bucket_quote = quote.getBucket().getData(),
|
|
|
|
bucket_tmp = QuoteDataBucket(),
|
|
|
|
data_tmp = {};
|
|
|
|
|
|
|
|
// this actually takes only 1ms, even with a reasonably
|
|
|
|
// sized bucket (tested with snowmobile) - both the copy and
|
|
|
|
// setValues()
|
|
|
|
for ( item in bucket_quote )
|
|
|
|
{
|
|
|
|
if ( !Array.isArray( bucket_quote[ item ] ) )
|
|
|
|
{
|
|
|
|
// this is a problem (FS#5849)
|
|
|
|
bucket_quote[ item ] = [];
|
|
|
|
|
|
|
|
server.logger.log( server.logger.PRIORITY_ERROR,
|
|
|
|
"Bucket item '%s' not an array for " +
|
|
|
|
"quote id %s in program %s; set to empty",
|
|
|
|
|
|
|
|
item, quote.getId(), program_id
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
data_tmp[ item ] = bucket_quote[ item ].slice( 0 );
|
|
|
|
}
|
|
|
|
|
|
|
|
bucket_tmp.setValues( data_tmp );
|
|
|
|
|
|
|
|
// Run all initialization stuff (e.g. calculated
|
|
|
|
// values) on the bucket to prepare for
|
|
|
|
// assertions. It's important to note that we
|
|
|
|
// duplicate the bucket to ensure that none of the
|
|
|
|
// calculated values are saved (the ones we want
|
|
|
|
// to save are already in there).
|
|
|
|
program.initQuote( bucket_tmp );
|
|
|
|
|
|
|
|
var classdata = program.classify( bucket_tmp.getData() );
|
|
|
|
|
|
|
|
// XXX
|
|
|
|
FieldClassMatcher( program.whens )
|
|
|
|
.match( classdata, function( cmatch )
|
|
|
|
{
|
|
|
|
var failures = program.submit( step_id,
|
|
|
|
bucket_tmp,
|
|
|
|
cmatch
|
|
|
|
);
|
|
|
|
|
|
|
|
// if there's any failures, abort the operation
|
|
|
|
if ( failures !== null )
|
|
|
|
{
|
|
|
|
server.logger.log( server.logger.PRIORITY_ERROR,
|
|
|
|
"Server-side validation failure"
|
|
|
|
);
|
|
|
|
_self.abort( failures );
|
|
|
|
}
|
|
|
|
} );
|
|
|
|
} );
|
|
|
|
|
|
|
|
resolve( program );
|
|
|
|
}
|
|
|
|
catch ( e )
|
|
|
|
{
|
|
|
|
reject( e );
|
|
|
|
}
|
|
|
|
} );
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
'private _getBucketCipher': function( program )
|
|
|
|
{
|
|
|
|
var _self = this;
|
|
|
|
|
|
|
|
return this._bucketCiphers[ program.id ] || ( function()
|
|
|
|
{
|
|
|
|
// create a new bucket cipher
|
|
|
|
var c = _self._bucketCiphers[ program.id ] = QuoteDataBucketCipher(
|
|
|
|
_self._encService,
|
|
|
|
program.secureFields.slice(0) || []
|
|
|
|
);
|
|
|
|
|
|
|
|
c.on( 'encrecover', function( field, length )
|
|
|
|
{
|
|
|
|
_self.logger.log( _self.logger.PRIORITY_ERROR,
|
|
|
|
"Invalid encrypted field data (%s of length %d); cleared.",
|
|
|
|
field,
|
|
|
|
length
|
|
|
|
);
|
|
|
|
} );
|
|
|
|
|
|
|
|
return c;
|
|
|
|
} )();
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle quick save request
|
|
|
|
*
|
|
|
|
* @param {UserRequest} request user request
|
|
|
|
* @param {Quote} quote quote to save
|
|
|
|
* @param {Program} program quote program
|
|
|
|
*
|
|
|
|
* @return {Server} self
|
|
|
|
*/
|
|
|
|
'public handleQuickSave': function( request, quote, program )
|
|
|
|
{
|
|
|
|
var _self = this;
|
|
|
|
|
|
|
|
// do not allow quote modification if locked unless logged in as an
|
|
|
|
// internal user (FS#5772) and the program is unlockable
|
|
|
|
if ( quote.isImported() )
|
|
|
|
{
|
|
|
|
//return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
request.getPostData( function( post_data )
|
|
|
|
{
|
|
|
|
// sanitize, permitting nulls (since the diff will have them)
|
|
|
|
try
|
|
|
|
{
|
|
|
|
var filtered = _self._sanitizeBucketData(
|
|
|
|
post_data.data, request, program, true
|
|
|
|
);
|
|
|
|
}
|
|
|
|
catch ( e )
|
|
|
|
{
|
|
|
|
_self.logger.log( server.logger.PRIORITY_ERROR,
|
|
|
|
"Invalid quicksave data string (%s): %s",
|
|
|
|
e.message,
|
|
|
|
post_data.data
|
|
|
|
);
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
var secure = program.secureFields,
|
|
|
|
i = secure.length;
|
|
|
|
|
|
|
|
// strip out secure fields (we can encrypt them later; this is just
|
|
|
|
// a quick solution to prevent sensitive data in plain text)
|
|
|
|
while ( i-- )
|
|
|
|
{
|
|
|
|
delete filtered[ secure[ i ] ];
|
|
|
|
}
|
|
|
|
|
|
|
|
// attempt to save the diff
|
|
|
|
_self.dao.quickSaveQuote( quote, filtered, function( err )
|
|
|
|
{
|
|
|
|
if ( !err )
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
_self.logger.log( server.logger.PRIORITY_DB,
|
|
|
|
"[Quick Save] Quick Save Failed: " + err
|
|
|
|
);
|
|
|
|
} );
|
|
|
|
} );
|
|
|
|
|
|
|
|
// just send the response immediately, as they do not need feedback if
|
|
|
|
// the quick-save fails (it is for debugging/backup in the event of a
|
|
|
|
// problem, so we need only concern ourselves if there is an issue)
|
|
|
|
this.sendEmptyReply( request, quote );
|
|
|
|
|
|
|
|
return this;
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
'public createRevision': function( request, quote )
|
|
|
|
{
|
|
|
|
var _self = this;
|
|
|
|
|
|
|
|
this.dao.createRevision( quote, function( err )
|
|
|
|
{
|
|
|
|
if ( err )
|
|
|
|
{
|
|
|
|
_self.logger.log( _self.logger.PRIORITY_DB,
|
|
|
|
"[mkrev] failed to create revision: " + err
|
|
|
|
);
|
|
|
|
|
|
|
|
_self.sendError( request, 'Failed to create revision.' );
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
_self.logger.log( _self.logger.PRIORITY_INFO,
|
|
|
|
"[mkrev] created new revision for quote %s",
|
|
|
|
quote.getId()
|
|
|
|
);
|
|
|
|
|
|
|
|
_self.sendEmptyReply( request, quote );
|
|
|
|
} );
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// TODO: currently only diffs against current revision (that is, the live
|
|
|
|
// bucket)
|
|
|
|
'public diffRevisionGroup': function( request, program, quote, gid, revid )
|
|
|
|
{
|
|
|
|
var _self = this,
|
|
|
|
progid = quote.getProgramId();
|
|
|
|
|
|
|
|
// this really should not happen...unless we delete a program, I suppose
|
|
|
|
if ( program === undefined )
|
|
|
|
{
|
|
|
|
this.sendError( request,
|
|
|
|
"Quote program id '" + progid + "' unknown"
|
|
|
|
);
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// get all fields linked to this group---not just the exclusive fields
|
|
|
|
var gfields = program.groupFields[ gid ];
|
|
|
|
if ( gfields === undefined )
|
|
|
|
{
|
|
|
|
this.sendError( request,
|
|
|
|
"Unknown group '" + gid + "' for program '" + progid + "'"
|
|
|
|
);
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// do we have leaders?
|
|
|
|
var lead_data = request.getGetData().leaders;
|
|
|
|
if ( !lead_data )
|
|
|
|
{
|
|
|
|
this.sendError( request,
|
|
|
|
"No leaders provided for group '" + gid + "'; available " +
|
|
|
|
"fields are: " + gfields.join( ', ' )
|
|
|
|
);
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
var leaders = lead_data.split( ',' );
|
|
|
|
this.dao.getRevision( quote, revid, function( revdata )
|
|
|
|
{
|
|
|
|
if ( !revdata )
|
|
|
|
{
|
|
|
|
_self.sendError( request,
|
|
|
|
"Revision " + revid + " not found for quote " +
|
|
|
|
quote.getId()
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
var revbucket = QuoteDataBucket().setValues( revdata.data );
|
|
|
|
|
|
|
|
// XXX: tightly coupled; temporary impl
|
|
|
|
try
|
|
|
|
{
|
|
|
|
var desc = BucketSiblingDescriptor()
|
|
|
|
.defineGroup( gid, gfields )
|
|
|
|
.markGroupLeaders( gid, leaders );
|
|
|
|
|
|
|
|
var diff = StdBucketDiff(
|
|
|
|
ShallowArrayDiff(),
|
|
|
|
function( context, changes )
|
|
|
|
{
|
|
|
|
return GroupedBucketDiffResult(
|
|
|
|
StdBucketDiffResult( context, changes ),
|
|
|
|
context
|
|
|
|
);
|
|
|
|
}
|
|
|
|
)
|
|
|
|
.diff(
|
|
|
|
GroupedBucketDiffContext(
|
|
|
|
StdBucketDiffContext(
|
|
|
|
quote.getBucket(),
|
|
|
|
revbucket
|
|
|
|
),
|
|
|
|
desc,
|
|
|
|
gid
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
catch ( e )
|
|
|
|
{
|
|
|
|
_self.sendError( request,
|
|
|
|
"An error occurred during processing: " +
|
|
|
|
e.message
|
|
|
|
);
|
|
|
|
throw e;
|
|
|
|
}
|
|
|
|
|
|
|
|
_self.sendResponse( request, quote, {
|
|
|
|
map: diff.createIndexMap(),
|
|
|
|
diff: diff.describeChangedValues(),
|
|
|
|
} );
|
|
|
|
} );
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
'public sendEmptyReply': function( request, quote )
|
|
|
|
{
|
|
|
|
this.sendResponse( request, quote, {} );
|
|
|
|
}
|
|
|
|
} );
|
|
|
|
|