Skip to content

Delegate forms $data validation to Forms::submit#1411

Open
Raruto wants to merge 1 commit intoagentejo:nextfrom
Raruto:forms-submit-delegation
Open

Delegate forms $data validation to Forms::submit#1411
Raruto wants to merge 1 commit intoagentejo:nextfrom
Raruto:forms-submit-delegation

Conversation

@Raruto
Copy link
Contributor

@Raruto Raruto commented Jan 27, 2021

Preamble

This pull request doesn't alter current forms submission behavior (in essence it ensures that the "forms.submit.before" event is always triggered for each form submission).

A borderline example is shown below.


Issue description:

When using a form call like the following:

<form action="/api/forms/submit/contact" method="post" enctype="multipart/form-data">
  <input name="files[]" type="file">
  <input name="submit" type="submit" value="Submit">
</form>

empty $_POST data is passed to Forms\Controller\RestApi::submit causing an early exit from that function and without being so able to parse $_FILES data inside the "forms.submit.before" hook

if ($data = $this->param('form', false)) {
$options = [];
if ($this->param('__mailsubject')) {
$options['subject'] = $this->param('__mailsubject');
}
return $this->module('forms')->submit($form, $data, $options);
}
return false;

Proposed solution:

Set fallback param $data to empty array and delegate empty check validation to Forms::submit function.

// Forms\Controller\RestApi::submit

$data    = $this->param('form', []);
$options = $this->param('form_options', []);

return $this->module('forms')->submit($form, $data, $options);
// Forms::submit

$this->app->trigger('forms.submit.before', [$form, &$data, $frm, &$options]); // <-- parse here form $data request

if (empty($data)) {
  return false;
}

Example of usage:

Dynamically check and populate forms $data['files'] entry:

// config/bootstrap.php

$app->on('forms.submit.before', function($form, &$data, $frm, &$options) use ($app) {

  // see "Helper Functions" for more info about it
  $files = get_uploaded_files()

  if (!empty($files)) {
    $files = $app->module('cockpit')->uploadAssets('files', ['folder' => get_forms_uploads_folder()]);
    $data['files'] = $files['uploaded']; // <-- save entries as filename
  }

});
Contact Form

Simple contact form template with single input files[]:

contact

Lexy template:

@form( 'contact', [ 'id' => 'contact-form', 'class'=>'contact-form' ] )
  <fieldset>
    <legend>@lang('Contact us'):</legend>
    <div>
      <label for="name">
        @lang('Name') <span title="@lang('required')">*</span>
      </label>
      <input type="text" name="form[name]" id="name" placeholder="" onblur="this.value= this.value.toLowerCase().replace(/\b\w/g, function(l){ return l.toUpperCase() })" required>
    </div>
    <div>
      <label for="email">
        @lang('E-mail') <span title="@lang('required')">*</span>
      </label>
      <input type="email" name="form[email]" id="email" placeholder="" pattern="[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$" onblur="this.value = this.value.toLowerCase()" required>
    </div>
    <div>
      <label for="phone">
        @lang('Phone')
      </label>
      <input type="tel" name="form[phone]" id="phone" placeholder="" pattern="[+]{0,1}[0-9]{9,}">
    </div>
    <div>
      <label for="message">
        @lang('Message') <span title="@lang('required')">*</span>
      </label>
      <textarea name="form[message]" id="message" placeholder="" rows="5" required></textarea>
    </div>
    <div>
      <label for="files">
        @lang('File')
      </label>
      <input type="hidden" name="MAX_FILE_SIZE" value="100000">
      <input name="files[]" type="file">
    </div>
    <p>
      <input type="checkbox" name="form[privacy]" id="privacy" required>
      <label for="privacy">
        {{ $app("i18n")->getstr('I accept the <a href="%s">privacy policy</a> and I give consent to processing of this data as established by the <a href="%s">GDPR</a>', [ $app['base_route'] . '/privacy-policy/', 'https://gdpr.eu/' ] ); }} <span title="required">*</span>
      </label>
    </p>
    <div>
      <input name="submit" type="submit" value="@lang('Submit')">
    </div>
    <p class="form-message-success" style="display: none;">
      @lang('Thank You! I\'ll get back to you real soon...')
    </p>
  </fieldset>
@endform
Upload Form

Simple contact form template with multiple input files[] :

upload

Lexy template:

@form( 'upload', [ 'id' => 'upload-form', 'class'=>'upload-form' ] )
  <fieldset>
    <legend>@lang('Upload some files'):</legend>
    </div>
      <input name="files[]" type="file">
      <input name="files[]" type="file">
    </div>
    <div>
      <input name="submit" type="submit" value="@lang('Submit')">
    </div>
    <p class="form-message-success" style="display: none;">
      @lang('Thank You! I\'ll get back to you real soon...')
    </p>
  </fieldset>
@endform

Helper Functions

config/bootstrap.php
/**
 * Check and retrieve forms uploaded files
 *
 * @return array $data
 */
function get_uploaded_files() {
    $app    = cockpit();

    $files  = $app->param('files', [], $_FILES);
    $data   = [];

    if (isset($files['name']) && is_array($files['name'])) {
      for ($i = 0; $i < count($files['name']); $i++) {
        if (is_uploaded_file($files['tmp_name'][$i]) && !$files['error'][$i]) {
            foreach($files as $k => $v) {
                $data['files'][$k]   = $data['files'][$k] ?? [];
                $data['files'][$k][] = $files[$k][$i];
            }
        }
      }
    }

    return $data;
}

/**
 * Check and retrieve forms upload folder
 *
 * @return array $folder
 */
function get_forms_uploads_folder() {
    $app    = cockpit();

    $name   = 'forms_uploads';
    $parent = '';
    $folder = $app->storage->findOne('cockpit/assets_folders', ['name'=>$name, '_p'=>$parent]);

    if (empty($folder)) {
      $user   = $app->storage->findOne('cockpit/accounts', ['group'=>'admin'], ['_id' => 1]);
      $meta   = [
          'name' => $name,
          '_p'   => $parent,
          '_by'  => $user['_id'] ?? '',
      ];
      $folder = $app->storage->save('cockpit/assets_folders', $meta);
    }

    return $folder;
}

/**
 * Check and populate forms $data['files'] entry
 */
$app->on('forms.submit.before', function($form, &$data, $frm, &$options) use ($app) {

    $files = get_uploaded_files();

    if (!empty($files)) {

        $files = $app->module('cockpit')->uploadAssets('files', ['folder' => get_forms_uploads_folder()]);

        $data['files'] = [];
        $ASSETS_URL    = rtrim($app->filestorage->getUrl('assets://'), '/');

        // save entries as filename
        // $data['files'] = $files['uploaded'];

        // save entries as absolute urls
        foreach($files['assets'] as $file) {
          $data['files'][] = $ASSETS_URL.$file['path'];
        }

    }

});

End notes

The Contact Form example reported above does not suffer from this problem because the variable $_POST is still populated by other input fields.

The Upload Form (with just input $_FILES) is purely demonstrative. It can be solved somehow by hooking the assets api, however, it would be nice to be able to do it with the same hook (regardless of the number of parameters and without having to define custom rest api endpoints...)

Hoping that's clear enough,
Raruto

raffaelj added a commit to raffaelj/cockpit_FormValidation that referenced this pull request Dec 2, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant