app/controllers/submission-controller.js

/**
 * @module submissions-controller
 */

const communicator = require( '../lib/communicator' );
const surveyModel = require( '../models/survey-model' );
const userModel = require( '../models/user-model' );
const instanceModel = require( '../models/instance-model' );
const submissionModel = require( '../models/submission-model' );
const utils = require( '../lib/utils' );
const request = require( 'request' );
const express = require( 'express' );
const router = express.Router();
const routerUtils = require( '../lib/router-utils' );
// var debug = require( 'debug' )( 'submission-controller' );

module.exports = app => {
    app.use( `${app.get( 'base path' )}/submission`, router );
};

router.param( 'enketo_id', routerUtils.enketoId );
router.param( 'encrypted_enketo_id_single', routerUtils.encryptedEnketoIdSingle );
router.param( 'encrypted_enketo_id_view', routerUtils.encryptedEnketoIdView );

router
    .all( '*', ( req, res, next ) => {
        res.set( 'Content-Type', 'application/json' );
        next();
    } )
    .get( '/max-size/:encrypted_enketo_id_single', maxSize )
    .get( '/max-size/:encrypted_enketo_id_view', maxSize )
    .get( '/max-size/:enketo_id?', maxSize )
    .get( '/:encrypted_enketo_id_view', getInstance )
    .get( '/:enketo_id', getInstance )
    .post( '/:encrypted_enketo_id_single', submit )
    .post( '/:enketo_id', submit )
    .all( '/*', ( req, res, next ) => {
        const error = new Error( 'Not allowed' );
        error.status = 405;
        next( error );
    } );

/**
 * Simply pipes well-formed request to the OpenRosa server and
 * copies the response received.
 *
 * @param {module:api-controller~ExpressRequest} req - HTTP request
 * @param {module:api-controller~ExpressResponse} res - HTTP response
 * @param {Function} next - Express callback
 */
function submit( req, res, next ) {
    let submissionUrl;
    const paramName = req.app.get( 'query parameter to pass to submission' );
    const paramValue = req.query[ paramName ];
    const query = paramValue ? `?${paramName}=${paramValue}` : '';
    const instanceId = req.headers[ 'x-openrosa-instance-id' ];
    const deprecatedId = req.headers[ 'x-openrosa-deprecated-id' ];
    const id = req.enketoId;

    surveyModel.get( id )
        .then( survey => {
            submissionUrl = communicator.getSubmissionUrl( survey.openRosaServer ) + query;
            const credentials = userModel.getCredentials( req );

            return communicator.getAuthHeader( submissionUrl, credentials );
        } )
        .then( authHeader => {
            // Note even though headers is part of these options, it does not overwrite the headers set on the client!
            const options = {
                method: 'POST',
                url: submissionUrl,
                headers: authHeader ? {
                    'Authorization': authHeader
                } : {},
                timeout: req.app.get( 'timeout' ) + 500
            };

            // The Date header is actually forbidden to set programmatically, but we do it anyway to comply with OpenRosa
            options.headers[ 'Date' ] = new Date().toUTCString();

            // pipe the request
            req.pipe( request( options ) )
                .on( 'response', orResponse => {
                    if ( orResponse.statusCode === 201 ) {
                        _logSubmission( id, instanceId, deprecatedId );
                    } else if ( orResponse.statusCode === 401 ) {
                        // replace the www-authenticate header to avoid browser built-in authentication dialog
                        orResponse.headers[ 'WWW-Authenticate' ] = `enketo${orResponse.headers[ 'WWW-Authenticate' ]}`;
                    }
                } )
                .on( 'error', error => {
                    if ( error && ( error.code === 'ETIMEDOUT' || error.code === 'ECONNRESET' ) ) {
                        if ( error.connect === true ) {
                            error.status = 504;
                        } else {
                            error.status = 408;
                        }
                    }

                    next( error );
                } )
                .pipe( res );

        } )
        .catch( next );
}

/**
 * Get max submission size.
 *
 * @param {module:api-controller~ExpressRequest} req - HTTP request
 * @param {module:api-controller~ExpressResponse} res - HTTP response
 * @param {Function} next - Express callback
 */
function maxSize( req, res, next ) {
    if ( req.query.xformUrl ) {
        // Non-standard way of attempting to obtain max submission size from XForm url directly
        communicator.getMaxSize( {
            info: {
                downloadUrl: req.query.xformUrl
            }
        } )
            .then( maxSize => {
                res.json( { maxSize } );
            } )
            .catch( next );
    } else {
        surveyModel.get( req.enketoId )
            .then( survey => {
                survey.credentials = userModel.getCredentials( req );

                return survey;
            } )
            .then( communicator.getMaxSize )
            .then( maxSize => {
                res.json( { maxSize } );
            } )
            .catch( next );
    }
}

/**
 * Obtains cached instance (for editing)
 *
 * @param {module:api-controller~ExpressRequest} req - HTTP request
 * @param {module:api-controller~ExpressResponse} res - HTTP response
 * @param {Function} next - Express callback
 */
function getInstance( req, res, next ) {
    surveyModel.get( req.enketoId )
        .then( survey => {
            survey.instanceId = req.query.instanceId;
            instanceModel.get( survey )
                .then( survey => {
                    // check if found instance actually belongs to the form
                    if ( utils.getOpenRosaKey( survey ) === survey.openRosaKey ) {
                        // Change URLs of instanceAttachments to local URLs
                        Object.keys( survey.instanceAttachments ).forEach( key => survey.instanceAttachments[ key ] = utils.toLocalMediaUrl( survey.instanceAttachments[ key ] ) );

                        res.json( {
                            instance: survey.instance,
                            instanceAttachments: survey.instanceAttachments
                        } );
                    } else {
                        const error = new Error( 'Instance doesn\'t belong to this form' );
                        error.status = 400;
                        throw error;
                    }
                } ).catch( next );
        } )
        .catch( next );
}

/**
 * @param { string } id - Enketo ID of survey
 * @param { string } instanceId - instance ID of record
 * @param { string } deprecatedId - deprecated (previous) ID of record
 */
function _logSubmission( id, instanceId, deprecatedId ) {
    submissionModel.isNew( id, instanceId )
        .then( notRecorded => {
            if ( notRecorded ) {
                // increment number of submissions
                surveyModel.incrementSubmissions( id );
                // store/log instanceId
                submissionModel.add( id, instanceId, deprecatedId );
            }
        } )
        .catch( error => {
            console.error( error );
        } );
}