How to validate max post size on custom form - drupal

I have a custom module with a multiple managed file upload.
$form['attachments'] = [
'#type' => 'managed_file',
'#multiple' => true,
'#upload_validators' => [
'file_validate_extensions' => ['jpg docx pdf xlsx jpeg png gif'],
'file_validate_size' => [10485760],
],
];
When uploading one file that is more then 10MB it gives via AJAX the correct error that a file can not be larger then 10MB. That works out of the box.
How can I limit the total MB of all files that can be uploaded via this form?
For example:
3 files of 3MB = fine
4 files of 3MB = error.
I have managed to show a message when that happens
public function validateForm(array &$form, FormStateInterface $form_state) {
$max_size = 10485760;
$total_size = 0;
$triggered_element = $form_state->getTriggeringElement();
if($triggered_element['#name'] == 'attachments_upload_button') {
$fids = (array) $form_state->getValue('attachments', []);
if(!empty($fids)) {
$files = File::loadMultiple($fids);
foreach ($files as $key => $uploadedFile) {
$total_size += $uploadedFile->getSize();
if($total_size > $max_size) {
$form_state->setErrorByName('attachments', $this->t('The total maximum size of your file sizes can not be more than 10MB.'));
$form_state->set('attachments',array_pop($fids));
return;
}
}
}
}
}
But I can not seem to remove the last uploaded file. It is still there and it is submitted on form submit. A part from the message, the code does not hold the form from submitting.
I want the last submitted file where the total of all files is > 10M to be removed from the form_state and also the tmp server folder. And ideally via AJAX without loss of field input.
I can't find a solution.
Thanks in advance.
How can I achieve this via ajax.
Ok, turns out it was not that difficult.
With upload validators it is possible to add variables. So I added $form_state as a variable to a custom upload validator.
$form['attachments'] = [
'#type' => 'managed_file',
'#multiple' => true,
'#upload_validators' => [
'file_validate_extensions' => ['jpg docx pdf xlsx jpeg png gif'],
'file_validate_size' => [10485760],
'size_max_upload' => [$form_state],
],
];
And wrote a custom validator function
/**
* Validate maximum size upload.
*
* #param \Drupal\file\FileInterface $file
* File object.
*
* #param \Drupal\Core\Form\FormStateInterface $form_state
* Form state object.
*
* #return array
* Errors array.
*/
function size_max_upload(File $file, &$form_state) {
$errors = [];
$max_size = 10485760;
$total_size = $file->getSize();
$triggered_element = $form_state->getTriggeringElement();
if($triggered_element['#name'] == 'attachments_upload_button') {
$fids = (array) $form_state->getValue('attachments', []);
if(!empty($fids)) {
$files = File::loadMultiple($fids);
foreach ($files as $key => $uploadedFile) {
$total_size += $uploadedFile->getSize();
if($total_size > $max_size) {
$errors[] = t("The total maximum size of your file sizes can not be more than 10MB.");
break;
}
}
}
}
return $errors;
}

Related

After submitting the form, providing a file, I get the error: "Field is required"

