Skip to content

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 before form().
  • The form name is CSRF because the view uses formName(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
}
        

Live Demo

/documentation/examples/run/forms2