Appearance
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.,
/epikoinoniain Greek,/contactin 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 (
contactFormview) and auto-generated from config (contactFormGeneratedview) - File uploads stored in
../storage/forms/with type/size validation per field - Submissions archived in
contact_emailstable with JSON metadata and rendered HTML - Email subject resolved from registry (
EMAIL_SUBJECTSgroup, 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_idis linked to the submission - Meta Conversions API integration:
Contactevent 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 tocontact_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.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /rest/cms/contact-email | Backend (ADMIN, CMS) | List submissions with filtering/sorting |
| GET | /rest/cms/contact-email/{id} | Backend (ADMIN, CMS) | Get single submission |
| GET | /rest/cms/contact-email/item | Backend (ADMIN, CMS) | Get single submission by filters |
| POST | /rest/cms/contact-email | Backend (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
| URL | Controller Method | Description |
|---|---|---|
/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
| URL | Controller | Description |
|---|---|---|
/contacts/list | Admin_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
- Load config:
FormsConfigloadscontact_forms.phpand resolves the form definition by key (default:contactForm) - Set validation:
setContactValidation()iterates config fields, setting CIform_validationrules per field (skipsuploadtype fields) - Set CAPTCHA:
setCaptchaValidationRule()adds CAPTCHA validation (trait-based: Google reCAPTCHA, CI image captcha, or no-op) - Check required files:
requiredFilesInPost()validates that requireduploadfields have files in$_FILES - Upload files:
uploadFiles()processes file uploads viaAdvUploaderto../storage/forms/with per-field type/size constraints - Collect post data:
getContactPostData()builds an associative array from POST values, handlingtext,email,textarea,select,radio,checkbox, andcheckbox-grouptypes - Render email: Loads the email template view with form data, external language helper, config, and attachment paths
- Allow check:
allowContactEmail()verifies the sender email field is present and non-empty (if configured) - Send email:
Adv_mailer::sendContactFormEmail()sends via SMTP usingEMAIL_CONTACT_FORMregistry credentials, with file attachments - Archive: Insert into
contact_emailstable: email, customer_id (if logged in), form key asemail_id, timestamp, JSON metadata, and rendered HTML - Success view: Swap the form view for
contactSuccesstemplate showing the generated email content
Email Delivery
File: application/models/Adv_mailer.php method sendContactFormEmail()
- Retrieves SMTP credentials from registry group
EMAIL_CONTACT_FORMviagetDefaultSmtpCredentials() - 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_modelfor 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:
- No file uploads: unlike the generic
contactForm(), there is nouploadfield type support; no call touploadFiles()orrequiredFilesInPost()(Adv_forms.php:98-164) - No allowContactEmail gate: the return form always archives and always sends, bypassing the
allowContactEmail()validation present incontactForm()(Adv_forms.php:118-126) - 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) - 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) - Localized success, no raw-body echo: the success page (
returnFormSuccesstemplate) shows only the localizedt('return.form.confirm.sent',[$safeCustomerEmail])message, not the raw email body (Adv_forms.php:146-148)
Domain Layer
| Component | Path |
|---|---|
| Entity | src/Domains/Cms/ContactEmail/Repository/Entity.php |
| Repository | src/Domains/Cms/ContactEmail/Repository/Repository.php |
| RepositoryConfigurator | src/Domains/Cms/ContactEmail/Repository/RepositoryConfigurator.php |
| WriteRepository | src/Domains/Cms/ContactEmail/Repository/WriteRepository.php |
| Service (Read) | src/Domains/Cms/ContactEmail/Service.php |
| WriteService | src/Domains/Cms/ContactEmail/WriteService.php |
| WriteData | src/Domains/Cms/ContactEmail/WriteData.php |
| Validator | src/Domains/Cms/ContactEmail/Validator.php |
| ListRequest | src/Domains/Cms/ContactEmail/ListRequest.php |
| REST Controller | src/Rest/Cms/Controllers/ContactEmail.php |
| Resource | src/Rest/Cms/Resources/ContactEmail/Resource.php |
| Collection | src/Rest/Cms/Resources/ContactEmail/Collection.php |
| Domain DI | src/Domains/Cms/container.php |
| REST DI | src/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:
| Method | Purpose |
|---|---|
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):
| Trait | File | Mechanism |
|---|---|---|
CaptchaTrait | ecommercen/core/CaptchaTrait.php | No-op (disabled) |
GoogleRecaptchaTrait | ecommercen/core/GoogleRecaptchaTrait.php | Google reCAPTCHA v2 via registry keys GOOGLE.RECAPTCHA_KEY / GOOGLE.RECAPTCHA_SECRET |
CodeigniterCaptchaTrait | ecommercen/core/CodeigniterCaptchaTrait.php | Server-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 Key | View | Description |
|---|---|---|
contactForm | layouts/contact_form/contact_form.php | Hand-crafted HTML for the default form |
contactFormGenerated | layouts/contact_form/contact_form_generated.php | Auto-generated from config (iterates fields by type) |
contactSuccess | layouts/contact_form/contact_form_success.php | Success confirmation page |
returnForm | layouts/contact_form/return_form.php | Product return-request form (application/config/mainTemplate.json:317-319) |
returnFormSuccess | layouts/contact_form/return_form_success.php | Return form success page (application/config/mainTemplate.json:321-323) |
Email templates in emailViews.json:
| Template Key | View |
|---|---|
contactEmail | mail/contact_email.php |
contactEmailGenerated | mail/contact_email_generated.php |
returnEmail | mail/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
| Column | Type | Description |
|---|---|---|
id | int(11) unsigned PK AI | Submission ID |
email | varchar(255) nullable | Sender email address |
customer_id | int(11) nullable | FK to customer if logged in |
email_id | varchar(255) NOT NULL | Form key identifier (e.g., contactForm) |
email_datetime | datetime NOT NULL | Submission timestamp |
meta_data | longtext nullable | JSON-encoded form field values |
generated_email | longtext nullable | Rendered 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
| Group | Key | Description |
|---|---|---|
EMAIL_CONTACT_FORM | (SMTP config) | SMTP credentials for sending contact emails |
EMAIL_SUBJECTS | {formKey} | Email subject line per form per language |
GOOGLE | RECAPTCHA_KEY | reCAPTCHA v2 site key (if Google trait active) |
GOOGLE | RECAPTCHA_SECRET | reCAPTCHA v2 secret key |
TITLE | SITE | Fallback email subject (site name) |
RETURN_FORM | RECIPIENT_EMAIL | Return form shop recipient email; falls back to EMAIL_CONTACT_FORM admin email (Adv_forms.php:128-131) |
EMAIL_SUBJECTS | returnForm | Return form customer email subject; falls back to t('return.form.customer.subject') "Return request received" (Adv_forms.php:139-141) |
EMAIL_SUBJECTS | returnFormRecipient | Return 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.phpClient Extension Points
- Custom forms: Add entries to
application/config/contact_forms.phpwith unique keys and views - FormsConfig override:
application/modules/forms/libraries/FormsConfig.phpextendsAdvFormsConfig-- add custom methods or override existing ones - Controller override:
Adv_formscan be extended inapplication/controllers/to add pre/post-submission hooks viacontactFormExtras() - 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.phpinapplication/views/main/mail/ - Form views: Override
contact_form.php/contact_form_generated.php/contact_form_success.phpinapplication/views/main/layouts/contact_form/ - Template mapping: Override
contactForm,contactFormGenerated,contactSuccesskeys inmainTemplate.json - Custom routes: Add language-specific routes in
application/config/my_routes.phppointing toforms/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_EMAILregistry key (ecommercen/settings/controllers/Adv_settings.php:926,977-978) - Meta Conversions API:
EventType::CONTACTavailable for Facebook pixel tracking of form submissions
Business Rules
| Rule | Implementation |
|---|---|
| Sender email required for archival | allowContactEmail() returns false if the configured senderEmailField is empty (generic form) |
| One email per submission | Email sent to admin only (single EMAIL_CONTACT_FORM SMTP config) (generic form) |
| File uploads validated per-field | allowed_types and size from config; AdvUploader enforces constraints |
| Required file uploads checked before processing | requiredFilesInPost() verifies required upload fields have files in $_FILES before upload attempt |
| CAPTCHA required | Enforced at validation level; trait determines which implementation |
| Customer linking | If session has customer_id, it is stored with the submission |
| Language-scoped URLs | Routes per language (Greek /epikoinonia, English /contact, etc.) with contactFormLinks() generating hreflang alternates |
| Email subject from registry | EMAIL_SUBJECTS.{formKey} per language, falls back to site name |
| Return form always archived | Return form bypasses allowContactEmail() gate; always persists to contact_emails and always sends (Adv_forms.php:118-126) |
| Return form dual-recipient | After 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 fallback | Return 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
Unauthenticated, un-throttled write with inert CAPTCHA (
ecommercen/core/CaptchaTrait.php:11-19, applied viause CaptchaTrait;atecommercen/core/Adv_front_controller.php:23): The public return-form endpoint/forma-epistrofisaccepts 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 withcontactFormand represents a potential spam/abuse vector. Mitigation: implement a CAPTCHA trait override (Google reCAPTCHA or CodeIgniter image captcha) or add a rate-limit seam.No empty-recipient guard (
Adv_forms.php:128-131): If bothRETURN_FORM/RECIPIENT_EMAILregistry key and the fallbackEMAIL_CONTACT_FORMadmin email are empty,sendReturnFormEmail()is invoked with an empty$toEmailparameter after thecontact_emailsrow has already been persisted. The method will fail silently (no exception thrown, no return value checked), leaving an orphaned submission record in the database.Persist-before-send with hard SMTP dependency (
Adv_forms.php:118-131): The submission is inserted intocontact_emailsbefore sending email. The fallback recipient resolution callsgetDefaultSmtpCredentials('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.No block-list or rate-limit seam (
Adv_forms.php:98-164): Unlike the genericcontactForm(), the return form does not callallowContactEmail()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.
Related Flows
- CF-10 Customer Auth -- customer_id linking for logged-in submissions
- CF-30 Cookie Consent -- CAPTCHA strategy shared via front controller trait hierarchy
- SY-24 Email Dispatch System --
Adv_mailer::sendContactFormEmail()SMTP routing and file attachment handling - SY-25 File Upload & Storage --
AdvUploaderprocesses per-field upload constraints and stores files understorage/forms/ - AD-53 Email Template Viewer -- admin preview of
contact_email,contact_email_generated, andreturn_formtemplates
See also the Contact Forms from Config guide for details on creating custom form definitions.