Prechádzať zdrojové kódy

Merge branch 'Dolibarr:develop' into NEW-Disable_bad_reputation_product_price

IC-faycal 2 rokov pred
rodič
commit
9ffdce6f24
91 zmenil súbory, kde vykonal 1552 pridanie a 510 odobranie
  1. 2 2
      .travis.yml
  2. 45 1
      ChangeLog
  3. 1 1
      build/generate_filelist_xml.php
  4. 1 1
      htdocs/accountancy/journal/sellsjournal.php
  5. 15 15
      htdocs/adherents/admin/member_emails.php
  6. 1 2
      htdocs/comm/action/card.php
  7. 5 3
      htdocs/comm/action/class/actioncomm.class.php
  8. 4 9
      htdocs/core/ajax/ajaxdirpreview.php
  9. 1 1
      htdocs/core/ajax/ajaxdirtree.php
  10. 7 3
      htdocs/core/ajax/ajaxtooltip.php
  11. 3 0
      htdocs/core/ajax/bankconciliate.php
  12. 5 5
      htdocs/core/ajax/box.php
  13. 7 11
      htdocs/core/ajax/check_notifications.php
  14. 9 6
      htdocs/core/ajax/constantonoff.php
  15. 26 43
      htdocs/core/ajax/extraparams.php
  16. 1 1
      htdocs/core/ajax/fetchKnowledgeRecord.php
  17. 34 12
      htdocs/core/ajax/fileupload.php
  18. 29 15
      htdocs/core/ajax/flowjs-server.php
  19. 3 0
      htdocs/core/ajax/getaccountcurrency.php
  20. 25 1
      htdocs/core/ajax/loadinplace.php
  21. 5 2
      htdocs/core/ajax/locationincoterms.php
  22. 8 9
      htdocs/core/ajax/objectonoff.php
  23. 1 1
      htdocs/core/ajax/onlineSign.php
  24. 3 2
      htdocs/core/ajax/pingresult.php
  25. 4 0
      htdocs/core/ajax/price.php
  26. 2 0
      htdocs/core/ajax/row.php
  27. 24 1
      htdocs/core/ajax/saveinplace.php
  28. 5 2
      htdocs/core/ajax/security.php
  29. 30 20
      htdocs/core/ajax/selectobject.php
  30. 5 2
      htdocs/core/ajax/selectsearchbox.php
  31. 7 4
      htdocs/core/ajax/vatrates.php
  32. 13 9
      htdocs/core/ajax/ziptown.php
  33. 5 3
      htdocs/core/class/CMailFile.class.php
  34. 0 34
      htdocs/core/class/fileupload.class.php
  35. 69 50
      htdocs/core/class/html.form.class.php
  36. 1 1
      htdocs/core/class/html.formaccounting.class.php
  37. 7 4
      htdocs/core/class/html.formfile.class.php
  38. 2 2
      htdocs/core/class/html.formmail.class.php
  39. 9 6
      htdocs/core/class/html.formother.class.php
  40. 61 6
      htdocs/core/class/html.formsetup.class.php
  41. 1 1
      htdocs/core/db/pgsql.class.php
  42. 6 3
      htdocs/core/lib/admin.lib.php
  43. 2 2
      htdocs/core/lib/ajax.lib.php
  44. 173 15
      htdocs/core/lib/files.lib.php
  45. 52 26
      htdocs/core/lib/functions.lib.php
  46. 122 0
      htdocs/core/lib/modulebuilder.lib.php
  47. 4 3
      htdocs/core/lib/pdf.lib.php
  48. 22 3
      htdocs/core/lib/security.lib.php
  49. 4 3
      htdocs/core/modules/facture/doc/pdf_crabe.modules.php
  50. 4 3
      htdocs/core/modules/facture/doc/pdf_sponge.modules.php
  51. 5 0
      htdocs/core/modules/modEventOrganization.class.php
  52. 1 1
      htdocs/core/modules/modFacture.class.php
  53. 3 3
      htdocs/core/tpl/bloc_showhide.tpl.php
  54. 5 2
      htdocs/core/tpl/filemanager.tpl.php
  55. 2 0
      htdocs/core/website.inc.php
  56. 12 1
      htdocs/cron/class/cronjob.class.php
  57. 3 0
      htdocs/cron/list.php
  58. 1 0
      htdocs/expedition/index.php
  59. 1 1
      htdocs/fichinter/card.php
  60. 1 1
      htdocs/fourn/facture/card.php
  61. 1 0
      htdocs/hrm/index.php
  62. 56 32
      htdocs/includes/odtphp/odf.php
  63. 0 0
      htdocs/install/doctemplates/index.html
  64. 0 0
      htdocs/install/doctemplates/websites/index.html
  65. 13 8
      htdocs/install/mysql/migration/repair.sql
  66. 1 1
      htdocs/install/mysql/tables/llx_bank.sql
  67. 1 1
      htdocs/knowledgemanagement/class/knowledgerecord.class.php
  68. 1 1
      htdocs/langs/en_US/ecm.lang
  69. 2 0
      htdocs/langs/en_US/errors.lang
  70. 2 0
      htdocs/langs/en_US/main.lang
  71. 9 1
      htdocs/main.inc.php
  72. 0 1
      htdocs/master.inc.php
  73. 4 0
      htdocs/modulebuilder/index.php
  74. 1 1
      htdocs/modulebuilder/template/class/myobject.class.php
  75. 1 1
      htdocs/modulebuilder/template/myobject_card.php
  76. 7 7
      htdocs/product/stats/card.php
  77. 5 5
      htdocs/projet/activity/index.php
  78. 1 1
      htdocs/public/payment/newpayment.php
  79. 2 1
      htdocs/public/payment/paymentok.php
  80. 2 1
      htdocs/public/project/index.php
  81. 2 1
      htdocs/public/project/viewandvote.php
  82. 13 10
      htdocs/public/stripe/ipn.php
  83. 2 2
      htdocs/societe/card.php
  84. 10 9
      htdocs/takepos/admin/receipt.php
  85. 46 30
      htdocs/takepos/receipt.php
  86. 23 0
      htdocs/theme/eldy/global.inc.php
  87. 23 0
      htdocs/theme/md/style.css.php
  88. 34 37
      htdocs/user/class/user.class.php
  89. 1 0
      htdocs/user/list.php
  90. 1 1
      test/phpunit/DoliDBTest.php
  91. 379 0
      test/phpunit/ODFTest.php

+ 2 - 2
.travis.yml

@@ -473,7 +473,7 @@ script:
 
 - |
   echo "Unit testing"
-  # Ensure we catch errors. Set this to +e if you want to go to the end to see dolibarr.log file.
+  # Ensure we catch errors. Set this to +e instead of -e if you want to go to the end to see dolibarr.log file.
   set -e
   phpunit -d memory_limit=-1 -c test/phpunit/phpunittest.xml test/phpunit/AllTests.php
   phpunitresult=$?
@@ -501,7 +501,7 @@ after_failure:
   # Show upgrade log files
   for ficlog in `ls $TRAVIS_BUILD_DIR/*.log`
   do
-    echo "Debugging informations for file $ficlog"
+    #echo "Debugging informations for file $ficlog"
     #cat $ficlog
   done
   # Show Apache log file

+ 45 - 1
ChangeLog

@@ -232,7 +232,51 @@ Following changes may create regressions for some external modules, but were nec
 
 ***** ChangeLog for 16.0.5 compared to 16.0.4 *****
 
-TODO
+FIX: 16.0 propalestats Unknown column 'p.fk_soc' in 'on clause'
+FIX: #23804
+FIX: #23860
+FIX: #23966 Error "Param dbt_keyfield is required but not defined"
+FIX: accountancy lettering: better error management
+FIX: accountancy lettering: correctly calculated number of lettering operations done
+FIX: accountancy lettering: error management and prevention
+FIX: accountancy lettering: prevent null results when fetching link with payments
+FIX: Add missing hook on LibStatut
+FIX: Add more context for selectForFormsListWhere Hook
+FIX: attach file and send by mail in ticket
+FIX: bad check on if in get_all_ways
+FIX: Cannot import find type_fees with cgenericdic.class because it has id and not rowid
+FIX: clicktodial backtopage
+FIX: discount wasn't taken into account when adding a line in BOM
+FIX: expense reports: error when selecting mileage fees expense type if MAIN_USE_EXPENSE_IK disabled
+FIX: expense reports: JS error when selecting mileage fees expense type if MAIN_USE_EXPENSE_IK disabled
+FIX: Extrafields in Notes to unify with orders or invoices.
+FIX: fatal error on clicktodial backtopage
+FIX: filter sql accounting account
+FIX: Get data back on product update
+FIX: Get data back when error on command create
+FIX: label dictionary is used by barcode and member module
+FIX: mandatory date for service didnt work for invoice
+FIX: missing "authorid" for getNomUrl link right access
+FIX: missing getEntity filter
+FIX: vulnerability: missing protection on ajax public ticket page for valid email.
+FIX: Missing right to edit service note when module product is disabled
+FIX: multicompany compatibility
+FIX: object $user is not defined
+FIX: Object of class LDAP\Connection could not be converted to string
+FIX: parse error and NAN
+FIX: product ref fourn same size in supplier order/invoice as in product price fourn
+FIX: Profit calculation on project preview tab.
+FIX: Remove orphelan $this->db->rollback() in the function insertExtrafields()
+FIX: request new password with "mc" and "twofactor" authentication
+FIX: Resolve error message due to missing arguments
+FIX: select for task in event card
+FIX: several email sent to the same recipient when adding message from ticket
+FIX: shipping list for external user
+FIX: SQL error "unknown column p.fk_soc" because ANSI-92 joins take precedence over ANSI-89 joins
+FIX: strato pdf
+FIX: typos in getAttchments() $arrayobject
+FIX: whitespaces
+FIX: wrong url param name action
 
 
 ***** ChangeLog for 16.0.4 compared to 16.0.3 *****

+ 1 - 1
build/generate_filelist_xml.php