I have this form: https://greektoenglish.com/translation
After I complete the form, provide it with a file, and finally submit it, I get this error: "field is required". That the file field is required. But I already completed the field.
If I remove "'#required' => TRUE," from the code where the file upload field is declared, fill the form out, and submit it, then the form is submitted correctly.
How can I solve this?
This is my code:
<?php
namespace Drupal\submit_translation\Form;
use Drupal\Component\Utility\EmailValidatorInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Mail\MailManagerInterface;
use Drupal\mimemail\Utility\MimeMailFormatHelper;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* The example email contact form.
*/
class SubmitTranslation extends FormBase {
/**
* The email.validator service.
*
* #var \Drupal\Component\Utility\EmailValidatorInterface
*/
protected $emailValidator;
/**
* The language manager service.
*
* #var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* The mail manager service.
*
* #var \Drupal\Core\Mail\MailManagerInterface
*/
protected $mailManager;
/**
* Constructs a new ExampleForm.
*
* #param \Drupal\Component\Utility\EmailValidatorInterface $email_validator
* The email validator service.
* #param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager service.
* #param \Drupal\Core\Mail\MailManagerInterface $mail_manager
* The mail manager service.
*/
public function __construct(EmailValidatorInterface $email_validator, LanguageManagerInterface $language_manager, MailManagerInterface $mail_manager) {
$this->emailValidator = $email_validator;
$this->languageManager = $language_manager;
$this->mailManager = $mail_manager;
}
/**
* {#inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('email.validator'),
$container->get('language_manager'),
$container->get('plugin.manager.mail')
);
}
/**
* {#inheritdoc}
*/
public function getFormId() {
return 'submit_translation_form';
}
/**
* {#inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $dir = NULL, $img = NULL) {
$form['intro'] = [
'#markup' => $this->t('Use this form to send us the document that we\'ll translate!'),
];
$form['from'] = [
'#type' => 'textfield',
'#title' => $this->t('Name'),
'#description' => $this->t("Your full name."),
'#required' => TRUE,
];
$form['from_mail'] = [
'#type' => 'textfield',
'#title' => $this->t('Email address'),
'#description' => $this->t("Your email address."),
'#required' => TRUE,
];
$form['params'] = [
'#tree' => TRUE,
'subject' => [
'#type' => 'textfield',
'#title' => $this->t('Title'),
'#description' => $this->t("The title of the document."),
'#required' => TRUE,
],
'count' => [
'#type' => 'textfield',
'#title' => $this->t('Word Count'),
'#description' => $this->t("The word count of the document."),
'#required' => TRUE,
],
'body' => [
'#type' => 'textarea',
'#title' => $this->t('Comments'),
'#description' => $this->t("Tell us if you have any special requirements."),
'#required' => TRUE,
],
// This form element forces plaintext-only email when there is no HTML
// content (that is, when the 'body' form element is empty).
'plain' => [
'#type' => 'hidden',
'#states' => [
'value' => [
':input[name="body"]' => ['value' => ''],
],
],
],
'attachments' => [
'#name' => 'files[attachment]',
'#type' => 'file',
'#title' => $this->t('Choose a file to send for translation.'),
'#required' => TRUE,
],
];
$form['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Send message'),
];
return $form;
}
/**
* {#inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
// Extract the address part of the entered email before trying to validate.
// The email.validator service does not work on RFC2822 formatted addresses
// so we need to extract the RFC822 part out first. This is not as good as
// actually validating the full RFC2822 address, but it is better than
// either just validating RFC822 or not validating at all.
$pattern = '/<(.*?)>/';
$address = $form_state->getValue('from_mail');
preg_match_all($pattern, $address, $matches);
$address = isset($matches[1][0]) ? $matches[1][0] : $address;
if (!$this->emailValidator->isValid($address)) {
$form_state->setErrorByName('from_mail', $this->t('That email address is not valid.'));
}
$file = file_save_upload('attachment', [ 'file_validate_extensions' => array('doc docx pdf')], 'temporary://', 0);
if ($file) {
$form_state->setValue(['params', 'attachments'], [['filepath' => $file->getFileUri()]]);
}
}
/**
* {#inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// First, assemble arguments for MailManager::mail().
$module = 'submit_translation';
$key = "solon_key";
$to = "info#gexl.eu";
$langcode = $this->languageManager->getDefaultLanguage()->getId();
$params = $form_state->getValue('params');
$reply = "";
$send = TRUE;
$params['body'] .= " Count: " . $params['count'];
// Second, add values to $params and/or modify submitted values.
// Set From header.
if (!empty($form_state->getValue('from_mail'))) {
$params['headers']['From'] = MimeMailFormatHelper::mimeMailAddress([
'name' => $form_state->getValue('from'),
'mail' => $form_state->getValue('from_mail')
]);
}
elseif (!empty($form_state->getValue('from'))) {
$params['headers']['From'] = $from = $form_state->getValue('from');
}
else {
// Empty 'from' will result in the default site email being used.
}
// Handle empty attachments - we require this to be an array.
if (empty($params['attachments'])) {
$params['attachments'] = [];
}
// Remove empty values from $param['headers'] - this will force the
// the formatting mailsystem and the sending mailsystem to use the
// default values for these elements.
foreach ($params['headers'] as $header => $value) {
if (empty($value)) {
unset($params['headers'][$header]);
}
}
// Finally, call MailManager::mail() to send the mail.
$result = $this->mailManager->mail($module, $key, $to, $langcode, $params, $reply, $send);
if ($result['result'] == TRUE) {
$this->messenger()->addMessage($this->t('Your message has been sent.'));
}
else {
// This condition is also logged to the 'mail' logger channel by the
// default PhpMail mailsystem.
$this->messenger()->addError($this->t('There was a problem sending your message and it was not sent.'));
}
}
}
This happens because the form element '#type' => 'file' has no #value to validate. #required fields must have a #value set otherwise validation fails.
This is (now considered) a very old issue that has been fixed in Drupal 9.5.x, but this was assumed in the good old days of Drupal 7, as mentioned in the Form API reference :
#required: Indicates whether or not the element is required. This
automatically validates for empty fields, and flags inputs as
required. File fields are NOT allowed to be required.
So I guess the best solution is to upgrade to 9.5.x or above, if feasible, but as sometimes upgrading makes things complicated, you might prefer to review and apply the patch manually to your current code base.
[EDIT]: If still having issues after upgrade to >= 9.5.2,
Looking at the patch, a default valueCallback is now used to provide a #value to file form elements, but.. well there is another issue :
public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
if ($input === FALSE) {
return NULL;
}
$parents = $element['#parents'];
$element_name = array_shift($parents); # <- problem here :/
$uploaded_files = \Drupal::request()->files->get('files', []);
$uploaded_file = $uploaded_files[$element_name] ?? NULL;
if ($uploaded_file) {
// Cast this to an array so that the structure is consistent regardless of
// whether #value is set or not.
return (array) $uploaded_file;
}
return NULL;
}
See how it doesn't care about whether or not the element has a #name explicitly defined ? and whether or not #parents is a tree ? Now because of those wrong assumptions on the element's name and its parents, you are somehow forced to either :
Leave the #name property unset and refer to the file later on validation/submit as 'params' (the parents root) instead of 'attachment'. Or,
Stick with #tree => FALSE. Or,
Provide your own #value_callback (deprecated ...?)

Drupal 8 Custom Form managed_file multiple upload field restrict limit number of upload files

I have a custom form in Drupal 8. Form has managed_file field with multiple true. I am facing challenge regarding the limit of number of files upload to this field.
I have to make this managed_file field to upload only 3 images.
Can someone help me regarding this?
I have tried below code.
public function buildForm(array $form, FormStateInterface $form_state) {
$form['upload_doc'] = array(
'#type' => 'managed_file',
'#title' => 'File Upload',
'#upload_location' => 'public://upload_document/',
'#required' => TRUE,
'#multiple' => TRUE,
'#upload_validators' => array(
'file_validate_extensions' => array('jpg jpeg png'),
'file_validate_size' => 2000,
),
);
// Add a submit button that handles the submission of the form.
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Submit'),
];
return $form;
}
public function validateForm(array &$form, FormStateInterface $form_state) {
$fileObj = $form_state->getValue('upload_doc');
if (count($fileObj) > 3) {
drupal_set_message('File limit exceed'.count($fileObj), 'error');
return false;
} else {
return true;
}
}
The problem is, i am not able to validate file upload limit. It's allow to upload more than limit. Please help me
Thank you
public function validateForm(array &$form, FormStateInterface $form_state) {
$fileObj = $form_state->getValue('upload_doc');
if (count($fileObj) >= 4) {
$form_state->setErrorByName('upload_doc', $this->t("<em>Only 3 images are allowed per run</em>."));}}
There is currently an opened issue to get this feature out of the box.
In the meanwhile, you can implement "element_validate" in your form:
$form['file_managed'] = [
'#type' => 'managed_file',
'#title' => $this->t('Files'),
'#multiple' => TRUE,
'#cardinality' => 3,
'#element_validate' => [
'your_module_max_files_validation',
],
];
Then in your_module.module:
/**
* Custom validation handler. Validate number of files.
*/
function your_module_max_files_validation($element, FormStateInterface &$form_state, $form) {
$is_removal = strpos($form_state->getTriggeringElement()['#name'], 'remove_button') !== FALSE ? TRUE : FALSE;
if (!empty($element['#cardinality']) && count($element['#files']) > $element['#cardinality'] && !$is_removal) {
$form_state->setErrorByName($element['#name'], t('Only max #limit files allowed.', [
'#limit' => $element['#cardinality'],
]));
}
}

Modify form input before submitting form in symfony

I have a form in which i am passing image in base64 format, now before submitting form i am converting image base64 to uploaded file object , which is working fine but now i want to validate the uploaded file image.
How to achieve this.
In my build form method i have added validation but its not working.
Right now want to restrict image to 2mb but its even allowing more than 2mb image to get uploaded.
$builder
->add('picture', FileType::class, [
'label' => $trans('user.picture.name'),
'required' => true,
'constraints' => [
// new Image(['mimeTypes' => ['image/jpeg']]),
new File(['maxSize' => '2M', 'mimeTypes' => ['image/jpeg']]),
],
]);
function onPreSubmit(FormEvent $event)
{
$form = $event->getForm();
$data = $event->getData();
list(, $data['imagebase64']) = explode(',', $data['imagebase64']);
$filePath = tempnam(sys_get_temp_dir(), 'hijob');
$image = base64_decode($data['imagebase64']);
file_put_contents($filePath, $image);
$photo = new UploadedFile(
$filePath,
'photo.jpeg',
'image/jpeg'
);
$data['picture'] = $photo;
$event->setData($data);
}
If I understood you want to use Symfony\Component\Validator\Constraints as Assert in Entity class. That should be done something like this.
**
* #Assert\File(
* maxSize = "1024k",
* mimeTypes = {...},
* mimeTypesMessage = "Please upload a valid ..."
* )
*/
Check https://symfony.com/doc/current/reference/constraints/File.html

