Skip to content

Contact Forms

Flow ID: CF-29 | Module(s): forms, Cms domain | Complexity: Medium | Last Updated: 2026-07-07

Business Overview

Config-driven dynamic contact forms with field-level customization, file attachments, CAPTCHA protection, and email delivery. Submissions are archived in the database and viewable in the admin panel. The system supports multiple form definitions from a single config file, allowing each site to define any number of contact forms with different field sets.

What customers experience:

  • Visit a language-specific contact URL (e.g., /epikoinonia in Greek, /contact in English)
  • Fill in a dynamically rendered form (text, email, textarea, select, radio, checkbox, checkbox-group, file upload fields)
  • CAPTCHA verification (Google reCAPTCHA v2 or CodeIgniter image captcha)
  • On success, see a confirmation page with the generated email content
  • Store owner receives an email with all submitted data and file attachments

Key business behaviors:

  • Forms are entirely config-driven: fields, validation rules, labels, and templates defined in contact_forms.php
  • Two rendering modes: hand-crafted HTML (contactForm view) and auto-generated from config (contactFormGenerated view)
  • File uploads stored in ../storage/forms/ with type/size validation per field
  • Submissions archived in contact_emails table with JSON metadata and rendered HTML
  • Email subject resolved from registry (EMAIL_SUBJECTS group, keyed by form name and language)
  • Sender email validation: submissions without a valid sender email are silently skipped (no email sent, no DB record)
  • If a logged-in customer submits, their customer_id is linked to the submission
  • Meta Conversions API integration: Contact event type available for Facebook pixel tracking
  • Product return form (returnForm): a second first-class form for product return requests (customer name, phone, email, order number, comment) at /forma-epistrofis (Greek only); always archives to contact_emails + always emails both the shop recipient and the customer (no allowContactEmail gate)

API Reference

REST Endpoints (Admin)

All submission types (contact form, return form, etc.) are archived in a single contact_emails table with the email_id field serving as a discriminator; return-form submissions have email_id='returnForm' and may be queried by filtering that field.

MethodPathAuthDescription
GET/rest/cms/contact-emailBackend (ADMIN, CMS)List submissions with filtering/sorting
GET/rest/cms/contact-email/{id}Backend (ADMIN, CMS)Get single submission
GET/rest/cms/contact-email/itemBackend (ADMIN, CMS)Get single submission by filters
POST/rest/cms/contact-emailBackend (ADMIN, CMS)Create submission record
POST/rest/cms/contact-email/{id}Backend (ADMIN, CMS)Update submission record
DELETE/rest/cms/contact-email/{id}Backend (ADMIN, CMS)Delete submission record

Filters: id (exact), email (partial), customerId (exact), emailId (exact) Sorts: id, emailDatetime, email, emailIdRelations: customer (belongs_to)

Legacy Storefront

URLController MethodDescription
/epikoinonia (Greek)Adv_forms::contactForm()Default contact form
/en/contact (English)Adv_forms::contactForm()Default contact form
/de/kontakt (German)Adv_forms::contactForm()Default contact form
/forma-epistrofis (Greek only)Adv_forms::returnForm()Product return-request form

Custom forms are accessed by passing a contactFormKey parameter: contactForm('fullSampleForm'). The return form is accessed via the dedicated route application/config/routes.php:709.

Admin Panel

URLControllerDescription
/contacts/listAdmin_contact_email_viewer::index()Paginated list with filters
/contacts/view/{id}Admin_contact_email_viewer::view()View rendered email in popup

Code Flow

Storefront Submission

File: ecommercen/forms/controllers/Adv_forms.php

  1. Load config: FormsConfig loads contact_forms.php and resolves the form definition by key (default: contactForm)
  2. Set validation: setContactValidation() iterates config fields, setting CI form_validation rules per field (skips upload type fields)
  3. Set CAPTCHA: setCaptchaValidationRule() adds CAPTCHA validation (trait-based: Google reCAPTCHA, CI image captcha, or no-op)
  4. Check required files: requiredFilesInPost() validates that required upload fields have files in $_FILES
  5. Upload files: uploadFiles() processes file uploads via AdvUploader to ../storage/forms/ with per-field type/size constraints
  6. Collect post data: getContactPostData() builds an associative array from POST values, handling text, email, textarea, select, radio, checkbox, and checkbox-group types
  7. Render email: Loads the email template view with form data, external language helper, config, and attachment paths
  8. Allow check: allowContactEmail() verifies the sender email field is present and non-empty (if configured)
  9. Send email: Adv_mailer::sendContactFormEmail() sends via SMTP using EMAIL_CONTACT_FORM registry credentials, with file attachments
  10. Archive: Insert into contact_emails table: email, customer_id (if logged in), form key as email_id, timestamp, JSON metadata, and rendered HTML
  11. Success view: Swap the form view for contactSuccess template showing the generated email content

