Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
wants to merge 1 commit into
base: next
Choose a base branch
from

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.

None yet

1 participant