Drupal validation solution when using file upload with other required form field

I am having problem while trying to upload a picture in a form that also has other required field. So if I dont enter anything on the required field and upload the picture, I lose the picture that's uploaded (during the form validation the picture is no longer there).
I can't print it anywhere in form_state and all. How can I have a file upload inside a form with other form elements which are required? I dont want user to upload the picture again if the user forgets to enter the info in the other required field.
Any ideas?
function form() {
$form['#attributes'] = array('enctype' => "multipart/form-data");
//'upload' will be used in file_check_upload()
$form['upload'] = array('#type' => 'file');
$form['my_require_field'] = array(
'#type' => 'textfield',
'#title' => t('Enter code here'),
'#default_value' => 1,
'#size' => 20,
'#required' => TRUE
);
}
function form_validate() {
if(!file_check_upload('upload')) {
form_set_error('upload', 'File missing for upload.');
}
}
function form_submit() {
$file = file_check_upload('upload');
}
You should use the managed_file type id you are using Drupal 7
$form['upload'] = array(
'#type' => 'managed_file',
'#title' => t('Upload Image'),
'#default_value' => '',
'#required' => TRUE,
'#description' => t("Upload Image description"),
);
In your submit handler you can write following:
// Load the file via file fid.
$file = file_load($form_state['values']['upload']);
// Change status to permanent and save.
$file->status = FILE_STATUS_PERMANENT;
file_save($file);
Hope this will help!
For Drupal 8
It appears that such basic functionality as making field of type file (not managed_file) required is not supported out of the box.
One need to implement custom validator for this field in formValidate() method.
A rare example of such functionality can be found in ConfigImportForm.php file.
Here is a snippet of code to handle file field setup in the form, required validation and submission.
<?php
class YourForm extends FormBase {
public function buildForm(array $form, FormStateInterface $form_state) {
$form['myfile'] = [
'#title' => $this->t('Upload myfile'),
'#type' => 'file',
// DO NOT PROVILDE '#required' => TRUE or your form will always fail validation!
];
}
public function validateForm(array &$form, FormStateInterface $form_state) {
$all_files = $this->getRequest()->files->get('files', []);
if (!empty($all_files['myfile'])) {
$file_upload = $all_files['myfile'];
if ($file_upload->isValid()) {
$form_state->setValue('myfile', $file_upload->getRealPath());
return;
}
}
$form_state->setErrorByName('myfile', $this->t('The file could not be uploaded.'));
}
public function submitForm(array &$form, FormStateInterface $form_state) {
// Add validator for your file type etc.
$validators = ['file_validate_extensions' => ['csv']];
$file = file_save_upload('myfile', $validators, FALSE, 0);
if (!$file) {
return;
}
// The rest of submission processing.
// ...
}
}
From https://api.drupal.org/comment/63172#comment-63172