Email Delivery

File: application/models/Adv_mailer.php method sendContactFormEmail()

  • Retrieves SMTP credentials from registry group EMAIL_CONTACT_FORM via getDefaultSmtpCredentials()
  • Sends to the admin email configured in that registry group
  • Attaches uploaded files from ../storage/forms/ directory

Admin Viewing

File: ecommercen/forms/controllers/AdvAdminContactEmailViewer.php

  • Paginated list (15 per page) with filters: email (LIKE), type/email_id (exact), date (LIKE)
  • Inline JSON metadata expansion per row
  • Popup window to view the original rendered HTML email
  • Customer name resolved from customer_model for linked submissions

Return Form Submission (returnForm)

File: ecommercen/forms/controllers/Adv_forms.php:98-164

The return form diverges from the generic contact form flow in several ways:

  1. No file uploads: unlike the generic contactForm(), there is no upload field type support; no call to uploadFiles() or requiredFilesInPost() (Adv_forms.php:98-164)
  2. No allowContactEmail gate: the return form always archives and always sends, bypassing the allowContactEmail() validation present in contactForm() (Adv_forms.php:118-126)
  3. Dual-recipient email: the form renders the email once (renderReturnFormEmail(), Adv_forms.php:116) before archiving (:118-126), then sends it twice — once to the shop recipient and once to the customer if their email is non-empty (Adv_forms.php:128-145)
  4. Render-once/send-twice: renderReturnFormEmail() is called once and the same HTML is sent to both recipients with different subject lines (Adv_forms.php:116,128-145)
  5. Localized success, no raw-body echo: the success page (returnFormSuccess template) shows only the localized t('return.form.confirm.sent',[$safeCustomerEmail]) message, not the raw email body (Adv_forms.php:146-148)

Domain Layer

ComponentPath
Entitysrc/Domains/Cms/ContactEmail/Repository/Entity.php
Repositorysrc/Domains/Cms/ContactEmail/Repository/Repository.php
RepositoryConfiguratorsrc/Domains/Cms/ContactEmail/Repository/RepositoryConfigurator.php
WriteRepositorysrc/Domains/Cms/ContactEmail/Repository/WriteRepository.php
Service (Read)src/Domains/Cms/ContactEmail/Service.php
WriteServicesrc/Domains/Cms/ContactEmail/WriteService.php
WriteDatasrc/Domains/Cms/ContactEmail/WriteData.php
Validatorsrc/Domains/Cms/ContactEmail/Validator.php
ListRequestsrc/Domains/Cms/ContactEmail/ListRequest.php
REST Controllersrc/Rest/Cms/Controllers/ContactEmail.php
Resourcesrc/Rest/Cms/Resources/ContactEmail/Resource.php
Collectionsrc/Rest/Cms/Resources/ContactEmail/Collection.php
Domain DIsrc/Domains/Cms/container.php
REST DIsrc/Rest/Cms/container.php

Architecture

Forms Config System

File: ecommercen/forms/libraries/AdvFormsConfig.php (extended by application/modules/forms/libraries/FormsConfig.php)

The FormsConfig class wraps application/config/contact_forms.php and provides accessor methods:

MethodPurpose
getContactFormKeys()All defined form keys
getContactFormKeysDropDown()Translated dropdown for admin filters
getContactFormConfig($key)Full config array for a form
getContactFormSubmitName($key)POST field name for submit detection
getFrontTemplateView($key)Template key for storefront layout
getEmailTemplateView($key)Template key for email body
getTranslatedSelectOptions($key, $field)Translated options for select/radio/checkbox-group
getRequiredFileKeys($key)Upload fields marked as required
getClientEmailFieldKey($key)Which field holds the sender's email
getPageTitleKey($key)Translation key for the page title

CAPTCHA Strategy

Three interchangeable traits (used on the front controller hierarchy):

TraitFileMechanism
CaptchaTraitecommercen/core/CaptchaTrait.phpNo-op (disabled)
GoogleRecaptchaTraitecommercen/core/GoogleRecaptchaTrait.phpGoogle reCAPTCHA v2 via registry keys GOOGLE.RECAPTCHA_KEY / GOOGLE.RECAPTCHA_SECRET
CodeigniterCaptchaTraitecommercen/core/CodeigniterCaptchaTrait.phpServer-generated image captcha using SimpleImage

