diff --git a/package-lock.json b/package-lock.json index 0e3719d9af..06285216bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12974,8 +12974,7 @@ "he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" }, "helmet": { "version": "4.1.1", diff --git a/package.json b/package.json index 9cab738cda..eec84db59e 100644 --- a/package.json +++ b/package.json @@ -120,6 +120,7 @@ "font-awesome": "4.7.0", "fp-ts": "^2.8.3", "has-ansi": "^4.0.0", + "he": "^1.2.0", "helmet": "^4.1.1", "http-status-codes": "^2.1.4", "intl-tel-input": "~12.1.6", diff --git a/src/app/controllers/public-forms.server.controller.js b/src/app/controllers/public-forms.server.controller.js index 38fccd3479..9eb9f280d6 100644 --- a/src/app/controllers/public-forms.server.controller.js +++ b/src/app/controllers/public-forms.server.controller.js @@ -2,6 +2,7 @@ const mongoose = require('mongoose') const { StatusCodes } = require('http-status-codes') +const querystring = require('querystring') const { createReqMeta } = require('../utils/request') const logger = require('../../config/logger').createLoggerWithLabel(module) @@ -47,6 +48,10 @@ exports.redirect = async function (req, res) { let redirectPath = req.params.state ? req.params.Id + '/' + req.params.state : req.params.Id + const reqQuery = querystring.stringify(req.query) + if (reqQuery.length > 0) { + redirectPath = redirectPath + '?' + encodeURIComponent(reqQuery) + } try { const metaTags = await fetchMetatags(req) return res.render('index', { @@ -62,8 +67,9 @@ exports.redirect = async function (req, res) { }, error: err, }) + res.redirect('/#!/' + redirectPath) + return } - res.redirect('/#!/' + redirectPath) } /** diff --git a/src/app/routes/frontend.server.routes.js b/src/app/routes/frontend.server.routes.js index 25ff95a9b0..e139ae225e 100644 --- a/src/app/routes/frontend.server.routes.js +++ b/src/app/routes/frontend.server.routes.js @@ -14,7 +14,7 @@ module.exports = function (app) { celebrate({ [Segments.QUERY]: { redirectPath: Joi.string() - .regex(/^[a-fA-F0-9]{24}(\/(preview|template|use-template))?$/) + .regex(/^[a-fA-F0-9]{24}(\/(preview|template|use-template))?/) .required(), }, }), diff --git a/src/public/modules/forms/base/directives/field.client.directive.js b/src/public/modules/forms/base/directives/field.client.directive.js index b62789db61..9a3859cc08 100644 --- a/src/public/modules/forms/base/directives/field.client.directive.js +++ b/src/public/modules/forms/base/directives/field.client.directive.js @@ -1,12 +1,19 @@ 'use strict' const { get } = require('lodash') +const he = require('he') +const querystring = require('querystring') angular .module('forms') - .directive('fieldDirective', ['FormFields', fieldDirective]) + .directive('fieldDirective', [ + 'FormFields', + '$location', + '$sanitize', + fieldDirective, + ]) -function fieldDirective(FormFields) { +function fieldDirective(FormFields, $location, $sanitize) { return { restrict: 'E', templateUrl: @@ -23,6 +30,30 @@ function fieldDirective(FormFields) { isValidateDate: '<', }, link: function (scope) { + // Stealth prefill feature + // If a query parameter is provided to a form URL in the form ?=&=... + // And if the fieldIds are valid mongoose object IDs and refer to a short text field, + // Then prefill and disable editing the corresponding form field on the frontend + + const decodedUrl = he.decode($location.url()) // tech debt; after redirect, & is encoded as & in the query string + const query = decodedUrl.split('?') + const queryParams = + query.length > 1 ? querystring.parse(query[1]) : undefined + + if ( + queryParams && + scope.field._id in queryParams && + scope.field.fieldType === 'textfield' + ) { + const prefillValue = queryParams[scope.field._id] + if (typeof prefillValue === 'string') { + // Only support unique query params. If query params are duplicated, + // none of the duplicated keys will be prefilled + scope.field.fieldValue = $sanitize(prefillValue) // $sanitize as a precaution to prevent xss + scope.field.disabled = true + } + } + if ((scope.isadminpreview || scope.isTemplate) && scope.field.myInfo) { // Determine whether to disable field in preview if ( diff --git a/tests/unit/backend/controllers/public-forms.server.controller.spec.js b/tests/unit/backend/controllers/public-forms.server.controller.spec.js index c29df9e2d4..a71c616dae 100644 --- a/tests/unit/backend/controllers/public-forms.server.controller.spec.js +++ b/tests/unit/backend/controllers/public-forms.server.controller.spec.js @@ -30,6 +30,7 @@ describe('Public-Forms Controller', () => { params: {}, body: {}, headers: {}, + url: '', ip: '127.0.0.1', get: () => '', } @@ -90,6 +91,35 @@ describe('Public-Forms Controller', () => { Controller.redirect(req, res) }) + it('should redirect to form with correct query params retained', (done) => { + req.params = { + Id: '321564654f65we4f65e4f5', + } + req.query = { + p1: 'v1-_', + p2: 'v2', + p3: ['v3', 'v4'], + } + + res.redirect = jasmine.createSpy().and.callFake(() => { + expect(res.redirect).toHaveBeenCalledWith( + jasmine.stringMatching(/\?.*p1%3Dv1-_/), + ) + expect(res.redirect).toHaveBeenCalledWith( + jasmine.stringMatching(/\?.*p2%3Dv2/), + ) + expect(res.redirect).toHaveBeenCalledWith( + jasmine.stringMatching(/\?.*p3%3Dv3/), + ) + expect(res.redirect).toHaveBeenCalledWith( + jasmine.stringMatching(/\?.*p3%3Dv4/), + ) + done() + }) + + Controller.redirect(req, res) + }) + it('should render index if getting fetchMetatags succeeds', (done) => { req.params = { Id: testForm._id,