Forms, validation and security
OpenCart does little for the programmer: there are no parameterised queries, no automatic escaping in templates, no form validator. Every value from a form has to be checked, protected before the database query and escaped on output — by hand. This lesson adds a callback form to the module and walks through each step.
The form
Add the form to the module template and its address to index() of the storefront controller:
// extension/acme_hello/catalog/controller/module/acme_hello.php, in index()
$data['send'] = $this->url->link('extension/acme_hello/module/acme_hello.send', 'language=' . $this->config->get('config_language'));{# extension/acme_hello/catalog/view/template/module/acme_hello.twig #}
<form id="form-callback" action="{{ send }}" method="post" data-oc-toggle="ajax">
<input type="text" name="name" id="input-name" placeholder="{{ entry_name }}" class="form-control mb-2">
<div id="error-name" class="invalid-feedback"></div>
<input type="tel" name="phone" id="input-phone" placeholder="{{ entry_phone }}" class="form-control mb-2">
<div id="error-phone" class="invalid-feedback"></div>
<button type="submit" class="btn btn-primary">{{ button_submit }}</button>
</form>The home page now shows two fields and a button inside the module card.
Validation
public function send(): void
{
$this->load->language('extension/acme_hello/module/acme_hello');
$json = [];
$name = trim($this->request->post['name'] ?? '');
$phone = preg_replace('/\D/', '', $this->request->post['phone'] ?? '');
if (mb_strlen($name) < 2 || mb_strlen($name) > 64) {
$json['error']['name'] = $this->language->get('error_name');
}
if (strlen($phone) < 10) {
$json['error']['phone'] = $this->language->get('error_phone');
}
if (!$json) {
$this->load->model('extension/acme_hello/module/acme_hello');
$this->model_extension_acme_hello_module_acme_hello->addCallback($name, $phone);
$json['success'] = $this->language->get('text_success');
}
$this->response->addHeader('Content-Type: application/json');
$this->response->setOutput(json_encode($json));
}Send the form empty — errors appear under both fields. With correct values the success message appears.
SQL injection
SQL injection is when a value from a form becomes part of the query and changes its meaning. The model protects every value:
// extension/acme_hello/catalog/model/module/acme_hello.php
namespace Opencart\Catalog\Model\Extension\AcmeHello\Module;
class AcmeHello extends \Opencart\System\Engine\Model
{
public function addCallback(string $name, string $phone): int
{
$this->db->query(
"INSERT INTO `" . DB_PREFIX . "acme_callback` SET
`name` = '" . $this->db->escape($name) . "',
`phone` = '" . $this->db->escape($phone) . "',
`customer_id` = '" . (int) $this->customer->getId() . "',
`date_added` = NOW()"
);
return $this->db->getLastId();
}
}The row appears in the table oc_acme_callback and in the list on the settings page of the module. A column name for sorting cannot be protected with escape() — it is compared with a fixed list of allowed names.
XSS
XSS is when text entered by one visitor is executed as a script in the browser of another. Strings from $this->request are already passed through htmlspecialchars() by the engine and are stored escaped. Data from other sources — an import file, JSON from an external service — is not, so it is escaped on output: {{ name|escape }}.
Request forgery
In the admin panel every request is protected by user_token. The storefront has no such protection: another site can send this form on behalf of a visitor. For an important form add your own token:
// in index()
$this->session->data['acme_hello_token'] ??= bin2hex(random_bytes(16));
$data['token'] = $this->session->data['acme_hello_token'];// in send(), before the other checks
if (!hash_equals($this->session->data['acme_hello_token'] ?? '', (string) ($this->request->post['token'] ?? ''))) {
$json['error'] = $this->language->get('error_token');
}The template sends it in a hidden field named token.
Common mistake
A value is escaped twice: the request already replaced & with &, then the template applies |escape again — and the page shows Tom & Anna. Escape a value once: request strings are already escaped, data from other sources is escaped on output.