The active trait is applied on the front controller base class. Only one is active at a time.

Template System

Two rendering strategies mapped in template.json / mainTemplate.json:

Template KeyViewDescription
contactFormlayouts/contact_form/contact_form.phpHand-crafted HTML for the default form
contactFormGeneratedlayouts/contact_form/contact_form_generated.phpAuto-generated from config (iterates fields by type)
contactSuccesslayouts/contact_form/contact_form_success.phpSuccess confirmation page
returnFormlayouts/contact_form/return_form.phpProduct return-request form (application/config/mainTemplate.json:317-319)
returnFormSuccesslayouts/contact_form/return_form_success.phpReturn form success page (application/config/mainTemplate.json:321-323)

Email templates in emailViews.json:

Template KeyView
contactEmailmail/contact_email.php
contactEmailGeneratedmail/contact_email_generated.php
returnEmailmail/return_form.php (application/config/emailViews.json:8-10)

The generated templates (contactFormGenerated / contactEmailGenerated) dynamically render all field types from config, while the standard templates (contactForm / contactEmail) have hardcoded field HTML for the default contact form. The return form uses dedicated templates returnForm / returnEmail.


Data Model

contact_emails

ColumnTypeDescription
idint(11) unsigned PK AISubmission ID
emailvarchar(255) nullableSender email address
customer_idint(11) nullableFK to customer if logged in
email_idvarchar(255) NOT NULLForm key identifier (e.g., contactForm)
email_datetimedatetime NOT NULLSubmission timestamp
meta_datalongtext nullableJSON-encoded form field values
generated_emaillongtext nullableRendered HTML email content

Indexes: customer_id, email, email_id, email_datetime, composite (email, email_id), composite (email, email_datetime), composite (email_id, email_datetime)


Configuration

Form Definition (application/config/contact_forms.php)

Each form is a keyed entry in $config['contactForms'] with this structure:

php
'formKey' => [
    'pageTitleKey' => 'translation.key',         // Page <title> translation key
    'fields' => [
        'fieldName' => [
            'type' => 'text|email|textarea|select|radio|checkbox|checkbox-group|upload',
            'labelKey' => 'translation.key',      // Field label translation key
            'validation' => 'trim|required|...',  // CI form_validation rules
            'options' => [...],                    // For select/radio/checkbox-group
            'upload' => [                          // For upload type
                'allowed_types' => 'pdf|png|jpg',
                'size' => 2048                     // Max KB
            ],
        ],
    ],
    'submit' => [
        'name' => 'postFieldName',               // POST key for submit detection
        'labelKey' => 'translation.key',
    ],
    'senderEmailField' => 'fieldName',           // Which field holds sender email
    'frontTemplateView' => 'contactForm',        // Template key for storefront
    'emailTemplateView' => 'contactEmail',       // Template key for notification email
]

Default form (contactForm): email, name, surname, phone, mobile, message Sample form (fullSampleForm): all field types including uploads, select, radio, checkbox-group Return form (returnForm): contactemail (required|valid_email), contactFullName (required), contacttel (trim), contactOrderNumber (required), contactmessage (required); front view returnForm, email view returnEmail (application/config/contact_forms.php:136-172)

Registry Keys

GroupKeyDescription
EMAIL_CONTACT_FORM(SMTP config)SMTP credentials for sending contact emails
EMAIL_SUBJECTS{formKey}Email subject line per form per language
GOOGLERECAPTCHA_KEYreCAPTCHA v2 site key (if Google trait active)
GOOGLERECAPTCHA_SECRETreCAPTCHA v2 secret key
TITLESITEFallback email subject (site name)
RETURN_FORMRECIPIENT_EMAILReturn form shop recipient email; falls back to EMAIL_CONTACT_FORM admin email (Adv_forms.php:128-131)
EMAIL_SUBJECTSreturnFormReturn form customer email subject; falls back to t('return.form.customer.subject') "Return request received" (Adv_forms.php:139-141)
EMAIL_SUBJECTSreturnFormRecipientReturn form shop recipient email subject; falls back to t('return.form.recipient.subject') "New return request" (Adv_forms.php:132-135)

REST Policies

php
ContactEmail::class => [
    'defaults' => ['auth' => 'backend', 'roles' => [AUTH_ROLE_ADMIN, AUTH_ROLE_CMS]],
],

Admin Menu

Listed under the CUSTOMERS group at route contacts/list.

Constants

php
const FORMS_ATTACHMENTS_PATH = '../storage/forms/';  // application/config/constants.php

