Secure Forms with dotapp.js
Open the live demo at /documentation/examples/run/forms2.
This example is the live Examples:Forms@index2! / @submit2! page. Security is in the kernel, not a plugin: select values are encrypted in the template. The browser posts with fo-rm, formName, CRC, and dotapp.js. The page does not reload.
Prerequisites
Start from the named-forms example so the Examples module and Forms controller already exist.
Creating the View
The live demo uses a standalone view at /app/modules/Examples/views/forms2.view.php. The file is a complete HTML document.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{{ var: $title }} — DotApp PHP Framework 2.0</title>
<link rel="stylesheet" href="/assets/modules/Examples/css/examples.css" />
</head>
<body class="ex-body">
<header class="ex-top">
<a href="{{ var: $docsUrl }}">Documentation</a>
<span>Secure form</span>
</header>
<main class="ex-main">
<h1>Secure form</h1>
<p class="ex-lead">Select values are encrypted in the template. The post uses <code>fo-rm</code>, CRC, and <code>dotapp.js</code>.</p>
<div id="error-message" class="ex-error" hide="hide"></div>
<div id="status" class="ex-status" hide="hide"></div>
<div id="noteWrap" class="ex-card">
<fo-rm method="POST" id="example2" action="{{ var: $postAction }}">
<label for="name">Name</label>
<input type="text" id="name" name="name" required />
<label for="category">Category</label>
<select id="category" name="category" required>
<option value="" disabled selected>Select a category</option>
<option value="{{ enc(additionalKey): "SupportVal" }}">Support</option>
<option value="{{ enc(additionalKey): "FeedbackVal" }}">Feedback</option>
<option value="{{ enc(additionalKey): "OtherVal" }}">Other</option>
</select>
<label for="foreach">Select created by foreach</label>
<select id="foreach" name="foreach" required>
<option value="" disabled selected>Select a category again</option>
{{ foreach $items as $item }}
<option value="{{ enc(additionalKey2): $item['value'] }}">{{ var: $item['text'] }}</option>
{{ /foreach }}
</select>
{{ formName(CSRF) }}
<button type="submit" id="noteBtn" class="ex-btn">Submit</button>
</fo-rm>
<div class="output ex-result">Submit the form to see the result</div>
</div>
</main>
<script src="{{ var: $dotappJs }}"></script>
<script src="/assets/modules/Examples/js/forms2.js"></script>
</body>
</html>
Put {{ formName(CSRF) }} between the <fo-rm> tags. Set action to the current path (the live demo uses $request->getPath()). Pass the same path as the last argument of form() so the handler matches. Load /assets/dotapp/dotapp.js with a unique query on each page render so the session CSRF token is not reused from a cached docs page. Encrypted option values use {{ enc(additionalKey): "SupportVal" }}.
Adding Styles
Shared example chrome lives at /app/modules/Examples/assets/css/examples.css and is served as /assets/modules/Examples/css/examples.css.
Creating the Layout
setView('forms2') renders the full HTML document. Modules can also wrap views with {{ layout:name }}. This demo keeps the view self-contained.
Configuring Routes
Router::get($pair($p . '/forms2'), 'Examples:Forms@index2!', Router::STATIC_ROUTE);
Router::post($pair($p . '/forms2'), 'Examples:Forms@submit2!', Router::STATIC_ROUTE);
Register those lines in initialize($dotApp) next to the named-forms routes. $pair returns both slash variants.
Updating the Controller
Methods are public static and take $request only. Use Renderer::new(). When the callable string ends with !, the method does not receive injected services as extra arguments.
public static function index2($request)
{
$items = [
['value' => 'ValueItem1', 'text' => 'Text item 1'],
['value' => 'ValueItem2', 'text' => 'Text item 2'],
['value' => 'ValueItem3', 'text' => 'Text item 3'],
];
return self::view('forms2', [
'title' => 'Secure form demo',
'docsUrl' => '/documentation/examples/secure-forms',
'items' => $items,
]);
}
public static function submit2($request)
{
$answer = ['code' => 403, 'body' => ['status' => 0, 'error' => 1, 'error_txt' => 'CRC check failed!', 'message' => 'CRC check failed!']];
if ($request->crcCheck()) {
$answer = $request->form(['POST'], 'CSRF', function ($request) {
$payload = $request->data(true)['data'] ?? [];
$categoryVal = Crypto::decrypt((string) ($payload['category'] ?? ''), 'additionalKey');
$foreachVal = Crypto::decrypt((string) ($payload['foreach'] ?? ''), 'additionalKey2');
if ($categoryVal === false || $foreachVal === false) {
return [
'code' => 403,
'body' => ['status' => 0, 'message' => 'Data manipulation detected!'],
];
}
return [
'code' => 200,
'body' => [
'status' => 1,
'text' => 'Category: ' . $categoryVal . ', Foreach: ' . $foreachVal,
'message' => 'Form submitted successfully.',
],
];
}, function () {
return ['code' => 403, 'body' => ['status' => 0, 'message' => 'Invalid signature']];
}, $request->getPath());
}
if (!is_array($answer) || !isset($answer['body'])) {
return DotApp::DotApp()->ajaxReply(['status' => 0, 'error' => 1, 'error_txt' => 'Invalid signature', 'message' => 'Invalid signature'], 403);
}
return DotApp::DotApp()->ajaxReply($answer['body'], $answer['code']);
}
crcCheck()runs beforeform().- The form name is
CSRFbecause the view usesformName(CSRF). - The error callback on
form()is required. - The last argument of
form()is$request->getPath()so the encrypted handler matches the POST URL. - AJAX replies go through
DotApp::DotApp()->ajaxReply($body, $code).
Implementing JavaScript
Live file: /app/modules/Examples/assets/js/forms2.js. The client API is $dotapp. Wait for the dotapp event if the library is still loading.
(function () {
var runMe = function ($dotapp) {
$dotapp()
.form("#example2")
.before(function (data, form) {
if ($dotapp(form).attr("blocked") == 1) return $dotapp().halt();
$dotapp(form).attr("blocked", "1");
$dotapp("#noteBtn").attr("loading", "true").attr("loader", "dots");
})
.after(function (data, response, form) {
var reply = $dotapp().parseReply(response);
if (reply && (reply.status == 1 || reply.error == 0)) {
$dotapp(".output").html("Form submitted successfully!<br>" + (reply.text || reply.message || ""));
} else if (reply && (reply.error_txt || reply.message)) {
$dotapp("#error-message").attr("hide", "false").html(reply.error_txt || reply.message);
}
$dotapp(form).attr("blocked", "0");
})
.onError(function (data, status, error, form) {
var reply = $dotapp().parseReply(error);
$dotapp("#error-message").attr("hide", "false").html((reply && reply.message) || "Request failed");
$dotapp(form).attr("blocked", "0");
});
};
if (window.$dotapp) runMe(window.$dotapp);
else window.addEventListener("dotapp", function () { runMe(window.$dotapp); }, { once: true });
})();
Using the Crypto Facade
Template encryption and PHP decryption must use the same extra key. Failed decryption returns false — compare with === false, never treat it as a string.
$categoryVal = Crypto::decrypt((string) ($payload['category'] ?? ''), 'additionalKey');
if ($categoryVal === false) {
// tampered or wrong key
}