Drupal 7 Batch Page Example

I'm trying to setup a batch page for processing and I need an example. The example that's given in the Example module is within a form and I need a page that I can run independently of a form that will process batch requests.
For instance:
function mymodule_batch_2() {
$operations[] = array('mymodule_onefunction','mymodule_anotherfunction')
$batch = array(
'operations' => $operations,
'finished' => 'mymodule_finished',
// We can define custom messages instead of the default ones.
'title' => t('Processing batch 2'),
'init_message' => t('Batch 2 is starting.'),
'progress_message' => t('Processed #current out of #total.'),
'error_message' => t('Batch 2 has encountered an error.'),
);
batch_set($batch);
batch_process('');
}
Where the batch function would call other functions in the form of $operations.
You need to give batch process an id to work from. batch_process('mybatch')otherwise yourmexample is correct. are you having a particular problem with this strategy?
Here you can see my sample of batch relization with form that calls batch:
function my_module_menu() {
$items['admin/commerce/import'] = array(
'title' => t('Import'),
'page callback' => 'drupal_get_form',
'page arguments' => array('my_module_settings_form'),
'access arguments' => array('administer site settings'),
'type' => MENU_NORMAL_ITEM,
);
return $items;
}
/**
* Import form
*/
function my_module_settings_form() {
$form = array();
$form['import'] = array(
'#type' => 'fieldset',
'#title' => t('Import'),
'#collapsible' => TRUE,
'#collapsed' => FALSE,
);
$form['import']['submit'] = array(
'#type' => 'submit',
'#value' => t('Import'),
);
return $form;
}
function my_module_settings_form_submit($form, &$form_state) {
batch_my_module_import_start();
}
/**
* Batch start function
*/
function batch_my_module_import_start() {
$batch = array(
'title' => t('Import products'),
'operations' => array(
array('_batch_my_module_import', array()),
),
'progress_message' => t('Import. Operation #current out of #total.'),
'error_message' => t('Error!'),
'finished' => 'my_module_batch_finished',
);
batch_set($batch);
}
/**
* Import from 1C operation. Deletes Products
*/
function _batch_my_module_import(&$context) {
// Your iterms. In my case select all products
$pids = db_select('commerce_product', 'p')
->fields('p', array('sku', 'product_id', 'title'))
->condition('p.type', 'product')
->execute()
->fetchAll();
// Get Count of products
if (empty($context['sandbox']['progress'])) {
$context['sandbox']['progress'] = 0;
$context['sandbox']['max'] = count($pids);
watchdog('import', 'import products');
}
// Create Iteration variable
if (empty($context['sandbox']['iteration'])) {
$context['sandbox']['iteration'] = 0;
}
// Check for the end of cycle
if ($context['sandbox']['iteration'] < $context['sandbox']['max']) {
// Count of operation in one batch step
$limit = 10;
// Counts completed operations in one batch step
$counter = 0;
if ($context['sandbox']['progress'] != 0) {
$context['sandbox']['iteration'] = $context['sandbox']['iteration'] + $limit;
}
// Loop all Product items in xml
for ($i = $context['sandbox']['iteration']; $i < $context['sandbox']['max'] && $counter < $limit; $i++) {
/* Loop items here */
/* ... */
/* ... */
$context['results']['added']['products'][] = $product_item->title;
// Update Progress
$context['sandbox']['progress']++;
$counter++;
// Messages
$context['message'] = t('Now processing product %name. Product %progress of %count', array('%name' => $product_item->title, '%progress' => $context['sandbox']['progress'], '%count' => $context['sandbox']['max']));
$context['results']['processed'] = $context['sandbox']['progress'];
}
}
if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
$context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
}
}
/**
* Finish of batch. Messagess
*/
function my_module_batch_finished($success, $results, $operations) {
if ($success) {
drupal_set_message(t('#count products added.', array('#count' => isset($results['added']) ? count($results['added']) : 0)));
}
else {
$error_operation = reset($operations);
drupal_set_message(t('An error occurred while processing #operation with arguments : #args', array('#operation' => $error_operation[0], '#args' => print_r($error_operation[0], TRUE))));
}
watchdog('import', 'import finished');
}
function module_name_import_form_submit($form, $form_state) {
// Check to make sure that the file was uploaded to the server properly
$uri = db_query("SELECT uri FROM {file_managed} WHERE fid = :fid", array(
':fid' => $form_state['input']['import']['fid'],
))->fetchField();
if(!empty($uri)) {
if(file_exists(drupal_realpath($uri))) {
// Open the csv
$handle = fopen(drupal_realpath($uri), "r");
// Go through each row in the csv and run a function on it. In this case we are parsing by '|' (pipe) characters.
// If you want commas are any other character, replace the pipe with it.
while (($data = fgetcsv($handle, 0, '|', '"')) !== FALSE) {
$operations[] = array(
'module_name_import_batch_processing', // The function to run on each row
array($data), // The row in the csv
);
}
// Once everything is gathered and ready to be processed... well... process it!
$batch = array(
'title' => t('Importing CSV...'),
'operations' => $operations, // Runs all of the queued processes from the while loop above.
'finished' => 'module_name_import_finished', // Function to run when the import is successful
'error_message' => t('The installation has encountered an error.'),
'progress_message' => t('Imported #current of #total products.'),
);
batch_set($batch);
fclose($handle);
}
}
else {
drupal_set_message(t('There was an error uploading your file. Please contact a System administator.'), 'error');
}
}
/**
* This function runs the batch processing and creates nodes with then given information
* #see
* module_name_import_form_submit()
*/
function module_name_import_batch_processing($data) {
// Lets make the variables more readable.
$title = $data[0];
$body = $data[1];
$serial_num = $data[2];
// Find out if the node already exists by looking up its serial number. Each serial number should be unique. You can use whatever you want.
$nid = db_query("SELECT DISTINCT n.nid FROM {node} n " .
"INNER JOIN {field_data_field_serial_number} s ON s.revision_id = n.vid AND s.entity_id = n.nid " .
"WHERE field_serial_number_value = :serial", array(
':serial' => $serial_num,
))->fetchField();
if(!empty($nid)) {
// The node exists! Load it.
$node = node_load($nid);
// Change the values. No need to update the serial number though.
$node->title = $title;
$node->body['und'][0]['value'] = $body;
$node->body['und'][0]['safe_value'] = check_plain($body);
node_save($node);
}
else {
// The node does not exist! Create it.
global $user;
$node = new StdClass();
$node->type = 'page'; // Choose your type
$node->status = 1; // Sets to published automatically, 0 will be unpublished
$node->title = $title;
$node->uid = $user->uid;
$node->body['und'][0]['value'] = $body;
$node->body['und'][0]['safe_value'] = check_plain($body);
$node->language = 'und';
$node->field_serial_number['und'][0]['value'] = $serial_num;
$node->field_serial_number['und'][0]['safe_value'] = check_plain($serial_num);
node_save($node);
}
}
/**
* This function runs when the batch processing is complete
*
* #see
* module_name_import_form_submit()
*/
function module_name_import_finished() {
drupal_set_message(t('Import Completed Successfully'));
}

Resources