Merge branch '23.0' of git@github.com:Dolibarr/dolibarr.git into develop

This commit is contained in:
Laurent Destailleur 2026-01-07 14:16:39 +01:00
commit 2ff2ee02ed
31 changed files with 274 additions and 219 deletions

View file

@ -1,5 +1,5 @@
---
exclude: (?x)^( htdocs/includes/ckeditor/.*|(\.(?!github/workflows)[^/]*/.*))$
exclude: (?x)^( htdocs/includes/ckeditor/.*|htdocs/public/includes/ckeditor/.*|(\.(?!github/workflows)[^/]*/.*))$
repos:
# Several miscellaneous checks and fix (on yaml files, end of files fix)
- repo: https://github.com/pre-commit/pre-commit-hooks

View file

@ -114,8 +114,8 @@ if ( !$ENV{"DESTIBETARC"} || !$ENV{"DESTISTABLE"} ) {
print "On Linux:\n";
print "export DESTIBETARC='/tmp'; export DESTISTABLE='/tmp';\n";
print "On Windows:\n";
print "set DESTIBETARC=c:/tmp\n";
print "set DESTISTABLE=c:/tmp\n";
print "set DESTIBETARC=c:/temp\n";
print "set DESTISTABLE=c:/temp\n";
print "\n";
print "Example in .bashrc:\n";
print

View file

@ -51,7 +51,7 @@ require_once DOL_DOCUMENT_ROOT.'/accountancy/class/lettering.class.php';
*/
// Load translation files required by the page
$langs->loadLangs(array("accountancy", "categories", "compta"));
$langs->loadLangs(array("accountancy", "categories", "compta", "other"));
// Get Parameters
$socid = GETPOSTINT('socid');

View file

@ -2,7 +2,7 @@
/* Copyright (C) 2016 Neil Orley <neil.orley@oeris.fr>
* Copyright (C) 2013-2016 Olivier Geffroy <jeff@jeffinfo.com>
* Copyright (C) 2013-2020 Florian Henry <florian.henry@open-concept.pro>
* Copyright (C) 2013-2025 Alexandre Spangaro <alexandre@inovea-conseil.com>
* Copyright (C) 2013-2026 Alexandre Spangaro <alexandre@inovea-conseil.com>
* Copyright (C) 2018-2025 Frédéric France <frederic.france@free.fr>
* Copyright (C) 2024-2025 MDW <mdeweerd@users.noreply.github.com>
* Copyright (C) 2025 Nicolas Barrouillet <nicolas@pragma-tech.fr>
@ -49,7 +49,7 @@ require_once DOL_DOCUMENT_ROOT.'/core/lib/date.lib.php';
*/
// Load translation files required by the page
$langs->loadLangs(array("accountancy", "categories", "compta"));
$langs->loadLangs(array("accountancy", "categories", "compta", "other"));
$journal_code = GETPOST('code_journal', 'alpha');
$account = GETPOST("account", 'int');

View file

@ -75,7 +75,7 @@ print "<br>\n";
$texttoshow = $langs->trans("AccountancySetupDoneFromAccountancyMenu", '{s1}'.$langs->transnoentitiesnoconv("Accounting").' - '.$langs->transnoentitiesnoconv("Setup").'{s2}');
$texttoshow = str_replace('{s1}', '<a href="'.dolBuildUrl(DOL_URL_ROOT.'/accountancy/index.php', ['mainmenu' => 'accountancy', 'leftmenu' => 'accountancy_admin', 'showtuto' => 1]).'">', $texttoshow);
$texttoshow = str_replace('{s2}', '</a>'.img_picto("", "url", 'class="paddingleft"'), $texttoshow);
print '<span class="opacitymedium">'.$texttoshow."</span><br>\n";
print '<div class="info">'.$texttoshow."</div><br>\n";
print "<br>\n";
llxFooter();

View file

@ -62,6 +62,7 @@ require_once DOL_DOCUMENT_ROOT.'/admin/remotestore/class/externalModules.class.p
$langs->loadLangs(array("errors", "admin", "modulebuilder"));
$action = GETPOST('action', 'aZ09');
$page = GETPOSTINT('page');
$page_y = GETPOSTINT('page_y');
$optioncss = GETPOST('optioncss', 'aZ09');
$sortfield = GETPOST('sortfield', 'aZ09');
@ -520,7 +521,7 @@ $i = 0; // is a sequencer of modules found
$j = 0; // j is module number. Automatically affected if module number not defined.
$modNameLoaded = array();
//if ($mode == 'common' || $mode == 'commonkanban') {
// Load $modules (required for the badge count)
foreach ($modulesdir as $dir) {
// Load modules attributes in arrays (name, numero, orders) from dir directory
@ -1213,7 +1214,7 @@ if ($mode == 'common' || $mode == 'commonkanban') {
print "</td>\n";
// Desc
print '<td class="valignmiddle tdoverflowmax300 minwidth200imp">';
print '<td class="valignmiddle tdoverflowmax300 minwidth200imp opacitylow">';
print nl2br($objMod->getDesc());
print "</td>\n";

View file

@ -821,6 +821,11 @@ class ActionComm extends CommonObject
$result = $this->create($fuser);
if ($result < 0) {
$error++;
} else {
$resultcat = $this->cloneCategories($objFrom->id, $this->id);
if ($resultcat < 0) {
$error++;
}
}
if (!$error) {

View file

@ -703,7 +703,7 @@ if ($action == 'create') {
print '<table class="border centpercent tableforfield">';
// Type
print '<tr><td class="titlefield">'.$langs->trans("AccountType").'</td>';
print '<tr><td class="titlefieldmiddle">'.$langs->trans("AccountType").'</td>';
print '<td>'.$object->type_lib[$object->type].'</td></tr>';
// Currency
@ -739,10 +739,13 @@ if ($action == 'create') {
print '<tr class="liste_titre_add"><td class="titlefield">'.$langs->trans("AccountancyCode").'</td>';
print '<td>';
if (isModEnabled('accounting')) {
$accountingaccount = new AccountingAccount($db);
$accountingaccount->fetch(0, $object->account_number, 1);
print $accountingaccount->getNomUrl(0, 1, 1, '', 1);
if (empty($object->account_number)) {
print img_warning($langs->trans("Mandatory"));
} else {
$accountingaccount = new AccountingAccount($db);
$accountingaccount->fetch(0, $object->account_number, 1);
print $accountingaccount->getNomUrl(0, 1, 1, '', 1);
}
} else {
print $object->account_number;
}
@ -752,11 +755,11 @@ if ($action == 'create') {
if (isModEnabled('accounting')) {
print '<tr><td>'.$langs->trans("AccountancyJournal").'</td>';
print '<td>';
if ($object->fk_accountancy_journal > 0) {
if (empty($object->fk_accountancy_journal)) {
print img_warning($langs->trans("Mandatory"));
} elseif ($object->fk_accountancy_journal > 0) {
$accountingjournal = new AccountingJournal($db);
$accountingjournal->fetch($object->fk_accountancy_journal);
print $accountingjournal->getNomUrl(0, 1, 1, '', 1);
}
print '</td></tr>';
@ -869,15 +872,15 @@ if ($action == 'create') {
print nl2br($object->owner_address);
print "</td></tr>\n";
print '<tr><td class="tdtop">'.$langs->trans("BankAccountOwnerZip").'</td>';
print '<tr><td>'.$langs->trans("BankAccountOwnerZip").'</td>';
print '<td>'.dol_escape_htmltag($object->owner_zip);
print '</td></tr>';
print '<tr><td class="tdtop">'.$langs->trans("BankAccountOwnerTown").'</td>';
print '<tr><td>'.$langs->trans("BankAccountOwnerTown").'</td>';
print '<td>'.dol_escape_htmltag($object->owner_town);
print '</td></tr>';
print '<tr><td class="tdtop">'.$langs->trans("BankAccountOwnerCountry").'</td>';
print '<tr><td>'.$langs->trans("BankAccountOwnerCountry").'</td>';
print '<td>';
$object->owner_country_code = dol_getIdFromCode($db, $object->owner_country_id, 'c_country', 'rowid', 'code');
$langs->load("dict");

View file

@ -712,12 +712,16 @@ foreach ($accounts as $key => $type) {
// Account number
if (!empty($arrayfields['b.account_number']['checked'])) {
print '<td class="tdoverflowmax250">';
if (isModEnabled('accounting') && !empty($objecttmp->account_number)) {
$accountingaccount = new AccountingAccount($db);
$accountingaccount->fetch(0, $objecttmp->account_number, 1);
print '<span title="'.dol_escape_htmltag($accountingaccount->account_number.' - '.$accountingaccount->label).'">';
print $accountingaccount->getNomUrl(0, 1, 1, '', 0);
print '</span>';
if (isModEnabled('accounting')) {
if (empty($objecttmp->account_number)) {
print img_warning($langs->trans("Mandatory"));
} else {
$accountingaccount = new AccountingAccount($db);
$accountingaccount->fetch(0, $objecttmp->account_number, 1);
print '<span title="'.dol_escape_htmltag($accountingaccount->account_number.' - '.$accountingaccount->label).'">';
print $accountingaccount->getNomUrl(0, 1, 1, '', 0);
print '</span>';
}
} else {
print '<span title="'.dol_escape_htmltag($objecttmp->account_number).'">'.$objecttmp->account_number.'</span>';
}

View file

@ -402,6 +402,11 @@ if ($search_date_when_end) {
// Add where from extra fields
include DOL_DOCUMENT_ROOT.'/core/tpl/extrafields_list_search_sql.tpl.php';
// Add where from hooks
$parameters = array();
$reshook = $hookmanager->executeHooks('printFieldListWhere', $parameters, $object, $action); // Note that $action and $object may have been modified by hook
$sql .= $hookmanager->resPrint;
if ($search_all) {
$sql .= " AND EXISTS (SELECT fdc.rowid FROM ".MAIN_DB_PREFIX."facturedet_rec as fdc WHERE f.rowid = fdc.fk_facture ".natural_search(array_keys($fieldstosearchall), $search_all).")";
}
@ -854,6 +859,12 @@ if (!empty($arrayfields['f.tms']['checked'])) {
}
// Extra fields
include DOL_DOCUMENT_ROOT.'/core/tpl/extrafields_list_search_title.tpl.php';
// Hook fields
$parameters = array('arrayfields'=>$arrayfields, 'param'=>$param, 'sortfield'=>$sortfield, 'sortorder'=>$sortorder, 'totalarray'=>$totalarray);
$reshook = $hookmanager->executeHooks('printFieldListTitle', $parameters, $object, $action); // Note that $action and $object may have been modified by hook
print $hookmanager->resPrint;
if (!empty($arrayfields['status']['checked'])) {
print_liste_field_titre($arrayfields['status']['label'], $_SERVER['PHP_SELF'], "f.suspended,f.frequency", "", $param, '', $sortfield, $sortorder, 'center ');
$totalarray['nbfield']++;

View file

@ -1627,7 +1627,7 @@ if (getDolGlobalString('MAIN_VIEW_LINE_NUMBER_IN_LIST')) {
// Ref
if (!empty($arrayfields['f.ref']['checked'])) {
print '<td class="liste_titre">';
print '<input class="flat maxwidth50imp" type="text" name="search_ref" value="'.dol_escape_htmltag($search_ref).'">';
print '<input class="flat maxwidth75imp" type="text" name="search_ref" value="'.dol_escape_htmltag($search_ref).'">';
print '</td>';
}
// Ref customer

View file

@ -1,58 +0,0 @@
<?php
/* Copyright (C) 2024 Frédéric France <frederic.france@free.fr>
*/
// Just for display errors in editor
ini_set('display_errors', 1);
if (!defined('NOTOKENRENEWAL')) {
define('NOTOKENRENEWAL', '1'); // Disables token renewal
}
if (!defined('NOREQUIREMENU')) {
define('NOREQUIREMENU', '1');
}
if (!defined('NOREQUIREHTML')) {
define('NOREQUIREHTML', '1');
}
if (!defined('NOREQUIREAJAX')) {
define('NOREQUIREAJAX', '1');
}
if (!defined('NOREQUIRESOC')) {
define('NOREQUIRESOC', '1');
}
require_once '../../main.inc.php';
require_once DOL_DOCUMENT_ROOT.'/website/class/websitepage.class.php';
/**
* @var Conf $conf
* @var DoliDB $db
* @var HookManager $hookmanager
* @var Translate $langs
* @var User $user
*/
top_httphead();
if ($_SERVER['REQUEST_METHOD'] == 'POST' && GETPOSTISSET('selectedIds')) {
$selectedIds = json_decode(GETPOST('selectedIds'), true);
$websitepage = new WebsitePage($db);
$selectedPosts = array();
foreach ($selectedIds as $id) {
$blog = new WebsitePage($db);
$blog->fetch($id);
$selectedPosts[] = array(
'id' => $blog->id,
'title' => $blog->title,
'description' => $blog->description,
'date_creation' => $blog->date_creation,
'image' => $blog->image,
);
}
print json_encode($selectedPosts);
} else {
print json_encode(array('error' => 'Invalid request'));
}

View file

@ -52,6 +52,7 @@ require_once '../lib/files.lib.php';
require_once DOL_DOCUMENT_ROOT.'/core/lib/admin.lib.php';
require_once DOL_DOCUMENT_ROOT.'/core/lib/date.lib.php';
require_once DOL_DOCUMENT_ROOT.'/core/lib/website.lib.php';
require_once DOL_DOCUMENT_ROOT.'/core/lib/product.lib.php';
$langs->load("mails");
@ -156,10 +157,11 @@ if (GETPOSTISSET('template')) {
$content);
$template = GETPOST('template', 'alpha');
// Get list of selected news or products
$selectedPostsStr = GETPOST('selectedPosts', 'alpha');
$selectedIdsStr = GETPOST('selectedPosts', 'alpha');
//$selectedPosts = array();
$selectedPosts = json_decode($selectedPostsStr);
$selectedIds = json_decode($selectedIdsStr);
/*if (is_array($selectedPostsStr)) {
$selectedPosts = explode(',', $selectedPostsStr);
}*/
@ -172,28 +174,43 @@ if (GETPOSTISSET('template')) {
}
} */
if (is_array($selectedPosts) && !empty($selectedPosts)) {
if (!empty($selectedIds) && !empty($template) && is_array($selectedIds) ) {
$newsList = '';
$productList = '';
foreach ($selectedIds as $Id) {
if ($template == "news") {
$post = getNewsDetailsById($Id);
foreach ($selectedPosts as $postId) {
$post = getNewsDetailsById($postId);
$newsList .= '<div style="display: flex; align-items: flex-start; justify-content: flex-start; width: 100%; max-width: 800px; margin-top: 20px;margin-bottom: 50px; padding: 20px;">
<div style="flex-grow: 1; margin-right: 30px; max-width: 600px; margin-left: 100px;">
<h2 style="margin: 0; font-size: 1.5em;">' . (empty($post['title']) ? '' : dol_htmlentitiesbr($post['title'])) . '</h2>
<p style="margin: 10px 0; color: #555;">' . (empty($post['description']) ? '' : dol_htmlentitiesbr($post['description'])) . '</p>
<span style="display: block; margin-bottom: 5px; color: #888;">Created By: <strong>' . dol_htmlentitiesbr(empty($post['user_fullname']) ? '' : $post['user_fullname']) . '</strong></span>
<br>
<span style="display: block; color: #888;">' . dol_print_date((empty($post['date_creation']) ? dol_now() : $post['date_creation']), 'daytext', 'tzserver', $langs) . '</span>
</div>
<div style="flex-shrink: 0; margin-left: 100px; float: right;">
' . (!empty($post['image']) ? '<img alt="Image" width="130px" height="130px" style="border-radius: 10px;" src="' . DOL_URL_ROOT . '/viewimage.php?modulepart=medias&file=' . dol_htmlentitiesbr($post['image']) . '">' : '<img alt="Gray rectangle" width="130px" height="130px" style="border-radius: 10px;" src="__GRAY_RECTANGLE__">') . '
</div>
</div>';
} elseif ($template == "product") {
$product = getProductForEmailTemplate($Id);
$newsList .= '<div style="display: flex; align-items: flex-start; justify-content: flex-start; width: 100%; max-width: 800px; margin-top: 20px;margin-bottom: 50px; padding: 20px;">
<div style="flex-grow: 1; margin-right: 30px; max-width: 600px; margin-left: 100px;">
<h2 style="margin: 0; font-size: 1.5em;">' . htmlentities(empty($post['title']) ? '' : $post['title']) . '</h2>
<p style="margin: 10px 0; color: #555;">' . htmlentities(empty($post['description']) ? '' : $post['description']) . '</p>
<span style="display: block; margin-bottom: 5px; color: #888;">Created By: <strong>' . htmlentities(empty($post['user_fullname']) ? '' : $post['user_fullname']) . '</strong></span>
<br>
<span style="display: block; color: #888;">' . dol_print_date((empty($post['date_creation']) ? dol_now() : $post['date_creation']), 'daytext', 'tzserver', $langs) . '</span>
</div>
<div style="flex-shrink: 0; margin-left: 100px; float: right;">
' . (!empty($post['image']) ? '<img alt="Image" width="130px" height="130px" style="border-radius: 10px;" src="' . DOL_URL_ROOT . '/viewimage.php?modulepart=medias&file=' . htmlentities($post['image']) . '">' : '<img alt="Gray rectangle" width="130px" height="130px" style="border-radius: 10px;" src="__GRAY_RECTANGLE__">') . '
</div>
</div>';
$productList .= '<div style="width:100%; padding: 20px;">
<div>
' . (!empty($product['image']) ? dol_htmlentitiesbr($product['image']) : '<img alt="Gray rectangle" width="130px" height="130px" style="border-radius: 10px;" src="__GRAY_RECTANGLE__">') . '
</div>
<div>
<h2>'.(dol_htmlentitiesbr($product["ref"])).(empty($product["label"]) ? '' : ' - '.dol_htmlentitiesbr($product["label"])).'</h2>
<p style="margin: 10px 0; color: #555;">'. (empty($product['description']) ? '' : dol_htmlentitiesbr($product['description'])) .'</p>
</div>
</div>
';
}
}
$content = str_replace('__NEWS_LIST__', $newsList, $content);
$content = str_replace('__PRODUCT_SELECTED__', $newsList, $content);
$content = str_replace('__PRODUCT_SELECTED__', $productList, $content);
} else {
$content = str_replace('__NEWS_LIST__', $langs->trans("SelectSomeArticlesOrEnterYourOwnContent"), $content);
$content = str_replace('__PRODUCT_SELECTED__', $langs->trans("SelectOneArticleOrEnterYourOwnContent"), $content);

View file

@ -11529,9 +11529,12 @@ abstract class CommonObject
require_once DOL_DOCUMENT_ROOT.'/categories/class/categorie.class.php';
$categorystatic = new Categorie($this->db);
$sql = "INSERT INTO ".$this->db->prefix()."categorie_".(empty($categorystatic->MAP_CAT_TABLE[$type]) ? $type : $categorystatic->MAP_CAT_TABLE[$type])." (fk_categorie, fk_product)";
$sql .= " SELECT fk_categorie, $toId FROM ".$this->db->prefix()."categorie_".(empty($categorystatic->MAP_CAT_TABLE[$type]) ? $type : $categorystatic->MAP_CAT_TABLE[$type]);
$sql .= " WHERE fk_product = ".((int) $fromId);
$tablename = $this->db->prefix()."categorie_".(empty($categorystatic->MAP_CAT_TABLE[$type]) ? $type : $categorystatic->MAP_CAT_TABLE[$type]);
$fkname = 'fk_' . (empty($categorystatic->MAP_CAT_FK[$type]) ? $type : $categorystatic->MAP_CAT_FK[$type]);
$sql = "INSERT INTO ".$tablename." (fk_categorie, ".$fkname.")";
$sql .= " SELECT fk_categorie, $toId FROM ".$tablename;
$sql .= " WHERE ".$fkname." = ".((int) $fromId);
if (!$this->db->query($sql)) {
$this->error = $this->db->lasterror();

View file

@ -1146,6 +1146,10 @@ class ExtraFields
$form = new Form($this->db);
}
if ($mode == 1) { // When field is used for search input into a list, we limit its size.
$morecss = 'maxwidth125';
}
$parameters = array(
'key' => $key,
'value' => &$value,

View file

@ -1578,11 +1578,28 @@ class FormMail extends Form
}
}
// Fetch Product / Services
$productArray = array();
if (isModEnabled('product') || isModEnabled('service')) {
include_once DOL_DOCUMENT_ROOT.'/core/class/html.form.class.php';
$form = new Form($this->db);
$arrayofproduct = $form->select_produits_list(0, 'product-select', '', 0, 0, '', 1, 2, 1);
if (!empty($arrayofproduct)) {
foreach ($arrayofproduct as $product) {
$productArray[$product["key"]] = array(
'id' => $product["key"],
'label' => $product["value"].' - '.dol_trunc($product["label2"], 40),
'labelhtml' => $product["value"].' - '.dol_trunc($product["label2"], 40),
);
}
}
}
// Use the multiselect array function to create the dropdown
$out .= '<div id="post-dropdown-container" class="email-layout-container hidden" style="height: 32px; display:none;">';
$out .= '<label for="blogpost-select">Select Posts: </label>';
$out .= '<!-- select component for selection of blog posts -->'."\n";
$out .= self::multiselectarray('blogpost-select', $blogArray, array(), 0, 0, 'minwidth200');
$out .= self::multiselectarray('blogpost-select', $blogArray, array(), 0, 0, 'minwidth200 select-template');
$out .= ' <input type="submit" class="smallpaddingimp button" name="submit" id="post-submit" value="'.dolPrintHTMLForAttribute($langs->trans("Select")).'">';
$out .= '</div>';
@ -1592,7 +1609,7 @@ class FormMail extends Form
$out .= '<div id="product-dropdown-container" class="email-layout-container hidden" style="height: 32px; display:none;">';
$out .= '<label for="product-select">'.img_picto('', 'product', 'class="pictofixedwidth"').$langs->trans("Product").' : </label>';
$out .= '<!-- select component for selection of product -->'."\n";
$out .= $form->select_produits(0, 'product-select', '', 0, 0, 1, 2, '', 0, array(), 0, '1', 0, '', 0, '', null, 1);
$out .= self::multiselectarray('product-select', $productArray, array(), 0, 0, 'minwidth200 select-template');
$out .= ' <input type="submit" class="smallpaddingimp button" name="submit" id="product-submit" value="'.dolPrintHTMLForAttribute($langs->trans("Select")).'">';
$out .= '</div>';
}
@ -1612,6 +1629,7 @@ class FormMail extends Form
$(".template-option").removeClass("selected");
$(this).addClass("selected");
$(".select-template").val("").trigger("change");
if (template === "news") {
$("#post-dropdown-container").show();
@ -1659,46 +1677,38 @@ class FormMail extends Form
updateSelectedPostsContent(contentHtml, selectedIds);
});
$("#product-select").change(function() {
var selectedIds = $(this).val();
var contentHtml = $(".template-option.selected").data("content");
updateSelectedPostsContent(contentHtml, selectedIds);
});
function updateSelectedPostsContent(contentHtml, selectedIds) {
var csrfToken = "' .newToken().'";
template = $(".template-option.selected").data("template");
var subject = $("#subject").val();
$.ajax({
type: "POST",
url: "'.dol_buildpath('/core/ajax/getnews.php', 1).'",
url: "'.dol_buildpath('/core/ajax/mailtemplate.php', 1).'",
data: {
selectedIds: JSON.stringify(selectedIds),
token : csrfToken
token: csrfToken,
template: template,
subject: subject,
selectedPosts: JSON.stringify(selectedIds)
},
success: function(response) {
var selectedPosts = JSON.parse(response);
var subject = $("#subject").val();
contentHtml = contentHtml.replace(/__SUBJECT__/g, subject);
template = $(".template-option.selected").data("template");
$.ajax({
type: "POST",
url: "'.dol_buildpath('/core/ajax/mailtemplate.php', 1).'",
data: {
token: csrfToken,
template: template,
subject: subject,
selectedPosts: JSON.stringify(selectedIds)
},
success: function(response) {
jQuery("#'.$htmlContent.'").val(response);
var editorInstance = CKEDITOR.instances["'.$htmlContent.'"];
if (editorInstance) {
editorInstance.setData(response);
}
},
error: function(xhr, status, error) {
console.error("An error occurred: " + xhr.responseText);
}
});
jQuery("#'.$htmlContent.'").val(response);
var editorInstance = CKEDITOR.instances["'.$htmlContent.'"];
if (editorInstance) {
editorInstance.setData(response);
}
},
error: function(xhr, status, error) {
console.error("An error occurred: " + xhr.responseText);
}
});
}
});
</script>';

View file

@ -167,6 +167,7 @@ class FormSetup
if ($editMode) {
$out .= '<form ' . self::generateAttributesStringFromArray($this->formAttributes) . ' >';
$out .= '<input type="hidden" name="page_y" value="">';
// generate hidden values from $this->formHiddenInputs
if (!empty($this->formHiddenInputs) && is_array($this->formHiddenInputs)) {

View file

@ -1925,16 +1925,21 @@ function dol_sanitizeFileName($str, $newstr = '_', $unaccent = 1, $includequotes
*
* @param string $str String to clean
* @param string $newstr String to replace bad chars with
* @param int $unaccent 1=Remove also accent (default), 0 do not remove them
* @param int $unaccent 1=Remove also accent, 0 do not remove them
* @param int $allowdash 1=Allow dash char after a space and before a string, 0 do not allow
* @return string String cleaned
*
* @see dol_string_nospecial(), dol_string_unaccent(), dol_sanitizeFileName()
*/
function dol_sanitizePathName($str, $newstr = '_', $unaccent = 1)
function dol_sanitizePathName($str, $newstr = '_', $unaccent = 0, $allowdash = 0)
{
// List of special chars for filenames in windows are defined on page https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file
// Char '>' '<' '|' '$' and ';' are special chars for shells.
// Chars '--' can be used into filename to inject special parameters like --use-compress-program to make command with file as parameter making remote execution of command
// Char '>' '<' '|' '$' ';' and '`' are special chars for shells.
// Char '?' and '*' are for wild card chars.
// Char '"' is dangerous.
// Char '°' is just not expected.
// Chars '-' and '--' can be used into filename to inject special parameters like --use-compress-program to make command with file as parameter making remote execution of command
// Chars '--' and '~' can be used for path transversal
$filesystem_forbidden_chars = array('<', '>', '?', '*', '|', '"', '°', '$', ';', '`');
$tmp = $str;
@ -1942,10 +1947,13 @@ function dol_sanitizePathName($str, $newstr = '_', $unaccent = 1)
$tmp = dol_string_unaccent($tmp);
}
$tmp = dol_string_nospecial($tmp, $newstr, $filesystem_forbidden_chars);
$tmp = preg_replace('/\-\-+/', '_', $tmp);
$tmp = preg_replace('/\s+\-([^\s])/', ' _$1', $tmp);
$tmp = preg_replace('/\s+\-$/', '', $tmp);
$tmp = str_replace('..', '', $tmp);
$tmp = preg_replace('/\-\-+/', $newstr, $tmp);
if (empty($allowdash)) {
$tmp = preg_replace('/\s+\-([^\s])/', ' '.$newstr.'$1', $tmp);
$tmp = preg_replace('/\s+\-$/', '', $tmp);
}
$tmp = str_replace('..', $newstr, $tmp);
$tmp = str_replace('~', $newstr, $tmp);
return $tmp;
}

View file

@ -1024,3 +1024,42 @@ function measuring_units_cubed($unit)
$measuring_units[99] = 89; // inch -> inch3
return $measuring_units[$unit];
}
/**
* Retrieve and return product for mail template.
*
* @param int $id The ID of the product to retrieve.
* @return array<string,mixed>|int<-1,-1> Return array if OK, -1 if KO
*/
function getProductForEmailTemplate($id)
{
require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
global $db, $conf;
$productarray = array();
$sql = "SELECT p.rowid as id, p.ref, p.label, p.description, p.entity";
$sql .= " FROM ".MAIN_DB_PREFIX."product as p";
$sql .= " WHERE p.entity IN (".getEntity('product').")";
$sql .= " AND p.rowid = ".((int) $id);
$resql = $db->query($sql);
if ($resql) {
$productarray = $db->fetch_array($resql);
} else {
dol_print_error($db);
return -1;
}
$object = new Product($db);
$result = $object->fetch($id);
if ($result < 0) {
dol_print_error($db, $object->error, $object->errors);
}
$entity = (empty($object->entity) ? $conf->entity : $object->entity);
$productarray["image"] = $object->show_photos('product', $conf->product->multidir_output[$entity], 1, 1, 0, 0, 0, 120, 160, 1, '');
if ($object->nbphoto <= 0) {
$productarray["image"] = "";
}
return $productarray;
}

View file

@ -2683,7 +2683,7 @@ class DolibarrModules // Can not be abstract, because we need to instantiate it
$return .= '</div>
<div class="info-box-content info-box-text-module'.(!getDolGlobalString($const_name) ? '' : ' info-box-module-enabled'.($versiontrans ? ' info-box-content-warning' : '')).'">
<span class="info-box-title">'.$this->getName().'</span>
<span class="info-box-title noopacity">'.$this->getName().'</span>
<span class="info-box-desc twolinesmax opacitymedium" title="'.dol_escape_htmltag($this->getDesc()).'">'.nl2br($this->getDesc()).'</span>';
$return .= '<div class="valignmiddle inline-block info-box-more">';

View file

@ -83,7 +83,7 @@ class modAccounting extends DolibarrModules
[
"MAIN_BANK_ACCOUNTANCY_CODE_ALWAYS_REQUIRED",
"chaine",
"1",
"0",
"With this constants on, bank account number is always required", 0, 'current', 1
],
[

View file

@ -457,7 +457,7 @@ if ($object->id > 0 && (empty($action) || ($action != 'edit' && $action != 'crea
include DOL_DOCUMENT_ROOT . '/core/tpl/extrafields_view.tpl.php';
} else {
// Login
print '<tr><td class="titlefield">'.$langs->trans("Login").'</td>';
print '<tr><td class="titlefieldmiddle">'.$langs->trans("Login").'</td>';
if (!empty($object->ldap_sid) && $object->statut == 0) {
print '<td class="error">';
print $langs->trans("LoginAccountDisableInDolibarr");

View file

@ -873,8 +873,8 @@ IMG;
*
* @param string $name Name of ODT file to generate before generating PDF
* @param int $dooutputfordownload Output the file content to make the download
* @throws OdfException
* @return void
* @throws OdfException
* @return void
*/
public function exportAsAttachedPDF($name = "", $dooutputfordownload = 1)
{
@ -888,12 +888,11 @@ IMG;
$execmethod = (getDolGlobalString('MAIN_EXEC_USE_POPEN') ? 2 : 1); // 1 or 2
// Method 1 sometimes hang the server.
// Export to PDF using LibreOffice
if (getDolGlobalString('MAIN_ODT_AS_PDF') == 'libreoffice') {
dol_mkdir($conf->user->dir_temp); // We must be sure the directory exists and is writable
// We delete and recreate a subdir because the soffice may have change pemrissions on it
// We delete and recreate a subdir because the soffice may have change permissions on it
$countdeleted = 0;
dol_delete_dir_recursive($conf->user->dir_temp.'/odtaspdf', 0, 0, 0, $countdeleted, 0, 1);
dol_mkdir($conf->user->dir_temp.'/odtaspdf');
@ -902,8 +901,10 @@ IMG;
// using windows libreoffice that must be in path
// using linux/mac libreoffice that must be in path
// Note PHP Config "fastcgi.impersonate=0" must set to 0 - Default is 1
$command ='soffice --headless -env:UserInstallation=file:'.(getDolGlobalString('MAIN_ODT_ADD_SLASH_FOR_WINDOWS') ? '///' : '').'\''.$conf->user->dir_temp.'/odtaspdf\' --convert-to pdf --outdir '. escapeshellarg(dirname($name)). " ".escapeshellarg($name);
$command ='soffice --headless -env:UserInstallation=file:'.escapeshellarg((getDolGlobalString('MAIN_ODT_ADD_SLASH_FOR_WINDOWS') ? '///' : '').dol_sanitizePathName($conf->user->dir_temp).'/odtaspdf').' --convert-to pdf --outdir '. escapeshellarg(dirname($name)). " ".escapeshellarg($name);
} elseif (preg_match('/unoconv/', getDolGlobalString('MAIN_ODT_AS_PDF'))) {
// This feature is now disabled by default. Must set var in conf.php to allow it.
global $dolibarr_main_allow_unoconv;
// If issue with unoconv, see https://github.com/dagwieers/unoconv/issues/87
// MAIN_ODT_AS_PDF should be "sudo -u unoconv /usr/bin/unoconv" and userunoconv must have sudo to be root by adding file /etc/sudoers.d/unoconv with content www-data ALL=(unoconv) NOPASSWD: /usr/bin/unoconv .
@ -927,18 +928,22 @@ IMG;
// If it fails:
// - set shell of user to bash instead of nologin.
// - set permission to read/write to user on home directory /var/www so user can create the libreoffice , dconf and .cache dir and files then set permission back
$command = getDolGlobalString('MAIN_ODT_AS_PDF').' '.escapeshellcmd($name);
//$command = '/usr/bin/unoconv -vvv '.escapeshellcmd($name);
if (!empty($dolibarr_main_allow_unoconv)) {
$command = dol_sanitizePathName(getDolGlobalString('MAIN_ODT_AS_PDF'), '_', 0, 1).' '.escapeshellarg($name);
//$command = '/usr/bin/unoconv -vvv '.escapeshellcmd($name);
} else {
throw new OdfException('Use of the unoconv method is deprecated. Try to use "libreoffice" method instead of set $dolibarr_main_allow_unoconv to 1 in conf.php for backward compatibility.');
}
} else {
// deprecated old method using odt2pdf.sh (native, jodconverter, ...)
$tmpname=preg_replace('/\.odt/i', '', $name);
$tmpname = dol_sanitizePathName(preg_replace('/\.odt/i', '', $name));
if (getDolGlobalString('MAIN_DOL_SCRIPTS_ROOT')) {
$command = getDolGlobalString('MAIN_DOL_SCRIPTS_ROOT').'/scripts/odt2pdf/odt2pdf.sh '.escapeshellcmd($tmpname).' '.(is_numeric(getDolGlobalString('MAIN_ODT_AS_PDF'))?'jodconverter':getDolGlobalString('MAIN_ODT_AS_PDF'));
$paramodt2pdf = (is_numeric(getDolGlobalString('MAIN_ODT_AS_PDF')) ? 'jodconverter' : getDolGlobalString('MAIN_ODT_AS_PDF'));
$paramodt2pdf = dol_sanitizePathName($paramodt2pdf);
$command = dol_sanitizePathName(getDolGlobalString('MAIN_DOL_SCRIPTS_ROOT')).'/scripts/odt2pdf/odt2pdf.sh '.escapeshellarg($tmpname).' '.escapeshellarg($paramodt2pdf);
} else {
dol_syslog(get_class($this).'::exportAsAttachedPDF is used but the constant MAIN_DOL_SCRIPTS_ROOT with path to script directory was not defined.', LOG_WARNING);
$command = '../../scripts/odt2pdf/odt2pdf.sh '.escapeshellcmd($tmpname).' '.(is_numeric(getDolGlobalString('MAIN_ODT_AS_PDF'))?'jodconverter':getDolGlobalString('MAIN_ODT_AS_PDF'));
throw new OdfException('Use of the ODT to PDF convertion with odt2pdf.sh script is deprecated when option MAIN_DOL_SCRIPTS_ROOT to define path of scripts directory is no set.');
}
}
@ -946,6 +951,7 @@ IMG;
//$command = DOL_DOCUMENT_ROOT.'/includes/odtphp/odt2pdf.sh '.$name.' '.$dirname;
dol_syslog(get_class($this).'::exportAsAttachedPDF $execmethod='.$execmethod.' Run command='.$command, LOG_DEBUG);
// TODO Use:
// $outputfile = DOL_DATA_ROOT.'/odt2pdf.log';
// $result = $utils->executeCLI($command, $outputfile); and replace test on $execmethod.
@ -953,17 +959,17 @@ IMG;
// $errorstring will be $result['output']
$retval=0; $output_arr=array();
if ($execmethod == 1) {
exec($command, $output_arr, $retval);
exec(escapeshellcmd($command), $output_arr, $retval);
}
if ($execmethod == 2) {
$outputfile = DOL_DATA_ROOT.'/odt2pdf.log';
$ok=0;
$handle = fopen($outputfile, 'w');
if ($handle) {
dol_syslog(get_class($this)."Run command ".$command, LOG_DEBUG);
dol_syslog(get_class($this)."escapeshellcmd(command) = ".escapeshellcmd($command), LOG_DEBUG);
fwrite($handle, $command."\n");
$handlein = popen($command, 'r');
$handlein = popen(escapeshellcmd($command), 'r');
while (!feof($handlein)) {
$read = fgets($handlein);
fwrite($handle, $read);
@ -987,6 +993,7 @@ IMG;
if (getDolGlobalString('MAIN_DISABLE_PDF_AUTOUPDATE')) {
$name = preg_replace('/\.od(x|t)/i', '', $name);
header('Content-type: application/pdf');
header('Content-Disposition: attachment; filename="' . basename($name) . '.pdf"');
readfile($name . ".pdf");
@ -1008,7 +1015,7 @@ IMG;
foreach ($output_arr as $line) {
$errorstring.= $line."<br>";
}
throw new OdfException('ODT to PDF convert fail (option MAIN_ODT_AS_PDF is '.$conf->global->MAIN_ODT_AS_PDF.', command was '.$command.', retval='.$retval.') : ' . $errorstring);
throw new OdfException('ODT to PDF convert fail (option MAIN_ODT_AS_PDF is '.getDolGlobalString('MAIN_ODT_AS_PDF').', command was '.$command.', retval='.$retval.') : ' . $errorstring);
}
}
}

View file

@ -448,8 +448,8 @@ if (!file_exists($conffile)) {
// Show title
if (getDolGlobalString('MAIN_VERSION_LAST_UPGRADE') || getDolGlobalString('MAIN_VERSION_LAST_INSTALL')) {
print $langs->trans("VersionLastUpgrade").' <b><span class="badge-text badge-secondary okinversed">'.getDolGlobalString('MAIN_VERSION_LAST_UPGRADE', getDolGlobalString('MAIN_VERSION_LAST_INSTALL')).'</span></b> &nbsp; - &nbsp; ';
print $langs->trans("VersionProgram").' <b><span class="badge-text badge-secondary okinversed">'.DOL_VERSION.'</span></b>';
print $langs->trans("VersionLastUpgrade").' <b><span class="badge-text okinversed">'.getDolGlobalString('MAIN_VERSION_LAST_UPGRADE', getDolGlobalString('MAIN_VERSION_LAST_INSTALL')).'</span></b> &nbsp; - &nbsp; ';
print $langs->trans("VersionProgram").' <b><span class="badge-text okinversed">'.DOL_VERSION.'</span></b>';
//print ' '.img_warning($langs->trans("RunningUpdateProcessMayBeRequired"));
print '<br>';
print '<br>';

View file

@ -45,7 +45,7 @@
}
.no-bottom {
padding-bottom: 0;
padding-bottom: 0 !important;
}
.small {

View file

@ -178,7 +178,7 @@ if ($action == "set") { // Test on permissions not required here
$morehtml = '';
pHeader($langs->trans("DolibarrSetup"), "step5", 'set', '', '', 'main-inside main-inside-borderbottom');
pHeader($langs->trans("DolibarrSetup"), "step5", 'set', '', '', 'main-inside main-inside-borderbottom no-bottom');
print '<br>';
// Test if we can run a first install process

View file

@ -7313,51 +7313,6 @@ class Product extends CommonObject
$return .= '</div>';
return $return;
}
/**
* Retrieve and display products.
*
* @param int $limit The maximum number of results to return.
* @return array<int, array<string, mixed>>|int return array if OK, -1 if KO
*/
public function getProductsToPreviewInEmail($limit)
{
if (!is_numeric($limit)) {
return -1;
}
$sql = "SELECT p.rowid, p.ref, p.label, p.description, p.entity, ef.filename
FROM ".MAIN_DB_PREFIX."product AS p
JOIN ".MAIN_DB_PREFIX."ecm_files AS ef ON p.rowid = ef.src_object_id
WHERE ef.entity IN (".getEntity('product').")
AND (ef.filename LIKE '%.png' OR ef.filename LIKE '%.jpeg' OR ef.filename LIKE '%.svg')
GROUP BY p.rowid, p.ref, p.label, p.description, p.entity, ef.filename
ORDER BY p.datec ASC
LIMIT " . ((int) $limit);
$resql = $this->db->query($sql);
$products = array();
if ($resql) {
while ($obj = $this->db->fetch_object($resql)) {
$products[] = array(
'rowid' => $obj->rowid,
'ref' => $obj->ref,
'label' => $obj->label,
'description' => $obj->description,
'entity' => $obj->entity,
'filename' => $obj->filename
);
}
} else {
dol_print_error($this->db);
}
if (empty($products)) {
return -1;
}
return $products;
}
}
/**

View file

@ -773,6 +773,9 @@ input.pageplusone {
.opacitytransp {
opacity: 0;
}
.noopacity {
opacity: unset !important;
}
.colorwhite {
color: var(--colorwhite);
}

View file

@ -944,6 +944,9 @@ input.pageplusone {
.opacitytransp {
opacity: 0;
}
.noopacity {
opacity: unset !important;
}
.colorwhite {
color: var(--colorwhite);
}

View file

@ -1,11 +1,11 @@
#!/bin/bash
# @copyright GPL License 2010 - Vikas Mahajan - http://vikasmahajan.wordpress.com
# @copyright GPL License 2013 - Florian HEnry - florian.henry@open-concept.pro
# @copyright GPL License 2017 - Laurent Destailleur - eldy@users.sourceforge.net
# @copyright GPL License 2019 - Camille Lafitte - cam.lafit@azerttyu.net
# Copyright (C) 2024 MDW <mdeweerd@users.noreply.github.com>
# Copyright (C) 2010 Vikas Mahajan http://vikasmahajan.wordpress.com
# Copyright (C) 2013 Florian HEnry florian.henry@open-concept.pro
# Copyright (C) 2017 Laurent Destailleur eldy@users.sourceforge.net
# Copyright (C) 2019 Camille Lafitte cam.lafit@azerttyu.net
# Copyright (C) 2024 MDW <mdeweerd@users.noreply.github.com>
#
# Convert an ODT into a PDF using "native" or "jodconverter" or "pyodconverter" or "unoconv" tool.
# Convert an ODT into a PDF using "native" or "jodconverter" or "pyodconverter".
# Dolibarr variable MAIN_ODT_AS_PDF must be defined ...
# to value "libreoffice" to call soffice native exporter feature (in such a case, this script is useless)
# or value "unoconv" to call unoconv CLI tool after ODT generation.
@ -21,8 +21,7 @@
if [ "$1" = "" ] || [ "$2" = "" ]
then
echo "Usage: odt2pdf.sh fullfilename [native|unoconv|jodconverter|pyodconverter|pathtojodconverterjar|pandoc]"
echo "Example: odt2pdf.sh myfile unoconv"
echo "Usage: odt2pdf.sh fullfilename [native|jodconverter|pyodconverter|pathtojodconverterjar|pandoc]"
echo "Example: odt2pdf.sh myfile ~/jodconverter/jodconverter-cli-2.2.2.jar"
exit
fi

View file

@ -2046,4 +2046,44 @@ class FunctionsLibTest extends CommonClassTest
$this->assertEquals("1", $result[0]);
$this->assertEquals("0", $result[1]);
}
/**
* testDolSanitizePathName
*
* @return void
*/
public function testDolSanitizePathName()
{
global $conf,$user,$langs,$db;
$conf = $this->savconf;
$user = $this->savuser;
$langs = $this->savlangs;
$db = $this->savdb;
$s = '../aéa/bbb ccc/ddd';
$result = dol_sanitizePathName($s);
$this->assertEquals('_/aéa/bbb ccc/ddd', $result);
$s = '../aéa/bbb ccc/ddd';
$result = dol_sanitizePathName($s, '_', 1);
$this->assertEquals('_/aea/bbb ccc/ddd', $result);
$s = 'C:\ccc/d\'d"d$';
$result = dol_sanitizePathName($s);
$this->assertEquals('C:\ccc/d\'d_d_', $result);
$s = 'C:\ccc/d\'d"d$';
$result = dol_sanitizePathName($s);
$this->assertEquals('C:\ccc/d\'d_d_', $result);
$s = '/aaa/bbb -a -b';
$result = dol_sanitizePathName($s);
$this->assertEquals('/aaa/bbb _a _b', $result);
$s = '/aaa/bbb -a -b';
$result = dol_sanitizePathName($s, '_', 0, 1);
$this->assertEquals('/aaa/bbb -a -b', $result);
}
}