From 999bdaa719571f7e2bf7725fd55506c7f1a67688 Mon Sep 17 00:00:00 2001 From: lamrani abdelwadoud Date: Sun, 5 May 2024 02:48:02 +0200 Subject: [PATCH 1/4] NEW online signature of shipments (#29559) * New functionality for signed shipment * Add attribute for check if was already signed * Update expedition.class.php * Update onlineSign.php * Update expedition.class.php --------- Co-authored-by: Laurent Destailleur --- htdocs/admin/expedition.php | 7 + htdocs/core/ajax/onlineSign.php | 146 +++++++++++++++++++ htdocs/expedition/card.php | 8 + htdocs/expedition/class/expedition.class.php | 20 ++- htdocs/langs/en_US/commercial.lang | 3 + htdocs/langs/en_US/propal.lang | 2 + htdocs/public/onlinesign/newonlinesign.php | 64 +++++++- 7 files changed, 248 insertions(+), 2 deletions(-) diff --git a/htdocs/admin/expedition.php b/htdocs/admin/expedition.php index 4f50ce75fe8..45ea1431143 100644 --- a/htdocs/admin/expedition.php +++ b/htdocs/admin/expedition.php @@ -480,6 +480,13 @@ print $form->textwithpicto($langs->trans("WatermarkOnDraftContractCards"), $html print ''; print "\n"; +// Allow OnLine Sign +print ''; +print ''.$langs->trans("AllowOnLineSign").''; +print ''; +print ajax_constantonoff('EXPEDITION_ALLOW_ONLINESIGN', array(), null, 0, 0, 0, 2, 0, 1); +print ''; + print ''; print $form->buttonsSaveCancel("Modify", ''); diff --git a/htdocs/core/ajax/onlineSign.php b/htdocs/core/ajax/onlineSign.php index 48e140299e7..5abc559430f 100644 --- a/htdocs/core/ajax/onlineSign.php +++ b/htdocs/core/ajax/onlineSign.php @@ -678,6 +678,152 @@ if ($action == "importSignature") { $db->rollback(); } } + } elseif ($mode == 'expedition') { + require_once DOL_DOCUMENT_ROOT . '/expedition/class/expedition.class.php'; + require_once DOL_DOCUMENT_ROOT . '/core/lib/pdf.lib.php'; + + $object = new Expedition($db); + $object->fetch(0, $ref); + + $upload_dir = $conf->expedition->dir_output."/sending/"; + $upload_dir .= '/'.dol_sanitizeFileName($object->ref).'/'; + + $langs->loadLangs(array("main", "companies")); + + $default_font_size = pdf_getPDFFontSize($langs); // Must be after pdf_getInstance + $default_font = pdf_getPDFFont($langs); // Must be + + $date = dol_print_date(dol_now(), "%Y%m%d%H%M%S"); + $filename = "signatures/" . $date . "_signature.png"; + if (!is_dir($upload_dir . "signatures/")) { + if (!dol_mkdir($upload_dir . "signatures/")) { + $response = "Error mkdir. Failed to create dir " . $upload_dir . "signatures/"; + $error++; + } + } + + if (!$error) { + $return = file_put_contents($upload_dir . $filename, $data); + if ($return == false) { + $error++; + $response = 'Error file_put_content: failed to create signature file.'; + } + } + + if (!$error) { + $last_main_doc_file = $object->last_main_doc; + // Defined modele of doc + if (empty($last_main_doc_file) || !dol_is_file(DOL_DATA_ROOT.'/'.$object->last_main_doc)) { + // It seems document has never been generated, or was generated and then deleted. + // So we try to regenerate it with its default template. + $defaulttemplate = ''; // We force the use an empty string instead of $object->model_pdf to be sure to use a "main" default template and not the last one used. + $object->generateDocument($defaulttemplate, $langs); + } + $last_main_doc_file = $object->last_main_doc; + $directdownloadlink = $object->getLastMainDocLink('expedition'); // url to download the $object->last_main_doc + if (preg_match('/\.pdf/i', $last_main_doc_file)) { + // TODO Use the $last_main_doc_file to defined the $newpdffilename and $sourcefile + $newpdffilename = $upload_dir . $ref . "_signed-" . $date . ".pdf"; + $sourcefile = $upload_dir . $ref . ".pdf"; + + + if (dol_is_file($sourcefile)) { + // We build the new PDF + $pdf = pdf_getInstance(); + if (class_exists('TCPDF')) { + $pdf->setPrintHeader(false); + $pdf->setPrintFooter(false); + } + $pdf->SetFont(pdf_getPDFFont($langs)); + + if (getDolGlobalString('MAIN_DISABLE_PDF_COMPRESSION')) { + $pdf->SetCompression(false); + } + + //$pdf->Open(); + $pagecount = $pdf->setSourceFile($sourcefile); // original PDF + + $param = array(); + $param['online_sign_name'] = $online_sign_name; + $param['pathtoimage'] = $upload_dir . $filename; + + $s = array(); // Array with size of each page. Example array(w'=>210, 'h'=>297); + for ($i = 1; $i < ($pagecount + 1); $i++) { + try { + $tppl = $pdf->importPage($i); + $s = $pdf->getTemplatesize($tppl); + $pdf->AddPage($s['h'] > $s['w'] ? 'P' : 'L'); + $pdf->useTemplate($tppl); + + if (getDolGlobalString("SHIPMENT_SIGNATURE_ON_ALL_PAGES")) { + // A signature image file is 720 x 180 (ratio 1/4) but we use only the size into PDF + // TODO Get position of box from PDF template + + $param['xforimgstart'] = 111; + $param['yforimgstart'] = (empty($s['h']) ? 250 : $s['h'] - 60); + $param['wforimg'] = $s['w'] - ($param['xforimgstart'] + 16); + + dolPrintSignatureImage($pdf, $langs, $param); + } + } catch (Exception $e) { + dol_syslog("Error when manipulating some PDF by onlineSign: " . $e->getMessage(), LOG_ERR); + $response = $e->getMessage(); + $error++; + } + } + + if (!getDolGlobalString("SHIPMENT_SIGNATURE_ON_ALL_PAGES")) { + // A signature image file is 720 x 180 (ratio 1/4) but we use only the size into PDF + // TODO Get position of box from PDF template + + $param['xforimgstart'] = 111; + $param['yforimgstart'] = (empty($s['h']) ? 250 : $s['h'] - 60); + $param['wforimg'] = $s['w'] - ($param['xforimgstart'] + 16); + + dolPrintSignatureImage($pdf, $langs, $param); + } + + //$pdf->Close(); + $pdf->Output($newpdffilename, "F"); + + // Index the new file and update the last_main_doc property of object. + $object->indexFile($newpdffilename, 1); + } + if (!$error) { + $response = "success"; + } + } elseif (preg_match('/\.odt/i', $last_main_doc_file)) { + // Adding signature on .ODT not yet supported + // TODO + } else { + // Document format not supported to insert online signature. + // We should just create an image file with the signature. + } + } + + if (!$error) { + $db->begin(); + + $sql = "UPDATE " . MAIN_DB_PREFIX . "expedition"; + $sql .= " SET signed_status = " . ((int) $object::STATUS_SIGNED) ; + $sql .= " WHERE rowid = " . ((int) $object->id); + + dol_syslog(__FILE__, LOG_DEBUG); + $resql = $db->query($sql); + if (!$resql) { + $error++; + } else { + $num = $db->affected_rows($resql); + } + + if (!$error) { + $db->commit(); + $response = "success"; + setEventMessages("ExpeditionSigned", null, 'warnings'); + } else { + $db->rollback(); + } + } } } else { $error++; diff --git a/htdocs/expedition/card.php b/htdocs/expedition/card.php index 7f748bf3e03..c6fa8c69881 100644 --- a/htdocs/expedition/card.php +++ b/htdocs/expedition/card.php @@ -2743,6 +2743,14 @@ if ($action == 'create') { $linktoelem = $form->showLinkToObjectBlock($object, null, array('shipping')); $somethingshown = $form->showLinkedObjectBlock($object, $linktoelem); + // Show online signature link + $useonlinesignature = getDolGlobalInt('EXPEDITION_ALLOW_ONLINESIGN'); + + if ($object->statut != Expedition::STATUS_DRAFT && $useonlinesignature) { + print '
'; + require_once DOL_DOCUMENT_ROOT.'/core/lib/signature.lib.php'; + print showOnlineSignatureUrl('expedition', $object->ref, $object).'
'; + } print '
'; diff --git a/htdocs/expedition/class/expedition.class.php b/htdocs/expedition/class/expedition.class.php index 75ff4241bd1..68a1ae689fc 100644 --- a/htdocs/expedition/class/expedition.class.php +++ b/htdocs/expedition/class/expedition.class.php @@ -224,6 +224,11 @@ class Expedition extends CommonObject public $multicurrency_total_tva; public $multicurrency_total_ttc; + /** + * @var int + */ + public $signed_status = 0; + /** * Draft status */ @@ -260,6 +265,18 @@ class Expedition extends CommonObject const STATUS_SHIPMENT_IN_PROGRESS = 3; + /** + * No signature + */ + const STATUS_NO_SIGNATURE = 0; + + /** + * Signed status + */ + const STATUS_SIGNED = 1; + + + /** * Constructor * @@ -603,6 +620,7 @@ class Expedition extends CommonObject $sql .= ", e.fk_shipping_method, e.tracking_number"; $sql .= ", e.note_private, e.note_public"; $sql .= ', e.fk_incoterms, e.location_incoterms'; + $sql .= ', e.signed_status'; $sql .= ', i.libelle as label_incoterms'; $sql .= ', s.libelle as shipping_method'; $sql .= ", el.fk_source as origin_id, el.sourcetype as origin_type"; @@ -653,7 +671,7 @@ class Expedition extends CommonObject $this->origin_id = $obj->origin_id; $this->billed = $obj->billed; $this->fk_project = $obj->fk_project; - + $this->signed_status = $obj->signed_status; $this->trueWeight = $obj->weight; $this->weight_units = $obj->weight_units; diff --git a/htdocs/langs/en_US/commercial.lang b/htdocs/langs/en_US/commercial.lang index 7b0ab3ed30e..2ac4a52aff9 100644 --- a/htdocs/langs/en_US/commercial.lang +++ b/htdocs/langs/en_US/commercial.lang @@ -77,14 +77,17 @@ WelcomeOnOnlineSignaturePageProposal=Welcome to the page to accept commercial pr WelcomeOnOnlineSignaturePageContract=Welcome to %s Contract PDF Signing Page WelcomeOnOnlineSignaturePageFichinter=Welcome to %s Intervention PDF Signing Page WelcomeOnOnlineSignaturePageSociete_rib=Welcome to %s SEPA mandate PDF Signing Page +WelcomeOnOnlineSignaturePageExpedition=Welcome to %s Shipment PDF Signing Page ThisScreenAllowsYouToSignDocFromProposal=This screen allow you to accept and sign, or refuse, a quote/commercial proposal ThisScreenAllowsYouToSignDocFromContract=This screen allow you to sign contract on PDF format online. ThisScreenAllowsYouToSignDocFromFichinter=This screen allow you to sign intervention on PDF format online. ThisScreenAllowsYouToSignDocFromSociete_rib=This screen allow you to sign SEPA Mandate on PDF format online. +ThisScreenAllowsYouToSignDocFromExpedition=This screen allow you to sign shipment on PDF format online. ThisIsInformationOnDocumentToSignProposal=This is information on document to accept or refuse ThisIsInformationOnDocumentToSignContract=This is information on contract to sign ThisIsInformationOnDocumentToSignFichinter=This is information on intervention to sign ThisIsInformationOnDocumentToSignSociete_rib=This is information on SEPA Mandate to sign +ThisIsInformationOnDocumentToSignExpedition= This is information on shipment to sign SignatureProposalRef=Signature of quote/commercial proposal %s SignatureContractRef=Signature of contract %s SignatureFichinterRef=Signature of intervention %s diff --git a/htdocs/langs/en_US/propal.lang b/htdocs/langs/en_US/propal.lang index 2e8207fe8c0..e4dd82ee3a1 100644 --- a/htdocs/langs/en_US/propal.lang +++ b/htdocs/langs/en_US/propal.lang @@ -122,3 +122,5 @@ SignSociete_rib=Sign mandate SignPropal=Accept proposal Signed=signed SignedOnly=Signed only +ExpeditionSigned=Shipment signed +SignExpedition=Sign shipment diff --git a/htdocs/public/onlinesign/newonlinesign.php b/htdocs/public/onlinesign/newonlinesign.php index d49a9d75cca..bbb0c558de6 100644 --- a/htdocs/public/onlinesign/newonlinesign.php +++ b/htdocs/public/onlinesign/newonlinesign.php @@ -54,6 +54,7 @@ require_once DOL_DOCUMENT_ROOT.'/core/lib/payments.lib.php'; require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php'; require_once DOL_DOCUMENT_ROOT.'/core/lib/functions2.lib.php'; require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php'; +require_once DOL_DOCUMENT_ROOT.'/expedition/class/expedition.class.php'; // Load translation files $langs->loadLangs(array("main", "other", "dict", "bills", "companies", "errors", "members", "paybox", "stripe", "propal", "commercial")); @@ -122,7 +123,6 @@ $urlko = preg_replace('/&$/', '', $urlko); // Remove last & $creditor = $mysoc->name; $type = $source; - if (!$action) { if ($source && !$ref) { httponly_accessforbidden($langs->trans('ErrorBadParameters')." - ref missing", 400, 1); @@ -160,6 +160,10 @@ if ($source == 'proposal') { require_once DOL_DOCUMENT_ROOT.'/societe/class/companybankaccount.class.php'; $object = new CompanyBankAccount($db); $result = $object->fetch(0, $ref); +} elseif ($source == 'expedition') { + require_once DOL_DOCUMENT_ROOT.'/expedition/class/expedition.class.php'; + $object = new Expedition($db); + $result = $object->fetch(0, $ref); } else { httponly_accessforbidden($langs->trans('ErrorBadParameters')." - Bad value for source. Value not supported.", 400, 1); } @@ -319,6 +323,9 @@ if (empty($text)) { } elseif ($source == 'fichinter') { $text .= '
'.$langs->trans("WelcomeOnOnlineSignaturePageFichinter", $mysoc->name).''."\n"; $text .= ''.$langs->trans("ThisScreenAllowsYouToSignDocFromFichinter", $creditor).'

'."\n"; + } elseif ($source == 'expedition') { + $text .= '
'.$langs->trans("WelcomeOnOnlineSignaturePageExpedition", $mysoc->name).''."\n"; + $text .= ''.$langs->trans("ThisScreenAllowsYouToSignDocFromExpedition", $creditor).'

'."\n"; } else { $text .= '
'.$langs->trans("WelcomeOnOnlineSignaturePage".dol_ucfirst($source), $mysoc->name).''."\n"; $text .= ''.$langs->trans("ThisScreenAllowsYouToSignDocFrom".dol_ucfirst($source), $creditor).'

'."\n"; @@ -335,6 +342,8 @@ if ($source == 'proposal') { print ''.$langs->trans("ThisIsInformationOnDocumentToSignContract").' :'."\n"; } elseif ($source == 'fichinter') { print ''.$langs->trans("ThisIsInformationOnDocumentToSignFichinter").' :'."\n"; +} elseif ($source == 'expedition') { + print ''.$langs->trans("ThisIsInformationOnDocumentToSignExpedition").' :'."\n"; } else { print ''.$langs->trans("ThisIsInformationOnDocumentToSign".dol_ucfirst($source)).' :'."\n"; } @@ -590,6 +599,53 @@ if ($source == 'proposal') { print $langs->trans("DownloadDocument").''; } } +} elseif ($source == 'expedition') { + // Signature on expedition + $found = true; + $langs->load("fichinter"); + + $result = $object->fetch_thirdparty($object->socid); + + // Proposer + print ''.$langs->trans("Proposer"); + print ''; + print img_picto('', 'company', 'class="pictofixedwidth"'); + print ''.$creditor.''; + print ''; + print ''."\n"; + + // Target + print ''.$langs->trans("ThirdParty"); + print ''; + print img_picto('', 'company', 'class="pictofixedwidth"'); + print ''.$object->thirdparty->name.''; + print ''."\n"; + + // Object + $text = ''.$langs->trans("SignatureFichinterRef", $object->ref).''; + print ''.$langs->trans("Designation"); + print ''.$text; + + $last_main_doc_file = $object->last_main_doc; + if (empty($last_main_doc_file) || !dol_is_file(DOL_DATA_ROOT.'/'.$object->last_main_doc)) { + // It seems document has never been generated, or was generated and then deleted. + // So we try to regenerate it with its default template. + $defaulttemplate = ''; // We force the use an empty string instead of $object->model_pdf to be sure to use a "main" default template and not the last one used. + $object->generateDocument($defaulttemplate, $langs); + } + $directdownloadlink = $object->getLastMainDocLink('', 0, 0); + if ($directdownloadlink) { + print '
'; + print img_mime($object->last_main_doc, ''); + if ($message == "signed") { + print $langs->trans("DownloadSignedDocument").''; + } else { + print $langs->trans("DownloadDocument").''; + } + } + print ''; + print ''; + print ''."\n"; } else { $found = true; $langs->load('companies'); @@ -769,6 +825,12 @@ if ($action == "dosign" && empty($cancel)) { } else { print ''; } + } elseif ($source == 'expedition') { + if ($message == 'signed' || $object->signed_status == Expedition::STATUS_SIGNED) { + print ''.$langs->trans("ExpeditionSigned").''; + } else { + print ''; + } } else { if ($message == 'signed') { print ''.$langs->trans(dol_ucfirst($source)."Signed").''; From a8bf1747229e4958f4a3065463db9d2f31a8ae9e Mon Sep 17 00:00:00 2001 From: Mohamed DAOUD Date: Sun, 5 May 2024 02:48:23 +0200 Subject: [PATCH 2/4] Fix edit inline mode for the website module (#29590) * edit inline website * check user rights before * Update editinline.php --------- Co-authored-by: Laurent Destailleur --- htdocs/core/ajax/editinline.php | 110 ++++++++++++++++++++++++++++++++ htdocs/website/index.php | 46 +++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 htdocs/core/ajax/editinline.php diff --git a/htdocs/core/ajax/editinline.php b/htdocs/core/ajax/editinline.php new file mode 100644 index 00000000000..4d4fbe74e17 --- /dev/null +++ b/htdocs/core/ajax/editinline.php @@ -0,0 +1,110 @@ + + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * \file htdocs/core/ajax/editinline.php + * \brief Save edit inline changes + */ + + +if (!defined('NOTOKENRENEWAL')) { + define('NOTOKENRENEWAL', '1'); // Disables token renewal +} +if (!defined('NOREQUIREMENU')) { + define('NOREQUIREMENU', '1'); +} +if (!defined('NOREQUIREAJAX')) { + define('NOREQUIREAJAX', '1'); +} +if (!defined('NOREQUIRESOC')) { + define('NOREQUIRESOC', '1'); +} + +// Load Dolibarr environment +require '../../main.inc.php'; +require_once DOL_DOCUMENT_ROOT.'/website/class/website.class.php'; +require_once DOL_DOCUMENT_ROOT.'/website/class/websitepage.class.php'; +require_once DOL_DOCUMENT_ROOT.'/core/lib/website2.lib.php'; + + +$action = GETPOST('action', 'alpha'); +$website_ref = GETPOST('website_ref'); +$page_id = GETPOST('page_id'); +$content = GETPOST('content', 'none'); +$element_id = GETPOST('element_id'); +$element_type = GETPOST('element_type'); + +$usercanmodify = $user->hasRight('website', 'write'); +if (!$usercanmodify) { + print "You don't have permission for this action."; + exit; +} + + +/* + * View + */ + +top_httphead(); + +if (!empty($action) && $action === 'updatedElementContent' && $usercanmodify && !empty($content) && !empty($element_id) && !empty($website_ref) && !empty($page_id)) { + // Page object + $objectpage = new WebsitePage($db); + $res = $objectpage->fetch($page_id); + if (!$res) { + print "Cannot find page with ID = " . $page_id . "."; + exit; + } + + // Website object + $objectwebsite = new Website($db); + $res = $objectwebsite->fetch($objectpage->fk_website); + if (!$res) { + print "Cannot find website with REF " . $objectpage->fk_website . "."; + exit; + } + + $db->begin(); + $error = 0; + + // Replace element content into database and tpl file + $objectpage->content = preg_replace('/<' . $element_type . '[^>]*id="' . $element_id . '"[^>]*>\K(.*?)(?=<\/' . $element_type . '>)/s', $content, $objectpage->content, 1); + $res = $objectpage->update($user); + if ($res) { + global $dolibarr_main_data_root; + $pathofwebsite = $dolibarr_main_data_root.($conf->entity > 1 ? '/'.$conf->entity : '').'/website/'.$website_ref; + $filetpl = $pathofwebsite.'/page'.$objectpage->id.'.tpl.php'; + + $result = dolSavePageContent($filetpl, $objectwebsite, $objectpage, 1); + if (!$result) { + print "Failed to write file " . $filetpl . "."; + $error++; + } + } else { + print "Failed to save changes error " . $objectpage->error . "."; + $error++; + } + + if (!$error) { + $db->commit(); + print "Changes are saved for " . $element_type . " with id " . $element_id; + } else { + $db->rollback(); + } + + $db->close(); +} diff --git a/htdocs/website/index.php b/htdocs/website/index.php index dcf3c05a2f0..4c7c574425c 100644 --- a/htdocs/website/index.php +++ b/htdocs/website/index.php @@ -3491,12 +3491,58 @@ if (!GETPOST('hide_websitemenu')) { // ... //}); + CKEDITOR.instances[idtouse].on(\'change\', function() { + $(this.element.$).addClass(\'modified\'); + }) } else { console.warn("A html section has the contenteditable=true attribute but has no id attribute"); } }) isEditingEnabled = true; + + // Trigger the function when clicking outside the elements with contenteditable=true attribute + $(document).on(\'click\', function(e) { + var target = $(e.target); + // Check if the click is outside the elements with contenteditable=true attribute + if (!target.closest(\'[contenteditable="true"]\').length) { + // Repeat through the elements with contenteditable="true" attribute + $(\'[contenteditable="true"]\').each(function() { + var idToUse = $(this).attr(\'id\'); + var elementType = $(this).prop("tagName").toLowerCase(); // Get the tag name (div, section, footer...) + var instance = CKEDITOR.instances[idToUse]; + // Check if the element has been modified + if ($(this).hasClass(\'modified\')) { + var content = instance.getData(); + content = "\\n" + content; + + // Retrieving the content and ID of the element + var elementId = $(this).attr(\'id\'); + + // Sending data via AJAX + $.ajax({ + type: \'POST\', + url: \'' . DOL_URL_ROOT . '/core/ajax/editinline.php\', + data: { + website_ref: \''.$website->ref.'\', + page_id: \'' . $websitepage->id . '\', + content: content, + element_id: elementId, + element_type: elementType, + action: \'updatedElementContent\', + token: \'' . newToken() . '\' + }, + success: function(response) { + console.log(response); + } + }); + + $(this).removeClass(\'modified\'); + } + }); + } + }); + } else { console.log("Disable inline edit"); for(name in CKEDITOR.instances) { From 7562eacbf61a180b13fb27bcd8eb1efe0dc905aa Mon Sep 17 00:00:00 2001 From: Vaadasch Date: Sun, 5 May 2024 02:51:05 +0200 Subject: [PATCH 3/4] NEW Add option to open files in new tab (#29604) * Add option to open files in new tab * Add option to open files in new tab - with MAIN_DISABLE_FORCE_SAVEAS * Add option to open files in new tab - fix * Open in new tab with MAIN_DISABLE_FORCE_SAVEAS == 2 --------- Co-authored-by: Vaadasch Co-authored-by: Laurent Destailleur --- htdocs/core/class/html.formfile.class.php | 30 +++++++++++++++++------ 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/htdocs/core/class/html.formfile.class.php b/htdocs/core/class/html.formfile.class.php index 05190edc8ae..c536efaabc8 100644 --- a/htdocs/core/class/html.formfile.class.php +++ b/htdocs/core/class/html.formfile.class.php @@ -890,7 +890,11 @@ class FormFile } else { $out .= ''; } - $out .= ' -