Client Extension Points

  • Custom forms: Add entries to application/config/contact_forms.php with unique keys and views
  • FormsConfig override: application/modules/forms/libraries/FormsConfig.php extends AdvFormsConfig -- add custom methods or override existing ones
  • Controller override: Adv_forms can be extended in application/controllers/ to add pre/post-submission hooks via contactFormExtras()
  • CAPTCHA strategy: Switch the active captcha trait on the front controller (no-op, Google reCAPTCHA, or CI image captcha)
  • Email templates: Override contact_email.php / contact_email_generated.php in application/views/main/mail/
  • Form views: Override contact_form.php / contact_form_generated.php / contact_form_success.php in application/views/main/layouts/contact_form/
  • Template mapping: Override contactForm, contactFormGenerated, contactSuccess keys in mainTemplate.json
  • Custom routes: Add language-specific routes in application/config/my_routes.php pointing to forms/contactForm/{formKey}
  • Block sender: Override allowContactEmail() to implement email blocklists
  • Return form email rendering: Override protected renderReturnFormEmail(string $contactFormKey, array $data): string (Adv_forms.php:166-182) to customize the return form email body
  • Return form recipient: Configure the shop recipient email via the Notification Settings admin panel (cross-ref AD-13 Settings), which writes RETURN_FORM/RECIPIENT_EMAIL registry key (ecommercen/settings/controllers/Adv_settings.php:926,977-978)
  • Meta Conversions API: EventType::CONTACT available for Facebook pixel tracking of form submissions

Business Rules

RuleImplementation
Sender email required for archivalallowContactEmail() returns false if the configured senderEmailField is empty (generic form)
One email per submissionEmail sent to admin only (single EMAIL_CONTACT_FORM SMTP config) (generic form)
File uploads validated per-fieldallowed_types and size from config; AdvUploader enforces constraints
Required file uploads checked before processingrequiredFilesInPost() verifies required upload fields have files in $_FILES before upload attempt
CAPTCHA requiredEnforced at validation level; trait determines which implementation
Customer linkingIf session has customer_id, it is stored with the submission
Language-scoped URLsRoutes per language (Greek /epikoinonia, English /contact, etc.) with contactFormLinks() generating hreflang alternates
Email subject from registryEMAIL_SUBJECTS.{formKey} per language, falls back to site name
Return form always archivedReturn form bypasses allowContactEmail() gate; always persists to contact_emails and always sends (Adv_forms.php:118-126)
Return form dual-recipientAfter archival, return form email is sent to both the shop recipient and the customer if email is non-empty, using the same rendered body with per-recipient subjects (Adv_forms.php:128-145)
Return recipient fallbackReturn form shop recipient resolved from RETURN_FORM/RECIPIENT_EMAIL registry key, falls back to EMAIL_CONTACT_FORM admin email if unset (Adv_forms.php:128-131)

Known Issues & Security Gaps

  1. Unauthenticated, un-throttled write with inert CAPTCHA (ecommercen/core/CaptchaTrait.php:11-19, applied via use CaptchaTrait; at ecommercen/core/Adv_front_controller.php:23): The public return-form endpoint /forma-epistrofis accepts form submissions with CAPTCHA validation controlled by a trait seam that defaults to a no-op. The route has no built-in authentication, rate-limiting, or bot protection. This is a shared issue with contactForm and represents a potential spam/abuse vector. Mitigation: implement a CAPTCHA trait override (Google reCAPTCHA or CodeIgniter image captcha) or add a rate-limit seam.

  2. No empty-recipient guard (Adv_forms.php:128-131): If both RETURN_FORM/RECIPIENT_EMAIL registry key and the fallback EMAIL_CONTACT_FORM admin email are empty, sendReturnFormEmail() is invoked with an empty $toEmail parameter after the contact_emails row has already been persisted. The method will fail silently (no exception thrown, no return value checked), leaving an orphaned submission record in the database.

  3. Persist-before-send with hard SMTP dependency (Adv_forms.php:118-131): The submission is inserted into contact_emails before sending email. The fallback recipient resolution calls getDefaultSmtpCredentials('EMAIL_CONTACT_FORM'), which can throw an exception after the row is already committed if the SMTP profile is not configured. If email send fails, the submission remains archived with no indicator that notification failed.

  4. No block-list or rate-limit seam (Adv_forms.php:98-164): Unlike the generic contactForm(), the return form does not call allowContactEmail() and has no hook to block submissions by sender, domain, or frequency. Combined with the inert CAPTCHA and lack of authentication, this creates an unprotected write channel for repeat submissions.


See also the Contact Forms from Config guide for details on creating custom form definitions.