UNPKG

@apostrophecms/form

Version:

Build forms for ApostropheCMS in a simple user interface.

345 lines (309 loc) 9.51 kB
const fs = require('fs'); const path = require('path'); const connectMultiparty = require('connect-multiparty'); const fields = require('./lib/fields'); const recaptcha = require('./lib/recaptcha'); const processor = require('./lib/processor'); module.exports = { extend: '@apostrophecms/piece-type', options: { label: 'aposForm:form', pluralLabel: 'aposForm:forms', quickCreate: true, seoFields: false, openGraph: false, i18n: { ns: 'aposForm', browser: true }, shortcut: 'G,O' }, bundle: { directory: 'modules', modules: getBundleModuleNames() }, fields (self) { let add = fields.initial(self.options); if (self.options.emailSubmissions !== false) { add = { ...add, ...fields.emailFields }; } const group = { basics: { label: 'aposForm:groupForm', fields: [ 'contents' ] }, afterSubmit: { label: 'aposForm:groupAfterSubmission', fields: [ 'thankYouHeading', 'thankYouBody', 'sendConfirmationEmail', 'emailConfirmationField' ] .concat( self.options.emailSubmissions !== false ? [ 'emails', 'email' ] : [] ) }, advanced: { label: 'aposForm:groupAdvanced', fields: [ 'submitLabel', 'enableRecaptcha', 'enableQueryParams', 'queryParamList' ] } }; return { add, group }; }, init (self) { self.ensureCollection(); self.cleanOptions(self.options); }, methods (self) { return { ...recaptcha(self), ...processor(self), async ensureCollection () { self.db = self.apos.db.collection('aposFormSubmissions'); await self.db.ensureIndex({ formId: 1, createdAt: 1 }); await self.db.ensureIndex({ formId: 1, createdAt: -1 }); }, processQueryParams (form, input, output, fieldNames) { if (!input.queryParams || (typeof input.queryParams !== 'object')) { output.queryParams = null; return; } if (Array.isArray(form.queryParamList) && form.queryParamList.length > 0) { form.queryParamList.forEach(param => { // Skip if this is an existing field submitted by the form. This value // capture will be done by populating the form inputs client-side. if (fieldNames.includes(param.key)) { return; } const value = input.queryParams[param.key]; if (value) { output[param.key] = self.tidyParamValue(param, value); } else { output[param.key] = null; } }); } }, tidyParamValue(param, value) { value = self.apos.launder.string(value); if (param.lengthLimit && param.lengthLimit > 0) { value = value.substring(0, (param.lengthLimit)); } return value; }, async sendEmailSubmissions (req, form, data) { if (self.options.emailSubmissions === false || !form.emails || form.emails.length === 0) { return; } let emails = []; form.emails.forEach(mailRule => { if (!mailRule.conditions || mailRule.conditions.length === 0) { emails.push(mailRule.email); return; } let passed = true; mailRule.conditions.forEach(condition => { if (!condition.value) { return; } let answer = data[condition.field]; if (!answer) { passed = false; } else { // Regex for comma-separation from https://stackoverflow.com/questions/11456850/split-a-string-by-commas-but-ignore-commas-within-double-quotes-using-javascript/11457952#comment56094979_11457952 const regex = /(".*?"|[^",]+)(?=\s*,|\s*$)/g; let acceptable = condition.value.match(regex); acceptable = acceptable.map(value => { // Remove leading/trailing white space and bounding double-quotes. value = value.trim(); if (value[0] === '"' && value[value.length - 1] === '"') { value = value.slice(1, -1); } return value.trim(); }); // If the value is stored as a string, convert to an array for checking. if (!Array.isArray(answer)) { answer = [ answer ]; } if (!(answer.some(val => acceptable.includes(val)))) { passed = false; } } }); if (passed === true) { emails.push(mailRule.email); } }); // Get array of email addresses without duplicates. emails = [ ...new Set(emails) ]; if (self.options.testing) { return emails; } if (emails.length === 0) { return null; } for (const key in data) { // Add some space to array lists. if (Array.isArray(data[key])) { data[key] = data[key].join(', '); } } try { const emailOptions = { form, data, to: emails.join(',') }; await self.sendEmail(req, 'emailSubmission', emailOptions); return null; } catch (err) { self.apos.util.error('⚠️ @apostrophecms/form submission email notification error: ', err); return null; } }, // Should be handled async. Options are: form, data, from, to and subject async sendEmail (req, emailTemplate, options) { const form = options.form; const data = options.data; return self.email( req, emailTemplate, { form, input: data }, { from: options.from || form.email, to: options.to, subject: options.subject || form.title } ); } }; }, helpers (self) { return { prependIfPrefix(str) { if (self.options.classPrefix) { return `${self.options.classPrefix}${str}`; } return ''; } }; }, apiRoutes (self) { return { post: { // Route to accept the submitted form. submit: [ connectMultiparty(), async function (req) { try { await self.submitForm(req); } finally { for (const file of (Object.values(req.files || {}))) { try { fs.unlinkSync(file.path); } catch (e) { self.apos.util.warn(req.t('aposForm:fileMissingEarly', { path: file })); } } } } ] } }; }, handlers (self) { return { submission: { async saveSubmission (req, form, data) { if (self.options.saveSubmissions === false) { return; } const submission = { createdAt: new Date(), formId: form._id, data }; await self.emit('beforeSaveSubmission', req, { form, data, submission }); return self.db.insertOne(submission); }, async emailSubmission (req, form, data) { await self.sendEmailSubmissions(req, form, data); }, async emailConfirmation (req, form, data) { if (form.sendConfirmationEmail !== true || !form.emailConfirmationField) { return; } // Email validation (Regex reference: https://stackoverflow.com/questions/46155/how-to-validate-an-email-address-in-javascript) const re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; if ( data[form.emailConfirmationField] && (typeof data[form.emailConfirmationField] !== 'string' || !re.test(data[form.emailConfirmationField])) ) { await self.apos.notify(req, 'aposForm:errorEmailConfirm', { type: 'warning', icon: 'alert-circle-icon', interpolate: { field: form.emailConfirmationField } }); return null; } try { const emailOptions = { form, data, to: data[form.emailConfirmationField] }; await self.sendEmail(req, 'emailConfirmation', emailOptions); return null; } catch (err) { self.apos.util.error('⚠️ @apostrophecms/form submission email confirmation error: ', err); return null; } } } }; } }; function getBundleModuleNames() { const source = path.join(__dirname, './modules/@apostrophecms'); return fs .readdirSync(source, { withFileTypes: true }) .filter(dirent => dirent.isDirectory()) .map(dirent => `@apostrophecms/${dirent.name}`); }