@@ -56,7 +56,7 @@ if (empty($argv[1])) {
 
 
 $i=0;
-$result=array();
+$result = array();
 while ($i < $argc) {
 	if (!empty($argv[$i])) {
 		parse_str($argv[$i], $result);	// set all params $release, $includecustom, $includeconstant, $buildzip ...

+ 1 - 1
htdocs/accountancy/journal/sellsjournal.php

@@ -158,7 +158,7 @@ if ($in_bookkeeping == 'notyet') {
 	$sql .= " AND f.rowid NOT IN (SELECT fk_doc FROM ".MAIN_DB_PREFIX."accounting_bookkeeping as ab WHERE ab.doc_type='customer_invoice')";
 	// $sql .= " AND fd.rowid NOT IN (SELECT fk_docdet FROM " . MAIN_DB_PREFIX . "accounting_bookkeeping as ab WHERE ab.doc_type='customer_invoice')";		// Useless, we save one line for all products with same account
 }
-$sql .= " ORDER BY f.datef";
+$sql .= " ORDER BY f.datef, f.ref";
 //print $sql; exit;
 
 dol_syslog('accountancy/journal/sellsjournal.php', LOG_DEBUG);

+ 15 - 15
htdocs/adherents/admin/member_emails.php

@@ -48,18 +48,23 @@ $action = GETPOST('action', 'aZ09');
 
 $error = 0;
 
+$helptext = '*'.$langs->trans("FollowingConstantsWillBeSubstituted").'<br>';
+$helptext .= '__DOL_MAIN_URL_ROOT__, __ID__, __FIRSTNAME__, __LASTNAME__, __FULLNAME__, __LOGIN__, __PASSWORD__, ';
+$helptext .= '__COMPANY__, __ADDRESS__, __ZIP__, __TOWN__, __COUNTRY__, __EMAIL__, __BIRTH__, __PHOTO__, __TYPE__, ';
+//$helptext.='__YEAR__, __MONTH__, __DAY__';	// Not supported
+
 // Editing global variables not related to a specific theme
 $constantes = array(
 	'MEMBER_REMINDER_EMAIL'=>array('type'=>'yesno', 'label'=>$langs->trans('MEMBER_REMINDER_EMAIL', $langs->transnoentities("Module2300Name"))),
-	'ADHERENT_EMAIL_TEMPLATE_REMIND_EXPIRATION' 	=>'emailtemplate:member',
-	'ADHERENT_EMAIL_TEMPLATE_AUTOREGISTER'			=>'emailtemplate:member',	// until Dolibarr 7 it was ADHERENT_AUTOREGISTER_MAIL
-	'ADHERENT_EMAIL_TEMPLATE_MEMBER_VALIDATION'		=>'emailtemplate:member',	// until Dolibarr 7 it was ADHERENT_MAIL_VALID
-	'ADHERENT_EMAIL_TEMPLATE_SUBSCRIPTION'			=>'emailtemplate:member',	// until Dolibarr 7 it was ADHERENT_MAIL_COTIS
-	'ADHERENT_EMAIL_TEMPLATE_CANCELATION'			=>'emailtemplate:member',	// until Dolibarr 7 it was ADHERENT_MAIL_RESIL
-	'ADHERENT_EMAIL_TEMPLATE_EXCLUSION'				=>'emailtemplate:member',
-	'ADHERENT_MAIL_FROM'							=>'string',
-	'ADHERENT_AUTOREGISTER_NOTIF_MAIL_SUBJECT'		=>'string',
-	'ADHERENT_AUTOREGISTER_NOTIF_MAIL'				=>'html',
+	'ADHERENT_EMAIL_TEMPLATE_REMIND_EXPIRATION' 	=>array('type'=>'emailtemplate:member'),
+	'ADHERENT_EMAIL_TEMPLATE_AUTOREGISTER'			=>array('type'=>'emailtemplate:member'),
+	'ADHERENT_EMAIL_TEMPLATE_MEMBER_VALIDATION'		=>array('type'=>'emailtemplate:member'),
+	'ADHERENT_EMAIL_TEMPLATE_SUBSCRIPTION'			=>array('type'=>'emailtemplate:member'),
+	'ADHERENT_EMAIL_TEMPLATE_CANCELATION'			=>array('type'=>'emailtemplate:member'),
+	'ADHERENT_EMAIL_TEMPLATE_EXCLUSION'				=>array('type'=>'emailtemplate:member'),
+	'ADHERENT_MAIL_FROM'							=>array('type'=>'string'),
+	'ADHERENT_AUTOREGISTER_NOTIF_MAIL_SUBJECT'		=>array('type'=>'string'),
+	'ADHERENT_AUTOREGISTER_NOTIF_MAIL'				=>array('type'=>'html', 'tooltip'=>$helptext)
 );
 
 
@@ -147,12 +152,7 @@ print '<form action="'.$_SERVER["PHP_SELF"].'" method="POST">';
 print '<input type="hidden" name="token" value="'.newToken().'">';
 print '<input type="hidden" name="action" value="updateall">';
 
-$helptext = '*'.$langs->trans("FollowingConstantsWillBeSubstituted").'<br>';
-$helptext .= '__DOL_MAIN_URL_ROOT__, __ID__, __FIRSTNAME__, __LASTNAME__, __FULLNAME__, __LOGIN__, __PASSWORD__, ';
-$helptext .= '__COMPANY__, __ADDRESS__, __ZIP__, __TOWN__, __COUNTRY__, __EMAIL__, __BIRTH__, __PHOTO__, __TYPE__, ';
-//$helptext.='__YEAR__, __MONTH__, __DAY__';	// Not supported
-
-form_constantes($constantes, 3, $helptext);
+form_constantes($constantes, 3, '');
 
 print '<div class="center"><input type="submit" class="button" value="'.$langs->trans("Update").'" name="update"></div>';
 print '</form>';

+ 1 - 2
htdocs/comm/action/card.php

@@ -32,7 +32,6 @@
 
 // Load Dolibarr environment
 require '../../main.inc.php';
-
 require_once DOL_DOCUMENT_ROOT.'/core/class/extrafields.class.php';
 require_once DOL_DOCUMENT_ROOT.'/core/class/html.form.class.php';
 require_once DOL_DOCUMENT_ROOT.'/core/class/html.formactions.class.php';
@@ -154,7 +153,7 @@ if (!empty($conf->global->AGENDA_REMINDER_EMAIL)) {
 
 $TDurationTypes = array('y'=>$langs->trans('Years'), 'm'=>$langs->trans('Month'), 'w'=>$langs->trans('Weeks'), 'd'=>$langs->trans('Days'), 'h'=>$langs->trans('Hours'), 'i'=>$langs->trans('Minutes'));
 
-$result = restrictedArea($user, 'agenda', $object->id, 'actioncomm&societe', 'myactions|allactions', 'fk_soc', 'id');
+$result = restrictedArea($user, 'agenda', $object, 'actioncomm&societe', 'myactions|allactions', 'fk_soc', 'id');
 
 $usercancreate = $user->hasRight('agenda', 'allactions', 'create') || (($object->authorid == $user->id || $object->userownerid == $user->id) && $user->rights->agenda->myactions->create);
 

+ 5 - 3
htdocs/comm/action/class/actioncomm.class.php

@@ -1319,15 +1319,17 @@ class ActionComm extends CommonObject
 	 */
 	public function getActions($socid = 0, $fk_element = 0, $elementtype = '', $filter = '', $sortfield = 'a.datep', $sortorder = 'DESC', $limit = 0)
 	{
-		global $conf, $langs;
+		global $conf, $langs, $hookmanager;
 
 		$resarray = array();
 
 		dol_syslog(get_class()."::getActions", LOG_DEBUG);
 
-		require_once DOL_DOCUMENT_ROOT . '/core/class/hookmanager.class.php';
-		$hookmanager = new HookManager($this->db);
 		// Initialize technical object to manage hooks of page. Note that conf->hooks_modules contains array of hook context
+		if (!is_object($hookmanager)) {
+			include_once DOL_DOCUMENT_ROOT.'/core/class/hookmanager.class.php';
+			$hookmanager = new HookManager($db);
+		}
 		$hookmanager->initHooks(array('agendadao'));
 
 		$sql = "SELECT a.id";

+ 4 - 9
htdocs/core/ajax/ajaxdirpreview.php

@@ -117,14 +117,14 @@ if (empty($url)) {	// autoset $url but it is better to have it defined before in
 // Load translation files required by the page
 $langs->loadLangs(array("ecm", "companies", "other"));
 
+if (empty($modulepart)) {
+	$modulepart = $module;
+}
+
 // Security check
 if ($user->socid > 0) {
 	$socid = $user->socid;
 }
-
-//print 'xxx'.$upload_dir;
-
-// Security:
 // On interdit les remontees de repertoire ainsi que les pipe dans les noms de fichiers.
 if (preg_match('/\.\./', $upload_dir) || preg_match('/[<>|]/', $upload_dir)) {
 	dol_syslog("Refused to deliver file ".$upload_dir);
@@ -132,11 +132,6 @@ if (preg_match('/\.\./', $upload_dir) || preg_match('/[<>|]/', $upload_dir)) {
 	dol_print_error(0, $langs->trans("ErrorFileNameInvalid", $upload_dir));
 	exit;
 }
-
-if (empty($modulepart)) {
-	$modulepart = $module;
-}
-
 // Check permissions
 if ($modulepart == 'ecm') {
 	if (!$user->hasRight('ecm', 'read')) {

+ 1 - 1
htdocs/core/ajax/ajaxdirtree.php

@@ -103,7 +103,7 @@ if (empty($modulepart)) {
 	$modulepart = $module;
 }
 
-// Check permissions
+// Security check
 if ($modulepart == 'ecm') {
 	if (!$user->hasRight('ecm', 'read')) {
 		accessforbidden();

+ 7 - 3
htdocs/core/ajax/ajaxtooltip.php

@@ -1,5 +1,5 @@
 <?php
-/* Copyright (C) 2007-2018  Laurent Destailleur     <eldy@users.sourceforge.net>
+/* Copyright (C) 2007-2023  Laurent Destailleur     <eldy@users.sourceforge.net>
  * Copyright (C) 2018-2023  Frédéric France         <frederic.france@netlogic.fr>
  *
  * This program is free software; you can redistribute it and/or modify
@@ -22,7 +22,6 @@
  *      \brief      This script returns content of tooltip
  */
 
-
 if (!defined('NOTOKENRENEWAL')) {
 	define('NOTOKENRENEWAL', 1); // Disables token renewal
 }
@@ -41,7 +40,7 @@ include_once DOL_DOCUMENT_ROOT.'/core/class/html.form.class.php';
 
 
 $id = GETPOST('id', 'aZ09');
-$objecttype = GETPOST('objecttype', 'aZ09');	// 'module' or 'myobject@mymodule', 'mymodule_myobject'
+$objecttype = GETPOST('objecttype', 'aZ09arobase');	// 'module' or 'myobject@mymodule', 'mymodule_myobject'
 
 $params = array();
 if (GETPOSTISSET('infologin')) {
@@ -53,6 +52,9 @@ if (GETPOSTISSET('option')) {
 
 // Load object according to $element
 $object = fetchObjectByElement($id, $objecttype);
+if (empty($object->element)) {
+	httponly_accessforbidden('Failed to get object from objecttype='.$objecttype.' id='.$id);
+}
 
 $module = $object->module;
 $element = $object->element;
@@ -62,6 +64,8 @@ if ($usesublevelpermission && !isset($user->rights->$module->$element)) {	// The
 	$usesublevelpermission = '';
 }
 
+//print $object->id.' - '.$object->module.' - '.$object->element.' - '.$object->table_element.' - '.$usesublevelpermission."\n";
+
 // Security check
 restrictedArea($user, $object->module, $object, $object->table_element, $usesublevelpermission);
 

+ 3 - 0
htdocs/core/ajax/bankconciliate.php

@@ -44,6 +44,9 @@ require_once DOL_DOCUMENT_ROOT.'/compta/bank/class/account.class.php';
 
 $action = GETPOST('action', 'aZ09');
 
+// Security check
+// Checks are done later
+
 
 /*
  * View

+ 5 - 5
htdocs/core/ajax/box.php

@@ -46,16 +46,16 @@ $boxorder = GETPOST('boxorder');
 $zone = GETPOST('zone', 'int');
 $userid = GETPOST('userid', 'int');
 
+// Security check
+if ($userid != $user->id) {
+	httponly_accessforbidden('Bad userid parameter. Must match logged user.');
+}
+
 
 /*
  * View
  */
 
-// Ajout directives pour resoudre bug IE
-//header('Cache-Control: Public, must-revalidate');
-//header('Pragma: public');
-
-//top_htmlhead("", "", 1);  // Replaced with top_httphead. An ajax page does not need html header.
 top_httphead();
 
 print '<!-- Ajax page called with url '.dol_escape_htmltag($_SERVER["PHP_SELF"]).'?'.dol_escape_htmltag($_SERVER["QUERY_STRING"]).' -->'."\n";

+ 7 - 11
htdocs/core/ajax/check_notifications.php

@@ -44,6 +44,9 @@ $time = dol_now();
 $action = GETPOST('action', 'aZ09');
 $listofreminderids = GETPOST('listofreminderids', 'aZ09');
 
+// Security check
+// No permission check at top, but action later are all done with a test on $user->id.
+
 
 /*
  * Actions
@@ -68,6 +71,7 @@ if ($action == 'stopreminder') {
 	// Clean database
 	$sql = 'DELETE FROM '.MAIN_DB_PREFIX.'actioncomm_reminder';
 	$sql .= " WHERE dateremind < '".$db->idate(dol_time_plus_duree(dol_now(), -1, 'm'))."'";
+	$sql .= " AND fk_user = ".((int) $user->id).' AND entity = '.((int) $conf->entity);
 	$resql = $db->query($sql);
 	if (!$resql) {
 		dol_print_error($db);
@@ -124,18 +128,10 @@ if (empty($_SESSION['auto_check_events_not_before']) || $time >= $_SESSION['auto
 
 	$sql = 'SELECT a.id as id_agenda, a.code, a.datep, a.label, a.location, ar.rowid as id_reminder, ar.dateremind, ar.fk_user as id_user_reminder';
 	$sql .= ' FROM '.MAIN_DB_PREFIX.'actioncomm as a';
-	if (!empty($user->conf->MAIN_USER_WANT_ALL_EVENTS_NOTIFICATIONS)) {
-		$sql .= ' LEFT JOIN '.MAIN_DB_PREFIX.'actioncomm_reminder as ar ON a.id = ar.fk_actioncomm AND ar.fk_user = '.((int) $user->id);
-		$sql .= ' WHERE a.code <> "AC_OTH_AUTO"';
-		$sql .= ' AND (';
-		$sql .= " ar.typeremind = 'browser' AND ar.dateremind < '".$db->idate(dol_now())."' AND ar.status = 0 AND ar.entity = ".$conf->entity;
-		$sql .= ' )';
-	} else {
-		$sql .= ' JOIN '.MAIN_DB_PREFIX.'actioncomm_reminder as ar ON a.id = ar.fk_actioncomm AND ar.fk_user = '.((int) $user->id);
-		$sql .= " AND ar.typeremind = 'browser' AND ar.dateremind < '".$db->idate(dol_now())."' AND ar.status = 0 AND ar.entity = ".$conf->entity;
-	}
+	$sql .= ' INNER JOIN '.MAIN_DB_PREFIX.'actioncomm_reminder as ar ON a.id = ar.fk_actioncomm AND ar.fk_user = '.((int) $user->id);
+	$sql .= " AND ar.typeremind = 'browser' AND ar.dateremind < '".$db->idate(dol_now())."' AND ar.status = 0 AND ar.entity = ".((int) $conf->entity);	// No sharing of entity for alerts
 	$sql .= $db->order('datep', 'ASC');
-	$sql .= ' LIMIT 10'; // Avoid too many notification at once
+	$sql .= $db->plimit(10); // Avoid too many notification at once
 
 	$resql = $db->query($sql);
 	if ($resql) {

+ 9 - 6
htdocs/core/ajax/constantonoff.php

@@ -52,6 +52,11 @@ $name = GETPOST('name', 'alpha');
 $entity = GETPOST('entity', 'int');
 $value = (GETPOST('value', 'aZ09') != '' ? GETPOST('value', 'aZ09') : 1);
 
+// Security check
+if (empty($user->admin)) {
+	httponly_accessforbidden('This ajax component can be called by admin user only');
+}
+
 
 /*
  * View
@@ -63,12 +68,10 @@ top_httphead();
 
 // Registering the new value of constant
 if (!empty($action) && !empty($name)) {
-	if ($user->admin) {
-		if ($action == 'set') {
-			dolibarr_set_const($db, $name, $value, 'chaine', 0, '', $entity);
-		} elseif ($action == 'del') {
-			dolibarr_del_const($db, $name, $entity);
-		}
+	if ($action == 'set') {
+		dolibarr_set_const($db, $name, $value, 'chaine', 0, '', $entity);
+	} elseif ($action == 'del') {
+		dolibarr_del_const($db, $name, $entity);
 	}
 } else {
 	http_response_code(403);

+ 26 - 43
htdocs/core/ajax/extraparams.php

@@ -17,7 +17,8 @@
 
 /**
  *	\file       /htdocs/core/ajax/extraparams.php
- *	\brief      File to make Ajax action on setting extra parameters of elements
+ *	\brief      File to make Ajax action on setting extra parameters of elements.
+ *				Called bu bloc_showhide.tpl.php, itself called when MAIN_DISABLE_CONTACTS_TAB or MAIN_DISABLE_NOTES_TAB are set
  */
 
 if (!defined('NOTOKENRENEWAL')) {
@@ -39,10 +40,29 @@ if (!defined('NOREQUIRESOC')) {
 include '../../main.inc.php';
 
 $id = GETPOST('id', 'int');
-$element = GETPOST('element', 'alpha');
+$element = GETPOST('element', 'aZ09arobase');
 $htmlelement = GETPOST('htmlelement', 'alpha');
 $type = GETPOST('type', 'alpha');
 
+// Load object according to $id and $element
+$object = fetchObjectByElement($id, $element);
+
+$module = $object->module;
+$element = $object->element;
+$usesublevelpermission = ($module != $element ? $element : '');
+if ($usesublevelpermission && !isset($user->rights->$module->$element)) {	// There is no permission on object defined, we will check permission on module directly
+	$usesublevelpermission = '';
+}
+
+//print $object->id.' - '.$object->module.' - '.$object->element.' - '.$object->table_element.' - '.$usesublevelpermission."\n";
+
+// Security check
+$result = restrictedArea($user, $object->module, $object, $object->table_element, $usesublevelpermission, 'fk_soc', 'rowid', 0, 1);	// Call with mode return
+if (!$result) {
+	httponly_accessforbidden('Not allowed by restrictArea');
+}
+
+
 /*
  * View
  */
@@ -57,47 +77,10 @@ if (!empty($id) && !empty($element) && !empty($htmlelement) && !empty($type)) {
 
 	dol_syslog("AjaxSetExtraParameters id=".$id." element=".$element." htmlelement=".$htmlelement." type=".$type." value=".$value, LOG_DEBUG);
 
-	$classpath = $subelement = $element;
-
-	// For compatibility
-	if ($element == 'order' || $element == 'commande') {
-		$classpath = $subelement = 'commande';
-	} elseif ($element == 'propal') {
-		$classpath = 'comm/propal';
-		$subelement = 'propal';
-	} elseif ($element == 'facture') {
-		$classpath = 'compta/facture';
-		$subelement = 'facture';
-	} elseif ($element == 'contract') {
-		$classpath = $subelement = 'contrat';
-	} elseif ($element == 'shipping') {
-		$classpath = $subelement = 'expedition';
-	} elseif ($element == 'deplacement') {
-		$classpath = 'compta/deplacement';
-		$subelement = 'deplacement';
-	} elseif ($element == 'order_supplier') {
-		$classpath = 'fourn';
-		$subelement = 'fournisseur.commande';
-	} elseif ($element == 'invoice_supplier') {
-		$classpath = 'fourn';
-		$subelement = 'fournisseur.facture';
-	}
-
-	dol_include_once('/'.$classpath.'/class/'.$subelement.'.class.php');
+	if (is_object($object)) {
+		$params[$htmlelement] = array($type => $value);
+		$object->extraparams = array_merge($object->extraparams, $params);
 
-	if ($element == 'order_supplier') {
-		$classname = 'CommandeFournisseur';
-	} elseif ($element == 'invoice_supplier') {
-		$classname = 'FactureFournisseur';
-	} else {
-		$classname = ucfirst($subelement);
+		$result = $object->setExtraParameters();
 	}
-
-	$object = new $classname($db);
-	$object->fetch($id);
-
-	$params[$htmlelement] = array($type => $value);
-	$object->extraparams = array_merge($object->extraparams, $params);
-
-	$result = $object->setExtraParameters();
 }

+ 1 - 1
htdocs/core/ajax/fetchKnowledgeRecord.php

@@ -55,7 +55,7 @@ $idticketgroup = GETPOST('idticketgroup', 'aZ09');
 $lang = GETPOST('lang', 'aZ09');
 
 // Security check
-if (!defined("NOLOGIN")) {	// No need for restrictedArea if not logged. Later the select will filter on public articles only if not logged.
+if (!defined("NOLOGIN")) {	// No need of restrictedArea if not logged: Later the select will filter on public articles only if not logged.
 	restrictedArea($user, 'knowledgemanagement', 0, 'knowledgemanagement_knowledgerecord', 'knowledgerecord');
 }
 

+ 34 - 12
htdocs/core/ajax/fileupload.php

@@ -19,24 +19,25 @@
 /**
  *       \file       htdocs/core/ajax/fileupload.php
  *       \brief      File to return Ajax response on file upload
- *
- *       Option MAIN_USE_JQUERY_FILEUPLOAD must be enabled to have this feature working. Use is NOT secured !
  */
 
-if (!defined('NOTOKENRENEWAL')) {
-	define('NOTOKENRENEWAL', '1');
-}
 if (!defined('NOREQUIREMENU')) {
 	define('NOREQUIREMENU', '1'); // If there is no menu to show
 }
 if (!defined('NOREQUIREHTML')) {
 	define('NOREQUIREHTML', '1'); // If we don't need to load the html.form.class.php
 }
-
+if (!defined('NOREQUIREAJAX')) {
+	define('NOREQUIREAJAX', '1');
+}
+if (!defined('NOREQUIRESOC')) {
+	define('NOREQUIRESOC', '1');
+}
 
 // Load Dolibarr environment
 require '../../main.inc.php';
 require_once DOL_DOCUMENT_ROOT.'/core/class/fileupload.class.php';
+require_once DOL_DOCUMENT_ROOT.'/core/class/genericobject.class.php';
 
 error_reporting(E_ALL | E_STRICT);
 
@@ -44,14 +45,33 @@ error_reporting(E_ALL | E_STRICT);
 //print_r($_GET);
 //print 'upload_dir='.GETPOST('upload_dir');
 
-$fk_element = GETPOST('fk_element', 'int');
-$element = GETPOST('element', 'alpha');
+$id = GETPOST('fk_element', 'int');
+$element = GETPOST('element', 'alpha');	// 'myobject' (myobject=mymodule) or 'myobject@mymodule' or 'myobject_mysubobject' (myobject=mymodule)
+$elementupload = $element;
 
-$upload_handler = new FileUpload(null, $fk_element, $element);
+// Load object according to $id and $element
+$object = fetchObjectByElement($id, $element);
 
-// Feature not enabled. Warning feature not used and not secured so disabled.
-if (!getDolGlobalInt('MAIN_USE_JQUERY_FILEUPLOAD')) {
-	return;
+$module = $object->module;
+$element = $object->element;
+$usesublevelpermission = ($module != $element ? $element : '');
+if ($usesublevelpermission && !isset($user->rights->$module->$element)) {	// There is no permission on object defined, we will check permission on module directly
+	$usesublevelpermission = '';
+}
+
+//print $object->id.' - '.$object->module.' - '.$object->element.' - '.$object->table_element.' - '.$usesublevelpermission."\n";
+
+// Security check
+if (!empty($user->socid)) {
+	$socid = $user->socid;
+	if (!empty($object->socid) && $socid != $object->socid) {
+		httponly_accessforbidden("Access on object not allowed for this external user.");	// This includes the exit.
+	}
+}
+
+$result = restrictedArea($user, $object->module, $object, $object->table_element, $usesublevelpermission, 'fk_soc', 'rowid', 0, 1);	// Call with mode return
+if (!$result) {
+	httponly_accessforbidden('Not allowed by restrictArea');
 }
 
 
@@ -59,6 +79,8 @@ if (!getDolGlobalInt('MAIN_USE_JQUERY_FILEUPLOAD')) {
  * View
  */
 
+$upload_handler = new FileUpload(null, $id, $elementupload);
+
 top_httphead();
 
 header('Pragma: no-cache');

+ 29 - 15
htdocs/core/ajax/flowjs-server.php

@@ -1,5 +1,5 @@
 <?php
-/* Copyright (C) 2012 Laurent Destailleur <eldy@users.sourceforge.net>
+/* Copyright (C) 2023 Laurent Destailleur <eldy@users.sourceforge.net>
  *
  * 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
@@ -16,8 +16,8 @@
  */
 
 /**
- *       \file       htdocs/core/ajax/bankconciliate.php
- *       \brief      File to set data for bank concilation
+ *       \file       htdocs/core/ajax/flowjs-server.php
+ *       \brief      File to upload very large file, higher than PHP limit. Using flowjs library.
  */
 
 if (!defined('NOTOKENRENEWAL')) {
@@ -46,20 +46,33 @@ require '../../main.inc.php';
 require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
 
 $action = GETPOST('action', 'aZ09');
-$module = GETPOST('module', 'aZ09');
-$upload_dir = GETPOST('upload_dir', 'alpha');
+
+$module = GETPOST('module', 'aZ09arobase');
+
 $flowFilename = GETPOST('flowFilename', 'alpha');
 $flowIdentifier = GETPOST('flowIdentifier', 'alpha');
 $flowChunkNumber = GETPOST('flowChunkNumber', 'alpha');
 $flowChunkSize = GETPOST('flowChunkSize', 'alpha');
 $flowTotalSize = GETPOST('flowTotalSize', 'alpha');
 
+$result = restrictedArea($user, $module, 0, '', 0, 'fk_soc', 'rowid', 0, 1);	// Call with mode return
+
+if ($action != 'upload') {
+	httponly_accessforbidden("Param action must be 'upload'");
+}
+
+if (!empty($conf->$module->dir_temp)) {
+	$upload_dir = $conf->$module->dir_temp;
+} else {
+	httponly_accessforbidden("Param module does not has a dir_temp directory. Module does not exists or is not activated.");
+}
+
 /*
  * Action
  */
 
-
 top_httphead();
+
 dol_syslog(join(',', $_GET));
 
 $result = false;
@@ -123,19 +136,19 @@ if ($result) {
 
 
 /**
- * Check if all the parts exist, and
- * gather all the parts of the file together
- * @param string    $temp_dir - the temporary directory holding all the parts of the file
- * @param string    $upload_dir - the temporary directory to create file
- * @param string    $fileName - the original file name
- * @param string    $chunkSize - each chunk size (in bytes)
- * @param string    $totalSize - original file size (in bytes)
- * @return bool     true if Ok false else
+ * Check if all the parts exist, and gather all the parts of the file together.
+ *
+ * @param string    $temp_dir 		the temporary directory holding all the parts of the file
+ * @param string    $upload_dir 	the temporary directory to create file
+ * @param string    $fileName 		the original file name
+ * @param string    $chunkSize 		each chunk size (in bytes)
+ * @param string    $totalSize 		original file size (in bytes)
+ * @return bool     				true if Ok false else
  */
 function createFileFromChunks($temp_dir, $upload_dir, $fileName, $chunkSize, $totalSize)
 {
-
 	dol_syslog(__METHOD__, LOG_DEBUG);
+
 	// count all the parts of this file
 	$total_files = 0;
 	$files = dol_dir_list($temp_dir, 'files');
@@ -164,5 +177,6 @@ function createFileFromChunks($temp_dir, $upload_dir, $fileName, $chunkSize, $to
 		// concurrent chunks uploads)
 		@rename($temp_dir, $temp_dir.'_UNUSED');
 	}
+
 	return true;
 }

+ 3 - 0
htdocs/core/ajax/getaccountcurrency.php

@@ -35,6 +35,9 @@ require '../../main.inc.php';
 
 $id = GETPOST('id', 'int');
 
+// Security check
+$result = restrictedArea($user, 'banque', $id, 'bank_account&bank_account');
+
 
 /*
  * View

+ 25 - 1
htdocs/core/ajax/loadinplace.php

@@ -17,7 +17,7 @@
 
 /**
  *       \file       htdocs/core/ajax/loadinplace.php
- *       \brief      File to load field value
+ *       \brief      File to load field value. used only when option "Edit In Place" is set (MAIN_USE_JQUERY_JEDITABLE).
  */
 
 if (!defined('NOTOKENRENEWAL')) {
@@ -41,6 +41,30 @@ $field = GETPOST('field', 'alpha');
 $element = GETPOST('element', 'alpha');
 $table_element = GETPOST('table_element', 'alpha');
 $fk_element = GETPOST('fk_element', 'alpha');
+$id = $fk_element;
+
+// Load object according to $id and $element
+$object = fetchObjectByElement($id, $element);
+
+$module = $object->module;
+$element = $object->element;
+$usesublevelpermission = ($module != $element ? $element : '');
+if ($usesublevelpermission && !isset($user->rights->$module->$element)) {	// There is no permission on object defined, we will check permission on module directly
+	$usesublevelpermission = '';
+}
+
+//print $object->id.' - '.$object->module.' - '.$object->element.' - '.$object->table_element.' - '.$usesublevelpermission."\n";
+
+// Security check
+$result = restrictedArea($user, $object->module, $object, $object->table_element, $usesublevelpermission, 'fk_soc', 'rowid', 0, 1);	// Call with mode return
+if (!$result) {
+	httponly_accessforbidden('Not allowed by restrictArea');
+}
+
+if (!getDolGlobalString('MAIN_USE_JQUERY_JEDITABLE')) {
+	httponly_accessforbidden('Can be used only when option MAIN_USE_JQUERY_JEDITABLE is set');
+}
+
 
 /*
  * View

+ 5 - 2
htdocs/core/ajax/locationincoterms.php

@@ -43,9 +43,12 @@ if (!defined('NOREQUIRESOC')) {
 require '../../main.inc.php';
 require_once DOL_DOCUMENT_ROOT.'/core/class/html.form.class.php';
 
+// Security check
 if (!isModEnabled('incoterm')) {
 	httponly_accessforbidden("Module incoterm not enabled");	// This includes the exit.
 }
+// There is no other permission on this component. Everybody connected can read content of the incoterm table
+
 
 /*
  * View
@@ -73,12 +76,12 @@ if (GETPOST('location_incoterms')) {
 	if (!empty($conf->global->MAIN_USE_LOCATION_INCOTERMS_DICTIONNARY)) {   // Use location_incoterms
 		$sql = "SELECT z.location as location_incoterms, z.label as label";
 		$sql .= " FROM ".MAIN_DB_PREFIX."c_location_incoterms as z";
-		$sql .= " WHERE z.active = 1  AND UPPER(z.location) LIKE UPPER('%".$db->escape($db->escapeforlike($location_incoterms))."%')";
+		$sql .= " WHERE z.active = 1 AND z.location LIKE '%".$db->escape($db->escapeforlike($location_incoterms))."%'";
 		$sql .= " ORDER BY z.location";
 		$sql .= $db->plimit(100); // Avoid pb with bad criteria
 	} else { // Use table of sale orders
 		$sql = "SELECT DISTINCT s.location_incoterms FROM ".MAIN_DB_PREFIX.'commande as s';
-		$sql .= " WHERE UPPER(s.location_incoterms) LIKE UPPER('%".$db->escape($db->escapeforlike($location_incoterms))."%')";
+		$sql .= " WHERE s.location_incoterms LIKE '%".$db->escape($db->escapeforlike($location_incoterms))."%'";
 
 		//Todo: merge with data from table of supplier order
 		/*	$sql .=" UNION";

+ 8 - 9
htdocs/core/ajax/objectonoff.php

@@ -47,19 +47,24 @@ require_once DOL_DOCUMENT_ROOT.'/core/class/genericobject.class.php';
 $action = GETPOST('action', 'aZ09');
 
 $id = GETPOST('id', 'int');
-$element = GETPOST('element', 'alpha');	// 'module' or 'myobject@mymodule' or 'mymodule_myobject'
+$element = GETPOST('element', 'alpha');	// 'myobject' (myobject=mymodule) or 'myobject@mymodule' or 'myobject_mysubobject' (myobject=mymodule)
 $field = GETPOST('field', 'alpha');
 $value = GETPOST('value', 'int');
 $format = 'int';
 
-// Load object according to $element
+// Load object according to $id and $element
 $object = fetchObjectByElement($id, $element);
 
 $object->fields[$field] = array('type' => $format, 'enabled' => 1);
 
 $module = $object->module;
 $element = $object->element;
-//var_dump($object->module); var_dump($object->element); var_dump($object->table_element);
+$usesublevelpermission = ($module != $element ? $element : '');
+if ($usesublevelpermission && !isset($user->rights->$module->$element)) {	// There is no permission on object defined, we will check permission on module directly
+	$usesublevelpermission = '';
+}
+
+//print $object->id.' - '.$object->module.' - '.$object->element.' - '.$object->table_element.' - '.$usesublevelpermission."\n";
 
 // Security check
 if (!empty($user->socid)) {
@@ -72,12 +77,6 @@ if (!empty($user->socid)) {
 // We check permission.
 // Check is done on $user->rights->element->create or $user->rights->element->subelement->create (because $action = 'set')
 if (preg_match('/status$/', $field)) {
-	$module = $object->module;
-	$element = $object->element;
-	$usesublevelpermission = ($module != $element ? $element : '');
-	if ($usesublevelpermission && !isset($user->rights->$module->$element)) {	// There is no permission on object defined, we will check permission on module directly
-		$usesublevelpermission = '';
-	}
 	restrictedArea($user, $object->module, $object, $object->table_element, $usesublevelpermission);
 } elseif ($element == 'product' && in_array($field, array('tosell', 'tobuy', 'tobatch'))) {	// Special case for products
 	restrictedArea($user, 'produit|service', $object, 'product&product', '', '', 'rowid');

+ 1 - 1
htdocs/core/ajax/onlineSign.php

@@ -66,7 +66,7 @@ $response = "";
 
 $type = $mode;
 
-// Check securitykey
+// Security check
 $securekeyseed = '';
 if ($type == 'proposal') {
 	$securekeyseed = getDolGlobalString('PROPOSAL_ONLINE_SIGNATURE_SECURITY_TOKEN');

+ 3 - 2
htdocs/core/ajax/pingresult.php

@@ -50,15 +50,16 @@ $hash_algo = GETPOST('hash_algo', 'alpha');
 
 
 // Security check
-// None.
+// None. Beeing connected is enough.
 
-$now = dol_now();
 
 
 /*
  * View
  */
 
+$now = dol_now();
+
 top_httphead();
 
 print '<!-- Ajax page called with url '.dol_escape_htmltag($_SERVER["PHP_SELF"]).'?'.dol_escape_htmltag($_SERVER["QUERY_STRING"]).' -->'."\n";

+ 4 - 0
htdocs/core/ajax/price.php

@@ -40,6 +40,10 @@ $output		= GETPOST('output', 'alpha');
 $amount		= price2num(GETPOST('amount', 'alpha'));
 $tva_tx		= str_replace('*', '', GETPOST('tva_tx', 'alpha'));
 
+// Security check
+// None. This is a formatting only component.
+
+
 /*
  * View
  */

+ 2 - 0
htdocs/core/ajax/row.php

@@ -49,7 +49,9 @@ if (!defined('NOREQUIRETRAN')) {
 // Load Dolibarr environment
 require '../../main.inc.php';
 require_once DOL_DOCUMENT_ROOT.'/core/class/genericobject.class.php';
+
 $hookmanager->initHooks(array('rowinterface'));
+
 // Security check
 // This is done later into view.
 

+ 24 - 1
htdocs/core/ajax/saveinplace.php

@@ -17,7 +17,7 @@
 
 /**
  *       \file       htdocs/core/ajax/saveinplace.php
- *       \brief      File to save field value
+ *       \brief      File to load field value. used only when option "Edit In Place" is set (MAIN_USE_JQUERY_JEDITABLE).
  */
 
 if (!defined('NOTOKENRENEWAL')) {
@@ -41,6 +41,7 @@ $field = GETPOST('field', 'alpha', 2);
 $element = GETPOST('element', 'alpha', 2);
 $table_element = GETPOST('table_element', 'alpha', 2);
 $fk_element = GETPOST('fk_element', 'alpha', 2);
+$id = $fk_element;
 
 /* Example:
 field:editval_ref_customer (8 first chars will removed to know name of property)
@@ -54,6 +55,28 @@ savemethod:
 savemethodname:
 */
 
+// Load object according to $id and $element
+$object = fetchObjectByElement($id, $element);
+
+$module = $object->module;
+$element = $object->element;
+$usesublevelpermission = ($module != $element ? $element : '');
+if ($usesublevelpermission && !isset($user->rights->$module->$element)) {	// There is no permission on object defined, we will check permission on module directly
+	$usesublevelpermission = '';
+}
+
+//print $object->id.' - '.$object->module.' - '.$object->element.' - '.$object->table_element.' - '.$usesublevelpermission."\n";
+
+// Security check
+$result = restrictedArea($user, $object->module, $object, $object->table_element, $usesublevelpermission, 'fk_soc', 'rowid', 0, 1);	// Call with mode return
+if (!$result) {
+	httponly_accessforbidden('Not allowed by restrictArea');
+}
+
+if (!getDolGlobalString('MAIN_USE_JQUERY_JEDITABLE')) {
+	httponly_accessforbidden('Can be used only when option MAIN_USE_JQUERY_JEDITABLE is set');
+}
+
 
 /*
  * View

+ 5 - 2
htdocs/core/ajax/security.php

@@ -17,8 +17,8 @@
 
 /**
  *       \file       htdocs/core/ajax/security.php
- *       \brief      This ajax component is used to generated hash keys for security purposes
- *                   like key to use into URL to protect them.
+ *       \brief      This ajax component is used to generated hash keys for security purposes,
+ *                   like the key to use into URL to protect them.
  */
 
 if (!defined('NOTOKENRENEWAL')) {
@@ -46,6 +46,9 @@ require '../../main.inc.php';
 
 $action = GETPOST('action');
 
+// Security check
+// None. This is public component with no effect on data.
+
 
 /*
  * View

+ 30 - 20
htdocs/core/ajax/selectobject.php

@@ -38,33 +38,18 @@ if (!defined('NOREQUIRESOC')) {
 
 // Load Dolibarr environment
 require '../../main.inc.php';
+require_once DOL_DOCUMENT_ROOT.'/core/class/html.form.class.php';
 
 $objectdesc = GETPOST('objectdesc', 'alpha');
 $htmlname = GETPOST('htmlname', 'aZ09');
 $outjson = (GETPOST('outjson', 'int') ? GETPOST('outjson', 'int') : 0);
 $id = GETPOST('id', 'int');
-$filter = GETPOST('filter', 'alphanohtml');
-
-
-/*
- * View
- */
-
-//print '<!-- Ajax page called with url '.dol_escape_htmltag($_SERVER["PHP_SELF"]).'?'.dol_escape_htmltag($_SERVER["QUERY_STRING"]).' -->'."\n";
-//print_r($_GET);
-
-require_once DOL_DOCUMENT_ROOT.'/core/class/html.form.class.php';
-$form = new Form($db);
-
-//$langs->load("companies");
-
-top_httphead();
+$filter = GETPOST('filter', 'alphanohtml');	// Universal Syntax filter
 
 if (empty($htmlname)) {
-	return;
+	httponly_accessforbidden('Bad value for param htmlname');
 }
 
-
 $InfoFieldList = explode(":", $objectdesc);
 $classname = $InfoFieldList[0];
 $classpath = $InfoFieldList[1];
@@ -75,16 +60,41 @@ if (!empty($classpath)) {
 	}
 }
 if (!is_object($objecttmp)) {
-	dol_syslog('Error bad param objectdesc', LOG_WARNING);
-	print 'Error bad param objectdesc';
+	httponly_accessforbidden('Bad value for param objectdesc');
 }
 
+/*
+// Load object according to $id and $element
+$object = fetchObjectByElement($id, $element);
+
+$module = $object->module;
+$element = $object->element;
+$usesublevelpermission = ($module != $element ? $element : '');
+if ($usesublevelpermission && !isset($user->rights->$module->$element)) {	// There is no permission on object defined, we will check permission on module directly
+	$usesublevelpermission = '';
+}
+*/
+
 // When used from jQuery, the search term is added as GET param "term".
 $searchkey = (($id && GETPOST($id, 'alpha')) ? GETPOST($id, 'alpha') : (($htmlname && GETPOST($htmlname, 'alpha')) ? GETPOST($htmlname, 'alpha') : ''));
 
 // Add a security test to avoid to get content of all tables
 restrictedArea($user, $objecttmp->element, $id);
 
+
+/*
+ * View
+ */
+
+//print '<!-- Ajax page called with url '.dol_escape_htmltag($_SERVER["PHP_SELF"]).'?'.dol_escape_htmltag($_SERVER["QUERY_STRING"]).' -->'."\n";
+//print_r($_GET);
+
+//$langs->load("companies");
+
+$form = new Form($db);
+
+top_httphead($outjson ? 'application/json' : 'text/html');
+
 $arrayresult = $form->selectForFormsList($objecttmp, $htmlname, '', 0, $searchkey, '', '', '', 0, 1, 0, '', $filter);
 
 $db->close();

+ 5 - 2
htdocs/core/ajax/selectsearchbox.php

@@ -1,5 +1,5 @@
 <?php
-/* Copyright (C) 2015-2018 Laurent Destailleur  <eldy@users.sourceforge.net>
+/* Copyright (C) 2015-2023 Laurent Destailleur  <eldy@users.sourceforge.net>
  *
  * 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
@@ -18,7 +18,7 @@
 /**
  *      \file       htdocs/core/ajax/selectsearchbox.php
  *      \ingroup    core
- *      \brief      This script returns content of possible search
+ *      \brief      This script returns json array of possible searches or just set the array if called by an include
  */
 
 // This script is called with a POST method or as an include.
@@ -43,6 +43,9 @@ if (!isset($usedbyinclude) || empty($usedbyinclude)) {
 
 	$res = @include '../../main.inc.php';
 
+	// Security check
+	// None. Beeing connected is enough.
+
 	top_httphead('application/json');
 
 	if ($res == 'ERROR_NOT_LOGGED') {

+ 7 - 4
htdocs/core/ajax/vatrates.php

@@ -17,7 +17,7 @@
 
 /**
  *       \file       htdocs/core/ajax/vatrates.php
- *       \brief      File to load vat rates combobox
+ *       \brief      File to load vat rates combobox according to thirdparty ID. Values are returned in JSON format.
  */
 
 if (!defined('NOTOKENRENEWAL')) {
@@ -34,16 +34,20 @@ if (!defined('NOREQUIREAJAX')) {
 require '../../main.inc.php';
 
 $id = GETPOST('id', 'int');
-$action = GETPOST('action', 'aZ09');
+$action = GETPOST('action', 'aZ09');	// 'getSellerVATRates' or 'getBuyerVATRates'
 $htmlname	= GETPOST('htmlname', 'alpha');
 $selected	= (GETPOST('selected') ?GETPOST('selected') : '-1');
 $productid = (GETPOST('productid', 'int') ?GETPOST('productid', 'int') : 0);
 
+// Security check
+$result = restrictedArea($user, 'societe', $id, '&societe', '', 'fk_soc', 'rowid', 0);
+
+
 /*
  * View
  */
 
-top_httphead();
+top_httphead('application/json');
 
 //print '<!-- Ajax page called with url '.dol_escape_htmltag($_SERVER["PHP_SELF"]).'?'.dol_escape_htmltag($_SERVER["QUERY_STRING"]).' -->'."\n";
 
@@ -63,7 +67,6 @@ if (!empty($id) && !empty($action) && !empty($htmlname)) {
 	}
 
 	$return = array();
-
 	$return['value']	= $form->load_tva('tva_tx', $selected, $seller, $buyer, $productid, 0, '', true);
 	$return['num'] = $form->num;
 	$return['error']	= $form->error;

+ 13 - 9
htdocs/core/ajax/ziptown.php

@@ -42,6 +42,11 @@ if (!defined('NOREQUIRESOC')) {
 require '../../main.inc.php';
 require_once DOL_DOCUMENT_ROOT.'/core/class/html.formcompany.class.php';
 
+// Security check
+if (!getDolGlobalString('MAIN_USE_ZIPTOWN_DICTIONNARY')) {
+	// If MAIN_USE_ZIPTOWN_DICTIONNARY is set, we make a search into a public page. If not we search into societe so we must check we have read permission.
+	$result = restrictedArea($user, 'societe', 0, '&societe', '', 'fk_soc', 'rowid', 0);
+}
 
 
 /*
@@ -53,11 +58,11 @@ require_once DOL_DOCUMENT_ROOT.'/core/class/html.formcompany.class.php';
 //header('Pragma: public');
 
 //top_htmlhead("", "", 1);  // Replaced with top_httphead. An ajax page does not need html header.
-top_httphead();
+top_httphead('application/json');
 
 //print '<!-- Ajax page called with url '.dol_escape_htmltag($_SERVER["PHP_SELF"]).'?'.dol_escape_htmltag($_SERVER["QUERY_STRING"]).' -->'."\n";
 
-dol_syslog('ziptown call with MAIN_USE_ZIPTOWN_DICTIONNARY='.(empty($conf->global->MAIN_USE_ZIPTOWN_DICTIONNARY) ? '' : $conf->global->MAIN_USE_ZIPTOWN_DICTIONNARY));
+dol_syslog('ziptown call with MAIN_USE_ZIPTOWN_DICTIONNARY='.getDolGlobalString('MAIN_USE_ZIPTOWN_DICTIONNARY'));
 //var_dump($_GET);
 
 // Generation of list of zip-town
@@ -69,7 +74,7 @@ if (GETPOST('zipcode') || GETPOST('town')) {
 	$zipcode = GETPOST('zipcode');
 	$town = GETPOST('town');
 
-	if (!empty($conf->global->MAIN_USE_ZIPTOWN_DICTIONNARY)) {   // Use zip-town table
+	if (getDolGlobalString('MAIN_USE_ZIPTOWN_DICTIONNARY')) {   // Use zip-town table
 		$sql = "SELECT z.rowid, z.zip, z.town, z.fk_county, z.fk_pays as fk_country";
 		$sql .= ", c.rowid as fk_country, c.code as country_code, c.label as country";
 		$sql .= ", d.rowid as fk_county, d.code_departement as county_code, d.nom as county";
@@ -80,15 +85,14 @@ if (GETPOST('zipcode') || GETPOST('town')) {
 		$sql .= " WHERE z.fk_pays = c.rowid";
 		$sql .= " AND z.active = 1 AND c.active = 1";
 		if ($zipcode) {
-			$sql .= " AND z.zip LIKE '".$db->escape($zipcode)."%'";
+			$sql .= " AND z.zip LIKE '".$db->escape($db->escapeforlike($zipcode))."%'";
 		}
 		if ($town) {
-			$sql .= " AND z.town LIKE '%".$db->escape($town)."%'";
+			$sql .= " AND z.town LIKE '%".$db->escape($db->escapeforlike($town))."%'";
 		}
 		$sql .= " ORDER BY z.zip, z.town";
 		$sql .= $db->plimit(100); // Avoid pb with bad criteria
-	} else // Use table of third parties
-	{
+	} else { // Use table of third parties
 		$sql = "SELECT DISTINCT s.zip, s.town, s.fk_departement as fk_county, s.fk_pays as fk_country";
 		$sql .= ", c.code as country_code, c.label as country";
 		$sql .= ", d.code_departement as county_code , d.nom as county";
@@ -97,10 +101,10 @@ if (GETPOST('zipcode') || GETPOST('town')) {
 		$sql .= " LEFT JOIN ".MAIN_DB_PREFIX.'c_country as c ON s.fk_pays = c.rowid';
 		$sql .= " WHERE";
 		if ($zipcode) {
-			$sql .= " s.zip LIKE '".$db->escape($zipcode)."%'";
+			$sql .= " s.zip LIKE '".$db->escape($db->escapeforlike($zipcode))."%'";
 		}
 		if ($town) {
-			$sql .= " s.town LIKE '%".$db->escape($town)."%'";
+			$sql .= " s.town LIKE '%".$db->escape($db->escapeforlike($town))."%'";
 		}
 		$sql .= " ORDER BY s.fk_pays, s.zip, s.town";
 		$sql .= $db->plimit(100); // Avoid pb with bad criteria

+ 5 - 3
htdocs/core/class/CMailFile.class.php

@@ -641,7 +641,7 @@ class CMailFile
 	 */
 	public function sendfile()
 	{
-		global $conf, $db, $langs;
+		global $conf, $db, $langs, $hookmanager;
 
 		$errorlevel = error_reporting();
 		//error_reporting($errorlevel ^ E_WARNING);   // Desactive warnings
@@ -649,8 +649,10 @@ class CMailFile
 		$res = false;
 
 		if (empty($conf->global->MAIN_DISABLE_ALL_MAILS)) {
-			require_once DOL_DOCUMENT_ROOT.'/core/class/hookmanager.class.php';
-			$hookmanager = new HookManager($db);
+			if (!is_object($hookmanager)) {
+				include_once DOL_DOCUMENT_ROOT.'/core/class/hookmanager.class.php';
+				$hookmanager = new HookManager($db);
+			}
 			$hookmanager->initHooks(array('mail'));
 
 			$parameters = array();

+ 0 - 34
htdocs/core/class/fileupload.class.php

@@ -19,8 +19,6 @@
 /**
  *       \file       htdocs/core/class/fileupload.class.php
  *       \brief      File to return Ajax response on file upload
- *
- *       Option MAIN_USE_JQUERY_FILEUPLOAD must be enabled to have feature working. Use is NOT secured !
  */
 
 require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
@@ -49,11 +47,6 @@ class FileUpload
 		global $object;
 		global $hookmanager;
 
-		// Feature not enabled. Warning feature not used and not secured so disabled.
-		if (!getDolGlobalInt('MAIN_USE_JQUERY_FILEUPLOAD')) {
-			return;
-		}
-
 		$hookmanager->initHooks(array('fileupload'));
 
 		$this->fk_element = $fk_element;
@@ -266,9 +259,6 @@ class FileUpload
 	 */
 	protected function getFileObject($file_name)
 	{
-		if (!getDolGlobalInt('MAIN_USE_JQUERY_FILEUPLOAD')) {
-			return null;
-		}
 
 		$file_path = $this->options['upload_dir'].$file_name;
 		if (is_file($file_path) && $file_name[0] !== '.') {
@@ -310,10 +300,6 @@ class FileUpload
 	{
 		global $maxwidthmini, $maxheightmini;
 
-		if (!getDolGlobalInt('MAIN_USE_JQUERY_FILEUPLOAD')) {
-			return false;
-		}
-
 		$file_path = $this->options['upload_dir'].$file_name;
 		$new_file_path = $options['upload_dir'].$file_name;
 
@@ -345,10 +331,6 @@ class FileUpload
 	 */
 	protected function validate($uploaded_file, $file, $error, $index)
 	{
-		if (!getDolGlobalInt('MAIN_USE_JQUERY_FILEUPLOAD')) {
-			return false;
-		}
-
 		if ($error) {
 			$file->error = $error;
 			return false;
@@ -464,10 +446,6 @@ class FileUpload
 	 */
 	protected function handleFileUpload($uploaded_file, $name, $size, $type, $error, $index)
 	{
-		if (!getDolGlobalInt('MAIN_USE_JQUERY_FILEUPLOAD')) {
-			return null;
-		}
-
 		$file = new stdClass();
 		$file->name = $this->trimFileName($name, $type, $index);
 		$file->mime = dol_mimetype($file->name, '', 2);
@@ -514,10 +492,6 @@ class FileUpload
 	 */
 	public function get()
 	{
-		if (!getDolGlobalInt('MAIN_USE_JQUERY_FILEUPLOAD')) {
-			return;
-		}
-
 		$file_name = isset($_REQUEST['file']) ?
 		basename(stripslashes($_REQUEST['file'])) : null;
 		if ($file_name) {
@@ -536,10 +510,6 @@ class FileUpload
 	 */
 	public function post()
 	{
-		if (!getDolGlobalInt('MAIN_USE_JQUERY_FILEUPLOAD')) {
-			return;
-		}
-
 		if (isset($_REQUEST['_method']) && $_REQUEST['_method'] === 'DELETE') {
 			return $this->delete();
 		}
@@ -595,10 +565,6 @@ class FileUpload
 	 */
 	public function delete()
 	{
-		if (!getDolGlobalInt('MAIN_USE_JQUERY_FILEUPLOAD')) {
-			return null;
-		}
-
 		$file_name = isset($_REQUEST['file']) ?
 		basename(stripslashes($_REQUEST['file'])) : null;
 		$file_path = $this->options['upload_dir'].$file_name;

+ 69 - 50
htdocs/core/class/html.form.class.php

@@ -1955,25 +1955,25 @@ class Form
 	/**
 	 *    Return select list of users
 	 *
-	 * @param string $selected User id or user object of user preselected. If 0 or < -2, we use id of current user. If -1, keep unselected (if empty is allowed)
-	 * @param string $htmlname Field name in form
-	 * @param int|string $show_empty 0=list with no empty value, 1=add also an empty value into list
-	 * @param array $exclude Array list of users id to exclude
-	 * @param int $disabled If select list must be disabled
-	 * @param array|string $include Array list of users id to include. User '' for all users or 'hierarchy' to have only supervised users or 'hierarchyme' to have supervised + me
-	 * @param array $enableonly Array list of users id to be enabled. If defined, it means that others will be disabled
-	 * @param string $force_entity '0' or Ids of environment to force
-	 * @param int $maxlength Maximum length of string into list (0=no limit)
-	 * @param int $showstatus 0=show user status only if status is disabled, 1=always show user status into label, -1=never show user status
-	 * @param string $morefilter Add more filters into sql request (Example: 'employee = 1'). This value must not come from user input.
-	 * @param integer $show_every 0=default list, 1=add also a value "Everybody" at beginning of list
-	 * @param string $enableonlytext If option $enableonlytext is set, we use this text to explain into label why record is disabled. Not used if enableonly is empty.
-	 * @param string $morecss More css
-	 * @param int $notdisabled Show only active users (this will also happened whatever is this option if USER_HIDE_INACTIVE_IN_COMBOBOX is on).
-	 * @param int $outputmode 0=HTML select string, 1=Array
-	 * @param bool $multiple add [] in the name of element and add 'multiple' attribut
-	 * @param int $forcecombo Force the component to be a simple combo box without ajax
-	 * @return    array|string                    HTML select string
+	 * @param string 		$selected 		User id or user object of user preselected. If 0 or < -2, we use id of current user. If -1, keep unselected (if empty is allowed)
+	 * @param string 		$htmlname 		Field name in form
+	 * @param int|string 	$show_empty 	0=list with no empty value, 1=add also an empty value into list
+	 * @param array 		$exclude 		Array list of users id to exclude
+	 * @param int 			$disabled 		If select list must be disabled
+	 * @param array|string 	$include 		Array list of users id to include. User '' for all users or 'hierarchy' to have only supervised users or 'hierarchyme' to have supervised + me
+	 * @param array|string	$enableonly 	Array list of users id to be enabled. If defined, it means that others will be disabled
+	 * @param string 		$force_entity 	'0' or Ids of environment to force
+	 * @param int 			$maxlength 		Maximum length of string into list (0=no limit)
+	 * @param int 			$showstatus 	0=show user status only if status is disabled, 1=always show user status into label, -1=never show user status
+	 * @param string 		$morefilter 	Add more filters into sql request (Example: 'employee = 1'). This value must not come from user input.
+	 * @param integer 		$show_every 	0=default list, 1=add also a value "Everybody" at beginning of list
+	 * @param string 		$enableonlytext If option $enableonlytext is set, we use this text to explain into label why record is disabled. Not used if enableonly is empty.
+	 * @param string 		$morecss 		More css
+	 * @param int 			$notdisabled 	Show only active users (this will also happened whatever is this option if USER_HIDE_INACTIVE_IN_COMBOBOX is on).
+	 * @param int 			$outputmode 	0=HTML select string, 1=Array
+	 * @param bool 			$multiple 		add [] in the name of element and add 'multiple' attribut
+	 * @param int 			$forcecombo 	Force the component to be a simple combo box without ajax
+	 * @return array|string                    HTML select string
 	 * @see select_dolgroups()
 	 */
 	public function select_dolusers($selected = '', $htmlname = 'userid', $show_empty = 0, $exclude = null, $disabled = 0, $include = '', $enableonly = '', $force_entity = '0', $maxlength = 0, $showstatus = 0, $morefilter = '', $show_every = 0, $enableonlytext = '', $morecss = '', $notdisabled = 0, $outputmode = 0, $multiple = false, $forcecombo = 0)
@@ -2013,6 +2013,7 @@ class Form
 
 		$out = '';
 		$outarray = array();
+		$outarray2 = array();
 
 		// Forge request to select users
 		$sql = "SELECT DISTINCT u.rowid, u.lastname as lastname, u.firstname, u.statut as status, u.login, u.admin, u.entity, u.photo";
@@ -2151,7 +2152,7 @@ class Form
 						}
 					}
 					$moreinfo .= ($moreinfo ? ')' : '');
-					$moreinfohtml .= ($moreinfohtml ? ')' : '');
+					$moreinfohtml .= ($moreinfohtml ? ')</span>' : '');
 					if ($disableline && $disableline != '1') {
 						// Add text from $enableonlytext parameter
 						$moreinfo .= ' - ' . $disableline;
@@ -2182,6 +2183,11 @@ class Form
 					$out .= '</option>';
 
 					$outarray[$userstatic->id] = $userstatic->getFullName($langs, $fullNameMode, -1, $maxlength) . $moreinfo;
+					$outarray2[$userstatic->id] = array(
+						'id'=>$userstatic->id,
+						'label'=>$labeltoshow,
+						'labelhtml'=>$labeltoshowhtml
+					);
 
 					$i++;
 				}
@@ -2200,7 +2206,9 @@ class Form
 			dol_print_error($this->db);
 		}
 
-		if ($outputmode) {
+		if ($outputmode == 2) {
+			return $outarray2;
+		} elseif ($outputmode) {
 			return $outarray;
 		}
 
@@ -6993,7 +7001,12 @@ class Form
 			if (empty($labeladddateof)) {
 				$labeladddateof = $langs->trans("DateInvoice");
 			}
-			$retstring .= ' - <button class="dpInvisibleButtons datenowlink" id="dateofinvoice" type="button" name="_dateofinvoice" value="now" onclick="console.log(\'Click on now link\'); jQuery(\'#re\').val(\'' . dol_print_date($adddateof, 'dayinputnoreduce') . '\');jQuery(\'#reday\').val(\'' . $tmparray['mday'] . '\');jQuery(\'#remonth\').val(\'' . $tmparray['mon'] . '\');jQuery(\'#reyear\').val(\'' . $tmparray['year'] . '\');">' . $labeladddateof . '</a>';
+			$reset_scripts = 'console.log(\'Click on now link\'); ';
+			$reset_scripts .= 'jQuery(\'#'.$prefix.'\').val(\''.dol_print_date($adddateof, 'dayinputnoreduce').'\');';
+			$reset_scripts .= 'jQuery(\'#'.$prefix.'day\').val(\''.$tmparray['mday'].'\');';
+			$reset_scripts .= 'jQuery(\'#'.$prefix.'month\').val(\''.$tmparray['mon'].'\');';
+			$reset_scripts .= 'jQuery(\'#'.$prefix.'year\').val(\''.$tmparray['year'].'\');';
+			$retstring .= ' - <button class="dpInvisibleButtons datenowlink" id="dateofinvoice" type="button" name="_dateofinvoice" value="now" onclick="'.$reset_scripts.'">'.$labeladddateof.'</a>';
 		}
 
 		return $retstring;
@@ -7874,20 +7887,20 @@ class Form
 	 * Output html form to select an object.
 	 * Note, this function is called by selectForForms or by ajax selectobject.php
 	 *
-	 * @param Object $objecttmp Object to knwo the table to scan for combo.
-	 * @param string $htmlname Name of HTML select component
-	 * @param int $preselectedvalue Preselected value (ID of element)
-	 * @param string $showempty ''=empty values not allowed, 'string'=value show if we allow empty values (for example 'All', ...)
-	 * @param string $searchkey Search value
-	 * @param string $placeholder Place holder
-	 * @param string $morecss More CSS
-	 * @param string $moreparams More params provided to ajax call
-	 * @param int $forcecombo Force to load all values and output a standard combobox (with no beautification)
-	 * @param int $outputmode 0=HTML select string, 1=Array
-	 * @param int $disabled 1=Html component is disabled
-	 * @param string $sortfield Sort field
-	 * @param string $filter Add more filter
-	 * @return    string|array                        Return HTML string
+	 * @param Object 		$objecttmp 			Object to knwo the table to scan for combo.
+	 * @param string 		$htmlname 			Name of HTML select component
+	 * @param int 			$preselectedvalue 	Preselected value (ID of element)
+	 * @param string 		$showempty 			''=empty values not allowed, 'string'=value show if we allow empty values (for example 'All', ...)
+	 * @param string 		$searchkey 			Search value
+	 * @param string 		$placeholder 		Place holder
+	 * @param string 		$morecss 			More CSS
+	 * @param string 		$moreparams 		More params provided to ajax call
+	 * @param int 			$forcecombo 		Force to load all values and output a standard combobox (with no beautification)
+	 * @param int 			$outputmode 		0=HTML select string, 1=Array
+	 * @param int 			$disabled 			1=Html component is disabled
+	 * @param string 		$sortfield 			Sort field
+	 * @param string 		$filter 			Add more filter
+	 * @return string|array                     Return HTML string
 	 * @see selectForForms()
 	 */
 	public function selectForFormsList($objecttmp, $htmlname, $preselectedvalue, $showempty = '', $searchkey = '', $placeholder = '', $morecss = '', $moreparams = '', $forcecombo = 0, $outputmode = 0, $disabled = 0, $sortfield = '', $filter = '')
@@ -8456,19 +8469,19 @@ class Form
 	/**
 	 *    Show a multiselect form from an array. WARNING: Use this only for short lists.
 	 *
-	 * @param string $htmlname Name of select
-	 * @param array $array Array(key=>value) or Array(key=>array('id'=> , 'label'=> ))
-	 * @param array $selected Array of keys preselected
-	 * @param int $key_in_label 1 to show key like in "[key] value"
-	 * @param int $value_as_key 1 to use value as key
-	 * @param string $morecss Add more css style
-	 * @param int $translate Translate and encode value
-	 * @param int|string $width Force width of select box. May be used only when using jquery couch. Example: 250, '95%'
-	 * @param string $moreattrib Add more options on select component. Example: 'disabled'
-	 * @param string $elemtype Type of element we show ('category', ...). Will execute a formating function on it. To use in readonly mode if js component support HTML formatting.
-	 * @param string $placeholder String to use as placeholder
-	 * @param int $addjscombo Add js combo
-	 * @return    string                        HTML multiselect string
+	 * @param 	string 		$htmlname 		Name of select
+	 * @param 	array 		$array 			Array(key=>value) or Array(key=>array('id'=> , 'label'=> , 'color'=> , 'picto'=> , 'labelhtml'=> ))
+	 * @param 	array 		$selected 		Array of keys preselected
+	 * @param 	int 		$key_in_label 	1 to show key like in "[key] value"
+	 * @param 	int 		$value_as_key 	1 to use value as key
+	 * @param 	string 		$morecss 		Add more css style
+	 * @param 	int 		$translate 		Translate and encode value
+	 * @param 	int|string 	$width 			Force width of select box. May be used only when using jquery couch. Example: 250, '95%'
+	 * @param 	string 		$moreattrib 	Add more options on select component. Example: 'disabled'
+	 * @param 	string 		$elemtype 		Type of element we show ('category', ...). Will execute a formating function on it. To use in readonly mode if js component support HTML formatting.
+	 * @param 	string 		$placeholder 	String to use as placeholder
+	 * @param 	int 		$addjscombo 	Add js combo
+	 * @return 	string                      HTML multiselect string
 	 * @see selectarray(), selectArrayAjax(), selectArrayFilter()
 	 */
 	public static function multiselectarray($htmlname, $array, $selected = array(), $key_in_label = 0, $value_as_key = 0, $morecss = '', $translate = 0, $width = 0, $moreattrib = '', $elemtype = '', $placeholder = '', $addjscombo = -1)
@@ -8503,11 +8516,13 @@ class Form
 					$tmpvalue = $value;
 					$tmpcolor = '';
 					$tmppicto = '';
+					$tmplabelhtml = '';
 					if (is_array($value) && array_key_exists('id', $value) && array_key_exists('label', $value)) {
 						$tmpkey = $value['id'];
 						$tmpvalue = $value['label'];
 						$tmpcolor = $value['color'];
 						$tmppicto = $value['picto'];
+						$tmplabelhtml = $value['labelhtml'];
 					}
 					$newval = ($translate ? $langs->trans($tmpvalue) : $tmpvalue);
 					$newval = ($key_in_label ? $tmpkey . ' - ' . $newval : $newval);
@@ -8516,7 +8531,11 @@ class Form
 					if (is_array($selected) && !empty($selected) && in_array((string) $tmpkey, $selected) && ((string) $tmpkey != '')) {
 						$out .= ' selected';
 					}
-					$out .= ' data-html="' . dol_escape_htmltag(($tmppicto ? img_picto('', $tmppicto, 'class="pictofixedwidth" style="color: #' . $tmpcolor . '"') : '') . $newval) . '"';
+					if (!empty($tmplabelhtml)) {
+						$out .= ' data-html="' . dol_escape_htmltag($tmplabelhtml) . '"';
+					} else {
+						$out .= ' data-html="' . dol_escape_htmltag(($tmppicto ? img_picto('', $tmppicto, 'class="pictofixedwidth" style="color: #' . $tmpcolor . '"') : '') . $newval) . '"';
+					}
 					$out .= '>';
 					$out .= dol_htmlentitiesbr($newval);
 					$out .= '</option>' . "\n";

+ 1 - 1
htdocs/core/class/html.formaccounting.class.php

@@ -451,7 +451,7 @@ class FormAccounting extends Form
 	 * @param string		$labelhtmlname	HTML name of label for autofill of account from name.
 	 * @return string       	   			String with HTML select
 	 */
-	public function select_auxaccount($selectid, $htmlname = 'account_num_aux', $showempty = 0, $morecss = 'maxwidth250', $usecache = '', $labelhtmlname = '')
+	public function select_auxaccount($selectid, $htmlname = 'account_num_aux', $showempty = 0, $morecss = 'minwidth100 maxwidth300 maxwidthonsmartphone', $usecache = '', $labelhtmlname = '')
 	{
 		// phpcs:enable
 

+ 7 - 4
htdocs/core/class/html.formfile.class.php

@@ -1457,10 +1457,13 @@ class FormFile
 									if ($modulepart == 'medias' && !GETPOST('website')) {
 										$moreparaminurl .= '&backtourl='.urlencode(DOL_URL_ROOT.'/ecm/index_medias.php?file_manager=1&modulepart='.$modulepart.'&section_dir='.$relativepath);
 									}
-									if ($modulepart == 'medias' && !GETPOST('website')) {
-										print '<a href="'.DOL_URL_ROOT.'/ecm/index_medias.php?action=confirmconvertimgwebp&token='.newToken().'&section_dir='.urlencode($relativepath).'&filetoregenerate='.urlencode($fileinfo['basename']).'&module='.$modulepart.$param.$moreparaminurl.'" title="'.dol_escape_htmltag($langs->trans("GenerateChosenImgWebp")).'">'.img_picto('', 'images', 'class="flip marginrightonly"').'</a>';
-									} elseif ($modulepart == 'medias' && GETPOST('website')) {
-										print '<a href="'.DOL_URL_ROOT.'/website/index.php?action=confirmconvertimgwebp&token='.newToken().'&section_dir='.urlencode($relativepath).'&filetoregenerate='.urlencode($fileinfo['basename']).'&module='.$modulepart.$param.$moreparaminurl.'" title="'.dol_escape_htmltag($langs->trans("GenerateChosenImgWebp")).'">'.img_picto('', 'images', 'class="flip marginrightonly"').'</a>';
+									// Link to convert into webp
+									if (!preg_match('/\.webp$/i', $file['name'])) {
+										if ($modulepart == 'medias' && !GETPOST('website')) {
+											print '<a href="'.DOL_URL_ROOT.'/ecm/index_medias.php?action=confirmconvertimgwebp&token='.newToken().'&section_dir='.urlencode($relativepath).'&filetoregenerate='.urlencode($fileinfo['basename']).'&module='.$modulepart.$param.$moreparaminurl.'" title="'.dol_escape_htmltag($langs->trans("GenerateChosenImgWebp")).'">'.img_picto('', 'images', 'class="flip marginrightonly"').'</a>';
+										} elseif ($modulepart == 'medias' && GETPOST('website')) {
+											print '<a href="'.DOL_URL_ROOT.'/website/index.php?action=confirmconvertimgwebp&token='.newToken().'&section_dir='.urlencode($relativepath).'&filetoregenerate='.urlencode($fileinfo['basename']).'&module='.$modulepart.$param.$moreparaminurl.'" title="'.dol_escape_htmltag($langs->trans("GenerateChosenImgWebp")).'">'.img_picto('', 'images', 'class="flip marginrightonly"').'</a>';
+										}
 									}
 								}
 							}

+ 2 - 2
htdocs/core/class/html.formmail.class.php

@@ -1324,10 +1324,10 @@ class FormMail extends Form
 			$sql .= " AND (lang = '".$dbs->escape($languagetosearch)."'".($languagetosearchmain ? " OR lang = '".$dbs->escape($languagetosearchmain)."'" : "")." OR lang IS NULL OR lang = '')";
 		}
 		if ($id > 0) {
-			$sql .= " AND rowid=".(int) $id;
+			$sql .= " AND rowid = ".(int) $id;
 		}
 		if ($id == -1) {
-			$sql .= " AND position=0";
+			$sql .= " AND position = 0";
 		}
 		if ($languagetosearch) {
 			$sql .= $dbs->order("position,lang,label", "ASC,DESC,ASC"); // We want line with lang set first, then with lang null or ''

+ 9 - 6
htdocs/core/class/html.formother.class.php

@@ -1289,18 +1289,21 @@ class FormOther
 					containment: \'document\',
 	        		connectWith: \'#boxhalfleft, #boxhalfright\',
 	        		stop: function(event, ui) {
+		        		console.log("We moved box so we call updateBoxOrder with ajax actions");
 	        			updateBoxOrder(1);  /* 1 to avoid message after a move */
 	        		}
 	    		});
 
 	        	jQuery(".boxclose").click(function() {
 	        		var self = this;	// because JQuery can modify this
-	        		var boxid=self.id.substring(8);
-	        		var label=jQuery(\'#boxlabelentry\'+boxid).val();
-	        		console.log("We close box "+boxid);
-	        		jQuery(\'#boxto_\'+boxid).remove();
-	        		if (boxid > 0) jQuery(\'#boxcombo\').append(new Option(label, boxid));
-	        		updateBoxOrder(1);  /* 1 to avoid message after a remove */
+	        		var boxid = self.id.substring(8);
+					if (boxid > 0) {
+		        		var label = jQuery(\'#boxlabelentry\'+boxid).val();
+		        		console.log("We close box "+boxid);
+	    	    		jQuery(\'#boxto_\'+boxid).remove();
+	        			jQuery(\'#boxcombo\').append(new Option(label, boxid));
+	        			updateBoxOrder(1);  /* 1 to avoid message after a remove */
+					}
 	        	});
 
         	});'."\n";

+ 61 - 6
htdocs/core/class/html.formsetup.class.php

@@ -834,6 +834,8 @@ class FormSetupItem
 			$out.= $this->generateInputFieldMultiSelect();
 		} elseif ($this->type == 'select') {
 			$out.= $this->generateInputFieldSelect();
+		} elseif ($this->type == 'selectUser') {
+			$out.= $this->generateInputFieldSelectUser();
 		} elseif ($this->type == 'textarea') {
 			$out.= $this->generateInputFieldTextarea();
 		} elseif ($this->type== 'html') {
@@ -993,6 +995,14 @@ class FormSetupItem
 		return $this->form->selectarray($this->confKey, $this->fieldOptions, $this->fieldValue);
 	}
 
+	/**
+	 * @return string
+	 */
+	public function generateInputFieldSelectUser()
+	{
+		return $this->form->select_dolusers($this->fieldValue, $this->confKey);
+	}
+
 	/**
 	 * get the type : used for old module builder setup conf style conversion and tests
 	 * because this two class will quickly evolve it's important to not set or get directly $this->type (will be protected) so this method exist
@@ -1067,6 +1077,8 @@ class FormSetupItem
 			$out.= $this->generateOutputFieldMultiSelect();
 		} elseif ($this->type == 'select') {
 			$out.= $this->generateOutputFieldSelect();
+		} elseif ($this->type == 'selectUser') {
+			$out.= $this->generateOutputFieldSelectUser();
 		} elseif ($this->type== 'html') {
 			$out.=  $this->fieldValue;
 		} elseif ($this->type== 'color') {
@@ -1136,6 +1148,8 @@ class FormSetupItem
 
 
 	/**
+	 * generateOutputFieldMultiSelect
+	 *
 	 * @return string
 	 */
 	public function generateOutputFieldMultiSelect()
@@ -1157,6 +1171,8 @@ class FormSetupItem
 	}
 
 	/**
+	 * generateOutputFieldColor
+	 *
 	 * @return string
 	 */
 	public function generateOutputFieldColor()
@@ -1165,6 +1181,8 @@ class FormSetupItem
 		return $this->generateInputField();
 	}
 	/**
+	 * generateInputFieldColor
+	 *
 	 * @return string
 	 */
 	public function generateInputFieldColor()
@@ -1174,6 +1192,8 @@ class FormSetupItem
 	}
 
 	/**
+	 * generateOutputFieldSelect
+	 *
 	 * @return string
 	 */
 	public function generateOutputFieldSelect()
@@ -1186,12 +1206,27 @@ class FormSetupItem
 		return $outPut;
 	}
 
+	/**
+	 * generateOutputFieldSelectUser
+	 *
+	 * @return string
+	 */
+	public function generateOutputFieldSelectUser()
+	{
+		$outPut = '';
+		$user = new User($this->db);
+		$user->fetch($this->fieldValue);
+		$outPut = $user->firstname . " "  . $user->lastname;
+		return $outPut;
+	}
+
 	/*
 	 * METHODS FOR SETTING DISPLAY TYPE
 	 */
 
 	/**
 	 * Set type of input as string
+	 *
 	 * @return self
 	 */
 	public function setAsString()
@@ -1202,6 +1237,7 @@ class FormSetupItem
 
 	/**
 	 * Set type of input as color
+	 *
 	 * @return self
 	 */
 	public function setAsColor()
@@ -1212,6 +1248,7 @@ class FormSetupItem
 
 	/**
 	 * Set type of input as textarea
+	 *
 	 * @return self
 	 */
 	public function setAsTextarea()
@@ -1222,6 +1259,7 @@ class FormSetupItem
 
 	/**
 	 * Set type of input as html editor
+	 *
 	 * @return self
 	 */
 	public function setAsHtml()
@@ -1232,6 +1270,7 @@ class FormSetupItem
 
 	/**
 	 * Set type of input as emailtemplate selector
+	 *
 	 * @param string $templateType email template type
 	 * @return self
 	 */
@@ -1243,6 +1282,7 @@ class FormSetupItem
 
 	/**
 	 * Set type of input as thirdparty_type selector
+	 *
 	 * @return self
 	 */
 	public function setAsThirdpartyType()
@@ -1253,6 +1293,7 @@ class FormSetupItem
 
 	/**
 	 * Set type of input as Yes
+	 *
 	 * @return self
 	 */
 	public function setAsYesNo()
@@ -1263,6 +1304,7 @@ class FormSetupItem
 
 	/**
 	 * Set type of input as secure key
+	 *
 	 * @return self
 	 */
 	public function setAsSecureKey()
@@ -1273,6 +1315,7 @@ class FormSetupItem
 
 	/**
 	 * Set type of input as product
+	 *
 	 * @return self
 	 */
 	public function setAsProduct()
@@ -1284,6 +1327,7 @@ class FormSetupItem
 	/**
 	 * Set type of input as a category selector
 	 * TODO add default value
+	 *
 	 * @param	int		$catType		Type of category ('customer', 'supplier', 'contact', 'product', 'member'). Old mode (0, 1, 2, ...) is deprecated.
 	 * @return self
 	 */
@@ -1294,8 +1338,8 @@ class FormSetupItem
 	}
 
 	/**
-	 * Set type of input as a simple title
-	 * no data to store
+	 * Set type of input as a simple title. No data to store
+	 *
 	 * @return self
 	 */
 	public function setAsTitle()
@@ -1306,8 +1350,8 @@ class FormSetupItem
 
 
 	/**
-	 * Set type of input as a simple title
-	 * no data to store
+	 * Set type of input as a simple title. No data to store
+	 *
 	 * @param array $fieldOptions A table of field options
 	 * @return self
 	 */
@@ -1322,8 +1366,8 @@ class FormSetupItem
 	}
 
 	/**
-	 * Set type of input as a simple title
-	 * no data to store
+	 * Set type of input as a simple title. No data to store
+	 *
 	 * @param array $fieldOptions  A table of field options
 	 * @return self
 	 */
@@ -1336,4 +1380,15 @@ class FormSetupItem
 		$this->type = 'select';
 		return $this;
 	}
+
+	/**
+	 * Set type of input as a simple title. No data to store
+	 *
+	 * @return self
+	 */
+	public function setAsSelectUser()
+	{
+		$this->type = 'selectUser';
+		return $this;
+	}
 }

+ 1 - 1
htdocs/core/db/pgsql.class.php

@@ -1225,7 +1225,7 @@ class DoliDBPgsql extends DoliDB
 	{
 		// phpcs:enable
 		$sql = "ALTER TABLE ".$table;
-		$sql .= " ALTER COLUMN '".$this->escape($field_name)."' TYPE ".$field_desc['type'];
+		$sql .= " ALTER COLUMN ".$this->escape($field_name)." TYPE ".$field_desc['type'];
 		if (preg_match("/^[^\s]/i", $field_desc['value'])) {
 			if (!in_array($field_desc['type'], array('smallint', 'int', 'date', 'datetime')) && $field_desc['value']) {
 				$sql .= "(".$field_desc['value'].")";

+ 6 - 3
htdocs/core/lib/admin.lib.php

@@ -1716,7 +1716,7 @@ function form_constantes($tableau, $strictw3c = 0, $helptext = '', $text = 'Valu
 
 			print '<tr class="oddeven">';
 
-			// Show constant
+			// Show label of parameter
 			print '<td>';
 			if (empty($strictw3c)) {
 				print '<input type="hidden" name="action" value="update">';
@@ -1725,8 +1725,11 @@ function form_constantes($tableau, $strictw3c = 0, $helptext = '', $text = 'Valu
 			print '<input type="hidden" name="constname'.(empty($strictw3c) ? '' : '[]').'" value="'.$const.'">';
 			print '<input type="hidden" name="constnote_'.$obj->name.'" value="'.nl2br(dol_escape_htmltag($obj->note)).'">';
 			print '<input type="hidden" name="consttype_'.$obj->name.'" value="'.($obj->type ? $obj->type : 'string').'">';
-
-			print ($label ? $label : $langs->trans('Desc'.$const));
+			if (!empty($tableau[$key]['tooltip'])) {
+				print $form->textwithpicto($label ? $label : $langs->trans('Desc'.$const), $tableau[$key]['tooltip']);
+			} else {
+				print ($label ? $label : $langs->trans('Desc'.$const));
+			}
 
 			if ($const == 'ADHERENT_MAILMAN_URL') {
 				print '. '.$langs->trans("Example").': <a href="#" id="exampleclick1">'.img_down().'</a><br>';

+ 2 - 2
htdocs/core/lib/ajax.lib.php

@@ -723,7 +723,7 @@ function ajax_object_onoff($object, $code, $field, $text_on, $text_off, $input =
                     action: \'set\',
                     field: \''.dol_escape_js($field).'\',
                     value: \'1\',
-                    element: \''.dol_escape_js(((empty($object->module) || $object->module == $object->element) ? '' : $object->module.'@').$object->element).'\',
+                    element: \''.dol_escape_js((empty($object->module) || $object->module == $object->element) ? $object->element : $object->element.'@'.$object->module).'\',
                     id: \''.((int) $object->id).'\',
 					token: \''.currentToken().'\'
                 },
@@ -755,7 +755,7 @@ function ajax_object_onoff($object, $code, $field, $text_on, $text_off, $input =
                     action: \'set\',
                     field: \''.dol_escape_js($field).'\',
                     value: \'0\',
-                    element: \''.dol_escape_js(((empty($object->module) || $object->module == $object->element) ? '' : $object->module.'@').$object->element).'\',
+                    element: \''.dol_escape_js((empty($object->module) || $object->module == $object->element) ? $object->element : $object->element.'@'.$object->module).'\',
                     id: \''.((int) $object->id).'\',
 					token: \''.currentToken().'\'
                 },

+ 173 - 15
htdocs/core/lib/files.lib.php

@@ -707,12 +707,14 @@ function dolReplaceInFile($srcfile, $arrayreplacement, $destfile = '', $newmask
  * @param	string	$destfile			Destination file (can't be a directory)
  * @param	int		$newmask			Mask for new file (0 by default means $conf->global->MAIN_UMASK). Example: '0666'
  * @param 	int		$overwriteifexists	Overwrite file if exists (1 by default)
+ * @param   int     $testvirus          Do an antivirus test. Move is canceled if a virus is found.
+ * @param	int		$indexdatabase		Index new file into database.
  * @return	int							<0 if error, 0 if nothing done (dest file already exists and overwriteifexists=0), >0 if OK
  * @see		dol_delete_file() dolCopyDir()
  */
-function dol_copy($srcfile, $destfile, $newmask = 0, $overwriteifexists = 1)
+function dol_copy($srcfile, $destfile, $newmask = 0, $overwriteifexists = 1, $testvirus = 0, $indexdatabase = 0)
 {
-	global $conf;
+	global $conf, $db, $user;
 
 	dol_syslog("files.lib.php::dol_copy srcfile=".$srcfile." destfile=".$destfile." newmask=".$newmask." overwriteifexists=".$overwriteifexists);
 
@@ -737,6 +739,17 @@ function dol_copy($srcfile, $destfile, $newmask = 0, $overwriteifexists = 1)
 		dol_syslog("files.lib.php::dol_copy failed Permission denied to write into target directory ".$newdirdestfile, LOG_WARNING);
 		return -2;
 	}
+
+	// Check virus
+	$testvirusarray = array();
+	if ($testvirus) {
+		$testvirusarray = dolCheckVirus($srcfile);
+		if (count($testvirusarray)) {
+			dol_syslog("files.lib.php::dol_copy canceled because a virus was found into source file. we ignore the copy request.", LOG_WARNING);
+			return -3;
+		}
+	}
+
 	// Copy with overwriting if exists
 	$result = @copy($newpathofsrcfile, $newpathofdestfile);
 	//$result=copy($newpathofsrcfile, $newpathofdestfile);	// To see errors, remove @
@@ -754,7 +767,64 @@ function dol_copy($srcfile, $destfile, $newmask = 0, $overwriteifexists = 1)
 
 	dolChmod($newpathofdestfile, $newmask);
 
-	return 1;
+	if ($result && $indexdatabase) {
+		// Add entry into ecm database
+		$rel_filetocopyafter = preg_replace('/^'.preg_quote(DOL_DATA_ROOT, '/').'/', '', $newpathofdestfile);
+		if (!preg_match('/([\\/]temp[\\/]|[\\/]thumbs|\.meta$)/', $rel_filetocopyafter)) {     // If not a tmp file
+			$rel_filetocopyafter = preg_replace('/^[\\/]/', '', $rel_filetocopyafter);
+			//var_dump($rel_filetorenamebefore.' - '.$rel_filetocopyafter);exit;
+
+			dol_syslog("Try to copy also entries in database for: ".$rel_filetocopyafter, LOG_DEBUG);
+			include_once DOL_DOCUMENT_ROOT.'/ecm/class/ecmfiles.class.php';
+
+			$ecmfiletarget = new EcmFiles($db);
+			$resultecmtarget = $ecmfiletarget->fetch(0, '', $rel_filetocopyafter);
+			if ($resultecmtarget > 0) {   // An entry for target name already exists for target, we delete it, a new one will be created.
+				dol_syslog("ECM dest file found, remove it", LOG_DEBUG);
+				$ecmfiletarget->delete($user);
+			} else {
+				dol_syslog("ECM dest file not found, create it", LOG_DEBUG);
+			}
+
+			$ecmSrcfile = new EcmFiles($db);
+			$resultecm  = $ecmSrcfile->fetch(0, '', $srcfile);
+			if ($resultecm) {
+				dol_syslog("Fetch src file ok", LOG_DEBUG);
+			} else {
+				dol_syslog("Fetch src file error", LOG_DEBUG);
+			}
+
+			$ecmfile = new EcmFiles($db);
+			$filename = basename($rel_filetocopyafter);
+			$rel_dir = dirname($rel_filetocopyafter);
+			$rel_dir = preg_replace('/[\\/]$/', '', $rel_dir);
+			$rel_dir = preg_replace('/^[\\/]/', '', $rel_dir);
+
+			$ecmfile->filepath = $rel_dir;
+			$ecmfile->filename = $filename;
+			$ecmfile->label = md5_file(dol_osencode($destfile)); // $destfile is a full path to file
+			$ecmfile->fullpath_orig = $srcfile;
+			$ecmfile->gen_or_uploaded = 'copy';
+			$ecmfile->description = $ecmSrcfile->description;
+			$ecmfile->keywords = $ecmSrcfile->keywords;
+			$resultecm = $ecmfile->create($user);
+			if ($resultecm < 0) {
+				dol_syslog("Create ECM file ok", LOG_DEBUG);
+				setEventMessages($ecmfile->error, $ecmfile->errors, 'warnings');
+			} else {
+				dol_syslog("Create ECM file error", LOG_DEBUG);
+				setEventMessages($ecmfile->error, $ecmfile->errors, 'warnings');
+			}
+
+			if ($resultecm > 0) {
+				$result = 1;
+			} else {
+				$result = -1;
+			}
+		}
+	}
+
+	return $result;
 }
 
 /**
@@ -3129,41 +3199,45 @@ function dol_check_secure_access_document($modulepart, $original_file, $entity,
 		// Define $accessallowed
 		$reg = array();
 		if (preg_match('/^([a-z]+)_user_temp$/i', $modulepart, $reg)) {
-			if (empty($conf->{$reg[1]}->dir_temp)) {	// modulepart not supported
+			$tmpmodule = $reg[1];
+			if (empty($conf->$tmpmodule->dir_temp)) {	// modulepart not supported
 				dol_print_error('', 'Error call dol_check_secure_access_document with not supported value for modulepart parameter ('.$modulepart.')');
 				exit;
 			}
-			if ($fuser->rights->{$reg[1]}->{$lire} || $fuser->rights->{$reg[1]}->{$read} || ($fuser->rights->{$reg[1]}->{$download})) {
+			if ($fuser->hasRight($tmpmodule, $lire) || $fuser->hasRight($tmpmodule, $read) || $fuser->hasRight($tmpmodule, $download)) {
 				$accessallowed = 1;
 			}
 			$original_file = $conf->{$reg[1]}->dir_temp.'/'.$fuser->id.'/'.$original_file;
 		} elseif (preg_match('/^([a-z]+)_temp$/i', $modulepart, $reg)) {
-			if (empty($conf->{$reg[1]}->dir_temp)) {	// modulepart not supported
+			$tmpmodule = $reg[1];
+			if (empty($conf->$tmpmodule->dir_temp)) {	// modulepart not supported
 				dol_print_error('', 'Error call dol_check_secure_access_document with not supported value for modulepart parameter ('.$modulepart.')');
 				exit;
 			}
-			if ($fuser->rights->{$reg[1]}->{$lire} || $fuser->rights->{$reg[1]}->{$read} || ($fuser->rights->{$reg[1]}->{$download})) {
+			if ($fuser->hasRight($tmpmodule, $lire) || $fuser->hasRight($tmpmodule, $read) || $fuser->hasRight($tmpmodule, $download)) {
 				$accessallowed = 1;
 			}
-			$original_file = $conf->{$reg[1]}->dir_temp.'/'.$original_file;
+			$original_file = $conf->$tmpmodule->dir_temp.'/'.$original_file;
 		} elseif (preg_match('/^([a-z]+)_user$/i', $modulepart, $reg)) {
-			if (empty($conf->{$reg[1]}->dir_output)) {	// modulepart not supported
+			$tmpmodule = $reg[1];
+			if (empty($conf->$tmpmodule->dir_output)) {	// modulepart not supported
 				dol_print_error('', 'Error call dol_check_secure_access_document with not supported value for modulepart parameter ('.$modulepart.')');
 				exit;
 			}
-			if ($fuser->rights->{$reg[1]}->{$lire} || $fuser->rights->{$reg[1]}->{$read} || ($fuser->rights->{$reg[1]}->{$download})) {
+			if ($fuser->hasRight($tmpmodule, $lire) || $fuser->hasRight($tmpmodule, $read) || $fuser->hasRight($tmpmodule, $download)) {
 				$accessallowed = 1;
 			}
-			$original_file = $conf->{$reg[1]}->dir_output.'/'.$fuser->id.'/'.$original_file;
+			$original_file = $conf->$tmpmodule->dir_output.'/'.$fuser->id.'/'.$original_file;
 		} elseif (preg_match('/^massfilesarea_([a-z]+)$/i', $modulepart, $reg)) {
-			if (empty($conf->{$reg[1]}->dir_output)) {	// modulepart not supported
+			$tmpmodule = $reg[1];
+			if (empty($conf->$tmpmodule->dir_output)) {	// modulepart not supported
 				dol_print_error('', 'Error call dol_check_secure_access_document with not supported value for modulepart parameter ('.$modulepart.')');
 				exit;
 			}
-			if ($fuser->rights->{$reg[1]}->{$lire} || preg_match('/^specimen/i', $original_file)) {
+			if ($fuser->hasRight($tmpmodule, $lire) || preg_match('/^specimen/i', $original_file)) {
 				$accessallowed = 1;
 			}
-			$original_file = $conf->{$reg[1]}->dir_output.'/temp/massgeneration/'.$user->id.'/'.$original_file;
+			$original_file = $conf->$tmpmodule->dir_output.'/temp/massgeneration/'.$user->id.'/'.$original_file;
 		} else {
 			if (empty($conf->$modulepart->dir_output)) {	// modulepart not supported
 				dol_print_error('', 'Error call dol_check_secure_access_document with not supported value for modulepart parameter ('.$modulepart.'). The module for this modulepart value may not be activated.');
@@ -3178,7 +3252,7 @@ function dol_check_secure_access_document($modulepart, $original_file, $entity,
 					$accessallowed = 1;
 				}
 			}
-			if (!empty($fuser->rights->$modulepart->{$lire}) || !empty($fuser->rights->$modulepart->{$read})) {
+			if ($fuser->hasRight($modulepart, $lire) || $fuser->hasRight($modulepart, $read)) {
 				$accessallowed = 1;
 			}
 
@@ -3316,3 +3390,87 @@ function getFilesUpdated(&$file_list, SimpleXMLElement $dir, $path = '', $pathre
 
 	return $file_list;
 }
+
+/**
+ * Function to manage the drag and drop of a file.
+ * We use global variable $object
+ *
+ * @param	string	$htmlname	The id of the component where we need to drag and drop
+ * @return  string				Js script to display
+ */
+function dragAndDropFileUpload($htmlname)
+{
+	global $object, $langs;
+
+	$out = "";
+	$out .= '<div id="'.$htmlname.'Message" class="dragDropAreaMessage hidden"><span>'.img_picto("", 'download').'<br>'.$langs->trans("DropFileToAddItToObject").'</span></div>';
+	$out .= "\n<!-- JS CODE TO ENABLE DRAG AND DROP OF FILE -->\n";
+	$out .= "<script>";
+	$out .= '
+		jQuery(document).ready(function() {
+			var enterTargetDragDrop = null;
+			$("#'.$htmlname.'").addClass("cssDragDropArea");
+			$(".cssDragDropArea").on("dragenter", function(ev) {
+				// Entering drop area. Highlight area
+				console.log("We add class highlightDragDropArea")
+				enterTargetDragDrop = ev.target;
+				$(this).addClass("highlightDragDropArea");
+				$("#'.$htmlname.'Message").removeClass("hidden");
+				ev.preventDefault();
+			});
+
+			$(".cssDragDropArea").on("dragleave", function(ev) {
+				// Going out of drop area. Remove Highlight
+				if (enterTargetDragDrop == ev.target){
+					console.log("We remove class highlightDragDropArea")
+					$("#'.$htmlname.'Message").addClass("hidden");
+					$(this).removeClass("highlightDragDropArea");
+				}
+			});
+
+			$(".cssDragDropArea").on("dragover", function(ev) {
+				ev.preventDefault();
+				return false;
+			});
+
+			$(".cssDragDropArea").on("drop", function(e) {
+				console.log("Trigger event file dropped. fk_element='.dol_escape_js($object->id).' element='.dol_escape_js($object->element).'");
+				e.preventDefault();
+				fd = new FormData();
+				fd.append("fk_element", "'.dol_escape_js($object->id).'");
+				fd.append("element", "'.dol_escape_js($object->element).'");
+				fd.append("token", "'.currentToken().'");
+				fd.append("action", "linkit");
+				var dataTransfer = e.originalEvent.dataTransfer;
+				if (dataTransfer.files && dataTransfer.files.length){
+					var droppedFiles = e.originalEvent.dataTransfer.files;
+					$.each(droppedFiles, function(index,file){
+						fd.append("files[]", file,file.name)
+					});
+				}
+				$(".cssDragDropArea").removeClass("highlightDragDropArea");
+				counterdragdrop = 0;
+				$.ajax({
+					url: "'.DOL_URL_ROOT.'/core/ajax/fileupload.php",
+					type: "POST",
+					processData: false,
+					contentType: false,
+					data: fd,
+					success:function() {
+						console.log("Uploaded.", arguments);
+						window.location.href = "'.$_SERVER["PHP_SELF"].'?id='.dol_escape_js($object->id).'&seteventmessages=UploadFileDragDropSuccess:mesgs";
+					},
+					error:function() {
+						console.log("Error Uploading.", arguments)
+						if (arguments[0].status == 403) {
+							window.location.href = "'.$_SERVER["PHP_SELF"].'?id='.dol_escape_js($object->id).'&seteventmessages=ErrorUploadPermissionDenied:errors";
+						}
+						window.location.href = "'.$_SERVER["PHP_SELF"].'?id='.dol_escape_js($object->id).'&seteventmessages=ErrorUploadFileDragDropPermissionDenied:errors";
+					},
+				})
+			});
+		});
+	';
+	$out .= "</script>\n";
+	return $out;
+}

+ 52 - 26
htdocs/core/lib/functions.lib.php

@@ -210,7 +210,8 @@ function getEntity($element, $shared = 1, $currentobject = null)
 {
 	global $conf, $mc, $hookmanager, $object, $action, $db;
 
-	if (! is_object($hookmanager)) {
+	if (!is_object($hookmanager)) {
+		include_once DOL_DOCUMENT_ROOT.'/core/class/hookmanager.class.php';
 		$hookmanager = new HookManager($db);
 	}
 
@@ -535,6 +536,7 @@ function GETPOSTISARRAY($paramname, $method = 0)
  *                               'alphanohtml'=check there is no html content and no " and no ../
  *                               'aZ'=check it's a-z only
  *                               'aZ09'=check it's simple alpha string (recommended for keys)
+ *                               'aZ09arobase'=check it's a string for an element type
  *                               'aZ09comma'=check it's a string for a sortfield or sortorder
  *                               'san_alpha'=Use filter_var with FILTER_SANITIZE_STRING (do not use this for free text string)
  *                               'nohtml'=check there is no html content
@@ -934,6 +936,14 @@ function sanitizeVal($out = '', $check = 'alphanohtml', $filter = null, $options
 				}
 			}
 			break;
+		case 'aZ09arobase':		// great to sanitize objecttype parameter
+			if (!is_array($out)) {
+				$out = trim($out);
+				if (preg_match('/[^a-z0-9_\-\.@]+/i', $out)) {
+					$out = '';
+				}
+			}
+			break;
 		case 'aZ09comma':		// great to sanitize sortfield or sortorder params that can be t.abc,t.def_gh
 			if (!is_array($out)) {
 				$out = trim($out);
@@ -1890,9 +1900,10 @@ function dol_fiche_head($links = array(), $active = '0', $title = '', $notab = 0
  *  @param	string	$morecss			More CSS on the link <a>
  *  @param	int		$limittoshow		Limit number of tabs to show. Use 0 to use automatic default value.
  *  @param	string	$moretabssuffix		A suffix to use when you have several dol_get_fiche_head() in same page
+ *  @param	int     $dragdropfile       0 (default) or 1. 1 enable a drop zone for file to be upload, 0 disable it
  * 	@return	string
  */
-function dol_get_fiche_head($links = array(), $active = '', $title = '', $notab = 0, $picto = '', $pictoisfullpath = 0, $morehtmlright = '', $morecss = '', $limittoshow = 0, $moretabssuffix = '')
+function dol_get_fiche_head($links = array(), $active = '', $title = '', $notab = 0, $picto = '', $pictoisfullpath = 0, $morehtmlright = '', $morecss = '', $limittoshow = 0, $moretabssuffix = '', $dragdropfile = 0)
 {
 	global $conf, $langs, $hookmanager;
 
@@ -2058,9 +2069,11 @@ function dol_get_fiche_head($links = array(), $active = '', $title = '', $notab
 	}
 
 	if (!$notab || $notab == -1 || $notab == -2 || $notab == -3) {
-		$out .= "\n".'<div class="tabBar'.($notab == -1 ? '' : ($notab == -2 ? ' tabBarNoTop' : (($notab == -3 ? ' noborderbottom' : '').' tabBarWithBottom'))).'">'."\n";
+		$out .= "\n".'<div id="dragDropAreaTabBar" class="tabBar'.($notab == -1 ? '' : ($notab == -2 ? ' tabBarNoTop' : (($notab == -3 ? ' noborderbottom' : '').' tabBarWithBottom'))).'">'."\n";
+	}
+	if (!empty($dragdropfile)) {
+		$out .= dragAndDropFileUpload("dragDropAreaTabBar");
 	}
-
 	$parameters = array('tabname' => $active, 'out' => $out);
 	$reshook = $hookmanager->executeHooks('printTabsHead', $parameters); // This hook usage is called just before output the head of tabs. Take also a look at "completeTabsHead"
 	if ($reshook > 0) {
@@ -7642,16 +7655,17 @@ function dol_concatdesc($text1, $text2, $forxml = false, $invert = false)
  * @param   int         $onlykey        1=Do not calculate some heavy values of keys (performance enhancement when we need only the keys), 2=Values are trunc and html sanitized (to use for help tooltip)
  * @param   array       $exclude        Array of family keys we want to exclude. For example array('system', 'mycompany', 'object', 'objectamount', 'date', 'user', ...)
  * @param   Object      $object         Object for keys on object
+ * @param   array       $include        Array of family keys we want to include. For example array('system', 'mycompany', 'object', 'objectamount', 'date', 'user', ...)
  * @return	array						Array of substitutions
  * @see setSubstitFromObject()
  */
-function getCommonSubstitutionArray($outputlangs, $onlykey = 0, $exclude = null, $object = null)
+function getCommonSubstitutionArray($outputlangs, $onlykey = 0, $exclude = null, $object = null, $include = null)
 {
 	global $db, $conf, $mysoc, $user, $extrafields;
 
 	$substitutionarray = array();
 
-	if (empty($exclude) || !in_array('user', $exclude)) {
+	if ((empty($exclude) || !in_array('user', $exclude)) && (empty($include) || in_array('user', $include))) {
 		// Add SIGNATURE into substitutionarray first, so, when we will make the substitution,
 		// this will include signature content first and then replace var found into content of signature
 		//var_dump($onlykey);
@@ -7680,7 +7694,7 @@ function getCommonSubstitutionArray($outputlangs, $onlykey = 0, $exclude = null,
 				));
 		}
 	}
-	if ((empty($exclude) || !in_array('mycompany', $exclude)) && is_object($mysoc)) {
+	if ((empty($exclude) || !in_array('mycompany', $exclude)) && is_object($mysoc) && (empty($include) || in_array('mycompany', $include))) {
 		$substitutionarray = array_merge($substitutionarray, array(
 			'__MYCOMPANY_NAME__'    => $mysoc->name,
 			'__MYCOMPANY_EMAIL__'   => $mysoc->email,
@@ -7704,7 +7718,7 @@ function getCommonSubstitutionArray($outputlangs, $onlykey = 0, $exclude = null,
 		));
 	}
 
-	if (($onlykey || is_object($object)) && (empty($exclude) || !in_array('object', $exclude))) {
+	if (($onlykey || is_object($object)) && (empty($exclude) || !in_array('object', $exclude)) && (empty($include) || in_array('object', $include))) {
 		if ($onlykey) {
 			$substitutionarray['__ID__'] = '__ID__';
 			$substitutionarray['__REF__'] = '__REF__';
@@ -7738,7 +7752,7 @@ function getCommonSubstitutionArray($outputlangs, $onlykey = 0, $exclude = null,
 				$substitutionarray['__THIRDPARTY_NOTE_PUBLIC__'] = '__THIRDPARTY_NOTE_PUBLIC__';
 				$substitutionarray['__THIRDPARTY_NOTE_PRIVATE__'] = '__THIRDPARTY_NOTE_PRIVATE__';
 			}
-			if (isModEnabled('adherent') && (!is_object($object) || $object->element == 'adherent')) {
+			if (isModEnabled('adherent') && (!is_object($object) || $object->element == 'adherent') && (empty($exclude) || !in_array('member', $exclude)) && (empty($include) || in_array('member', $include))) {
 				$substitutionarray['__MEMBER_ID__'] = '__MEMBER_ID__';
 				$substitutionarray['__MEMBER_CIVILITY__'] = '__MEMBER_CIVILITY__';
 				$substitutionarray['__MEMBER_FIRSTNAME__'] = '__MEMBER_FIRSTNAME__';
@@ -7748,7 +7762,7 @@ function getCommonSubstitutionArray($outputlangs, $onlykey = 0, $exclude = null,
 				$substitutionarray['__MEMBER_NOTE_PRIVATE__'] = '__MEMBER_NOTE_PRIVATE__';*/
 			}
 			// add variables subtitutions ticket
-			if (isModEnabled('ticket') && (!is_object($object) || $object->element == 'ticket')) {
+			if (isModEnabled('ticket') && (!is_object($object) || $object->element == 'ticket') && (empty($exclude) || !in_array('ticket', $exclude)) && (empty($include) || in_array('ticket', $include))) {
 				$substitutionarray['__TICKET_TRACKID__'] = '__TICKET_TRACKID__';
 				$substitutionarray['__TICKET_SUBJECT__'] = '__TICKET_SUBJECT__';
 				$substitutionarray['__TICKET_TYPE__'] = '__TICKET_TYPE__';
@@ -7760,28 +7774,28 @@ function getCommonSubstitutionArray($outputlangs, $onlykey = 0, $exclude = null,
 				$substitutionarray['__TICKET_USER_ASSIGN__'] = '__TICKET_USER_ASSIGN__';
 			}
 
-			if (isModEnabled('recruitment') && (!is_object($object) || $object->element == 'recruitmentcandidature')) {
+			if (isModEnabled('recruitment') && (!is_object($object) || $object->element == 'recruitmentcandidature') && (empty($exclude) || !in_array('recruitment', $exclude)) && (empty($include) || in_array('recruitment', $include))) {
 				$substitutionarray['__CANDIDATE_FULLNAME__'] = '__CANDIDATE_FULLNAME__';
 				$substitutionarray['__CANDIDATE_FIRSTNAME__'] = '__CANDIDATE_FIRSTNAME__';
 				$substitutionarray['__CANDIDATE_LASTNAME__'] = '__CANDIDATE_LASTNAME__';
 			}
-			if (isModEnabled('project')) {		// Most objects
+			if (isModEnabled('project') && (empty($exclude) || !in_array('project', $exclude)) && (empty($include) || in_array('project', $include))) {		// Most objects
 				$substitutionarray['__PROJECT_ID__'] = '__PROJECT_ID__';
 				$substitutionarray['__PROJECT_REF__'] = '__PROJECT_REF__';
 				$substitutionarray['__PROJECT_NAME__'] = '__PROJECT_NAME__';
 				/*$substitutionarray['__PROJECT_NOTE_PUBLIC__'] = '__PROJECT_NOTE_PUBLIC__';
 				$substitutionarray['__PROJECT_NOTE_PRIVATE__'] = '__PROJECT_NOTE_PRIVATE__';*/
 			}
-			if (isModEnabled('contrat') && (!is_object($object) || $object->element == 'contract')) {
+			if (isModEnabled('contrat') && (!is_object($object) || $object->element == 'contract') && (empty($exclude) || !in_array('contract', $exclude)) && (empty($include) || in_array('contract', $include))) {
 				$substitutionarray['__CONTRACT_HIGHEST_PLANNED_START_DATE__'] = 'Highest date planned for a service start';
 				$substitutionarray['__CONTRACT_HIGHEST_PLANNED_START_DATETIME__'] = 'Highest date and hour planned for service start';
 				$substitutionarray['__CONTRACT_LOWEST_EXPIRATION_DATE__'] = 'Lowest data for planned expiration of service';
 				$substitutionarray['__CONTRACT_LOWEST_EXPIRATION_DATETIME__'] = 'Lowest date and hour for planned expiration of service';
 			}
-			if (isModEnabled("propal") && (!is_object($object) || $object->element == 'propal')) {
+			if (isModEnabled("propal") && (!is_object($object) || $object->element == 'propal') && (empty($exclude) || !in_array('propal', $exclude)) && (empty($include) || in_array('propal', $include))) {
 				$substitutionarray['__ONLINE_SIGN_URL__'] = 'ToOfferALinkForOnlineSignature';
 			}
-			if (isModEnabled("ficheinter") && (!is_object($object) || $object->element == 'fichinter')) {
+			if (isModEnabled("ficheinter") && (!is_object($object) || $object->element == 'fichinter') && (empty($exclude) || !in_array('intervention', $exclude)) && (empty($include) || in_array('intervention', $include))) {
 				$substitutionarray['__ONLINE_SIGN_FICHINTER_URL__'] = 'ToOfferALinkForOnlineSignature';
 			}
 			$substitutionarray['__ONLINE_PAYMENT_URL__'] = 'UrlToPayOnlineIfApplicable';
@@ -8119,7 +8133,7 @@ function getCommonSubstitutionArray($outputlangs, $onlykey = 0, $exclude = null,
 			}
 		}
 	}
-	if (empty($exclude) || !in_array('objectamount', $exclude)) {
+	if ((empty($exclude) || !in_array('objectamount', $exclude)) && (empty($include) || in_array('objectamount', $include))) {
 		include_once DOL_DOCUMENT_ROOT.'/core/lib/functionsnumtoword.lib.php';
 
 		$substitutionarray['__DATE_YMD__']        = is_object($object) ? (isset($object->date) ? dol_print_date($object->date, 'day', 0, $outputlangs) : null) : '';
@@ -8175,7 +8189,7 @@ function getCommonSubstitutionArray($outputlangs, $onlykey = 0, $exclude = null,
 	}
 
 	//var_dump($substitutionarray['__AMOUNT_FORMATED__']);
-	if (empty($exclude) || !in_array('date', $exclude)) {
+	if ((empty($exclude) || !in_array('date', $exclude)) && (empty($include) || in_array('date', $include))) {
 		include_once DOL_DOCUMENT_ROOT.'/core/lib/date.lib.php';
 
 		$now = dol_now();
@@ -8212,7 +8226,7 @@ function getCommonSubstitutionArray($outputlangs, $onlykey = 0, $exclude = null,
 	if (isModEnabled('multicompany')) {
 		$substitutionarray = array_merge($substitutionarray, array('__ENTITY_ID__' => $conf->entity));
 	}
-	if (empty($exclude) || !in_array('system', $exclude)) {
+	if ((empty($exclude) || !in_array('system', $exclude)) && (empty($include) || in_array('user', $include))) {
 		$substitutionarray['__DOL_MAIN_URL_ROOT__'] = DOL_MAIN_URL_ROOT;
 		$substitutionarray['__(AnyTranslationKey)__'] = $outputlangs->trans('TranslationOfKey');
 		$substitutionarray['__(AnyTranslationKey|langfile)__'] = $outputlangs->trans('TranslationOfKey').' (load also language file before)';
@@ -11194,7 +11208,10 @@ function dolGetButtonTitle($label, $helpText = '', $iconClass = 'fa fa-file', $u
 /**
  * Get an array with properties of an element.
  *
- * @param   string 	$element_type 	Element type (Value of $object->element). Example: 'action', 'facture', 'project_task', 'myobject@mymodule' or 'mymodule_myobject' ...
+ * @param   string 	$element_type 	Element type (Value of $object->element). Example:
+ * 									'action', 'facture', 'project_task',
+ * 									'myobject@mymodule' or
+ * 									'myobject_mysubobject' (where mymodule = myobject, like 'project_task')
  * @return  array					(module, classpath, element, subelement, classfile, classname)
  * @see fetchObjectByElement()
  */
@@ -11204,19 +11221,20 @@ function getElementProperties($element_type)
 
 	$classfile = $classname = $classpath = '';
 
-	// Parse element/subelement (ex: project_task)
+	// Parse element/subelement
 	$module = $element_type;
 	$element = $element_type;
 	$subelement = $element_type;
 
-	// If we ask an resource form external module (instead of default path)
-	if (preg_match('/^([^@]+)@([^@]+)$/i', $element_type, $regs)) {
+	// If we ask a resource form external module (instead of default path)
+	if (preg_match('/^([^@]+)@([^@]+)$/i', $element_type, $regs)) {	// 'myobject@mymodule'
 		$element = $subelement = $regs[1];
 		$module = $regs[2];
 	}
 
-	//print '<br>1. element : '.$element.' - module : '.$module .'<br>';
-	if (preg_match('/^([^_]+)_([^_]+)/i', $element, $regs)) {
+	// If we ask a resource for a string with an element and a subelement
+	// Example 'project_task'
+	if (preg_match('/^([^_]+)_([^_]+)/i', $element, $regs)) {	// 'myobject_mysubobject' with myobject=mymodule
 		$module = $element = $regs[1];
 		$subelement = $regs[2];
 	}
@@ -11288,14 +11306,18 @@ function getElementProperties($element_type)
 	if ($element_type == 'order_supplier') {
 		$classpath = 'fourn/class';
 		$module = 'fournisseur';
-		$subelement = 'commandefournisseur';
 		$classfile = 'fournisseur.commande';
+		$element = 'commande';
+		$subelement = '';
+		$classname = 'CommandeFournisseur';
 	}
 	if ($element_type == 'invoice_supplier') {
 		$classpath = 'fourn/class';
 		$module = 'fournisseur';
-		$subelement = 'facturefournisseur';
 		$classfile = 'fournisseur.facture';
+		$element = 'facture';
+		$subelement = '';
+		$classname = 'FactureFournisseur';
 	}
 	if ($element_type == "service") {
 		$classpath = 'product/class';
@@ -11348,6 +11370,10 @@ function fetchObjectByElement($element_id, $element_type, $element_ref = '')
 			$objecttmp = new $classname($db);
 			$ret = $objecttmp->fetch($element_id, $element_ref);
 			if ($ret >= 0) {
+				if (empty($objecttmp->module)) {
+					$objecttmp->module = $element_prop['module'];
+				}
+
 				return $objecttmp;
 			}
 		} else {

+ 122 - 0
htdocs/core/lib/modulebuilder.lib.php

@@ -620,3 +620,125 @@ function writePropsInAsciiDoc($file, $objectname, $destfile)
 	}
 	return 1;
 }
+
+/**
+ * Search a string and return all lines needed from file
+ * @param  string  $file    file for searching
+ * @param  string  $start   start line if exist
+ * @param  string  $end     end line if exist
+ * @return string           return the content needed
+ */
+function getFromFile($file, $start, $end)
+{
+	$i = 1;
+	$keys = array();
+	$lines = file($file);
+	// Search for start and end lines
+	foreach ($lines as $i => $line) {
+		if (strpos($line, $start) !== false) {
+			// Copy lines until the end on array
+			while (($line = $lines[++$i]) !== false) {
+				if (strpos($line, $end) !== false) {
+					break;
+				}
+				$keys[] = $line;
+			}
+			break;
+		}
+	}
+	$content = implode("", $keys);
+	return $content;
+}
+
+/**
+ * Write all permissions of each object in AsciiDoc format
+ * @param  string   $file           path of the class
+ * @param  string   $destfile       file where write table of permissions
+ * @return int                      1 if OK, -1 if KO
+ */
+function writePermsInAsciiDoc($file, $destfile)
+{
+	global $langs;
+	//search and get all permssion in stirng
+	$start = '/* BEGIN MODULEBUILDER PERMISSIONS */';
+	$end = '/* END MODULEBUILDER PERMISSIONS */';
+	$content = getFromFile($file, $start, $end);
+	if (empty($content)) {
+		return -1;
+	}
+	//prepare table
+	$string = "[options='header',grid=rows,width=60%,caption=Organisation]\n";
+	$string .= "|===\n";
+	// header for table
+	$header = array($langs->trans('Objects'),$langs->trans('Permission'));
+	foreach ($header as $h) {
+		$string .= "|".$h;
+	}
+	$string .= "\n";
+	//content table
+	$array = explode(";", $content);
+	$indexIgnored = 15;
+	$permissions = array_slice($array, $indexIgnored, null, true);
+	// delete  occurrences "$r++" and ID
+	$permissions = str_replace('$r++', 1, $permissions);
+
+	$permsN = array();
+	foreach ($permissions as $i => $element) {
+		if ($element == 1) {
+			unset($permissions[$i]);
+		}
+		if (str_contains($element, '$this->numero')) {
+			unset($permissions[$i]);
+		}
+		if (str_contains($element, '$this->rights[$r][5]')) {
+			unset($permissions[$i]);
+		}
+	}
+	// cleaning the string on each element
+	foreach ($permissions as $key => $element) {
+		$element = str_replace(" '", '', $element);
+		$element = trim($element, "'");
+		$permsN[] = substr($element, strpos($element, "=")+1);
+	}
+	array_pop($permsN);
+
+	// Group permissions by Object and add it to string
+	$temp_array = [];
+	$final_array = [];
+	$countRights = count($permsN);
+	for ($i = 0; $i < $countRights ; $i++) {
+		// Add current element to temporary array
+		$temp_array[] = $permsN[$i];
+		//  add them to the final array and empty the temporary array
+		if (count($temp_array) == 2) {
+			$final_array[] = $temp_array;
+			$temp_array = [];
+		}
+	}
+	//  add it to the final array
+	if (count($temp_array) > 0) {
+		$final_array[] = $temp_array;
+	}
+
+	$result = array();
+	foreach ($final_array as $subarray) {
+		// found object
+		$key = $subarray[1];
+		// add sub array to object
+		$result[$key][] = $subarray;
+	}
+	foreach ($result as $i => $pems) {
+		$string .= "|*".$i."*|";
+		foreach ($pems as $tab) {
+			$string .= $tab[0]." , ";
+		}
+		$string .= "\n";
+	}
+	// end table
+	$string .= "\n|===\n";
+	$write = dolReplaceInFile($destfile, array('__DATA_PERMISSIONS__'=> $string));
+	if ($write<0) {
+		return -1;
+	}
+	return 1;
+}

+ 4 - 3
htdocs/core/lib/pdf.lib.php

@@ -744,12 +744,13 @@ function pdf_pagehead(&$pdf, $outputlangs, $page_height)
  *	@param	Translate	$outputlangs	Output language
  *	@param	array       $exclude        Array of family keys we want to exclude. For example array('mycompany', 'object', 'date', 'user', ...)
  *	@param	Object      $object         Object
- *	@param	int         $onlykey       1=Do not calculate some heavy values of keys (performance enhancement when we need only the keys), 2=Values are truncated and html sanitized (to use for help tooltip)
+ *	@param	int         $onlykey       	1=Do not calculate some heavy values of keys (performance enhancement when we need only the keys), 2=Values are truncated and html sanitized (to use for help tooltip)
+ *  @param   array      $include        Array of family keys we want to include. For example array('system', 'mycompany', 'object', 'objectamount', 'date', 'user', ...)
  *	@return	array						Array of substitutions
  */
-function pdf_getSubstitutionArray($outputlangs, $exclude = null, $object = null, $onlykey = 0)
+function pdf_getSubstitutionArray($outputlangs, $exclude = null, $object = null, $onlykey = 0, $include = null)
 {
-	$substitutionarray = getCommonSubstitutionArray($outputlangs, $onlykey, $exclude, $object);
+	$substitutionarray = getCommonSubstitutionArray($outputlangs, $onlykey, $exclude, $object, $include);
 	$substitutionarray['__FROM_NAME__'] = '__FROM_NAME__';
 	$substitutionarray['__FROM_EMAIL__'] = '__FROM_EMAIL__';
 	return $substitutionarray;

+ 22 - 3
htdocs/core/lib/security.lib.php

@@ -340,7 +340,7 @@ function dolGetLdapPasswordHash($password, $type = 'md5')
  *	@param  string				$feature2		Feature to check, second level of permission (optional). Can be a 'or' check with 'sublevela|sublevelb'.
  *												This is used to check permission $user->rights->features->feature2...
  *  @param  string				$dbt_keyfield   Field name for socid foreign key if not fk_soc. Not used if objectid is null (optional). Can use '' if NA.
- *  @param  string				$dbt_select     Field name for select if not "rowid". Not used if objectid is null (optional)
+ *  @param  string				$dbt_select     Field rowid name, for select into tableandshare if not "rowid". Not used if objectid is null (optional)
  *  @param	int					$isdraft		1=The object with id=$objectid is a draft
  *  @param	int					$mode			Mode (0=default, 1=return without dieing)
  * 	@return	int									If mode = 0 (default): Always 1, die process if not allowed. If mode = 1: Return 0 if access not allowed.
@@ -351,6 +351,7 @@ function restrictedArea(User $user, $features, $object = 0, $tableandshare = '',
 	global $db, $conf;
 	global $hookmanager;
 
+	// Define $objectid
 	if (is_object($object)) {
 		$objectid = $object->id;
 	} else {
@@ -369,6 +370,11 @@ function restrictedArea(User $user, $features, $object = 0, $tableandshare = '',
 
 	// Fix syntax of $features param
 	$originalfeatures = $features;
+	if ($features == 'agenda') {
+		$tableandshare = 'actioncomm&societe';
+		$feature2 = 'myactions|allactions';
+		$dbt_select = 'id';
+	}
 	if ($features == 'facturerec') {
 		$features = 'facture';
 	}
@@ -393,6 +399,17 @@ function restrictedArea(User $user, $features, $object = 0, $tableandshare = '',
 	if ($features == 'product') {
 		$features = 'produit';
 	}
+	if ($features == 'fournisseur') {	// When vendor invoice and pruchase order are into module 'fournisseur'
+		$features = 'fournisseur';
+		$feature2 = '';
+		if ($object->element == 'invoice_supplier') {
+			$feature2 = 'facture';
+		} elseif ($object->element == 'order_supplier') {
+			$feature2 = 'commande';
+		}
+	}
+
+	//print $features.' - '.$tableandshare.' - '.$feature2.' - '.$dbt_select."\n";
 
 	// Get more permissions checks from hooks
 	$parameters = array('features'=>$features, 'originalfeatures'=>$originalfeatures, 'objectid'=>$objectid, 'dbt_select'=>$dbt_select, 'idtype'=>$dbt_select, 'isdraft'=>$isdraft);
@@ -531,7 +548,7 @@ function restrictedArea(User $user, $features, $object = 0, $tableandshare = '',
 	// Check write permission from module (we need to know write permission to create but also to delete drafts record or to upload files)
 	$createok = 1;
 	$nbko = 0;
-	$wemustcheckpermissionforcreate = (GETPOST('sendit', 'alpha') || GETPOST('linkit', 'alpha') || in_array(GETPOST('action', 'aZ09'), array('create', 'update', 'set', 'add_element_resource', 'confirm_delete_linked_resource')) || GETPOST('roworder', 'alpha', 2));
+	$wemustcheckpermissionforcreate = (GETPOST('sendit', 'alpha') || GETPOST('linkit', 'alpha') || in_array(GETPOST('action', 'aZ09'), array('create', 'update', 'set', 'upload', 'add_element_resource', 'confirm_delete_linked_resource')) || GETPOST('roworder', 'alpha', 2));
 	$wemustcheckpermissionfordeletedraft = ((GETPOST("action", "aZ09") == 'confirm_delete' && GETPOST("confirm", "aZ09") == 'yes') || GETPOST("action", "aZ09") == 'delete');
 
 	if ($wemustcheckpermissionforcreate || $wemustcheckpermissionfordeletedraft) {
@@ -1114,6 +1131,7 @@ function httponly_accessforbidden($message = 1, $http_response_code = 403, $stri
 function accessforbidden($message = '', $printheader = 1, $printfooter = 1, $showonlymessage = 0, $params = null)
 {
 	global $conf, $db, $user, $langs, $hookmanager;
+	global $action, $object;
 
 	if (!is_object($langs)) {
 		include_once DOL_DOCUMENT_ROOT.'/core/class/translate.class.php';
@@ -1139,12 +1157,13 @@ function accessforbidden($message = '', $printheader = 1, $printfooter = 1, $sho
 	print '</div>';
 	print '<br>';
 	if (empty($showonlymessage)) {
-		global $action, $object;
 		if (empty($hookmanager)) {
+			include_once DOL_DOCUMENT_ROOT.'/core/class/hookmanager.class.php';
 			$hookmanager = new HookManager($db);
 			// Initialize technical object to manage hooks of page. Note that conf->hooks_modules contains array of hook context
 			$hookmanager->initHooks(array('main'));
 		}
+
 		$parameters = array('message'=>$message, 'params'=>$params);
 		$reshook = $hookmanager->executeHooks('getAccessForbiddenMessage', $parameters, $object, $action); // Note that $action and $object may have been modified by some hooks
 		print $hookmanager->resPrint;

+ 4 - 3
htdocs/core/modules/facture/doc/pdf_crabe.modules.php

@@ -1876,7 +1876,8 @@ class pdf_crabe extends ModelePDFFactures
 			$title = $outputlangs->transnoentities("InvoiceProForma");
 		}
 		if ($this->situationinvoice) {
-			$title = $outputlangs->transnoentities("PDFInvoiceSituation");
+			$langs->loadLangs(array("other"));
+			$title = $outputlangs->transnoentities("PDFInvoiceSituation") . " " . $outputlangs->transnoentities("NumberingShort") . $object->situation_counter . " -";
 		}
 		if (!empty($conf->global->PDF_USE_ALSO_LANGUAGE_CODE) && is_object($outputlangsbis)) {
 			$title .= ' - ';
@@ -1919,11 +1920,11 @@ class pdf_crabe extends ModelePDFFactures
 		$posy += 3;
 		$pdf->SetFont('', '', $default_font_size - 2);
 
-		if ($object->ref_client) {
+		if ($object->ref_customer) {
 			$posy += 4;
 			$pdf->SetXY($posx, $posy);
 			$pdf->SetTextColor(0, 0, 60);
-			$pdf->MultiCell($w, 3, $outputlangs->transnoentities("RefCustomer")." : ".$outputlangs->convToOutputCharset($object->ref_client), '', 'R');
+			$pdf->MultiCell($w, 3, $outputlangs->transnoentities("RefCustomer")." : ".$outputlangs->convToOutputCharset($object->ref_customer), '', 'R');
 		}
 
 		if (!empty($conf->global->PDF_SHOW_PROJECT_TITLE)) {

+ 4 - 3
htdocs/core/modules/facture/doc/pdf_sponge.modules.php

@@ -2126,7 +2126,8 @@ class pdf_sponge extends ModelePDFFactures
 			$title = $outputlangs->transnoentities("InvoiceProForma");
 		}
 		if ($this->situationinvoice) {
-			$title = $outputlangs->transnoentities("PDFInvoiceSituation");
+			$langs->loadLangs(array("other"));
+			$title = $outputlangs->transnoentities("PDFInvoiceSituation") . " " . $outputlangs->transnoentities("NumberingShort") . $object->situation_counter . " -";
 		}
 		if (!empty($conf->global->PDF_USE_ALSO_LANGUAGE_CODE) && is_object($outputlangsbis)) {
 			$title .= ' - ';
@@ -2169,11 +2170,11 @@ class pdf_sponge extends ModelePDFFactures
 		$posy += 3;
 		$pdf->SetFont('', '', $default_font_size - 2);
 
-		if ($object->ref_client) {
+		if ($object->ref_customer) {
 			$posy += 4;
 			$pdf->SetXY($posx, $posy);
 			$pdf->SetTextColor(0, 0, 60);
-			$pdf->MultiCell($w, 3, $outputlangs->transnoentities("RefCustomer")." : ".dol_trunc($outputlangs->convToOutputCharset($object->ref_client), 65), '', 'R');
+			$pdf->MultiCell($w, 3, $outputlangs->transnoentities("RefCustomer")." : ".dol_trunc($outputlangs->convToOutputCharset($object->ref_customer), 65), '', 'R');
 		}
 
 		if (!empty($conf->global->PDF_SHOW_PROJECT_TITLE)) {

+ 5 - 0
htdocs/core/modules/modEventOrganization.class.php

@@ -375,6 +375,11 @@ class modEventOrganization extends DolibarrModules
 		include_once DOL_DOCUMENT_ROOT.'/core/class/html.formmail.class.php';
 		$formmail = new FormMail($this->db);
 
+		include_once DOL_DOCUMENT_ROOT.'/user/class/user.class.php';
+		if (!is_object($user)) {
+			$user = new User($this->db); // To avoid error during migration
+		}
+
 		$template = $formmail->getEMailTemplate($this->db, 'conferenceorbooth', $user, $langs, 0, 1, '(EventOrganizationEmailAskConf)');
 		if ($template->id > 0) {
 			dolibarr_set_const($this->db, 'EVENTORGANIZATION_TEMPLATE_EMAIL_ASK_CONF', $template->id, 'chaine', 0, '', $conf->entity);

+ 1 - 1
htdocs/core/modules/modFacture.class.php

@@ -135,7 +135,7 @@ class modFacture extends DolibarrModules
 				'objectname'=>'Facture',
 				'method'=>'sendEmailsRemindersOnInvoiceDueDate',
 				'parameters'=>"10,all,EmailTemplateCode",
-				'comment'=>'Send an emails when the unpaid invoices reach a due date + n days = today. First param is the offset n of days, second parameter is "all" or a payment mode code, last parameter is the code of email template to use (an email template with EmailTemplateCode must exists. The version in the language of the thirdparty will be used in priority to update the PDF of the sent invoice).',
+				'comment'=>'Send an emails when we reach the due date - n days of an invoice. First param is n, the number of days before due date to send the remind, second parameter is "all" or a payment mode code, last parameter is the code of email template to use (an email template with the EmailTemplateCode must exists. The version of the email template in the language of the thirdparty will be used in priority. Language of the thirdparty will be also used to update the PDF of the sent invoice).',
 				'frequency'=>1,
 				'unitfrequency'=>3600 * 24,
 				'priority'=>50,

+ 3 - 3
htdocs/core/tpl/bloc_showhide.tpl.php

@@ -52,9 +52,9 @@ print '		$("#hide-'.$blocname.'").show();'."\n";
 print '});'."\n";
 
 print 'function setShowHide(status) {'."\n";
-print '		var id			= '.$object->id.";\n";
-print "		var element		= '".$object->element."';\n";
-print "		var htmlelement	= '".$blocname."';\n";
+print '		var id			= '.((int) $object->id).";\n";
+print "		var element		= '".dol_escape_js($object->element)."';\n";
+print "		var htmlelement	= '".dol_escape_js($blocname)."';\n";
 print '		var type		= "showhide";'."\n";
 print '		$.get("'.dol_buildpath('/core/ajax/extraparams.php', 1);
 print '?id="+id+"&element="+element+"&htmlelement="+htmlelement+"&type="+type+"&value="+status);'."\n";

+ 5 - 2
htdocs/core/tpl/filemanager.tpl.php

@@ -92,7 +92,7 @@ if ($module == 'ecm') {
 	print '</a>';
 }
 if ($permtoadd && GETPOSTISSET('website')) {	// If on file manager to manage medias of a web site
-	print '<a id="agenerateimgwebp" href="'.$_SERVER["PHP_SELF"].'?action=confirmconvertimgwebp&token='.newToken().'&website='.$website->ref.'" class="inline-block valignmiddle toolbarbutton paddingtop" title="'.dol_escape_htmltag($langs->trans("GenerateImgWebp")).'">';
+	print '<a id="agenerateimgwebp" href="'.$_SERVER["PHP_SELF"].'?action=confirmconvertimgwebp&token='.newToken().'&website='.urlencode($website->ref).'" class="inline-block valignmiddle toolbarbutton paddingtop" title="'.dol_escape_htmltag($langs->trans("GenerateImgWebp")).'">';
 	print img_picto('', 'images', '', false, 0, 0, '', 'size15x flip marginrightonly');
 	print '</a>';
 } elseif ($permtoadd && $module == 'ecm') {	// If on file manager medias in ecm
@@ -205,7 +205,10 @@ if ($action == 'confirmconvertimgwebp') {
 	if ($module == 'medias') {
 		$formquestion['website']=array('type'=>'hidden', 'value'=>$website->ref, 'name'=>'website');
 	}
-	print $form->formconfirm($_SERVER["PHP_SELF"], empty($file) ? $langs->trans('ConfirmImgWebpCreation') : $langs->trans('ConfirmChosenImgWebpCreation'), empty($file) ? $langs->trans('ConfirmGenerateImgWebp') : $langs->trans('ConfirmGenerateChosenImgWebp'), 'convertimgwebp', $formquestion, "yes", 1);
+	$param = '';
+	if (!empty($sortfield)) $param .= '&sortfield='.urlencode($sortfield);
+	if (!empty($sortorder)) $param .= '&sortorder='.urlencode($sortorder);
+	print $form->formconfirm($_SERVER["PHP_SELF"].($param ? '?'.$param : ''), empty($file) ? $langs->trans('ConfirmImgWebpCreation') : $langs->trans('ConfirmChosenImgWebpCreation'), empty($file) ? $langs->trans('ConfirmGenerateImgWebp') : $langs->trans('ConfirmGenerateChosenImgWebp'), 'convertimgwebp', $formquestion, "yes", 1);
 	$action = 'file_manager';
 }
 

+ 2 - 0
htdocs/core/website.inc.php

@@ -122,6 +122,7 @@ if (!defined('USEDOLIBARRSERVER') && !defined('USEDOLIBARREDITOR')) {
 		$contentsecuritypolicy = getDolGlobalString('WEBSITE_MAIN_SECURITY_FORCECSPRO');
 
 		if (!is_object($hookmanager)) {
+			include_once DOL_DOCUMENT_ROOT.'/core/class/hookmanager.class.php';
 			$hookmanager = new HookManager($db);
 		}
 		$hookmanager->initHooks(array("main"));
@@ -154,6 +155,7 @@ if (!defined('USEDOLIBARRSERVER') && !defined('USEDOLIBARREDITOR')) {
 		$contentsecuritypolicy = getDolGlobalString('WEBSITE_MAIN_SECURITY_FORCECSP');
 
 		if (!is_object($hookmanager)) {
+			include_once DOL_DOCUMENT_ROOT.'/core/class/hookmanager.class.php';
 			$hookmanager = new HookManager($db);
 		}
 		$hookmanager->initHooks(array("main"));

+ 12 - 1
htdocs/cron/class/cronjob.class.php

@@ -995,17 +995,28 @@ class Cronjob extends CommonObject
 		if (isset($this->status)) {
 			$label .= ' '.$this->getLibStatut(5);
 		}
-		$label .= '<br><b>'.$langs->trans('Ref').':</b> '.$this->ref;
+		$label .= '<br><b>'.$langs->trans('Ref').':</b> '.dol_escape_htmltag($this->ref);
 		$label .= '<br><b>'.$langs->trans('Title').':</b> '.$langs->trans($this->label);
 		if ($this->label != $langs->trans($this->label)) {
 			$label .= ' <span class="opacitymedium">('.$this->label.')</span>';
 		}
+		if (!empty($this->params)) {
+			$label .= '<br><b>'.$langs->trans('Parameters').':</b> '.dol_escape_htmltag($this->params);
+		}
+		$label .= '<br>';
+
 		if (!empty($this->datestart)) {
 			$label .= '<br><b>'.$langs->trans('CronDtStart').':</b> '.dol_print_date($this->datestart, 'dayhour', 'tzuserrel');
 		}
 		if (!empty($this->dateend)) {
 			$label .= '<br><b>'.$langs->trans('CronDtEnd').':</b> '.dol_print_date($this->dateend, 'dayhour', 'tzuserrel');
 		}
+		if (!empty($this->datelastrun)) {
+			$label .= '<br><b>'.$langs->trans('CronDtLastLaunch').':</b> '.dol_print_date($this->datelastrun, 'dayhour', 'tzuserrel');
+		}
+		if (!empty($this->datenextrun)) {
+			$label .= '<br><b>'.$langs->trans('CronDtNextLaunch').':</b> '.dol_print_date($this->datenextrun, 'dayhour', 'tzuserrel');
+		}
 
 		$url = DOL_URL_ROOT.'/cron/card.php?id='.$this->id;
 

+ 3 - 0
htdocs/cron/list.php

@@ -509,6 +509,9 @@ if ($num > 0) {
 		$object->datestart = $db->jdate($obj->datestart);
 		$object->dateend = $db->jdate($obj->dateend);
 		$object->module_name = $obj->module_name;
+		$object->params = $obj->params;
+		$object->datelastrun = $db->jdate($obj->datelastrun);
+		$object->datenextrun = $db->jdate($obj->datenextrun);
 
 		$datelastrun = $db->jdate($obj->datelastrun);
 		$datelastresult = $db->jdate($obj->datelastresult);

+ 1 - 0
htdocs/expedition/index.php

@@ -31,6 +31,7 @@ require_once DOL_DOCUMENT_ROOT.'/commande/class/commande.class.php';
 require_once DOL_DOCUMENT_ROOT.'/expedition/class/expedition.class.php';
 
 $hookmanager = new HookManager($db);
+
 $socid = GETPOST('socid', 'int');
 
 // Initialize technical object to manage hooks. Note that conf->hooks_modules contains array

+ 1 - 1
htdocs/fichinter/card.php

@@ -607,7 +607,7 @@ if (empty($reshook)) {
 		// Extrafields
 		$extrafields->fetch_name_optionals_label($object->table_element_line);
 		$array_options = $extrafields->getOptionalsFromPost($object->table_element_line);
-		$objectline->array_options = $array_options;
+		$objectline->array_options = array_merge($objectline->array_options, $array_options);
 
 		$result = $objectline->update($user);
 		if ($result < 0) {

+ 1 - 1
htdocs/fourn/facture/card.php

@@ -2800,7 +2800,7 @@ if ($action == 'create') {
 		$head = facturefourn_prepare_head($object);
 		$titre = $langs->trans('SupplierInvoice');
 
-		print dol_get_fiche_head($head, 'card', $titre, -1, 'supplier_invoice');
+		print dol_get_fiche_head($head, 'card', $titre, -1, 'supplier_invoice', 0, '', '', 0, '', 1);
 
 		$formconfirm = '';
 

+ 1 - 0
htdocs/hrm/index.php

@@ -54,6 +54,7 @@ if (isModEnabled('holiday')) {
 
 // Initialize technical object to manage hooks of page. Note that conf->hooks_modules contains array of hook context
 $hookmanager = new HookManager($db);
+
 $hookmanager->initHooks('hrmindex');
 
 // Load translation files required by the page

+ 56 - 32
htdocs/includes/odtphp/odf.php

@@ -18,6 +18,7 @@ class OdfException extends Exception
  * @copyright  2010-2015 - Laurent Destailleur - eldy@users.sourceforge.net
  * @copyright  2010 - Vikas Mahajan - http://vikasmahajan.wordpress.com
  * @copyright  2012 - Stephen Larroque - lrq3000@gmail.com
+ * @copyright  2023 - Thomas Negre - contact@open-dsi.fr
  * @license    https://www.gnu.org/copyleft/gpl.html  GPL License
  * @version 1.5.0
  */
@@ -46,6 +47,9 @@ class Odf
 	public $userdefined=array();
 
 	const PIXEL_TO_CM = 0.026458333;
+	const FIND_TAGS_REGEX = '/<([A-Za-z0-9]+)(?:\s([A-Za-z]+(?:\-[A-Za-z]+)?(?:=(?:".*?")|(?:[0-9]+))))*(?:(?:\s\/>)|(?:>(.*)<\/\1>))/';
+	const FIND_ENCODED_TAGS_REGEX = '/&lt;([A-Za-z]+)(?:\s([A-Za-z]+(?:\-[A-Za-z]+)?(?:=(?:".*?")|(?:[0-9]+))))*(?:(?:\s\/&gt;)|(?:&gt;(.*)&lt;\/\1&gt;))/';
+
 
 	/**
 	 * Class constructor
@@ -161,12 +165,16 @@ class Odf
 	 */
 	public function convertVarToOdf($value, $encode = true, $charset = 'ISO-8859')
 	{
-		$value = $encode ? htmlspecialchars($value) : $value;
-		$value = ($charset == 'ISO-8859') ? utf8_encode($value) : $value;
+		$value = html_entity_decode($value, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401);
+
+		// fix breaklines.
+		$value = preg_replace('/<br\s*\/?>/', "<br />", $value);
 		$convertedValue = $value;
 
 		// Check if the value includes html tags
 		if ($this->_hasHtmlTag($value) === true) {
+			$value = strip_tags($value, '<br><strong><b><i><em><u><s><sub><sup><span>');
+
 			// Default styles for strong/b, i/em, u, s, sub & sup
 			$automaticStyles = array(
 				'<style:style style:name="boldText" style:family="text"><style:text-properties fo:font-weight="bold" style:font-weight-asian="bold" style:font-weight-complex="bold" /></style:style>',
@@ -180,7 +188,7 @@ class Odf
 			$customStyles = array();
 			$fontDeclarations = array();
 
-			$convertedValue = $this->_replaceHtmlWithOdtTag($this->_getDataFromHtml($value), $customStyles, $fontDeclarations);
+			$convertedValue = $this->_replaceHtmlWithOdtTag($this->_getDataFromHtml($value), $customStyles, $fontDeclarations, $encode, $charset);
 
 			foreach ($customStyles as $key => $val) {
 				array_push($automaticStyles, '<style:style style:name="customStyle' . $key . '" style:family="text">' . $val . '</style:style>');
@@ -204,21 +212,23 @@ class Odf
 			}
 			$this->contentXml = str_replace('</office:font-face-decls>', $fonts . '</office:font-face-decls>', $this->contentXml);
 		} else {
-			$convertedValue = preg_replace('/(\r\n|\r|\n)/i', "<text:line-break/>", $value);
+			$convertedValue = $this->encode_chars($convertedValue, $encode, $charset);
+			$convertedValue = preg_replace('/(\r\n|\r|\n)/i', "<text:line-break/>", $convertedValue);
 		}
 
 		return $convertedValue;
 	}
 
 	/**
-	 * Replaces html tags in with odt tags and returns an odt string
-	 *
-	 * @param array $tags   			An array with html tags generated by the getDataFromHtml() function
-	 * @param array $customStyles   	An array of style defenitions that should be included inside the odt file
-	 * @param array $fontDeclarations   An array of font declarations that should be included inside the odt file
-	 * @return string
-	 */
-	private function _replaceHtmlWithOdtTag($tags, &$customStyles, &$fontDeclarations)
+     * Replaces html tags in with odt tags and returns an odt string. Encodes and converts inner text.
+	 * @param array 	$tags   			An array with html tags generated by the getDataFromHtml() function
+	 * @param array 	$customStyles   	An array of style defenitions that should be included inside the odt file
+	 * @param array 	$fontDeclarations   An array of font declarations that should be included inside the odt file
+	 * @param bool     	$encode     		If true, special XML characters are encoded
+	 * @param string   	$charset    		Charset. See encode_chars()
+     * @return string
+     */
+	private function _replaceHtmlWithOdtTag($tags, &$customStyles, &$fontDeclarations, $encode = false, $charset = '')
 	{
 		if ($customStyles == null) $customStyles = array();
 		if ($fontDeclarations == null) $fontDeclarations = array();
@@ -228,7 +238,8 @@ class Odf
 		foreach ((array) $tags as $tag) {
 			// Check if the current item is a tag or just plain text
 			if (isset($tag['text'])) {
-				$odtResult .= $tag['text'];
+				$text = $this->encode_chars($tag['text'], $encode, $charset);
+				$odtResult .= $text;
 			} elseif (isset($tag['name'])) {
 				switch ($tag['name']) {
 					case 'br':
@@ -236,23 +247,23 @@ class Odf
 						break;
 					case 'strong':
 					case 'b':
-						$odtResult .= '<text:span text:style-name="boldText">' . ($tag['children'] != null ? $this->_replaceHtmlWithOdtTag($tag['children'], $customStyles, $fontDeclarations) : $tag['innerText']) . '</text:span>';
+						$odtResult .= '<text:span text:style-name="boldText">' . ($tag['children'] != null ? $this->_replaceHtmlWithOdtTag($tag['children'], $customStyles, $fontDeclarations) : $this->encode_chars($tag['innerText'], $encode, $charset)) . '</text:span>';
 						break;
 					case 'i':
 					case 'em':
-						$odtResult .= '<text:span text:style-name="italicText">' . ($tag['children'] != null ? $this->_replaceHtmlWithOdtTag($tag['children'], $customStyles, $fontDeclarations) : $tag['innerText']) . '</text:span>';
+						$odtResult .= '<text:span text:style-name="italicText">' . ($tag['children'] != null ? $this->_replaceHtmlWithOdtTag($tag['children'], $customStyles, $fontDeclarations) : $this->encode_chars($tag['innerText'], $encode, $charset)) . '</text:span>';
 						break;
 					case 'u':
-						$odtResult .= '<text:span text:style-name="underlineText">' . ($tag['children'] != null ? $this->_replaceHtmlWithOdtTag($tag['children'], $customStyles, $fontDeclarations) : $tag['innerText']) . '</text:span>';
+						$odtResult .= '<text:span text:style-name="underlineText">' . ($tag['children'] != null ? $this->_replaceHtmlWithOdtTag($tag['children'], $customStyles, $fontDeclarations) : $this->encode_chars($tag['innerText'], $encode, $charset)) . '</text:span>';
 						break;
 					case 's':
-						$odtResult .= '<text:span text:style-name="strikethroughText">' . ($tag['children'] != null ? $this->_replaceHtmlWithOdtTag($tag['children'], $customStyles, $fontDeclarations) : $tag['innerText']) . '</text:span>';
+						$odtResult .= '<text:span text:style-name="strikethroughText">' . ($tag['children'] != null ? $this->_replaceHtmlWithOdtTag($tag['children'], $customStyles, $fontDeclarations) : $this->encode_chars($tag['innerText'], $encode, $charset)) . '</text:span>';
 						break;
 					case 'sub':
-						$odtResult .= '<text:span text:style-name="subText">' . ($tag['children'] != null ? $this->_replaceHtmlWithOdtTag($tag['children'], $customStyles, $fontDeclarations) : $tag['innerText']) . '</text:span>';
+						$odtResult .= '<text:span text:style-name="subText">' . ($tag['children'] != null ? $this->_replaceHtmlWithOdtTag($tag['children'], $customStyles, $fontDeclarations) : $this->encode_chars($tag['innerText'], $encode, $charset)) . '</text:span>';
 						break;
 					case 'sup':
-						$odtResult .= '<text:span text:style-name="supText">' . ($tag['children'] != null ? $this->_replaceHtmlWithOdtTag($tag['children'], $customStyles, $fontDeclarations) : $tag['innerText']) . '</text:span>';
+						$odtResult .= '<text:span text:style-name="supText">' . ($tag['children'] != null ? $this->_replaceHtmlWithOdtTag($tag['children'], $customStyles, $fontDeclarations) : $this->encode_chars($tag['innerText'], $encode, $charset)) . '</text:span>';
 						break;
 					case 'span':
 						if (isset($tag['attributes']['style'])) {
@@ -287,9 +298,9 @@ class Odf
 							}
 							if (strlen($odtStyles) > 0) {
 								// Generate a unique id for the style (using microtime and random because some CPUs are really fast...)
-								$key = floatval(str_replace('.', '', microtime(true)))+rand(0, 10);
+								$key = floatval(str_replace('.', '', microtime(true))) + rand(0, 10);
 								$customStyles[$key] = $odtStyles;
-								$odtResult .= '<text:span text:style-name="customStyle' . $key . '">' . ($tag['children'] != null ? $this->_replaceHtmlWithOdtTag($tag['children'], $customStyles, $fontDeclarations) : $tag['innerText']) . '</text:span>';
+								$odtResult .= '<text:span text:style-name="customStyle' . $key . '">' . ($tag['children'] != null ? $this->_replaceHtmlWithOdtTag($tag['children'], $customStyles, $fontDeclarations) : $this->encode_chars($tag['innerText'], $encode, $charset)) . '</text:span>';
 							}
 						}
 						break;
@@ -303,15 +314,30 @@ class Odf
 	}
 
 	/**
-	 * Checks if the given text is a html string
-	 * @param string    $text   The text to check
-	 * @return bool
+	 * Correctly encode chars
+	 * @param string   $text       The text to encode or not
+	 * @param bool     $encode     If true, special XML characters are encoded
+	 * @param string   $charset    Charset
+     * @return string	The converted text
+	 * @see self::convertVarToOdf()
 	 */
-	private function _isHtmlTag($text)
+	private function encode_chars($text, $encode = false, $charset = '')
 	{
-		return preg_match('/<([A-Za-z]+)(?:\s([A-Za-z]+(?:\-[A-Za-z]+)?(?:=(?:".*?")|(?:[0-9]+))))*(?:(?:\s\/>)|(?:>(.*)<\/\1>))/', $text);
+		$newtext = $encode ? htmlspecialchars($text, ENT_QUOTES | ENT_XML1) : $text;
+		$newtext = ($charset == 'ISO-8859') ? utf8_encode($newtext) : $newtext;
+		return $newtext;
 	}
 
+    /**
+     * Checks if the given text is a html string
+     * @param string    $text   The text to check
+     * @return bool
+     */
+    private function _isHtmlTag($text)
+	{
+        return preg_match(self::FIND_TAGS_REGEX, $text);
+    }
+
 	/**
 	 * Checks if the given text includes a html string
 	 * @param string    $text   The text to check
@@ -319,7 +345,7 @@ class Odf
 	 */
 	private function _hasHtmlTag($text)
 	{
-		$result = preg_match_all('/<([A-Za-z]+)(?:\s([A-Za-z]+(?:\-[A-Za-z]+)?(?:=(?:".*?")|(?:[0-9]+))))*(?:(?:\s\/>)|(?:>(.*)<\/\1>))/', $text);
+		$result = preg_match_all(self::FIND_TAGS_REGEX, $text);
 		return is_numeric($result) && $result > 0;
 	}
 
@@ -334,9 +360,8 @@ class Odf
 		$tempHtml = $html;
 
 		while (strlen($tempHtml) > 0) {
-			$matches = array();
 			// Check if the string includes a html tag
-			if (preg_match_all('/<([A-Za-z]+)(?:\s([A-Za-z]+(?:\-[A-Za-z]+)?(?:=(?:".*?")|(?:[0-9]+))))*(?:(?:\s\/>)|(?:>(.*)<\/\1>))/', $tempHtml, $matches)) {
+			if (preg_match_all(self::FIND_TAGS_REGEX, $tempHtml, $matches)) {
 				$tagOffset = strpos($tempHtml, $matches[0][0]);
 				// Check if the string starts with the html tag
 				if ($tagOffset > 0) {
@@ -348,13 +373,12 @@ class Odf
 					$tempHtml = substr($tempHtml, $tagOffset);
 				}
 				// Extract the attribute data from the html tag
-				$explodedAttributes = array();
 				preg_match_all('/([0-9A-Za-z]+(?:="[0-9A-Za-z\:\-\s\,\;\#]*")?)+/', $matches[2][0], $explodedAttributes);
 				$explodedAttributes = array_filter($explodedAttributes[0]);
 				$attributes = array();
 				// Store each attribute with its name in the $attributes array
 				$explodedAttributesCount = count($explodedAttributes);
-				for ($i=0; $i<$explodedAttributesCount; $i++) {
+				for ($i = 0; $i < $explodedAttributesCount; $i++) {
 					$attribute = trim($explodedAttributes[$i]);
 					// Check if the attribute has a value (like style="") or has no value (like required)
 					if (strpos($attribute, '=') !== false) {
@@ -368,7 +392,7 @@ class Odf
 								// Split the style properties and store them in an array
 								$explodedStyles = explode(';', $attrValue);
 								$explodedStylesCount = count($explodedStyles);
-								for ($n=0; $n<$explodedStylesCount; $n++) {
+								for ($n = 0; $n < $explodedStylesCount; $n++) {
 									$splitStyle = explode(':', $explodedStyles[$n]);
 									$attributes[$attrName][trim($splitStyle[0])] = trim($splitStyle[1]);
 								}

+ 0 - 0
htdocs/install/doctemplates/index.html


+ 0 - 0
htdocs/install/doctemplates/websites/index.html


+ 13 - 8
htdocs/install/mysql/migration/repair.sql

@@ -569,17 +569,22 @@ UPDATE llx_facturedet SET situation_percent = 100 WHERE situation_percent IS NUL
 DELETE FROM llx_rights_def WHERE module = 'hrm' AND perms = 'employee';
 
 
--- Sequence to fix the content of llx_bank.amount_main_currency
--- Note: amount is amount in currency of bank account
+
+-- Sequence to fix the content of llx_bank.amount_main_currency (value was empty and should not for payment on bank account with a different currency so when amount_main_currency is different than amount)
+-- Note: amount is amount in the currency of the bank account
 -- Note: pamount is always amount into the main currency
--- Note: pmulticurrencyamount is in currency of invoice 
--- Note: amount_main_currency must be amount in main currency
+-- Note: pmulticurrencyamount is in the currency of invoice 
+-- Note: amount_main_currency must be NULL or amount in main currency of company (we set it when the currency of the bank account differs from main currency)
 -- DROP TABLE tmp_bank;
--- CREATE TABLE tmp_bank SELECT b.rowid, b.amount, p.rowid as pid, p.amount as pamount, p.multicurrency_amount as pmulticurrencyamount FROM llx_bank as b INNER JOIN llx_bank_url as bu ON bu.fk_bank=b.rowid AND bu.type = 'payment' INNER JOIN llx_paiement as p ON bu.url_id = p.rowid WHERE p.multicurrency_amount <> 0 AND p.multicurrency_amount <> p.amount;
--- UPDATE llx_bank as b SET b.amount_main_currency = (SELECT tb.pamount FROM tmp_bank as tb WHERE tb.rowid = b.rowid) WHERE b.amount_main_currency IS NULL;
+-- CREATE TABLE tmp_bank SELECT b.rowid, b.amount, p.rowid as pid, p.amount as pamount, p.multicurrency_amount as pmulticurrencyamount, b.datec FROM llx_bank as b INNER JOIN llx_bank_url as bu ON bu.fk_bank=b.rowid AND bu.type = 'payment' INNER JOIN llx_paiement as p ON bu.url_id = p.rowid WHERE p.multicurrency_amount <> 0 AND p.multicurrency_amount <> p.amount;
+-- UPDATE llx_bank as b SET b.amount_main_currency = (SELECT tb.pamount FROM tmp_bank as tb WHERE tb.rowid = b.rowid) WHERE b.amount_main_currency IS NULL AND b.rowid IN (SELECT rowid FROM tmp_bank);
 -- DROP TABLE tmp_bank2;
--- CREATE TABLE tmp_bank2 SELECT b.rowid, b.amount, p.rowid as pid, p.amount as pamount, p.multicurrency_amount as pmulticurrencyamount FROM llx_bank as b INNER JOIN llx_bank_url as bu ON bu.fk_bank=b.rowid AND bu.type = 'payment_supplier' INNER JOIN llx_paiementfourn as p ON bu.url_id = p.rowid WHERE p.multicurrency_amount <> 0 AND p.multicurrency_amount <> p.amount;
--- UPDATE llx_bank as b SET b.amount_main_currency = (SELECT tb.pamount FROM tmp_bank2 as tb WHERE tb.rowid = b.rowid) WHERE b.amount_main_currency IS NULL;
+-- CREATE TABLE tmp_bank2 SELECT b.rowid, b.amount, p.rowid as pid, - p.amount as pamount, - p.multicurrency_amount as pmulticurrencyamount, b.datec FROM llx_bank as b INNER JOIN llx_bank_url as bu ON bu.fk_bank=b.rowid AND bu.type = 'payment_supplier' INNER JOIN llx_paiementfourn as p ON bu.url_id = p.rowid WHERE p.multicurrency_amount <> 0 AND p.multicurrency_amount <> p.amount;
+-- UPDATE llx_bank as b SET b.amount_main_currency = (SELECT tb.pamount FROM tmp_bank2 as tb WHERE tb.rowid = b.rowid) WHERE b.amount_main_currency IS NULL AND b.rowid IN (SELECT rowid FROM tmp_bank2);
+
+-- Sequence to fix the content of llx_bank.amount_main_currency (sign was wrong with some version)
+-- UPDATE llx_bank as b SET b.amount_main_currency = -b.amount_main_currency WHERE b.amount IS NOT NULL AND b.amount_main_currency IS NOT NULL AND SIGN(b.amount_main_currency) <> SIGN(b.amount);
+
 
 
 -- Delete duplicate entries into llx_c_transport_mode

+ 1 - 1
htdocs/install/mysql/tables/llx_bank.sql

@@ -25,7 +25,7 @@ create table llx_bank
   datev           date,                      -- date de valeur
   dateo           date,                      -- date operation
   amount          double(24,8) NOT NULL default 0,		-- amount in the currency of the bank account
-  amount_main_currency double(24,8) NULL,				-- amount in the main currency of the company
+  amount_main_currency double(24,8) NULL,				-- amount in the main currency of the company when payment done in a bank account with a different currency
   label           varchar(255),
   fk_account      integer,
   fk_user_author  integer,

+ 1 - 1
htdocs/knowledgemanagement/class/knowledgerecord.class.php

@@ -770,7 +770,7 @@ class KnowledgeRecord extends CommonObject
 
 		$params = [
 			'id' => $this->id,
-			'objecttype' => $this->element,
+			'objecttype' => $this->element.($this->module ? '@'.$this->module : ''),
 			'option' => $option,
 			'nofetch' => 1,
 		];

+ 1 - 1
htdocs/langs/en_US/ecm.lang

@@ -48,7 +48,7 @@ GenerateImgWebp=Duplicate all images with another version with .webp format
 ConfirmGenerateImgWebp=If you confirm, you will generate an image in .webp format for all images currently into this folder (subfolders are not included)...
 ConfirmImgWebpCreation=Confirm all images duplication
 GenerateChosenImgWebp=Duplicate chosen image with another version with .webp format
-ConfirmGenerateChosenImgWebp=If you confirm, you will generate an image in .webp format for chosen image
+ConfirmGenerateChosenImgWebp=If you confirm, you will generate an image in .webp format for the image
 ConfirmChosenImgWebpCreation=Confirm chosen images duplication
 SucessConvertImgWebp=Images successfully duplicated
 SucessConvertChosenImgWebp=Chosen image successfully duplicated

+ 2 - 0
htdocs/langs/en_US/errors.lang

@@ -310,6 +310,8 @@ ErrorFieldExist=The value for <b>%s</b> already exist
 ErrorEqualModule=Module invalid in <b>%s</b>
 ErrorFieldValue=Value for <b>%s</b> is incorrect
 ErrorCoherenceMenu=<b>%s</b> is required when <b>%s</b> is 'left'
+ErrorUploadFileDragDrop=There was an error while the file(s) upload
+ErrorUploadFileDragDropPermissionDenied=There was an error while the file(s) upload : Permission denied 
 
 # Warnings
 WarningParamUploadMaxFileSizeHigherThanPostMaxSize=Your PHP parameter upload_max_filesize (%s) is higher than PHP parameter post_max_size (%s). This is not a consistent setup.

+ 2 - 0
htdocs/langs/en_US/main.lang

@@ -1229,3 +1229,5 @@ LastPasswordChangeDate=Last password change date
 PublicVirtualCardUrl=Virtual business card page URL
 PublicVirtualCard=Virtual business card
 TreeView=Tree view
+DropFileToAddItToObject=Drop a file to add it to this object
+UploadFileDragDropSuccess=The file(s) have been uploaded successfully

+ 9 - 1
htdocs/main.inc.php

@@ -1388,7 +1388,14 @@ if (!defined('NOREQUIREMENU')) {
 	$menumanager->loadMenu();
 }
 
-
+if (!empty(GETPOST('seteventmessages', 'alpha'))) {
+	$message = GETPOST('seteventmessages', 'alpha');
+	$messages  = explode(',', $message);
+	foreach ($messages as $key => $msg) {
+		$tmp = explode(':', $msg);
+		setEventMessages($tmp[0], null, !empty($tmp[1]) ? $tmp[1] : 'mesgs');
+	}
+}
 
 // Functions
 
@@ -1615,6 +1622,7 @@ function top_htmlhead($head, $title = '', $disablejs = 0, $disablehead = 0, $arr
 	//print '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr">'."\n";
 	if (empty($disablehead)) {
 		if (!is_object($hookmanager)) {
+			include_once DOL_DOCUMENT_ROOT.'/core/class/hookmanager.class.php';
 			$hookmanager = new HookManager($db);
 		}
 		$hookmanager->initHooks(array("main"));

+ 0 - 1
htdocs/master.inc.php

@@ -250,4 +250,3 @@ if (!defined('NOREQUIRETRAN')) {
 if (!defined('MAIN_LABEL_MENTION_NPR')) {
 	define('MAIN_LABEL_MENTION_NPR', 'NPR');
 }
-//if (! defined('PCLZIP_TEMPORARY_DIR')) define('PCLZIP_TEMPORARY_DIR', $conf->user->dir_temp);

+ 4 - 0
htdocs/modulebuilder/index.php

@@ -870,6 +870,10 @@ if ($dirins && $action == 'initdoc' && !empty($module)) {
 			writePropsInAsciiDoc($path, $obj, $destfile);
 		}
 
+		// add table of permissions
+		$moduledescriptorfile = $dirins.'/'.strtolower($module).'/core/modules/mod'.$module.'.class.php';
+		writePermsInAsciiDoc($moduledescriptorfile, $destfile);
+
 		// Delete old documentation files
 		$FILENAMEDOC = $modulelowercase.'.html';
 		$FILENAMEDOCPDF = $modulelowercase.'.pdf';

+ 1 - 1
htdocs/modulebuilder/template/class/myobject.class.php

@@ -796,7 +796,7 @@ class MyObject extends CommonObject
 		$result = '';
 		$params = [
 			'id' => $this->id,
-			'objecttype' => $this->element,
+			'objecttype' => $this->element.($this->module ? '@'.$this->module : ''),
 			'option' => $option,
 		];
 		$classfortooltip = 'classfortooltip';

+ 1 - 1
htdocs/modulebuilder/template/myobject_card.php

@@ -333,7 +333,7 @@ if (($id || $ref) && $action == 'edit') {
 if ($object->id > 0 && (empty($action) || ($action != 'edit' && $action != 'create'))) {
 	$head = myobjectPrepareHead($object);
 
-	print dol_get_fiche_head($head, 'card', $langs->trans("MyObject"), -1, $object->picto);
+	print dol_get_fiche_head($head, 'card', $langs->trans("MyObject"), -1, $object->picto, 0, '', '', 0, '', 1);
 
 	$formconfirm = '';
 

+ 7 - 7
htdocs/product/stats/card.php

@@ -191,24 +191,24 @@ if ($result || !($id > 0)) {
 	}
 
 	print '<table class="noborder centpercent">';
-	print '<tr class="liste_titre"><td class="liste_titre" colspan="2">'.$langs->trans("Filter").'</td></tr>';
+	print '<tr class="liste_titre"><td class="liste_titre">'.$langs->trans("Filter").'</td><td></td></tr>';
 
 	if (!($id > 0) || $notab) {
 		// Type
-		print '<tr><td class="titlefield">'.$langs->trans("Type").'</td><td>';
+		print '<tr class="nooddeven"><td class="titlefield">'.$langs->trans("Type").'</td><td>';
 		$array = array('-1'=>'&nbsp;', '0'=>$langs->trans('Product'), '1'=>$langs->trans('Service'));
 		print $form->selectarray('type', $array, $type, 0, 0, 0, '', 0, 0, 0, '', 'minwidth100');
 		print '</td></tr>';
 
 		// Product
-		print '<tr><td class="titlefield">'.$langs->trans("ProductOrService").'</td><td>';
+		print '<tr class="nooddeven"><td class="titlefield">'.$langs->trans("ProductOrService").'</td><td>';
 		print img_picto('', 'product', 'class="pictofixedwidth"');
 		print $form->select_produits($id, 'id', '', 0, 0, 1, 2, '', ($conf->dol_optimize_smallscreen ? 1 : 0), array(), 0, '1', 0, 'widthcentpercentminusx maxwidth400');
 		print '</td></tr>';
 
 		// Tag
 		if (isModEnabled('categorie')) {
-			print '<tr><td class="titlefield">'.$langs->trans("Categories").'</td><td>';
+			print '<tr class="nooddeven"><td class="titlefield">'.$langs->trans("Categories").'</td><td>';
 			$moreforfilter .= img_picto($langs->trans("Categories"), 'category', 'class="pictofixedwidth"');
 			$moreforfilter .= $htmlother->select_categories(Categorie::TYPE_PRODUCT, $search_categ, 'search_categ', 1, 1, 'widthcentpercentminusx maxwidth400');
 			print $moreforfilter;
@@ -219,7 +219,7 @@ if ($result || !($id > 0)) {
 	}
 
 	// Year
-	print '<tr><td class="titlefield">'.$langs->trans("Year").'</td><td>';
+	print '<tr class="nooddeven"><td class="titlefield">'.$langs->trans("Year").'</td><td>';
 	$arrayyears = array();
 	for ($year = $currentyear - 25; $year < $currentyear; $year++) {
 		$arrayyears[$year] = $year;
@@ -235,7 +235,7 @@ if ($result || !($id > 0)) {
 	print '</td></tr>';
 
 	// thirdparty
-	print '<tr><td class="titlefield">'.$langs->trans("ThirdParty").'</td><td>';
+	print '<tr class="nooddeven"><td class="titlefield">'.$langs->trans("ThirdParty").'</td><td>';
 	print img_picto('', 'company', 'class="pictofixedwidth"');
 	print $form->select_company($socid, 'socid', '', 1, 0, 0, array(), 0, 'widthcentpercentminusx maxwidth400');
 	print '</td></tr>';
@@ -494,7 +494,7 @@ if ($result || !($id > 0)) {
 			print '<td align="right">'.$linktoregenerate.'</td>';
 			print '</tr>';
 			// Image
-			print '<tr class="impair"><td colspan="2" class="nohover" align="center">';
+			print '<tr><td colspan="2" class="nohover" align="center">';
 			print $graphfiles[$key]['output'];
 			print '</td></tr>';
 			print '</table>';

+ 5 - 5
htdocs/projet/activity/index.php

@@ -37,6 +37,11 @@ if ($search_project_user == $user->id) {
 	$mine = 1;
 }
 
+$hookmanager = new HookManager($db);
+
+// Initialize technical object to manage hooks. Note that conf->hooks_modules contains array
+$hookmanager->initHooks(array('activityindex'));
+
 // Security check
 $socid = 0;
 if ($user->socid > 0) {
@@ -47,11 +52,6 @@ if (!$user->rights->projet->lire) {
 	accessforbidden();
 }
 
-$hookmanager = new HookManager($db);
-
-// Initialize technical object to manage hooks. Note that conf->hooks_modules contains array
-$hookmanager->initHooks(array('activityindex'));
-
 // Load translation files required by the page
 $langs->load("projects");
 

+ 1 - 1
htdocs/public/payment/newpayment.php

@@ -66,8 +66,8 @@ require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
 require_once DOL_DOCUMENT_ROOT.'/societe/class/societeaccount.class.php';
 require_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
 require_once DOL_DOCUMENT_ROOT.'/projet/class/project.class.php';
+
 // Hook to be used by external payment modules (ie Payzen, ...)
-include_once DOL_DOCUMENT_ROOT.'/core/class/hookmanager.class.php';
 $hookmanager = new HookManager($db);
 $hookmanager->initHooks(array('newpayment'));
 

+ 2 - 1
htdocs/public/payment/paymentok.php

@@ -58,9 +58,10 @@ if (isModEnabled('paypal')) {
 	require_once DOL_DOCUMENT_ROOT.'/paypal/lib/paypal.lib.php';
 	require_once DOL_DOCUMENT_ROOT.'/paypal/lib/paypalfunctions.lib.php';
 }
+
 // Hook to be used by external payment modules (ie Payzen, ...)
-include_once DOL_DOCUMENT_ROOT.'/core/class/hookmanager.class.php';
 $hookmanager = new HookManager($db);
+
 $hookmanager->initHooks(array('newpayment'));
 
 $langs->loadLangs(array("main", "other", "dict", "bills", "companies", "paybox", "paypal"));

+ 2 - 1
htdocs/public/project/index.php

@@ -50,9 +50,10 @@ require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
 require_once DOL_DOCUMENT_ROOT.'/societe/class/societeaccount.class.php';
 require_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
 require_once DOL_DOCUMENT_ROOT.'/projet/class/project.class.php';
+
 // Hook to be used by external payment modules (ie Payzen, ...)
-include_once DOL_DOCUMENT_ROOT.'/core/class/hookmanager.class.php';
 $hookmanager = new HookManager($db);
+
 $hookmanager->initHooks(array('newpayment'));
 
 // Load translation files

+ 2 - 1
htdocs/public/project/viewandvote.php

@@ -51,9 +51,10 @@ require_once DOL_DOCUMENT_ROOT.'/societe/class/societeaccount.class.php';
 require_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
 require_once DOL_DOCUMENT_ROOT.'/projet/class/project.class.php';
 require_once DOL_DOCUMENT_ROOT . '/comm/action/class/actioncomm.class.php';
+
 // Hook to be used by external payment modules (ie Payzen, ...)
-include_once DOL_DOCUMENT_ROOT.'/core/class/hookmanager.class.php';
 $hookmanager = new HookManager($db);
+
 $hookmanager->initHooks(array('newpayment'));
 
 // For encryption

+ 13 - 10
htdocs/public/stripe/ipn.php

@@ -109,12 +109,12 @@ $sig_header = empty($_SERVER["HTTP_STRIPE_SIGNATURE"]) ? '' : $_SERVER["HTTP_STR
 $event = null;
 
 if (getDolGlobalString('STRIPE_DEBUG')) {
-	$fh = fopen(DOL_DATA_ROOT.'/dolibarr_stripe.log', 'w+');
+	$fh = fopen(DOL_DATA_ROOT.'/dolibarr_stripeipn_payload.log', 'w+');
 	if ($fh) {
-		fwrite($fh, dol_print_date(dol_now('gmt'), 'standard').' HTTP_STRIPE_SIGNATURE='.$sig_header."\n");
+		fwrite($fh, dol_print_date(dol_now('gmt'), 'standard').' IPN Called. HTTP_STRIPE_SIGNATURE='.$sig_header."\n");
 		fwrite($fh, $payload);
 		fclose($fh);
-		dolChmod(DOL_DATA_ROOT.'/dolibarr_stripe.log');
+		dolChmod(DOL_DATA_ROOT.'/dolibarr_stripeipn_payload.log');
 	}
 }
 
@@ -139,7 +139,7 @@ $langs->load("main");
 if (isModEnabled('multicompany') && !empty($conf->stripeconnect->enabled) && is_object($mc)) {
 	$sql = "SELECT entity";
 	$sql .= " FROM ".MAIN_DB_PREFIX."oauth_token";
-	$sql .= " WHERE service = '".$db->escape($service)."' and tokenstring LIKE '%".$db->escape($event->account)."%'";
+	$sql .= " WHERE service = '".$db->escape($service)."' and tokenstring LIKE '%".$db->escape($db->escapeforlike($event->account))."%'";
 
 	dol_syslog(get_class($db)."::fetch", LOG_DEBUG);
 	$result = $db->query($sql);
@@ -314,8 +314,8 @@ if ($event->type == 'payout.created') {
 	$db->query($sql);
 	$db->commit();
 } elseif ($event->type == 'payment_intent.succeeded') {		// Called when making payment with PaymentIntent method ($conf->global->STRIPE_USE_NEW_CHECKOUT is on).
-	dol_syslog("object = ".var_export($event->data, true));
-	include_once DOL_DOCUMENT_ROOT . '/compta/paiement/class/paiement.class.php'; //TOTEST
+	//dol_syslog("object = ".var_export($event->data, true));
+	include_once DOL_DOCUMENT_ROOT . '/compta/paiement/class/paiement.class.php';
 	global $stripearrayofkeysbyenv;
 	$error = 0;
 	$object = $event->data->object;
@@ -329,6 +329,8 @@ if ($event->type == 'payout.created') {
 	$paymentTypeId = "";
 	$payment_amount = 0;
 
+	dol_syslog("Try to find the payment in database for the payment id = ".$TRANSACTIONID);
+
 	$sql = "SELECT pi.fk_facture, pi.fk_prelevement_bons, pi.amount, pi.type";
 	$sql .= " FROM llx_prelevement_demande as pi";
 	$sql .= " WHERE pi.ext_payment_id = '".$db->escape($TRANSACTIONID)."'";
@@ -345,14 +347,14 @@ if ($event->type == 'payout.created') {
 			$paymentTypeId = $obj->type;
 		}
 	} else {
-		$postactionmessages[] = $db->lasterror();
 		http_response_code(500);
+		print $db->lasterror();
 		return -1;
 	}
 
 	$stripeacc = $stripearrayofkeysbyenv[$servicestatus]['secret_key'];
 
-	dol_syslog("Try to find a payment method with id = ".json_encode($paymentmethodstripeid));
+	dol_syslog("Get the Stripe payment object for the payment method id = ".json_encode($paymentmethodstripeid));
 
 	$s = new \Stripe\StripeClient($stripeacc);
 
@@ -516,12 +518,13 @@ if ($event->type == 'payout.created') {
 			http_response_code(500);
 			return -1;
 		}
+	} else {
+		dol_syslog("The payment mode of this payment is ".$paymentTypeId.". This payment mode is not managed by the IPN");
 	}
 } elseif ($event->type == 'payment_intent.payment_failed') {
-	// TODO: Redirect to paymentko.php
+	dol_syslog("A try to make a payment has failed");
 } elseif ($event->type == 'checkout.session.completed') {		// Called when making payment with new Checkout method ($conf->global->STRIPE_USE_NEW_CHECKOUT is on).
 	// TODO: create fees
-	// TODO: Redirect to paymentok.php
 } elseif ($event->type == 'payment_method.attached') {
 	require_once DOL_DOCUMENT_ROOT.'/societe/class/companypaymentmode.class.php';
 	require_once DOL_DOCUMENT_ROOT.'/societe/class/societeaccount.class.php';

+ 2 - 2
htdocs/societe/card.php

@@ -1632,7 +1632,7 @@ if (is_object($objcanvas) && $objcanvas->displayCanvasExists($action)) {
 		print '<tr><td>'.$form->editfieldkey('Web', 'url', '', $object, 0).'</td>';
 		print '<td colspan="3">'.img_picto('', 'globe', 'class="pictofixedwidth"').' <input type="text" class="maxwidth500 widthcentpercentminusx" name="url" id="url" value="'.$object->url.'"></td></tr>';
 
-			// Unsubscribe
+		// Unsubscribe
 		if (isModEnabled('mailing')) {
 			if ($conf->use_javascript_ajax && $conf->global->MAILING_CONTACT_DEFAULT_BULK_STATUS == 2) {
 				print "\n".'<script type="text/javascript">'."\n";
@@ -1863,7 +1863,7 @@ if (is_object($objcanvas) && $objcanvas->displayCanvasExists($action)) {
 		print '<tr>';
 		print '<td>'.$form->editfieldkey('AllocateCommercial', 'commercial_id', '', $object, 0).'</td>';
 		print '<td colspan="3" class="maxwidthonsmartphone">';
-		$userlist = $form->select_dolusers('', '', 0, null, 0, '', '', 0, 0, 0, 'AND u.statut = 1', 0, '', '', 0, 1);
+		$userlist = $form->select_dolusers('', '', 0, null, 0, '', '', '0', 0, 0, 'AND u.statut = 1', 0, '', '', 0, 2);
 		// Note: If user has no right to "see all thirdparties", we force selection of sale representative to him, so after creation he can see the record.
 		$selected = (count(GETPOST('commercial', 'array')) > 0 ? GETPOST('commercial', 'array') : (GETPOST('commercial', 'int') > 0 ? array(GETPOST('commercial', 'int')) : (empty($user->rights->societe->client->voir) ? array($user->id) : array())));
 		print img_picto('', 'user').$form->multiselectarray('commercial', $userlist, $selected, null, null, 'quatrevingtpercent widthcentpercentminusx', 0, 0);

+ 10 - 9
htdocs/takepos/admin/receipt.php

@@ -109,8 +109,9 @@ print ajax_constantonoff("TAKEPOS_TICKET_VAT_GROUPPED", array(), $conf->entity,
 print "</td></tr>\n";
 
 if (getDolGlobalString('TAKEPOS_PRINT_METHOD') == "browser" || getDolGlobalString('TAKEPOS_PRINT_METHOD') == "takeposconnector") {
-	$substitutionarray = pdf_getSubstitutionArray($langs, null, null, 2);
+	$substitutionarray = pdf_getSubstitutionArray($langs, array('ticket', 'member', 'candidate'), null, 2, array('company', 'user', 'object', 'system'));
 	$substitutionarray['__(AnyTranslationKey)__'] = $langs->trans("Translation");
+
 	$htmltext = '<i>'.$langs->trans("AvailableVariables").':<br>';
 	foreach ($substitutionarray as $key => $val) {
 		$htmltext .= $key.'<br>';
@@ -162,14 +163,6 @@ if (getDolGlobalString('TAKEPOS_PRINT_METHOD') == "browser" || getDolGlobalStrin
 	print "</td></tr>\n";
 }
 
-// Auto print tickets
-print '<tr class="oddeven"><td>';
-print $langs->trans("AutoPrintTickets");
-print '<td colspan="2">';
-print ajax_constantonoff("TAKEPOS_AUTO_PRINT_TICKETS", array(), $conf->entity, 0, 0, 1, 0);
-print "</td></tr>\n";
-
-
 // Show price without vat
 print '<tr class="oddeven"><td>';
 print $langs->trans('ShowPriceHTOnReceipt');
@@ -207,6 +200,14 @@ if (getDolGlobalString('TAKEPOS_PRINT_WITHOUT_DETAILS')) {
 	print "</td></tr>\n";
 }
 
+// Auto print tickets
+print '<tr class="oddeven"><td>';
+print $langs->trans("AutoPrintTickets");
+print '<td colspan="2">';
+print ajax_constantonoff("TAKEPOS_AUTO_PRINT_TICKETS", array(), $conf->entity, 0, 0, 1, 0);
+print "</td></tr>\n";
+
+
 print '</table>';
 print '</div>';
 

+ 46 - 30
htdocs/takepos/receipt.php

@@ -26,6 +26,7 @@
  *	\brief      Page to show a receipt.
  */
 
+// Includes
 if (!isset($action)) {
 	//if (! defined('NOREQUIREUSER'))	define('NOREQUIREUSER', '1');	// Not disabled cause need to load personalized language
 	//if (! defined('NOREQUIREDB'))		define('NOREQUIREDB', '1');		// Not disabled cause need to load personalized language
@@ -57,7 +58,7 @@ $facid = GETPOST('facid', 'int');
 $action = GETPOST('action', 'aZ09');
 $gift = GETPOST('gift', 'int');
 
-if (empty($user->rights->takepos->run)) {
+if (!$user->hasRight('takepos', 'run')) {
 	accessforbidden();
 }
 
@@ -269,42 +270,57 @@ if (isModEnabled('multicurrency') && $_SESSION["takeposcustomercurrency"] != ""
 	echo '</td></tr>';
 }
 
-if ($conf->global->TAKEPOS_PRINT_PAYMENT_METHOD) {
-	$sql = "SELECT p.pos_change as pos_change, p.datep as date, p.fk_paiement, p.num_paiement as num, pf.amount as amount, pf.multicurrency_amount,";
-	$sql .= " cp.code";
-	$sql .= " FROM ".MAIN_DB_PREFIX."paiement_facture as pf, ".MAIN_DB_PREFIX."paiement as p";
-	$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."c_paiement as cp ON p.fk_paiement = cp.id";
-	$sql .= " WHERE pf.fk_paiement = p.rowid AND pf.fk_facture = ".((int) $facid);
-	$sql .= " ORDER BY p.datep";
-	$resql = $db->query($sql);
-	if ($resql) {
-		$num = $db->num_rows($resql);
-		$i = 0;
-		while ($i < $num) {
-			$row = $db->fetch_object($resql);
-			echo '<tr>';
-			echo '<td class="right">';
-			echo $langs->transnoentitiesnoconv("PaymentTypeShort".$row->code);
-			echo '</td>';
-			echo '<td class="right">';
-			$amount_payment = (isModEnabled('multicurrency') && $object->multicurrency_tx != 1) ? $row->multicurrency_amount : $row->amount;
-			if ($row->code == "LIQ") {
-				$amount_payment = $amount_payment + $row->pos_change; // Show amount with excess received if is cash payment
-			}
-			echo price($amount_payment, 1, '', 1, - 1, - 1, $conf->currency);
-			echo '</td>';
-			echo '</tr>';
-			if ($row->code == "LIQ" && $row->pos_change > 0) { // Print change only in cash payments
+if (getDolGlobalString('TAKEPOS_PRINT_PAYMENT_METHOD')) {
+	if (empty($facid)) {
+		// Case of specimen
+		echo '<tr>';
+		echo '<td class="right">';
+		echo $langs->transnoentitiesnoconv("PaymentTypeShortLIQ");
+		echo '</td>';
+		echo '<td class="right">';
+		$amount_payment = 0;
+		echo price($amount_payment, 1, '', 1, - 1, - 1, $conf->currency);
+		echo '</td>';
+		echo '</tr>';
+	} else {
+		$sql = "SELECT p.pos_change as pos_change, p.datep as date, p.fk_paiement, p.num_paiement as num, pf.amount as amount, pf.multicurrency_amount,";
+		$sql .= " cp.code";
+		$sql .= " FROM ".MAIN_DB_PREFIX."paiement_facture as pf, ".MAIN_DB_PREFIX."paiement as p";
+		$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."c_paiement as cp ON p.fk_paiement = cp.id";
+		$sql .= " WHERE pf.fk_paiement = p.rowid AND pf.fk_facture = ".((int) $facid);
+		$sql .= " ORDER BY p.datep";
+
+		$resql = $db->query($sql);
+		if ($resql) {
+			$num = $db->num_rows($resql);
+
+			$i = 0;
+			while ($i < $num) {
+				$row = $db->fetch_object($resql);
 				echo '<tr>';
 				echo '<td class="right">';
-				echo $langs->trans("Change");
+				echo $langs->transnoentitiesnoconv("PaymentTypeShort".$row->code);
 				echo '</td>';
 				echo '<td class="right">';
-				echo price($row->pos_change, 1, '', 1, - 1, - 1, $conf->currency);
+				$amount_payment = (isModEnabled('multicurrency') && $object->multicurrency_tx != 1) ? $row->multicurrency_amount : $row->amount;
+				if ($row->code == "LIQ") {
+					$amount_payment = $amount_payment + $row->pos_change; // Show amount with excess received if is cash payment
+				}
+				echo price($amount_payment, 1, '', 1, - 1, - 1, $conf->currency);
 				echo '</td>';
 				echo '</tr>';
+				if ($row->code == "LIQ" && $row->pos_change > 0) { // Print change only in cash payments
+					echo '<tr>';
+					echo '<td class="right">';
+					echo $langs->trans("Change");
+					echo '</td>';
+					echo '<td class="right">';
+					echo price($row->pos_change, 1, '', 1, - 1, - 1, $conf->currency);
+					echo '</td>';
+					echo '</tr>';
+				}
+				$i++;
 			}
-			$i++;
 		}
 	}
 }

+ 23 - 0
htdocs/theme/eldy/global.inc.php

@@ -7591,6 +7591,29 @@ div.clipboardCPValue.hidewithsize {
 	/* filter: blur(4px); */
 }
 
+/* ============================================================================== */
+/* For drag and drop file feature                                                 */
+/* ============================================================================== */
+
+.cssDragDropArea{
+	position: relative;
+}
+.highlightDragDropArea{
+	border: 2px #000 dashed !important;
+	background-color: #eee !important;
+}
+.highlightDragDropArea * :not(.dragDropAreaMessage *){
+	opacity:0.8;
+	filter: blur(1px) grayscale(90%);
+}
+.dragDropAreaMessage {
+	position: absolute;
+	left:50%;
+	top:50%;
+	transform: translate(-50%, -50%);
+	text-align:center;
+	font-size: 2em;
+}
 
 /* ============================================================================== */
 /* CSS style used for small screen                                                */

+ 23 - 0
htdocs/theme/md/style.css.php

@@ -7508,6 +7508,29 @@ div.clipboardCPValue.hidewithsize {
 	zoom: 0.20;
 }
 
+/* ============================================================================== */
+/* For drag and drop file feature                                                 */
+/* ============================================================================== */
+
+.cssDragDropArea{
+	position: relative;
+}
+.highlightDragDropArea{
+	border: 2px #000 dashed !important;
+	background-color: #eee !important;
+}
+.highlightDragDropArea * :not(.dragDropAreaMessage *){
+	opacity:0.8;
+	filter: blur(1px) grayscale(90%);
+}
+.dragDropAreaMessage {
+	position: absolute;
+	left:50%;
+	top:50%;
+	transform: translate(-50%, -50%);
+	text-align:center;
+	font-size: 2em;
+}
 
 /* ============================================================================== */
 /* CSS style used for small screen                                                */

+ 34 - 37
htdocs/user/class/user.class.php

@@ -888,6 +888,7 @@ class User extends CommonObject
 		}
 
 		// Add automatically other permission using the criteria whereforadd
+		// $whereforadd can be a SQL filter or the string 'allmodules'
 		if (!empty($whereforadd)) {
 			//print "$module-$perms-$subperms";
 			$sql = "SELECT id";
@@ -897,31 +898,37 @@ class User extends CommonObject
 				$sql .= " AND (".$whereforadd.")";	// Note: parenthesis are important because whereforadd can contains OR. Also note that $whereforadd is already sanitized
 			}
 
-			$result = $this->db->query($sql);
-			if ($result) {
-				$num = $this->db->num_rows($result);
-				$i = 0;
-				while ($i < $num) {
-					$obj = $this->db->fetch_object($result);
+			$sqldelete = "DELETE FROM ".$this->db->prefix()."user_rights";
+			$sqldelete .= " WHERE fk_user = ".((int) $this->id)." AND fk_id IN (";
+			$sqldelete .= $sql;
+			$sqldelete .= ") AND entity = ".((int) $entity);
+			if (!$this->db->query($sqldelete)) {
+				$error++;
+			}
 
-					if ($obj) {
-						$nid = $obj->id;
+			if (!$error) {
+				$resql = $this->db->query($sql);
+				if ($resql) {
+					$num = $this->db->num_rows($resql);
+					$i = 0;
+					while ($i < $num) {
+						$obj = $this->db->fetch_object($resql);
 
-						$sql = "DELETE FROM ".$this->db->prefix()."user_rights WHERE fk_user = ".((int) $this->id)." AND fk_id = ".((int) $nid)." AND entity = ".((int) $entity);
-						if (!$this->db->query($sql)) {
-							$error++;
-						}
-						$sql = "INSERT INTO ".$this->db->prefix()."user_rights (entity, fk_user, fk_id) VALUES (".((int) $entity).", ".((int) $this->id).", ".((int) $nid).")";
-						if (!$this->db->query($sql)) {
-							$error++;
+						if ($obj) {
+							$nid = $obj->id;
+
+							$sql = "INSERT INTO ".$this->db->prefix()."user_rights (entity, fk_user, fk_id) VALUES (".((int) $entity).", ".((int) $this->id).", ".((int) $nid).")";
+							if (!$this->db->query($sql)) {
+								$error++;
+							}
 						}
-					}
 
-					$i++;
+						$i++;
+					}
+				} else {
+					$error++;
+					dol_print_error($this->db);
 				}
-			} else {
-				$error++;
-				dol_print_error($this->db);
 			}
 		}
 
@@ -1034,24 +1041,14 @@ class User extends CommonObject
 				$sql .= " AND id NOT IN (358)"; // user export
 			}
 
-			$result = $this->db->query($sql);
-			if ($result) {
-				$num = $this->db->num_rows($result);
-				$i = 0;
-				while ($i < $num) {
-					$obj = $this->db->fetch_object($result);
-					$nid = $obj->id;
-
-					$sql = "DELETE FROM ".$this->db->prefix()."user_rights";
-					$sql .= " WHERE fk_user = ".((int) $this->id)." AND fk_id = ".((int) $nid);
-					$sql .= " AND entity = ".((int) $entity);
-					if (!$this->db->query($sql)) {
-						$error++;
-					}
+			$sqldelete = "DELETE FROM ".$this->db->prefix()."user_rights";
+			$sqldelete .= " WHERE fk_user = ".((int) $this->id)." AND fk_id IN (";
+			$sqldelete .= $sql;
+			$sqldelete .= ")";
+			$sqldelete .= " AND entity = ".((int) $entity);
 
-					$i++;
-				}
-			} else {
+			$resql = $this->db->query($sqldelete);
+			if (!$resql) {
 				$error++;
 				dol_print_error($this->db);
 			}

+ 1 - 0
htdocs/user/list.php

@@ -365,6 +365,7 @@ $morehtmlright = "";
 // Build and execute select
 // --------------------------------------------------------------------
 $sql = "SELECT DISTINCT u.rowid, u.lastname, u.firstname, u.admin, u.fk_soc, u.login, u.office_phone, u.user_mobile, u.email, u.api_key, u.accountancy_code, u.gender, u.employee, u.photo,";
+$sql .= " u.fk_user,";
 $sql .= " u.ref_employee, u.national_registration_number, u.job, u.salary, u.datelastlogin, u.datepreviouslogin,";
 $sql .= " u.ldap_sid, u.statut as status, u.entity,";
 $sql .= " u.tms as date_update, u.datec as date_creation,";

+ 1 - 1
test/phpunit/DoliDBTest.php

@@ -143,7 +143,7 @@ class DoliDBTest extends PHPUnit\Framework\TestCase
 		$savtype = '';
 		$savnull = '';
 		$resql = $db->DDLDescTable($db->prefix().'c_paper_format', 'code');
-		while ($obj = $resql->fetch_object()) {
+		while ($obj = $db->fetch_object($resql)) {
 			if ($obj->Field == 'code') {
 				$savtype = $obj->Type;
 				$savnull = $obj->Null;

+ 379 - 0
test/phpunit/ODFTest.php

@@ -0,0 +1,379 @@
+<?php
+/* Copyright (C) 2007-2017 Laurent Destailleur  <eldy@users.sourceforge.net>
+ * Copyright (C) 2023 - Thomas Negre - contact@open-dsi.fr
+ *
+ * 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 <https://www.gnu.org/licenses/>.
+ */
+
+/**
+ * \file    test/unit/ODFTest.php
+ * \ingroup odf
+ * \brief   PHPUnit test for odf class.
+ */
+
+global $conf,$user,$langs,$db;
+//define('TEST_DB_FORCE_TYPE','mysql');	// This is to force using mysql driver
+//require_once 'PHPUnit/Autoload.php';
+require_once dirname(__FILE__).'/../../htdocs/master.inc.php';
+require_once dirname(__FILE__).'/../../htdocs/includes/odtphp/odf.php';
+
+if (empty($user->id)) {
+	print "Load permissions for admin user nb 1\n";
+	$user->fetch(1);
+	$user->getrights();
+}
+$conf->global->MAIN_DISABLE_ALL_MAILS=1;
+
+$langs->load("main");
+
+
+/**
+ * Class for PHPUnit tests
+ *
+ * @backupGlobals disabled
+ * @backupStaticAttributes enabled
+ * @remarks	backupGlobals must be disabled to have db,conf,user and lang not erased.
+ */
+class ODFTest extends PHPUnit\Framework\TestCase
+{
+	protected $savconf;
+	protected $savuser;
+	protected $savlangs;
+	protected $savdb;
+
+	/**
+	 * Constructor
+	 * We save global variables into local variables
+	 *
+	 * @return BOMTest
+	 */
+	public function __construct()
+	{
+		parent::__construct();
+
+		//$this->sharedFixture
+		global $conf,$user,$langs,$db;
+		$this->savconf=$conf;
+		$this->savuser=$user;
+		$this->savlangs=$langs;
+		$this->savdb=$db;
+
+		print __METHOD__." db->type=".$db->type." user->id=".$user->id;
+		//print " - db ".$db->db;
+		print "\n";
+	}
+
+	/**
+	 * setUpBeforeClass
+	 *
+	 * @return void
+	 */
+	public static function setUpBeforeClass()
+	{
+		global $conf,$user,$langs,$db;
+		$db->begin(); // This is to have all actions inside a transaction even if test launched without suite.
+
+		print __METHOD__."\n";
+	}
+
+	/**
+	 * tearDownAfterClass
+	 *
+	 * @return	void
+	 */
+	public static function tearDownAfterClass()
+	{
+		global $conf,$user,$langs,$db;
+		$db->rollback();
+
+		print __METHOD__."\n";
+	}
+
+	/**
+	 * Init phpunit tests
+	 *
+	 * @return  void
+	 */
+	protected function setUp()
+	{
+		global $conf,$user,$langs,$db;
+		$conf=$this->savconf;
+		$user=$this->savuser;
+		$langs=$this->savlangs;
+		$db=$this->savdb;
+
+		print __METHOD__."\n";
+	}
+
+	/**
+	 * End phpunit tests
+	 *
+	 * @return  void
+	 */
+	protected function tearDown()
+	{
+		print __METHOD__."\n";
+	}
+
+	/**
+	 * test ODF convertVarToOdf
+	 *
+	 * @return int
+	 */
+	public function testODFconvertVarToOdf()
+	{
+		global $conf,$user,$langs,$db;
+		$conf=$this->savconf;
+		$user=$this->savuser;
+		$langs=$this->savlangs;
+		$db=$this->savdb;
+
+		// we test using template_invoice, it does not matter, we just need a valid odt.
+		$filename = '../../htdocs/install/doctemplates/invoices/template_invoice.odt';
+		$config = [
+			'PATH_TO_TMP'  => "/tmp",
+			'ZIP_PROXY' => "PclZipProxy",
+			'DELIMITER_LEFT' => "{",
+			'DELIMITER_RIGHT' => "}",
+		];
+
+		$to_test = [
+			/** No HTML **/
+			// Simple strings
+			1 => [
+				'to_convert' => 'Simple string',
+				'encode' => true,
+				'charset' => null,
+				'expected' => 'Simple string',
+			],
+			2 => [
+				'to_convert' => 'Simple string',
+				'encode' => false,
+				'charset' => null,
+				'expected' => 'Simple string',
+			],
+			3 => [
+				'to_convert' => "Simple string\nwith line break",
+				'encode' => true,
+				'charset' => null,
+				'expected' => "Simple string<text:line-break/>with line break",
+			],
+			4 => [
+				'to_convert' => "Simple string\nwith line break",
+				'encode' => false,
+				'charset' => null,
+				'expected' => "Simple string<text:line-break/>with line break",
+			],
+			// Special chars
+			5 => [
+				'to_convert' => 'One&two',
+				'encode' => true,
+				'charset' => null,
+				'expected' => 'One&amp;two',
+			],
+			6 => [
+				'to_convert' => 'One&two',
+				'encode' => false,
+				'charset' => null,
+				'expected' => 'One&two',
+			],
+			7 => [
+				'to_convert' => "/a&él'èàüöç€Ğ~<>",
+				'encode' => true,
+				'charset' => null,
+				'expected' => utf8_encode("/a&amp;él&apos;èàüöç€Ğ~&lt;&gt;"),
+			],
+			8 => [
+				'to_convert' => "/a&él'èàüöç€Ğ~<>",
+				'encode' => false,
+				'charset' => null,
+				'expected' => utf8_encode("/a&él'èàüöç€Ğ~<>"),
+			],
+			// special chars with non-default charset
+			9 => [
+				'to_convert' => "/a&él'èàüöç€Ğ~<>",
+				'encode' => true,
+				'charset' => 'UTF-16',
+				'expected' => "/a&amp;él&apos;èàüöç€Ğ~&lt;&gt;",
+			],
+			10 => [
+				'to_convert' => "/a&él'èàüöç€Ğ~<>",
+				'encode' => false,
+				'charset' => 'UTF-16', // When the charset differs from ISO-8859 string is not converted.
+				'expected' => "/a&él'èàüöç€Ğ~<>",
+			],
+			11 => [
+				'to_convert' => "Greater > than",
+				'encode' => true,
+				'charset' => null,
+				'expected' => utf8_encode("Greater &gt; than"),
+			],
+			12 => [
+				'to_convert' => "Greater > than",
+				'encode' => false,
+				'charset' => null,
+				'expected' => utf8_encode("Greater > than"),
+			],
+			13 => [
+				'to_convert' => "Smaller < than",
+				'encode' => true,
+				'charset' => null,
+				'expected' => utf8_encode("Smaller &lt; than"),
+			],
+			14 => [
+				'to_convert' => "Smaller < than",
+				'encode' => false,
+				'charset' => null,
+				'expected' => utf8_encode("Smaller < than"),
+			],
+			/** HTML **/
+			// break lines
+			15 => [
+				'to_convert' => "Break<br>line",
+				'encode' => true,
+				'charset' => null,
+				'expected' => utf8_encode("Break<text:line-break/>line"),
+			],
+			16 => [
+				'to_convert' => "Break<br>line",
+				'encode' => false,
+				'charset' => null,
+				'expected' => utf8_encode("Break<text:line-break/>line"),
+			],
+			17 => [
+				'to_convert' => "Break<br />line",
+				'encode' => true,
+				'charset' => null,
+				'expected' => utf8_encode("Break<text:line-break/>line"),
+			],
+			18 => [
+				'to_convert' => "Break<br />line",
+				'encode' => false,
+				'charset' => null,
+				'expected' => utf8_encode("Break<text:line-break/>line"),
+			],
+			// HTML tags
+			19 => [
+				'to_convert' => "text with <strong>strong, </strong><em>emphasis</em> and <u>underlined</u> words with <i>it@lic sp&ciàlchärs éè l'</i>",
+				'encode' => false,
+				'charset' => 'UTF-8',
+				'expected' => 'text with <text:span text:style-name="boldText">strong, </text:span><text:span text:style-name="italicText">emphasis</text:span> and <text:span text:style-name="underlineText">underlined</text:span> words with <text:span text:style-name="italicText">it@lic sp&ciàlchärs éè l\'</text:span>',
+			],
+			20 => [
+				'to_convert' => "text with <strong>strong, </strong><em>emphasis</em> and <u>underlined</u> words with <i>it@lic sp&ciàlchärs éè l'</i>",
+				'encode' => true,
+				'charset' => 'UTF-8',
+				'expected' => 'text with <text:span text:style-name="boldText">strong, </text:span><text:span text:style-name="italicText">emphasis</text:span> and <text:span text:style-name="underlineText">underlined</text:span> words with <text:span text:style-name="italicText">it@lic sp&amp;ciàlchärs éè l&apos;</text:span>',
+			],
+			21 => [
+				'to_convert' => "text with <strong>strong, </strong><em>emphasis</em> and <u>underlined</u> words with <i>it@lic sp&ciàlchärs éè l'</i>",
+				'encode' => false,
+				'charset' => null,
+				'expected' => utf8_encode('text with <text:span text:style-name="boldText">strong, </text:span><text:span text:style-name="italicText">emphasis</text:span> and <text:span text:style-name="underlineText">underlined</text:span> words with <text:span text:style-name="italicText">it@lic sp&ciàlchärs éè l\'</text:span>'),
+			],
+			22 => [
+				'to_convert' => "text with <strong>strong, </strong><em>emphasis</em> and <u>underlined</u> words with <i>it@lic sp&ciàlchärs éè l'</i>",
+				'encode' => true,
+				'charset' => null,
+				'expected' => utf8_encode('text with <text:span text:style-name="boldText">strong, </text:span><text:span text:style-name="italicText">emphasis</text:span> and <text:span text:style-name="underlineText">underlined</text:span> words with <text:span text:style-name="italicText">it@lic sp&amp;ciàlchärs éè l&apos;</text:span>'),
+			],
+			23 => [
+				'to_convert' => "text with <strong>intricated<u>tags</u></strong>",
+				'encode' => true,
+				'charset' => null,
+				'expected' => utf8_encode('text with <text:span text:style-name="boldText">intricated<text:span text:style-name="underlineText">tags</text:span></text:span>'),
+			],
+
+			// One can also pass html-encoded string to the method
+			24 => [
+				'to_convert' => 'One&amp;two',
+				'encode' => true,
+				'charset' => null,
+				'expected' => 'One&amp;two',
+			],
+			25 => [
+				'to_convert' => "text with &lt;strong&gt;strong, &lt;/strong&gt;&lt;em&gt;emphasis&lt;/em&gt; and &lt;u&gt;underlined&lt;/u&gt; words with &lt;i&gt;it@lic sp&amp;ciàlchärs éè l'&lt;/i&gt;",
+				'encode' => false,
+				'charset' => 'UTF-8',
+				'expected' => 'text with <text:span text:style-name="boldText">strong, </text:span><text:span text:style-name="italicText">emphasis</text:span> and <text:span text:style-name="underlineText">underlined</text:span> words with <text:span text:style-name="italicText">it@lic sp&ciàlchärs éè l\'</text:span>',
+			],
+			26 => [
+				'to_convert' => "text with &lt;strong&gt;strong, &lt;/strong&gt;&lt;em&gt;emphasis&lt;/em&gt; and &lt;u&gt;underlined&lt;/u&gt; words with &lt;i&gt;it@lic sp&amp;ciàlchärs éè l'&lt;/i&gt;",
+				'encode' => true,
+				'charset' => 'UTF-8',
+				'expected' => 'text with <text:span text:style-name="boldText">strong, </text:span><text:span text:style-name="italicText">emphasis</text:span> and <text:span text:style-name="underlineText">underlined</text:span> words with <text:span text:style-name="italicText">it@lic sp&amp;ciàlchärs éè l&apos;</text:span>',
+			],
+			27 => [
+				'to_convert' => "text with &lt;strong&gt;strong, &lt;/strong&gt;&lt;em&gt;emphasis&lt;/em&gt; and &lt;u&gt;underlined&lt;/u&gt; words with &lt;i&gt;it@lic sp&amp;ciàlchärs éè l'&lt;/i&gt;",
+				'encode' => false,
+				'charset' => null,
+				'expected' => utf8_encode('text with <text:span text:style-name="boldText">strong, </text:span><text:span text:style-name="italicText">emphasis</text:span> and <text:span text:style-name="underlineText">underlined</text:span> words with <text:span text:style-name="italicText">it@lic sp&ciàlchärs éè l\'</text:span>'),
+			],
+			28 => [
+				'to_convert' => "text with &lt;strong&gt;strong, &lt;/strong&gt;&lt;em&gt;emphasis&lt;/em&gt; and &lt;u&gt;underlined&lt;/u&gt; words with &lt;i&gt;it@lic sp&amp;ciàlchärs éè l'&lt;/i&gt;",
+				'encode' => true,
+				'charset' => null,
+				'expected' => utf8_encode('text with <text:span text:style-name="boldText">strong, </text:span><text:span text:style-name="italicText">emphasis</text:span> and <text:span text:style-name="underlineText">underlined</text:span> words with <text:span text:style-name="italicText">it@lic sp&amp;ciàlchärs éè l&apos;</text:span>'),
+			],
+
+			// // TODO custom styles are not tested for now : the custom style have a custom ID based on time. Not random, but hard to mock or predict. generated in _replaceHtmlWithOdtTag() case 'span'.
+			// [
+			// 	'to_convert' => '123 <span style="color:#e74c3c">trucmachin > truc < troc > trac</span>bla bla',
+			// 	'encode' => true,
+			// 	'charset' => 'UTF-8',
+			// 	'expected' => "123 <text:span text:style-name="customStyle1668592427018">trucmachin &gt; truc &lt; troc &gt; trac</text:span>bla bla'",
+			// ],
+
+			/* Tests that can evolve */
+			// Following tests reflect the current behavior. They may evolve if the method behavior changes.
+
+			// The method removes hyperlinks and tags that are not dealt with.
+			29 => [
+				'to_convert' => '123 <a href="/test.php">trucmachin > truc < troc > trac</a>bla bla',
+				'encode' => true,
+				'charset' => null,
+				'expected' => "123 trucmachin &gt; truc &lt; troc &gt; tracbla bla",
+			],
+			30 => [
+				'to_convert' => '123 <h3>Title</h3> bla',
+				'encode' => true,
+				'charset' => null,
+				'expected' => "123 Title bla",
+			],
+			// HTML should not take \n into account, but only <br />.
+			31 => [
+				'to_convert' => "text with <strong>strong text </strong>, a line\nbreak and <u>underlined</u> words with <i>it@lic sp&ciàlchärs éè l'</i>",
+				'encode' => false,
+				'charset' => 'UTF-8',
+				'expected' => 'text with <text:span text:style-name="boldText">strong text </text:span>, a line'."\n".'break and <text:span text:style-name="underlineText">underlined</text:span> words with <text:span text:style-name="italicText">it@lic sp&ciàlchärs éè l\'</text:span>',
+			],
+		];
+
+		$odf=new Odf($filename, array());
+		if (is_object($odf)) $result = 1; // Just to test
+
+		foreach ($to_test as $case) {
+			if ($case['charset'] !== null) {
+				$res = $odf->convertVarToOdf($case['to_convert'], $case['encode'], $case['charset']);
+			} else {
+				$res = $odf->convertVarToOdf($case['to_convert'], $case['encode']);
+			}
+			$this->assertEquals($res, $case['expected']);
+		}
+
+		print __METHOD__." result=".$result."\n";
+
+		return $result;
+	}
+}