Browse Source

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

# Conflicts:
#	htdocs/compta/facture/card.php
#	htdocs/fourn/facture/card.php
Laurent Destailleur 2 years ago
parent
commit
7be8ea782d
100 changed files with 8470 additions and 629 deletions
  1. 6 0
      .travis.yml
  2. 3 1
      COPYRIGHT
  3. 86 2
      ChangeLog
  4. 1 0
      htdocs/adherents/admin/member_emails.php
  5. 1 1
      htdocs/adherents/canvas/actions_adherentcard_common.class.php
  6. 7 1
      htdocs/adherents/class/adherent.class.php
  7. 1 1
      htdocs/adherents/subscription.php
  8. 1 1
      htdocs/adherents/subscription/list.php
  9. 4 4
      htdocs/admin/dav.php
  10. 15 8
      htdocs/admin/emailcollector_card.php
  11. 11 8
      htdocs/admin/pdf_other.php
  12. 4 4
      htdocs/api/class/api_setup.class.php
  13. 3 3
      htdocs/categories/index.php
  14. 8 4
      htdocs/categories/viewcat.php
  15. 8 2
      htdocs/compta/bank/card.php
  16. 6 2
      htdocs/compta/bank/class/account.class.php
  17. 1 1
      htdocs/compta/facture/card.php
  18. 3 3
      htdocs/compta/facture/class/facture.class.php
  19. 294 166
      htdocs/compta/prelevement/list.php
  20. 244 119
      htdocs/compta/prelevement/orders_list.php
  21. 14 7
      htdocs/compta/prelevement/stats.php
  22. 1 1
      htdocs/contact/canvas/actions_contactcard_common.class.php
  23. 8 1
      htdocs/contrat/class/contrat.class.php
  24. 1 1
      htdocs/core/boxes/box_members_by_tags.php
  25. 2 2
      htdocs/core/class/CMailFile.class.php
  26. 15 0
      htdocs/core/class/commonobject.class.php
  27. 12 5
      htdocs/core/class/extrafields.class.php
  28. 85 28
      htdocs/core/class/html.form.class.php
  29. 6 6
      htdocs/core/class/notify.class.php
  30. 251 86
      htdocs/core/customreports.php
  31. 1 1
      htdocs/core/lib/admin.lib.php
  32. 1 1
      htdocs/core/lib/files.lib.php
  33. 68 13
      htdocs/core/lib/functions.lib.php
  34. 39 0
      htdocs/core/lib/functions2.lib.php
  35. 109 0
      htdocs/core/lib/modulebuilder.lib.php
  36. 4 4
      htdocs/core/menus/standard/eldy.lib.php
  37. 32 13
      htdocs/core/modules/facture/doc/pdf_crabe.modules.php
  38. 36 21
      htdocs/core/modules/facture/doc/pdf_sponge.modules.php
  39. 155 0
      htdocs/core/modules/facture/modules_facture.php
  40. 4 2
      htdocs/core/modules/modEmailCollector.class.php
  41. 1 1
      htdocs/core/modules/modFournisseur.class.php
  42. 1 35
      htdocs/core/modules/modWebhook.class.php
  43. 2 1
      htdocs/core/modules/modWorkstation.class.php
  44. 5 2
      htdocs/core/modules/supplier_invoice/mod_facture_fournisseur_cactus.php
  45. 1 1
      htdocs/core/tpl/list_print_total.tpl.php
  46. 13 10
      htdocs/emailcollector/class/emailcollector.class.php
  47. 1 1
      htdocs/expedition/card.php
  48. 6 6
      htdocs/expedition/dispatch.php
  49. 1 1
      htdocs/fourn/facture/card.php
  50. 1 43
      htdocs/holiday/class/holiday.class.php
  51. 6 5
      htdocs/holiday/list.php
  52. 22 0
      htdocs/includes/bacon/bacon-qr-code/LICENSE
  53. 39 0
      htdocs/includes/bacon/bacon-qr-code/README.md
  54. 44 0
      htdocs/includes/bacon/bacon-qr-code/composer.json.disabled
  55. 372 0
      htdocs/includes/bacon/bacon-qr-code/src/Common/BitArray.php
  56. 313 0
      htdocs/includes/bacon/bacon-qr-code/src/Common/BitMatrix.php
  57. 41 0
      htdocs/includes/bacon/bacon-qr-code/src/Common/BitUtils.php
  58. 183 0
      htdocs/includes/bacon/bacon-qr-code/src/Common/CharacterSetEci.php
  59. 49 0
      htdocs/includes/bacon/bacon-qr-code/src/Common/EcBlock.php
  60. 74 0
      htdocs/includes/bacon/bacon-qr-code/src/Common/EcBlocks.php
  61. 63 0
      htdocs/includes/bacon/bacon-qr-code/src/Common/ErrorCorrectionLevel.php
  62. 203 0
      htdocs/includes/bacon/bacon-qr-code/src/Common/FormatInformation.php
  63. 79 0
      htdocs/includes/bacon/bacon-qr-code/src/Common/Mode.php
  64. 468 0
      htdocs/includes/bacon/bacon-qr-code/src/Common/ReedSolomonCodec.php
  65. 596 0
      htdocs/includes/bacon/bacon-qr-code/src/Common/Version.php
  66. 58 0
      htdocs/includes/bacon/bacon-qr-code/src/Encoder/BlockPair.php
  67. 150 0
      htdocs/includes/bacon/bacon-qr-code/src/Encoder/ByteMatrix.php
  68. 668 0
      htdocs/includes/bacon/bacon-qr-code/src/Encoder/Encoder.php
  69. 271 0
      htdocs/includes/bacon/bacon-qr-code/src/Encoder/MaskUtil.php
  70. 513 0
      htdocs/includes/bacon/bacon-qr-code/src/Encoder/MatrixUtil.php
  71. 141 0
      htdocs/includes/bacon/bacon-qr-code/src/Encoder/QrCode.php
  72. 10 0
      htdocs/includes/bacon/bacon-qr-code/src/Exception/ExceptionInterface.php
  73. 8 0
      htdocs/includes/bacon/bacon-qr-code/src/Exception/InvalidArgumentException.php
  74. 8 0
      htdocs/includes/bacon/bacon-qr-code/src/Exception/OutOfBoundsException.php
  75. 8 0
      htdocs/includes/bacon/bacon-qr-code/src/Exception/RuntimeException.php
  76. 8 0
      htdocs/includes/bacon/bacon-qr-code/src/Exception/UnexpectedValueException.php
  77. 8 0
      htdocs/includes/bacon/bacon-qr-code/src/Exception/WriterException.php
  78. 57 0
      htdocs/includes/bacon/bacon-qr-code/src/Renderer/Color/Alpha.php
  79. 103 0
      htdocs/includes/bacon/bacon-qr-code/src/Renderer/Color/Cmyk.php
  80. 22 0
      htdocs/includes/bacon/bacon-qr-code/src/Renderer/Color/ColorInterface.php
  81. 46 0
      htdocs/includes/bacon/bacon-qr-code/src/Renderer/Color/Gray.php
  82. 88 0
      htdocs/includes/bacon/bacon-qr-code/src/Renderer/Color/Rgb.php
  83. 38 0
      htdocs/includes/bacon/bacon-qr-code/src/Renderer/Eye/CompositeEye.php
  84. 26 0
      htdocs/includes/bacon/bacon-qr-code/src/Renderer/Eye/EyeInterface.php
  85. 54 0
      htdocs/includes/bacon/bacon-qr-code/src/Renderer/Eye/ModuleEye.php
  86. 54 0
      htdocs/includes/bacon/bacon-qr-code/src/Renderer/Eye/SimpleCircleEye.php
  87. 53 0
      htdocs/includes/bacon/bacon-qr-code/src/Renderer/Eye/SquareEye.php
  88. 376 0
      htdocs/includes/bacon/bacon-qr-code/src/Renderer/Image/EpsImageBackEnd.php
  89. 87 0
      htdocs/includes/bacon/bacon-qr-code/src/Renderer/Image/ImageBackEndInterface.php
  90. 336 0
      htdocs/includes/bacon/bacon-qr-code/src/Renderer/Image/ImagickImageBackEnd.php
  91. 369 0
      htdocs/includes/bacon/bacon-qr-code/src/Renderer/Image/SvgImageBackEnd.php
  92. 68 0
      htdocs/includes/bacon/bacon-qr-code/src/Renderer/Image/TransformationMatrix.php
  93. 152 0
      htdocs/includes/bacon/bacon-qr-code/src/Renderer/ImageRenderer.php
  94. 63 0
      htdocs/includes/bacon/bacon-qr-code/src/Renderer/Module/DotsModule.php
  95. 100 0
      htdocs/includes/bacon/bacon-qr-code/src/Renderer/Module/EdgeIterator/Edge.php
  96. 169 0
      htdocs/includes/bacon/bacon-qr-code/src/Renderer/Module/EdgeIterator/EdgeIterator.php
  97. 18 0
      htdocs/includes/bacon/bacon-qr-code/src/Renderer/Module/ModuleInterface.php
  98. 129 0
      htdocs/includes/bacon/bacon-qr-code/src/Renderer/Module/RoundnessModule.php
  99. 47 0
      htdocs/includes/bacon/bacon-qr-code/src/Renderer/Module/SquareModule.php
  100. 29 0
      htdocs/includes/bacon/bacon-qr-code/src/Renderer/Path/Close.php

+ 6 - 0
.travis.yml

@@ -221,6 +221,8 @@ before_script:
       pgloader mysql://root@127.0.0.1/travis postgresql://postgres@/travis
       pgloader mysql://root@127.0.0.1/travis postgresql://postgres@/travis
       echo 'ALTER SEQUENCE llx_accountingaccount_rowid_seq RENAME TO llx_accounting_account_rowid_seq' | psql -U postgres travis
       echo 'ALTER SEQUENCE llx_accountingaccount_rowid_seq RENAME TO llx_accounting_account_rowid_seq' | psql -U postgres travis
       echo 'ALTER SEQUENCE llx_accounting_account_rowid_seq RESTART WITH 1000001;' | psql -U postgres travis
       echo 'ALTER SEQUENCE llx_accounting_account_rowid_seq RESTART WITH 1000001;' | psql -U postgres travis
+      # Create pgsql compatibility functions
+      psql -U postgres travis < htdocs/install/pgsql/functions/functions.sql
     fi
     fi
     echo
     echo
 
 
@@ -459,6 +461,9 @@ script:
   php upgrade2.php 17.0.0 18.0.0 > $TRAVIS_BUILD_DIR/upgrade17001800-2.log
   php upgrade2.php 17.0.0 18.0.0 > $TRAVIS_BUILD_DIR/upgrade17001800-2.log
   php step5.php 17.0.0 18.0.0 > $TRAVIS_BUILD_DIR/upgrade17001800-3.log
   php step5.php 17.0.0 18.0.0 > $TRAVIS_BUILD_DIR/upgrade17001800-3.log
   ls -alrt $TRAVIS_BUILD_DIR/
   ls -alrt $TRAVIS_BUILD_DIR/
+  echo
+  #cat $TRAVIS_BUILD_DIR/upgrade17001800.log
+  #cat $TRAVIS_BUILD_DIR/upgrade17001800-2.log
 
 
 - |
 - |
   echo "Enabling new modules"
   echo "Enabling new modules"
@@ -468,6 +473,7 @@ script:
   php upgrade2.php 0.0.0 0.0.0 MAIN_MODULE_WEBSITE,MAIN_MODULE_TICKET,MAIN_MODULE_ACCOUNTING,MAIN_MODULE_MRP >> $TRAVIS_BUILD_DIR/enablemodule.log
   php upgrade2.php 0.0.0 0.0.0 MAIN_MODULE_WEBSITE,MAIN_MODULE_TICKET,MAIN_MODULE_ACCOUNTING,MAIN_MODULE_MRP >> $TRAVIS_BUILD_DIR/enablemodule.log
   php upgrade2.php 0.0.0 0.0.0 MAIN_MODULE_RECEPTION,MAIN_MODULE_RECRUITMENT >> $TRAVIS_BUILD_DIR/enablemodule.log
   php upgrade2.php 0.0.0 0.0.0 MAIN_MODULE_RECEPTION,MAIN_MODULE_RECRUITMENT >> $TRAVIS_BUILD_DIR/enablemodule.log
   php upgrade2.php 0.0.0 0.0.0 MAIN_MODULE_KNOWLEDGEMANAGEMENT,MAIN_MODULE_EVENTORGANIZATION,MAIN_MODULE_PARTNERSHIP >> $TRAVIS_BUILD_DIR/enablemodule.log
   php upgrade2.php 0.0.0 0.0.0 MAIN_MODULE_KNOWLEDGEMANAGEMENT,MAIN_MODULE_EVENTORGANIZATION,MAIN_MODULE_PARTNERSHIP >> $TRAVIS_BUILD_DIR/enablemodule.log
+  php upgrade2.php 0.0.0 0.0.0 MAIN_MODULE_EMAILCOLLECTOR >> $TRAVIS_BUILD_DIR/enablemodule.log
   echo $?
   echo $?
   cd -
   cd -
   set +e
   set +e

+ 3 - 1
COPYRIGHT

@@ -40,13 +40,15 @@ PHPPrintIPP            1.3           GPL-2+                      Yes
 PSR/Logs               1.0           MIT License                 Yes             Library for logs (used by DebugBar)
 PSR/Logs               1.0           MIT License                 Yes             Library for logs (used by DebugBar)
 PSR/simple-cache       ?             MIT License                 Yes             Library for cache (used by PHPSpreadSheet)
 PSR/simple-cache       ?             MIT License                 Yes             Library for cache (used by PHPSpreadSheet)
 Restler                3.1.1         LGPL-3+                     Yes             Library to develop REST Web services (+ swagger-ui js lib into dir explorer)
 Restler                3.1.1         LGPL-3+                     Yes             Library to develop REST Web services (+ swagger-ui js lib into dir explorer)
-Sabre                  3.2.2         BSD                         Yes             DAV support
+Sabre                  4.0.2         BSD                         Yes             DAV support
 Swift Mailer           5.4.2-DEV     MIT License                 Yes             Comprehensive mailing tools for PHP
 Swift Mailer           5.4.2-DEV     MIT License                 Yes             Comprehensive mailing tools for PHP
 Symfony/var-dumper     ??? 			 MIT License                 Yes             Library to make var dump (used by DebugBar)
 Symfony/var-dumper     ??? 			 MIT License                 Yes             Library to make var dump (used by DebugBar)
 Stripe                 10.7.0        MIT Licence                 Yes             Library for Stripe module
 Stripe                 10.7.0        MIT Licence                 Yes             Library for Stripe module
 TCPDF                  6.3.2         LGPL-3+                     Yes             PDF generation
 TCPDF                  6.3.2         LGPL-3+                     Yes             PDF generation
 TCPDI                  1.0.0         LGPL-3+ / Apache 2.0        Yes             FPDI replacement
 TCPDI                  1.0.0         LGPL-3+ / Apache 2.0        Yes             FPDI replacement
 
 
+bacon, dasprid, swiss-qr-bill, kmukku, symfony/validator
+
 JS libraries:
 JS libraries:
 Ace                    1.4.14        BSD                         Yes             JS library to get code syntaxique coloration in a textarea.
 Ace                    1.4.14        BSD                         Yes             JS library to get code syntaxique coloration in a textarea.
 ChartJS                3.7.1         MIT License                 Yes             JS library for graph
 ChartJS                3.7.1         MIT License                 Yes             JS library for graph

+ 86 - 2
ChangeLog

@@ -9,6 +9,8 @@ For uses:
 ---------
 ---------
 
 
 NEW: PHP 8.2 compatibility (test not yet completed).
 NEW: PHP 8.2 compatibility (test not yet completed).
+NEW: Module Workstations Management upgraded to stable status
+NEW: Module Webhook upgraded to stable status
 NEW: #23436 Group social networks fields
 NEW: #23436 Group social networks fields
 NEW: Accountancy - Add specific page to export accounting data rather than the journals page
 NEW: Accountancy - Add specific page to export accounting data rather than the journals page
 NEW: Accountancy - Add sub-account balance FPC22
 NEW: Accountancy - Add sub-account balance FPC22
@@ -211,8 +213,90 @@ Following changes may create regressions for some external modules, but were nec
 * The type 'text' in ->fields property dos not accept html content anymore. Use the type 'html' for that.
 * The type 'text' in ->fields property dos not accept html content anymore. Use the type 'html' for that.
 * The module for WebService SOAP API have been deprecated. Use instead the Webservice REST API module.
 * The module for WebService SOAP API have been deprecated. Use instead the Webservice REST API module.
 * The method htmlPrintOnlinePaymentFooter() used for public footer pages has been renamed into htmlPrintOnlineFooter() and moved into company.lib.php
 * The method htmlPrintOnlinePaymentFooter() used for public footer pages has been renamed into htmlPrintOnlineFooter() and moved into company.lib.php
-
-
+* The method getCheckOption() and deleteCPUser() of class Holiday has been removed (it was not used)
+
+
+***** ChangeLog for 17.0.2 compared to 17.0.1 *****
+
+FIX: #24414
+FIX: #24798 Deleting member subscription is not possible
+FIX: add a test for updating date on task update in tab time consummed pro…
+FIX: add charchesociales in security.lib.php
+FIX: Add Missing rights check on holiday calendar
+FIX: Add the possibility to events owner to check their events from the list when the perm "Read the actions (events or tasks) of others" is not active
+FIX: Authorize '0' subprice in supplier proposal line
+FIX: avoid error when computed property of extrafields is used
+FIX: avoid warnings php8
+FIX: Back to cancel on reception creation
+FIX: Bad deletion of email when there is several emails processed
+FIX: bank account not visible on credit transfer
+FIX: Better support for option MAIN_NO_INPUT_PRICE_WITH_TAX
+FIX: broken feature : send private message by email
+FIX: categorie compatibility with actioncomm
+FIX: Click on "NEW" in simple POS was broken
+FIX: Compress in xz for better debian old version compatibility
+FIX: Correct Evaluation for extrafields
+FIX: Count only attendee draft of validated.
+FIX: Creation of thumbs when images.lib.php was already included
+FIX: delete of warehouse
+FIX: deletion of a line of time spent (backport e3aa438d2a582313dfd5178b8cc5975e0c912c25)
+FIX: Deployment of external module failed with copy dir error.
+FIX: edit field value of url
+FIX: error management on emoji and utf8 validity by emailcollector
+FIX: expense report autofill ttc input if force ttc conf is enabled
+FIX: filter missing id on consumption contact card
+FIX: Filter on member status
+FIX: Filter status orders in list no invoiced if validated + in progress + delivered
+FIX: Fiscal year list ref display
+FIX: for empty shipping
+FIX: INVOICE_SHOW_SHIPPING_ADDRESS
+FIX: jump to direct record on member search was broken
+FIX: label of columns must be short into invoice PDF table
+FIX: making search in takepos broken when TAKEPOS_PRODUCT_IN_STOCK is set
+FIX: Margin calculation for credit notes on margin reports
+FIX: message MAIN_MESSAGE_INSTALL_MODULES_DISABLED_CONTACT_US
+FIX: missing checking if file is uploaded
+FIX: missing constant and avoid submit button conflict
+FIX: missing entity field in unique index (since v16)
+FIX: Missing error message display on insertExtrafields()
+FIX: missing mrp module dependency
+FIX: More complete fix for #24411
+FIX: No error message because  $price_ht_devise is equal to '0' if not filled because of price2num
+FIX: No usage of the function updateProduction in the update function
+FIX: On company change, we must reuse the company parameters
+FIX: Operator to search category Knowledge
+FIX: Pb in install when password start with some special char like !
+FIX: pb in sending email when mail contains data src image.
+FIX: PDF Font for turkish language
+FIX: product notes rights
+FIX: Propagate correct origin/origin_id when creating order from proposal
+FIX: Propagate extrafields from supplier order to reception
+FIX: reassortlot search categorie + add inithooks
+FIX: Reception process loose some lines on first error.
+FIX: redesign of the function : updateProduction
+FIX: ref_client on Project Overview for propale
+FIX: reference id in getnomurl function.
+FIX: regression Undefined $datepaid
+FIX: remove NOTOKENRENOWAL (backport commit v17 7c316229db8060781ee50f4465b1133b5aeef156)
+FIX: Remove warning on lettering - Impossible to write in ledger v16 v17 v18
+FIX: Report of date of task suggested only if there is tasks
+FIX: Rounding on total margin on invoice list
+FIX: Search List Select Extrafields with condition
+FIX: Search when criteria start with !
+FIX: Sending email from attendee list
+FIX: Shipping address same third party
+FIX: special chars in generated file name from build doc mass action
+FIX: supplier invoice status on bank transfer line
+FIX: supplier link on bank transfer line
+FIX: Task events not displayed
+FIX: token errors on public interface
+FIX: Transfer between accounts with different currencies was broken
+FIX: Update hour of intervention line
+FIX: Upload of files on public ticket interface
+FIX: Use max parameters of widget graph product distribution
+FIX: Warehouse total line
+FIX: When salary module is not enabled, bad permission check on user list
+FIX: wrong colspan for tasks list
 
 
 ***** ChangeLog for 17.0.1 compared to 17.0.0 *****
 ***** ChangeLog for 17.0.1 compared to 17.0.0 *****
 
 

+ 1 - 0
htdocs/adherents/admin/member_emails.php

@@ -63,6 +63,7 @@ $constantes = array(
 	'ADHERENT_EMAIL_TEMPLATE_CANCELATION'			=>array('type'=>'emailtemplate:member'),
 	'ADHERENT_EMAIL_TEMPLATE_CANCELATION'			=>array('type'=>'emailtemplate:member'),
 	'ADHERENT_EMAIL_TEMPLATE_EXCLUSION'				=>array('type'=>'emailtemplate:member'),
 	'ADHERENT_EMAIL_TEMPLATE_EXCLUSION'				=>array('type'=>'emailtemplate:member'),
 	'ADHERENT_MAIL_FROM'							=>array('type'=>'string'),
 	'ADHERENT_MAIL_FROM'							=>array('type'=>'string'),
+	'ADHERENT_CC_MAIL_FROM'							=>array('type'=>'string'),
 	'ADHERENT_AUTOREGISTER_NOTIF_MAIL_SUBJECT'		=>array('type'=>'string'),
 	'ADHERENT_AUTOREGISTER_NOTIF_MAIL_SUBJECT'		=>array('type'=>'string'),
 	'ADHERENT_AUTOREGISTER_NOTIF_MAIL'				=>array('type'=>'html', 'tooltip'=>$helptext)
 	'ADHERENT_AUTOREGISTER_NOTIF_MAIL'				=>array('type'=>'html', 'tooltip'=>$helptext)
 );
 );

+ 1 - 1
htdocs/adherents/canvas/actions_adherentcard_common.class.php

@@ -241,7 +241,7 @@ abstract class ActionsAdherentCardCommon
 	/**
 	/**
 	 *  Assign POST values into object
 	 *  Assign POST values into object
 	 *
 	 *
-	 *  @return		string					HTML output
+	 *  @return		void
 	 */
 	 */
 	private function assign_post()
 	private function assign_post()
 	{
 	{

+ 7 - 1
htdocs/adherents/class/adherent.class.php

@@ -64,6 +64,11 @@ class Adherent extends CommonObject
 	 */
 	 */
 	public $ismultientitymanaged = 1;
 	public $ismultientitymanaged = 1;
 
 
+	/**
+	 * @var int  Does object support extrafields ? 0=No, 1=Yes
+	 */
+	public $isextrafieldmanaged = 1;
+
 	/**
 	/**
 	 * @var string picto
 	 * @var string picto
 	 */
 	 */
@@ -3146,12 +3151,13 @@ class Adherent extends CommonObject
 							$msg = make_substitutions($arraydefaultmessage->content, $substitutionarray, $outputlangs);
 							$msg = make_substitutions($arraydefaultmessage->content, $substitutionarray, $outputlangs);
 							$from = getDolGlobalString('ADHERENT_MAIL_FROM');
 							$from = getDolGlobalString('ADHERENT_MAIL_FROM');
 							$to = $adherent->email;
 							$to = $adherent->email;
+							$cc = getDolGlobalString('ADHERENT_CC_MAIL_FROM');
 
 
 							$trackid = 'mem'.$adherent->id;
 							$trackid = 'mem'.$adherent->id;
 							$moreinheader = 'X-Dolibarr-Info: sendReminderForExpiredSubscription'."\r\n";
 							$moreinheader = 'X-Dolibarr-Info: sendReminderForExpiredSubscription'."\r\n";
 
 
 							include_once DOL_DOCUMENT_ROOT.'/core/class/CMailFile.class.php';
 							include_once DOL_DOCUMENT_ROOT.'/core/class/CMailFile.class.php';
-							$cmail = new CMailFile($subject, $to, $from, $msg, array(), array(), array(), '', '', 0, 1, '', '', $trackid, $moreinheader);
+							$cmail = new CMailFile($subject, $to, $from, $msg, array(), array(), array(), $cc, '', 0, 1, '', '', $trackid, $moreinheader);
 							$result = $cmail->sendfile();
 							$result = $cmail->sendfile();
 							if (!$result) {
 							if (!$result) {
 								$error++;
 								$error++;

+ 1 - 1
htdocs/adherents/subscription.php

@@ -995,7 +995,7 @@ if ($rowid > 0) {
 
 
 		if ($adht->subscription) {
 		if ($adht->subscription) {
 			// Amount
 			// Amount
-			print '<tr><td class="fieldrequired">'.$langs->trans("Amount").'</td><td><input type="text" name="subscription" size="6" value="'. price(GETPOSTISSET('subscription') ? GETPOST('subscription') : $adht->amount).'"> '.$langs->trans("Currency".$conf->currency) .'</td></tr>';
+			print '<tr><td class="fieldrequired">'.$langs->trans("Amount").'</td><td><input type="text" name="subscription" size="6" value="'.(GETPOSTISSET('subscription') ? GETPOST('subscription') : price($adht->amount, 0, '', 0)).'"> '.$langs->trans("Currency".$conf->currency) .'</td></tr>';
 
 
 			// Label
 			// Label
 			print '<tr><td>'.$langs->trans("Label").'</td>';
 			print '<tr><td>'.$langs->trans("Label").'</td>';

+ 1 - 1
htdocs/adherents/subscription/list.php

@@ -715,7 +715,7 @@ while ($i < $imaxinloop) {
 
 
 		// Label
 		// Label
 		if (!empty($arrayfields['t.libelle']['checked'])) {
 		if (!empty($arrayfields['t.libelle']['checked'])) {
-			print '<td class="tdoverflowmax400" title="'.dol_escape_htmltag($obj->note).'">';
+			print '<td class="tdoverflowmax400" title="'.dol_escape_htmltag($obj->note_private).'">';
 			print dol_escape_htmltag(dolGetFirstLineOfText($obj->note_private));
 			print dol_escape_htmltag(dolGetFirstLineOfText($obj->note_private));
 			print '</td>';
 			print '</td>';
 			if (!$i) {
 			if (!$i) {

+ 4 - 4
htdocs/admin/dav.php

@@ -104,7 +104,7 @@ if ($action == 'edit') {
 		if ($key == 'DAV_ALLOW_PRIVATE_DIR') {
 		if ($key == 'DAV_ALLOW_PRIVATE_DIR') {
 			print $langs->trans("AlwaysActive");
 			print $langs->trans("AlwaysActive");
 		} elseif ($key == 'DAV_ALLOW_PUBLIC_DIR' || $key == 'DAV_ALLOW_ECM_DIR') {
 		} elseif ($key == 'DAV_ALLOW_PUBLIC_DIR' || $key == 'DAV_ALLOW_ECM_DIR') {
-			print $form->selectyesno($key, $conf->global->$key, 1);
+			print $form->selectyesno($key, getDolGlobalString($key), 1);
 		} else {
 		} else {
 			print '<input name="'.$key.'"  class="flat '.(empty($val['css']) ? 'minwidth200' : $val['css']).'" value="'.getDolGlobalString($key).'">';
 			print '<input name="'.$key.'"  class="flat '.(empty($val['css']) ? 'minwidth200' : $val['css']).'" value="'.getDolGlobalString($key).'">';
 		}
 		}
@@ -140,9 +140,9 @@ if ($action == 'edit') {
 		if ($key == 'DAV_ALLOW_PRIVATE_DIR') {
 		if ($key == 'DAV_ALLOW_PRIVATE_DIR') {
 			print $langs->trans("AlwaysActive");
 			print $langs->trans("AlwaysActive");
 		} elseif ($key == 'DAV_ALLOW_PUBLIC_DIR' || $key == 'DAV_ALLOW_ECM_DIR') {
 		} elseif ($key == 'DAV_ALLOW_PUBLIC_DIR' || $key == 'DAV_ALLOW_ECM_DIR') {
-			print yn($conf->global->$key);
+			print yn(getDolGlobalString($key));
 		} else {
 		} else {
-			print $conf->global->$key;
+			print getDolGlobalString($key);
 		}
 		}
 		print '</td></tr>';
 		print '</td></tr>';
 	}
 	}
@@ -189,7 +189,7 @@ $message .= '</div>';
 $message .= ajax_autoselect('webdavpublicurl');
 $message .= ajax_autoselect('webdavpublicurl');
 
 
 $message .= '<br>';
 $message .= '<br>';
-if (!empty($conf->global->DAV_ALLOW_PUBLIC_DIR)) {
+if (!empty(getDolGlobalString('DAV_ALLOW_PUBLIC_DIR'))) {
 	$urlEntity = (isModEnabled('multicompany') ? '?entity=' . $conf->entity : '');
 	$urlEntity = (isModEnabled('multicompany') ? '?entity=' . $conf->entity : '');
 	$url = '<a href="' . $urlwithroot . '/dav/fileserver.php/public/' . $urlEntity . '" target="_blank" rel="noopener noreferrer">' . $urlwithroot . '/dav/fileserver.php/public/' . $urlEntity . '</a>';
 	$url = '<a href="' . $urlwithroot . '/dav/fileserver.php/public/' . $urlEntity . '" target="_blank" rel="noopener noreferrer">' . $urlwithroot . '/dav/fileserver.php/public/' . $urlEntity . '</a>';
 
 

+ 15 - 8
htdocs/admin/emailcollector_card.php

@@ -395,9 +395,7 @@ if ($object->id > 0 && (empty($action) || ($action != 'edit' && $action != 'crea
 	$connectstringtarget = '';
 	$connectstringtarget = '';
 
 
 	// Note: $object->host has been loaded by the fetch
 	// Note: $object->host has been loaded by the fetch
-	$usessl = 1;
-
-	$connectstringserver = $object->getConnectStringIMAP($usessl);
+	$connectstringserver = $object->getConnectStringIMAP();
 
 
 	if ($action == 'scan') {
 	if ($action == 'scan') {
 		if (!empty($conf->global->MAIN_IMAP_USE_PHPIMAP)) {
 		if (!empty($conf->global->MAIN_IMAP_USE_PHPIMAP)) {
@@ -645,9 +643,13 @@ if ($object->id > 0 && (empty($action) || ($action != 'edit' && $action != 'crea
         console.log("We change a filter");
         console.log("We change a filter");
         if (jQuery("#filtertype option:selected").attr("data-noparam")) {
         if (jQuery("#filtertype option:selected").attr("data-noparam")) {
             jQuery("#rulevalue").attr("placeholder", "");
             jQuery("#rulevalue").attr("placeholder", "");
-            jQuery("#rulevalue").text(""); jQuery("#rulevalue").prop("disabled", true);
-        }
-        else { jQuery("#rulevalue").prop("disabled", false); }
+            jQuery("#rulevalue").text("");
+			jQuery("#rulevalue").prop("disabled", true);
+			jQuery("#rulevaluehelp").addClass("unvisible");
+        } else {
+			jQuery("#rulevalue").prop("disabled", false);
+			jQuery("#rulevaluehelp").removeClass("unvisible");
+		}
         jQuery("#rulevalue").attr("placeholder", (jQuery("#filtertype option:selected").attr("data-placeholder")));
         jQuery("#rulevalue").attr("placeholder", (jQuery("#filtertype option:selected").attr("data-placeholder")));
     ';
     ';
 	/*$noparam = array();
 	/*$noparam = array();
@@ -658,8 +660,13 @@ if ($object->id > 0 && (empty($action) || ($action != 'edit' && $action != 'crea
 	print '})';
 	print '})';
 	print '</script>'."\n";
 	print '</script>'."\n";
 
 
-	print '</td><td>';
-	print '<input type="text" name="rulevalue" id="rulevalue">';
+	print '</td><td class="nowraponall">';
+	print '<div class="nowraponall">';
+	print '<input type="text" name="rulevalue" id="rulevalue" class="inline-block valignmiddle">';
+	print '<div class="inline-block valignmiddle unvisible" id="rulevaluehelp">';
+	print img_warning($langs->trans("FilterSearchImapHelp"), '', 'pictowarning classfortooltip');
+	print '</div>';
+	print '</div>';
 	print '</td>';
 	print '</td>';
 	print '<td class="right"><input type="submit" name="addfilter" id="addfilter" class="flat button smallpaddingimp" value="'.$langs->trans("Add").'"></td>';
 	print '<td class="right"><input type="submit" name="addfilter" id="addfilter" class="flat button smallpaddingimp" value="'.$langs->trans("Add").'"></td>';
 	print '</tr>';
 	print '</tr>';

+ 11 - 8
htdocs/admin/pdf_other.php

@@ -50,6 +50,9 @@ $action = GETPOST('action', 'aZ09');
  */
  */
 
 
 if ($action == 'update') {
 if ($action == 'update') {
+	if (GETPOSTISSET('MAIN_PDF_PROPAL_USE_ELECTRONIC_SIGNING')) {
+		dolibarr_set_const($db, "MAIN_PDF_PROPAL_USE_ELECTRONIC_SIGNING", GETPOST("MAIN_PDF_PROPAL_USE_ELECTRONIC_SIGNING"), 'chaine', 0, '', $conf->entity);
+	}
 	if (GETPOSTISSET('PROPOSAL_PDF_HIDE_PAYMENTTERM')) {
 	if (GETPOSTISSET('PROPOSAL_PDF_HIDE_PAYMENTTERM')) {
 		dolibarr_set_const($db, "PROPOSAL_PDF_HIDE_PAYMENTTERM", GETPOST("PROPOSAL_PDF_HIDE_PAYMENTTERM"), 'chaine', 0, '', $conf->entity);
 		dolibarr_set_const($db, "PROPOSAL_PDF_HIDE_PAYMENTTERM", GETPOST("PROPOSAL_PDF_HIDE_PAYMENTTERM"), 'chaine', 0, '', $conf->entity);
 	}
 	}
@@ -67,7 +70,7 @@ if ($action == 'update') {
 		dolibarr_del_const($db, "INVOICE_ADD_SWISS_QR_CODE", $conf->entity);
 		dolibarr_del_const($db, "INVOICE_ADD_SWISS_QR_CODE", $conf->entity);
 	}
 	}
 	if (GETPOSTISSET('INVOICE_ADD_SWISS_QR_CODE')) {
 	if (GETPOSTISSET('INVOICE_ADD_SWISS_QR_CODE')) {
-		dolibarr_set_const($db, "INVOICE_ADD_SWISS_QR_CODE", GETPOST("INVOICE_ADD_SWISS_QR_CODE", 'int'), 'chaine', 0, '', $conf->entity);
+		dolibarr_set_const($db, "INVOICE_ADD_SWISS_QR_CODE", GETPOST("INVOICE_ADD_SWISS_QR_CODE", 'alpha'), 'chaine', 0, '', $conf->entity);
 		dolibarr_del_const($db, "INVOICE_ADD_ZATCA_QR_CODE", $conf->entity);
 		dolibarr_del_const($db, "INVOICE_ADD_ZATCA_QR_CODE", $conf->entity);
 	}
 	}
 	if (GETPOSTISSET('INVOICE_CATEGORY_OF_OPERATION')) {
 	if (GETPOSTISSET('INVOICE_CATEGORY_OF_OPERATION')) {
@@ -165,14 +168,14 @@ if (isModEnabled('facture')) {
 	print '</td></tr>';
 	print '</td></tr>';
 
 
 	print '<tr class="oddeven"><td>';
 	print '<tr class="oddeven"><td>';
-	print $form->textwithpicto($langs->trans("INVOICE_ADD_SWISS_QR_CODE"), '');
+	print $form->textwithpicto($langs->trans("INVOICE_ADD_SWISS_QR_CODE"), $langs->trans("INVOICE_ADD_SWISS_QR_CODEMore"));
 	print '</td><td>';
 	print '</td><td>';
-	if ($conf->use_javascript_ajax) {
-		print ajax_constantonoff('INVOICE_ADD_SWISS_QR_CODE');
-	} else {
-		$arrval = array('0' => $langs->trans("No"), '1' => $langs->trans("Yes"));
-		print $form->selectarray("INVOICE_ADD_SWISS_QR_CODE", $arrval, $conf->global->INVOICE_ADD_SWISS_QR_CODE);
-	}
+	//if ($conf->use_javascript_ajax) {
+	//	print ajax_constantonoff('INVOICE_ADD_SWISS_QR_CODE');
+	//} else {
+	$arrval = array('0' => $langs->trans("No"), '1' => $langs->trans("Yes"), 'bottom' => $langs->trans("AtBottomOfPage").' ('.$langs->trans("Experimental").' - Need PHP 8.1+)');
+	print $form->selectarray("INVOICE_ADD_SWISS_QR_CODE", $arrval, $conf->global->INVOICE_ADD_SWISS_QR_CODE);
+	//}
 	print '</td></tr>';
 	print '</td></tr>';
 
 
 	// Mention category of operations
 	// Mention category of operations

+ 4 - 4
htdocs/api/class/api_setup.class.php

@@ -310,8 +310,8 @@ class Setup extends DolibarrApi
 	/**
 	/**
 	 * Get region by ID.
 	 * Get region by ID.
 	 *
 	 *
-	 * @param int       $id        ID of region
-	 * @return array    		   Array of cleaned object properties
+	 * @param 	int       $id       ID of region
+	 * @return 	Object 				Object with cleaned properties
 	 *
 	 *
 	 * @url     GET dictionary/regions/{id}
 	 * @url     GET dictionary/regions/{id}
 	 *
 	 *
@@ -325,8 +325,8 @@ class Setup extends DolibarrApi
 	/**
 	/**
 	 * Get region by Code.
 	 * Get region by Code.
 	 *
 	 *
-	 * @param string    $code      Code of region
-	 * @return array 			   Array of cleaned object properties
+	 * @param 	string    $code     Code of region
+	 * @return 	Object 				Object with cleaned properties
 	 *
 	 *
 	 * @url     GET dictionary/regions/byCode/{code}
 	 * @url     GET dictionary/regions/byCode/{code}
 	 *
 	 *

+ 3 - 3
htdocs/categories/index.php

@@ -249,13 +249,13 @@ if ($morethan1level && !empty($conf->use_javascript_ajax)) {
 print '</td></tr>';
 print '</td></tr>';
 
 
 if ($nbofentries > 0) {
 if ($nbofentries > 0) {
-	print '<tr class="pair"><td colspan="3">';
+	print '<tr class="oddeven"><td colspan="3">';
 	tree_recur($data, $data[0], 0);
 	tree_recur($data, $data[0], 0);
 	print '</td></tr>';
 	print '</td></tr>';
 } else {
 } else {
-	print '<tr class="pair">';
+	print '<tr class="oddeven">';
 	print '<td colspan="3"><table class="nobordernopadding"><tr class="nobordernopadding"><td>'.img_picto_common('', 'treemenu/branchbottom.gif').'</td>';
 	print '<td colspan="3"><table class="nobordernopadding"><tr class="nobordernopadding"><td>'.img_picto_common('', 'treemenu/branchbottom.gif').'</td>';
-	print '<td valign="middle">';
+	print '<td class="valignmiddle">';
 	print $langs->trans("NoCategoryYet");
 	print $langs->trans("NoCategoryYet");
 	print '</td>';
 	print '</td>';
 	print '<td>&nbsp;</td>';
 	print '<td>&nbsp;</td>';

+ 8 - 4
htdocs/categories/viewcat.php

@@ -1,4 +1,6 @@
 <?php
 <?php
+use Stripe\BankAccount;
+
 /* Copyright (C) 2005       Matthieu Valleton	<mv@seeschloss.org>
 /* Copyright (C) 2005       Matthieu Valleton	<mv@seeschloss.org>
  * Copyright (C) 2006-2020  Laurent Destailleur  <eldy@users.sourceforge.net>
  * Copyright (C) 2006-2020  Laurent Destailleur  <eldy@users.sourceforge.net>
  * Copyright (C) 2007       Patrick Raguin		<patrick.raguin@gmail.com>
  * Copyright (C) 2007       Patrick Raguin		<patrick.raguin@gmail.com>
@@ -215,12 +217,14 @@ if ($elemid && $action == 'addintocategory' &&
 		$elementtype = 'user';
 		$elementtype = 'user';
 	} elseif ($type == Categorie::TYPE_ACCOUNT) {
 	} elseif ($type == Categorie::TYPE_ACCOUNT) {
 		require_once DOL_DOCUMENT_ROOT.'/compta/bank/class/account.class.php';
 		require_once DOL_DOCUMENT_ROOT.'/compta/bank/class/account.class.php';
-		$newobject = new User($db);
+		$newobject = new Account($db);
 		$elementtype = 'bank_account';
 		$elementtype = 'bank_account';
+	} else {
+		dol_print_error("Not supported value of type = ".$type);
 	}
 	}
 	$result = $newobject->fetch($elemid);
 	$result = $newobject->fetch($elemid);
 
 
-	// TODO Add into categ
+	// Add into categ
 	$result = $object->add_type($newobject, $elementtype);
 	$result = $object->add_type($newobject, $elementtype);
 	if ($result >= 0) {
 	if ($result >= 0) {
 		setEventMessages($langs->trans("WasAddedSuccessfully", $newobject->ref), null, 'mesgs');
 		setEventMessages($langs->trans("WasAddedSuccessfully", $newobject->ref), null, 'mesgs');
@@ -925,8 +929,8 @@ if ($type == Categorie::TYPE_ACCOUNT) {
 				print '<input type="hidden" name="action" value="addintocategory">';
 				print '<input type="hidden" name="action" value="addintocategory">';
 				print '<table class="noborder centpercent">';
 				print '<table class="noborder centpercent">';
 				print '<tr class="liste_titre"><td>';
 				print '<tr class="liste_titre"><td>';
-					print $langs->trans("AddAccountIntoCategory").' &nbsp;';
-				$form->select_comptes('', 'elemid');
+				print $langs->trans("AddObjectIntoCategory").' &nbsp;';
+				print $form->select_comptes('', 'elemid', 0, '', 0, '', 0, '', 1);
 				print '<input type="submit" class="button buttongen" value="'.$langs->trans("ClassifyInCategory").'"></td>';
 				print '<input type="submit" class="button buttongen" value="'.$langs->trans("ClassifyInCategory").'"></td>';
 				print '</tr>';
 				print '</tr>';
 				print '</table>';
 				print '</table>';

+ 8 - 2
htdocs/compta/bank/card.php

@@ -1096,11 +1096,17 @@ if ($action == 'create') {
 			}
 			}
 
 
 			// IBAN
 			// IBAN
-			print '<tr><td>'.$langs->trans($ibankey).'</td>';
+			print '<tr><td>';
+			$tooltip = $langs->trans("Example").':<br>LT12 1000 0111 0100 1000<br>FR14 2004 1010 0505 0001 3M02 606<br>LU28 0019 4006 4475 0000<br>DE89 3704 0044 0532 0130 00';
+			print $form->textwithpicto($langs->trans($ibankey), $tooltip);
+			print '</td>';
 			print '<td><input class="minwidth300 maxwidth200onsmartphone" maxlength="34" type="text" class="flat" name="iban" value="'.(GETPOSTISSET('iban') ? GETPOST('iban',  'alphanohtml') : $object->iban).'"></td></tr>';
 			print '<td><input class="minwidth300 maxwidth200onsmartphone" maxlength="34" type="text" class="flat" name="iban" value="'.(GETPOSTISSET('iban') ? GETPOST('iban',  'alphanohtml') : $object->iban).'"></td></tr>';
 
 
 			// BIC
 			// BIC
-			print '<tr><td>'.$langs->trans($bickey).'</td>';
+			print '<tr><td>';
+			$tooltip = $langs->trans("Example").': LIABLT2XXXX';
+			print $form->textwithpicto($langs->trans($bickey), $tooltip);
+			print '</td>';
 			print '<td><input class="minwidth150 maxwidth200onsmartphone" maxlength="11" type="text" class="flat" name="bic" value="'.(GETPOSTISSET('bic') ? GETPOST('bic',  'alphanohtml') : $object->bic).'"></td></tr>';
 			print '<td><input class="minwidth150 maxwidth200onsmartphone" maxlength="11" type="text" class="flat" name="bic" value="'.(GETPOSTISSET('bic') ? GETPOST('bic',  'alphanohtml') : $object->bic).'"></td></tr>';
 
 
 			// Show fields of bank account
 			// Show fields of bank account

+ 6 - 2
htdocs/compta/bank/class/account.class.php

@@ -1338,7 +1338,7 @@ class Account extends CommonObject
 	 *      Charge indicateurs this->nb de tableau de bord
 	 *      Charge indicateurs this->nb de tableau de bord
 	 *
 	 *
 	 *		@param		int			$filteraccountid	To get info for a particular account id
 	 *		@param		int			$filteraccountid	To get info for a particular account id
-	 *      @return     int         <0 if ko, >0 if ok
+	 *      @return     int         <0 if KO, >0 if OK
 	 */
 	 */
 	public function load_state_board($filteraccountid = 0)
 	public function load_state_board($filteraccountid = 0)
 	{
 	{
@@ -1366,6 +1366,7 @@ class Account extends CommonObject
 				$this->nb["banklines"] = $obj->nb;
 				$this->nb["banklines"] = $obj->nb;
 			}
 			}
 			$this->db->free($resql);
 			$this->db->free($resql);
+			return 1;
 		} else {
 		} else {
 			dol_print_error($this->db);
 			dol_print_error($this->db);
 			$this->error = $this->db->error();
 			$this->error = $this->db->error();
@@ -2611,7 +2612,8 @@ class AccountLine extends CommonObjectLine
 	public function LibStatut($status, $mode = 0)
 	public function LibStatut($status, $mode = 0)
 	{
 	{
 		// phpcs:enable
 		// phpcs:enable
-		global $langs;
+		//global $langs;
+
 		//$langs->load('companies');
 		//$langs->load('companies');
 		/*
 		/*
 		if ($mode == 0)
 		if ($mode == 0)
@@ -2644,6 +2646,8 @@ class AccountLine extends CommonObjectLine
 			if ($status==0) return $langs->trans("ActivityCeased").' '.img_picto($langs->trans("ActivityCeased"),'statut5', 'class="pictostatus"');
 			if ($status==0) return $langs->trans("ActivityCeased").' '.img_picto($langs->trans("ActivityCeased"),'statut5', 'class="pictostatus"');
 			if ($status==1) return $langs->trans("InActivity").' '.img_picto($langs->trans("InActivity"),'statut4', 'class="pictostatus"');
 			if ($status==1) return $langs->trans("InActivity").' '.img_picto($langs->trans("InActivity"),'statut4', 'class="pictostatus"');
 		}*/
 		}*/
+
+		return '';
 	}
 	}
 
 
 
 

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

@@ -5212,7 +5212,7 @@ if ($action == 'create') {
 				print '<td class="right"><span class="amount">'.price($obj->amount_ttc).'</span></td>';
 				print '<td class="right"><span class="amount">'.price($obj->amount_ttc).'</span></td>';
 				print '<td class="right">';
 				print '<td class="right">';
 				print '<a href="'.$_SERVER["PHP_SELF"].'?facid='.$object->id.'&action=unlinkdiscount&token='.newToken().'&discountid='.$obj->rowid.'">';
 				print '<a href="'.$_SERVER["PHP_SELF"].'?facid='.$object->id.'&action=unlinkdiscount&token='.newToken().'&discountid='.$obj->rowid.'">';
-				print img_picto($langs->trans("RemoveDiscount"), 'unlink');
+				print img_picto($langs->transnoentitiesnoconv("RemoveDiscount"), 'unlink');
 				print '</a>';
 				print '</a>';
 				print '</td></tr>';
 				print '</td></tr>';
 				$i++;
 				$i++;

+ 3 - 3
htdocs/compta/facture/class/facture.class.php

@@ -5629,11 +5629,11 @@ class Facture extends CommonInvoice
 	 *  @param	int			$nbdays				Delay before due date (or after if delay is negative)
 	 *  @param	int			$nbdays				Delay before due date (or after if delay is negative)
 	 *  @param	string		$paymentmode		'' or 'all' by default (no filter), or 'LIQ', 'CHQ', CB', ...
 	 *  @param	string		$paymentmode		'' or 'all' by default (no filter), or 'LIQ', 'CHQ', CB', ...
 	 *  @param	int|string	$template			Name (or id) of email template (Must be a template of type 'facture_send')
 	 *  @param	int|string	$template			Name (or id) of email template (Must be a template of type 'facture_send')
-	 *  @param	string		$forcerecipient		Force email of recipient (for example to send the email to an accountant supervisor instead of the customer)
 	 *  @param	string		$datetouse			'duedate' (default) or 'invoicedate'
 	 *  @param	string		$datetouse			'duedate' (default) or 'invoicedate'
+	 *  @param	string		$forcerecipient		Force email of recipient (for example to send the email to an accountant supervisor instead of the customer)
 	 *  @return int         					0 if OK, <>0 if KO (this function is used also by cron so only 0 is OK)
 	 *  @return int         					0 if OK, <>0 if KO (this function is used also by cron so only 0 is OK)
 	 */
 	 */
-	public function sendEmailsRemindersOnInvoiceDueDate($nbdays = 0, $paymentmode = 'all', $template = '', $forcerecipient = '', $datetouse = 'duedate')
+	public function sendEmailsRemindersOnInvoiceDueDate($nbdays = 0, $paymentmode = 'all', $template = '', $datetouse = 'duedate', $forcerecipient = '')
 	{
 	{
 		global $conf, $langs, $user;
 		global $conf, $langs, $user;
 
 
@@ -5829,7 +5829,7 @@ class Facture extends CommonInvoice
 								$actioncomm->contact_id = 0;
 								$actioncomm->contact_id = 0;
 
 
 								$actioncomm->code = 'AC_EMAIL';
 								$actioncomm->code = 'AC_EMAIL';
-								$actioncomm->label = 'sendEmailsRemindersOnInvoiceDueDateOK (nbdays='.$nbdays.' paymentmode='.$paymentmode.' template='.$template.' forcerecipient='.$forcerecipient.' datetouse='.$datetouse.')';
+								$actioncomm->label = 'sendEmailsRemindersOnInvoiceDueDateOK (nbdays='.$nbdays.' paymentmode='.$paymentmode.' template='.$template.' datetouse='.$datetouse.' forcerecipient='.$forcerecipient.')';
 								$actioncomm->note_private = $sendContent;
 								$actioncomm->note_private = $sendContent;
 								$actioncomm->fk_project = $tmpinvoice->fk_project;
 								$actioncomm->fk_project = $tmpinvoice->fk_project;
 								$actioncomm->datep = dol_now();
 								$actioncomm->datep = dol_now();

+ 294 - 166
htdocs/compta/prelevement/list.php

@@ -22,7 +22,7 @@
 /**
 /**
  *      \file       htdocs/compta/prelevement/list.php
  *      \file       htdocs/compta/prelevement/list.php
  *      \ingroup    prelevement
  *      \ingroup    prelevement
- *      \brief      Page list of direct debit orders or credit transfers orders
+ *      \brief      Page to list direct debit orders or credit transfers orders
  */
  */
 
 
 // Load Dolibarr environment
 // Load Dolibarr environment
@@ -34,7 +34,7 @@ require_once DOL_DOCUMENT_ROOT.'/compta/bank/class/account.class.php';
 // Load translation files required by the page
 // Load translation files required by the page
 $langs->loadLangs(array('banks', 'withdrawals', 'companies', 'categories'));
 $langs->loadLangs(array('banks', 'withdrawals', 'companies', 'categories'));
 
 
-$action     = GETPOST('action', 'aZ09') ?GETPOST('action', 'aZ09') : 'view'; // The action 'add', 'create', 'edit', 'update', 'view', ...
+$action     = GETPOST('action', 'aZ09') ? GETPOST('action', 'aZ09') : 'view'; // The action 'add', 'create', 'edit', 'update', 'view', ...
 $massaction = GETPOST('massaction', 'alpha'); // The bulk action (combo box choice into lists)
 $massaction = GETPOST('massaction', 'alpha'); // The bulk action (combo box choice into lists)
 $show_files = GETPOST('show_files', 'int'); // Show files area generated by bulk actions ?
 $show_files = GETPOST('show_files', 'int'); // Show files area generated by bulk actions ?
 $confirm    = GETPOST('confirm', 'alpha'); // Result of a confirmation
 $confirm    = GETPOST('confirm', 'alpha'); // Result of a confirmation
@@ -43,17 +43,19 @@ $toselect   = GETPOST('toselect', 'array'); // Array of ids of elements selected
 $contextpage = GETPOST('contextpage', 'aZ') ? GETPOST('contextpage', 'aZ') : 'directdebitcredittransferlinelist'; // To manage different context of search
 $contextpage = GETPOST('contextpage', 'aZ') ? GETPOST('contextpage', 'aZ') : 'directdebitcredittransferlinelist'; // To manage different context of search
 $backtopage = GETPOST('backtopage', 'alpha'); // Go back to a dedicated page
 $backtopage = GETPOST('backtopage', 'alpha'); // Go back to a dedicated page
 $optioncss  = GETPOST('optioncss', 'aZ'); // Option for the css output (always '' except when 'print')
 $optioncss  = GETPOST('optioncss', 'aZ'); // Option for the css output (always '' except when 'print')
-$mode       = GETPOST('mode', 'alpha');
+$mode       = GETPOST('mode', 'aZ'); // The output mode ('list', 'kanban', 'hierarchy', 'calendar', ...)
 
 
 $type = GETPOST('type', 'aZ09');
 $type = GETPOST('type', 'aZ09');
 
 
+// Load variable for pagination
 $limit = GETPOST('limit', 'int') ?GETPOST('limit', 'int') : $conf->liste_limit;
 $limit = GETPOST('limit', 'int') ?GETPOST('limit', 'int') : $conf->liste_limit;
 $sortfield = GETPOST('sortfield', 'aZ09comma');
 $sortfield = GETPOST('sortfield', 'aZ09comma');
 $sortorder = GETPOST('sortorder', 'aZ09comma');
 $sortorder = GETPOST('sortorder', 'aZ09comma');
 $page = GETPOSTISSET('pageplusone') ? (GETPOST('pageplusone') - 1) : GETPOST("page", 'int');
 $page = GETPOSTISSET('pageplusone') ? (GETPOST('pageplusone') - 1) : GETPOST("page", 'int');
-if (empty($page) || $page == -1) {
+if (empty($page) || $page < 0 || GETPOST('button_search', 'alpha') || GETPOST('button_removefilter', 'alpha')) {
+	// If $page is not defined, or '' or -1 or if we click on clear filters
 	$page = 0;
 	$page = 0;
-}     // If $page is not defined, or '' or -1
+}
 $offset = $limit * $page;
 $offset = $limit * $page;
 $pageprev = $page - 1;
 $pageprev = $page - 1;
 $pagenext = $page + 1;
 $pagenext = $page + 1;
@@ -107,7 +109,11 @@ if (GETPOST('button_removefilter_x', 'alpha') || GETPOST('button_removefilter.x'
 
 
 $form = new Form($db);
 $form = new Form($db);
 
 
-llxHeader('', $langs->trans("WithdrawalsLines"));
+$title = $langs->trans("WithdrawalsLines");
+if ($type == 'bank-transfer') {
+	$title = $langs->trans("CreditTransferLines");
+}
+$help_url = '';
 
 
 $sql = "SELECT p.rowid, p.ref, p.statut as status, p.datec";
 $sql = "SELECT p.rowid, p.ref, p.statut as status, p.datec";
 $sql .= " , f.rowid as facid, f.ref as invoiceref, f.total_ttc";
 $sql .= " , f.rowid as facid, f.ref as invoiceref, f.total_ttc";
@@ -170,193 +176,315 @@ if (!getDolGlobalInt('MAIN_DISABLE_FULL_SCANLIST')) {
 		dol_print_error($db);
 		dol_print_error($db);
 	}
 	}
 
 
-	if (($page * $limit) > $nbtotalofrecords) {	// if total resultset is smaller then paging size (filtering), goto and load page 0
+	if (($page * $limit) > $nbtotalofrecords) {	// if total resultset is smaller than the paging size (filtering), goto and load page 0
 		$page = 0;
 		$page = 0;
 		$offset = 0;
 		$offset = 0;
 	}
 	}
 	$db->free($resql);
 	$db->free($resql);
 }
 }
 
 
+// Complete request and execute it with limit
 $sql .= $db->order($sortfield, $sortorder);
 $sql .= $db->order($sortfield, $sortorder);
 if ($limit) {
 if ($limit) {
 	$sql .= $db->plimit($limit + 1, $offset);
 	$sql .= $db->plimit($limit + 1, $offset);
 }
 }
 
 
-$result = $db->query($sql);
-if ($result) {
-	$num = $db->num_rows($result);
-	$i = 0;
+$resql = $db->query($sql);
+if (!$resql) {
+	dol_print_error($db);
+	exit;
+}
 
 
-	$param = "&amp;statut=".urlencode($statut);
-	$param .= "&amp;search_bon=".urlencode($search_bon);
-	if (!empty($mode)) {
-		$param .= '&mode='.urlencode($mode);
-	}
-	if ($type == 'bank-transfer') {
-		$param .= '&amp;type=bank-transfer';
-	}
-	if ($limit > 0 && $limit != $conf->liste_limit) {
-		$param .= '&limit='.((int) $limit);
-	}
-	$newcardbutton = '';
-	$newcardbutton .= dolGetButtonTitle($langs->trans('ViewList'), '', 'fa fa-bars imgforviewmode', $_SERVER["PHP_SELF"].'?mode=common'.preg_replace('/(&|\?)*mode=[^&]+/', '', $param), '', ((empty($mode) || $mode == 'common') ? 2 : 1), array('morecss'=>'reposition'));
-	$newcardbutton .= dolGetButtonTitle($langs->trans('ViewKanban'), '', 'fa fa-th-list imgforviewmode', $_SERVER["PHP_SELF"].'?mode=kanban'.preg_replace('/(&|\?)*mode=[^&]+/', '', $param), '', ($mode == 'kanban' ? 2 : 1), array('morecss'=>'reposition'));
-
-	print"\n<!-- debut table -->\n";
-	print '<form method="POST" id="searchFormList" action="'.$_SERVER["PHP_SELF"].'">'."\n";
-	if ($optioncss != '') {
-		print '<input type="hidden" name="optioncss" value="'.$optioncss.'">';
-	}
-	print '<input type="hidden" name="token" value="'.newToken().'">';
-	print '<input type="hidden" name="formfilteraction" id="formfilteraction" value="list">';
-	print '<input type="hidden" name="action" value="list">';
-	print '<input type="hidden" name="sortfield" value="'.$sortfield.'">';
-	print '<input type="hidden" name="sortorder" value="'.$sortorder.'">';
-	print '<input type="hidden" name="contextpage" value="'.$contextpage.'">';
-	print '<input type="hidden" name="mode" value="'.$mode.'">';
-
-	if ($type != '') {
-		print '<input type="hidden" name="type" value="'.$type.'">';
-	}
+$num = $db->num_rows($resql);
 
 
-	$title = $langs->trans("WithdrawalsLines");
-	if ($type == 'bank-transfer') {
-		$title = $langs->trans("CreditTransferLines");
-	}
-	print_barre_liste($title, $page, $_SERVER["PHP_SELF"], $param, $sortfield, $sortorder, '', $num, $nbtotalofrecords, 'generic', 0, $newcardbutton, '', $limit, 0, 0, 1);
-
-	$moreforfilter = '';
-
-	print '<div class="div-table-responsive">';
-	print '<table class="tagtable liste'.($moreforfilter ? " listwithfilterbefore" : "").'">'."\n";
-
-	print '<tr class="liste_titre">';
-	print '<td class="liste_titre"><input type="text" class="flat" name="search_line" value="'.dol_escape_htmltag($search_line).'" size="6"></td>';
-	print '<td class="liste_titre"><input type="text" class="flat" name="search_bon" value="'.dol_escape_htmltag($search_bon).'" size="6"></td>';
-	print '<td class="liste_titre">&nbsp;</td>';
-	print '<td class="liste_titre"><input type="text" class="flat" name="search_company" value="'.dol_escape_htmltag($search_company).'" size="6"></td>';
-	print '<td class="liste_titre center"><input type="text" class="flat" name="search_code" value="'.dol_escape_htmltag($search_code).'" size="6"></td>';
-	print '<td class="liste_titre">&nbsp;</td>';
-	print '<td class="liste_titre">&nbsp;</td>';
-	print '<td class="liste_titre maxwidthsearch">';
+// Output page
+// --------------------------------------------------------------------
+
+llxHeader('', $title, $help_url);
+
+$arrayofselected = is_array($toselect) ? $toselect : array();
+
+$param = '';
+$param .= "&statut=".urlencode($statut);
+$param .= "&search_bon=".urlencode($search_bon);
+if ($type == 'bank-transfer') {
+	$param .= '&type=bank-transfer';
+}
+if (!empty($mode)) {
+	$param .= '&mode='.urlencode($mode);
+}
+if (!empty($contextpage) && $contextpage != $_SERVER["PHP_SELF"]) {
+	$param .= '&contextpage='.urlencode($contextpage);
+}
+if ($limit > 0 && $limit != $conf->liste_limit) {
+	$param .= '&limit='.((int) $limit);
+}
+if ($optioncss != '') {
+	$param .= '&optioncss='.urlencode($optioncss);
+}
+
+print '<form method="POST" id="searchFormList" action="'.$_SERVER["PHP_SELF"].'">'."\n";
+print '<input type="hidden" name="token" value="'.newToken().'">';
+if ($optioncss != '') {
+	print '<input type="hidden" name="optioncss" value="'.$optioncss.'">';
+}
+print '<input type="hidden" name="formfilteraction" id="formfilteraction" value="list">';
+print '<input type="hidden" name="action" value="list">';
+print '<input type="hidden" name="sortfield" value="'.$sortfield.'">';
+print '<input type="hidden" name="sortorder" value="'.$sortorder.'">';
+print '<input type="hidden" name="page" value="'.$page.'">';
+print '<input type="hidden" name="contextpage" value="'.$contextpage.'">';
+print '<input type="hidden" name="page_y" value="">';
+print '<input type="hidden" name="mode" value="'.$mode.'">';
+
+if ($type != '') {
+	print '<input type="hidden" name="type" value="'.$type.'">';
+}
+
+$newcardbutton = '';
+$newcardbutton .= dolGetButtonTitle($langs->trans('ViewList'), '', 'fa fa-bars imgforviewmode', $_SERVER["PHP_SELF"].'?mode=common'.preg_replace('/(&|\?)*mode=[^&]+/', '', $param), '', ((empty($mode) || $mode == 'common') ? 2 : 1), array('morecss'=>'reposition'));
+$newcardbutton .= dolGetButtonTitle($langs->trans('ViewKanban'), '', 'fa fa-th-list imgforviewmode', $_SERVER["PHP_SELF"].'?mode=kanban'.preg_replace('/(&|\?)*mode=[^&]+/', '', $param), '', ($mode == 'kanban' ? 2 : 1), array('morecss'=>'reposition'));
+
+print_barre_liste($title, $page, $_SERVER["PHP_SELF"], $param, $sortfield, $sortorder, $massactionbutton, $num, $nbtotalofrecords, 'generic', 0, $newcardbutton, '', $limit, 0, 0, 1);
+
+$moreforfilter = '';
+/*$moreforfilter.='<div class="divsearchfield">';
+ $moreforfilter.= $langs->trans('MyFilter') . ': <input type="text" name="search_myfield" value="'.dol_escape_htmltag($search_myfield).'">';
+ $moreforfilter.= '</div>';*/
+
+$parameters = array();
+$reshook = $hookmanager->executeHooks('printFieldPreListTitle', $parameters, $object, $action); // Note that $action and $object may have been modified by hook
+if (empty($reshook)) {
+	$moreforfilter .= $hookmanager->resPrint;
+} else {
+	$moreforfilter = $hookmanager->resPrint;
+}
+
+if (!empty($moreforfilter)) {
+	print '<div class="liste_titre liste_titre_bydiv centpercent">';
+	print $moreforfilter;
+	$parameters = array();
+	$reshook = $hookmanager->executeHooks('printFieldPreListTitle', $parameters, $object, $action); // Note that $action and $object may have been modified by hook
+	print $hookmanager->resPrint;
+	print '</div>';
+}
+
+$varpage = empty($contextpage) ? $_SERVER["PHP_SELF"] : $contextpage;
+$selectedfields = ($mode != 'kanban' ? $form->multiSelectArrayWithCheckbox('selectedfields', $arrayfields, $varpage, getDolGlobalString('MAIN_CHECKBOX_LEFT_COLUMN', '')) : ''); // This also change content of $arrayfields
+$selectedfields .= (count($arrayofmassactions) ? $form->showCheckAddButtons('checkforselect', 1) : '');
+
+print '<div class="div-table-responsive">'; // You can use div-table-responsive-no-min if you dont need reserved height for your table
+print '<table class="tagtable nobottomiftotal liste'.($moreforfilter ? " listwithfilterbefore" : "").'">'."\n";
+
+// Fields title search
+// --------------------------------------------------------------------
+print '<tr class="liste_titre_filter">';
+// Action column
+if (getDolGlobalString('MAIN_CHECKBOX_LEFT_COLUMN')) {
+	print '<td class="liste_titre center maxwidthsearch">';
+	$searchpicto = $form->showFilterButtons('left');
+	print $searchpicto;
+	print '</td>';
+}
+print '<td class="liste_titre"><input type="text" class="flat" name="search_line" value="'.dol_escape_htmltag($search_line).'" size="6"></td>';
+print '<td class="liste_titre"><input type="text" class="flat" name="search_bon" value="'.dol_escape_htmltag($search_bon).'" size="6"></td>';
+print '<td class="liste_titre">&nbsp;</td>';
+print '<td class="liste_titre"><input type="text" class="flat" name="search_company" value="'.dol_escape_htmltag($search_company).'" size="6"></td>';
+print '<td class="liste_titre center"><input type="text" class="flat" name="search_code" value="'.dol_escape_htmltag($search_code).'" size="6"></td>';
+print '<td class="liste_titre">&nbsp;</td>';
+print '<td class="liste_titre">&nbsp;</td>';
+// Action column
+if (!getDolGlobalString('MAIN_CHECKBOX_LEFT_COLUMN')) {
+	print '<td class="liste_titre center maxwidthsearch">';
 	$searchpicto = $form->showFilterButtons();
 	$searchpicto = $form->showFilterButtons();
 	print $searchpicto;
 	print $searchpicto;
 	print '</td>';
 	print '</td>';
-	print '</tr>';
-
-	$columntitle = "WithdrawalsReceipts";
-	$columntitlethirdparty = "CustomerCode";
-	$columncodethirdparty = "s.code_client";
-	if ($type == 'bank-transfer') {
-		$columntitle = "BankTransferReceipts";
-		$columntitlethirdparty = "SupplierCode";
-		$columncodethirdparty = "s.code_fournisseur";
-	}
+}
+print '</tr>'."\n";
 
 
-	print '<tr class="liste_titre">';
-	print_liste_field_titre($columntitle, $_SERVER["PHP_SELF"], "p.ref", '', $param, '', $sortfield, $sortorder);
-	print_liste_field_titre("Line", $_SERVER["PHP_SELF"], '', '', $param, '', $sortfield, $sortorder);
-	print_liste_field_titre("Bill", $_SERVER["PHP_SELF"], "f.ref", '', $param, '', $sortfield, $sortorder);
-	print_liste_field_titre("Company", $_SERVER["PHP_SELF"], "s.nom", '', $param, '', $sortfield, $sortorder);
-	print_liste_field_titre($columntitlethirdparty, $_SERVER["PHP_SELF"], $columncodethirdparty, '', $param, '', $sortfield, $sortorder, 'center ');
-	print_liste_field_titre("Date", $_SERVER["PHP_SELF"], "p.datec", "", $param, '', $sortfield, $sortorder, 'center ');
-	print_liste_field_titre("Amount", $_SERVER["PHP_SELF"], "pl.amount", "", $param, '', $sortfield, $sortorder, 'right ');
-	print_liste_field_titre('');
-	print "</tr>\n";
-
-	if ($num) {
-		$imaxinloop = ($limit ? min($num, $limit) : $num);
-		while ($i < $imaxinloop) {
-			$obj = $db->fetch_object($result);
-
-			$bon->id = $obj->rowid;
-			$bon->ref = $obj->ref;
-			$bon->statut = $obj->status;
-			$bon->date_echeance = $obj->datec;
-			$bon->total = $obj->amount;
-
-			$company->id = $obj->socid;
-			$company->name = $obj->name;
-			$company->email = $obj->email;
-			$company->code_client = $obj->code_client;
-
-			if ($mode == 'kanban') {
-				if ($i == 0) {
-					print '<tr><td colspan="12">';
-					print '<div class="box-flex-container kanban">';
-				}
-				// Output Kanban
-				print $bon->getKanbanView('', array('selected' => in_array($bon->id, $arrayofselected)));
-				if ($i == ($imaxinloop - 1)) {
-					print '</div>';
-					print '</td></tr>';
-				}
-			} else {
-				print '<tr class="oddeven">';
-
-				print '<td>';
-				print $bon->getNomUrl(1);
-				print "</td>\n";
-
-				print '<td>';
-				print $line->LibStatut($obj->statut_ligne, 2);
-				print "&nbsp;";
-				print '<a href="'.DOL_URL_ROOT.'/compta/prelevement/line.php?id='.$obj->rowid_ligne.'">';
-				print substr('000000'.$obj->rowid_ligne, -6);
-				print '</a></td>';
-
-				print '<td>';
-				$link_to_bill = '/compta/facture/card.php?facid=';
-				$link_title = 'Invoice';
-				$link_picto = 'bill';
-				if ($type == 'bank-transfer') {
-					$link_to_bill = '/fourn/facture/card.php?facid=';
-					$link_title = 'SupplierInvoice';
-					$link_picto = 'supplier_invoice';
-				}
-				print '<a href="'.DOL_URL_ROOT.$link_to_bill.$obj->facid.'">';
-				print img_object($langs->trans($link_title), $link_picto);
-				print '&nbsp;'.$obj->invoiceref."</td>\n";
-				print '</a>';
-				print '</td>';
-
-				print '<td>';
-				print $company->getNomUrl(1);
-				print "</td>\n";
-
-
-				print '<td class="center">';
-				$link_to_tab = '/comm/card.php?socid=';
-				$link_code = $obj->code_client;
-				if ($type == 'bank-transfer') {
-					$link_to_tab = '/fourn/card.php?socid=';
-					$link_code = $obj->code_fournisseur;
-				}
-				print '<a href="'.DOL_URL_ROOT.$link_to_tab.$company->id.'">'.$link_code."</a></td>\n";
+$totalarray = array();
+$totalarray['nbfield'] = 0;
 
 
-				print '<td class="center">'.dol_print_date($db->jdate($obj->datec), 'day')."</td>\n";
+$columntitle = "WithdrawalsReceipts";
+$columntitlethirdparty = "CustomerCode";
+$columncodethirdparty = "s.code_client";
+if ($type == 'bank-transfer') {
+	$columntitle = "BankTransferReceipts";
+	$columntitlethirdparty = "SupplierCode";
+	$columncodethirdparty = "s.code_fournisseur";
+}
 
 
-				print '<td class="right"><span class="amount">'.price($obj->amount)."</span></td>\n";
+// Fields title label
+// --------------------------------------------------------------------
+print '<tr class="liste_titre">';
+// Action column
+if (getDolGlobalString('MAIN_CHECKBOX_LEFT_COLUMN')) {
+	print getTitleFieldOfList($selectedfields, 0, $_SERVER["PHP_SELF"], '', '', '', '', $sortfield, $sortorder, 'center maxwidthsearch ')."\n";
+	$totalarray['nbfield']++;
+}
+print_liste_field_titre($columntitle, $_SERVER["PHP_SELF"], "p.ref", '', $param, '', $sortfield, $sortorder);
+$totalarray['nbfield']++;
+print_liste_field_titre("Line", $_SERVER["PHP_SELF"], '', '', $param, '', $sortfield, $sortorder);
+$totalarray['nbfield']++;
+print_liste_field_titre("Bill", $_SERVER["PHP_SELF"], "f.ref", '', $param, '', $sortfield, $sortorder);
+$totalarray['nbfield']++;
+print_liste_field_titre("Company", $_SERVER["PHP_SELF"], "s.nom", '', $param, '', $sortfield, $sortorder);
+$totalarray['nbfield']++;
+print_liste_field_titre($columntitlethirdparty, $_SERVER["PHP_SELF"], $columncodethirdparty, '', $param, '', $sortfield, $sortorder, 'center ');
+$totalarray['nbfield']++;
+print_liste_field_titre("Date", $_SERVER["PHP_SELF"], "p.datec", "", $param, '', $sortfield, $sortorder, 'center ');
+$totalarray['nbfield']++;
+print_liste_field_titre("Amount", $_SERVER["PHP_SELF"], "pl.amount", "", $param, '', $sortfield, $sortorder, 'right ');
+$totalarray['nbfield']++;
+// Action column
+if (!getDolGlobalString('MAIN_CHECKBOX_LEFT_COLUMN')) {
+	print getTitleFieldOfList($selectedfields, 0, $_SERVER["PHP_SELF"], '', '', '', '', $sortfield, $sortorder, 'center maxwidthsearch ')."\n";
+	$totalarray['nbfield']++;
+}
+print '</tr>'."\n";
+
+// Loop on record
+// --------------------------------------------------------------------
+$i = 0;
+$savnbfield = $totalarray['nbfield'];
+$totalarray = array();
+$totalarray['nbfield'] = 0;
+
+$imaxinloop = ($limit ? min($num, $limit) : $num);
+while ($i < $imaxinloop) {
+	$obj = $db->fetch_object($resql);
+
+	$bon->id = $obj->rowid;
+	$bon->ref = $obj->ref;
+	$bon->statut = $obj->status;
+	$bon->date_echeance = $obj->datec;
+	$bon->total = $obj->amount;
+
+	$object = $bon;
+
+	$company->id = $obj->socid;
+	$company->name = $obj->name;
+	$company->email = $obj->email;
+	$company->code_client = $obj->code_client;
+
+	if ($mode == 'kanban') {
+		if ($i == 0) {
+			print '<tr><td colspan="'.$savnbfield.'">';
+			print '<div class="box-flex-container kanban">';
+		}
+		// Output Kanban
+		if ($massactionbutton || $massaction) { // If we are in select mode (massactionbutton defined) or if we have already selected and sent an action ($massaction) defined
+			$selected = 0;
+			if (in_array($object->id, $arrayofselected)) {
+				$selected = 1;
+			}
+		}
+		print $object->getKanbanView('', array('selected' => in_array($bon->id, $arrayofselected)));
+		if ($i == ($imaxinloop - 1)) {
+			print '</div>';
+			print '</td></tr>';
+		}
+	} else {
+		// Show line of result
+		$j = 0;
+		print '<tr data-rowid="'.$object->id.'" class="oddeven">';
+
+		// Action column
+		if (getDolGlobalString('MAIN_CHECKBOX_LEFT_COLUMN')) {
+			print '<td class="nowrap center">';
+			if ($massactionbutton || $massaction) { // If we are in select mode (massactionbutton defined) or if we have already selected and sent an action ($massaction) defined
+				$selected = 0;
+				if (in_array($object->id, $arrayofselected)) {
+					$selected = 1;
+				}
+				print '<input id="cb'.$object->id.'" class="flat checkforselect" type="checkbox" name="toselect[]" value="'.$object->id.'"'.($selected ? ' checked="checked"' : '').'>';
+			}
+			print '</td>';
+			if (!$i) {
+				$totalarray['nbfield']++;
+			}
+		}
+		print '<td>';
+		print $bon->getNomUrl(1);
+		print "</td>\n";
+
+		print '<td>';
+		print $line->LibStatut($obj->statut_ligne, 2);
+		print "&nbsp;";
+		print '<a href="'.DOL_URL_ROOT.'/compta/prelevement/line.php?id='.$obj->rowid_ligne.'">';
+		print substr('000000'.$obj->rowid_ligne, -6);
+		print '</a></td>';
+
+		print '<td>';
+		$link_to_bill = '/compta/facture/card.php?facid=';
+		$link_title = 'Invoice';
+		$link_picto = 'bill';
+		if ($type == 'bank-transfer') {
+			$link_to_bill = '/fourn/facture/card.php?facid=';
+			$link_title = 'SupplierInvoice';
+			$link_picto = 'supplier_invoice';
+		}
+		print '<a href="'.DOL_URL_ROOT.$link_to_bill.$obj->facid.'">';
+		print img_object($langs->trans($link_title), $link_picto);
+		print '&nbsp;'.$obj->invoiceref."</td>\n";
+		print '</a>';
+		print '</td>';
+
+		print '<td>';
+		print $company->getNomUrl(1);
+		print "</td>\n";
+
+
+		print '<td class="center">';
+		$link_to_tab = '/comm/card.php?socid=';
+		$link_code = $obj->code_client;
+		if ($type == 'bank-transfer') {
+			$link_to_tab = '/fourn/card.php?socid=';
+			$link_code = $obj->code_fournisseur;
+		}
+		print '<a href="'.DOL_URL_ROOT.$link_to_tab.$company->id.'">'.$link_code."</a></td>\n";
 
 
-				print '<td>&nbsp;</td>';
+		print '<td class="center">'.dol_print_date($db->jdate($obj->datec), 'day')."</td>\n";
 
 
-				print "</tr>\n";
+		print '<td class="right"><span class="amount">'.price($obj->amount)."</span></td>\n";
+
+		// Action column
+		if (!getDolGlobalString('MAIN_CHECKBOX_LEFT_COLUMN')) {
+			print '<td class="nowrap center">';
+			if ($massactionbutton || $massaction) { // If we are in select mode (massactionbutton defined) or if we have already selected and sent an action ($massaction) defined
+				$selected = 0;
+				if (in_array($object->id, $arrayofselected)) {
+					$selected = 1;
+				}
+				print '<input id="cb'.$object->id.'" class="flat checkforselect" type="checkbox" name="toselect[]" value="'.$object->id.'"'.($selected ? ' checked="checked"' : '').'>';
+			}
+			print '</td>';
+			if (!$i) {
+				$totalarray['nbfield']++;
 			}
 			}
-			$i++;
 		}
 		}
-	} else {
-		print '<tr><td colspan="8"><span class="opacitymedium">'.$langs->trans("None").'</span></td></tr>';
-	}
-	print "</table>";
-	print '</div>';
 
 
-	print '</form>';
+		print '</tr>'."\n";
+	}
+	$i++;
+}
 
 
-	$db->free($result);
-} else {
-	dol_print_error($db);
+if ($num == 0) {
+	print '<tr><td colspan="8"><span class="opacitymedium">'.$langs->trans("None").'</span></td></tr>';
 }
 }
 
 
+$db->free($result);
+
+$parameters = array('arrayfields' => $arrayfields, 'sql' => $sql);
+$reshook = $hookmanager->executeHooks('printFieldListFooter', $parameters, $object, $action); // Note that $action and $object may have been modified by hook
+print $hookmanager->resPrint;
+
+print '</table>'."\n";
+print '</div>'."\n";
+
+print '</form>'."\n";
+
+
 // End of page
 // End of page
 llxFooter();
 llxFooter();
 $db->close();
 $db->close();

+ 244 - 119
htdocs/compta/prelevement/orders_list.php

@@ -32,17 +32,27 @@ require_once DOL_DOCUMENT_ROOT.'/compta/bank/class/account.class.php';
 // Load translation files required by the page
 // Load translation files required by the page
 $langs->loadLangs(array('banks', 'categories', 'withdrawals'));
 $langs->loadLangs(array('banks', 'categories', 'withdrawals'));
 
 
+$action     = GETPOST('action', 'aZ09') ? GETPOST('action', 'aZ09') : 'view'; // The action 'add', 'create', 'edit', 'update', 'view', ...
+$massaction = GETPOST('massaction', 'alpha'); // The bulk action (combo box choice into lists)
+$confirm    = GETPOST('confirm', 'alpha'); // Result of a confirmation
+$cancel     = GETPOST('cancel', 'alpha'); // We click on a Cancel button
+$toselect   = GETPOST('toselect', 'array'); // Array of ids of elements selected into a list
 $contextpage = GETPOST('contextpage', 'aZ') ? GETPOST('contextpage', 'aZ') : 'directdebitcredittransferlist'; // To manage different context of search
 $contextpage = GETPOST('contextpage', 'aZ') ? GETPOST('contextpage', 'aZ') : 'directdebitcredittransferlist'; // To manage different context of search
+$backtopage = GETPOST('backtopage', 'alpha'); // Go back to a dedicated page
+$optioncss = GETPOST('optioncss', 'alpha');
+$mode = GETPOST('mode', 'alpha');
 
 
 $type = GETPOST('type', 'aZ09');
 $type = GETPOST('type', 'aZ09');
 
 
+// Load variable for pagination
 $limit = GETPOST('limit', 'int') ? GETPOST('limit', 'int') : $conf->liste_limit;
 $limit = GETPOST('limit', 'int') ? GETPOST('limit', 'int') : $conf->liste_limit;
 $sortfield = GETPOST('sortfield', 'aZ09comma');
 $sortfield = GETPOST('sortfield', 'aZ09comma');
 $sortorder = GETPOST('sortorder', 'aZ09comma');
 $sortorder = GETPOST('sortorder', 'aZ09comma');
 $page = GETPOSTISSET('pageplusone') ? (GETPOST('pageplusone') - 1) : GETPOST("page", 'int');
 $page = GETPOSTISSET('pageplusone') ? (GETPOST('pageplusone') - 1) : GETPOST("page", 'int');
-if (empty($page) || $page == -1) {
+if (empty($page) || $page < 0 || GETPOST('button_search', 'alpha') || GETPOST('button_removefilter', 'alpha')) {
+	// If $page is not defined, or '' or -1 or if we click on clear filters
 	$page = 0;
 	$page = 0;
-}     // If $page is not defined, or '' or -1
+}
 $offset = $limit * $page;
 $offset = $limit * $page;
 $pageprev = $page - 1;
 $pageprev = $page - 1;
 $pagenext = $page + 1;
 $pagenext = $page + 1;
@@ -53,10 +63,6 @@ if (!$sortfield) {
 	$sortfield = "p.datec";
 	$sortfield = "p.datec";
 }
 }
 
 
-$optioncss = GETPOST('optioncss', 'alpha');
-$mode = GETPOST('mode', 'alpha');
-
-
 // Get supervariables
 // Get supervariables
 $statut = GETPOST('statut', 'int');
 $statut = GETPOST('statut', 'int');
 $search_ref = GETPOST('search_ref', 'alpha');
 $search_ref = GETPOST('search_ref', 'alpha');
@@ -96,7 +102,16 @@ if (GETPOST('button_removefilter_x', 'alpha') || GETPOST('button_removefilter.x'
  * View
  * View
  */
  */
 
 
-llxHeader('', $langs->trans("WithdrawalsReceipts"));
+$directdebitorder = new BonPrelevement($db);
+
+$titlekey = "WithdrawalsReceipts";
+$title = $langs->trans("WithdrawalsReceipts");
+if ($type == 'bank-transfer') {
+	$titlekey = "BankTransferReceipts";
+	$title = $langs->trans("BankTransferReceipts");
+}
+$help_url = '';
+
 
 
 $sql = "SELECT p.rowid, p.ref, p.amount, p.statut, p.datec";
 $sql = "SELECT p.rowid, p.ref, p.amount, p.statut, p.datec";
 
 
@@ -130,7 +145,7 @@ if (!getDolGlobalInt('MAIN_DISABLE_FULL_SCANLIST')) {
 		dol_print_error($db);
 		dol_print_error($db);
 	}
 	}
 
 
-	if (($page * $limit) > $nbtotalofrecords) {	// if total resultset is smaller then paging size (filtering), goto and load page 0
+	if (($page * $limit) > $nbtotalofrecords) {	// if total resultset is smaller than the paging size (filtering), goto and load page 0
 		$page = 0;
 		$page = 0;
 		$offset = 0;
 		$offset = 0;
 	}
 	}
@@ -143,142 +158,252 @@ if ($limit) {
 	$sql .= $db->plimit($limit + 1, $offset);
 	$sql .= $db->plimit($limit + 1, $offset);
 }
 }
 
 
-$result = $db->query($sql);
-if ($result) {
-	$num = $db->num_rows($result);
-	$i = 0;
+$resql = $db->query($sql);
+if (!$resql) {
+	dol_print_error($db);
+	exit;
+}
 
 
-	$param = '';
-	if (!empty($mode)) {
-		$param .= '&mode='.urlencode($mode);
-	}
-	if (!empty($contextpage) && $contextpage != $_SERVER["PHP_SELF"]) {
-		$param .= '&contextpage='.urlencode($contextpage);
-	}
-	if ($type == 'bank-transfer') {
-		$param .= '&amp;type=bank-transfer';
-	}
-	if ($limit > 0 && $limit != $conf->liste_limit) {
-		$param .= '&limit='.((int) $limit);
-	}
-	$param .= "&statut=".urlencode($statut);
+$num = $db->num_rows($resql);
 
 
-	$selectedfields = '';
+// Output page
+// --------------------------------------------------------------------
 
 
-	$newcardbutton = '';
-	$newcardbutton .= dolGetButtonTitle($langs->trans('ViewList'), '', 'fa fa-bars imgforviewmode', $_SERVER["PHP_SELF"].'?mode=common'.preg_replace('/(&|\?)*mode=[^&]+/', '', $param), '', ((empty($mode) || $mode == 'common') ? 2 : 1), array('morecss'=>'reposition'));
-	$newcardbutton .= dolGetButtonTitle($langs->trans('ViewKanban'), '', 'fa fa-th-list imgforviewmode', $_SERVER["PHP_SELF"].'?mode=kanban'.preg_replace('/(&|\?)*mode=[^&]+/', '', $param), '', ($mode == 'kanban' ? 2 : 1), array('morecss'=>'reposition'));
-	if ($usercancreate) {
-		$newcardbutton .= dolGetButtonTitle($langs->trans('NewStandingOrder'), '', 'fa fa-plus-circle', DOL_URL_ROOT.'/compta/prelevement/create.php?type='.urlencode($type));
-	}
+llxHeader('', $title, $help_url);
 
 
-	// Lines of title fields
-	print '<form method="POST" id="searchFormList" action="'.$_SERVER["PHP_SELF"].'">';
-	print '<input type="hidden" name="token" value="'.newToken().'">';
-	if ($optioncss != '') {
-		print '<input type="hidden" name="optioncss" value="'.$optioncss.'">';
-	}
-	print '<input type="hidden" name="formfilteraction" id="formfilteraction" value="list">';
-	print '<input type="hidden" name="action" value="list">';
-	print '<input type="hidden" name="sortfield" value="'.$sortfield.'">';
-	print '<input type="hidden" name="sortorder" value="'.$sortorder.'">';
-	print '<input type="hidden" name="contextpage" value="'.$contextpage.'">';
-	print '<input type="hidden" name="mode" value="'.$mode.'">';
-
-	if ($type != '') {
-		print '<input type="hidden" name="type" value="'.$type.'">';
-	}
-	$titlekey = "WithdrawalsReceipts";
-	$title = $langs->trans("WithdrawalsReceipts");
-	if ($type == 'bank-transfer') {
-		$titlekey = "BankTransferReceipts";
-		$title = $langs->trans("BankTransferReceipts");
-	}
+$arrayofselected = is_array($toselect) ? $toselect : array();
+
+$param = '';
+$param .= "&statut=".urlencode($statut);
+if ($type == 'bank-transfer') {
+	$param .= '&type=bank-transfer';
+}
+if (!empty($mode)) {
+	$param .= '&mode='.urlencode($mode);
+}
+if (!empty($contextpage) && $contextpage != $_SERVER["PHP_SELF"]) {
+	$param .= '&contextpage='.urlencode($contextpage);
+}
+if ($limit > 0 && $limit != $conf->liste_limit) {
+	$param .= '&limit='.((int) $limit);
+}
+if ($optioncss != '') {
+	$param .= '&optioncss='.urlencode($optioncss);
+}
+
+
+print '<form method="POST" id="searchFormList" action="'.$_SERVER["PHP_SELF"].'">'."\n";
+print '<input type="hidden" name="token" value="'.newToken().'">';
+if ($optioncss != '') {
+	print '<input type="hidden" name="optioncss" value="'.$optioncss.'">';
+}
+print '<input type="hidden" name="formfilteraction" id="formfilteraction" value="list">';
+print '<input type="hidden" name="action" value="list">';
+print '<input type="hidden" name="sortfield" value="'.$sortfield.'">';
+print '<input type="hidden" name="sortorder" value="'.$sortorder.'">';
+print '<input type="hidden" name="page" value="'.$page.'">';
+print '<input type="hidden" name="contextpage" value="'.$contextpage.'">';
+print '<input type="hidden" name="page_y" value="">';
+print '<input type="hidden" name="mode" value="'.$mode.'">';
+
+if ($type != '') {
+	print '<input type="hidden" name="type" value="'.$type.'">';
+}
+
+$newcardbutton = '';
+$newcardbutton .= dolGetButtonTitle($langs->trans('ViewList'), '', 'fa fa-bars imgforviewmode', $_SERVER["PHP_SELF"].'?mode=common'.preg_replace('/(&|\?)*mode=[^&]+/', '', $param), '', ((empty($mode) || $mode == 'common') ? 2 : 1), array('morecss'=>'reposition'));
+$newcardbutton .= dolGetButtonTitle($langs->trans('ViewKanban'), '', 'fa fa-th-list imgforviewmode', $_SERVER["PHP_SELF"].'?mode=kanban'.preg_replace('/(&|\?)*mode=[^&]+/', '', $param), '', ($mode == 'kanban' ? 2 : 1), array('morecss'=>'reposition'));
+if ($usercancreate) {
+	$newcardbutton .= dolGetButtonTitle($langs->trans('NewStandingOrder'), '', 'fa fa-plus-circle', DOL_URL_ROOT.'/compta/prelevement/create.php?type='.urlencode($type));
+}
+
+print_barre_liste($title, $page, $_SERVER["PHP_SELF"], $param, $sortfield, $sortorder, '', $num, $nbtotalofrecords, 'generic', 0, $newcardbutton, '', $limit, 0, 0, 1);
+
+$moreforfilter = '';
+/*$moreforfilter.='<div class="divsearchfield">';
+ $moreforfilter.= $langs->trans('MyFilter') . ': <input type="text" name="search_myfield" value="'.dol_escape_htmltag($search_myfield).'">';
+ $moreforfilter.= '</div>';*/
+
+$parameters = array();
+$reshook = $hookmanager->executeHooks('printFieldPreListTitle', $parameters, $object, $action); // Note that $action and $object may have been modified by hook
+if (empty($reshook)) {
+	$moreforfilter .= $hookmanager->resPrint;
+} else {
+	$moreforfilter = $hookmanager->resPrint;
+}
 
 
-	print_barre_liste($title, $page, $_SERVER["PHP_SELF"], $param, $sortfield, $sortorder, '', $num, $nbtotalofrecords, 'generic', 0, $newcardbutton, '', $limit, 0, 0, 1);
+if (!empty($moreforfilter)) {
+	print '<div class="liste_titre liste_titre_bydiv centpercent">';
+	print $moreforfilter;
+	$parameters = array();
+	$reshook = $hookmanager->executeHooks('printFieldPreListTitle', $parameters, $object, $action); // Note that $action and $object may have been modified by hook
+	print $hookmanager->resPrint;
+	print '</div>';
+}
 
 
-	$moreforfilter = '';
+$varpage = empty($contextpage) ? $_SERVER["PHP_SELF"] : $contextpage;
+$selectedfields = ($mode != 'kanban' ? $form->multiSelectArrayWithCheckbox('selectedfields', $arrayfields, $varpage, getDolGlobalString('MAIN_CHECKBOX_LEFT_COLUMN', '')) : ''); // This also change content of $arrayfields
+$selectedfields .= (count($arrayofmassactions) ? $form->showCheckAddButtons('checkforselect', 1) : '');
 
 
-	print '<div class="div-table-responsive">';
-	print '<table class="tagtable liste'.($moreforfilter ? " listwithfilterbefore" : "").'">'."\n";
+print '<div class="div-table-responsive">'; // You can use div-table-responsive-no-min if you dont need reserved height for your table
+print '<table class="tagtable nobottomiftotal liste'.($moreforfilter ? " listwithfilterbefore" : "").'">'."\n";
 
 
-	print '<tr class="liste_titre">';
-	print '<td class="liste_titre"><input type="text" class="flat maxwidth100" name="search_ref" value="'.dol_escape_htmltag($search_ref).'"></td>';
-	print '<td class="liste_titre">&nbsp;</td>';
-	print '<td class="liste_titre right"><input type="text" class="flat maxwidth100" name="search_amount" value="'.dol_escape_htmltag($search_amount).'"></td>';
-	print '<td class="liste_titre">&nbsp;</td>';
-	print '<td class="liste_titre maxwidthsearch">';
+// Fields title search
+// --------------------------------------------------------------------
+print '<tr class="liste_titre_filter">';
+// Action column
+if (getDolGlobalString('MAIN_CHECKBOX_LEFT_COLUMN')) {
+	print '<td class="liste_titre center maxwidthsearch">';
+	$searchpicto = $form->showFilterButtons('left');
+	print $searchpicto;
+	print '</td>';
+}
+print '<td class="liste_titre"><input type="text" class="flat maxwidth100" name="search_ref" value="'.dol_escape_htmltag($search_ref).'"></td>';
+print '<td class="liste_titre">&nbsp;</td>';
+print '<td class="liste_titre right"><input type="text" class="flat maxwidth100" name="search_amount" value="'.dol_escape_htmltag($search_amount).'"></td>';
+print '<td class="liste_titre">&nbsp;</td>';
+// Action column
+if (!getDolGlobalString('MAIN_CHECKBOX_LEFT_COLUMN')) {
+	print '<td class="liste_titre center maxwidthsearch">';
 	$searchpicto = $form->showFilterButtons();
 	$searchpicto = $form->showFilterButtons();
 	print $searchpicto;
 	print $searchpicto;
 	print '</td>';
 	print '</td>';
-	print '</tr>';
-
-	print '<tr class="liste_titre">';
-	print_liste_field_titre($titlekey, $_SERVER["PHP_SELF"], "p.ref", '', $param, '', $sortfield, $sortorder);
-	print_liste_field_titre("Date", $_SERVER["PHP_SELF"], "p.datec", "", $param, '', $sortfield, $sortorder, 'center ');
-	print_liste_field_titre("Amount", $_SERVER["PHP_SELF"], "p.amount", "", $param, '', $sortfield, $sortorder, 'right ');
-	print_liste_field_titre("Status", $_SERVER["PHP_SELF"], "", "", $param, '', $sortfield, $sortorder, 'right ');
-	print getTitleFieldOfList($selectedfields, 0, $_SERVER["PHP_SELF"], "", '', $param, '', $sortfield, $sortorder, 'maxwidthsearch center ')."\n";
-	print "</tr>\n";
-
-	$directdebitorder = new BonPrelevement($db);
-
-	if ($num) {
-		while ($i < min($num, $limit)) {
-			$obj = $db->fetch_object($result);
-
-			$directdebitorder->id = $obj->rowid;
-			$directdebitorder->ref = $obj->ref;
-			$directdebitorder->date_echeance = $obj->datec;
-			$directdebitorder->total = $obj->amount;
-			$directdebitorder->statut = $obj->statut;
-
-			if ($mode == 'kanban') {
-				if ($i == 0) {
-					print '<tr><td colspan="12">';
-					print '<div class="box-flex-container kanban">';
-				}
-				// Output Kanban
-				print $directdebitorder->getKanbanView('', array('selected' => in_array($obj->id, $arrayofselected)));
-				if ($i == (min($num, $limit) - 1)) {
-					print '</div>';
-					print '</td></tr>';
-				}
-			} else {
-				print '<tr class="oddeven">';
+}
+print '</tr>'."\n";
+
+$totalarray = array();
+$totalarray['nbfield'] = 0;
+
+// Fields title label
+// --------------------------------------------------------------------
+print '<tr class="liste_titre">';
+// Action column
+if (getDolGlobalString('MAIN_CHECKBOX_LEFT_COLUMN')) {
+	print getTitleFieldOfList($selectedfields, 0, $_SERVER["PHP_SELF"], '', '', '', '', $sortfield, $sortorder, 'center maxwidthsearch ')."\n";
+	$totalarray['nbfield']++;
+}
+print_liste_field_titre($titlekey, $_SERVER["PHP_SELF"], "p.ref", '', $param, '', $sortfield, $sortorder);
+$totalarray['nbfield']++;
+print_liste_field_titre("Date", $_SERVER["PHP_SELF"], "p.datec", "", $param, '', $sortfield, $sortorder, 'center ');
+$totalarray['nbfield']++;
+print_liste_field_titre("Amount", $_SERVER["PHP_SELF"], "p.amount", "", $param, '', $sortfield, $sortorder, 'right ');
+$totalarray['nbfield']++;
+print_liste_field_titre("Status", $_SERVER["PHP_SELF"], "", "", $param, '', $sortfield, $sortorder, 'right ');
+$totalarray['nbfield']++;
+// Action column
+if (!getDolGlobalString('MAIN_CHECKBOX_LEFT_COLUMN')) {
+	print getTitleFieldOfList($selectedfields, 0, $_SERVER["PHP_SELF"], '', '', '', '', $sortfield, $sortorder, 'center maxwidthsearch ')."\n";
+	$totalarray['nbfield']++;
+}
+print '</tr>'."\n";
 
 
-				print '<td>';
-				print $directdebitorder->getNomUrl(1);
-				print "</td>\n";
+// Loop on record
+// --------------------------------------------------------------------
 
 
-				print '<td class="center">'.dol_print_date($db->jdate($obj->datec), 'day')."</td>\n";
+$i = 0;
+$savnbfield = $totalarray['nbfield'];
+$totalarray = array();
+$totalarray['nbfield'] = 0;
 
 
-				print '<td class="right"><span class="amount">'.price($obj->amount)."</span></td>\n";
+$imaxinloop = ($limit ? min($num, $limit) : $num);
+while ($i < $imaxinloop) {
+	$obj = $db->fetch_object($resql);
 
 
-				print '<td class="right">';
-				print $bon->LibStatut($obj->statut, 5);
-				print '</td>';
+	$directdebitorder->id = $obj->rowid;
+	$directdebitorder->ref = $obj->ref;
+	$directdebitorder->date_echeance = $obj->datec;
+	$directdebitorder->total = $obj->amount;
+	$directdebitorder->statut = $obj->statut;
 
 
-				print '<td class="right"></td>'."\n";
+	$object = $directdebitorder;
 
 
-				print "</tr>\n";
+	if ($mode == 'kanban') {
+		if ($i == 0) {
+			print '<tr><td colspan="'.$savnbfield.'">';
+			print '<div class="box-flex-container kanban">';
+		}
+		// Output Kanban
+		if ($massactionbutton || $massaction) { // If we are in select mode (massactionbutton defined) or if we have already selected and sent an action ($massaction) defined
+			$selected = 0;
+			if (in_array($object->id, $arrayofselected)) {
+				$selected = 1;
 			}
 			}
-			$i++;
+		}
+		print $directdebitorder->getKanbanView('', array('selected' => in_array($obj->id, $arrayofselected)));
+		if ($i == ($imaxinloop - 1)) {
+			print '</div>';
+			print '</td></tr>';
 		}
 		}
 	} else {
 	} else {
-		print '<tr><td colspan="5"><span class="opacitymedium">'.$langs->trans("None").'</span></td></tr>';
-	}
+		// Show line of result
+		$j = 0;
+		print '<tr data-rowid="'.$object->id.'" class="oddeven">';
+
+		// Action column
+		if (getDolGlobalString('MAIN_CHECKBOX_LEFT_COLUMN')) {
+			print '<td class="nowrap center">';
+			if ($massactionbutton || $massaction) { // If we are in select mode (massactionbutton defined) or if we have already selected and sent an action ($massaction) defined
+				$selected = 0;
+				if (in_array($object->id, $arrayofselected)) {
+					$selected = 1;
+				}
+				print '<input id="cb'.$object->id.'" class="flat checkforselect" type="checkbox" name="toselect[]" value="'.$object->id.'"'.($selected ? ' checked="checked"' : '').'>';
+			}
+			print '</td>';
+			if (!$i) {
+				$totalarray['nbfield']++;
+			}
+		}
 
 
-	print "</table>";
-	print '</div>';
+		print '<td>';
+		print $directdebitorder->getNomUrl(1);
+		print "</td>\n";
 
 
-	print '</form>';
+		print '<td class="center">'.dol_print_date($db->jdate($obj->datec), 'day')."</td>\n";
 
 
-	$db->free($result);
-} else {
-	dol_print_error($db);
+		print '<td class="right"><span class="amount">'.price($obj->amount)."</span></td>\n";
+
+		print '<td class="right">';
+		print $bon->LibStatut($obj->statut, 5);
+		print '</td>';
+
+		// Action column
+		if (!getDolGlobalString('MAIN_CHECKBOX_LEFT_COLUMN')) {
+			print '<td class="nowrap center">';
+			if ($massactionbutton || $massaction) { // If we are in select mode (massactionbutton defined) or if we have already selected and sent an action ($massaction) defined
+				$selected = 0;
+				if (in_array($object->id, $arrayofselected)) {
+					$selected = 1;
+				}
+				print '<input id="cb'.$object->id.'" class="flat checkforselect" type="checkbox" name="toselect[]" value="'.$object->id.'"'.($selected ? ' checked="checked"' : '').'>';
+			}
+			print '</td>';
+			if (!$i) {
+				$totalarray['nbfield']++;
+			}
+		}
+
+		print '</tr>'."\n";
+	}
+	$i++;
 }
 }
 
 
+if ($num == 0) {
+	print '<tr><td colspan="5"><span class="opacitymedium">'.$langs->trans("None").'</span></td></tr>';
+}
+
+$db->free($result);
+
+$parameters = array('arrayfields' => $arrayfields, 'sql' => $sql);
+$reshook = $hookmanager->executeHooks('printFieldListFooter', $parameters, $object, $action); // Note that $action and $object may have been modified by hook
+print $hookmanager->resPrint;
+
+print '</table>'."\n";
+print '</div>'."\n";
+
+print '</form>'."\n";
+
+
 // End of page
 // End of page
 llxFooter();
 llxFooter();
 $db->close();
 $db->close();

+ 14 - 7
htdocs/compta/prelevement/stats.php

@@ -118,23 +118,30 @@ if ($resql) {
 	while ($i < $num) {
 	while ($i < $num) {
 		$row = $db->fetch_row($resql);
 		$row = $db->fetch_row($resql);
 
 
-		print '<tr class="oddeven"><td>';
+		print '<tr class="oddeven">';
 
 
+		print '<td>';
 		print $ligne->LibStatut($row[2], 1);
 		print $ligne->LibStatut($row[2], 1);
 		//print $st[$row[2]];
 		//print $st[$row[2]];
-		print '</td><td align="center">';
+		print '</td>';
+
+		print '<td align="center">';
 		print $row[1];
 		print $row[1];
+		print '</td>';
 
 
-		print '</td><td class="right">';
+		print '<td class="right">';
 		print round($row[1] / $nbtotal * 100, 2)." %";
 		print round($row[1] / $nbtotal * 100, 2)." %";
+		print '</td>';
 
 
-		print '</td><td class="right">';
-
+		print '<td class="right amount">';
 		print price($row[0]);
 		print price($row[0]);
+		print '</td>';
 
 
-		print '</td><td class="right">';
+		print '<td class="right">';
 		print round($row[0] / $total * 100, 2)." %";
 		print round($row[0] / $total * 100, 2)." %";
-		print '</td></tr>';
+		print '</td>';
+
+		print '</tr>';
 
 
 		$i++;
 		$i++;
 	}
 	}

+ 1 - 1
htdocs/contact/canvas/actions_contactcard_common.class.php

@@ -278,7 +278,7 @@ abstract class ActionsContactCardCommon
 	/**
 	/**
 	 *  Assign POST values into object
 	 *  Assign POST values into object
 	 *
 	 *
-	 *  @return		string					HTML output
+	 *  @return		void
 	 */
 	 */
 	private function assign_post()
 	private function assign_post()
 	{
 	{

+ 8 - 1
htdocs/contrat/class/contrat.class.php

@@ -738,6 +738,10 @@ class Contrat extends CommonObject
 					}
 					}
 
 
 					return $this->id;
 					return $this->id;
+				} else {
+					dol_syslog(get_class($this)."::fetch Contract failed");
+					$this->error = "Fetch contract failed";
+					return -1;
 				}
 				}
 			} else {
 			} else {
 				dol_syslog(get_class($this)."::fetch Contract not found");
 				dol_syslog(get_class($this)."::fetch Contract not found");
@@ -1664,7 +1668,7 @@ class Contrat extends CommonObject
 	 *  @param	array		$array_options		extrafields array
 	 *  @param	array		$array_options		extrafields array
 	 * 	@param 	string		$fk_unit 			Code of the unit to use. Null to use the default one
 	 * 	@param 	string		$fk_unit 			Code of the unit to use. Null to use the default one
 	 * 	@param 	string		$rang 				Position
 	 * 	@param 	string		$rang 				Position
-	 *  @return int              				< 0 si erreur, > 0 si ok
+	 *  @return int              				<0 if KO, >0 if OK
 	 */
 	 */
 	public function updateline($rowid, $desc, $pu, $qty, $remise_percent, $date_start, $date_end, $tvatx, $localtax1tx = 0.0, $localtax2tx = 0.0, $date_start_real = '', $date_end_real = '', $price_base_type = 'HT', $info_bits = 0, $fk_fournprice = null, $pa_ht = 0, $array_options = 0, $fk_unit = null, $rang = 0)
 	public function updateline($rowid, $desc, $pu, $qty, $remise_percent, $date_start, $date_end, $tvatx, $localtax1tx = 0.0, $localtax2tx = 0.0, $date_start_real = '', $date_end_real = '', $price_base_type = 'HT', $info_bits = 0, $fk_fournprice = null, $pa_ht = 0, $array_options = 0, $fk_unit = null, $rang = 0)
 	{
 	{
@@ -1819,6 +1823,9 @@ class Contrat extends CommonObject
 
 
 				$this->db->commit();
 				$this->db->commit();
 				return 1;
 				return 1;
+			} else {
+				$this->db->rollback();
+				return -1;
 			}
 			}
 		} else {
 		} else {
 			$this->db->rollback();
 			$this->db->rollback();

+ 1 - 1
htdocs/core/boxes/box_members_by_tags.php

@@ -160,7 +160,7 @@ class box_members_by_tags extends ModeleBoxes
 
 
 					$this->info_box_contents[$line][] = array(
 					$this->info_box_contents[$line][] = array(
 						'td' => 'class="tdoverflowmax150 maxwidth150onsmartphone"',
 						'td' => 'class="tdoverflowmax150 maxwidth150onsmartphone"',
-						'text' => '<a href="'.$DOL_MAIN_URL_ROOT.'/adherents/list.php?search_categ='.$adhtag->id.'&sortfield=d.datefin,t.subscription&sortorder=desc,desc&backtopage='.urlencode($_SERVER['PHP_SELF']).'">'.dol_trunc(($adhtag->ref ? $adhtag->ref : $adhtag->label), dol_size(32)).'</a>',
+						'text' => '<a href="'.DOL_MAIN_URL_ROOT.'/adherents/list.php?search_categ='.$adhtag->id.'&sortfield=d.datefin,t.subscription&sortorder=desc,desc&backtopage='.urlencode($_SERVER['PHP_SELF']).'">'.dol_trunc(($adhtag->ref ? $adhtag->ref : $adhtag->label), dol_size(32)).'</a>',
 						'asis' => 1,
 						'asis' => 1,
 					);
 					);
 					$this->info_box_contents[$line][] = array(
 					$this->info_box_contents[$line][] = array(

+ 2 - 2
htdocs/core/class/CMailFile.class.php

@@ -164,7 +164,7 @@ class CMailFile
 	 *	@param	string	$css                 Css option
 	 *	@param	string	$css                 Css option
 	 *	@param	string	$trackid             Tracking string (contains type and id of related element)
 	 *	@param	string	$trackid             Tracking string (contains type and id of related element)
 	 *  @param  string  $moreinheader        More in header. $moreinheader must contains the "\r\n" (TODO not supported for other MAIL_SEND_MODE different than 'mail' and 'smtps' for the moment)
 	 *  @param  string  $moreinheader        More in header. $moreinheader must contains the "\r\n" (TODO not supported for other MAIL_SEND_MODE different than 'mail' and 'smtps' for the moment)
-	 *  @param  string  $sendcontext      	 'standard', 'emailing', ... (used to define which sending mode and parameters to use)
+	 *  @param  string  $sendcontext      	 'standard', 'emailing', 'ticket', 'password', ... (used to define which sending mode and parameters to use)
 	 *  @param	string	$replyto			 Reply-to email (will be set to same value than From by default if not provided)
 	 *  @param	string	$replyto			 Reply-to email (will be set to same value than From by default if not provided)
 	 *  @param	string	$upload_dir_tmp		 Temporary directory (used to convert images embedded as img src=data:image)
 	 *  @param	string	$upload_dir_tmp		 Temporary directory (used to convert images embedded as img src=data:image)
 	 */
 	 */
@@ -187,7 +187,7 @@ class CMailFile
 
 
 		$this->sendcontext = $sendcontext;
 		$this->sendcontext = $sendcontext;
 
 
-		// Define this->sendmode ('mail', 'smtps', 'siwftmailer', ...) according to $sendcontext ('standard', 'emailing', 'ticket')
+		// Define this->sendmode ('mail', 'smtps', 'swiftmailer', ...) according to $sendcontext ('standard', 'emailing', 'ticket', 'password')
 		$this->sendmode = '';
 		$this->sendmode = '';
 		if (!empty($this->sendcontext)) {
 		if (!empty($this->sendcontext)) {
 			$smtpContextKey = strtoupper($this->sendcontext);
 			$smtpContextKey = strtoupper($this->sendcontext);

+ 15 - 0
htdocs/core/class/commonobject.class.php

@@ -9888,6 +9888,21 @@ abstract class CommonObject
 				$this->{$key} = $value;
 				$this->{$key} = $value;
 			}
 			}
 		}
 		}
+
+		// Force values to default values when known
+		if (property_exists($this, 'fields')) {
+			foreach ($this->fields as $key => $value) {
+				// If fields are already set, do nothing
+				if (array_key_exists($key, $fields)) {
+					continue;
+				}
+
+				if (!empty($value['default'])) {
+					$this->$key = $value['default'];
+				}
+			}
+		}
+
 		return 1;
 		return 1;
 	}
 	}
 
 

+ 12 - 5
htdocs/core/class/extrafields.class.php

@@ -823,9 +823,9 @@ class ExtraFields
 
 
 	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
 	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
 	/**
 	/**
-	 * 	Load array this->attributes
+	 * 	Load the array of extrafields defintion $this->attributes
 	 *
 	 *
-	 * 	@param	string		$elementtype		Type of element ('' = all or $object->table_element like 'adherent', 'commande', 'thirdparty', 'facture', 'propal', 'product', ...).
+	 * 	@param	string		$elementtype		Type of element ('all' = all or $object->table_element like 'adherent', 'commande', 'thirdparty', 'facture', 'propal', 'product', ...).
 	 * 	@param	boolean		$forceload			Force load of extra fields whatever is status of cache.
 	 * 	@param	boolean		$forceload			Force load of extra fields whatever is status of cache.
 	 * 	@return	array							Array of attributes keys+label for all extra fields.
 	 * 	@return	array							Array of attributes keys+label for all extra fields.
 	 */
 	 */
@@ -848,6 +848,9 @@ class ExtraFields
 			$elementtype = 'commande_fournisseur';
 			$elementtype = 'commande_fournisseur';
 		}
 		}
 
 
+		// Test cache $this->attributes[$elementtype]['loaded'] to see if we must do something
+		// TODO
+
 		$array_name_label = array();
 		$array_name_label = array();
 
 
 		// We should not have several time this request. If we have, there is some optimization to do by calling a simple $extrafields->fetch_optionals() in top of code and not into subcode
 		// We should not have several time this request. If we have, there is some optimization to do by calling a simple $extrafields->fetch_optionals() in top of code and not into subcode
@@ -855,7 +858,7 @@ class ExtraFields
 		$sql .= " css, cssview, csslist";
 		$sql .= " css, cssview, csslist";
 		$sql .= " FROM ".$this->db->prefix()."extrafields";
 		$sql .= " FROM ".$this->db->prefix()."extrafields";
 		//$sql.= " WHERE entity IN (0,".$conf->entity.")";    // Filter is done later
 		//$sql.= " WHERE entity IN (0,".$conf->entity.")";    // Filter is done later
-		if ($elementtype) {
+		if ($elementtype && $elementtype != 'all') {
 			$sql .= " WHERE elementtype = '".$this->db->escape($elementtype)."'"; // Filed with object->table_element
 			$sql .= " WHERE elementtype = '".$this->db->escape($elementtype)."'"; // Filed with object->table_element
 		}
 		}
 		$sql .= " ORDER BY pos";
 		$sql .= " ORDER BY pos";
@@ -906,7 +909,7 @@ class ExtraFields
 				}
 				}
 			}
 			}
 			if ($elementtype) {
 			if ($elementtype) {
-				$this->attributes[$elementtype]['loaded'] = 1; // If nothing found, we also save tag 'loaded'
+				$this->attributes[$elementtype]['loaded'] = 1; // Note: If nothing is found, we also set the key 'loaded' to 1.
 				$this->attributes[$elementtype]['count'] = $count;
 				$this->attributes[$elementtype]['count'] = $count;
 			}
 			}
 		} else {
 		} else {
@@ -2136,8 +2139,12 @@ class ExtraFields
 						|| (is_array($_POST["options_".$key]) && empty($_POST["options_".$key]))) {
 						|| (is_array($_POST["options_".$key]) && empty($_POST["options_".$key]))) {
 						//print 'ccc'.$value.'-'.$this->attributes[$object->table_element]['required'][$key];
 						//print 'ccc'.$value.'-'.$this->attributes[$object->table_element]['required'][$key];
 
 
-						// Field is not defined. We mark this as a problem. We may fix it later if there is a default value and $todefaultifmissing is set.
+						// Field is not defined. We mark this as an error. We may fix it later if there is a default value and $todefaultifmissing is set.
+
 						$nofillrequired++;
 						$nofillrequired++;
+						if (!empty($this->attributes[$object->table_element]['langfile'][$key])) {
+							$langs->load($this->attributes[$object->table_element]['langfile'][$key]);
+						}
 						$error_field_required[$key] = $langs->transnoentitiesnoconv($value);
 						$error_field_required[$key] = $langs->transnoentitiesnoconv($value);
 					}
 					}
 				}
 				}

+ 85 - 28
htdocs/core/class/html.form.class.php

@@ -4791,16 +4791,16 @@ class Form
 	/**
 	/**
 	 *  Return a HTML select list of bank accounts
 	 *  Return a HTML select list of bank accounts
 	 *
 	 *
-	 * @param string $selected Id account pre-selected
-	 * @param string $htmlname Name of select zone
-	 * @param int $status Status of searched accounts (0=open, 1=closed, 2=both)
-	 * @param string $filtre To filter list. This parameter must not come from input of users
-	 * @param int $useempty 1=Add an empty value in list, 2=Add an empty value in list only if there is more than 2 entries.
-	 * @param string $moreattrib To add more attribute on select
-	 * @param int $showcurrency Show currency in label
-	 * @param string $morecss More CSS
-	 * @param int $nooutput 1=Return string, do not send to output
-	 * @return    int                            <0 if error, Num of bank account found if OK (0, 1, 2, ...)
+	 * @param string 	$selected 		Id account pre-selected
+	 * @param string 	$htmlname 		Name of select zone
+	 * @param int 		$status 		Status of searched accounts (0=open, 1=closed, 2=both)
+	 * @param string 	$filtre 		To filter list. This parameter must not come from input of users
+	 * @param int 		$useempty 		1=Add an empty value in list, 2=Add an empty value in list only if there is more than 2 entries.
+	 * @param string 	$moreattrib 	To add more attribute on select
+	 * @param int 		$showcurrency 	Show currency in label
+	 * @param string 	$morecss 		More CSS
+	 * @param int 		$nooutput 		1=Return string, do not send to output
+	 * @return int                   	<0 if error, Num of bank account found if OK (0, 1, 2, ...)
 	 */
 	 */
 	public function select_comptes($selected = '', $htmlname = 'accountid', $status = 0, $filtre = '', $useempty = 0, $moreattrib = '', $showcurrency = 0, $morecss = '', $nooutput = 0)
 	public function select_comptes($selected = '', $htmlname = 'accountid', $status = 0, $filtre = '', $useempty = 0, $moreattrib = '', $showcurrency = 0, $morecss = '', $nooutput = 0)
 	{
 	{
@@ -10431,23 +10431,25 @@ class Form
 	/**
 	/**
 	 * Output the component to make advanced search criteries
 	 * Output the component to make advanced search criteries
 	 *
 	 *
-	 * @param array $arrayofcriterias Array of available search criterias. Example: array($object->element => $object->fields, 'otherfamily' => otherarrayoffields, ...)
-	 * @param array $search_component_params Array of selected search criterias
-	 * @param array $arrayofinputfieldsalreadyoutput Array of input fields already inform. The component will not generate a hidden input field if it is in this list.
-	 * @param string $search_component_params_hidden String with $search_component_params criterias
-	 * @return    string                                              HTML component for advanced search
+	 * @param 	array 	$arrayofcriterias 					Array of available search criterias. Example: array($object->element => $object->fields, 'otherfamily' => otherarrayoffields, ...)
+	 * @param 	array 	$search_component_params 			Array of selected search criterias
+	 * @param 	array 	$arrayofinputfieldsalreadyoutput 	Array of input fields already inform. The component will not generate a hidden input field if it is in this list.
+	 * @param 	string 	$search_component_params_hidden 	String with $search_component_params criterias
+	 * @return	string                                    	HTML component for advanced search
 	 */
 	 */
 	public function searchComponent($arrayofcriterias, $search_component_params, $arrayofinputfieldsalreadyoutput = array(), $search_component_params_hidden = '')
 	public function searchComponent($arrayofcriterias, $search_component_params, $arrayofinputfieldsalreadyoutput = array(), $search_component_params_hidden = '')
 	{
 	{
 		global $langs;
 		global $langs;
 
 
+		if ($search_component_params_hidden != '' && !preg_match('/^\(.*\)$/', $search_component_params_hidden)) {    // If $search_component_params_hidden does not start and end with ()
+			$search_component_params_hidden = '(' . $search_component_params_hidden . ')';
+		}
+
 		$ret = '';
 		$ret = '';
 
 
 		$ret .= '<div class="divadvancedsearchfieldcomp inline-block">';
 		$ret .= '<div class="divadvancedsearchfieldcomp inline-block">';
-		//$ret .= '<button type="submit" class="liste_titre button_removefilter" name="button_removefilter_x" value="x"><span class="fa fa-remove"></span></button>';
 		$ret .= '<a href="#" class="dropdownsearch-toggle unsetcolor">';
 		$ret .= '<a href="#" class="dropdownsearch-toggle unsetcolor">';
 		$ret .= '<span class="fas fa-filter linkobject boxfilter paddingright pictofixedwidth" title="' . dol_escape_htmltag($langs->trans("Filters")) . '" id="idsubimgproductdistribution"></span>';
 		$ret .= '<span class="fas fa-filter linkobject boxfilter paddingright pictofixedwidth" title="' . dol_escape_htmltag($langs->trans("Filters")) . '" id="idsubimgproductdistribution"></span>';
-		//$ret .= $langs->trans("Filters");
 		$ret .= '</a>';
 		$ret .= '</a>';
 
 
 		$ret .= '<div class="divadvancedsearchfieldcompinput inline-block minwidth500 maxwidth300onsmartphone">';
 		$ret .= '<div class="divadvancedsearchfieldcompinput inline-block minwidth500 maxwidth300onsmartphone">';
@@ -10456,16 +10458,58 @@ class Form
 		$ret .= '<div name="divsearch_component_params" class="noborderbottom search_component_params inline-block valignmiddle">';
 		$ret .= '<div name="divsearch_component_params" class="noborderbottom search_component_params inline-block valignmiddle">';
 
 
 		if ($search_component_params_hidden) {
 		if ($search_component_params_hidden) {
-			if (!preg_match('/^\(.*\)$/', $search_component_params_hidden)) {    // If $search_component_params_hidden does not start and end with ()
-				$search_component_params_hidden .= '(' . $search_component_params_hidden . ')';
+			// Split the criteria on each AND
+			//var_dump($search_component_params_hidden);
+
+			$nbofchars = dol_strlen($search_component_params_hidden);
+			$arrayofandtags = array();
+			$i = 0; $s = '';
+			$countparenthesis = 0;
+			while ($i < $nbofchars) {
+				$char = dol_substr($search_component_params_hidden, $i, 1);
+
+				if ($char == '(') {
+					$countparenthesis++;
+				} elseif ($char == ')') {
+					$countparenthesis--;
+				}
+
+				if ($countparenthesis == 0) {
+					$char2 = dol_substr($search_component_params_hidden, $i+1, 1);
+					$char3 = dol_substr($search_component_params_hidden, $i+2, 1);
+					if ($char == 'A' && $char2 == 'N' && $char3 == 'D') {
+						// We found a AND
+						$arrayofandtags[] = trim($s);
+						$s = '';
+						$i+=2;
+					} else {
+						$s .= $char;
+					}
+				} else {
+					$s .= $char;
+				}
+				$i++;
+			}
+			if ($s) {
+				$arrayofandtags[] = trim($s);
 			}
 			}
-			$errormessage = '';
-			$searchtags = forgeSQLFromUniversalSearchCriteria($search_component_params_hidden, $errormessage);
-			if ($errormessage) {
-				print 'ERROR in parsing search string: ' . dol_escape_htmltag($errormessage);
+
+			// Show each AND part
+			foreach ($arrayofandtags as $tmpkey => $tmpval) {
+				$errormessage = '';
+				$searchtags = forgeSQLFromUniversalSearchCriteria($tmpval, $errormessage, 1, 1);
+				if ($errormessage) {
+					$this->error = 'ERROR in parsing search string: '.$errormessage;
+				}
+				// Remove first and last parenthesis but only if first is the opening and last the closing of the same group
+				include_once DOL_DOCUMENT_ROOT.'/core/lib/functions2.lib.php';
+				$searchtags = removeGlobalParenthesis($searchtags);
+
+				$ret .= '<span class="marginleftonlyshort valignmiddle tagsearch" data-ufilterid="'.($tmpkey+1).'" data-ufilter="'.dol_escape_htmltag($tmpval).'">';
+				$ret .= '<span class="tagsearchdelete select2-selection__choice__remove" data-ufilterid="'.($tmpkey+1).'">x</span> ';
+				$ret .= dol_escape_htmltag($searchtags);
+				$ret .= '</span>';
 			}
 			}
-			//var_dump($searchtags);
-			$ret .= '<span class="marginleftonlyshort valignmiddle tagsearch"><span class="tagsearchdelete select2-selection__choice__remove">x</span> ' . dol_escape_htmltag($searchtags) . '</span>';
 		}
 		}
 
 
 		//$ret .= '<button type="submit" class="liste_titre button_search paddingleftonly" name="button_search_x" value="x"><span class="fa fa-search"></span></button>';
 		//$ret .= '<button type="submit" class="liste_titre button_search paddingleftonly" name="button_search_x" value="x"><span class="fa fa-search"></span></button>';
@@ -10478,9 +10522,11 @@ class Form
 		if ($show_search_component_params_hidden) {
 		if ($show_search_component_params_hidden) {
 			$ret .= '<input type="hidden" name="show_search_component_params_hidden" value="1">';
 			$ret .= '<input type="hidden" name="show_search_component_params_hidden" value="1">';
 		}
 		}
-		$ret .= "<!-- We store the full search string into this field. For example: (t.ref:like:'SO-%') and ((t.ref:like:'CO-%') or (t.ref:like:'AA%')) -->";
+		$ret .= "<!-- We store the full Universal Search String into this field. For example: (t.ref:like:'SO-%') AND ((t.ref:like:'CO-%') OR (t.ref:like:'AA%')) -->";
 		$ret .= '<input type="hidden" name="search_component_params_hidden" value="' . dol_escape_htmltag($search_component_params_hidden) . '">';
 		$ret .= '<input type="hidden" name="search_component_params_hidden" value="' . dol_escape_htmltag($search_component_params_hidden) . '">';
-		// For compatibility with forms that show themself the search criteria in addition of this component, we output the fields
+		// $ret .= "<!-- sql= ".forgeSQLFromUniversalSearchCriteria($search_component_params_hidden, $errormessage)." -->";
+
+		// For compatibility with forms that show themself the search criteria in addition of this component, we output these fields
 		foreach ($arrayofcriterias as $criterias) {
 		foreach ($arrayofcriterias as $criterias) {
 			foreach ($criterias as $criteriafamilykey => $criteriafamilyval) {
 			foreach ($criterias as $criteriafamilykey => $criteriafamilyval) {
 				if (in_array('search_' . $criteriafamilykey, $arrayofinputfieldsalreadyoutput)) {
 				if (in_array('search_' . $criteriafamilykey, $arrayofinputfieldsalreadyoutput)) {
@@ -10506,12 +10552,23 @@ class Form
 
 
 		$ret .= '</div>';
 		$ret .= '</div>';
 
 
-		$ret .= "<!-- Syntax of Generic filter string: t.ref:like:'SO-%', t.date_creation:<:'20160101', t.date_creation:<:'2016-01-01 12:30:00', t.nature:is:NULL, t.field2:isnot:NULL -->\n";
+		$ret .= "<!-- Field to enter a generic filter string: t.ref:like:'SO-%', t.date_creation:<:'20160101', t.date_creation:<:'2016-01-01 12:30:00', t.nature:is:NULL, t.field2:isnot:NULL -->\n";
 		$ret .= '<input type="text" placeholder="' . $langs->trans("Search") . '" name="search_component_params_input" class="noborderbottom search_component_input" value="">';
 		$ret .= '<input type="text" placeholder="' . $langs->trans("Search") . '" name="search_component_params_input" class="noborderbottom search_component_input" value="">';
 
 
 		$ret .= '</div>';
 		$ret .= '</div>';
 		$ret .= '</div>';
 		$ret .= '</div>';
 
 
+		$ret .= '<script>
+		jQuery(".tagsearchdelete").click(function() {
+			var filterid = $(this).parents().data("ufilterid");
+			console.log("We click to delete a criteria nb "+filterid);
+			// TODO Update the search_component_params_hidden with all data-ufilter except the one delete and post page
+
+		});
+		</script>
+		';
+
+
 		return $ret;
 		return $ret;
 	}
 	}
 
 

+ 6 - 6
htdocs/core/class/notify.class.php

@@ -547,7 +547,7 @@ class Notify
 								break;
 								break;
 							case 'ORDER_SUPPLIER_VALIDATE':
 							case 'ORDER_SUPPLIER_VALIDATE':
 								$link = '<a href="'.$urlwithroot.'/fourn/commande/card.php?id='.$object->id.'&entity='.$object->entity.'">'.$newref.'</a>';
 								$link = '<a href="'.$urlwithroot.'/fourn/commande/card.php?id='.$object->id.'&entity='.$object->entity.'">'.$newref.'</a>';
-								$dir_output = $conf->fournisseur->commande->dir_output;
+								$dir_output = $conf->fournisseur->commande->multidir_output[$object->entity]."/".get_exdir(0, 0, 0, 1, $object);
 								$object_type = 'order_supplier';
 								$object_type = 'order_supplier';
 								$mesg = $outputlangs->transnoentitiesnoconv("Hello").",\n\n";
 								$mesg = $outputlangs->transnoentitiesnoconv("Hello").",\n\n";
 								$mesg .= $outputlangs->transnoentitiesnoconv("EMailTextOrderValidatedBy", $link, $user->getFullName($outputlangs));
 								$mesg .= $outputlangs->transnoentitiesnoconv("EMailTextOrderValidatedBy", $link, $user->getFullName($outputlangs));
@@ -555,7 +555,7 @@ class Notify
 								break;
 								break;
 							case 'ORDER_SUPPLIER_APPROVE':
 							case 'ORDER_SUPPLIER_APPROVE':
 								$link = '<a href="'.$urlwithroot.'/fourn/commande/card.php?id='.$object->id.'&entity='.$object->entity.'">'.$newref.'</a>';
 								$link = '<a href="'.$urlwithroot.'/fourn/commande/card.php?id='.$object->id.'&entity='.$object->entity.'">'.$newref.'</a>';
-								$dir_output = $conf->fournisseur->commande->dir_output;
+								$dir_output = $conf->fournisseur->commande->multidir_output[$object->entity]."/".get_exdir(0, 0, 0, 1, $object);
 								$object_type = 'order_supplier';
 								$object_type = 'order_supplier';
 								$mesg = $outputlangs->transnoentitiesnoconv("Hello").",\n\n";
 								$mesg = $outputlangs->transnoentitiesnoconv("Hello").",\n\n";
 								$mesg .= $outputlangs->transnoentitiesnoconv("EMailTextOrderApprovedBy", $link, $user->getFullName($outputlangs));
 								$mesg .= $outputlangs->transnoentitiesnoconv("EMailTextOrderApprovedBy", $link, $user->getFullName($outputlangs));
@@ -563,7 +563,7 @@ class Notify
 								break;
 								break;
 							case 'ORDER_SUPPLIER_REFUSE':
 							case 'ORDER_SUPPLIER_REFUSE':
 								$link = '<a href="'.$urlwithroot.'/fourn/commande/card.php?id='.$object->id.'&entity='.$object->entity.'">'.$newref.'</a>';
 								$link = '<a href="'.$urlwithroot.'/fourn/commande/card.php?id='.$object->id.'&entity='.$object->entity.'">'.$newref.'</a>';
-								$dir_output = $conf->fournisseur->commande->dir_output;
+								$dir_output = $conf->fournisseur->commande->multidir_output[$object->entity]."/".get_exdir(0, 0, 0, 1, $object);
 								$object_type = 'order_supplier';
 								$object_type = 'order_supplier';
 								$mesg = $outputlangs->transnoentitiesnoconv("Hello").",\n\n";
 								$mesg = $outputlangs->transnoentitiesnoconv("Hello").",\n\n";
 								$mesg .= $outputlangs->transnoentitiesnoconv("EMailTextOrderRefusedBy", $link, $user->getFullName($outputlangs));
 								$mesg .= $outputlangs->transnoentitiesnoconv("EMailTextOrderRefusedBy", $link, $user->getFullName($outputlangs));
@@ -815,7 +815,7 @@ class Notify
 						break;
 						break;
 					case 'ORDER_SUPPLIER_VALIDATE':
 					case 'ORDER_SUPPLIER_VALIDATE':
 						$link = '<a href="'.$urlwithroot.'/fourn/commande/card.php?id='.$object->id.'&entity='.$object->entity.'">'.$newref.'</a>';
 						$link = '<a href="'.$urlwithroot.'/fourn/commande/card.php?id='.$object->id.'&entity='.$object->entity.'">'.$newref.'</a>';
-						$dir_output = $conf->fournisseur->commande->dir_output;
+						$dir_output = $conf->fournisseur->commande->multidir_output[$object->entity]."/".get_exdir(0, 0, 0, 1, $object);
 						$object_type = 'order_supplier';
 						$object_type = 'order_supplier';
 						$mesg = $langs->transnoentitiesnoconv("Hello").",\n\n";
 						$mesg = $langs->transnoentitiesnoconv("Hello").",\n\n";
 						$mesg .= $langs->transnoentitiesnoconv("EMailTextOrderValidatedBy", $link, $user->getFullName($langs));
 						$mesg .= $langs->transnoentitiesnoconv("EMailTextOrderValidatedBy", $link, $user->getFullName($langs));
@@ -823,7 +823,7 @@ class Notify
 						break;
 						break;
 					case 'ORDER_SUPPLIER_APPROVE':
 					case 'ORDER_SUPPLIER_APPROVE':
 						$link = '<a href="'.$urlwithroot.'/fourn/commande/card.php?id='.$object->id.'&entity='.$object->entity.'">'.$newref.'</a>';
 						$link = '<a href="'.$urlwithroot.'/fourn/commande/card.php?id='.$object->id.'&entity='.$object->entity.'">'.$newref.'</a>';
-						$dir_output = $conf->fournisseur->commande->dir_output;
+						$dir_output = $conf->fournisseur->commande->multidir_output[$object->entity]."/".get_exdir(0, 0, 0, 1, $object);
 						$object_type = 'order_supplier';
 						$object_type = 'order_supplier';
 						$mesg = $langs->transnoentitiesnoconv("Hello").",\n\n";
 						$mesg = $langs->transnoentitiesnoconv("Hello").",\n\n";
 						$mesg .= $langs->transnoentitiesnoconv("EMailTextOrderApprovedBy", $link, $user->getFullName($langs));
 						$mesg .= $langs->transnoentitiesnoconv("EMailTextOrderApprovedBy", $link, $user->getFullName($langs));
@@ -831,7 +831,7 @@ class Notify
 						break;
 						break;
 					case 'ORDER_SUPPLIER_APPROVE2':
 					case 'ORDER_SUPPLIER_APPROVE2':
 						$link = '<a href="'.$urlwithroot.'/fourn/commande/card.php?id='.$object->id.'&entity='.$object->entity.'">'.$newref.'</a>';
 						$link = '<a href="'.$urlwithroot.'/fourn/commande/card.php?id='.$object->id.'&entity='.$object->entity.'">'.$newref.'</a>';
-						$dir_output = $conf->fournisseur->commande->dir_output;
+						$dir_output = $conf->fournisseur->commande->multidir_output[$object->entity]."/".get_exdir(0, 0, 0, 1, $object);
 						$object_type = 'order_supplier';
 						$object_type = 'order_supplier';
 						$mesg = $langs->transnoentitiesnoconv("Hello").",\n\n";
 						$mesg = $langs->transnoentitiesnoconv("Hello").",\n\n";
 						$mesg .= $langs->transnoentitiesnoconv("EMailTextOrderApprovedBy", $link, $user->getFullName($langs));
 						$mesg .= $langs->transnoentitiesnoconv("EMailTextOrderApprovedBy", $link, $user->getFullName($langs));

+ 251 - 86
htdocs/core/customreports.php

@@ -152,7 +152,7 @@ if ($user->socid > 0) {	// Protection if external user
 }
 }
 
 
 // Fetch optionals attributes and labels
 // Fetch optionals attributes and labels
-$extrafields->fetch_name_optionals_label($object->table_element);
+$extrafields->fetch_name_optionals_label('all');	// We load all extrafields definitions for all objects
 //$extrafields->fetch_name_optionals_label($object->table_element_line);
 //$extrafields->fetch_name_optionals_label($object->table_element_line);
 
 
 $search_array_options = $extrafields->getOptionalsFromPost($object->table_element, '', 'search_');
 $search_array_options = $extrafields->getOptionalsFromPost($object->table_element, '', 'search_');
@@ -277,8 +277,18 @@ if (is_array($search_groupby) && count($search_groupby)) {
 
 
 		$sql = "SELECT DISTINCT ".$fieldtocount." as val";
 		$sql = "SELECT DISTINCT ".$fieldtocount." as val";
 
 
-		if (strpos($fieldtocount, 'te.') === 0) {
-			$sql .= " FROM ".MAIN_DB_PREFIX.$object->table_element."_extrafields as te";
+		if (strpos($fieldtocount, 'te') === 0) {
+			$tabletouse = $object->table_element;
+			$tablealiastouse = 'te';
+			if (!empty($arrayofgroupby[$gval])) {
+				$tmpval = explode('.', $gval);
+				$tabletouse = $arrayofgroupby[$gval]['table'];
+				$tablealiastouse = $tmpval[0];
+			}
+			//var_dump($tablealiastouse);exit;
+
+			//$sql .= " FROM ".MAIN_DB_PREFIX.$object->table_element."_extrafields as te";
+			$sql .= " FROM ".MAIN_DB_PREFIX.$tabletouse."_extrafields as ".$tablealiastouse;
 		} else {
 		} else {
 			$tabletouse = $object->table_element;
 			$tabletouse = $object->table_element;
 			$tablealiastouse = 't';
 			$tablealiastouse = 't';
@@ -290,9 +300,9 @@ if (is_array($search_groupby) && count($search_groupby)) {
 			$sql .= " FROM ".MAIN_DB_PREFIX.$tabletouse." as ".$tablealiastouse;
 			$sql .= " FROM ".MAIN_DB_PREFIX.$tabletouse." as ".$tablealiastouse;
 		}
 		}
 
 
-		// Add the where here
-		/*
-		$sqlfilters = GETPOST('search_component_params_hidden', 'alphanohtml');
+		// Add a where here keeping only the citeria on $tabletouse
+		// TODO
+		/*$sqlfilters = ... GETPOST('search_component_params_hidden', 'alphanohtml');
 		if ($sqlfilters) {
 		if ($sqlfilters) {
 			$errormessage = '';
 			$errormessage = '';
 			$sql .= forgeSQLFromUniversalSearchCriteria($sqlfilters, $errormessage);
 			$sql .= forgeSQLFromUniversalSearchCriteria($sqlfilters, $errormessage);
@@ -358,7 +368,8 @@ if (is_array($search_groupby) && count($search_groupby)) {
 		// Add a protection/error to refuse the request if number of differentr values for the group by is higher than $MAXUNIQUEVALFORGROUP
 		// Add a protection/error to refuse the request if number of differentr values for the group by is higher than $MAXUNIQUEVALFORGROUP
 		if (count($arrayofvaluesforgroupby['g_'.$gkey]) > $MAXUNIQUEVALFORGROUP) {
 		if (count($arrayofvaluesforgroupby['g_'.$gkey]) > $MAXUNIQUEVALFORGROUP) {
 			$langs->load("errors");
 			$langs->load("errors");
-			if (strpos($fieldtocount, 'te.') === 0) {			// This is an extrafield
+
+			if (strpos($fieldtocount, 'te') === 0) {			// This is a field of an extrafield
 				//if (!empty($extrafields->attributes[$object->table_element]['langfile'][$gvalwithoutprefix])) {
 				//if (!empty($extrafields->attributes[$object->table_element]['langfile'][$gvalwithoutprefix])) {
 				//      $langs->load($extrafields->attributes[$object->table_element]['langfile'][$gvalwithoutprefix]);
 				//      $langs->load($extrafields->attributes[$object->table_element]['langfile'][$gvalwithoutprefix]);
 				//}
 				//}
@@ -367,10 +378,13 @@ if (is_array($search_groupby) && count($search_groupby)) {
 			} elseif (strpos($fieldtocount, 't__') === 0) {		// This is a field of a foreign key
 			} elseif (strpos($fieldtocount, 't__') === 0) {		// This is a field of a foreign key
 				$reg = array();
 				$reg = array();
 				if (preg_match('/^(.*)\.(.*)/', $gvalwithoutprefix, $reg)) {
 				if (preg_match('/^(.*)\.(.*)/', $gvalwithoutprefix, $reg)) {
+					/*
 					$gvalwithoutprefix = preg_replace('/\..*$/', '', $gvalwithoutprefix);
 					$gvalwithoutprefix = preg_replace('/\..*$/', '', $gvalwithoutprefix);
-					$gvalwithoutprefix = preg_replace('/t__/', '', $gvalwithoutprefix);
+					$gvalwithoutprefix = preg_replace('/^t__/', '', $gvalwithoutprefix);
 					$keyforlabeloffield = $object->fields[$gvalwithoutprefix]['label'];
 					$keyforlabeloffield = $object->fields[$gvalwithoutprefix]['label'];
 					$labeloffield = $langs->transnoentitiesnoconv($keyforlabeloffield).'-'.$reg[2];
 					$labeloffield = $langs->transnoentitiesnoconv($keyforlabeloffield).'-'.$reg[2];
+					*/
+					$labeloffield = $arrayofgroupby[$fieldtocount]['labelnohtml'];
 				} else {
 				} else {
 					$labeloffield = $langs->transnoentitiesnoconv($keyforlabeloffield);
 					$labeloffield = $langs->transnoentitiesnoconv($keyforlabeloffield);
 				}
 				}
@@ -385,7 +399,6 @@ if (is_array($search_groupby) && count($search_groupby)) {
 					$labeloffield = $langs->transnoentitiesnoconv($keyforlabeloffield);
 					$labeloffield = $langs->transnoentitiesnoconv($keyforlabeloffield);
 				}
 				}
 			}
 			}
-			//var_dump($gkey.' '.$gval.' '.$gvalwithoutprefix.' '.$fieldtocount.' '.$keyforlabeloffield);
 			//var_dump($object->fields);
 			//var_dump($object->fields);
 			setEventMessages($langs->trans("ErrorTooManyDifferentValueForSelectedGroupBy", $MAXUNIQUEVALFORGROUP, $labeloffield), null, 'warnings');
 			setEventMessages($langs->trans("ErrorTooManyDifferentValueForSelectedGroupBy", $MAXUNIQUEVALFORGROUP, $labeloffield), null, 'warnings');
 			$search_groupby = array();
 			$search_groupby = array();
@@ -405,7 +418,7 @@ $startyear = $endyear - 2;
 
 
 $param = '';
 $param = '';
 
 
-print '<form method="post" action="'.$_SERVER['PHP_SELF'].'">';
+print '<form method="post" action="'.$_SERVER['PHP_SELF'].'" autocomplete="off">';
 print '<input type="hidden" name="token" value="'.newToken().'">';
 print '<input type="hidden" name="token" value="'.newToken().'">';
 print '<input type="hidden" name="action" value="viewgraph">';
 print '<input type="hidden" name="action" value="viewgraph">';
 print '<input type="hidden" name="tabfamily" value="'.$tabfamily.'">';
 print '<input type="hidden" name="tabfamily" value="'.$tabfamily.'">';
@@ -452,12 +465,12 @@ if (empty($conf->use_javascript_ajax)) {
 }
 }
 print '</div><div class="clearboth"></div>';
 print '</div><div class="clearboth"></div>';
 
 
-// Add Filter (you can use param &show_search_component_params_hidden=1 for debug)
+// Filter (you can use param &show_search_component_params_hidden=1 for debug)
 print '<div class="divadvancedsearchfield quatrevingtpercent">';
 print '<div class="divadvancedsearchfield quatrevingtpercent">';
 print $form->searchComponent(array($object->element => $object->fields), $search_component_params, array(), $search_component_params_hidden);
 print $form->searchComponent(array($object->element => $object->fields), $search_component_params, array(), $search_component_params_hidden);
 print '</div>';
 print '</div>';
 
 
-// Add measures into array
+// YAxis (add measures into array)
 $count = 0;
 $count = 0;
 //var_dump($arrayofmesures);
 //var_dump($arrayofmesures);
 print '<div class="divadvancedsearchfield clearboth">';
 print '<div class="divadvancedsearchfield clearboth">';
@@ -559,6 +572,8 @@ print '</form>';
 // Generate the SQL request
 // Generate the SQL request
 $sql = '';
 $sql = '';
 if (!empty($search_measures) && !empty($search_xaxis)) {
 if (!empty($search_measures) && !empty($search_xaxis)) {
+	$errormessage = '';
+
 	$fieldid = 'rowid';
 	$fieldid = 'rowid';
 
 
 	$sql = "SELECT ";
 	$sql = "SELECT ";
@@ -613,7 +628,7 @@ if (!empty($search_measures) && !empty($search_xaxis)) {
 	if ($object->isextrafieldmanaged) {
 	if ($object->isextrafieldmanaged) {
 		$sql .= " LEFT JOIN ".MAIN_DB_PREFIX.$object->table_element."_extrafields as te ON te.fk_object = t.".$fieldid;
 		$sql .= " LEFT JOIN ".MAIN_DB_PREFIX.$object->table_element."_extrafields as te ON te.fk_object = t.".$fieldid;
 	}
 	}
-	// Add table for link for multientity
+	// Add table for link on multientity
 	if ($object->ismultientitymanaged) {	// 0=No test on entity, 1=Test with field entity, 'field@table'=Test with link by field@table
 	if ($object->ismultientitymanaged) {	// 0=No test on entity, 1=Test with field entity, 'field@table'=Test with link by field@table
 		if ($object->ismultientitymanaged == 1) {
 		if ($object->ismultientitymanaged == 1) {
 			// No table to add here
 			// No table to add here
@@ -624,6 +639,7 @@ if (!empty($search_measures) && !empty($search_xaxis)) {
 		}
 		}
 	}
 	}
 
 
+	// Init the list of tables added. We include by default always the main table.
 	$listoftablesalreadyadded = array($object->table_element => $object->table_element);
 	$listoftablesalreadyadded = array($object->table_element => $object->table_element);
 
 
 	// Add LEFT JOIN for all parent tables mentionned into the Xaxis
 	// Add LEFT JOIN for all parent tables mentionned into the Xaxis
@@ -631,13 +647,25 @@ if (!empty($search_measures) && !empty($search_xaxis)) {
 	foreach ($search_xaxis as $key => $val) {
 	foreach ($search_xaxis as $key => $val) {
 		if (!empty($arrayofxaxis[$val])) {
 		if (!empty($arrayofxaxis[$val])) {
 			$tmpval = explode('.', $val);
 			$tmpval = explode('.', $val);
-			//var_dump($arrayofxaxis[$val]['table']);
-			if (! in_array($arrayofxaxis[$val]['table'], $listoftablesalreadyadded)) {	// We do not add join for main table already added
-				$sql .= " LEFT JOIN ".MAIN_DB_PREFIX.$arrayofxaxis[$val]['table']." as ".$db->sanitize($tmpval[0])." ON t.".str_replace('t__', '', $db->sanitize($tmpval[0]))." = ".$db->sanitize($tmpval[0]).".rowid";
-				$listoftablesalreadyadded[$arrayofxaxis[$val]['table']] = $arrayofxaxis[$val]['table'];
+			//var_dump($arrayofgroupby);
+			$tmpforloop = dolExplodeIntoArray($arrayofxaxis[$val]['tablefromt'], ',');
+			foreach ($tmpforloop as $tmptable => $tmptablealias) {
+				if (! in_array($tmptable, $listoftablesalreadyadded)) {	// We do not add join for main table and tables already added
+					$tmpforexplode = explode('__', $tmptablealias);
+					$endpart = end($tmpforexplode);
+					$parenttableandfield = preg_replace('/__'.$endpart.'$/', '', $tmptablealias).'.'.$endpart;
+
+					$sql .= " LEFT JOIN ".MAIN_DB_PREFIX.$tmptable." as ".$db->sanitize($tmptablealias)." ON ".$db->sanitize($parenttableandfield)." = ".$db->sanitize($tmptablealias).".rowid";
+					$listoftablesalreadyadded[$tmptable] = $tmptable;
+
+					if (preg_match('/^te/', $tmpval[0]) && preg_replace('/^t_/', 'te_', $tmptablealias) == $tmpval[0]) {
+						$sql .= " LEFT JOIN ".MAIN_DB_PREFIX.$tmptable."_extrafields as ".$db->sanitize($tmpval[0])." ON ".$db->sanitize($tmpval[0]).".fk_object = ".$db->sanitize($tmptablealias).".rowid";
+						$listoftablesalreadyadded[$tmptable] = $tmptable;
+					}
+				}
 			}
 			}
 		} else {
 		} else {
-			dol_print_error($db, 'Found a key into search_xaxis not found into arrayofxaxis');
+			$errormessage = 'Found a key into search_xaxis not found into arrayofxaxis';
 		}
 		}
 	}
 	}
 
 
@@ -646,13 +674,25 @@ if (!empty($search_measures) && !empty($search_xaxis)) {
 	foreach ($search_groupby as $key => $val) {
 	foreach ($search_groupby as $key => $val) {
 		if (!empty($arrayofgroupby[$val])) {
 		if (!empty($arrayofgroupby[$val])) {
 			$tmpval = explode('.', $val);
 			$tmpval = explode('.', $val);
-			//var_dump($arrayofxaxis[$val]['table']);
-			if (! in_array($arrayofgroupby[$val]['table'], $listoftablesalreadyadded)) {	// We do not add join for main table already added
-				$sql .= " LEFT JOIN ".MAIN_DB_PREFIX.$arrayofgroupby[$val]['table']." as ".$tmpval[0]." ON t.".str_replace('t__', '', $tmpval[0])." = ".$tmpval[0].".rowid";
-				$listoftablesalreadyadded[$arrayofgroupby[$val]['table']] = $arrayofgroupby[$val]['table'];
+			//var_dump($arrayofgroupby[$val]); var_dump($tmpval);
+			$tmpforloop = dolExplodeIntoArray($arrayofgroupby[$val]['tablefromt'], ',');
+			foreach ($tmpforloop as $tmptable => $tmptablealias) {
+				if (! in_array($tmptable, $listoftablesalreadyadded)) {	// We do not add join for main table and tables already added
+					$tmpforexplode = explode('__', $tmptablealias);
+					$endpart = end($tmpforexplode);
+					$parenttableandfield = preg_replace('/__'.$endpart.'$/', '', $tmptablealias).'.'.$endpart;
+
+					$sql .= " LEFT JOIN ".MAIN_DB_PREFIX.$tmptable." as ".$db->sanitize($tmptablealias)." ON ".$db->sanitize($parenttableandfield)." = ".$db->sanitize($tmptablealias).".rowid";
+					$listoftablesalreadyadded[$tmptable] = $tmptable;
+
+					if (preg_match('/^te/', $tmpval[0]) && preg_replace('/^t_/', 'te_', $tmptablealias) == $tmpval[0]) {
+						$sql .= " LEFT JOIN ".MAIN_DB_PREFIX.$tmptable."_extrafields as ".$db->sanitize($tmpval[0])." ON ".$db->sanitize($tmpval[0]).".fk_object = ".$db->sanitize($tmptablealias).".rowid";
+						$listoftablesalreadyadded[$tmptable] = $tmptable;
+					}
+				}
 			}
 			}
 		} else {
 		} else {
-			dol_print_error($db, 'Found a key into search_groupby not found into arrayofgroupby');
+			$errormessage = 'Found a key into search_groupby not found into arrayofgroupby';
 		}
 		}
 	}
 	}
 
 
@@ -661,13 +701,25 @@ if (!empty($search_measures) && !empty($search_xaxis)) {
 	foreach ($search_measures as $key => $val) {
 	foreach ($search_measures as $key => $val) {
 		if (!empty($arrayofmesures[$val])) {
 		if (!empty($arrayofmesures[$val])) {
 			$tmpval = explode('.', $val);
 			$tmpval = explode('.', $val);
-			//var_dump($arrayofxaxis[$val]['table']);
-			if (! in_array($arrayofmesures[$val]['table'], $listoftablesalreadyadded)) {	// We do not add join for main table already added
-				$sql .= " LEFT JOIN ".MAIN_DB_PREFIX.$arrayofmesures[$val]['table']." as ".$tmpval[0]." ON t.".str_replace('t__', '', $tmpval[0])." = ".$tmpval[0].".rowid";
-				$listoftablesalreadyadded[$arrayofmesures[$val]['table']] = $arrayofmesures[$val]['table'];
+			//var_dump($arrayofgroupby);
+			$tmpforloop = dolExplodeIntoArray($arrayofmesures[$val]['tablefromt'], ',');
+			foreach ($tmpforloop as $tmptable => $tmptablealias) {
+				if (! in_array($tmptable, $listoftablesalreadyadded)) {	// We do not add join for main table and tables already added
+					$tmpforexplode = explode('__', $tmptablealias);
+					$endpart = end($tmpforexplode);
+					$parenttableandfield = preg_replace('/__'.$endpart.'$/', '', $tmptablealias).'.'.$endpart;
+
+					$sql .= " LEFT JOIN ".MAIN_DB_PREFIX.$tmptable." as ".$db->sanitize($tmptablealias)." ON ".$db->sanitize($parenttableandfield)." = ".$db->sanitize($tmptablealias).".rowid";
+					$listoftablesalreadyadded[$tmptable] = $tmptable;
+
+					if (preg_match('/^te/', $tmpval[0]) && preg_replace('/^t_/', 'te_', $tmptablealias) == $tmpval[0]) {
+						$sql .= " LEFT JOIN ".MAIN_DB_PREFIX.$tmptable."_extrafields as ".$db->sanitize($tmpval[0])." ON ".$db->sanitize($tmpval[0]).".fk_object = ".$db->sanitize($tmptablealias).".rowid";
+						$listoftablesalreadyadded[$tmptable] = $tmptable;
+					}
+				}
 			}
 			}
 		} else {
 		} else {
-			dol_print_error($db, 'Found a key into search_measures not found into arrayofmesures');
+			$errormessage = 'Found a key into search_measures not found into arrayofmesures';
 		}
 		}
 	}
 	}
 
 
@@ -678,11 +730,7 @@ if (!empty($search_measures) && !empty($search_xaxis)) {
 	// Add the where here
 	// Add the where here
 	$sqlfilters = $search_component_params_hidden;
 	$sqlfilters = $search_component_params_hidden;
 	if ($sqlfilters) {
 	if ($sqlfilters) {
-		$errormessage = '';
-		$sql .= forgeSQLFromUniversalSearchCriteria($sqlfilters, $errormessage);
-		if ($errormessage) {
-			print dol_escape_htmltag($errormessage);
-		}
+		$sql .= forgeSQLFromUniversalSearchCriteria($sqlfilters, $errormessage, 0, 0, 1);
 	}
 	}
 	$sql .= " GROUP BY ";
 	$sql .= " GROUP BY ";
 	foreach ($search_xaxis as $key => $val) {
 	foreach ($search_xaxis as $key => $val) {
@@ -747,6 +795,11 @@ if (!empty($search_measures) && !empty($search_xaxis)) {
 }
 }
 //print $sql;
 //print $sql;
 
 
+if ($errormessage) {
+	print dol_escape_htmltag($errormessage);
+	$sql = '';
+}
+
 $legend = array();
 $legend = array();
 foreach ($search_measures as $key => $val) {
 foreach ($search_measures as $key => $val) {
 	$legend[] = $langs->trans($arrayofmesures[$val]['label']);
 	$legend[] = $langs->trans($arrayofmesures[$val]['label']);
@@ -952,7 +1005,6 @@ $db->close();
 
 
 
 
 
 
-
 /**
 /**
  * Fill arrayofmesures for an object
  * Fill arrayofmesures for an object
  *
  *
@@ -962,9 +1014,10 @@ $db->close();
  * @param	array		$arrayofmesures	Array of mesures already filled
  * @param	array		$arrayofmesures	Array of mesures already filled
  * @param	int			$level 			Level
  * @param	int			$level 			Level
  * @param	int			$count			Count
  * @param	int			$count			Count
+ * @param	string		$tablepath		Path of all tables ('t' or 't,contract' or 't,contract,societe'...)
  * @return 	array						Array of mesures
  * @return 	array						Array of mesures
  */
  */
-function fillArrayOfMeasures($object, $tablealias, $labelofobject, &$arrayofmesures, $level = 0, &$count = 0)
+function fillArrayOfMeasures($object, $tablealias, $labelofobject, &$arrayofmesures, $level = 0, &$count = 0, &$tablepath = '')
 {
 {
 	global $langs, $extrafields, $db;
 	global $langs, $extrafields, $db;
 
 
@@ -972,38 +1025,56 @@ function fillArrayOfMeasures($object, $tablealias, $labelofobject, &$arrayofmesu
 		return $arrayofmesures;
 		return $arrayofmesures;
 	}
 	}
 
 
+	if (empty($tablepath)) {
+		$tablepath = $object->table_element.'='.$tablealias;
+	} else {
+		$tablepath .= ','.$object->table_element.'='.$tablealias;
+	}
+
 	if ($level == 0) {
 	if ($level == 0) {
 		// Add the count of record only for the main/first level object. Parents are necessarly unique for each record.
 		// Add the count of record only for the main/first level object. Parents are necessarly unique for each record.
 		$arrayofmesures[$tablealias.'.count'] = array(
 		$arrayofmesures[$tablealias.'.count'] = array(
 			'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': Count',
 			'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': Count',
+			'labelnohtml' => $labelofobject.': Count',
 			'position' => 0,
 			'position' => 0,
-			'table' => $object->table_element
+			'table' => $object->table_element,
+			'tablefromt' => $tablepath
 		);
 		);
 	}
 	}
 
 
+	// Note: here $tablealias can be 't' or 't__fk_contract' or 't_fk_contract_fk_soc'
+
 	// Add main fields of object
 	// Add main fields of object
 	foreach ($object->fields as $key => $val) {
 	foreach ($object->fields as $key => $val) {
 		if (!empty($val['isameasure']) && (!isset($val['enabled']) || dol_eval($val['enabled'], 1, 1, '1'))) {
 		if (!empty($val['isameasure']) && (!isset($val['enabled']) || dol_eval($val['enabled'], 1, 1, '1'))) {
 			$position = (empty($val['position']) ? 0 : intVal($val['position']));
 			$position = (empty($val['position']) ? 0 : intVal($val['position']));
 			$arrayofmesures[$tablealias.'.'.$key.'-sum'] = array(
 			$arrayofmesures[$tablealias.'.'.$key.'-sum'] = array(
 				'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val['label']).' <span class="opacitymedium">('.$langs->trans("Sum").')</span>',
 				'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val['label']).' <span class="opacitymedium">('.$langs->trans("Sum").')</span>',
+				'labelnohtml' => $labelofobject.': '.$langs->trans($val['label']),
 				'position' => ($position + ($count * 100000)).'.1',
 				'position' => ($position + ($count * 100000)).'.1',
-				'table' => $object->table_element
+				'table' => $object->table_element,
+				'tablefromt' => $tablepath
 			);
 			);
 			$arrayofmesures[$tablealias.'.'.$key.'-average'] = array(
 			$arrayofmesures[$tablealias.'.'.$key.'-average'] = array(
 				'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val['label']).' <span class="opacitymedium">('.$langs->trans("Average").')</span>',
 				'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val['label']).' <span class="opacitymedium">('.$langs->trans("Average").')</span>',
+				'labelnohtml' => $labelofobject.': '.$langs->trans($val['label']),
 				'position' => ($position + ($count * 100000)).'.2',
 				'position' => ($position + ($count * 100000)).'.2',
-				'table' => $object->table_element
+				'table' => $object->table_element,
+				'tablefromt' => $tablepath
 			);
 			);
 			$arrayofmesures[$tablealias.'.'.$key.'-min'] = array(
 			$arrayofmesures[$tablealias.'.'.$key.'-min'] = array(
 				'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val['label']).' <span class="opacitymedium">('.$langs->trans("Minimum").')</span>',
 				'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val['label']).' <span class="opacitymedium">('.$langs->trans("Minimum").')</span>',
+				'labelnohtml' => $labelofobject.': '.$langs->trans($val['label']),
 				'position' => ($position + ($count * 100000)).'.3',
 				'position' => ($position + ($count * 100000)).'.3',
-				'table' => $object->table_element
+				'table' => $object->table_element,
+				'tablefromt' => $tablepath
 			);
 			);
 			$arrayofmesures[$tablealias.'.'.$key.'-max'] = array(
 			$arrayofmesures[$tablealias.'.'.$key.'-max'] = array(
 				'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val['label']).' <span class="opacitymedium">('.$langs->trans("Maximum").')</span>',
 				'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val['label']).' <span class="opacitymedium">('.$langs->trans("Maximum").')</span>',
+				'labelnohtml' => $labelofobject.': '.$langs->trans($val['label']),
 				'position' => ($position + ($count * 100000)).'.4',
 				'position' => ($position + ($count * 100000)).'.4',
-				'table' => $object->table_element
+				'table' => $object->table_element,
+				'tablefromt' => $tablepath
 			);
 			);
 		}
 		}
 	}
 	}
@@ -1012,25 +1083,33 @@ function fillArrayOfMeasures($object, $tablealias, $labelofobject, &$arrayofmesu
 		foreach ($extrafields->attributes[$object->table_element]['label'] as $key => $val) {
 		foreach ($extrafields->attributes[$object->table_element]['label'] as $key => $val) {
 			if (!empty($extrafields->attributes[$object->table_element]['totalizable'][$key]) && (!isset($extrafields->attributes[$object->table_element]['enabled'][$key]) || dol_eval($extrafields->attributes[$object->table_element]['enabled'][$key], 1, 1, '1'))) {
 			if (!empty($extrafields->attributes[$object->table_element]['totalizable'][$key]) && (!isset($extrafields->attributes[$object->table_element]['enabled'][$key]) || dol_eval($extrafields->attributes[$object->table_element]['enabled'][$key], 1, 1, '1'))) {
 				$position = (!empty($val['position']) ? $val['position'] : 0);
 				$position = (!empty($val['position']) ? $val['position'] : 0);
-				$arrayofmesures[$tablealias.'e.'.$key.'-sum'] = array(
+				$arrayofmesures[preg_replace('/^t/', 'te', $tablealias).'.'.$key.'-sum'] = array(
 					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($extrafields->attributes[$object->table_element]['label'][$key]).' <span class="opacitymedium">('.$langs->trans("Sum").')</span>',
 					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($extrafields->attributes[$object->table_element]['label'][$key]).' <span class="opacitymedium">('.$langs->trans("Sum").')</span>',
+					'labelnohtml' => $labelofobject.': '.$langs->trans($val),
 					'position' => ($position+($count * 100000)).'.1',
 					'position' => ($position+($count * 100000)).'.1',
-					'table' => $object->table_element
+					'table' => $object->table_element,
+					'tablefromt' => $tablepath
 				);
 				);
-				$arrayofmesures[$tablealias.'e.'.$key.'-average'] = array(
+				$arrayofmesures[preg_replace('/^t/', 'te', $tablealias).'.'.$key.'-average'] = array(
 					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($extrafields->attributes[$object->table_element]['label'][$key]).' <span class="opacitymedium">('.$langs->trans("Average").')</span>',
 					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($extrafields->attributes[$object->table_element]['label'][$key]).' <span class="opacitymedium">('.$langs->trans("Average").')</span>',
+					'labelnohtml' => $labelofobject.': '.$langs->trans($val),
 					'position' => ($position+($count * 100000)).'.2',
 					'position' => ($position+($count * 100000)).'.2',
-					'table' => $object->table_element
+					'table' => $object->table_element,
+					'tablefromt' => $tablepath
 				);
 				);
-				$arrayofmesures[$tablealias.'e.'.$key.'-min'] = array(
+				$arrayofmesures[preg_replace('/^t/', 'te', $tablealias).'.'.$key.'-min'] = array(
 					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($extrafields->attributes[$object->table_element]['label'][$key]).' <span class="opacitymedium">('.$langs->trans("Minimum").')</span>',
 					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($extrafields->attributes[$object->table_element]['label'][$key]).' <span class="opacitymedium">('.$langs->trans("Minimum").')</span>',
+					'labelnohtml' => $labelofobject.': '.$langs->trans($val),
 					'position' => ($position+($count * 100000)).'.3',
 					'position' => ($position+($count * 100000)).'.3',
-					'table' => $object->table_element
+					'table' => $object->table_element,
+					'tablefromt' => $tablepath
 				);
 				);
-				$arrayofmesures[$tablealias.'e.'.$key.'-max'] = array(
+				$arrayofmesures[preg_replace('/^t/', 'te', $tablealias).'.'.$key.'-max'] = array(
 					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($extrafields->attributes[$object->table_element]['label'][$key]).' <span class="opacitymedium">('.$langs->trans("Maximum").')</span>',
 					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($extrafields->attributes[$object->table_element]['label'][$key]).' <span class="opacitymedium">('.$langs->trans("Maximum").')</span>',
+					'labelnohtml' => $labelofobject.': '.$langs->trans($val),
 					'position' => ($position+($count * 100000)).'.4',
 					'position' => ($position+($count * 100000)).'.4',
-					'table' => $object->table_element
+					'table' => $object->table_element,
+					'tablefromt' => $tablepath
 				);
 				);
 			}
 			}
 		}
 		}
@@ -1039,14 +1118,14 @@ function fillArrayOfMeasures($object, $tablealias, $labelofobject, &$arrayofmesu
 	foreach ($object->fields as $key => $val) {
 	foreach ($object->fields as $key => $val) {
 		if (preg_match('/^[^:]+:[^:]+:/', $val['type'])) {
 		if (preg_match('/^[^:]+:[^:]+:/', $val['type'])) {
 			$tmptype = explode(':', $val['type'], 4);
 			$tmptype = explode(':', $val['type'], 4);
-			if ($tmptype[0] == 'integer' && $tmptype[1] && $tmptype[2]) {
+			if ($tmptype[0] == 'integer' && !empty($tmptype[1]) && !empty($tmptype[2])) {
 				$newobject = $tmptype[1];
 				$newobject = $tmptype[1];
 				dol_include_once($tmptype[2]);
 				dol_include_once($tmptype[2]);
 				if (class_exists($newobject)) {
 				if (class_exists($newobject)) {
 					$tmpobject = new $newobject($db);
 					$tmpobject = new $newobject($db);
 					//var_dump($key); var_dump($tmpobject->element); var_dump($val['label']); var_dump($tmptype); var_dump('t-'.$key);
 					//var_dump($key); var_dump($tmpobject->element); var_dump($val['label']); var_dump($tmptype); var_dump('t-'.$key);
 					$count++;
 					$count++;
-					$arrayofmesures = fillArrayOfMeasures($tmpobject, $tablealias.'__'.$key, $langs->trans($val['label']), $arrayofmesures, $level + 1, $count);
+					$arrayofmesures = fillArrayOfMeasures($tmpobject, $tablealias.'__'.$key, $langs->trans($val['label']), $arrayofmesures, $level + 1, $count, $tablepath);
 				} else {
 				} else {
 					print 'For property '.$object->element.'->'.$key.', type="'.$val['type'].'": Failed to find class '.$newobject." in file ".$tmptype[2]."<br>\n";
 					print 'For property '.$object->element.'->'.$key.', type="'.$val['type'].'": Failed to find class '.$newobject." in file ".$tmptype[2]."<br>\n";
 				}
 				}
@@ -1067,18 +1146,21 @@ function fillArrayOfMeasures($object, $tablealias, $labelofobject, &$arrayofmesu
  * @param	array		$arrayofxaxis	Array of xaxis already filled
  * @param	array		$arrayofxaxis	Array of xaxis already filled
  * @param	int			$level 			Level
  * @param	int			$level 			Level
  * @param	int			$count			Count
  * @param	int			$count			Count
+ * @param	string		$tablepath		Path of all tables ('t' or 't,contract' or 't,contract,societe'...)
  * @return 	array						Array of xaxis
  * @return 	array						Array of xaxis
  */
  */
-function fillArrayOfXAxis($object, $tablealias, $labelofobject, &$arrayofxaxis, $level = 0, &$count = 0)
+function fillArrayOfXAxis($object, $tablealias, $labelofobject, &$arrayofxaxis, $level = 0, &$count = 0, &$tablepath = '')
 {
 {
 	global $langs, $extrafields, $db;
 	global $langs, $extrafields, $db;
 
 
-	if ($level > 10) {	// Protection against infinite loop
+	if ($level >= 3) {	// Limit scan on 2 levels max
 		return $arrayofxaxis;
 		return $arrayofxaxis;
 	}
 	}
 
 
-	if ($level >= 2) {
-		return $arrayofxaxis;
+	if (empty($tablepath)) {
+		$tablepath = $object->table_element.'='.$tablealias;
+	} else {
+		$tablepath .= ','.$object->table_element.'='.$tablealias;
 	}
 	}
 
 
 	$YYYY = substr($langs->trans("Year"), 0, 1).substr($langs->trans("Year"), 0, 1).substr($langs->trans("Year"), 0, 1).substr($langs->trans("Year"), 0, 1);
 	$YYYY = substr($langs->trans("Year"), 0, 1).substr($langs->trans("Year"), 0, 1).substr($langs->trans("Year"), 0, 1).substr($langs->trans("Year"), 0, 1);
@@ -1088,6 +1170,12 @@ function fillArrayOfXAxis($object, $tablealias, $labelofobject, &$arrayofxaxis,
 	$MI = substr($langs->trans("Minute"), 0, 1).substr($langs->trans("Minute"), 0, 1);
 	$MI = substr($langs->trans("Minute"), 0, 1).substr($langs->trans("Minute"), 0, 1);
 	$SS = substr($langs->trans("Second"), 0, 1).substr($langs->trans("Second"), 0, 1);
 	$SS = substr($langs->trans("Second"), 0, 1).substr($langs->trans("Second"), 0, 1);
 
 
+	/*if ($level > 0) {
+		var_dump($object->element.' '.$object->isextrafieldmanaged);
+	}*/
+
+	// Note: here $tablealias can be 't' or 't__fk_contract' or 't_fk_contract_fk_soc'
+
 	// Add main fields of object
 	// Add main fields of object
 	foreach ($object->fields as $key => $val) {
 	foreach ($object->fields as $key => $val) {
 		if (empty($val['measure'])) {
 		if (empty($val['measure'])) {
@@ -1115,25 +1203,33 @@ function fillArrayOfXAxis($object, $tablealias, $labelofobject, &$arrayofxaxis,
 				$position = (empty($val['position']) ? 0 : intVal($val['position']));
 				$position = (empty($val['position']) ? 0 : intVal($val['position']));
 				$arrayofxaxis[$tablealias.'.'.$key.'-year'] = array(
 				$arrayofxaxis[$tablealias.'.'.$key.'-year'] = array(
 					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val['label']).' <span class="opacitymedium">('.$YYYY.')</span>',
 					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val['label']).' <span class="opacitymedium">('.$YYYY.')</span>',
+					'labelnohtml' => $labelofobject.': '.$langs->trans($val['label']),
 					'position' => ($position + ($count * 100000)).'.1',
 					'position' => ($position + ($count * 100000)).'.1',
-					'table' => $object->table_element
+					'table' => $object->table_element,
+					'tablefromt' => $tablepath
 				);
 				);
 				$arrayofxaxis[$tablealias.'.'.$key.'-month'] = array(
 				$arrayofxaxis[$tablealias.'.'.$key.'-month'] = array(
 					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val['label']).' <span class="opacitymedium">('.$YYYY.'-'.$MM.')</span>',
 					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val['label']).' <span class="opacitymedium">('.$YYYY.'-'.$MM.')</span>',
+					'labelnohtml' => $labelofobject.': '.$langs->trans($val['label']),
 					'position' => ($position + ($count * 100000)).'.2',
 					'position' => ($position + ($count * 100000)).'.2',
-					'table' => $object->table_element
+					'table' => $object->table_element,
+					'tablefromt' => $tablepath
 				);
 				);
 				$arrayofxaxis[$tablealias.'.'.$key.'-day'] = array(
 				$arrayofxaxis[$tablealias.'.'.$key.'-day'] = array(
 					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val['label']).' <span class="opacitymedium">('.$YYYY.'-'.$MM.'-'.$DD.')</span>',
 					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val['label']).' <span class="opacitymedium">('.$YYYY.'-'.$MM.'-'.$DD.')</span>',
+					'labelnohtml' => $labelofobject.': '.$langs->trans($val['label']),
 					'position' => ($position + ($count * 100000)).'.3',
 					'position' => ($position + ($count * 100000)).'.3',
-					'table' => $object->table_element
+					'table' => $object->table_element,
+					'tablefromt' => $tablepath
 				);
 				);
 			} else {
 			} else {
 				$position = (empty($val['position']) ? 0 : intVal($val['position']));
 				$position = (empty($val['position']) ? 0 : intVal($val['position']));
 				$arrayofxaxis[$tablealias.'.'.$key] = array(
 				$arrayofxaxis[$tablealias.'.'.$key] = array(
 					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val['label']),
 					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val['label']),
+					'labelnohtml' => $labelofobject.': '.$langs->trans($val['label']),
 					'position' => ($position + ($count * 100000)),
 					'position' => ($position + ($count * 100000)),
-					'table' => $object->table_element
+					'table' => $object->table_element,
+					'tablefromt' => $tablepath
 				);
 				);
 			}
 			}
 		}
 		}
@@ -1148,11 +1244,39 @@ function fillArrayOfXAxis($object, $tablealias, $labelofobject, &$arrayofxaxis,
 			if (!empty($extrafields->attributes[$object->table_element]['totalizable'][$key])) {
 			if (!empty($extrafields->attributes[$object->table_element]['totalizable'][$key])) {
 				continue;
 				continue;
 			}
 			}
-			$arrayofxaxis[$tablealias.'e.'.$key] = array(
-				'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($extrafields->attributes[$object->table_element]['label'][$key]),
-				'position' => 1000 + (int) $extrafields->attributes[$object->table_element]['pos'][$key] + ($count * 100000),
-				'table' => $object->table_element
-			);
+
+			if (in_array($extrafields->attributes[$object->table_element]['type'][$key], array('timestamp', 'date', 'datetime'))) {
+				$position = (empty($extrafields->attributes[$object->table_element]['pos'][$key]) ? 0 : intVal($extrafields->attributes[$object->table_element]['pos'][$key]));
+				$arrayofxaxis[preg_replace('/^t/', 'te', $tablealias).'.'.$key.'-year'] = array(
+					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val).' <span class="opacitymedium">('.$YYYY.')</span>',
+					'labelnohtml' => $labelofobject.': '.$langs->trans($val),
+					'position' => ($position + ($count * 100000)).'.1',
+					'table' => $object->table_element,
+					'tablefromt' => $tablepath
+				);
+				$arrayofxaxis[preg_replace('/^t/', 'te', $tablealias).'.'.$key.'-month'] = array(
+					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val).' <span class="opacitymedium">('.$YYYY.'-'.$MM.')</span>',
+					'labelnohtml' => $labelofobject.': '.$langs->trans($val),
+					'position' => ($position + ($count * 100000)).'.2',
+					'table' => $object->table_element,
+					'tablefromt' => $tablepath
+				);
+				$arrayofxaxis[preg_replace('/^t/', 'te', $tablealias).'.'.$key.'-day'] = array(
+					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val).' <span class="opacitymedium">('.$YYYY.'-'.$MM.'-'.$DD.')</span>',
+					'labelnohtml' => $labelofobject.': '.$langs->trans($val),
+					'position' => ($position + ($count * 100000)).'.3',
+					'table' => $object->table_element,
+					'tablefromt' => $tablepath
+				);
+			} else {
+				$arrayofxaxis[preg_replace('/^t/', 'te', $tablealias).'.'.$key] = array(
+					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val),
+					'labelnohtml' => $labelofobject.': '.$langs->trans($val),
+					'position' => 1000 + (int) $extrafields->attributes[$object->table_element]['pos'][$key] + ($count * 100000),
+					'table' => $object->table_element,
+					'tablefromt' => $tablepath
+				);
+			}
 		}
 		}
 	}
 	}
 
 
@@ -1167,7 +1291,7 @@ function fillArrayOfXAxis($object, $tablealias, $labelofobject, &$arrayofxaxis,
 					$tmpobject = new $newobject($db);
 					$tmpobject = new $newobject($db);
 					//var_dump($key); var_dump($tmpobject->element); var_dump($val['label']); var_dump($tmptype); var_dump('t-'.$key);
 					//var_dump($key); var_dump($tmpobject->element); var_dump($val['label']); var_dump($tmptype); var_dump('t-'.$key);
 					$count++;
 					$count++;
-					$arrayofxaxis = fillArrayOfXAxis($tmpobject, $tablealias.'__'.$key, $langs->trans($val['label']), $arrayofxaxis, $level + 1, $count);
+					$arrayofxaxis = fillArrayOfXAxis($tmpobject, $tablealias.'__'.$key, $langs->trans($val['label']), $arrayofxaxis, $level + 1, $count, $tablepath);
 				} else {
 				} else {
 					print 'For property '.$object->element.'->'.$key.', type="'.$val['type'].'": Failed to find class '.$newobject." in file ".$tmptype[2]."<br>\n";
 					print 'For property '.$object->element.'->'.$key.', type="'.$val['type'].'": Failed to find class '.$newobject." in file ".$tmptype[2]."<br>\n";
 				}
 				}
@@ -1188,18 +1312,21 @@ function fillArrayOfXAxis($object, $tablealias, $labelofobject, &$arrayofxaxis,
  * @param	array		$arrayofgroupby	Array of groupby already filled
  * @param	array		$arrayofgroupby	Array of groupby already filled
  * @param	int			$level 			Level
  * @param	int			$level 			Level
  * @param	int			$count			Count
  * @param	int			$count			Count
+ * @param	string		$tablepath		Path of all tables ('t' or 't,contract' or 't,contract,societe'...)
  * @return 	array						Array of groupby
  * @return 	array						Array of groupby
  */
  */
-function fillArrayOfGroupBy($object, $tablealias, $labelofobject, &$arrayofgroupby, $level = 0, &$count = 0)
+function fillArrayOfGroupBy($object, $tablealias, $labelofobject, &$arrayofgroupby, $level = 0, &$count = 0, &$tablepath = '')
 {
 {
 	global $langs, $extrafields, $db;
 	global $langs, $extrafields, $db;
 
 
-	if ($level > 10) {	// Protection against infinite loop
+	if ($level >= 3) {
 		return $arrayofgroupby;
 		return $arrayofgroupby;
 	}
 	}
 
 
-	if ($level >= 2) {
-		return $arrayofgroupby;
+	if (empty($tablepath)) {
+		$tablepath = $object->table_element.'='.$tablealias;
+	} else {
+		$tablepath .= ','.$object->table_element.'='.$tablealias;
 	}
 	}
 
 
 	$YYYY = substr($langs->trans("Year"), 0, 1).substr($langs->trans("Year"), 0, 1).substr($langs->trans("Year"), 0, 1).substr($langs->trans("Year"), 0, 1);
 	$YYYY = substr($langs->trans("Year"), 0, 1).substr($langs->trans("Year"), 0, 1).substr($langs->trans("Year"), 0, 1).substr($langs->trans("Year"), 0, 1);
@@ -1209,6 +1336,8 @@ function fillArrayOfGroupBy($object, $tablealias, $labelofobject, &$arrayofgroup
 	$MI = substr($langs->trans("Minute"), 0, 1).substr($langs->trans("Minute"), 0, 1);
 	$MI = substr($langs->trans("Minute"), 0, 1).substr($langs->trans("Minute"), 0, 1);
 	$SS = substr($langs->trans("Second"), 0, 1).substr($langs->trans("Second"), 0, 1);
 	$SS = substr($langs->trans("Second"), 0, 1).substr($langs->trans("Second"), 0, 1);
 
 
+	// Note: here $tablealias can be 't' or 't__fk_contract' or 't_fk_contract_fk_soc'
+
 	// Add main fields of object
 	// Add main fields of object
 	foreach ($object->fields as $key => $val) {
 	foreach ($object->fields as $key => $val) {
 		if (empty($val['isameasure'])) {
 		if (empty($val['isameasure'])) {
@@ -1235,26 +1364,34 @@ function fillArrayOfGroupBy($object, $tablealias, $labelofobject, &$arrayofgroup
 			if (in_array($val['type'], array('timestamp', 'date', 'datetime'))) {
 			if (in_array($val['type'], array('timestamp', 'date', 'datetime'))) {
 				$position = (empty($val['position']) ? 0 : intVal($val['position']));
 				$position = (empty($val['position']) ? 0 : intVal($val['position']));
 				$arrayofgroupby[$tablealias.'.'.$key.'-year'] = array(
 				$arrayofgroupby[$tablealias.'.'.$key.'-year'] = array(
-					'label' => img_picto('', $object->picto,
-					'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val['label']).' <span class="opacitymedium">('.$YYYY.')</span>', 'position' => ($position + ($count * 100000)).'.1',
-					'table' => $object->table_element
+					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val['label']).' <span class="opacitymedium">('.$YYYY.')</span>',
+					'labelnohtml' => $labelofobject.': '.$langs->trans($val['label']),
+					'position' => ($position + ($count * 100000)).'.1',
+					'table' => $object->table_element,
+					'tablefromt' => $tablepath
 				);
 				);
 				$arrayofgroupby[$tablealias.'.'.$key.'-month'] = array(
 				$arrayofgroupby[$tablealias.'.'.$key.'-month'] = array(
-					'label' => img_picto('', $object->picto,
-					'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val['label']).' <span class="opacitymedium">('.$YYYY.'-'.$MM.')</span>', 'position' => ($position + ($count * 100000)).'.2',
-					'table' => $object->table_element
+					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val['label']).' <span class="opacitymedium">('.$YYYY.'-'.$MM.')</span>',
+					'labelnohtml' => $labelofobject.': '.$langs->trans($val['label']),
+					'position' => ($position + ($count * 100000)).'.2',
+					'table' => $object->table_element,
+					'tablefromt' => $tablepath
 				);
 				);
 				$arrayofgroupby[$tablealias.'.'.$key.'-day'] = array(
 				$arrayofgroupby[$tablealias.'.'.$key.'-day'] = array(
-					'label' => img_picto('', $object->picto,
-					'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val['label']).' <span class="opacitymedium">('.$YYYY.'-'.$MM.'-'.$DD.')</span>', 'position' => ($position + ($count * 100000)).'.3',
-					'table' => $object->table_element
+					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val['label']).' <span class="opacitymedium">('.$YYYY.'-'.$MM.'-'.$DD.')</span>',
+					'labelnohtml' => $labelofobject.': '.$langs->trans($val['label']),
+					'position' => ($position + ($count * 100000)).'.3',
+					'table' => $object->table_element,
+					'tablefromt' => $tablepath
 				);
 				);
 			} else {
 			} else {
 				$position = (empty($val['position']) ? 0 : intVal($val['position']));
 				$position = (empty($val['position']) ? 0 : intVal($val['position']));
 				$arrayofgroupby[$tablealias.'.'.$key] = array(
 				$arrayofgroupby[$tablealias.'.'.$key] = array(
-					'label' => img_picto('', $object->picto,
-					'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val['label']), 'position' => ($position + ($count * 100000)),
-					'table' => $object->table_element
+					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val['label']),
+					'labelnohtml' => $labelofobject.': '.$langs->trans($val['label']),
+					'position' => ($position + ($count * 100000)),
+					'table' => $object->table_element,
+					'tablefromt' => $tablepath
 				);
 				);
 			}
 			}
 		}
 		}
@@ -1269,11 +1406,39 @@ function fillArrayOfGroupBy($object, $tablealias, $labelofobject, &$arrayofgroup
 			if (!empty($extrafields->attributes[$object->table_element]['totalizable'][$key])) {
 			if (!empty($extrafields->attributes[$object->table_element]['totalizable'][$key])) {
 				continue;
 				continue;
 			}
 			}
-			$arrayofgroupby[$tablealias.'e.'.$key] = array(
-				'label' => img_picto('', $object->picto,
-				'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($extrafields->attributes[$object->table_element]['label'][$key]), 'position' => 1000 + (int) $extrafields->attributes[$object->table_element]['pos'][$key] + ($count * 100000),
-				'table' => $object->table_element
-			);
+
+			if (in_array($extrafields->attributes[$object->table_element]['type'][$key], array('timestamp', 'date', 'datetime'))) {
+				$position = (empty($extrafields->attributes[$object->table_element]['pos'][$key]) ? 0 : intVal($extrafields->attributes[$object->table_element]['pos'][$key]));
+				$arrayofgroupby[preg_replace('/^t/', 'te', $tablealias).'.'.$key.'-year'] = array(
+					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val).' <span class="opacitymedium">('.$YYYY.')</span>',
+					'labelnohtml' => $labelofobject.': '.$langs->trans($val),
+					'position' => ($position + ($count * 100000)).'.1',
+					'table' => $object->table_element,
+					'tablefromt' => $tablepath
+				);
+				$arrayofgroupby[preg_replace('/^t/', 'te', $tablealias).'.'.$key.'-month'] = array(
+					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val).' <span class="opacitymedium">('.$YYYY.'-'.$MM.')</span>',
+					'labelnohtml' => $labelofobject.': '.$langs->trans($val),
+					'position' => ($position + ($count * 100000)).'.2',
+					'table' => $object->table_element,
+					'tablefromt' => $tablepath
+				);
+				$arrayofgroupby[preg_replace('/^t/', 'te', $tablealias).'.'.$key.'-day'] = array(
+					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val).' <span class="opacitymedium">('.$YYYY.'-'.$MM.'-'.$DD.')</span>',
+					'labelnohtml' => $labelofobject.': '.$langs->trans($val),
+					'position' => ($position + ($count * 100000)).'.3',
+					'table' => $object->table_element,
+					'tablefromt' => $tablepath
+				);
+			} else {
+				$arrayofgroupby[preg_replace('/^t/', 'te', $tablealias).'.'.$key] = array(
+					'label' => img_picto('', $object->picto, 'class="pictofixedwidth"').' '.$labelofobject.': '.$langs->trans($val),
+					'labelnohtml' => $labelofobject.': '.$langs->trans($val),
+					'position' => 1000 + (int) $extrafields->attributes[$object->table_element]['pos'][$key] + ($count * 100000),
+					'table' => $object->table_element,
+					'tablefromt' => $tablepath
+				);
+			}
 		}
 		}
 	}
 	}
 
 
@@ -1288,7 +1453,7 @@ function fillArrayOfGroupBy($object, $tablealias, $labelofobject, &$arrayofgroup
 					$tmpobject = new $newobject($db);
 					$tmpobject = new $newobject($db);
 					//var_dump($key); var_dump($tmpobject->element); var_dump($val['label']); var_dump($tmptype); var_dump('t-'.$key);
 					//var_dump($key); var_dump($tmpobject->element); var_dump($val['label']); var_dump($tmptype); var_dump('t-'.$key);
 					$count++;
 					$count++;
-					$arrayofgroupby = fillArrayOfGroupBy($tmpobject, $tablealias.'__'.$key, $langs->trans($val['label']), $arrayofgroupby, $level + 1, $count);
+					$arrayofgroupby = fillArrayOfGroupBy($tmpobject, $tablealias.'__'.$key, $langs->trans($val['label']), $arrayofgroupby, $level + 1, $count, $tablepath);
 				} else {
 				} else {
 					print 'For property '.$object->element.'->'.$key.', type="'.$val['type'].'": Failed to find class '.$newobject." in file ".$tmptype[2]."<br>\n";
 					print 'For property '.$object->element.'->'.$key.', type="'.$val['type'].'": Failed to find class '.$newobject." in file ".$tmptype[2]."<br>\n";
 				}
 				}

+ 1 - 1
htdocs/core/lib/admin.lib.php

@@ -1766,7 +1766,7 @@ function form_constantes($tableau, $strictw3c = 0, $helptext = '', $text = 'Valu
 				}
 				}
 				print '</div>';
 				print '</div>';
 				//print 'http://lists.example.com/cgi-bin/mailman/admin/%LISTE%/members/remove?adminpw=%MAILMAN_ADMINPW%&unsubscribees=%EMAIL%';
 				//print 'http://lists.example.com/cgi-bin/mailman/admin/%LISTE%/members/remove?adminpw=%MAILMAN_ADMINPW%&unsubscribees=%EMAIL%';
-			} elseif ($const == 'ADHERENT_MAIL_FROM') {
+			} elseif (in_array($const, ['ADHERENT_MAIL_FROM', 'ADHERENT_CC_MAIL_FROM'])) {
 				print ' '.img_help(1, $langs->trans("EMailHelpMsgSPFDKIM"));
 				print ' '.img_help(1, $langs->trans("EMailHelpMsgSPFDKIM"));
 			}
 			}
 
 

+ 1 - 1
htdocs/core/lib/files.lib.php

@@ -2606,7 +2606,7 @@ function dol_check_secure_access_document($modulepart, $original_file, $entity,
 		$original_file = $conf->medias->multidir_output[$entity].'/'.$original_file;
 		$original_file = $conf->medias->multidir_output[$entity].'/'.$original_file;
 	} elseif ($modulepart == 'logs' && !empty($dolibarr_main_data_root)) {
 	} elseif ($modulepart == 'logs' && !empty($dolibarr_main_data_root)) {
 		// Wrapping for *.log files, like when used with url http://.../document.php?modulepart=logs&file=dolibarr.log
 		// Wrapping for *.log files, like when used with url http://.../document.php?modulepart=logs&file=dolibarr.log
-		$accessallowed = ($user->admin && basename($original_file) == $original_file && preg_match('/^dolibarr.*\.log$/', basename($original_file)));
+		$accessallowed = ($user->admin && basename($original_file) == $original_file && preg_match('/^dolibarr.*\.(log|json)$/', basename($original_file)));
 		$original_file = $dolibarr_main_data_root.'/'.$original_file;
 		$original_file = $dolibarr_main_data_root.'/'.$original_file;
 	} elseif ($modulepart == 'doctemplates' && !empty($dolibarr_main_data_root)) {
 	} elseif ($modulepart == 'doctemplates' && !empty($dolibarr_main_data_root)) {
 		// Wrapping for doctemplates
 		// Wrapping for doctemplates

+ 68 - 13
htdocs/core/lib/functions.lib.php

@@ -45,7 +45,7 @@
 
 
 include_once DOL_DOCUMENT_ROOT.'/core/lib/json.lib.php';
 include_once DOL_DOCUMENT_ROOT.'/core/lib/json.lib.php';
 
 
-
+// Function for better PHP x compatibility
 if (!function_exists('utf8_encode')) {
 if (!function_exists('utf8_encode')) {
 	/**
 	/**
 	 * Implement utf8_encode for PHP that does not support it.
 	 * Implement utf8_encode for PHP that does not support it.
@@ -71,6 +71,46 @@ if (!function_exists('utf8_decode')) {
 		return mb_convert_encoding($elements, 'ISO-8859-1', 'UTF-8');
 		return mb_convert_encoding($elements, 'ISO-8859-1', 'UTF-8');
 	}
 	}
 }
 }
+if (!function_exists('str_starts_with')) {
+	/**
+	 * str_starts_with
+	 *
+	 * @param string $haystack	haystack
+	 * @param string $needle	needle
+	 * @return boolean
+	 */
+	function str_starts_with($haystack, $needle)
+	{
+		return (string) $needle !== '' && strncmp($haystack, $needle, strlen($needle)) === 0;
+	}
+}
+if (!function_exists('str_ends_with')) {
+	/**
+	 * str_ends_with
+	 *
+	 * @param string $haystack	haystack
+	 * @param string $needle	needle
+	 * @return boolean
+	 */
+	function str_ends_with($haystack, $needle)
+	{
+		return $needle !== '' && substr($haystack, -strlen($needle)) === (string) $needle;
+	}
+}
+if (!function_exists('str_contains')) {
+	/**
+	 * str_contains
+	 *
+	 * @param string $haystack	haystack
+	 * @param string $needle	needle
+	 * @return boolean
+	 */
+	function str_contains($haystack, $needle)
+	{
+		return $needle !== '' && mb_strpos($haystack, $needle) !== false;
+	}
+}
+
 
 
 /**
 /**
  * Return the full path of the directory where a module (or an object of a module) stores its files. Path may depends on the entity if a multicompany module is enabled.
  * Return the full path of the directory where a module (or an object of a module) stores its files. Path may depends on the entity if a multicompany module is enabled.
@@ -11846,19 +11886,30 @@ function jsonOrUnserialize($stringtodecode)
 /**
 /**
  * forgeSQLFromUniversalSearchCriteria
  * forgeSQLFromUniversalSearchCriteria
  *
  *
- * @param 	string		$filter		String with universal search string. Must be  (aaa:bbb:...) with
+ * @param 	string		$filter		String with universal search string. Must be '(aaa:bbb:...) OR (ccc:ddd:...) ...' with
  * 									aaa is a field name (with alias or not) and
  * 									aaa is a field name (with alias or not) and
  * 									bbb is one of this operator '=', '<', '>', '<=', '>=', '!=', 'in', 'notin', 'like', 'notlike', 'is', 'isnot'.
  * 									bbb is one of this operator '=', '<', '>', '<=', '>=', '!=', 'in', 'notin', 'like', 'notlike', 'is', 'isnot'.
- * @param	string		$error		Error message
- * @param	int			$noand		0=Default, 1=Do not add the AND before the condition string.
+ * 									Example: '((client:=:1) OR ((client:>=:2) AND (client:<=:3))) AND (client:!=:8) AND (nom:like:'a%')'
+ * @param	string		$errorstr	Error message string
+ * @param	int			$noand		1=Do not add the AND before the condition string.
+ * @param	int			$nopar		1=Do not add the perenthesis around the condition string.
+ * @param	int			$noerror	1=If search criteria is not valid, does not return an error string but invalidate the SQL
  * @return	string					Return forged SQL string
  * @return	string					Return forged SQL string
  */
  */
-function forgeSQLFromUniversalSearchCriteria($filter, &$error = '', $noand = 0)
+function forgeSQLFromUniversalSearchCriteria($filter, &$errorstr = '', $noand = 0, $nopar = 0, $noerror = 0)
 {
 {
+	if (!preg_match('/^\(.*\)$/', $filter)) {    // If $filter does not start and end with ()
+		$filter = '(' . $filter . ')';
+	}
+
 	$regexstring = '\(([a-zA-Z0-9_\.]+:[<>!=insotlke]+:[^\(\)]+)\)';	// Must be  (aaa:bbb:...) with aaa is a field name (with alias or not) and bbb is one of this operator '=', '<', '>', '<=', '>=', '!=', 'in', 'notin', 'like', 'notlike', 'is', 'isnot'
 	$regexstring = '\(([a-zA-Z0-9_\.]+:[<>!=insotlke]+:[^\(\)]+)\)';	// Must be  (aaa:bbb:...) with aaa is a field name (with alias or not) and bbb is one of this operator '=', '<', '>', '<=', '>=', '!=', 'in', 'notin', 'like', 'notlike', 'is', 'isnot'
 
 
-	if (!dolCheckFilters($filter, $error)) {
-		return '1 = 2';		// Bad balance of parenthesis, we force a SQL not found
+	if (!dolCheckFilters($filter, $errorstr)) {
+		if ($noerror) {
+			return '1 = 2';
+		} else {
+			return 'Filter syntax error - '.$errorstr;		// Bad balance of parenthesis, we return an error message or force a SQL not found
+		}
 	}
 	}
 
 
 	// Test the filter syntax
 	// Test the filter syntax
@@ -11866,11 +11917,15 @@ function forgeSQLFromUniversalSearchCriteria($filter, &$error = '', $noand = 0)
 	$t = str_replace(array('and','or','AND','OR',' '), '', $t);		// Remove the only strings allowed between each () criteria
 	$t = str_replace(array('and','or','AND','OR',' '), '', $t);		// Remove the only strings allowed between each () criteria
 	// If the string result contains something else than '()', the syntax was wrong
 	// If the string result contains something else than '()', the syntax was wrong
 	if (preg_match('/[^\(\)]/', $t)) {
 	if (preg_match('/[^\(\)]/', $t)) {
-		$error = 'Bad syntax of the search string, filter criteria is invalidated';
-		return 'Filter syntax error';		// Bad syntax of the search string, we force a SQL not found
+		$errorstr = 'Bad syntax of the search string';
+		if ($noerror) {
+			return '1 = 2';
+		} else {
+			return 'Filter syntax error - '.$errorstr;		// Bad syntax of the search string, we return an error message or force a SQL not found
+		}
 	}
 	}
 
 
-	return ($noand ? "" : " AND ")."(".preg_replace_callback('/'.$regexstring.'/i', 'dolForgeCriteriaCallback', $filter).")";
+	return ($noand ? "" : " AND ").($nopar ? "" : '(').preg_replace_callback('/'.$regexstring.'/i', 'dolForgeCriteriaCallback', $filter).($nopar ? "" : ')');
 }
 }
 
 
 /**
 /**
@@ -11909,7 +11964,7 @@ function dolCheckFilters($sqlfilters, &$error = '')
  * This method is called by forgeSQLFromUniversalSearchCriteria()
  * This method is called by forgeSQLFromUniversalSearchCriteria()
  *
  *
  * @param  array    $matches       Array of found string by regex search. Example: "t.ref:like:'SO-%'" or "t.date_creation:<:'20160101'" or "t.nature:is:NULL"
  * @param  array    $matches       Array of found string by regex search. Example: "t.ref:like:'SO-%'" or "t.date_creation:<:'20160101'" or "t.nature:is:NULL"
- * @return string                  Forged criteria. Example: "t.field like 'abc%'"
+ * @return string                  Forged criteria. Example: "" or "()"
  */
  */
 function dolForgeDummyCriteriaCallback($matches)
 function dolForgeDummyCriteriaCallback($matches)
 {
 {
@@ -11931,7 +11986,7 @@ function dolForgeDummyCriteriaCallback($matches)
  *
  *
  * @param  array    $matches       	Array of found string by regex search.
  * @param  array    $matches       	Array of found string by regex search.
  * 									Example: "t.ref:like:'SO-%'" or "t.date_creation:<:'20160101'" or "t.date_creation:<:'2016-01-01 12:30:00'" or "t.nature:is:NULL"
  * 									Example: "t.ref:like:'SO-%'" or "t.date_creation:<:'20160101'" or "t.date_creation:<:'2016-01-01 12:30:00'" or "t.nature:is:NULL"
- * @return string                  	Forged criteria. Example: "t.field like 'abc%'"
+ * @return string                  	Forged criteria. Example: "t.field LIKE 'abc%'"
  */
  */
 function dolForgeCriteriaCallback($matches)
 function dolForgeCriteriaCallback($matches)
 {
 {
@@ -11987,7 +12042,7 @@ function dolForgeCriteriaCallback($matches)
 		}
 		}
 	}
 	}
 
 
-	return $db->escape($operand).' '.strtoupper($operator).' '.$tmpescaped;
+	return '('.$db->escape($operand).' '.strtoupper($operator).' '.$tmpescaped.')';
 }
 }
 
 
 
 

+ 39 - 0
htdocs/core/lib/functions2.lib.php

@@ -2909,3 +2909,42 @@ function acceptLocalLinktoMedia()
 	//return 1;
 	//return 1;
 	return $acceptlocallinktomedia;
 	return $acceptlocallinktomedia;
 }
 }
+
+
+/**
+ * Remove first and last parenthesis but only if first is the opening and last the closing of the same group
+ *
+ * @param 	string	$string		String to sanitize
+ * @return 	string				String without global parenthesis
+ */
+function removeGlobalParenthesis($string)
+{
+	$string = trim($string);
+
+	// If string does not start and end with parenthesis, we return $string as is.
+	if (! preg_match('/^\(.*\)$/', $string)) {
+		return $string;
+	}
+
+	$nbofchars = dol_strlen($string);
+	$i = 0; $g = 0;
+	$countparenthesis = 0;
+	while ($i < $nbofchars) {
+		$char = dol_substr($string, $i, 1);
+		if ($char == '(') {
+			$countparenthesis++;
+		} elseif ($char == ')') {
+			$countparenthesis--;
+			if ($countparenthesis <= 0) {	// We reach the end of an independent group of parenthesis
+				$g++;
+			}
+		}
+		$i++;
+	}
+
+	if ($g <= 1) {
+		return preg_replace('/^\(/', '', preg_replace('/\)$/', '', $string));
+	}
+
+	return $string;
+}

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

@@ -950,3 +950,112 @@ function removeObjectFromApiFile($file, $objectname, $modulename)
 	}
 	}
 	return 1;
 	return 1;
 }
 }
+
+/**
+ * Compare menus by their object
+ * @param  mixed  $a  first value
+ * @param  mixed  $b  seconde value
+ * @return int        1 if OK, -1 if KO
+ */
+function compareMenus($a, $b)
+{
+	return strcmp($a['fk_menu'], $b['fk_menu']);
+}
+
+/**
+ * @param    string         $file       path of filename
+ * @param    mixed          $menus      all menus for module
+ * @param    mixed|null     $menuWantTo  menu get for do actions
+ * @param    int|null       $key        key for the concerned menu
+ * @param    int            $action     for specify what action (0 = delete, 1 = add, 2 = update, -1 = when delete object)
+ * @return   int            1 if OK, -1 if KO
+ */
+function reWriteAllMenus($file, $menus, $menuWantTo, $key, $action)
+{
+	$errors =0;
+	$counter = 0;
+	if (!file_exists($file)) {
+		return -1;
+	}
+	if ($action == 0 && !empty($key)) {
+		// delete menu manuelly
+		array_splice($menus, array_search($menus[$key], $menus), 1);
+	} elseif ($action == 1) {
+		// add menu manualy
+		array_push($menus, $menuWantTo);
+	} elseif ($action == 2 && !empty($key) && !empty($menuWantTo)) {
+		// update right from permissions array
+
+		// check if the values already exists
+		foreach ($menus as $index => $menu) {
+			if ($index !== $key) {
+				if ($menu['type'] === $menuWantTo['type']) {
+					if (strcasecmp(str_replace(' ', '', $menu['titre']), str_replace(' ', '', $menuWantTo['titre'])) === 0) {
+						$counter++;
+					}
+					if (strcasecmp(str_replace(' ', '', $menu['url']), str_replace(' ', '', $menuWantTo['url'])) === 0) {
+						$counter++;
+					}
+				}
+			}
+		}
+		if (!$counter) {
+			$menus[$key] = $menuWantTo;
+		} else {
+			$errors++;
+		}
+	} elseif ($action == -1 && !empty($menuWantTo)) {
+		// delete menus when delete Object
+		foreach ($menus as $index => $menu) {
+			if ((strpos(strtolower($menu['fk_menu']), strtolower($menuWantTo)) !== false) || (strpos(strtolower($menu['leftmenu']), strtolower($menuWantTo)) !== false)) {
+				array_splice($menus, array_search($menu, $menus), 1);
+			}
+		}
+	} else {
+		$errors++;
+	}
+	if (!$errors) {
+		// delete All LEFT Menus
+		$beginMenu = '/* BEGIN MODULEBUILDER LEFTMENU MYOBJECT */';
+		$endMenu = '/* END MODULEBUILDER LEFTMENU MYOBJECT */';
+		$allMenus = getFromFile($file, $beginMenu, $endMenu);
+		dolReplaceInFile($file, array($allMenus => ''));
+
+		// orders menu with other menus that have the same object
+		usort($menus, 'compareMenus');
+
+		//prepare each menu and stock them in string
+		$str_menu = "";
+		foreach ($menus as $index =>$menu) {
+			$menu['position'] = "1000 + \$r";
+			if ($menu['type'] === 'left') {
+				$start = "\t\t".'/* LEFTMENU '.strtoupper($menu['titre']).' */';
+				$end   = "\t\t".'/* END LEFTMENU '.strtoupper($menu['titre']).' */';
+				$val_actuel = $menu;
+				$next_val = $menus[$index + 1];
+				$str_menu .= $start."\n";
+				$str_menu.= "\t\t\$this->menu[\$r++]=array(\n";
+				$str_menu.= "\t\t\t 'fk_menu' =>'".$menu['fk_menu']."',\n";
+				$str_menu.= "\t\t\t 'type' =>'".$menu['type']."',\n";
+				$str_menu.= "\t\t\t 'titre' =>'".$menu['titre']."',\n";
+				$str_menu.= "\t\t\t 'mainmenu' =>'".$menu['mainmenu']."',\n";
+				$str_menu.= "\t\t\t 'leftmenu' =>'".$menu['leftmenu']."',\n";
+				$str_menu.= "\t\t\t 'url' =>'".$menu['url']."',\n";
+				$str_menu.= "\t\t\t 'langs' =>'".$menu['langs']."',\n";
+				$str_menu.= "\t\t\t 'position' =>".$menu['position'].",\n";
+				$str_menu.= "\t\t\t 'enabled' =>'".$menu['enabled']."',\n";
+				$str_menu.= "\t\t\t 'perms' =>'".$menu['perms']."',\n";
+				$str_menu.= "\t\t\t 'target' =>'".$menu['target']."',\n";
+				$str_menu.= "\t\t\t 'user' =>".$menu['user'].",\n";
+				$str_menu.= "\t\t);\n";
+
+				if ($val_actuel['leftmenu'] !== $next_val['leftmenu']) {
+					$str_menu .= $end."\n";
+				}
+			}
+		}
+
+		dolReplaceInFile($file, array($beginMenu => $beginMenu."\n".$str_menu."\n"));
+		return 1;
+	}return -1;
+}

+ 4 - 4
htdocs/core/menus/standard/eldy.lib.php

@@ -1169,16 +1169,16 @@ function get_left_menu_home($mainmenu, &$newmenu, $usemenuhider = 1, $leftmenu =
 		if ($user->hasRight('user', 'user', 'read')) {
 		if ($user->hasRight('user', 'user', 'read')) {
 			if ($usemenuhider || empty($leftmenu) || $leftmenu == "users") {
 			if ($usemenuhider || empty($leftmenu) || $leftmenu == "users") {
 				$newmenu->add("", $langs->trans("Users"), 1, $user->hasRight('user',  'user', 'lire') || $user->admin);
 				$newmenu->add("", $langs->trans("Users"), 1, $user->hasRight('user',  'user', 'lire') || $user->admin);
-				$newmenu->add("/user/card.php?leftmenu=users&action=create", $langs->trans("NewUser"), 2, ($user->hasRight("user", "user", "write") || $user->admin) && !(isModEnabled('multicompany') && $conf->entity > 1 && $conf->global->MULTICOMPANY_TRANSVERSE_MODE), '', 'home');
+				$newmenu->add("/user/card.php?leftmenu=users&action=create", $langs->trans("NewUser"), 2, ($user->hasRight("user", "user", "write") || $user->admin) && !(isModEnabled('multicompany') && $conf->entity > 1 && !empty($conf->global->MULTICOMPANY_TRANSVERSE_MODE)), '', 'home');
 				$newmenu->add("/user/list.php?leftmenu=users", $langs->trans("ListOfUsers"), 2, $user->hasRight('user',  'user', 'lire') || $user->admin);
 				$newmenu->add("/user/list.php?leftmenu=users", $langs->trans("ListOfUsers"), 2, $user->hasRight('user',  'user', 'lire') || $user->admin);
 				$newmenu->add("/user/hierarchy.php?leftmenu=users", $langs->trans("HierarchicView"), 2, $user->hasRight('user',  'user', 'lire') || $user->admin);
 				$newmenu->add("/user/hierarchy.php?leftmenu=users", $langs->trans("HierarchicView"), 2, $user->hasRight('user',  'user', 'lire') || $user->admin);
 				if (isModEnabled('categorie')) {
 				if (isModEnabled('categorie')) {
 					$langs->load("categories");
 					$langs->load("categories");
 					$newmenu->add("/categories/index.php?leftmenu=users&type=7", $langs->trans("UsersCategoriesShort"), 2, $user->hasRight('categorie',  'lire'), '', $mainmenu, 'cat');
 					$newmenu->add("/categories/index.php?leftmenu=users&type=7", $langs->trans("UsersCategoriesShort"), 2, $user->hasRight('categorie',  'lire'), '', $mainmenu, 'cat');
 				}
 				}
-				$newmenu->add("", $langs->trans("Groups"), 1, ($user->hasRight('user',  'user', 'lire') || $user->admin) && !(isModEnabled('multicompany') && $conf->entity > 1 && $conf->global->MULTICOMPANY_TRANSVERSE_MODE));
-				$newmenu->add("/user/group/card.php?leftmenu=users&action=create", $langs->trans("NewGroup"), 2, ((!empty($conf->global->MAIN_USE_ADVANCED_PERMS) ? $user->hasRight("user", "group_advance", "create") : $user->hasRight("user", "user", "create")) || $user->admin) && !(isModEnabled('multicompany') && $conf->entity > 1 && $conf->global->MULTICOMPANY_TRANSVERSE_MODE));
-				$newmenu->add("/user/group/list.php?leftmenu=users", $langs->trans("ListOfGroups"), 2, ((!empty($conf->global->MAIN_USE_ADVANCED_PERMS) ? $user->hasRight('user',  'group_advance', 'read') : $user->hasRight('user',  'user', 'lire')) || $user->admin) && !(isModEnabled('multicompany') && $conf->entity > 1 && $conf->global->MULTICOMPANY_TRANSVERSE_MODE));
+				$newmenu->add("", $langs->trans("Groups"), 1, ($user->hasRight('user',  'user', 'lire') || $user->admin) && !(isModEnabled('multicompany') && $conf->entity > 1 && !empty($conf->global->MULTICOMPANY_TRANSVERSE_MODE)));
+				$newmenu->add("/user/group/card.php?leftmenu=users&action=create", $langs->trans("NewGroup"), 2, ((!empty($conf->global->MAIN_USE_ADVANCED_PERMS) ? $user->hasRight("user", "group_advance", "create") : $user->hasRight("user", "user", "create")) || $user->admin) && !(isModEnabled('multicompany') && $conf->entity > 1 && !empty($conf->global->MULTICOMPANY_TRANSVERSE_MODE)));
+				$newmenu->add("/user/group/list.php?leftmenu=users", $langs->trans("ListOfGroups"), 2, ((!empty($conf->global->MAIN_USE_ADVANCED_PERMS) ? $user->hasRight('user',  'group_advance', 'read') : $user->hasRight('user',  'user', 'lire')) || $user->admin) && !(isModEnabled('multicompany') && $conf->entity > 1 && !empty($conf->global->MULTICOMPANY_TRANSVERSE_MODE)));
 			}
 			}
 		}
 		}
 	}
 	}

+ 32 - 13
htdocs/core/modules/facture/doc/pdf_crabe.modules.php

@@ -354,6 +354,15 @@ class pdf_crabe extends ModelePDFFactures
 					$heightforfooter += 6;
 					$heightforfooter += 6;
 				}
 				}
 
 
+				$heightforqrinvoice_firstpage = 0;
+				if (getDolGlobalString('INVOICE_ADD_SWISS_QR_CODE') == 'bottom') {
+					$heightforqrinvoice_firstpage = $this->getHeightForQRInvoice(1, $object, $langs);
+					if ($heightforqrinvoice_firstpage > 0) {
+						// Shrink infotot to a base 30
+						$heightforinfotot = 30 + (4 * $nbpayments); // Height reserved to output the info and total part and payment part
+					}
+				}
+
 				if (class_exists('TCPDF')) {
 				if (class_exists('TCPDF')) {
 					$pdf->setPrintHeader(false);
 					$pdf->setPrintHeader(false);
 					$pdf->setPrintFooter(false);
 					$pdf->setPrintFooter(false);
@@ -487,9 +496,10 @@ class pdf_crabe extends ModelePDFFactures
 				$qrcodestring = '';
 				$qrcodestring = '';
 				if (!empty($conf->global->INVOICE_ADD_ZATCA_QR_CODE)) {
 				if (!empty($conf->global->INVOICE_ADD_ZATCA_QR_CODE)) {
 					$qrcodestring = $object->buildZATCAQRString();
 					$qrcodestring = $object->buildZATCAQRString();
-				} elseif (!empty($conf->global->INVOICE_ADD_SWISS_QR_CODE)) {
+				} elseif (getDolGlobalString('INVOICE_ADD_SWISS_QR_CODE') == '1') {
 					$qrcodestring = $object->buildSwitzerlandQRString();
 					$qrcodestring = $object->buildSwitzerlandQRString();
 				}
 				}
+
 				if ($qrcodestring) {
 				if ($qrcodestring) {
 					$qrcodecolor = array('25', '25', '25');
 					$qrcodecolor = array('25', '25', '25');
 					// set style for QR-code
 					// set style for QR-code
@@ -597,7 +607,8 @@ class pdf_crabe extends ModelePDFFactures
 					}
 					}
 
 
 					$pdf->setTopMargin($tab_top_newpage);
 					$pdf->setTopMargin($tab_top_newpage);
-					$pdf->setPageOrientation('', 1, $heightforfooter + $heightforfreetext + $heightforinfotot); // The only function to edit the bottom margin of current page to set it.
+					$page_bottom_margin = $heightforfooter + $heightforfreetext + $heightforinfotot + $this->getHeightForQRInvoice($pdf->getPage(), $object, $langs);
+					$pdf->setPageOrientation('', 1, $page_bottom_margin);
 					$pageposbefore = $pdf->getPage();
 					$pageposbefore = $pdf->getPage();
 
 
 					$showpricebeforepagebreak = 1;
 					$showpricebeforepagebreak = 1;
@@ -605,7 +616,7 @@ class pdf_crabe extends ModelePDFFactures
 					$posYAfterDescription = 0;
 					$posYAfterDescription = 0;
 
 
 					// We start with Photo of product line
 					// We start with Photo of product line
-					if (isset($imglinesize['width']) && isset($imglinesize['height']) && ($curY + $imglinesize['height']) > ($this->page_hauteur - ($heightforfooter + $heightforfreetext + $heightforinfotot))) {	// If photo too high, we moved completely on new page
+					if (isset($imglinesize['width']) && isset($imglinesize['height']) && ($curY + $imglinesize['height']) > ($this->page_hauteur - $page_bottom_margin)) {	// If photo too high, we moved completely on new page
 						$pdf->AddPage('', '', true);
 						$pdf->AddPage('', '', true);
 						if (!empty($tplidx)) {
 						if (!empty($tplidx)) {
 							$pdf->useTemplate($tplidx);
 							$pdf->useTemplate($tplidx);
@@ -648,7 +659,7 @@ class pdf_crabe extends ModelePDFFactures
 						$pageposafter = $pdf->getPage();
 						$pageposafter = $pdf->getPage();
 						$posyafter = $pdf->GetY();
 						$posyafter = $pdf->GetY();
 						//var_dump($posyafter); var_dump(($this->page_hauteur - ($heightforfooter+$heightforfreetext+$heightforinfotot))); exit;
 						//var_dump($posyafter); var_dump(($this->page_hauteur - ($heightforfooter+$heightforfreetext+$heightforinfotot))); exit;
-						if ($posyafter > ($this->page_hauteur - ($heightforfooter + $heightforfreetext + $heightforinfotot))) {	// There is no space left for total+free text
+						if ($posyafter > ($this->page_hauteur - $page_bottom_margin)) {	// There is no space left for total+free text
 							if ($i == ($nblines - 1)) {	// No more lines, and no space left to show total, so we create a new page
 							if ($i == ($nblines - 1)) {	// No more lines, and no space left to show total, so we create a new page
 								$pdf->AddPage('', '', true);
 								$pdf->AddPage('', '', true);
 								if (!empty($tplidx)) {
 								if (!empty($tplidx)) {
@@ -832,11 +843,11 @@ class pdf_crabe extends ModelePDFFactures
 					while ($pagenb < $pageposafter) {
 					while ($pagenb < $pageposafter) {
 						$pdf->setPage($pagenb);
 						$pdf->setPage($pagenb);
 						if ($pagenb == 1) {
 						if ($pagenb == 1) {
-							$this->_tableau($pdf, $tab_top, $this->page_hauteur - $tab_top - $heightforfooter, 0, $outputlangs, 0, 1, $object->multicurrency_code);
+							$this->_tableau($pdf, $tab_top, $this->page_hauteur - $tab_top - $heightforfooter - $heightforqrinvoice_firstpage, 0, $outputlangs, 0, 1, $object->multicurrency_code);
 						} else {
 						} else {
 							$this->_tableau($pdf, $tab_top_newpage, $this->page_hauteur - $tab_top_newpage - $heightforfooter, 0, $outputlangs, 1, 1, $object->multicurrency_code);
 							$this->_tableau($pdf, $tab_top_newpage, $this->page_hauteur - $tab_top_newpage - $heightforfooter, 0, $outputlangs, 1, 1, $object->multicurrency_code);
 						}
 						}
-						$this->_pagefoot($pdf, $object, $outputlangs, 1);
+						$this->_pagefoot($pdf, $object, $outputlangs, 1, $this->getHeightForQRInvoice($pagenb, $object, $langs));
 						$pagenb++;
 						$pagenb++;
 						$pdf->setPage($pagenb);
 						$pdf->setPage($pagenb);
 						$pdf->setPageOrientation('', 1, 0); // The only function to edit the bottom margin of current page to set it.
 						$pdf->setPageOrientation('', 1, 0); // The only function to edit the bottom margin of current page to set it.
@@ -850,11 +861,11 @@ class pdf_crabe extends ModelePDFFactures
 					}
 					}
 					if (isset($object->lines[$i + 1]->pagebreak) && $object->lines[$i + 1]->pagebreak) {
 					if (isset($object->lines[$i + 1]->pagebreak) && $object->lines[$i + 1]->pagebreak) {
 						if ($pagenb == 1) {
 						if ($pagenb == 1) {
-							$this->_tableau($pdf, $tab_top, $this->page_hauteur - $tab_top - $heightforfooter, 0, $outputlangs, 0, 1, $object->multicurrency_code);
+							$this->_tableau($pdf, $tab_top, $this->page_hauteur - $tab_top - $heightforfooter - $heightforqrinvoice_firstpage, 0, $outputlangs, 0, 1, $object->multicurrency_code);
 						} else {
 						} else {
 							$this->_tableau($pdf, $tab_top_newpage, $this->page_hauteur - $tab_top_newpage - $heightforfooter, 0, $outputlangs, 1, 1, $object->multicurrency_code);
 							$this->_tableau($pdf, $tab_top_newpage, $this->page_hauteur - $tab_top_newpage - $heightforfooter, 0, $outputlangs, 1, 1, $object->multicurrency_code);
 						}
 						}
-						$this->_pagefoot($pdf, $object, $outputlangs, 1);
+						$this->_pagefoot($pdf, $object, $outputlangs, 1, $this->getHeightForQRInvoice($pagenb, $object, $langs));
 						// New page
 						// New page
 						$pdf->AddPage();
 						$pdf->AddPage();
 						if (!empty($tplidx)) {
 						if (!empty($tplidx)) {
@@ -870,8 +881,8 @@ class pdf_crabe extends ModelePDFFactures
 
 
 				// Show square
 				// Show square
 				if ($pagenb == 1) {
 				if ($pagenb == 1) {
-					$this->_tableau($pdf, $tab_top, $this->page_hauteur - $tab_top - $heightforinfotot - $heightforfreetext - $heightforfooter, 0, $outputlangs, 0, 0, $object->multicurrency_code);
-					$bottomlasttab = $this->page_hauteur - $heightforinfotot - $heightforfreetext - $heightforfooter + 1;
+					$this->_tableau($pdf, $tab_top, $this->page_hauteur - $tab_top - $heightforinfotot - $heightforfreetext - $heightforfooter - $heightforqrinvoice_firstpage, 0, $outputlangs, 0, 0, $object->multicurrency_code);
+					$bottomlasttab = $this->page_hauteur - $heightforinfotot - $heightforfreetext - $heightforfooter - $heightforqrinvoice_firstpage + 1;
 				} else {
 				} else {
 					$this->_tableau($pdf, $tab_top_newpage, $this->page_hauteur - $tab_top_newpage - $heightforinfotot - $heightforfreetext - $heightforfooter, 0, $outputlangs, 1, 0, $object->multicurrency_code);
 					$this->_tableau($pdf, $tab_top_newpage, $this->page_hauteur - $tab_top_newpage - $heightforinfotot - $heightforfreetext - $heightforfooter, 0, $outputlangs, 1, 0, $object->multicurrency_code);
 					$bottomlasttab = $this->page_hauteur - $heightforinfotot - $heightforfreetext - $heightforfooter + 1;
 					$bottomlasttab = $this->page_hauteur - $heightforinfotot - $heightforfreetext - $heightforfooter + 1;
@@ -890,11 +901,18 @@ class pdf_crabe extends ModelePDFFactures
 				}
 				}
 
 
 				// Pagefoot
 				// Pagefoot
-				$this->_pagefoot($pdf, $object, $outputlangs);
+				$this->_pagefoot($pdf, $object, $outputlangs, 0, $this->getHeightForQRInvoice($pageposbefore, $object, $langs));
 				if (method_exists($pdf, 'AliasNbPages')) {
 				if (method_exists($pdf, 'AliasNbPages')) {
 					$pdf->AliasNbPages();
 					$pdf->AliasNbPages();
 				}
 				}
 
 
+				if (getDolGlobalString('INVOICE_ADD_SWISS_QR_CODE') == 'bottom') {
+					$result = $this->addBottomQRInvoice($pdf, $object, $outputlangs);
+					if (!$result) {
+						$pdf->Close();
+						return 0;
+					}
+				}
 				$pdf->Close();
 				$pdf->Close();
 
 
 				$pdf->Output($file, 'F');
 				$pdf->Output($file, 'F');
@@ -2190,11 +2208,12 @@ class pdf_crabe extends ModelePDFFactures
 	 * 		@param	Facture		$object				Object to show
 	 * 		@param	Facture		$object				Object to show
 	 *      @param	Translate	$outputlangs		Object lang for output
 	 *      @param	Translate	$outputlangs		Object lang for output
 	 *      @param	int			$hidefreetext		1=Hide free text
 	 *      @param	int			$hidefreetext		1=Hide free text
+	 *      @param	int			$heightforqrinvoice	Height for QR invoices
 	 *      @return	int								Return height of bottom margin including footer text
 	 *      @return	int								Return height of bottom margin including footer text
 	 */
 	 */
-	protected function _pagefoot(&$pdf, $object, $outputlangs, $hidefreetext = 0)
+	protected function _pagefoot(&$pdf, $object, $outputlangs, $hidefreetext = 0, $heightforqrinvoice = 0)
 	{
 	{
 		$showdetails = getDolGlobalInt('MAIN_GENERATE_DOCUMENTS_SHOW_FOOT_DETAILS', 0);
 		$showdetails = getDolGlobalInt('MAIN_GENERATE_DOCUMENTS_SHOW_FOOT_DETAILS', 0);
-		return pdf_pagefoot($pdf, $outputlangs, 'INVOICE_FREE_TEXT', $this->emetteur, $this->marge_basse, $this->marge_gauche, $this->page_hauteur, $object, $showdetails, $hidefreetext, $this->page_largeur, $this->watermark);
+		return pdf_pagefoot($pdf, $outputlangs, 'INVOICE_FREE_TEXT', $this->emetteur, $heightforqrinvoice + $this->marge_basse, $this->marge_gauche, $this->page_hauteur, $object, $showdetails, $hidefreetext, $this->page_largeur, $this->watermark);
 	}
 	}
 }
 }

+ 36 - 21
htdocs/core/modules/facture/doc/pdf_sponge.modules.php

@@ -382,6 +382,14 @@ class pdf_sponge extends ModelePDFFactures
 				$this->heightforfreetext = (isset($conf->global->MAIN_PDF_FREETEXT_HEIGHT) ? $conf->global->MAIN_PDF_FREETEXT_HEIGHT : 5); // Height reserved to output the free text on last page
 				$this->heightforfreetext = (isset($conf->global->MAIN_PDF_FREETEXT_HEIGHT) ? $conf->global->MAIN_PDF_FREETEXT_HEIGHT : 5); // Height reserved to output the free text on last page
 				$this->heightforfooter = $this->marge_basse + (empty($conf->global->MAIN_GENERATE_DOCUMENTS_SHOW_FOOT_DETAILS) ? 12 : 22); // Height reserved to output the footer (value include bottom margin)
 				$this->heightforfooter = $this->marge_basse + (empty($conf->global->MAIN_GENERATE_DOCUMENTS_SHOW_FOOT_DETAILS) ? 12 : 22); // Height reserved to output the footer (value include bottom margin)
 
 
+				$heightforqrinvoice = $heightforqrinvoice_firstpage = 0;
+				if (getDolGlobalString('INVOICE_ADD_SWISS_QR_CODE') == 'bottom') {
+					if ($this->getHeightForQRInvoice(1, $object, $langs) > 0) {
+						// Shrink infotot to a base 30
+						$this->heightforinfotot = 30 + (4 * $nbpayments); // Height reserved to output the info and total part and payment part
+					}
+				}
+
 				if (class_exists('TCPDF')) {
 				if (class_exists('TCPDF')) {
 					$pdf->setPrintHeader(false);
 					$pdf->setPrintHeader(false);
 					$pdf->setPrintFooter(false);
 					$pdf->setPrintFooter(false);
@@ -498,7 +506,7 @@ class pdf_sponge extends ModelePDFFactures
 				$qrcodestring = '';
 				$qrcodestring = '';
 				if (!empty($conf->global->INVOICE_ADD_ZATCA_QR_CODE)) {
 				if (!empty($conf->global->INVOICE_ADD_ZATCA_QR_CODE)) {
 					$qrcodestring = $object->buildZATCAQRString();
 					$qrcodestring = $object->buildZATCAQRString();
-				} elseif (!empty($conf->global->INVOICE_ADD_SWISS_QR_CODE)) {
+				} elseif (getDolGlobalString('INVOICE_ADD_SWISS_QR_CODE') == '1') {
 					$qrcodestring = $object->buildSwitzerlandQRString();
 					$qrcodestring = $object->buildSwitzerlandQRString();
 				}
 				}
 				if ($qrcodestring) {
 				if ($qrcodestring) {
@@ -534,7 +542,7 @@ class pdf_sponge extends ModelePDFFactures
 
 
 
 
 				// Define heigth of table for lines (for first page)
 				// Define heigth of table for lines (for first page)
-				$tab_height = $this->page_hauteur - $this->tab_top - $this->heightforfooter - $this->heightforfreetext;
+				$tab_height = $this->page_hauteur - $this->tab_top - $this->heightforfooter - $this->heightforfreetext - $this->getHeightForQRInvoice(1, $object, $langs);
 
 
 				$nexY = $this->tab_top - 1;
 				$nexY = $this->tab_top - 1;
 
 
@@ -612,7 +620,6 @@ class pdf_sponge extends ModelePDFFactures
 							if (!getDolGlobalInt('MAIN_PDF_DONOTREPEAT_HEAD')) {
 							if (!getDolGlobalInt('MAIN_PDF_DONOTREPEAT_HEAD')) {
 								$this->_pagehead($pdf, $object, 0, $outputlangs, $outputlangsbis);
 								$this->_pagehead($pdf, $object, 0, $outputlangs, $outputlangsbis);
 							}
 							}
-							// $this->_pagefoot($pdf,$object,$outputlangs,1);
 							$pdf->setTopMargin($this->tab_top_newpage);
 							$pdf->setTopMargin($this->tab_top_newpage);
 							// The only function to edit the bottom margin of current page to set it.
 							// The only function to edit the bottom margin of current page to set it.
 							$pdf->setPageOrientation('', 1, $this->heightforfooter + $this->heightforfreetext);
 							$pdf->setPageOrientation('', 1, $this->heightforfooter + $this->heightforfreetext);
@@ -657,7 +664,7 @@ class pdf_sponge extends ModelePDFFactures
 
 
 							// Add footer
 							// Add footer
 							$pdf->setPageOrientation('', 1, 0); // The only function to edit the bottom margin of current page to set it.
 							$pdf->setPageOrientation('', 1, 0); // The only function to edit the bottom margin of current page to set it.
-							$this->_pagefoot($pdf, $object, $outputlangs, 1);
+							$this->_pagefoot($pdf, $object, $outputlangs, 1, $this->getHeightForQRInvoice($pdf->getPage(), $object, $outputlangs));
 
 
 							$i++;
 							$i++;
 						}
 						}
@@ -728,7 +735,8 @@ class pdf_sponge extends ModelePDFFactures
 					}
 					}
 
 
 					$pdf->setTopMargin($this->tab_top_newpage);
 					$pdf->setTopMargin($this->tab_top_newpage);
-					$pdf->setPageOrientation('', 1, $this->heightforfooter + $this->heightforfreetext + $this->heightforinfotot); // The only function to edit the bottom margin of current page to set it.
+					$page_bottom_margin =  $this->heightforfooter + $this->heightforfreetext + $this->heightforinfotot + $this->getHeightForQRInvoice($pdf->getPage(), $object, $langs);
+					$pdf->setPageOrientation('', 1, $page_bottom_margin);
 					$pageposbefore = $pdf->getPage();
 					$pageposbefore = $pdf->getPage();
 
 
 					$showpricebeforepagebreak = 1;
 					$showpricebeforepagebreak = 1;
@@ -737,7 +745,7 @@ class pdf_sponge extends ModelePDFFactures
 
 
 					if ($this->getColumnStatus('photo')) {
 					if ($this->getColumnStatus('photo')) {
 						// We start with Photo of product line
 						// We start with Photo of product line
-						if (isset($imglinesize['width']) && isset($imglinesize['height']) && ($curY + $imglinesize['height']) > ($this->page_hauteur - ($this->heightforfooter + $this->heightforfreetext + $this->heightforinfotot))) {	// If photo too high, we moved completely on new page
+						if (isset($imglinesize['width']) && isset($imglinesize['height']) && ($curY + $imglinesize['height']) > ($this->page_hauteur - $page_bottom_margin)) {	// If photo too high, we moved completely on new page
 							$pdf->AddPage('', '', true);
 							$pdf->AddPage('', '', true);
 							if (!empty($tplidx)) {
 							if (!empty($tplidx)) {
 								$pdf->useTemplate($tplidx);
 								$pdf->useTemplate($tplidx);
@@ -778,7 +786,7 @@ class pdf_sponge extends ModelePDFFactures
 							$pageposafter = $pdf->getPage();
 							$pageposafter = $pdf->getPage();
 							$posyafter = $pdf->GetY();
 							$posyafter = $pdf->GetY();
 							//var_dump($posyafter); var_dump(($this->page_hauteur - ($this->heightforfooter+$this->heightforfreetext+$this->heightforinfotot))); exit;
 							//var_dump($posyafter); var_dump(($this->page_hauteur - ($this->heightforfooter+$this->heightforfreetext+$this->heightforinfotot))); exit;
-							if ($posyafter > ($this->page_hauteur - ($this->heightforfooter + $this->heightforfreetext + $this->heightforinfotot))) {	// There is no space left for total+free text
+							if ($posyafter > ($this->page_hauteur - $page_bottom_margin)) {	// There is no space left for total+free text
 								if ($i == ($nblines - 1)) {	// No more lines, and no space left to show total, so we create a new page
 								if ($i == ($nblines - 1)) {	// No more lines, and no space left to show total, so we create a new page
 									$pdf->AddPage('', '', true);
 									$pdf->AddPage('', '', true);
 									if (!empty($tplidx)) {
 									if (!empty($tplidx)) {
@@ -983,12 +991,13 @@ class pdf_sponge extends ModelePDFFactures
 					// Detect if some page were added automatically and output _tableau for past pages
 					// Detect if some page were added automatically and output _tableau for past pages
 					while ($pagenb < $pageposafter) {
 					while ($pagenb < $pageposafter) {
 						$pdf->setPage($pagenb);
 						$pdf->setPage($pagenb);
+						$heightforqrinvoice = $this->getHeightForQRInvoice($pagenb, $object, $langs);
 						if ($pagenb == $pageposbeforeprintlines) {
 						if ($pagenb == $pageposbeforeprintlines) {
-							$this->_tableau($pdf, $this->tab_top, $this->page_hauteur - $this->tab_top - $this->heightforfooter, 0, $outputlangs, $hidetop, 1, $object->multicurrency_code, $outputlangsbis);
+							$this->_tableau($pdf, $this->tab_top, $this->page_hauteur - $this->tab_top - $this->heightforfooter - $heightforqrinvoice, 0, $outputlangs, $hidetop, 1, $object->multicurrency_code, $outputlangsbis);
 						} else {
 						} else {
-							$this->_tableau($pdf, $this->tab_top_newpage, $this->page_hauteur - $this->tab_top_newpage - $this->heightforfooter, 0, $outputlangs, 1, 1, $object->multicurrency_code, $outputlangsbis);
+							$this->_tableau($pdf, $this->tab_top_newpage, $this->page_hauteur - $this->tab_top_newpage - $this->heightforfooter - $heightforqrinvoice, 0, $outputlangs, 1, 1, $object->multicurrency_code, $outputlangsbis);
 						}
 						}
-						$this->_pagefoot($pdf, $object, $outputlangs, 1);
+						$this->_pagefoot($pdf, $object, $outputlangs, 1, $this->getHeightForQRInvoice($pdf->getPage(), $object, $outputlangs));
 						$pagenb++;
 						$pagenb++;
 						$pdf->setPage($pagenb);
 						$pdf->setPage($pagenb);
 						$pdf->setPageOrientation('', 1, 0); // The only function to edit the bottom margin of current page to set it.
 						$pdf->setPageOrientation('', 1, 0); // The only function to edit the bottom margin of current page to set it.
@@ -1001,12 +1010,13 @@ class pdf_sponge extends ModelePDFFactures
 					}
 					}
 
 
 					if (isset($object->lines[$i + 1]->pagebreak) && $object->lines[$i + 1]->pagebreak) {
 					if (isset($object->lines[$i + 1]->pagebreak) && $object->lines[$i + 1]->pagebreak) {
+						$heightforqrinvoice = $this->getHeightForQRInvoice($pagenb, $object, $langs);
 						if ($pagenb == $pageposafter) {
 						if ($pagenb == $pageposafter) {
-							$this->_tableau($pdf, $this->tab_top, $this->page_hauteur - $this->tab_top - $this->heightforfooter, 0, $outputlangs, $hidetop, 1, $object->multicurrency_code, $outputlangsbis);
+							$this->_tableau($pdf, $this->tab_top, $this->page_hauteur - $this->tab_top - $this->heightforfooter - $heightforqrinvoice, 0, $outputlangs, $hidetop, 1, $object->multicurrency_code, $outputlangsbis);
 						} else {
 						} else {
-							$this->_tableau($pdf, $this->tab_top_newpage, $this->page_hauteur - $this->tab_top_newpage - $this->heightforfooter, 0, $outputlangs, 1, 1, $object->multicurrency_code, $outputlangsbis);
+							$this->_tableau($pdf, $this->tab_top_newpage, $this->page_hauteur - $this->tab_top_newpage - $this->heightforfooter - $heightforqrinvoice, 0, $outputlangs, 1, 1, $object->multicurrency_code, $outputlangsbis);
 						}
 						}
-						$this->_pagefoot($pdf, $object, $outputlangs, 1);
+						$this->_pagefoot($pdf, $object, $outputlangs, 1, $this->getHeightForQRInvoice($pdf->getPage(), $object, $outputlangs));
 						// New page
 						// New page
 						$pdf->AddPage();
 						$pdf->AddPage();
 						if (!empty($tplidx)) {
 						if (!empty($tplidx)) {
@@ -1020,12 +1030,13 @@ class pdf_sponge extends ModelePDFFactures
 				}
 				}
 
 
 				// Show square
 				// Show square
+				$heightforqrinvoice = $this->getHeightForQRInvoice($pagenb, $object, $langs);
 				if ($pagenb == $pageposbeforeprintlines) {
 				if ($pagenb == $pageposbeforeprintlines) {
-					$this->_tableau($pdf, $this->tab_top, $this->page_hauteur - $this->tab_top - $this->heightforinfotot - $this->heightforfreetext - $this->heightforfooter, 0, $outputlangs, $hidetop, 0, $object->multicurrency_code, $outputlangsbis);
-					$bottomlasttab = $this->page_hauteur - $this->heightforinfotot - $this->heightforfreetext - $this->heightforfooter + 1;
+					$this->_tableau($pdf, $this->tab_top, $this->page_hauteur - $this->tab_top - $this->heightforinfotot - $this->heightforfreetext - $this->heightforfooter - $heightforqrinvoice, 0, $outputlangs, $hidetop, 0, $object->multicurrency_code, $outputlangsbis);
+					$bottomlasttab = $this->page_hauteur - $this->heightforinfotot - $this->heightforfreetext - $this->heightforfooter - $heightforqrinvoice + 1;
 				} else {
 				} else {
-					$this->_tableau($pdf, $this->tab_top_newpage, $this->page_hauteur - $this->tab_top_newpage - $this->heightforinfotot - $this->heightforfreetext - $this->heightforfooter, 0, $outputlangs, 1, 0, $object->multicurrency_code, $outputlangsbis);
-					$bottomlasttab = $this->page_hauteur - $this->heightforinfotot - $this->heightforfreetext - $this->heightforfooter + 1;
+					$this->_tableau($pdf, $this->tab_top_newpage, $this->page_hauteur - $this->tab_top_newpage - $this->heightforinfotot - $this->heightforfreetext - $this->heightforfooter - $heightforqrinvoice, 0, $outputlangs, 1, 0, $object->multicurrency_code, $outputlangsbis);
+					$bottomlasttab = $this->page_hauteur - $this->heightforinfotot - $this->heightforfreetext - $this->heightforfooter - $heightforqrinvoice + 1;
 				}
 				}
 
 
 				// Display infos area
 				// Display infos area
@@ -1040,11 +1051,14 @@ class pdf_sponge extends ModelePDFFactures
 				}
 				}
 
 
 				// Pagefoot
 				// Pagefoot
-				$this->_pagefoot($pdf, $object, $outputlangs);
+				$this->_pagefoot($pdf, $object, $outputlangs, 0, $this->getHeightForQRInvoice($pageposbefore, $object, $langs));
 				if (method_exists($pdf, 'AliasNbPages')) {
 				if (method_exists($pdf, 'AliasNbPages')) {
 					$pdf->AliasNbPages();
 					$pdf->AliasNbPages();
 				}
 				}
 
 
+				if (getDolGlobalString('INVOICE_ADD_SWISS_QR_CODE') == 'bottom') {
+					$this->addBottomQRInvoice($pdf, $object, $outputlangs);
+				}
 				$pdf->Close();
 				$pdf->Close();
 
 
 				$pdf->Output($file, 'F');
 				$pdf->Output($file, 'F');
@@ -1530,7 +1544,7 @@ class pdf_sponge extends ModelePDFFactures
 
 
 			foreach ($TPreviousIncoice as &$fac) {
 			foreach ($TPreviousIncoice as &$fac) {
 				if ($posy > $this->page_hauteur - 4 - $this->heightforfooter) {
 				if ($posy > $this->page_hauteur - 4 - $this->heightforfooter) {
-					$this->_pagefoot($pdf, $object, $outputlangs, 1);
+					$this->_pagefoot($pdf, $object, $outputlangs, 1, $this->getHeightForQRInvoice($pdf->getPage(), $object, $outputlangs));
 					$pdf->addPage();
 					$pdf->addPage();
 					if (!getDolGlobalInt('MAIN_PDF_DONOTREPEAT_HEAD')) {
 					if (!getDolGlobalInt('MAIN_PDF_DONOTREPEAT_HEAD')) {
 						$this->_pagehead($pdf, $object, 0, $outputlangs, $outputlangsbis);
 						$this->_pagehead($pdf, $object, 0, $outputlangs, $outputlangsbis);
@@ -2442,12 +2456,13 @@ class pdf_sponge extends ModelePDFFactures
 	 * 		@param	Facture		$object				Object to show
 	 * 		@param	Facture		$object				Object to show
 	 *      @param	Translate	$outputlangs		Object lang for output
 	 *      @param	Translate	$outputlangs		Object lang for output
 	 *      @param	int			$hidefreetext		1=Hide free text
 	 *      @param	int			$hidefreetext		1=Hide free text
+	 *      @param	int			$heightforqrinvoice	Height for QR invoices
 	 *      @return	int								Return height of bottom margin including footer text
 	 *      @return	int								Return height of bottom margin including footer text
 	 */
 	 */
-	protected function _pagefoot(&$pdf, $object, $outputlangs, $hidefreetext = 0)
+	protected function _pagefoot(&$pdf, $object, $outputlangs, $hidefreetext = 0, $heightforqrinvoice = 0)
 	{
 	{
 		$showdetails = getDolGlobalInt('MAIN_GENERATE_DOCUMENTS_SHOW_FOOT_DETAILS', 0);
 		$showdetails = getDolGlobalInt('MAIN_GENERATE_DOCUMENTS_SHOW_FOOT_DETAILS', 0);
-		return pdf_pagefoot($pdf, $outputlangs, 'INVOICE_FREE_TEXT', $this->emetteur, $this->marge_basse, $this->marge_gauche, $this->page_hauteur, $object, $showdetails, $hidefreetext, $this->page_largeur, $this->watermark);
+		return pdf_pagefoot($pdf, $outputlangs, 'INVOICE_FREE_TEXT', $this->emetteur, $heightforqrinvoice + $this->marge_basse, $this->marge_gauche, $this->page_hauteur, $object, $showdetails, $hidefreetext, $this->page_largeur, $this->watermark);
 	}
 	}
 
 
 	/**
 	/**

+ 155 - 0
htdocs/core/modules/facture/modules_facture.php

@@ -31,6 +31,7 @@ require_once DOL_DOCUMENT_ROOT.'/core/class/commondocgenerator.class.php';
 require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
 require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
 require_once DOL_DOCUMENT_ROOT.'/compta/bank/class/account.class.php'; // Required because used in classes that inherit
 require_once DOL_DOCUMENT_ROOT.'/compta/bank/class/account.class.php'; // Required because used in classes that inherit
 
 
+use \Sprain\SwissQrBill;
 
 
 /**
 /**
  *	Parent class of invoice document generators
  *	Parent class of invoice document generators
@@ -78,6 +79,160 @@ abstract class ModelePDFFactures extends CommonDocGenerator
 
 
 		return $list;
 		return $list;
 	}
 	}
+
+	/**
+	 * Get the SwissQR object, including validation
+	 *
+	 * @param 	Facture 				$object  	Invoice object
+	 * @param 	Translate 				$langs 		Translation object
+	 * @return 	SwissQrBill\QrBill|bool 			The valid SwissQR object, or false
+	 */
+	private function getSwissQrBill(Facture $object, Translate $langs)
+	{
+		if (getDolGlobalString('INVOICE_ADD_SWISS_QR_CODE') != 'bottom') {
+			return false;
+		}
+
+		if ($object->mode_reglement_code != 'VIR') {
+			$this->error = $langs->transnoentities("SwissQrOnlyVIR");
+			return false;
+		}
+
+		if (empty($object->fk_account)) {
+			$this->error = 'Bank account must be defined to use this experimental feature';
+			return false;
+		}
+
+		require_once DOL_DOCUMENT_ROOT.'/includes/sprain/swiss-qr-bill/autoload.php';
+
+		// Create a new instance of SwissQrBill, containing default headers with fixed values
+		$qrBill = SwissQrBill\QrBill::create();
+
+		// First, check creditor address
+		$address = SwissQrBill\DataGroup\Element\CombinedAddress::create(
+			$this->emetteur->name,
+			$this->emetteur->address,
+			$this->emetteur->zip . " " . $this->emetteur->town,
+			$this->emetteur->country_code
+		);
+		if (!$address->isValid()) {
+			$this->error = $langs->transnoentities("SwissQrCreditorAddressInvalid", (string) $address->getViolations());
+			return false;
+		}
+		$qrBill->setCreditor($address);
+
+		// Get IBAN from account.
+		$account = new Account($this->db);
+		$account->fetch($object->fk_account);
+
+		$creditorInformation = SwissQrBill\DataGroup\Element\CreditorInformation::create($account->iban);
+		if (!$creditorInformation->isValid()) {
+			$this->error = $langs->transnoentities("SwissQrCreditorInformationInvalid", $account->iban, (string) $creditorInformation->getViolations());
+			return false;
+		}
+		$qrBill->setCreditorInformation($creditorInformation);
+
+		if ($creditorInformation->containsQrIban()) {
+			$this->error = $langs->transnoentities("SwissQrIbanNotImplementedYet", $account->iban);
+			return false;
+		}
+
+		// Add payment reference CLASSIC-IBAN
+		// This is what you will need to identify incoming payments.
+		$qrBill->setPaymentReference(
+			SwissQrBill\DataGroup\Element\PaymentReference::create(
+				SwissQrBill\DataGroup\Element\PaymentReference::TYPE_NON
+			)
+		);
+
+		// Add payment amount, with currency
+		$pai = SwissQrBill\DataGroup\Element\PaymentAmountInformation::create($object->multicurrency_code, $object->total_ttc);
+		if (!$pai->isValid()) {
+			$this->error = $langs->transnoentities("SwissQrPaymentInformationInvalid", $object->total_ttc, (string) $pai->getViolations());
+			return false;
+		}
+		$qrBill->setPaymentAmountInformation($pai);
+
+		// Add some human-readable information about what the bill is for.
+		$qrBill->setAdditionalInformation(
+			SwissQrBill\DataGroup\Element\AdditionalInformation::create(
+				$object->ref
+			)
+		);
+
+		// Check debtor address; We _know_ zip&town have to be filled, so skip that if unfilled.
+		if (!empty($object->thirdparty->zip) && !empty($object->thirdparty->town)) {
+			$address = SwissQrBill\DataGroup\Element\CombinedAddress::create(
+				$object->thirdparty->name,
+				$object->thirdparty->address,
+				$object->thirdparty->zip . " " . $object->thirdparty->town,
+				$object->thirdparty->country_code
+			);
+			if (!$address->isValid()) {
+				$this->error = $langs->transnoentities("SwissQrDebitorAddressInvalid", (string) $address->getViolations());
+				return false;
+			}
+			$qrBill->setUltimateDebtor($address);
+		}
+
+		return $qrBill;
+	}
+
+	/**
+	 * Get the height for bottom-page QR invoice in mm, depending on the page number.
+	 *
+	 * @param int       $pagenbr Page number
+	 * @param Facture   $object  Invoice object
+	 * @param Translate $langs   Translation object
+	 * @return int      Height in mm of the bottom-page QR invoice. Can be zero if not on right page; not enabled
+	 */
+	protected function getHeightForQRInvoice(int $pagenbr, \Facture $object, \Translate $langs) : int
+	{
+		if (getDolGlobalString('INVOICE_ADD_SWISS_QR_CODE') == 'bottom') {
+			// Keep it, to reset it after QRinvoice getter
+			$error = $this->error;
+
+			if (!$this->getSwissQrBill($object, $langs)) {
+				// Reset error to previous one if exists
+				if (!empty($error)) {
+					$this->error = $error;
+				}
+				return 0;
+			}
+			// SWIFT's requirementis 105, but we get more room with 100 and the page number is in a nice place.
+			return $pagenbr == 1 ? 100 : 0;
+		}
+
+		return 0;
+	}
+
+	/**
+	 * Add SwissQR invoice at bottom of page 1
+	 *
+	 * @param TCPDF     $pdf     TCPDF object
+	 * @param Facture   $object  Invoice object
+	 * @param Translate $langs   Translation object
+	 * @return bool for success
+	 */
+	public function addBottomQRInvoice(\TCPDF $pdf, \Facture $object, \Translate $langs) : bool
+	{
+		if (!($qrBill = $this->getSwissQrBill($object, $langs))) {
+			return false;
+		}
+
+		try {
+			$pdf->startTransaction();
+
+			$pdf->setPage(1);
+			$pdf->SetTextColor(0, 0, 0);
+			$output = new SwissQrBill\PaymentPart\Output\TcPdfOutput\TcPdfOutput($qrBill, in_array($langs->shortlang, ['de', 'fr', 'it']) ? $langs->shortlang : 'en', $pdf);
+			$output->setPrintable(false)->getPaymentPart();
+		} catch (Exception $e) {
+			$pdf->rollbackTransaction(true);
+			return false;
+		}
+		return true;
+	}
 }
 }
 
 
 /**
 /**

+ 4 - 2
htdocs/core/modules/modEmailCollector.class.php

@@ -296,8 +296,9 @@ class modEmailCollector extends DolibarrModules
 				$sqlforexampleFilterC3 = "INSERT INTO ".MAIN_DB_PREFIX."emailcollector_emailcollectorfilter (fk_emailcollector, type, rulevalue, date_creation, fk_user_creat, status)";
 				$sqlforexampleFilterC3 = "INSERT INTO ".MAIN_DB_PREFIX."emailcollector_emailcollectorfilter (fk_emailcollector, type, rulevalue, date_creation, fk_user_creat, status)";
 				$sqlforexampleFilterC3 .= " VALUES ((SELECT rowid FROM ".MAIN_DB_PREFIX."emailcollector_emailcollector WHERE ref = 'Collect_Leads' and entity = ".((int) $conf->entity)."), 'to', 'sales@example.com', '".$this->db->idate(dol_now())."', ".((int) $user->id).", 1)";
 				$sqlforexampleFilterC3 .= " VALUES ((SELECT rowid FROM ".MAIN_DB_PREFIX."emailcollector_emailcollector WHERE ref = 'Collect_Leads' and entity = ".((int) $conf->entity)."), 'to', 'sales@example.com', '".$this->db->idate(dol_now())."', ".((int) $user->id).", 1)";
 
 
+				$paramstring = 'tmp_from=EXTRACT:HEADER:^From:(.*)'."\n".'socid=SETIFEMPTY:1'."\n".'usage_opportunity=SET:1'."\n".'description=EXTRACT:BODY:(.*)'."\n".'title=SET:Lead or message from __tmp_from__ received by email';
 				$sqlforexampleActionC4 = "INSERT INTO ".MAIN_DB_PREFIX."emailcollector_emailcollectoraction (fk_emailcollector, type, actionparam, date_creation, fk_user_creat, status)";
 				$sqlforexampleActionC4 = "INSERT INTO ".MAIN_DB_PREFIX."emailcollector_emailcollectoraction (fk_emailcollector, type, actionparam, date_creation, fk_user_creat, status)";
-				$sqlforexampleActionC4 .= " VALUES ((SELECT rowid FROM ".MAIN_DB_PREFIX."emailcollector_emailcollector WHERE ref = 'Collect_Leads' and entity = ".((int) $conf->entity)."), 'project', 'tmp_from=EXTRACT:HEADER:^From:(.*);socid=SETIFEMPTY:1;usage_opportunity=SET:1;description=EXTRACT:BODY:(.*);title=SET:Lead or message from __tmp_from__ received by email', '".$this->db->idate(dol_now())."', ".((int) $user->id).", 1)";
+				$sqlforexampleActionC4 .= " VALUES ((SELECT rowid FROM ".MAIN_DB_PREFIX."emailcollector_emailcollector WHERE ref = 'Collect_Leads' and entity = ".((int) $conf->entity)."), 'project', '".$this->db->escape($paramstring)."', '".$this->db->idate(dol_now())."', ".((int) $user->id).", 1)";
 
 
 				$sql[] = $sqlforexampleC1;
 				$sql[] = $sqlforexampleC1;
 
 
@@ -327,8 +328,9 @@ class modEmailCollector extends DolibarrModules
 				$sqlforexampleFilterC3 = "INSERT INTO ".MAIN_DB_PREFIX."emailcollector_emailcollectorfilter (fk_emailcollector, type, rulevalue, date_creation, fk_user_creat, status)";
 				$sqlforexampleFilterC3 = "INSERT INTO ".MAIN_DB_PREFIX."emailcollector_emailcollectorfilter (fk_emailcollector, type, rulevalue, date_creation, fk_user_creat, status)";
 				$sqlforexampleFilterC3 .= " VALUES ((SELECT rowid FROM ".MAIN_DB_PREFIX."emailcollector_emailcollector WHERE ref = 'Collect_Candidatures' and entity = ".((int) $conf->entity)."), 'to', 'jobs@example.com', '".$this->db->idate(dol_now())."', ".((int) $user->id).", 1)";
 				$sqlforexampleFilterC3 .= " VALUES ((SELECT rowid FROM ".MAIN_DB_PREFIX."emailcollector_emailcollector WHERE ref = 'Collect_Candidatures' and entity = ".((int) $conf->entity)."), 'to', 'jobs@example.com', '".$this->db->idate(dol_now())."', ".((int) $user->id).", 1)";
 
 
+				$paramstring = 'tmp_from=EXTRACT:HEADER:^From:(.*)(<.*>)?'."\n".'fk_recruitmentjobposition=EXTRACT:HEADER:^To:[^\n]*\+([^\n]*)'."\n".'description=EXTRACT:BODY:(.*)'."\n".'lastname=SET:__tmp_from__';
 				$sqlforexampleActionC4 = "INSERT INTO ".MAIN_DB_PREFIX."emailcollector_emailcollectoraction (fk_emailcollector, type, actionparam, date_creation, fk_user_creat, status)";
 				$sqlforexampleActionC4 = "INSERT INTO ".MAIN_DB_PREFIX."emailcollector_emailcollectoraction (fk_emailcollector, type, actionparam, date_creation, fk_user_creat, status)";
-				$sqlforexampleActionC4 .= " VALUES ((SELECT rowid FROM ".MAIN_DB_PREFIX."emailcollector_emailcollector WHERE ref = 'Collect_Candidatures' and entity = ".((int) $conf->entity)."), 'candidature', 'tmp_from=EXTRACT:HEADER:^From:(.*)(<.*>)?;fk_recruitmentjobposition=EXTRACT:HEADER:^To:[^\n]*\+([^\n]*);description=EXTRACT:BODY:(.*);lastname=SET:__tmp_from__', '".$this->db->idate(dol_now())."', ".((int) $user->id).", 1)";
+				$sqlforexampleActionC4 .= " VALUES ((SELECT rowid FROM ".MAIN_DB_PREFIX."emailcollector_emailcollector WHERE ref = 'Collect_Candidatures' and entity = ".((int) $conf->entity)."), 'candidature', '".$this->db->escape($paramstring)."', '".$this->db->idate(dol_now())."', ".((int) $user->id).", 1)";
 
 
 				$sql[] = $sqlforexampleC1;
 				$sql[] = $sqlforexampleC1;
 
 

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

@@ -592,7 +592,7 @@ class modFournisseur extends DolibarrModules
 		if (empty($conf->multicurrency->enabled)) {
 		if (empty($conf->multicurrency->enabled)) {
 			$this->import_fieldshidden_array[$r]['f.multicurrency_code'] = 'const-'.$conf->currency;
 			$this->import_fieldshidden_array[$r]['f.multicurrency_code'] = 'const-'.$conf->currency;
 		}
 		}
-		$this->import_regex_array[$r] = array('f.ref' => '(SI\d{4}-\d{4}|PROV.{1,32}$)', 'f.multicurrency_code' => 'code@'.MAIN_DB_PREFIX.'multicurrency');
+		$this->import_regex_array[$r] = array('f.multicurrency_code' => 'code@'.MAIN_DB_PREFIX.'multicurrency');
 		$import_sample = array(
 		$import_sample = array(
 			'f.ref' => '(PROV001)',
 			'f.ref' => '(PROV001)',
 			'f.ref_supplier' => 'Supplier1',
 			'f.ref_supplier' => 'Supplier1',

+ 1 - 35
htdocs/core/modules/modWebhook.class.php

@@ -67,7 +67,7 @@ class modWebhook extends DolibarrModules
 		$this->descriptionlong = "WebhookDescription";
 		$this->descriptionlong = "WebhookDescription";
 
 
 		// Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated' or a version string like 'x.y.z'
 		// Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated' or a version string like 'x.y.z'
-		$this->version = 'experimental';
+		$this->version = 'dolibarr';
 		// Url to the file with your last numberversion of this module
 		// Url to the file with your last numberversion of this module
 		//$this->url_last_version = 'http://www.example.com/versionmodule.txt';
 		//$this->url_last_version = 'http://www.example.com/versionmodule.txt';
 
 
@@ -473,40 +473,6 @@ class modWebhook extends DolibarrModules
 
 
 		$sql = array();
 		$sql = array();
 
 
-		// Document templates
-		$moduledir = dol_sanitizeFileName('webhook');
-		$myTmpObjects = array();
-		$myTmpObjects['Webhook_target'] = array('includerefgeneration'=>0, 'includedocgeneration'=>0);
-
-		foreach ($myTmpObjects as $myTmpObjectKey => $myTmpObjectArray) {
-			if ($myTmpObjectKey == 'Webhook_target') {
-				continue;
-			}
-			if ($myTmpObjectArray['includerefgeneration']) {
-				$src = DOL_DOCUMENT_ROOT.'/install/doctemplates/'.$moduledir.'/template_webhook_targets.odt';
-				$dirodt = DOL_DATA_ROOT.'/doctemplates/'.$moduledir;
-				$dest = $dirodt.'/template_webhook_targets.odt';
-
-				if (file_exists($src) && !file_exists($dest)) {
-					require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
-					dol_mkdir($dirodt);
-					$result = dol_copy($src, $dest, 0, 0);
-					if ($result < 0) {
-						$langs->load("errors");
-						$this->error = $langs->trans('ErrorFailToCopyFile', $src, $dest);
-						return 0;
-					}
-				}
-
-				$sql = array_merge($sql, array(
-					"DELETE FROM ".MAIN_DB_PREFIX."document_model WHERE nom = 'standard_".strtolower($myTmpObjectKey)."' AND type = '".$this->db->escape(strtolower($myTmpObjectKey))."' AND entity = ".((int) $conf->entity),
-					"INSERT INTO ".MAIN_DB_PREFIX."document_model (nom, type, entity) VALUES('standard_".strtolower($myTmpObjectKey)."', '".$this->db->escape(strtolower($myTmpObjectKey))."', ".((int) $conf->entity).")",
-					"DELETE FROM ".MAIN_DB_PREFIX."document_model WHERE nom = 'generic_".strtolower($myTmpObjectKey)."_odt' AND type = '".$this->db->escape(strtolower($myTmpObjectKey))."' AND entity = ".((int) $conf->entity),
-					"INSERT INTO ".MAIN_DB_PREFIX."document_model (nom, type, entity) VALUES('generic_".strtolower($myTmpObjectKey)."_odt', '".$this->db->escape(strtolower($myTmpObjectKey))."', ".((int) $conf->entity).")"
-				));
-			}
-		}
-
 		return $this->_init($sql, $options);
 		return $this->_init($sql, $options);
 	}
 	}
 
 

+ 2 - 1
htdocs/core/modules/modWorkstation.class.php

@@ -41,6 +41,7 @@ class modWorkstation extends DolibarrModules
 	public function __construct($db)
 	public function __construct($db)
 	{
 	{
 		global $langs, $conf;
 		global $langs, $conf;
+
 		$this->db = $db;
 		$this->db = $db;
 
 
 		// Id for module (must be unique).
 		// Id for module (must be unique).
@@ -62,7 +63,7 @@ class modWorkstation extends DolibarrModules
 		// Used only if file README.md and README-LL.md not found.
 		// Used only if file README.md and README-LL.md not found.
 		$this->descriptionlong = "WorkstationsDescription";
 		$this->descriptionlong = "WorkstationsDescription";
 		// Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated' or a version string like 'x.y.z'
 		// Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated' or a version string like 'x.y.z'
-		$this->version = 'experimental';
+		$this->version = 'dolibarr';
 		// Url to the file with your last numberversion of this module
 		// Url to the file with your last numberversion of this module
 		//$this->url_last_version = 'http://www.example.com/versionmodule.txt';
 		//$this->url_last_version = 'http://www.example.com/versionmodule.txt';
 
 

+ 5 - 2
htdocs/core/modules/supplier_invoice/mod_facture_fournisseur_cactus.php

@@ -88,7 +88,7 @@ class mod_facture_fournisseur_cactus extends ModeleNumRefSuppliersInvoices
 
 
 
 
 	/**
 	/**
-	 * 	Tests if the numbers already in force in the database do not cause conflicts that would prevent this numbering.
+	 * 	Tests if the numbers already in the database do not cause conflicts that would prevent this numbering.
 	 *
 	 *
 	 *  @return     boolean     false if conflict, true if ok
 	 *  @return     boolean     false if conflict, true if ok
 	 */
 	 */
@@ -164,6 +164,8 @@ class mod_facture_fournisseur_cactus extends ModeleNumRefSuppliersInvoices
 			$this->error = $langs->trans('ErrorNumRefModel', $max);
 			$this->error = $langs->trans('ErrorNumRefModel', $max);
 			return false;
 			return false;
 		}
 		}
+
+		return true;
 	}
 	}
 
 
 	/**
 	/**
@@ -172,7 +174,7 @@ class mod_facture_fournisseur_cactus extends ModeleNumRefSuppliersInvoices
 	 * @param	Societe		$objsoc     Object third party
 	 * @param	Societe		$objsoc     Object third party
 	 * @param  	Object		$object		Object invoice
 	 * @param  	Object		$object		Object invoice
 	 * @param   string		$mode       'next' for next value or 'last' for last value
 	 * @param   string		$mode       'next' for next value or 'last' for last value
-	 * @return 	string      			Value if OK, 0 if KO
+	 * @return 	string      			Value if OK, <=0 if KO
 	 */
 	 */
 	public function getNextValue($objsoc, $object, $mode = 'next')
 	public function getNextValue($objsoc, $object, $mode = 'next')
 	{
 	{
@@ -244,6 +246,7 @@ class mod_facture_fournisseur_cactus extends ModeleNumRefSuppliersInvoices
 			return $prefix.$yymm."-".$num;
 			return $prefix.$yymm."-".$num;
 		} else {
 		} else {
 			dol_print_error('', 'Bad parameter for getNextValue');
 			dol_print_error('', 'Bad parameter for getNextValue');
+			retun -1;
 		}
 		}
 	}
 	}
 
 

+ 1 - 1
htdocs/core/tpl/list_print_total.tpl.php

@@ -13,7 +13,7 @@ if (isset($totalarray['pos'])) {
 	while ($i < $totalarray['nbfield']) {
 	while ($i < $totalarray['nbfield']) {
 		$i++;
 		$i++;
 		if (!empty($totalarray['pos'][$i])) {
 		if (!empty($totalarray['pos'][$i])) {
-			switch ($totalarray['type'][$i]) {
+			switch ($totalarray['pos'][$i]) {
 				case 'duration';
 				case 'duration';
 					print '<td class="right">';
 					print '<td class="right">';
 					print (!empty($totalarray['val'][$totalarray['pos'][$i]]) ? convertSecondToTime($totalarray['val'][$totalarray['pos'][$i]], 'allhourmin') : 0);
 					print (!empty($totalarray['val'][$totalarray['pos'][$i]]) ? convertSecondToTime($totalarray['val'][$totalarray['pos'][$i]], 'allhourmin') : 0);

+ 13 - 10
htdocs/emailcollector/class/emailcollector.class.php

@@ -131,6 +131,8 @@ class EmailCollector extends CommonObject
 		'host'          => array('type'=>'varchar(255)', 'label'=>'EMailHost', 'visible'=>1, 'enabled'=>1, 'position'=>90, 'notnull'=>1, 'searchall'=>1, 'comment'=>"IMAP server", 'help'=>'Example: imap.gmail.com', 'csslist'=>'tdoverflowmax125'),
 		'host'          => array('type'=>'varchar(255)', 'label'=>'EMailHost', 'visible'=>1, 'enabled'=>1, 'position'=>90, 'notnull'=>1, 'searchall'=>1, 'comment'=>"IMAP server", 'help'=>'Example: imap.gmail.com', 'csslist'=>'tdoverflowmax125'),
 		'port'          => array('type'=>'varchar(10)', 'label'=>'EMailHostPort', 'visible'=>1, 'enabled'=>1, 'position'=>91, 'notnull'=>1, 'searchall'=>0, 'comment'=>"IMAP server port", 'help'=>'Example: 993', 'csslist'=>'tdoverflowmax50', 'default'=>'993'),
 		'port'          => array('type'=>'varchar(10)', 'label'=>'EMailHostPort', 'visible'=>1, 'enabled'=>1, 'position'=>91, 'notnull'=>1, 'searchall'=>0, 'comment'=>"IMAP server port", 'help'=>'Example: 993', 'csslist'=>'tdoverflowmax50', 'default'=>'993'),
 		'hostcharset'   => array('type'=>'varchar(16)', 'label'=>'HostCharset', 'visible'=>-1, 'enabled'=>1, 'position'=>92, 'notnull'=>0, 'searchall'=>0, 'comment'=>"IMAP server charset", 'help'=>'Example: "UTF-8" (May be "US-ASCII" with some Office365)', 'default'=>'UTF-8'),
 		'hostcharset'   => array('type'=>'varchar(16)', 'label'=>'HostCharset', 'visible'=>-1, 'enabled'=>1, 'position'=>92, 'notnull'=>0, 'searchall'=>0, 'comment'=>"IMAP server charset", 'help'=>'Example: "UTF-8" (May be "US-ASCII" with some Office365)', 'default'=>'UTF-8'),
+		'imap_encryption'  => array('type'=>'varchar(16)', 'label'=>'ImapEncryption', 'visible'=>-1, 'enabled'=>1, 'position'=>93, 'searchall'=>0, 'comment'=>"IMAP encryption", 'help'=>'ImapEncryptionHelp', 'arrayofkeyval'=> array('ssl'=>'SSL', 'tls' => 'TLS', 'notls' => 'NOTLS'), 'default'=>'ssl'),
+		'norsh'  => array('type'=>'integer', 'label'=>'NoRSH', 'visible'=>-1, 'enabled'=>"!getDolGlobalInt('MAIN_IMAP_USE_PHPIMAP')", 'position'=>94, 'searchall'=>0, 'help'=>'NoRSHHelp', 'arrayofkeyval'=> array(0 =>'No', 1 => 'Yes'), 'default'=> 0),
 		'acces_type'     => array('type'=>'integer', 'label'=>'accessType', 'visible'=>-1, 'enabled'=>"getDolGlobalInt('MAIN_IMAP_USE_PHPIMAP')", 'position'=>101, 'notnull'=>1, 'index'=>1, 'comment'=>"IMAP login type", 'arrayofkeyval'=>array('0'=>'loginPassword', '1'=>'oauthToken'), 'default'=>'0', 'help'=>''),
 		'acces_type'     => array('type'=>'integer', 'label'=>'accessType', 'visible'=>-1, 'enabled'=>"getDolGlobalInt('MAIN_IMAP_USE_PHPIMAP')", 'position'=>101, 'notnull'=>1, 'index'=>1, 'comment'=>"IMAP login type", 'arrayofkeyval'=>array('0'=>'loginPassword', '1'=>'oauthToken'), 'default'=>'0', 'help'=>''),
 		'login'         => array('type'=>'varchar(128)', 'label'=>'Login', 'visible'=>-1, 'enabled'=>1, 'position'=>102, 'notnull'=>-1, 'index'=>1, 'comment'=>"IMAP login", 'help'=>'Example: myaccount@gmail.com'),
 		'login'         => array('type'=>'varchar(128)', 'label'=>'Login', 'visible'=>-1, 'enabled'=>1, 'position'=>102, 'notnull'=>-1, 'index'=>1, 'comment'=>"IMAP login", 'help'=>'Example: myaccount@gmail.com'),
 		'password'      => array('type'=>'password', 'label'=>'Password', 'visible'=>-1, 'enabled'=>"1", 'position'=>103, 'notnull'=>-1, 'comment'=>"IMAP password", 'help'=>'WithGMailYouCanCreateADedicatedPassword'),
 		'password'      => array('type'=>'password', 'label'=>'Password', 'visible'=>-1, 'enabled'=>"1", 'position'=>103, 'notnull'=>-1, 'comment'=>"IMAP password", 'help'=>'WithGMailYouCanCreateADedicatedPassword'),
@@ -213,6 +215,8 @@ class EmailCollector extends CommonObject
 	public $password;
 	public $password;
 	public $acces_type;
 	public $acces_type;
 	public $oauth_service;
 	public $oauth_service;
+	public $imap_encryption;
+	public $norsh;
 	public $source_directory;
 	public $source_directory;
 	public $target_directory;
 	public $target_directory;
 	public $maxemailpercollect;
 	public $maxemailpercollect;
@@ -775,11 +779,9 @@ class EmailCollector extends CommonObject
 	/**
 	/**
 	 * Return the connectstring to use with IMAP connection function
 	 * Return the connectstring to use with IMAP connection function
 	 *
 	 *
-	 * @param	int		$ssl		Add /ssl tag
-	 * @param	int		$norsh		Add /norsh to connectstring
 	 * @return string
 	 * @return string
 	 */
 	 */
-	public function getConnectStringIMAP($ssl = 1, $norsh = 0)
+	public function getConnectStringIMAP()
 	{
 	{
 		global $conf;
 		global $conf;
 
 
@@ -787,15 +789,16 @@ class EmailCollector extends CommonObject
 		$flags = '/service=imap'; // IMAP
 		$flags = '/service=imap'; // IMAP
 		if (!empty($conf->global->IMAP_FORCE_TLS)) {
 		if (!empty($conf->global->IMAP_FORCE_TLS)) {
 			$flags .= '/tls';
 			$flags .= '/tls';
-		} elseif (empty($conf->global->IMAP_FORCE_NOSSL)) {
-			if ($ssl) {
-				$flags .= '/ssl';
-			}
+		} elseif (empty($this->imap_encryption) || ($this->imap_encryption == 'ssl' && !empty($conf->global->IMAP_FORCE_NOSSL))) {
+			$flags .= '';
+		} else {
+			$flags .= '/' . $this->imap_encryption;
 		}
 		}
+
 		$flags .= '/novalidate-cert';
 		$flags .= '/novalidate-cert';
 		//$flags.='/readonly';
 		//$flags.='/readonly';
 		//$flags.='/debug';
 		//$flags.='/debug';
-		if ($norsh || !empty($conf->global->IMAP_FORCE_NORSH)) {
+		if (!empty($this->norsh) || !empty($conf->global->IMAP_FORCE_NORSH)) {
 			$flags .= '/norsh';
 			$flags .= '/norsh';
 		}
 		}
 		//Used in shared mailbox from Office365
 		//Used in shared mailbox from Office365
@@ -1191,7 +1194,7 @@ class EmailCollector extends CommonObject
 				$client = $cm->make([
 				$client = $cm->make([
 					'host'           => $this->host,
 					'host'           => $this->host,
 					'port'           => $this->port,
 					'port'           => $this->port,
-					'encryption'     => 'ssl',
+					'encryption'     => !empty($this->imap_encryption) ? $this->imap_encryption : false,
 					'validate_cert'  => true,
 					'validate_cert'  => true,
 					'protocol'       => 'imap',
 					'protocol'       => 'imap',
 					'username'       => $this->login,
 					'username'       => $this->login,
@@ -1204,7 +1207,7 @@ class EmailCollector extends CommonObject
 				$client = $cm->make([
 				$client = $cm->make([
 					'host'           => $this->host,
 					'host'           => $this->host,
 					'port'           => $this->port,
 					'port'           => $this->port,
-					'encryption'     => 'ssl',
+					'encryption'     => !empty($this->imap_encryption) ? $this->imap_encryption : false,
 					'validate_cert'  => true,
 					'validate_cert'  => true,
 					'protocol'       => 'imap',
 					'protocol'       => 'imap',
 					'username'       => $this->login,
 					'username'       => $this->login,

+ 1 - 1
htdocs/expedition/card.php

@@ -387,7 +387,7 @@ if (empty($reshook)) {
 								$entrepot_id = 0;
 								$entrepot_id = 0;
 							}
 							}
 
 
-							$ret = $object->addline($entrepot_id, GETPOST($idl, 'int'), GETPOST($qty, 'int'), $array_options[$i]);
+							$ret = $object->addline($entrepot_id, GETPOST($idl, 'int'), GETPOSTINT($qty), $array_options[$i]);
 							if ($ret < 0) {
 							if ($ret < 0) {
 								setEventMessages($object->error, $object->errors, 'errors');
 								setEventMessages($object->error, $object->errors, 'errors');
 								$error++;
 								$error++;

+ 6 - 6
htdocs/expedition/dispatch.php

@@ -684,7 +684,7 @@ if ($id > 0 || !empty($ref)) {
 							print $linktoprod;
 							print $linktoprod;
 							print "</td>";
 							print "</td>";
 						}
 						}
-						
+
 						// Define unit price for PMP calculation
 						// Define unit price for PMP calculation
 						$up_ht_disc = $objp->subprice;
 						$up_ht_disc = $objp->subprice;
 						if (!empty($objp->remise_percent) && empty($conf->global->STOCK_EXCLUDE_DISCOUNT_FOR_PMP)) {
 						if (!empty($objp->remise_percent) && empty($conf->global->STOCK_EXCLUDE_DISCOUNT_FOR_PMP)) {
@@ -722,7 +722,7 @@ if ($id > 0 || !empty($ref)) {
 							while ($j < $numd) {
 							while ($j < $numd) {
 								$suffix = "_".$j."_".$i;
 								$suffix = "_".$j."_".$i;
 								$objd = $db->fetch_object($resultsql);
 								$objd = $db->fetch_object($resultsql);
-								
+
 								if (isModEnabled('productbatch') && !empty($objd->batch)) {
 								if (isModEnabled('productbatch') && !empty($objd->batch)) {
 									$type = 'batch';
 									$type = 'batch';
 
 
@@ -753,10 +753,10 @@ if ($id > 0 || !empty($ref)) {
 									print '<input id="fk_commandedet'.$suffix.'" name="fk_commandedet'.$suffix.'" type="hidden" value="'.$objp->rowid.'">';
 									print '<input id="fk_commandedet'.$suffix.'" name="fk_commandedet'.$suffix.'" type="hidden" value="'.$objp->rowid.'">';
 									print '<input id="idline'.$suffix.'" name="idline'.$suffix.'" type="hidden" value="'.$objd->rowid.'">';
 									print '<input id="idline'.$suffix.'" name="idline'.$suffix.'" type="hidden" value="'.$objd->rowid.'">';
 									print '<input name="product_batch'.$suffix.'" type="hidden" value="'.$objd->fk_product.'">';
 									print '<input name="product_batch'.$suffix.'" type="hidden" value="'.$objd->fk_product.'">';
-									
+
 									print '<!-- This is a U.P. (may include discount or not depending on STOCK_EXCLUDE_DISCOUNT_FOR_PMP. will be used for PMP calculation) -->';
 									print '<!-- This is a U.P. (may include discount or not depending on STOCK_EXCLUDE_DISCOUNT_FOR_PMP. will be used for PMP calculation) -->';
 									print '<input class="maxwidth75" name="pu'.$suffix.'" type="hidden" value="'.price2num($up_ht_disc, 'MU').'">';
 									print '<input class="maxwidth75" name="pu'.$suffix.'" type="hidden" value="'.price2num($up_ht_disc, 'MU').'">';
-									
+
 									print '</td>';
 									print '</td>';
 
 
 									print '<td>';
 									print '<td>';
@@ -866,7 +866,7 @@ if ($id > 0 || !empty($ref)) {
 							}
 							}
 							$suffix = "_".$j."_".$i;
 							$suffix = "_".$j."_".$i;
 						}
 						}
-						
+
 						if ($j == 0) {
 						if ($j == 0) {
 							if (isModEnabled('productbatch') && !empty($objp->tobatch)) {
 							if (isModEnabled('productbatch') && !empty($objp->tobatch)) {
 								$type = 'batch';
 								$type = 'batch';
@@ -972,7 +972,7 @@ if ($id > 0 || !empty($ref)) {
 							}
 							}
 
 
 							print '</td>';
 							print '</td>';
-							
+
 							// Warehouse
 							// Warehouse
 							print '<td class="right">';
 							print '<td class="right">';
 							if (count($listwarehouses) > 1) {
 							if (count($listwarehouses) > 1) {

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

@@ -3622,7 +3622,7 @@ if ($action == 'create') {
 					print '<td class="right">'.price($obj->amount_ttc).'</td>';
 					print '<td class="right">'.price($obj->amount_ttc).'</td>';
 					print '<td class="right">';
 					print '<td class="right">';
 					print '<a href="'.$_SERVER["PHP_SELF"].'?facid='.$object->id.'&action=unlinkdiscount&discountid='.$obj->rowid.'">';
 					print '<a href="'.$_SERVER["PHP_SELF"].'?facid='.$object->id.'&action=unlinkdiscount&discountid='.$obj->rowid.'">';
-					print img_picto($langs->trans("RemoveDiscount"), 'unlink');
+					print img_picto($langs->transnoentitiesnoconv("RemoveDiscount"), 'unlink');
 					print '</a>';
 					print '</a>';
 					print '</td></tr>';
 					print '</td></tr>';
 					$i++;
 					$i++;

+ 1 - 43
htdocs/holiday/class/holiday.class.php

@@ -1695,33 +1695,7 @@ class Holiday extends CommonObject
 	}
 	}
 
 
 	/**
 	/**
-	 *	Retourne un checked si vrai
-	 *
-	 *  @param	string	$name       name du paramètre de configuration
-	 *  @return string      		retourne checked si > 0
-	 */
-	public function getCheckOption($name)
-	{
-
-		$sql = "SELECT value";
-		$sql .= " FROM ".MAIN_DB_PREFIX."holiday_config";
-		$sql .= " WHERE name = '".$this->db->escape($name)."'";
-
-		$result = $this->db->query($sql);
-
-		if ($result) {
-			$obj = $this->db->fetch_object($result);
-
-			// Si la valeur est 1 on retourne checked
-			if ($obj->value) {
-				return 'checked';
-			}
-		}
-	}
-
-
-	/**
-	 *  Créer les entrées pour chaque utilisateur au moment de la configuration
+	 *  Create entries for each user at setup step
 	 *
 	 *
 	 *  @param	boolean		$single		Single
 	 *  @param	boolean		$single		Single
 	 *  @param	int			$userid		Id user
 	 *  @param	int			$userid		Id user
@@ -1756,22 +1730,6 @@ class Holiday extends CommonObject
 		}
 		}
 	}
 	}
 
 
-	/**
-	 *  Supprime un utilisateur du module Congés Payés
-	 *
-	 *  @param	int		$user_id        ID de l'utilisateur à supprimer
-	 *  @return boolean      			Vrai si pas d'erreur, faut si Erreur
-	 */
-	public function deleteCPuser($user_id)
-	{
-
-		$sql = "DELETE FROM ".MAIN_DB_PREFIX."holiday_users";
-		$sql .= " WHERE fk_user = ".((int) $user_id);
-
-		$this->db->query($sql);
-	}
-
-
 	/**
 	/**
 	 *  Return balance of holiday for one user
 	 *  Return balance of holiday for one user
 	 *
 	 *

+ 6 - 5
htdocs/holiday/list.php

@@ -511,7 +511,7 @@ if ($id > 0) {		// For user tab
 
 
 		print '<br>';
 		print '<br>';
 
 
-		showMyBalance($object, $user_id);
+		print showMyBalance($object, $user_id);
 	}
 	}
 
 
 	print dol_get_fiche_end();
 	print dol_get_fiche_end();
@@ -1109,9 +1109,9 @@ $db->close();
  */
  */
 function showMyBalance($holiday, $user_id)
 function showMyBalance($holiday, $user_id)
 {
 {
-	global $conf, $langs;
+	global $langs;
 
 
-	$alltypeleaves = $holiday->getTypes(1, -1); // To have labels
+	//$alltypeleaves = $holiday->getTypes(1, -1); // To have labels
 
 
 	$out = '';
 	$out = '';
 	$nb_holiday = 0;
 	$nb_holiday = 0;
@@ -1121,6 +1121,7 @@ function showMyBalance($holiday, $user_id)
 		$nb_holiday += $nb_type;
 		$nb_holiday += $nb_type;
 		$out .= ' - '.$val['label'].': <strong>'.($nb_type ?price2num($nb_type) : 0).'</strong><br>';
 		$out .= ' - '.$val['label'].': <strong>'.($nb_type ?price2num($nb_type) : 0).'</strong><br>';
 	}
 	}
-	print $langs->trans('SoldeCPUser', round($nb_holiday, 5)).'<br>';
-	print $out;
+	$out = $langs->trans('SoldeCPUser', round($nb_holiday, 5)).'<br>'.$out;
+
+	return $out;
 }
 }

+ 22 - 0
htdocs/includes/bacon/bacon-qr-code/LICENSE

@@ -0,0 +1,22 @@
+Copyright (c) 2017, Ben Scholzen 'DASPRiD'
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

+ 39 - 0
htdocs/includes/bacon/bacon-qr-code/README.md

@@ -0,0 +1,39 @@
+# QR Code generator
+
+[![PHP CI](https://github.com/Bacon/BaconQrCode/actions/workflows/ci.yml/badge.svg)](https://github.com/Bacon/BaconQrCode/actions/workflows/ci.yml)
+[![codecov](https://codecov.io/gh/Bacon/BaconQrCode/branch/master/graph/badge.svg?token=rD0HcAiEEx)](https://codecov.io/gh/Bacon/BaconQrCode)
+[![Latest Stable Version](https://poser.pugx.org/bacon/bacon-qr-code/v/stable)](https://packagist.org/packages/bacon/bacon-qr-code)
+[![Total Downloads](https://poser.pugx.org/bacon/bacon-qr-code/downloads)](https://packagist.org/packages/bacon/bacon-qr-code)
+[![License](https://poser.pugx.org/bacon/bacon-qr-code/license)](https://packagist.org/packages/bacon/bacon-qr-code)
+
+
+## Introduction
+BaconQrCode is a port of QR code portion of the ZXing library. It currently
+only features the encoder part, but could later receive the decoder part as
+well.
+
+As the Reed Solomon codec implementation of the ZXing library performs quite
+slow in PHP, it was exchanged with the implementation by Phil Karn.
+
+
+## Example usage
+```php
+use BaconQrCode\Renderer\ImageRenderer;
+use BaconQrCode\Renderer\Image\ImagickImageBackEnd;
+use BaconQrCode\Renderer\RendererStyle\RendererStyle;
+use BaconQrCode\Writer;
+
+$renderer = new ImageRenderer(
+    new RendererStyle(400),
+    new ImagickImageBackEnd()
+);
+$writer = new Writer($renderer);
+$writer->writeFile('Hello World!', 'qrcode.png');
+```
+
+## Available image renderer back ends
+BaconQrCode comes with multiple back ends for rendering images. Currently included are the following:
+
+- `ImagickImageBackEnd`: renders raster images using the Imagick library
+- `SvgImageBackEnd`: renders SVG files using XMLWriter
+- `EpsImageBackEnd`: renders EPS files

+ 44 - 0
htdocs/includes/bacon/bacon-qr-code/composer.json.disabled

@@ -0,0 +1,44 @@
+{
+    "name": "bacon/bacon-qr-code",
+    "description": "BaconQrCode is a QR code generator for PHP.",
+    "license" : "BSD-2-Clause",
+    "homepage": "https://github.com/Bacon/BaconQrCode",
+    "require": {
+        "php": "^7.1 || ^8.0",
+        "ext-iconv": "*",
+        "dasprid/enum": "^1.0.3"
+    },
+    "suggest": {
+        "ext-imagick": "to generate QR code images"
+    },
+    "authors": [
+        {
+            "name": "Ben Scholzen 'DASPRiD'",
+            "email": "mail@dasprids.de",
+            "homepage": "https://dasprids.de/",
+            "role": "Developer"
+        }
+    ],
+    "autoload": {
+        "psr-4": {
+            "BaconQrCode\\": "src/"
+        }
+    },
+    "require-dev": {
+        "phpunit/phpunit": "^7 | ^8 | ^9",
+        "spatie/phpunit-snapshot-assertions": "^4.2.9",
+        "squizlabs/php_codesniffer": "^3.4",
+        "phly/keep-a-changelog": "^2.1"
+    },
+    "config": {
+        "allow-plugins": {
+            "ocramius/package-versions": true
+        }
+    },
+    "archive": {
+        "exclude": [
+            "/test",
+            "/phpunit.xml.dist"
+        ]
+    }
+}

+ 372 - 0
htdocs/includes/bacon/bacon-qr-code/src/Common/BitArray.php

@@ -0,0 +1,372 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Common;
+
+use BaconQrCode\Exception\InvalidArgumentException;
+use SplFixedArray;
+
+/**
+ * A simple, fast array of bits.
+ */
+final class BitArray
+{
+    /**
+     * Bits represented as an array of integers.
+     *
+     * @var SplFixedArray<int>
+     */
+    private $bits;
+
+    /**
+     * Size of the bit array in bits.
+     *
+     * @var int
+     */
+    private $size;
+
+    /**
+     * Creates a new bit array with a given size.
+     */
+    public function __construct(int $size = 0)
+    {
+        $this->size = $size;
+        $this->bits = SplFixedArray::fromArray(array_fill(0, ($this->size + 31) >> 3, 0));
+    }
+
+    /**
+     * Gets the size in bits.
+     */
+    public function getSize() : int
+    {
+        return $this->size;
+    }
+
+    /**
+     * Gets the size in bytes.
+     */
+    public function getSizeInBytes() : int
+    {
+        return ($this->size + 7) >> 3;
+    }
+
+    /**
+     * Ensures that the array has a minimum capacity.
+     */
+    public function ensureCapacity(int $size) : void
+    {
+        if ($size > count($this->bits) << 5) {
+            $this->bits->setSize(($size + 31) >> 5);
+        }
+    }
+
+    /**
+     * Gets a specific bit.
+     */
+    public function get(int $i) : bool
+    {
+        return 0 !== ($this->bits[$i >> 5] & (1 << ($i & 0x1f)));
+    }
+
+    /**
+     * Sets a specific bit.
+     */
+    public function set(int $i) : void
+    {
+        $this->bits[$i >> 5] = $this->bits[$i >> 5] | 1 << ($i & 0x1f);
+    }
+
+    /**
+     * Flips a specific bit.
+     */
+    public function flip(int $i) : void
+    {
+        $this->bits[$i >> 5] ^= 1 << ($i & 0x1f);
+    }
+
+    /**
+     * Gets the next set bit position from a given position.
+     */
+    public function getNextSet(int $from) : int
+    {
+        if ($from >= $this->size) {
+            return $this->size;
+        }
+
+        $bitsOffset = $from >> 5;
+        $currentBits = $this->bits[$bitsOffset];
+        $bitsLength = count($this->bits);
+        $currentBits &= ~((1 << ($from & 0x1f)) - 1);
+
+        while (0 === $currentBits) {
+            if (++$bitsOffset === $bitsLength) {
+                return $this->size;
+            }
+
+            $currentBits = $this->bits[$bitsOffset];
+        }
+
+        $result = ($bitsOffset << 5) + BitUtils::numberOfTrailingZeros($currentBits);
+        return $result > $this->size ? $this->size : $result;
+    }
+
+    /**
+     * Gets the next unset bit position from a given position.
+     */
+    public function getNextUnset(int $from) : int
+    {
+        if ($from >= $this->size) {
+            return $this->size;
+        }
+
+        $bitsOffset = $from >> 5;
+        $currentBits = ~$this->bits[$bitsOffset];
+        $bitsLength = count($this->bits);
+        $currentBits &= ~((1 << ($from & 0x1f)) - 1);
+
+        while (0 === $currentBits) {
+            if (++$bitsOffset === $bitsLength) {
+                return $this->size;
+            }
+
+            $currentBits = ~$this->bits[$bitsOffset];
+        }
+
+        $result = ($bitsOffset << 5) + BitUtils::numberOfTrailingZeros($currentBits);
+        return $result > $this->size ? $this->size : $result;
+    }
+
+    /**
+     * Sets a bulk of bits.
+     */
+    public function setBulk(int $i, int $newBits) : void
+    {
+        $this->bits[$i >> 5] = $newBits;
+    }
+
+    /**
+     * Sets a range of bits.
+     *
+     * @throws InvalidArgumentException if end is smaller than start
+     */
+    public function setRange(int $start, int $end) : void
+    {
+        if ($end < $start) {
+            throw new InvalidArgumentException('End must be greater or equal to start');
+        }
+
+        if ($end === $start) {
+            return;
+        }
+
+        --$end;
+
+        $firstInt = $start >> 5;
+        $lastInt = $end >> 5;
+
+        for ($i = $firstInt; $i <= $lastInt; ++$i) {
+            $firstBit = $i > $firstInt ? 0 : $start & 0x1f;
+            $lastBit = $i < $lastInt ? 31 : $end & 0x1f;
+
+            if (0 === $firstBit && 31 === $lastBit) {
+                $mask = 0x7fffffff;
+            } else {
+                $mask = 0;
+
+                for ($j = $firstBit; $j < $lastBit; ++$j) {
+                    $mask |= 1 << $j;
+                }
+            }
+
+            $this->bits[$i] = $this->bits[$i] | $mask;
+        }
+    }
+
+    /**
+     * Clears the bit array, unsetting every bit.
+     */
+    public function clear() : void
+    {
+        $bitsLength = count($this->bits);
+
+        for ($i = 0; $i < $bitsLength; ++$i) {
+            $this->bits[$i] = 0;
+        }
+    }
+
+    /**
+     * Checks if a range of bits is set or not set.
+
+     * @throws InvalidArgumentException if end is smaller than start
+     */
+    public function isRange(int $start, int $end, bool $value) : bool
+    {
+        if ($end < $start) {
+            throw new InvalidArgumentException('End must be greater or equal to start');
+        }
+
+        if ($end === $start) {
+            return true;
+        }
+
+        --$end;
+
+        $firstInt = $start >> 5;
+        $lastInt = $end >> 5;
+
+        for ($i = $firstInt; $i <= $lastInt; ++$i) {
+            $firstBit = $i > $firstInt ? 0 : $start & 0x1f;
+            $lastBit = $i < $lastInt ? 31 : $end & 0x1f;
+
+            if (0 === $firstBit && 31 === $lastBit) {
+                $mask = 0x7fffffff;
+            } else {
+                $mask = 0;
+
+                for ($j = $firstBit; $j <= $lastBit; ++$j) {
+                    $mask |= 1 << $j;
+                }
+            }
+
+            if (($this->bits[$i] & $mask) !== ($value ? $mask : 0)) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Appends a bit to the array.
+     */
+    public function appendBit(bool $bit) : void
+    {
+        $this->ensureCapacity($this->size + 1);
+
+        if ($bit) {
+            $this->bits[$this->size >> 5] = $this->bits[$this->size >> 5] | (1 << ($this->size & 0x1f));
+        }
+
+        ++$this->size;
+    }
+
+    /**
+     * Appends a number of bits (up to 32) to the array.
+
+     * @throws InvalidArgumentException if num bits is not between 0 and 32
+     */
+    public function appendBits(int $value, int $numBits) : void
+    {
+        if ($numBits < 0 || $numBits > 32) {
+            throw new InvalidArgumentException('Num bits must be between 0 and 32');
+        }
+
+        $this->ensureCapacity($this->size + $numBits);
+
+        for ($numBitsLeft = $numBits; $numBitsLeft > 0; $numBitsLeft--) {
+            $this->appendBit((($value >> ($numBitsLeft - 1)) & 0x01) === 1);
+        }
+    }
+
+    /**
+     * Appends another bit array to this array.
+     */
+    public function appendBitArray(self $other) : void
+    {
+        $otherSize = $other->getSize();
+        $this->ensureCapacity($this->size + $other->getSize());
+
+        for ($i = 0; $i < $otherSize; ++$i) {
+            $this->appendBit($other->get($i));
+        }
+    }
+
+    /**
+     * Makes an exclusive-or comparision on the current bit array.
+     *
+     * @throws InvalidArgumentException if sizes don't match
+     */
+    public function xorBits(self $other) : void
+    {
+        $bitsLength = count($this->bits);
+        $otherBits  = $other->getBitArray();
+
+        if ($bitsLength !== count($otherBits)) {
+            throw new InvalidArgumentException('Sizes don\'t match');
+        }
+
+        for ($i = 0; $i < $bitsLength; ++$i) {
+            $this->bits[$i] = $this->bits[$i] ^ $otherBits[$i];
+        }
+    }
+
+    /**
+     * Converts the bit array to a byte array.
+     *
+     * @return SplFixedArray<int>
+     */
+    public function toBytes(int $bitOffset, int $numBytes) : SplFixedArray
+    {
+        $bytes = new SplFixedArray($numBytes);
+
+        for ($i = 0; $i < $numBytes; ++$i) {
+            $byte = 0;
+
+            for ($j = 0; $j < 8; ++$j) {
+                if ($this->get($bitOffset)) {
+                    $byte |= 1 << (7 - $j);
+                }
+
+                ++$bitOffset;
+            }
+
+            $bytes[$i] = $byte;
+        }
+
+        return $bytes;
+    }
+
+    /**
+     * Gets the internal bit array.
+     *
+     * @return SplFixedArray<int>
+     */
+    public function getBitArray() : SplFixedArray
+    {
+        return $this->bits;
+    }
+
+    /**
+     * Reverses the array.
+     */
+    public function reverse() : void
+    {
+        $newBits = new SplFixedArray(count($this->bits));
+
+        for ($i = 0; $i < $this->size; ++$i) {
+            if ($this->get($this->size - $i - 1)) {
+                $newBits[$i >> 5] = $newBits[$i >> 5] | (1 << ($i & 0x1f));
+            }
+        }
+
+        $this->bits = $newBits;
+    }
+
+    /**
+     * Returns a string representation of the bit array.
+     */
+    public function __toString() : string
+    {
+        $result = '';
+
+        for ($i = 0; $i < $this->size; ++$i) {
+            if (0 === ($i & 0x07)) {
+                $result .= ' ';
+            }
+
+            $result .= $this->get($i) ? 'X' : '.';
+        }
+
+        return $result;
+    }
+}

+ 313 - 0
htdocs/includes/bacon/bacon-qr-code/src/Common/BitMatrix.php

@@ -0,0 +1,313 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Common;
+
+use BaconQrCode\Exception\InvalidArgumentException;
+use SplFixedArray;
+
+/**
+ * Bit matrix.
+ *
+ * Represents a 2D matrix of bits. In function arguments below, and throughout
+ * the common module, x is the column position, and y is the row position. The
+ * ordering is always x, y. The origin is at the top-left.
+ */
+class BitMatrix
+{
+    /**
+     * Width of the bit matrix.
+     *
+     * @var int
+     */
+    private $width;
+
+    /**
+     * Height of the bit matrix.
+     *
+     * @var int
+     */
+    private $height;
+
+    /**
+     * Size in bits of each individual row.
+     *
+     * @var int
+     */
+    private $rowSize;
+
+    /**
+     * Bits representation.
+     *
+     * @var SplFixedArray<int>
+     */
+    private $bits;
+
+    /**
+     * @throws InvalidArgumentException if a dimension is smaller than zero
+     */
+    public function __construct(int $width, int $height = null)
+    {
+        if (null === $height) {
+            $height = $width;
+        }
+
+        if ($width < 1 || $height < 1) {
+            throw new InvalidArgumentException('Both dimensions must be greater than zero');
+        }
+
+        $this->width = $width;
+        $this->height = $height;
+        $this->rowSize = ($width + 31) >> 5;
+        $this->bits = SplFixedArray::fromArray(array_fill(0, $this->rowSize * $height, 0));
+    }
+
+    /**
+     * Gets the requested bit, where true means black.
+     */
+    public function get(int $x, int $y) : bool
+    {
+        $offset = $y * $this->rowSize + ($x >> 5);
+        return 0 !== (BitUtils::unsignedRightShift($this->bits[$offset], ($x & 0x1f)) & 1);
+    }
+
+    /**
+     * Sets the given bit to true.
+     */
+    public function set(int $x, int $y) : void
+    {
+        $offset = $y * $this->rowSize + ($x >> 5);
+        $this->bits[$offset] = $this->bits[$offset] | (1 << ($x & 0x1f));
+    }
+
+    /**
+     * Flips the given bit.
+     */
+    public function flip(int $x, int $y) : void
+    {
+        $offset = $y * $this->rowSize + ($x >> 5);
+        $this->bits[$offset] = $this->bits[$offset] ^ (1 << ($x & 0x1f));
+    }
+
+    /**
+     * Clears all bits (set to false).
+     */
+    public function clear() : void
+    {
+        $max = count($this->bits);
+
+        for ($i = 0; $i < $max; ++$i) {
+            $this->bits[$i] = 0;
+        }
+    }
+
+    /**
+     * Sets a square region of the bit matrix to true.
+     *
+     * @throws InvalidArgumentException if left or top are negative
+     * @throws InvalidArgumentException if width or height are smaller than 1
+     * @throws InvalidArgumentException if region does not fit into the matix
+     */
+    public function setRegion(int $left, int $top, int $width, int $height) : void
+    {
+        if ($top < 0 || $left < 0) {
+            throw new InvalidArgumentException('Left and top must be non-negative');
+        }
+
+        if ($height < 1 || $width < 1) {
+            throw new InvalidArgumentException('Width and height must be at least 1');
+        }
+
+        $right = $left + $width;
+        $bottom = $top + $height;
+
+        if ($bottom > $this->height || $right > $this->width) {
+            throw new InvalidArgumentException('The region must fit inside the matrix');
+        }
+
+        for ($y = $top; $y < $bottom; ++$y) {
+            $offset = $y * $this->rowSize;
+
+            for ($x = $left; $x < $right; ++$x) {
+                $index = $offset + ($x >> 5);
+                $this->bits[$index] = $this->bits[$index] | (1 << ($x & 0x1f));
+            }
+        }
+    }
+
+    /**
+     * A fast method to retrieve one row of data from the matrix as a BitArray.
+     */
+    public function getRow(int $y, BitArray $row = null) : BitArray
+    {
+        if (null === $row || $row->getSize() < $this->width) {
+            $row = new BitArray($this->width);
+        }
+
+        $offset = $y * $this->rowSize;
+
+        for ($x = 0; $x < $this->rowSize; ++$x) {
+            $row->setBulk($x << 5, $this->bits[$offset + $x]);
+        }
+
+        return $row;
+    }
+
+    /**
+     * Sets a row of data from a BitArray.
+     */
+    public function setRow(int $y, BitArray $row) : void
+    {
+        $bits = $row->getBitArray();
+
+        for ($i = 0; $i < $this->rowSize; ++$i) {
+            $this->bits[$y * $this->rowSize + $i] = $bits[$i];
+        }
+    }
+
+    /**
+     * This is useful in detecting the enclosing rectangle of a 'pure' barcode.
+     *
+     * @return int[]|null
+     */
+    public function getEnclosingRectangle() : ?array
+    {
+        $left = $this->width;
+        $top = $this->height;
+        $right = -1;
+        $bottom = -1;
+
+        for ($y = 0; $y < $this->height; ++$y) {
+            for ($x32 = 0; $x32 < $this->rowSize; ++$x32) {
+                $bits = $this->bits[$y * $this->rowSize + $x32];
+
+                if (0 !== $bits) {
+                    if ($y < $top) {
+                        $top = $y;
+                    }
+
+                    if ($y > $bottom) {
+                        $bottom = $y;
+                    }
+
+                    if ($x32 * 32 < $left) {
+                        $bit = 0;
+
+                        while (($bits << (31 - $bit)) === 0) {
+                            $bit++;
+                        }
+
+                        if (($x32 * 32 + $bit) < $left) {
+                            $left = $x32 * 32 + $bit;
+                        }
+                    }
+                }
+
+                if ($x32 * 32 + 31 > $right) {
+                    $bit = 31;
+
+                    while (0 === BitUtils::unsignedRightShift($bits, $bit)) {
+                        --$bit;
+                    }
+
+                    if (($x32 * 32 + $bit) > $right) {
+                        $right = $x32 * 32 + $bit;
+                    }
+                }
+            }
+        }
+
+        $width = $right - $left;
+        $height = $bottom - $top;
+
+        if ($width < 0 || $height < 0) {
+            return null;
+        }
+
+        return [$left, $top, $width, $height];
+    }
+
+    /**
+     * Gets the most top left set bit.
+     *
+     * This is useful in detecting a corner of a 'pure' barcode.
+     *
+     * @return int[]|null
+     */
+    public function getTopLeftOnBit() : ?array
+    {
+        $bitsOffset = 0;
+
+        while ($bitsOffset < count($this->bits) && 0 === $this->bits[$bitsOffset]) {
+            ++$bitsOffset;
+        }
+
+        if (count($this->bits) === $bitsOffset) {
+            return null;
+        }
+
+        $x = intdiv($bitsOffset, $this->rowSize);
+        $y = ($bitsOffset % $this->rowSize) << 5;
+
+        $bits = $this->bits[$bitsOffset];
+        $bit = 0;
+
+        while (0 === ($bits << (31 - $bit))) {
+            ++$bit;
+        }
+
+        $x += $bit;
+
+        return [$x, $y];
+    }
+
+    /**
+     * Gets the most bottom right set bit.
+     *
+     * This is useful in detecting a corner of a 'pure' barcode.
+     *
+     * @return int[]|null
+     */
+    public function getBottomRightOnBit() : ?array
+    {
+        $bitsOffset = count($this->bits) - 1;
+
+        while ($bitsOffset >= 0 && 0 === $this->bits[$bitsOffset]) {
+            --$bitsOffset;
+        }
+
+        if ($bitsOffset < 0) {
+            return null;
+        }
+
+        $x = intdiv($bitsOffset, $this->rowSize);
+        $y = ($bitsOffset % $this->rowSize) << 5;
+
+        $bits = $this->bits[$bitsOffset];
+        $bit  = 0;
+
+        while (0 === BitUtils::unsignedRightShift($bits, $bit)) {
+            --$bit;
+        }
+
+        $x += $bit;
+
+        return [$x, $y];
+    }
+
+    /**
+     * Gets the width of the matrix,
+     */
+    public function getWidth() : int
+    {
+        return $this->width;
+    }
+
+    /**
+     * Gets the height of the matrix.
+     */
+    public function getHeight() : int
+    {
+        return $this->height;
+    }
+}

+ 41 - 0
htdocs/includes/bacon/bacon-qr-code/src/Common/BitUtils.php

@@ -0,0 +1,41 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Common;
+
+/**
+ * General bit utilities.
+ *
+ * All utility methods are based on 32-bit integers and also work on 64-bit
+ * systems.
+ */
+final class BitUtils
+{
+    private function __construct()
+    {
+    }
+
+    /**
+     * Performs an unsigned right shift.
+     *
+     * This is the same as the unsigned right shift operator ">>>" in other
+     * languages.
+     */
+    public static function unsignedRightShift(int $a, int $b) : int
+    {
+        return (
+            $a >= 0
+            ? $a >> $b
+            : (($a & 0x7fffffff) >> $b) | (0x40000000 >> ($b - 1))
+        );
+    }
+
+    /**
+     * Gets the number of trailing zeros.
+     */
+    public static function numberOfTrailingZeros(int $i) : int
+    {
+        $lastPos = strrpos(str_pad(decbin($i), 32, '0', STR_PAD_LEFT), '1');
+        return $lastPos === false ? 32 : 31 - $lastPos;
+    }
+}

+ 183 - 0
htdocs/includes/bacon/bacon-qr-code/src/Common/CharacterSetEci.php

@@ -0,0 +1,183 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Common;
+
+use BaconQrCode\Exception\InvalidArgumentException;
+use DASPRiD\Enum\AbstractEnum;
+
+/**
+ * Encapsulates a Character Set ECI, according to "Extended Channel Interpretations" 5.3.1.1 of ISO 18004.
+ *
+ * @method static self CP437()
+ * @method static self ISO8859_1()
+ * @method static self ISO8859_2()
+ * @method static self ISO8859_3()
+ * @method static self ISO8859_4()
+ * @method static self ISO8859_5()
+ * @method static self ISO8859_6()
+ * @method static self ISO8859_7()
+ * @method static self ISO8859_8()
+ * @method static self ISO8859_9()
+ * @method static self ISO8859_10()
+ * @method static self ISO8859_11()
+ * @method static self ISO8859_12()
+ * @method static self ISO8859_13()
+ * @method static self ISO8859_14()
+ * @method static self ISO8859_15()
+ * @method static self ISO8859_16()
+ * @method static self SJIS()
+ * @method static self CP1250()
+ * @method static self CP1251()
+ * @method static self CP1252()
+ * @method static self CP1256()
+ * @method static self UNICODE_BIG_UNMARKED()
+ * @method static self UTF8()
+ * @method static self ASCII()
+ * @method static self BIG5()
+ * @method static self GB18030()
+ * @method static self EUC_KR()
+ */
+final class CharacterSetEci extends AbstractEnum
+{
+    protected const CP437 = [[0, 2]];
+    protected const ISO8859_1 = [[1, 3], 'ISO-8859-1'];
+    protected const ISO8859_2 = [[4], 'ISO-8859-2'];
+    protected const ISO8859_3 = [[5], 'ISO-8859-3'];
+    protected const ISO8859_4 = [[6], 'ISO-8859-4'];
+    protected const ISO8859_5 = [[7], 'ISO-8859-5'];
+    protected const ISO8859_6 = [[8], 'ISO-8859-6'];
+    protected const ISO8859_7 = [[9], 'ISO-8859-7'];
+    protected const ISO8859_8 = [[10], 'ISO-8859-8'];
+    protected const ISO8859_9 = [[11], 'ISO-8859-9'];
+    protected const ISO8859_10 = [[12], 'ISO-8859-10'];
+    protected const ISO8859_11 = [[13], 'ISO-8859-11'];
+    protected const ISO8859_12 = [[14], 'ISO-8859-12'];
+    protected const ISO8859_13 = [[15], 'ISO-8859-13'];
+    protected const ISO8859_14 = [[16], 'ISO-8859-14'];
+    protected const ISO8859_15 = [[17], 'ISO-8859-15'];
+    protected const ISO8859_16 = [[18], 'ISO-8859-16'];
+    protected const SJIS = [[20], 'Shift_JIS'];
+    protected const CP1250 = [[21], 'windows-1250'];
+    protected const CP1251 = [[22], 'windows-1251'];
+    protected const CP1252 = [[23], 'windows-1252'];
+    protected const CP1256 = [[24], 'windows-1256'];
+    protected const UNICODE_BIG_UNMARKED = [[25], 'UTF-16BE', 'UnicodeBig'];
+    protected const UTF8 = [[26], 'UTF-8'];
+    protected const ASCII = [[27, 170], 'US-ASCII'];
+    protected const BIG5 = [[28]];
+    protected const GB18030 = [[29], 'GB2312', 'EUC_CN', 'GBK'];
+    protected const EUC_KR = [[30], 'EUC-KR'];
+
+    /**
+     * @var int[]
+     */
+    private $values;
+
+    /**
+     * @var string[]
+     */
+    private $otherEncodingNames;
+
+    /**
+     * @var array<int, self>|null
+     */
+    private static $valueToEci;
+
+    /**
+     * @var array<string, self>|null
+     */
+    private static $nameToEci;
+
+    /**
+     * @param int[] $values
+     */
+    public function __construct(array $values, string ...$otherEncodingNames)
+    {
+        $this->values = $values;
+        $this->otherEncodingNames = $otherEncodingNames;
+    }
+
+    /**
+     * Returns the primary value.
+     */
+    public function getValue() : int
+    {
+        return $this->values[0];
+    }
+
+    /**
+     * Gets character set ECI by value.
+     *
+     * Returns the representing ECI of a given value, or null if it is legal but unsupported.
+     *
+     * @throws InvalidArgumentException if value is not between 0 and 900
+     */
+    public static function getCharacterSetEciByValue(int $value) : ?self
+    {
+        if ($value < 0 || $value >= 900) {
+            throw new InvalidArgumentException('Value must be between 0 and 900');
+        }
+
+        $valueToEci = self::valueToEci();
+
+        if (! array_key_exists($value, $valueToEci)) {
+            return null;
+        }
+
+        return $valueToEci[$value];
+    }
+
+    /**
+     * Returns character set ECI by name.
+     *
+     * Returns the representing ECI of a given name, or null if it is legal but unsupported
+     */
+    public static function getCharacterSetEciByName(string $name) : ?self
+    {
+        $nameToEci = self::nameToEci();
+        $name = strtolower($name);
+
+        if (! array_key_exists($name, $nameToEci)) {
+            return null;
+        }
+
+        return $nameToEci[$name];
+    }
+
+    private static function valueToEci() : array
+    {
+        if (null !== self::$valueToEci) {
+            return self::$valueToEci;
+        }
+
+        self::$valueToEci = [];
+
+        foreach (self::values() as $eci) {
+            foreach ($eci->values as $value) {
+                self::$valueToEci[$value] = $eci;
+            }
+        }
+
+        return self::$valueToEci;
+    }
+
+    private static function nameToEci() : array
+    {
+        if (null !== self::$nameToEci) {
+            return self::$nameToEci;
+        }
+
+        self::$nameToEci = [];
+
+        foreach (self::values() as $eci) {
+            self::$nameToEci[strtolower($eci->name())] = $eci;
+
+            foreach ($eci->otherEncodingNames as $name) {
+                self::$nameToEci[strtolower($name)] = $eci;
+            }
+        }
+
+        return self::$nameToEci;
+    }
+}

+ 49 - 0
htdocs/includes/bacon/bacon-qr-code/src/Common/EcBlock.php

@@ -0,0 +1,49 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Common;
+
+/**
+ * Encapsulates the parameters for one error-correction block in one symbol version.
+ *
+ * This includes the number of data codewords, and the number of times a block with these parameters is used
+ * consecutively in the QR code version's format.
+ */
+final class EcBlock
+{
+    /**
+     * How many times the block is used.
+     *
+     * @var int
+     */
+    private $count;
+
+    /**
+     * Number of data codewords.
+     *
+     * @var int
+     */
+    private $dataCodewords;
+
+    public function __construct(int $count, int $dataCodewords)
+    {
+        $this->count = $count;
+        $this->dataCodewords = $dataCodewords;
+    }
+
+    /**
+     * Returns how many times the block is used.
+     */
+    public function getCount() : int
+    {
+        return $this->count;
+    }
+
+    /**
+     * Returns the number of data codewords.
+     */
+    public function getDataCodewords() : int
+    {
+        return $this->dataCodewords;
+    }
+}

+ 74 - 0
htdocs/includes/bacon/bacon-qr-code/src/Common/EcBlocks.php

@@ -0,0 +1,74 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Common;
+
+/**
+ * Encapsulates a set of error-correction blocks in one symbol version.
+ *
+ * Most versions will use blocks of differing sizes within one version, so, this encapsulates the parameters for each
+ * set of blocks. It also holds the number of error-correction codewords per block since it will be the same across all
+ * blocks within one version.
+ */
+final class EcBlocks
+{
+    /**
+     * Number of EC codewords per block.
+     *
+     * @var int
+     */
+    private $ecCodewordsPerBlock;
+
+    /**
+     * List of EC blocks.
+     *
+     * @var EcBlock[]
+     */
+    private $ecBlocks;
+
+    public function __construct(int $ecCodewordsPerBlock, EcBlock ...$ecBlocks)
+    {
+        $this->ecCodewordsPerBlock = $ecCodewordsPerBlock;
+        $this->ecBlocks = $ecBlocks;
+    }
+
+    /**
+     * Returns the number of EC codewords per block.
+     */
+    public function getEcCodewordsPerBlock() : int
+    {
+        return $this->ecCodewordsPerBlock;
+    }
+
+    /**
+     * Returns the total number of EC block appearances.
+     */
+    public function getNumBlocks() : int
+    {
+        $total = 0;
+
+        foreach ($this->ecBlocks as $ecBlock) {
+            $total += $ecBlock->getCount();
+        }
+
+        return $total;
+    }
+
+    /**
+     * Returns the total count of EC codewords.
+     */
+    public function getTotalEcCodewords() : int
+    {
+        return $this->ecCodewordsPerBlock * $this->getNumBlocks();
+    }
+
+    /**
+     * Returns the EC blocks included in this collection.
+     *
+     * @return EcBlock[]
+     */
+    public function getEcBlocks() : array
+    {
+        return $this->ecBlocks;
+    }
+}

+ 63 - 0
htdocs/includes/bacon/bacon-qr-code/src/Common/ErrorCorrectionLevel.php

@@ -0,0 +1,63 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Common;
+
+use BaconQrCode\Exception\OutOfBoundsException;
+use DASPRiD\Enum\AbstractEnum;
+
+/**
+ * Enum representing the four error correction levels.
+ *
+ * @method static self L() ~7% correction
+ * @method static self M() ~15% correction
+ * @method static self Q() ~25% correction
+ * @method static self H() ~30% correction
+ */
+final class ErrorCorrectionLevel extends AbstractEnum
+{
+    protected const L = [0x01];
+    protected const M = [0x00];
+    protected const Q = [0x03];
+    protected const H = [0x02];
+
+    /**
+     * @var int
+     */
+    private $bits;
+
+    protected function __construct(int $bits)
+    {
+        $this->bits = $bits;
+    }
+
+    /**
+     * @throws OutOfBoundsException if number of bits is invalid
+     */
+    public static function forBits(int $bits) : self
+    {
+        switch ($bits) {
+            case 0:
+                return self::M();
+
+            case 1:
+                return self::L();
+
+            case 2:
+                return self::H();
+
+            case 3:
+                return self::Q();
+        }
+
+        throw new OutOfBoundsException('Invalid number of bits');
+    }
+
+    /**
+     * Returns the two bits used to encode this error correction level.
+     */
+    public function getBits() : int
+    {
+        return $this->bits;
+    }
+}

+ 203 - 0
htdocs/includes/bacon/bacon-qr-code/src/Common/FormatInformation.php

@@ -0,0 +1,203 @@
+<?php
+/**
+ * BaconQrCode
+ *
+ * @link      http://github.com/Bacon/BaconQrCode For the canonical source repository
+ * @copyright 2013 Ben 'DASPRiD' Scholzen
+ * @license   http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
+ */
+
+namespace BaconQrCode\Common;
+
+/**
+ * Encapsulates a QR Code's format information, including the data mask used and error correction level.
+ */
+class FormatInformation
+{
+    /**
+     * Mask for format information.
+     */
+    private const FORMAT_INFO_MASK_QR = 0x5412;
+
+    /**
+     * Lookup table for decoding format information.
+     *
+     * See ISO 18004:2006, Annex C, Table C.1
+     */
+    private const FORMAT_INFO_DECODE_LOOKUP = [
+        [0x5412, 0x00],
+        [0x5125, 0x01],
+        [0x5e7c, 0x02],
+        [0x5b4b, 0x03],
+        [0x45f9, 0x04],
+        [0x40ce, 0x05],
+        [0x4f97, 0x06],
+        [0x4aa0, 0x07],
+        [0x77c4, 0x08],
+        [0x72f3, 0x09],
+        [0x7daa, 0x0a],
+        [0x789d, 0x0b],
+        [0x662f, 0x0c],
+        [0x6318, 0x0d],
+        [0x6c41, 0x0e],
+        [0x6976, 0x0f],
+        [0x1689, 0x10],
+        [0x13be, 0x11],
+        [0x1ce7, 0x12],
+        [0x19d0, 0x13],
+        [0x0762, 0x14],
+        [0x0255, 0x15],
+        [0x0d0c, 0x16],
+        [0x083b, 0x17],
+        [0x355f, 0x18],
+        [0x3068, 0x19],
+        [0x3f31, 0x1a],
+        [0x3a06, 0x1b],
+        [0x24b4, 0x1c],
+        [0x2183, 0x1d],
+        [0x2eda, 0x1e],
+        [0x2bed, 0x1f],
+    ];
+
+    /**
+     * Offset i holds the number of 1 bits in the binary representation of i.
+     *
+     * @var int[]
+     */
+    private const BITS_SET_IN_HALF_BYTE = [0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4];
+
+    /**
+     * Error correction level.
+     *
+     * @var ErrorCorrectionLevel
+     */
+    private $ecLevel;
+
+    /**
+     * Data mask.
+     *
+     * @var int
+     */
+    private $dataMask;
+
+    protected function __construct(int $formatInfo)
+    {
+        $this->ecLevel = ErrorCorrectionLevel::forBits(($formatInfo >> 3) & 0x3);
+        $this->dataMask = $formatInfo & 0x7;
+    }
+
+    /**
+     * Checks how many bits are different between two integers.
+     */
+    public static function numBitsDiffering(int $a, int $b) : int
+    {
+        $a ^= $b;
+
+        return (
+            self::BITS_SET_IN_HALF_BYTE[$a & 0xf]
+            + self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 4) & 0xf)]
+            + self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 8) & 0xf)]
+            + self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 12) & 0xf)]
+            + self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 16) & 0xf)]
+            + self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 20) & 0xf)]
+            + self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 24) & 0xf)]
+            + self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 28) & 0xf)]
+        );
+    }
+
+    /**
+     * Decodes format information.
+     */
+    public static function decodeFormatInformation(int $maskedFormatInfo1, int $maskedFormatInfo2) : ?self
+    {
+        $formatInfo = self::doDecodeFormatInformation($maskedFormatInfo1, $maskedFormatInfo2);
+
+        if (null !== $formatInfo) {
+            return $formatInfo;
+        }
+
+        // Should return null, but, some QR codes apparently do not mask this info. Try again by actually masking the
+        // pattern first.
+        return self::doDecodeFormatInformation(
+            $maskedFormatInfo1 ^ self::FORMAT_INFO_MASK_QR,
+            $maskedFormatInfo2 ^ self::FORMAT_INFO_MASK_QR
+        );
+    }
+
+    /**
+     * Internal method for decoding format information.
+     */
+    private static function doDecodeFormatInformation(int $maskedFormatInfo1, int $maskedFormatInfo2) : ?self
+    {
+        $bestDifference = PHP_INT_MAX;
+        $bestFormatInfo = 0;
+
+        foreach (self::FORMAT_INFO_DECODE_LOOKUP as $decodeInfo) {
+            $targetInfo = $decodeInfo[0];
+
+            if ($targetInfo === $maskedFormatInfo1 || $targetInfo === $maskedFormatInfo2) {
+                // Found an exact match
+                return new self($decodeInfo[1]);
+            }
+
+            $bitsDifference = self::numBitsDiffering($maskedFormatInfo1, $targetInfo);
+
+            if ($bitsDifference < $bestDifference) {
+                $bestFormatInfo = $decodeInfo[1];
+                $bestDifference = $bitsDifference;
+            }
+
+            if ($maskedFormatInfo1 !== $maskedFormatInfo2) {
+                // Also try the other option
+                $bitsDifference = self::numBitsDiffering($maskedFormatInfo2, $targetInfo);
+
+                if ($bitsDifference < $bestDifference) {
+                    $bestFormatInfo = $decodeInfo[1];
+                    $bestDifference = $bitsDifference;
+                }
+            }
+        }
+
+        // Hamming distance of the 32 masked codes is 7, by construction, so <= 3 bits differing means we found a match.
+        if ($bestDifference <= 3) {
+            return new self($bestFormatInfo);
+        }
+
+        return null;
+    }
+
+    /**
+     * Returns the error correction level.
+     */
+    public function getErrorCorrectionLevel() : ErrorCorrectionLevel
+    {
+        return $this->ecLevel;
+    }
+
+    /**
+     * Returns the data mask.
+     */
+    public function getDataMask() : int
+    {
+        return $this->dataMask;
+    }
+
+    /**
+     * Hashes the code of the EC level.
+     */
+    public function hashCode() : int
+    {
+        return ($this->ecLevel->getBits() << 3) | $this->dataMask;
+    }
+
+    /**
+     * Verifies if this instance equals another one.
+     */
+    public function equals(self $other) : bool
+    {
+        return (
+            $this->ecLevel === $other->ecLevel
+            && $this->dataMask === $other->dataMask
+        );
+    }
+}

+ 79 - 0
htdocs/includes/bacon/bacon-qr-code/src/Common/Mode.php

@@ -0,0 +1,79 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Common;
+
+use DASPRiD\Enum\AbstractEnum;
+
+/**
+ * Enum representing various modes in which data can be encoded to bits.
+ *
+ * @method static self TERMINATOR()
+ * @method static self NUMERIC()
+ * @method static self ALPHANUMERIC()
+ * @method static self STRUCTURED_APPEND()
+ * @method static self BYTE()
+ * @method static self ECI()
+ * @method static self KANJI()
+ * @method static self FNC1_FIRST_POSITION()
+ * @method static self FNC1_SECOND_POSITION()
+ * @method static self HANZI()
+ */
+final class Mode extends AbstractEnum
+{
+    protected const TERMINATOR = [[0, 0, 0], 0x00];
+    protected const NUMERIC = [[10, 12, 14], 0x01];
+    protected const ALPHANUMERIC = [[9, 11, 13], 0x02];
+    protected const STRUCTURED_APPEND = [[0, 0, 0], 0x03];
+    protected const BYTE = [[8, 16, 16], 0x04];
+    protected const ECI = [[0, 0, 0], 0x07];
+    protected const KANJI = [[8, 10, 12], 0x08];
+    protected const FNC1_FIRST_POSITION = [[0, 0, 0], 0x05];
+    protected const FNC1_SECOND_POSITION = [[0, 0, 0], 0x09];
+    protected const HANZI = [[8, 10, 12], 0x0d];
+
+    /**
+     * @var int[]
+     */
+    private $characterCountBitsForVersions;
+
+    /**
+     * @var int
+     */
+    private $bits;
+
+    /**
+     * @param int[] $characterCountBitsForVersions
+     */
+    protected function __construct(array $characterCountBitsForVersions, int $bits)
+    {
+        $this->characterCountBitsForVersions = $characterCountBitsForVersions;
+        $this->bits = $bits;
+    }
+
+    /**
+     * Returns the number of bits used in a specific QR code version.
+     */
+    public function getCharacterCountBits(Version $version) : int
+    {
+        $number = $version->getVersionNumber();
+
+        if ($number <= 9) {
+            $offset = 0;
+        } elseif ($number <= 26) {
+            $offset = 1;
+        } else {
+            $offset = 2;
+        }
+
+        return $this->characterCountBitsForVersions[$offset];
+    }
+
+    /**
+     * Returns the four bits used to encode this mode.
+     */
+    public function getBits() : int
+    {
+        return $this->bits;
+    }
+}

+ 468 - 0
htdocs/includes/bacon/bacon-qr-code/src/Common/ReedSolomonCodec.php

@@ -0,0 +1,468 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Common;
+
+use BaconQrCode\Exception\InvalidArgumentException;
+use BaconQrCode\Exception\RuntimeException;
+use SplFixedArray;
+
+/**
+ * Reed-Solomon codec for 8-bit characters.
+ *
+ * Based on libfec by Phil Karn, KA9Q.
+ */
+final class ReedSolomonCodec
+{
+    /**
+     * Symbol size in bits.
+     *
+     * @var int
+     */
+    private $symbolSize;
+
+    /**
+     * Block size in symbols.
+     *
+     * @var int
+     */
+    private $blockSize;
+
+    /**
+     * First root of RS code generator polynomial, index form.
+     *
+     * @var int
+     */
+    private $firstRoot;
+
+    /**
+     * Primitive element to generate polynomial roots, index form.
+     *
+     * @var int
+     */
+    private $primitive;
+
+    /**
+     * Prim-th root of 1, index form.
+     *
+     * @var int
+     */
+    private $iPrimitive;
+
+    /**
+     * RS code generator polynomial degree (number of roots).
+     *
+     * @var int
+     */
+    private $numRoots;
+
+    /**
+     * Padding bytes at front of shortened block.
+     *
+     * @var int
+     */
+    private $padding;
+
+    /**
+     * Log lookup table.
+     *
+     * @var SplFixedArray
+     */
+    private $alphaTo;
+
+    /**
+     * Anti-Log lookup table.
+     *
+     * @var SplFixedArray
+     */
+    private $indexOf;
+
+    /**
+     * Generator polynomial.
+     *
+     * @var SplFixedArray
+     */
+    private $generatorPoly;
+
+    /**
+     * @throws InvalidArgumentException if symbol size ist not between 0 and 8
+     * @throws InvalidArgumentException if first root is invalid
+     * @throws InvalidArgumentException if num roots is invalid
+     * @throws InvalidArgumentException if padding is invalid
+     * @throws RuntimeException if field generator polynomial is not primitive
+     */
+    public function __construct(
+        int $symbolSize,
+        int $gfPoly,
+        int $firstRoot,
+        int $primitive,
+        int $numRoots,
+        int $padding
+    ) {
+        if ($symbolSize < 0 || $symbolSize > 8) {
+            throw new InvalidArgumentException('Symbol size must be between 0 and 8');
+        }
+
+        if ($firstRoot < 0 || $firstRoot >= (1 << $symbolSize)) {
+            throw new InvalidArgumentException('First root must be between 0 and ' . (1 << $symbolSize));
+        }
+
+        if ($numRoots < 0 || $numRoots >= (1 << $symbolSize)) {
+            throw new InvalidArgumentException('Num roots must be between 0 and ' . (1 << $symbolSize));
+        }
+
+        if ($padding < 0 || $padding >= ((1 << $symbolSize) - 1 - $numRoots)) {
+            throw new InvalidArgumentException(
+                'Padding must be between 0 and ' . ((1 << $symbolSize) - 1 - $numRoots)
+            );
+        }
+
+        $this->symbolSize = $symbolSize;
+        $this->blockSize = (1 << $symbolSize) - 1;
+        $this->padding = $padding;
+        $this->alphaTo = SplFixedArray::fromArray(array_fill(0, $this->blockSize + 1, 0), false);
+        $this->indexOf = SplFixedArray::fromArray(array_fill(0, $this->blockSize + 1, 0), false);
+
+        // Generate galous field lookup table
+        $this->indexOf[0] = $this->blockSize;
+        $this->alphaTo[$this->blockSize] = 0;
+
+        $sr = 1;
+
+        for ($i = 0; $i < $this->blockSize; ++$i) {
+            $this->indexOf[$sr] = $i;
+            $this->alphaTo[$i]  = $sr;
+
+            $sr <<= 1;
+
+            if ($sr & (1 << $symbolSize)) {
+                $sr ^= $gfPoly;
+            }
+
+            $sr &= $this->blockSize;
+        }
+
+        if (1 !== $sr) {
+            throw new RuntimeException('Field generator polynomial is not primitive');
+        }
+
+        // Form RS code generator polynomial from its roots
+        $this->generatorPoly = SplFixedArray::fromArray(array_fill(0, $numRoots + 1, 0), false);
+        $this->firstRoot = $firstRoot;
+        $this->primitive = $primitive;
+        $this->numRoots = $numRoots;
+
+        // Find prim-th root of 1, used in decoding
+        for ($iPrimitive = 1; ($iPrimitive % $primitive) !== 0; $iPrimitive += $this->blockSize) {
+        }
+
+        $this->iPrimitive = intdiv($iPrimitive, $primitive);
+
+        $this->generatorPoly[0] = 1;
+
+        for ($i = 0, $root = $firstRoot * $primitive; $i < $numRoots; ++$i, $root += $primitive) {
+            $this->generatorPoly[$i + 1] = 1;
+
+            for ($j = $i; $j > 0; $j--) {
+                if ($this->generatorPoly[$j] !== 0) {
+                    $this->generatorPoly[$j] = $this->generatorPoly[$j - 1] ^ $this->alphaTo[
+                        $this->modNn($this->indexOf[$this->generatorPoly[$j]] + $root)
+                    ];
+                } else {
+                    $this->generatorPoly[$j] = $this->generatorPoly[$j - 1];
+                }
+            }
+
+            $this->generatorPoly[$j] = $this->alphaTo[$this->modNn($this->indexOf[$this->generatorPoly[0]] + $root)];
+        }
+
+        // Convert generator poly to index form for quicker encoding
+        for ($i = 0; $i <= $numRoots; ++$i) {
+            $this->generatorPoly[$i] = $this->indexOf[$this->generatorPoly[$i]];
+        }
+    }
+
+    /**
+     * Encodes data and writes result back into parity array.
+     */
+    public function encode(SplFixedArray $data, SplFixedArray $parity) : void
+    {
+        for ($i = 0; $i < $this->numRoots; ++$i) {
+            $parity[$i] = 0;
+        }
+
+        $iterations = $this->blockSize - $this->numRoots - $this->padding;
+
+        for ($i = 0; $i < $iterations; ++$i) {
+            $feedback = $this->indexOf[$data[$i] ^ $parity[0]];
+
+            if ($feedback !== $this->blockSize) {
+                // Feedback term is non-zero
+                $feedback = $this->modNn($this->blockSize - $this->generatorPoly[$this->numRoots] + $feedback);
+
+                for ($j = 1; $j < $this->numRoots; ++$j) {
+                    $parity[$j] = $parity[$j] ^ $this->alphaTo[
+                        $this->modNn($feedback + $this->generatorPoly[$this->numRoots - $j])
+                    ];
+                }
+            }
+
+            for ($j = 0; $j < $this->numRoots - 1; ++$j) {
+                $parity[$j] = $parity[$j + 1];
+            }
+
+            if ($feedback !== $this->blockSize) {
+                $parity[$this->numRoots - 1] = $this->alphaTo[$this->modNn($feedback + $this->generatorPoly[0])];
+            } else {
+                $parity[$this->numRoots - 1] = 0;
+            }
+        }
+    }
+
+    /**
+     * Decodes received data.
+     */
+    public function decode(SplFixedArray $data, SplFixedArray $erasures = null) : ?int
+    {
+        // This speeds up the initialization a bit.
+        $numRootsPlusOne = SplFixedArray::fromArray(array_fill(0, $this->numRoots + 1, 0), false);
+        $numRoots = SplFixedArray::fromArray(array_fill(0, $this->numRoots, 0), false);
+
+        $lambda = clone $numRootsPlusOne;
+        $b = clone $numRootsPlusOne;
+        $t = clone $numRootsPlusOne;
+        $omega = clone $numRootsPlusOne;
+        $root = clone $numRoots;
+        $loc = clone $numRoots;
+
+        $numErasures = (null !== $erasures ? count($erasures) : 0);
+
+        // Form the Syndromes; i.e., evaluate data(x) at roots of g(x)
+        $syndromes = SplFixedArray::fromArray(array_fill(0, $this->numRoots, $data[0]), false);
+
+        for ($i = 1; $i < $this->blockSize - $this->padding; ++$i) {
+            for ($j = 0; $j < $this->numRoots; ++$j) {
+                if ($syndromes[$j] === 0) {
+                    $syndromes[$j] = $data[$i];
+                } else {
+                    $syndromes[$j] = $data[$i] ^ $this->alphaTo[
+                        $this->modNn($this->indexOf[$syndromes[$j]] + ($this->firstRoot + $j) * $this->primitive)
+                    ];
+                }
+            }
+        }
+
+        // Convert syndromes to index form, checking for nonzero conditions
+        $syndromeError = 0;
+
+        for ($i = 0; $i < $this->numRoots; ++$i) {
+            $syndromeError |= $syndromes[$i];
+            $syndromes[$i] = $this->indexOf[$syndromes[$i]];
+        }
+
+        if (! $syndromeError) {
+            // If syndrome is zero, data[] is a codeword and there are no errors to correct, so return data[]
+            // unmodified.
+            return 0;
+        }
+
+        $lambda[0] = 1;
+
+        if ($numErasures > 0) {
+            // Init lambda to be the erasure locator polynomial
+            $lambda[1] = $this->alphaTo[$this->modNn($this->primitive * ($this->blockSize - 1 - $erasures[0]))];
+
+            for ($i = 1; $i < $numErasures; ++$i) {
+                $u = $this->modNn($this->primitive * ($this->blockSize - 1 - $erasures[$i]));
+
+                for ($j = $i + 1; $j > 0; --$j) {
+                    $tmp = $this->indexOf[$lambda[$j - 1]];
+
+                    if ($tmp !== $this->blockSize) {
+                        $lambda[$j] = $lambda[$j] ^ $this->alphaTo[$this->modNn($u + $tmp)];
+                    }
+                }
+            }
+        }
+
+        for ($i = 0; $i <= $this->numRoots; ++$i) {
+            $b[$i] = $this->indexOf[$lambda[$i]];
+        }
+
+        // Begin Berlekamp-Massey algorithm to determine error+erasure locator polynomial
+        $r  = $numErasures;
+        $el = $numErasures;
+
+        while (++$r <= $this->numRoots) {
+            // Compute discrepancy at the r-th step in poly form
+            $discrepancyR = 0;
+
+            for ($i = 0; $i < $r; ++$i) {
+                if ($lambda[$i] !== 0 && $syndromes[$r - $i - 1] !== $this->blockSize) {
+                    $discrepancyR ^= $this->alphaTo[
+                        $this->modNn($this->indexOf[$lambda[$i]] + $syndromes[$r - $i - 1])
+                    ];
+                }
+            }
+
+            $discrepancyR = $this->indexOf[$discrepancyR];
+
+            if ($discrepancyR === $this->blockSize) {
+                $tmp = $b->toArray();
+                array_unshift($tmp, $this->blockSize);
+                array_pop($tmp);
+                $b = SplFixedArray::fromArray($tmp, false);
+                continue;
+            }
+
+            $t[0] = $lambda[0];
+
+            for ($i = 0; $i < $this->numRoots; ++$i) {
+                if ($b[$i] !== $this->blockSize) {
+                    $t[$i + 1] = $lambda[$i + 1] ^ $this->alphaTo[$this->modNn($discrepancyR + $b[$i])];
+                } else {
+                    $t[$i + 1] = $lambda[$i + 1];
+                }
+            }
+
+            if (2 * $el <= $r + $numErasures - 1) {
+                $el = $r + $numErasures - $el;
+
+                for ($i = 0; $i <= $this->numRoots; ++$i) {
+                    $b[$i] = (
+                        $lambda[$i] === 0
+                        ? $this->blockSize
+                        : $this->modNn($this->indexOf[$lambda[$i]] - $discrepancyR + $this->blockSize)
+                    );
+                }
+            } else {
+                $tmp = $b->toArray();
+                array_unshift($tmp, $this->blockSize);
+                array_pop($tmp);
+                $b = SplFixedArray::fromArray($tmp, false);
+            }
+
+            $lambda = clone $t;
+        }
+
+        // Convert lambda to index form and compute deg(lambda(x))
+        $degLambda = 0;
+
+        for ($i = 0; $i <= $this->numRoots; ++$i) {
+            $lambda[$i] = $this->indexOf[$lambda[$i]];
+
+            if ($lambda[$i] !== $this->blockSize) {
+                $degLambda = $i;
+            }
+        }
+
+        // Find roots of the error+erasure locator polynomial by Chien search.
+        $reg = clone $lambda;
+        $reg[0] = 0;
+        $count = 0;
+        $i = 1;
+
+        for ($k = $this->iPrimitive - 1; $i <= $this->blockSize; ++$i, $k = $this->modNn($k + $this->iPrimitive)) {
+            $q = 1;
+
+            for ($j = $degLambda; $j > 0; $j--) {
+                if ($reg[$j] !== $this->blockSize) {
+                    $reg[$j] = $this->modNn($reg[$j] + $j);
+                    $q ^= $this->alphaTo[$reg[$j]];
+                }
+            }
+
+            if ($q !== 0) {
+                // Not a root
+                continue;
+            }
+
+            // Store root (index-form) and error location number
+            $root[$count] = $i;
+            $loc[$count] = $k;
+
+            if (++$count === $degLambda) {
+                break;
+            }
+        }
+
+        if ($degLambda !== $count) {
+            // deg(lambda) unequal to number of roots: uncorrectable error detected
+            return null;
+        }
+
+        // Compute err+eras evaluate poly omega(x) = s(x)*lambda(x) (modulo x**numRoots). In index form. Also find
+        // deg(omega).
+        $degOmega = $degLambda - 1;
+
+        for ($i = 0; $i <= $degOmega; ++$i) {
+            $tmp = 0;
+
+            for ($j = $i; $j >= 0; --$j) {
+                if ($syndromes[$i - $j] !== $this->blockSize && $lambda[$j] !== $this->blockSize) {
+                    $tmp ^= $this->alphaTo[$this->modNn($syndromes[$i - $j] + $lambda[$j])];
+                }
+            }
+
+            $omega[$i] = $this->indexOf[$tmp];
+        }
+
+        // Compute error values in poly-form. num1 = omega(inv(X(l))), num2 = inv(X(l))**(firstRoot-1) and
+        // den = lambda_pr(inv(X(l))) all in poly form.
+        for ($j = $count - 1; $j >= 0; --$j) {
+            $num1 = 0;
+
+            for ($i = $degOmega; $i >= 0; $i--) {
+                if ($omega[$i] !== $this->blockSize) {
+                    $num1 ^= $this->alphaTo[$this->modNn($omega[$i] + $i * $root[$j])];
+                }
+            }
+
+            $num2 = $this->alphaTo[$this->modNn($root[$j] * ($this->firstRoot - 1) + $this->blockSize)];
+            $den  = 0;
+
+            // lambda[i+1] for i even is the formal derivativelambda_pr of lambda[i]
+            for ($i = min($degLambda, $this->numRoots - 1) & ~1; $i >= 0; $i -= 2) {
+                if ($lambda[$i + 1] !== $this->blockSize) {
+                    $den ^= $this->alphaTo[$this->modNn($lambda[$i + 1] + $i * $root[$j])];
+                }
+            }
+
+            // Apply error to data
+            if ($num1 !== 0 && $loc[$j] >= $this->padding) {
+                $data[$loc[$j] - $this->padding] = $data[$loc[$j] - $this->padding] ^ (
+                    $this->alphaTo[
+                        $this->modNn(
+                            $this->indexOf[$num1] + $this->indexOf[$num2] + $this->blockSize - $this->indexOf[$den]
+                        )
+                    ]
+                );
+            }
+        }
+
+        if (null !== $erasures) {
+            if (count($erasures) < $count) {
+                $erasures->setSize($count);
+            }
+
+            for ($i = 0; $i < $count; $i++) {
+                $erasures[$i] = $loc[$i];
+            }
+        }
+
+        return $count;
+    }
+
+    /**
+     * Computes $x % GF_SIZE, where GF_SIZE is 2**GF_BITS - 1, without a slow divide.
+     */
+    private function modNn(int $x) : int
+    {
+        while ($x >= $this->blockSize) {
+            $x -= $this->blockSize;
+            $x = ($x >> $this->symbolSize) + ($x & $this->blockSize);
+        }
+
+        return $x;
+    }
+}

+ 596 - 0
htdocs/includes/bacon/bacon-qr-code/src/Common/Version.php

@@ -0,0 +1,596 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Common;
+
+use BaconQrCode\Exception\InvalidArgumentException;
+use SplFixedArray;
+
+/**
+ * Version representation.
+ */
+final class Version
+{
+    private const VERSION_DECODE_INFO = [
+        0x07c94,
+        0x085bc,
+        0x09a99,
+        0x0a4d3,
+        0x0bbf6,
+        0x0c762,
+        0x0d847,
+        0x0e60d,
+        0x0f928,
+        0x10b78,
+        0x1145d,
+        0x12a17,
+        0x13532,
+        0x149a6,
+        0x15683,
+        0x168c9,
+        0x177ec,
+        0x18ec4,
+        0x191e1,
+        0x1afab,
+        0x1b08e,
+        0x1cc1a,
+        0x1d33f,
+        0x1ed75,
+        0x1f250,
+        0x209d5,
+        0x216f0,
+        0x228ba,
+        0x2379f,
+        0x24b0b,
+        0x2542e,
+        0x26a64,
+        0x27541,
+        0x28c69,
+    ];
+
+    /**
+     * Version number of this version.
+     *
+     * @var int
+     */
+    private $versionNumber;
+
+    /**
+     * Alignment pattern centers.
+     *
+     * @var SplFixedArray
+     */
+    private $alignmentPatternCenters;
+
+    /**
+     * Error correction blocks.
+     *
+     * @var EcBlocks[]
+     */
+    private $ecBlocks;
+
+    /**
+     * Total number of codewords.
+     *
+     * @var int
+     */
+    private $totalCodewords;
+
+    /**
+     * Cached version instances.
+     *
+     * @var array<int, self>|null
+     */
+    private static $versions;
+
+    /**
+     * @param int[] $alignmentPatternCenters
+     */
+    private function __construct(
+        int $versionNumber,
+        array $alignmentPatternCenters,
+        EcBlocks ...$ecBlocks
+    ) {
+        $this->versionNumber = $versionNumber;
+        $this->alignmentPatternCenters = $alignmentPatternCenters;
+        $this->ecBlocks = $ecBlocks;
+
+        $totalCodewords = 0;
+        $ecCodewords = $ecBlocks[0]->getEcCodewordsPerBlock();
+
+        foreach ($ecBlocks[0]->getEcBlocks() as $ecBlock) {
+            $totalCodewords += $ecBlock->getCount() * ($ecBlock->getDataCodewords() + $ecCodewords);
+        }
+
+        $this->totalCodewords = $totalCodewords;
+    }
+
+    /**
+     * Returns the version number.
+     */
+    public function getVersionNumber() : int
+    {
+        return $this->versionNumber;
+    }
+
+    /**
+     * Returns the alignment pattern centers.
+     *
+     * @return int[]
+     */
+    public function getAlignmentPatternCenters() : array
+    {
+        return $this->alignmentPatternCenters;
+    }
+
+    /**
+     * Returns the total number of codewords.
+     */
+    public function getTotalCodewords() : int
+    {
+        return $this->totalCodewords;
+    }
+
+    /**
+     * Calculates the dimension for the current version.
+     */
+    public function getDimensionForVersion() : int
+    {
+        return 17 + 4 * $this->versionNumber;
+    }
+
+    /**
+     * Returns the number of EC blocks for a specific EC level.
+     */
+    public function getEcBlocksForLevel(ErrorCorrectionLevel $ecLevel) : EcBlocks
+    {
+        return $this->ecBlocks[$ecLevel->ordinal()];
+    }
+
+    /**
+     * Gets a provisional version number for a specific dimension.
+     *
+     * @throws InvalidArgumentException if dimension is not 1 mod 4
+     */
+    public static function getProvisionalVersionForDimension(int $dimension) : self
+    {
+        if (1 !== $dimension % 4) {
+            throw new InvalidArgumentException('Dimension is not 1 mod 4');
+        }
+
+        return self::getVersionForNumber(intdiv($dimension - 17, 4));
+    }
+
+    /**
+     * Gets a version instance for a specific version number.
+     *
+     * @throws InvalidArgumentException if version number is out of range
+     */
+    public static function getVersionForNumber(int $versionNumber) : self
+    {
+        if ($versionNumber < 1 || $versionNumber > 40) {
+            throw new InvalidArgumentException('Version number must be between 1 and 40');
+        }
+
+        return self::versions()[$versionNumber - 1];
+    }
+
+    /**
+     * Decodes version information from an integer and returns the version.
+     */
+    public static function decodeVersionInformation(int $versionBits) : ?self
+    {
+        $bestDifference = PHP_INT_MAX;
+        $bestVersion = 0;
+
+        foreach (self::VERSION_DECODE_INFO as $i => $targetVersion) {
+            if ($targetVersion === $versionBits) {
+                return self::getVersionForNumber($i + 7);
+            }
+
+            $bitsDifference = FormatInformation::numBitsDiffering($versionBits, $targetVersion);
+
+            if ($bitsDifference < $bestDifference) {
+                $bestVersion = $i + 7;
+                $bestDifference = $bitsDifference;
+            }
+        }
+
+        if ($bestDifference <= 3) {
+            return self::getVersionForNumber($bestVersion);
+        }
+
+        return null;
+    }
+
+    /**
+     * Builds the function pattern for the current version.
+     */
+    public function buildFunctionPattern() : BitMatrix
+    {
+        $dimension = $this->getDimensionForVersion();
+        $bitMatrix = new BitMatrix($dimension);
+
+        // Top left finder pattern + separator + format
+        $bitMatrix->setRegion(0, 0, 9, 9);
+        // Top right finder pattern + separator + format
+        $bitMatrix->setRegion($dimension - 8, 0, 8, 9);
+        // Bottom left finder pattern + separator + format
+        $bitMatrix->setRegion(0, $dimension - 8, 9, 8);
+
+        // Alignment patterns
+        $max = count($this->alignmentPatternCenters);
+
+        for ($x = 0; $x < $max; ++$x) {
+            $i = $this->alignmentPatternCenters[$x] - 2;
+
+            for ($y = 0; $y < $max; ++$y) {
+                if (($x === 0 && ($y === 0 || $y === $max - 1)) || ($x === $max - 1 && $y === 0)) {
+                    // No alignment patterns near the three finder paterns
+                    continue;
+                }
+
+                $bitMatrix->setRegion($this->alignmentPatternCenters[$y] - 2, $i, 5, 5);
+            }
+        }
+
+        // Vertical timing pattern
+        $bitMatrix->setRegion(6, 9, 1, $dimension - 17);
+        // Horizontal timing pattern
+        $bitMatrix->setRegion(9, 6, $dimension - 17, 1);
+
+        if ($this->versionNumber > 6) {
+            // Version info, top right
+            $bitMatrix->setRegion($dimension - 11, 0, 3, 6);
+            // Version info, bottom left
+            $bitMatrix->setRegion(0, $dimension - 11, 6, 3);
+        }
+
+        return $bitMatrix;
+    }
+
+    /**
+     * Returns a string representation for the version.
+     */
+    public function __toString() : string
+    {
+        return (string) $this->versionNumber;
+    }
+
+    /**
+     * Build and cache a specific version.
+     *
+     * See ISO 18004:2006 6.5.1 Table 9.
+     *
+     * @return array<int, self>
+     */
+    private static function versions() : array
+    {
+        if (null !== self::$versions) {
+            return self::$versions;
+        }
+
+        return self::$versions = [
+            new self(
+                1,
+                [],
+                new EcBlocks(7, new EcBlock(1, 19)),
+                new EcBlocks(10, new EcBlock(1, 16)),
+                new EcBlocks(13, new EcBlock(1, 13)),
+                new EcBlocks(17, new EcBlock(1, 9))
+            ),
+            new self(
+                2,
+                [6, 18],
+                new EcBlocks(10, new EcBlock(1, 34)),
+                new EcBlocks(16, new EcBlock(1, 28)),
+                new EcBlocks(22, new EcBlock(1, 22)),
+                new EcBlocks(28, new EcBlock(1, 16))
+            ),
+            new self(
+                3,
+                [6, 22],
+                new EcBlocks(15, new EcBlock(1, 55)),
+                new EcBlocks(26, new EcBlock(1, 44)),
+                new EcBlocks(18, new EcBlock(2, 17)),
+                new EcBlocks(22, new EcBlock(2, 13))
+            ),
+            new self(
+                4,
+                [6, 26],
+                new EcBlocks(20, new EcBlock(1, 80)),
+                new EcBlocks(18, new EcBlock(2, 32)),
+                new EcBlocks(26, new EcBlock(3, 24)),
+                new EcBlocks(16, new EcBlock(4, 9))
+            ),
+            new self(
+                5,
+                [6, 30],
+                new EcBlocks(26, new EcBlock(1, 108)),
+                new EcBlocks(24, new EcBlock(2, 43)),
+                new EcBlocks(18, new EcBlock(2, 15), new EcBlock(2, 16)),
+                new EcBlocks(22, new EcBlock(2, 11), new EcBlock(2, 12))
+            ),
+            new self(
+                6,
+                [6, 34],
+                new EcBlocks(18, new EcBlock(2, 68)),
+                new EcBlocks(16, new EcBlock(4, 27)),
+                new EcBlocks(24, new EcBlock(4, 19)),
+                new EcBlocks(28, new EcBlock(4, 15))
+            ),
+            new self(
+                7,
+                [6, 22, 38],
+                new EcBlocks(20, new EcBlock(2, 78)),
+                new EcBlocks(18, new EcBlock(4, 31)),
+                new EcBlocks(18, new EcBlock(2, 14), new EcBlock(4, 15)),
+                new EcBlocks(26, new EcBlock(4, 13), new EcBlock(1, 14))
+            ),
+            new self(
+                8,
+                [6, 24, 42],
+                new EcBlocks(24, new EcBlock(2, 97)),
+                new EcBlocks(22, new EcBlock(2, 38), new EcBlock(2, 39)),
+                new EcBlocks(22, new EcBlock(4, 18), new EcBlock(2, 19)),
+                new EcBlocks(26, new EcBlock(4, 14), new EcBlock(2, 15))
+            ),
+            new self(
+                9,
+                [6, 26, 46],
+                new EcBlocks(30, new EcBlock(2, 116)),
+                new EcBlocks(22, new EcBlock(3, 36), new EcBlock(2, 37)),
+                new EcBlocks(20, new EcBlock(4, 16), new EcBlock(4, 17)),
+                new EcBlocks(24, new EcBlock(4, 12), new EcBlock(4, 13))
+            ),
+            new self(
+                10,
+                [6, 28, 50],
+                new EcBlocks(18, new EcBlock(2, 68), new EcBlock(2, 69)),
+                new EcBlocks(26, new EcBlock(4, 43), new EcBlock(1, 44)),
+                new EcBlocks(24, new EcBlock(6, 19), new EcBlock(2, 20)),
+                new EcBlocks(28, new EcBlock(6, 15), new EcBlock(2, 16))
+            ),
+            new self(
+                11,
+                [6, 30, 54],
+                new EcBlocks(20, new EcBlock(4, 81)),
+                new EcBlocks(30, new EcBlock(1, 50), new EcBlock(4, 51)),
+                new EcBlocks(28, new EcBlock(4, 22), new EcBlock(4, 23)),
+                new EcBlocks(24, new EcBlock(3, 12), new EcBlock(8, 13))
+            ),
+            new self(
+                12,
+                [6, 32, 58],
+                new EcBlocks(24, new EcBlock(2, 92), new EcBlock(2, 93)),
+                new EcBlocks(22, new EcBlock(6, 36), new EcBlock(2, 37)),
+                new EcBlocks(26, new EcBlock(4, 20), new EcBlock(6, 21)),
+                new EcBlocks(28, new EcBlock(7, 14), new EcBlock(4, 15))
+            ),
+            new self(
+                13,
+                [6, 34, 62],
+                new EcBlocks(26, new EcBlock(4, 107)),
+                new EcBlocks(22, new EcBlock(8, 37), new EcBlock(1, 38)),
+                new EcBlocks(24, new EcBlock(8, 20), new EcBlock(4, 21)),
+                new EcBlocks(22, new EcBlock(12, 11), new EcBlock(4, 12))
+            ),
+            new self(
+                14,
+                [6, 26, 46, 66],
+                new EcBlocks(30, new EcBlock(3, 115), new EcBlock(1, 116)),
+                new EcBlocks(24, new EcBlock(4, 40), new EcBlock(5, 41)),
+                new EcBlocks(20, new EcBlock(11, 16), new EcBlock(5, 17)),
+                new EcBlocks(24, new EcBlock(11, 12), new EcBlock(5, 13))
+            ),
+            new self(
+                15,
+                [6, 26, 48, 70],
+                new EcBlocks(22, new EcBlock(5, 87), new EcBlock(1, 88)),
+                new EcBlocks(24, new EcBlock(5, 41), new EcBlock(5, 42)),
+                new EcBlocks(30, new EcBlock(5, 24), new EcBlock(7, 25)),
+                new EcBlocks(24, new EcBlock(11, 12), new EcBlock(7, 13))
+            ),
+            new self(
+                16,
+                [6, 26, 50, 74],
+                new EcBlocks(24, new EcBlock(5, 98), new EcBlock(1, 99)),
+                new EcBlocks(28, new EcBlock(7, 45), new EcBlock(3, 46)),
+                new EcBlocks(24, new EcBlock(15, 19), new EcBlock(2, 20)),
+                new EcBlocks(30, new EcBlock(3, 15), new EcBlock(13, 16))
+            ),
+            new self(
+                17,
+                [6, 30, 54, 78],
+                new EcBlocks(28, new EcBlock(1, 107), new EcBlock(5, 108)),
+                new EcBlocks(28, new EcBlock(10, 46), new EcBlock(1, 47)),
+                new EcBlocks(28, new EcBlock(1, 22), new EcBlock(15, 23)),
+                new EcBlocks(28, new EcBlock(2, 14), new EcBlock(17, 15))
+            ),
+            new self(
+                18,
+                [6, 30, 56, 82],
+                new EcBlocks(30, new EcBlock(5, 120), new EcBlock(1, 121)),
+                new EcBlocks(26, new EcBlock(9, 43), new EcBlock(4, 44)),
+                new EcBlocks(28, new EcBlock(17, 22), new EcBlock(1, 23)),
+                new EcBlocks(28, new EcBlock(2, 14), new EcBlock(19, 15))
+            ),
+            new self(
+                19,
+                [6, 30, 58, 86],
+                new EcBlocks(28, new EcBlock(3, 113), new EcBlock(4, 114)),
+                new EcBlocks(26, new EcBlock(3, 44), new EcBlock(11, 45)),
+                new EcBlocks(26, new EcBlock(17, 21), new EcBlock(4, 22)),
+                new EcBlocks(26, new EcBlock(9, 13), new EcBlock(16, 14))
+            ),
+            new self(
+                20,
+                [6, 34, 62, 90],
+                new EcBlocks(28, new EcBlock(3, 107), new EcBlock(5, 108)),
+                new EcBlocks(26, new EcBlock(3, 41), new EcBlock(13, 42)),
+                new EcBlocks(30, new EcBlock(15, 24), new EcBlock(5, 25)),
+                new EcBlocks(28, new EcBlock(15, 15), new EcBlock(10, 16))
+            ),
+            new self(
+                21,
+                [6, 28, 50, 72, 94],
+                new EcBlocks(28, new EcBlock(4, 116), new EcBlock(4, 117)),
+                new EcBlocks(26, new EcBlock(17, 42)),
+                new EcBlocks(28, new EcBlock(17, 22), new EcBlock(6, 23)),
+                new EcBlocks(30, new EcBlock(19, 16), new EcBlock(6, 17))
+            ),
+            new self(
+                22,
+                [6, 26, 50, 74, 98],
+                new EcBlocks(28, new EcBlock(2, 111), new EcBlock(7, 112)),
+                new EcBlocks(28, new EcBlock(17, 46)),
+                new EcBlocks(30, new EcBlock(7, 24), new EcBlock(16, 25)),
+                new EcBlocks(24, new EcBlock(34, 13))
+            ),
+            new self(
+                23,
+                [6, 30, 54, 78, 102],
+                new EcBlocks(30, new EcBlock(4, 121), new EcBlock(5, 122)),
+                new EcBlocks(28, new EcBlock(4, 47), new EcBlock(14, 48)),
+                new EcBlocks(30, new EcBlock(11, 24), new EcBlock(14, 25)),
+                new EcBlocks(30, new EcBlock(16, 15), new EcBlock(14, 16))
+            ),
+            new self(
+                24,
+                [6, 28, 54, 80, 106],
+                new EcBlocks(30, new EcBlock(6, 117), new EcBlock(4, 118)),
+                new EcBlocks(28, new EcBlock(6, 45), new EcBlock(14, 46)),
+                new EcBlocks(30, new EcBlock(11, 24), new EcBlock(16, 25)),
+                new EcBlocks(30, new EcBlock(30, 16), new EcBlock(2, 17))
+            ),
+            new self(
+                25,
+                [6, 32, 58, 84, 110],
+                new EcBlocks(26, new EcBlock(8, 106), new EcBlock(4, 107)),
+                new EcBlocks(28, new EcBlock(8, 47), new EcBlock(13, 48)),
+                new EcBlocks(30, new EcBlock(7, 24), new EcBlock(22, 25)),
+                new EcBlocks(30, new EcBlock(22, 15), new EcBlock(13, 16))
+            ),
+            new self(
+                26,
+                [6, 30, 58, 86, 114],
+                new EcBlocks(28, new EcBlock(10, 114), new EcBlock(2, 115)),
+                new EcBlocks(28, new EcBlock(19, 46), new EcBlock(4, 47)),
+                new EcBlocks(28, new EcBlock(28, 22), new EcBlock(6, 23)),
+                new EcBlocks(30, new EcBlock(33, 16), new EcBlock(4, 17))
+            ),
+            new self(
+                27,
+                [6, 34, 62, 90, 118],
+                new EcBlocks(30, new EcBlock(8, 122), new EcBlock(4, 123)),
+                new EcBlocks(28, new EcBlock(22, 45), new EcBlock(3, 46)),
+                new EcBlocks(30, new EcBlock(8, 23), new EcBlock(26, 24)),
+                new EcBlocks(30, new EcBlock(12, 15), new EcBlock(28, 16))
+            ),
+            new self(
+                28,
+                [6, 26, 50, 74, 98, 122],
+                new EcBlocks(30, new EcBlock(3, 117), new EcBlock(10, 118)),
+                new EcBlocks(28, new EcBlock(3, 45), new EcBlock(23, 46)),
+                new EcBlocks(30, new EcBlock(4, 24), new EcBlock(31, 25)),
+                new EcBlocks(30, new EcBlock(11, 15), new EcBlock(31, 16))
+            ),
+            new self(
+                29,
+                [6, 30, 54, 78, 102, 126],
+                new EcBlocks(30, new EcBlock(7, 116), new EcBlock(7, 117)),
+                new EcBlocks(28, new EcBlock(21, 45), new EcBlock(7, 46)),
+                new EcBlocks(30, new EcBlock(1, 23), new EcBlock(37, 24)),
+                new EcBlocks(30, new EcBlock(19, 15), new EcBlock(26, 16))
+            ),
+            new self(
+                30,
+                [6, 26, 52, 78, 104, 130],
+                new EcBlocks(30, new EcBlock(5, 115), new EcBlock(10, 116)),
+                new EcBlocks(28, new EcBlock(19, 47), new EcBlock(10, 48)),
+                new EcBlocks(30, new EcBlock(15, 24), new EcBlock(25, 25)),
+                new EcBlocks(30, new EcBlock(23, 15), new EcBlock(25, 16))
+            ),
+            new self(
+                31,
+                [6, 30, 56, 82, 108, 134],
+                new EcBlocks(30, new EcBlock(13, 115), new EcBlock(3, 116)),
+                new EcBlocks(28, new EcBlock(2, 46), new EcBlock(29, 47)),
+                new EcBlocks(30, new EcBlock(42, 24), new EcBlock(1, 25)),
+                new EcBlocks(30, new EcBlock(23, 15), new EcBlock(28, 16))
+            ),
+            new self(
+                32,
+                [6, 34, 60, 86, 112, 138],
+                new EcBlocks(30, new EcBlock(17, 115)),
+                new EcBlocks(28, new EcBlock(10, 46), new EcBlock(23, 47)),
+                new EcBlocks(30, new EcBlock(10, 24), new EcBlock(35, 25)),
+                new EcBlocks(30, new EcBlock(19, 15), new EcBlock(35, 16))
+            ),
+            new self(
+                33,
+                [6, 30, 58, 86, 114, 142],
+                new EcBlocks(30, new EcBlock(17, 115), new EcBlock(1, 116)),
+                new EcBlocks(28, new EcBlock(14, 46), new EcBlock(21, 47)),
+                new EcBlocks(30, new EcBlock(29, 24), new EcBlock(19, 25)),
+                new EcBlocks(30, new EcBlock(11, 15), new EcBlock(46, 16))
+            ),
+            new self(
+                34,
+                [6, 34, 62, 90, 118, 146],
+                new EcBlocks(30, new EcBlock(13, 115), new EcBlock(6, 116)),
+                new EcBlocks(28, new EcBlock(14, 46), new EcBlock(23, 47)),
+                new EcBlocks(30, new EcBlock(44, 24), new EcBlock(7, 25)),
+                new EcBlocks(30, new EcBlock(59, 16), new EcBlock(1, 17))
+            ),
+            new self(
+                35,
+                [6, 30, 54, 78, 102, 126, 150],
+                new EcBlocks(30, new EcBlock(12, 121), new EcBlock(7, 122)),
+                new EcBlocks(28, new EcBlock(12, 47), new EcBlock(26, 48)),
+                new EcBlocks(30, new EcBlock(39, 24), new EcBlock(14, 25)),
+                new EcBlocks(30, new EcBlock(22, 15), new EcBlock(41, 16))
+            ),
+            new self(
+                36,
+                [6, 24, 50, 76, 102, 128, 154],
+                new EcBlocks(30, new EcBlock(6, 121), new EcBlock(14, 122)),
+                new EcBlocks(28, new EcBlock(6, 47), new EcBlock(34, 48)),
+                new EcBlocks(30, new EcBlock(46, 24), new EcBlock(10, 25)),
+                new EcBlocks(30, new EcBlock(2, 15), new EcBlock(64, 16))
+            ),
+            new self(
+                37,
+                [6, 28, 54, 80, 106, 132, 158],
+                new EcBlocks(30, new EcBlock(17, 122), new EcBlock(4, 123)),
+                new EcBlocks(28, new EcBlock(29, 46), new EcBlock(14, 47)),
+                new EcBlocks(30, new EcBlock(49, 24), new EcBlock(10, 25)),
+                new EcBlocks(30, new EcBlock(24, 15), new EcBlock(46, 16))
+            ),
+            new self(
+                38,
+                [6, 32, 58, 84, 110, 136, 162],
+                new EcBlocks(30, new EcBlock(4, 122), new EcBlock(18, 123)),
+                new EcBlocks(28, new EcBlock(13, 46), new EcBlock(32, 47)),
+                new EcBlocks(30, new EcBlock(48, 24), new EcBlock(14, 25)),
+                new EcBlocks(30, new EcBlock(42, 15), new EcBlock(32, 16))
+            ),
+            new self(
+                39,
+                [6, 26, 54, 82, 110, 138, 166],
+                new EcBlocks(30, new EcBlock(20, 117), new EcBlock(4, 118)),
+                new EcBlocks(28, new EcBlock(40, 47), new EcBlock(7, 48)),
+                new EcBlocks(30, new EcBlock(43, 24), new EcBlock(22, 25)),
+                new EcBlocks(30, new EcBlock(10, 15), new EcBlock(67, 16))
+            ),
+            new self(
+                40,
+                [6, 30, 58, 86, 114, 142, 170],
+                new EcBlocks(30, new EcBlock(19, 118), new EcBlock(6, 119)),
+                new EcBlocks(28, new EcBlock(18, 47), new EcBlock(31, 48)),
+                new EcBlocks(30, new EcBlock(34, 24), new EcBlock(34, 25)),
+                new EcBlocks(30, new EcBlock(20, 15), new EcBlock(61, 16))
+            ),
+        ];
+    }
+}

+ 58 - 0
htdocs/includes/bacon/bacon-qr-code/src/Encoder/BlockPair.php

@@ -0,0 +1,58 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Encoder;
+
+use SplFixedArray;
+
+/**
+ * Block pair.
+ */
+final class BlockPair
+{
+    /**
+     * Data bytes in the block.
+     *
+     * @var SplFixedArray<int>
+     */
+    private $dataBytes;
+
+    /**
+     * Error correction bytes in the block.
+     *
+     * @var SplFixedArray<int>
+     */
+    private $errorCorrectionBytes;
+
+    /**
+     * Creates a new block pair.
+     *
+     * @param SplFixedArray<int> $data
+     * @param SplFixedArray<int> $errorCorrection
+     */
+    public function __construct(SplFixedArray $data, SplFixedArray $errorCorrection)
+    {
+        $this->dataBytes = $data;
+        $this->errorCorrectionBytes = $errorCorrection;
+    }
+
+    /**
+     * Gets the data bytes.
+     *
+     * @return SplFixedArray<int>
+     */
+    public function getDataBytes() : SplFixedArray
+    {
+        return $this->dataBytes;
+    }
+
+    /**
+     * Gets the error correction bytes.
+     *
+     * @return SplFixedArray<int>
+     */
+    public function getErrorCorrectionBytes() : SplFixedArray
+    {
+        return $this->errorCorrectionBytes;
+    }
+}

+ 150 - 0
htdocs/includes/bacon/bacon-qr-code/src/Encoder/ByteMatrix.php

@@ -0,0 +1,150 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Encoder;
+
+use SplFixedArray;
+use Traversable;
+
+/**
+ * Byte matrix.
+ */
+final class ByteMatrix
+{
+    /**
+     * Bytes in the matrix, represented as array.
+     *
+     * @var SplFixedArray<SplFixedArray<int>>
+     */
+    private $bytes;
+
+    /**
+     * Width of the matrix.
+     *
+     * @var int
+     */
+    private $width;
+
+    /**
+     * Height of the matrix.
+     *
+     * @var int
+     */
+    private $height;
+
+    public function __construct(int $width, int $height)
+    {
+        $this->height = $height;
+        $this->width = $width;
+        $this->bytes = new SplFixedArray($height);
+
+        for ($y = 0; $y < $height; ++$y) {
+            $this->bytes[$y] = SplFixedArray::fromArray(array_fill(0, $width, 0));
+        }
+    }
+
+    /**
+     * Gets the width of the matrix.
+     */
+    public function getWidth() : int
+    {
+        return $this->width;
+    }
+
+    /**
+     * Gets the height of the matrix.
+     */
+    public function getHeight() : int
+    {
+        return $this->height;
+    }
+
+    /**
+     * Gets the internal representation of the matrix.
+     *
+     * @return SplFixedArray<SplFixedArray<int>>
+     */
+    public function getArray() : SplFixedArray
+    {
+        return $this->bytes;
+    }
+
+    /**
+     * @return Traversable<int>
+     */
+    public function getBytes() : Traversable
+    {
+        foreach ($this->bytes as $row) {
+            foreach ($row as $byte) {
+                yield $byte;
+            }
+        }
+    }
+
+    /**
+     * Gets the byte for a specific position.
+     */
+    public function get(int $x, int $y) : int
+    {
+        return $this->bytes[$y][$x];
+    }
+
+    /**
+     * Sets the byte for a specific position.
+     */
+    public function set(int $x, int $y, int $value) : void
+    {
+        $this->bytes[$y][$x] = $value;
+    }
+
+    /**
+     * Clears the matrix with a specific value.
+     */
+    public function clear(int $value) : void
+    {
+        for ($y = 0; $y < $this->height; ++$y) {
+            for ($x = 0; $x < $this->width; ++$x) {
+                $this->bytes[$y][$x] = $value;
+            }
+        }
+    }
+
+    public function __clone()
+    {
+        $this->bytes = clone $this->bytes;
+
+        foreach ($this->bytes as $index => $row) {
+            $this->bytes[$index] = clone $row;
+        }
+    }
+
+    /**
+     * Returns a string representation of the matrix.
+     */
+    public function __toString() : string
+    {
+        $result = '';
+
+        for ($y = 0; $y < $this->height; $y++) {
+            for ($x = 0; $x < $this->width; $x++) {
+                switch ($this->bytes[$y][$x]) {
+                    case 0:
+                        $result .= ' 0';
+                        break;
+
+                    case 1:
+                        $result .= ' 1';
+                        break;
+
+                    default:
+                        $result .= '  ';
+                        break;
+                }
+            }
+
+            $result .= "\n";
+        }
+
+        return $result;
+    }
+}

+ 668 - 0
htdocs/includes/bacon/bacon-qr-code/src/Encoder/Encoder.php

@@ -0,0 +1,668 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Encoder;
+
+use BaconQrCode\Common\BitArray;
+use BaconQrCode\Common\CharacterSetEci;
+use BaconQrCode\Common\ErrorCorrectionLevel;
+use BaconQrCode\Common\Mode;
+use BaconQrCode\Common\ReedSolomonCodec;
+use BaconQrCode\Common\Version;
+use BaconQrCode\Exception\WriterException;
+use SplFixedArray;
+
+/**
+ * Encoder.
+ */
+final class Encoder
+{
+    /**
+     * Default byte encoding.
+     */
+    public const DEFAULT_BYTE_MODE_ECODING = 'ISO-8859-1';
+
+    /**
+     * The original table is defined in the table 5 of JISX0510:2004 (p.19).
+     */
+    private const ALPHANUMERIC_TABLE = [
+        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,  // 0x00-0x0f
+        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,  // 0x10-0x1f
+        36, -1, -1, -1, 37, 38, -1, -1, -1, -1, 39, 40, -1, 41, 42, 43,  // 0x20-0x2f
+        0,   1,  2,  3,  4,  5,  6,  7,  8,  9, 44, -1, -1, -1, -1, -1,  // 0x30-0x3f
+        -1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,  // 0x40-0x4f
+        25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, -1, -1, -1, -1, -1,  // 0x50-0x5f
+    ];
+
+    /**
+     * Codec cache.
+     *
+     * @var array<string,ReedSolomonCodec>
+     */
+    private static $codecs = [];
+
+    /**
+     * Encodes "content" with the error correction level "ecLevel".
+     */
+    public static function encode(
+        string $content,
+        ErrorCorrectionLevel $ecLevel,
+        string $encoding = self::DEFAULT_BYTE_MODE_ECODING,
+        ?Version $forcedVersion = null
+    ) : QrCode {
+        // Pick an encoding mode appropriate for the content. Note that this
+        // will not attempt to use multiple modes / segments even if that were
+        // more efficient. Would be nice.
+        $mode = self::chooseMode($content, $encoding);
+
+        // This will store the header information, like mode and length, as well
+        // as "header" segments like an ECI segment.
+        $headerBits = new BitArray();
+
+        // Append ECI segment if applicable
+        if (Mode::BYTE() === $mode && self::DEFAULT_BYTE_MODE_ECODING !== $encoding) {
+            $eci = CharacterSetEci::getCharacterSetEciByName($encoding);
+
+            if (null !== $eci) {
+                self::appendEci($eci, $headerBits);
+            }
+        }
+
+        // (With ECI in place,) Write the mode marker
+        self::appendModeInfo($mode, $headerBits);
+
+        // Collect data within the main segment, separately, to count its size
+        // if needed. Don't add it to main payload yet.
+        $dataBits = new BitArray();
+        self::appendBytes($content, $mode, $dataBits, $encoding);
+
+        // Hard part: need to know version to know how many bits length takes.
+        // But need to know how many bits it takes to know version. First we
+        // take a guess at version by assuming version will be the minimum, 1:
+        $provisionalBitsNeeded = $headerBits->getSize()
+            + $mode->getCharacterCountBits(Version::getVersionForNumber(1))
+            + $dataBits->getSize();
+        $provisionalVersion = self::chooseVersion($provisionalBitsNeeded, $ecLevel);
+
+        // Use that guess to calculate the right version. I am still not sure
+        // this works in 100% of cases.
+        $bitsNeeded = $headerBits->getSize()
+            + $mode->getCharacterCountBits($provisionalVersion)
+            + $dataBits->getSize();
+        $version = self::chooseVersion($bitsNeeded, $ecLevel);
+
+        if (null !== $forcedVersion) {
+            // Forced version check
+            if ($version->getVersionNumber() <= $forcedVersion->getVersionNumber()) {
+                // Calculated minimum version is same or equal as forced version
+                $version = $forcedVersion;
+            } else {
+                throw new WriterException(
+                    'Invalid version! Calculated version: '
+                    . $version->getVersionNumber()
+                    . ', requested version: '
+                    . $forcedVersion->getVersionNumber()
+                );
+            }
+        }
+
+        $headerAndDataBits = new BitArray();
+        $headerAndDataBits->appendBitArray($headerBits);
+
+        // Find "length" of main segment and write it.
+        $numLetters = (Mode::BYTE() === $mode ? $dataBits->getSizeInBytes() : strlen($content));
+        self::appendLengthInfo($numLetters, $version, $mode, $headerAndDataBits);
+
+        // Put data together into the overall payload.
+        $headerAndDataBits->appendBitArray($dataBits);
+        $ecBlocks = $version->getEcBlocksForLevel($ecLevel);
+        $numDataBytes = $version->getTotalCodewords() - $ecBlocks->getTotalEcCodewords();
+
+        // Terminate the bits properly.
+        self::terminateBits($numDataBytes, $headerAndDataBits);
+
+        // Interleave data bits with error correction code.
+        $finalBits = self::interleaveWithEcBytes(
+            $headerAndDataBits,
+            $version->getTotalCodewords(),
+            $numDataBytes,
+            $ecBlocks->getNumBlocks()
+        );
+
+        // Choose the mask pattern.
+        $dimension = $version->getDimensionForVersion();
+        $matrix = new ByteMatrix($dimension, $dimension);
+        $maskPattern = self::chooseMaskPattern($finalBits, $ecLevel, $version, $matrix);
+
+        // Build the matrix.
+        MatrixUtil::buildMatrix($finalBits, $ecLevel, $version, $maskPattern, $matrix);
+
+        return new QrCode($mode, $ecLevel, $version, $maskPattern, $matrix);
+    }
+
+    /**
+     * Gets the alphanumeric code for a byte.
+     */
+    private static function getAlphanumericCode(int $code) : int
+    {
+        if (isset(self::ALPHANUMERIC_TABLE[$code])) {
+            return self::ALPHANUMERIC_TABLE[$code];
+        }
+
+        return -1;
+    }
+
+    /**
+     * Chooses the best mode for a given content.
+     */
+    private static function chooseMode(string $content, string $encoding = null) : Mode
+    {
+        if (null !== $encoding && 0 === strcasecmp($encoding, 'SHIFT-JIS')) {
+            return self::isOnlyDoubleByteKanji($content) ? Mode::KANJI() : Mode::BYTE();
+        }
+
+        $hasNumeric = false;
+        $hasAlphanumeric = false;
+        $contentLength = strlen($content);
+
+        for ($i = 0; $i < $contentLength; ++$i) {
+            $char = $content[$i];
+
+            if (ctype_digit($char)) {
+                $hasNumeric = true;
+            } elseif (-1 !== self::getAlphanumericCode(ord($char))) {
+                $hasAlphanumeric = true;
+            } else {
+                return Mode::BYTE();
+            }
+        }
+
+        if ($hasAlphanumeric) {
+            return Mode::ALPHANUMERIC();
+        } elseif ($hasNumeric) {
+            return Mode::NUMERIC();
+        }
+
+        return Mode::BYTE();
+    }
+
+    /**
+     * Calculates the mask penalty for a matrix.
+     */
+    private static function calculateMaskPenalty(ByteMatrix $matrix) : int
+    {
+        return (
+            MaskUtil::applyMaskPenaltyRule1($matrix)
+            + MaskUtil::applyMaskPenaltyRule2($matrix)
+            + MaskUtil::applyMaskPenaltyRule3($matrix)
+            + MaskUtil::applyMaskPenaltyRule4($matrix)
+        );
+    }
+
+    /**
+     * Checks if content only consists of double-byte kanji characters.
+     */
+    private static function isOnlyDoubleByteKanji(string $content) : bool
+    {
+        $bytes = @iconv('utf-8', 'SHIFT-JIS', $content);
+
+        if (false === $bytes) {
+            return false;
+        }
+
+        $length = strlen($bytes);
+
+        if (0 !== $length % 2) {
+            return false;
+        }
+
+        for ($i = 0; $i < $length; $i += 2) {
+            $byte = $bytes[$i] & 0xff;
+
+            if (($byte < 0x81 || $byte > 0x9f) && $byte < 0xe0 || $byte > 0xeb) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Chooses the best mask pattern for a matrix.
+     */
+    private static function chooseMaskPattern(
+        BitArray $bits,
+        ErrorCorrectionLevel $ecLevel,
+        Version $version,
+        ByteMatrix $matrix
+    ) : int {
+        $minPenalty = PHP_INT_MAX;
+        $bestMaskPattern = -1;
+
+        for ($maskPattern = 0; $maskPattern < QrCode::NUM_MASK_PATTERNS; ++$maskPattern) {
+            MatrixUtil::buildMatrix($bits, $ecLevel, $version, $maskPattern, $matrix);
+            $penalty = self::calculateMaskPenalty($matrix);
+
+            if ($penalty < $minPenalty) {
+                $minPenalty = $penalty;
+                $bestMaskPattern = $maskPattern;
+            }
+        }
+
+        return $bestMaskPattern;
+    }
+
+    /**
+     * Chooses the best version for the input.
+     *
+     * @throws WriterException if data is too big
+     */
+    private static function chooseVersion(int $numInputBits, ErrorCorrectionLevel $ecLevel) : Version
+    {
+        for ($versionNum = 1; $versionNum <= 40; ++$versionNum) {
+            $version = Version::getVersionForNumber($versionNum);
+            $numBytes = $version->getTotalCodewords();
+
+            $ecBlocks = $version->getEcBlocksForLevel($ecLevel);
+            $numEcBytes = $ecBlocks->getTotalEcCodewords();
+
+            $numDataBytes = $numBytes - $numEcBytes;
+            $totalInputBytes = intdiv($numInputBits + 8, 8);
+
+            if ($numDataBytes >= $totalInputBytes) {
+                return $version;
+            }
+        }
+
+        throw new WriterException('Data too big');
+    }
+
+    /**
+     * Terminates the bits in a bit array.
+     *
+     * @throws WriterException if data bits cannot fit in the QR code
+     * @throws WriterException if bits size does not equal the capacity
+     */
+    private static function terminateBits(int $numDataBytes, BitArray $bits) : void
+    {
+        $capacity = $numDataBytes << 3;
+
+        if ($bits->getSize() > $capacity) {
+            throw new WriterException('Data bits cannot fit in the QR code');
+        }
+
+        for ($i = 0; $i < 4 && $bits->getSize() < $capacity; ++$i) {
+            $bits->appendBit(false);
+        }
+
+        $numBitsInLastByte = $bits->getSize() & 0x7;
+
+        if ($numBitsInLastByte > 0) {
+            for ($i = $numBitsInLastByte; $i < 8; ++$i) {
+                $bits->appendBit(false);
+            }
+        }
+
+        $numPaddingBytes = $numDataBytes - $bits->getSizeInBytes();
+
+        for ($i = 0; $i < $numPaddingBytes; ++$i) {
+            $bits->appendBits(0 === ($i & 0x1) ? 0xec : 0x11, 8);
+        }
+
+        if ($bits->getSize() !== $capacity) {
+            throw new WriterException('Bits size does not equal capacity');
+        }
+    }
+
+    /**
+     * Gets number of data- and EC bytes for a block ID.
+     *
+     * @return int[]
+     * @throws WriterException if block ID is too large
+     * @throws WriterException if EC bytes mismatch
+     * @throws WriterException if RS blocks mismatch
+     * @throws WriterException if total bytes mismatch
+     */
+    private static function getNumDataBytesAndNumEcBytesForBlockId(
+        int $numTotalBytes,
+        int $numDataBytes,
+        int $numRsBlocks,
+        int $blockId
+    ) : array {
+        if ($blockId >= $numRsBlocks) {
+            throw new WriterException('Block ID too large');
+        }
+
+        $numRsBlocksInGroup2 = $numTotalBytes % $numRsBlocks;
+        $numRsBlocksInGroup1 = $numRsBlocks - $numRsBlocksInGroup2;
+        $numTotalBytesInGroup1 = intdiv($numTotalBytes, $numRsBlocks);
+        $numTotalBytesInGroup2 = $numTotalBytesInGroup1 + 1;
+        $numDataBytesInGroup1 = intdiv($numDataBytes, $numRsBlocks);
+        $numDataBytesInGroup2 = $numDataBytesInGroup1 + 1;
+        $numEcBytesInGroup1 = $numTotalBytesInGroup1 - $numDataBytesInGroup1;
+        $numEcBytesInGroup2 = $numTotalBytesInGroup2 - $numDataBytesInGroup2;
+
+        if ($numEcBytesInGroup1 !== $numEcBytesInGroup2) {
+            throw new WriterException('EC bytes mismatch');
+        }
+
+        if ($numRsBlocks !== $numRsBlocksInGroup1 + $numRsBlocksInGroup2) {
+            throw new WriterException('RS blocks mismatch');
+        }
+
+        if ($numTotalBytes !==
+            (($numDataBytesInGroup1 + $numEcBytesInGroup1) * $numRsBlocksInGroup1)
+            + (($numDataBytesInGroup2 + $numEcBytesInGroup2) * $numRsBlocksInGroup2)
+        ) {
+            throw new WriterException('Total bytes mismatch');
+        }
+
+        if ($blockId < $numRsBlocksInGroup1) {
+            return [$numDataBytesInGroup1, $numEcBytesInGroup1];
+        } else {
+            return [$numDataBytesInGroup2, $numEcBytesInGroup2];
+        }
+    }
+
+    /**
+     * Interleaves data with EC bytes.
+     *
+     * @throws WriterException if number of bits and data bytes does not match
+     * @throws WriterException if data bytes does not match offset
+     * @throws WriterException if an interleaving error occurs
+     */
+    private static function interleaveWithEcBytes(
+        BitArray $bits,
+        int $numTotalBytes,
+        int $numDataBytes,
+        int $numRsBlocks
+    ) : BitArray {
+        if ($bits->getSizeInBytes() !== $numDataBytes) {
+            throw new WriterException('Number of bits and data bytes does not match');
+        }
+
+        $dataBytesOffset = 0;
+        $maxNumDataBytes = 0;
+        $maxNumEcBytes   = 0;
+
+        $blocks = new SplFixedArray($numRsBlocks);
+
+        for ($i = 0; $i < $numRsBlocks; ++$i) {
+            list($numDataBytesInBlock, $numEcBytesInBlock) = self::getNumDataBytesAndNumEcBytesForBlockId(
+                $numTotalBytes,
+                $numDataBytes,
+                $numRsBlocks,
+                $i
+            );
+
+            $size = $numDataBytesInBlock;
+            $dataBytes = $bits->toBytes(8 * $dataBytesOffset, $size);
+            $ecBytes = self::generateEcBytes($dataBytes, $numEcBytesInBlock);
+            $blocks[$i] = new BlockPair($dataBytes, $ecBytes);
+
+            $maxNumDataBytes = max($maxNumDataBytes, $size);
+            $maxNumEcBytes = max($maxNumEcBytes, count($ecBytes));
+            $dataBytesOffset += $numDataBytesInBlock;
+        }
+
+        if ($numDataBytes !== $dataBytesOffset) {
+            throw new WriterException('Data bytes does not match offset');
+        }
+
+        $result = new BitArray();
+
+        for ($i = 0; $i < $maxNumDataBytes; ++$i) {
+            foreach ($blocks as $block) {
+                $dataBytes = $block->getDataBytes();
+
+                if ($i < count($dataBytes)) {
+                    $result->appendBits($dataBytes[$i], 8);
+                }
+            }
+        }
+
+        for ($i = 0; $i < $maxNumEcBytes; ++$i) {
+            foreach ($blocks as $block) {
+                $ecBytes = $block->getErrorCorrectionBytes();
+
+                if ($i < count($ecBytes)) {
+                    $result->appendBits($ecBytes[$i], 8);
+                }
+            }
+        }
+
+        if ($numTotalBytes !== $result->getSizeInBytes()) {
+            throw new WriterException(
+                'Interleaving error: ' . $numTotalBytes . ' and ' . $result->getSizeInBytes() . ' differ'
+            );
+        }
+
+        return $result;
+    }
+
+    /**
+     * Generates EC bytes for given data.
+     *
+     * @param  SplFixedArray<int> $dataBytes
+     * @return SplFixedArray<int>
+     */
+    private static function generateEcBytes(SplFixedArray $dataBytes, int $numEcBytesInBlock) : SplFixedArray
+    {
+        $numDataBytes = count($dataBytes);
+        $toEncode = new SplFixedArray($numDataBytes + $numEcBytesInBlock);
+
+        for ($i = 0; $i < $numDataBytes; $i++) {
+            $toEncode[$i] = $dataBytes[$i] & 0xff;
+        }
+
+        $ecBytes = new SplFixedArray($numEcBytesInBlock);
+        $codec = self::getCodec($numDataBytes, $numEcBytesInBlock);
+        $codec->encode($toEncode, $ecBytes);
+
+        return $ecBytes;
+    }
+
+    /**
+     * Gets an RS codec and caches it.
+     */
+    private static function getCodec(int $numDataBytes, int $numEcBytesInBlock) : ReedSolomonCodec
+    {
+        $cacheId = $numDataBytes . '-' . $numEcBytesInBlock;
+
+        if (isset(self::$codecs[$cacheId])) {
+            return self::$codecs[$cacheId];
+        }
+
+        return self::$codecs[$cacheId] = new ReedSolomonCodec(
+            8,
+            0x11d,
+            0,
+            1,
+            $numEcBytesInBlock,
+            255 - $numDataBytes - $numEcBytesInBlock
+        );
+    }
+
+    /**
+     * Appends mode information to a bit array.
+     */
+    private static function appendModeInfo(Mode $mode, BitArray $bits) : void
+    {
+        $bits->appendBits($mode->getBits(), 4);
+    }
+
+    /**
+     * Appends length information to a bit array.
+     *
+     * @throws WriterException if num letters is bigger than expected
+     */
+    private static function appendLengthInfo(int $numLetters, Version $version, Mode $mode, BitArray $bits) : void
+    {
+        $numBits = $mode->getCharacterCountBits($version);
+
+        if ($numLetters >= (1 << $numBits)) {
+            throw new WriterException($numLetters . ' is bigger than ' . ((1 << $numBits) - 1));
+        }
+
+        $bits->appendBits($numLetters, $numBits);
+    }
+
+    /**
+     * Appends bytes to a bit array in a specific mode.
+     *
+     * @throws WriterException if an invalid mode was supplied
+     */
+    private static function appendBytes(string $content, Mode $mode, BitArray $bits, string $encoding) : void
+    {
+        switch ($mode) {
+            case Mode::NUMERIC():
+                self::appendNumericBytes($content, $bits);
+                break;
+
+            case Mode::ALPHANUMERIC():
+                self::appendAlphanumericBytes($content, $bits);
+                break;
+
+            case Mode::BYTE():
+                self::append8BitBytes($content, $bits, $encoding);
+                break;
+
+            case Mode::KANJI():
+                self::appendKanjiBytes($content, $bits);
+                break;
+
+            default:
+                throw new WriterException('Invalid mode: ' . $mode);
+        }
+    }
+
+    /**
+     * Appends numeric bytes to a bit array.
+     */
+    private static function appendNumericBytes(string $content, BitArray $bits) : void
+    {
+        $length = strlen($content);
+        $i = 0;
+
+        while ($i < $length) {
+            $num1 = (int) $content[$i];
+
+            if ($i + 2 < $length) {
+                // Encode three numeric letters in ten bits.
+                $num2 = (int) $content[$i + 1];
+                $num3 = (int) $content[$i + 2];
+                $bits->appendBits($num1 * 100 + $num2 * 10 + $num3, 10);
+                $i += 3;
+            } elseif ($i + 1 < $length) {
+                // Encode two numeric letters in seven bits.
+                $num2 = (int) $content[$i + 1];
+                $bits->appendBits($num1 * 10 + $num2, 7);
+                $i += 2;
+            } else {
+                // Encode one numeric letter in four bits.
+                $bits->appendBits($num1, 4);
+                ++$i;
+            }
+        }
+    }
+
+    /**
+     * Appends alpha-numeric bytes to a bit array.
+     *
+     * @throws WriterException if an invalid alphanumeric code was found
+     */
+    private static function appendAlphanumericBytes(string $content, BitArray $bits) : void
+    {
+        $length = strlen($content);
+        $i = 0;
+
+        while ($i < $length) {
+            $code1 = self::getAlphanumericCode(ord($content[$i]));
+
+            if (-1 === $code1) {
+                throw new WriterException('Invalid alphanumeric code');
+            }
+
+            if ($i + 1 < $length) {
+                $code2 = self::getAlphanumericCode(ord($content[$i + 1]));
+
+                if (-1 === $code2) {
+                    throw new WriterException('Invalid alphanumeric code');
+                }
+
+                // Encode two alphanumeric letters in 11 bits.
+                $bits->appendBits($code1 * 45 + $code2, 11);
+                $i += 2;
+            } else {
+                // Encode one alphanumeric letter in six bits.
+                $bits->appendBits($code1, 6);
+                ++$i;
+            }
+        }
+    }
+
+    /**
+     * Appends regular 8-bit bytes to a bit array.
+     *
+     * @throws WriterException if content cannot be encoded to target encoding
+     */
+    private static function append8BitBytes(string $content, BitArray $bits, string $encoding) : void
+    {
+        $bytes = @iconv('utf-8', $encoding, $content);
+
+        if (false === $bytes) {
+            throw new WriterException('Could not encode content to ' . $encoding);
+        }
+
+        $length = strlen($bytes);
+
+        for ($i = 0; $i < $length; $i++) {
+            $bits->appendBits(ord($bytes[$i]), 8);
+        }
+    }
+
+    /**
+     * Appends KANJI bytes to a bit array.
+     *
+     * @throws WriterException if content does not seem to be encoded in SHIFT-JIS
+     * @throws WriterException if an invalid byte sequence occurs
+     */
+    private static function appendKanjiBytes(string $content, BitArray $bits) : void
+    {
+        if (strlen($content) % 2 > 0) {
+            // We just do a simple length check here. The for loop will check
+            // individual characters.
+            throw new WriterException('Content does not seem to be encoded in SHIFT-JIS');
+        }
+
+        $length = strlen($content);
+
+        for ($i = 0; $i < $length; $i += 2) {
+            $byte1 = ord($content[$i]) & 0xff;
+            $byte2 = ord($content[$i + 1]) & 0xff;
+            $code = ($byte1 << 8) | $byte2;
+
+            if ($code >= 0x8140 && $code <= 0x9ffc) {
+                $subtracted = $code - 0x8140;
+            } elseif ($code >= 0xe040 && $code <= 0xebbf) {
+                $subtracted = $code - 0xc140;
+            } else {
+                throw new WriterException('Invalid byte sequence');
+            }
+
+            $encoded = (($subtracted >> 8) * 0xc0) + ($subtracted & 0xff);
+
+            $bits->appendBits($encoded, 13);
+        }
+    }
+
+    /**
+     * Appends ECI information to a bit array.
+     */
+    private static function appendEci(CharacterSetEci $eci, BitArray $bits) : void
+    {
+        $mode = Mode::ECI();
+        $bits->appendBits($mode->getBits(), 4);
+        $bits->appendBits($eci->getValue(), 8);
+    }
+}

+ 271 - 0
htdocs/includes/bacon/bacon-qr-code/src/Encoder/MaskUtil.php

@@ -0,0 +1,271 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Encoder;
+
+use BaconQrCode\Common\BitUtils;
+use BaconQrCode\Exception\InvalidArgumentException;
+
+/**
+ * Mask utility.
+ */
+final class MaskUtil
+{
+    /**#@+
+     * Penalty weights from section 6.8.2.1
+     */
+    const N1 = 3;
+    const N2 = 3;
+    const N3 = 40;
+    const N4 = 10;
+    /**#@-*/
+
+    private function __construct()
+    {
+    }
+
+    /**
+     * Applies mask penalty rule 1 and returns the penalty.
+     *
+     * Finds repetitive cells with the same color and gives penalty to them.
+     * Example: 00000 or 11111.
+     */
+    public static function applyMaskPenaltyRule1(ByteMatrix $matrix) : int
+    {
+        return (
+            self::applyMaskPenaltyRule1Internal($matrix, true)
+            + self::applyMaskPenaltyRule1Internal($matrix, false)
+        );
+    }
+
+    /**
+     * Applies mask penalty rule 2 and returns the penalty.
+     *
+     * Finds 2x2 blocks with the same color and gives penalty to them. This is
+     * actually equivalent to the spec's rule, which is to find MxN blocks and
+     * give a penalty proportional to (M-1)x(N-1), because this is the number of
+     * 2x2 blocks inside such a block.
+     */
+    public static function applyMaskPenaltyRule2(ByteMatrix $matrix) : int
+    {
+        $penalty = 0;
+        $array = $matrix->getArray();
+        $width = $matrix->getWidth();
+        $height = $matrix->getHeight();
+
+        for ($y = 0; $y < $height - 1; ++$y) {
+            for ($x = 0; $x < $width - 1; ++$x) {
+                $value = $array[$y][$x];
+
+                if ($value === $array[$y][$x + 1]
+                    && $value === $array[$y + 1][$x]
+                    && $value === $array[$y + 1][$x + 1]
+                ) {
+                    ++$penalty;
+                }
+            }
+        }
+
+        return self::N2 * $penalty;
+    }
+
+    /**
+     * Applies mask penalty rule 3 and returns the penalty.
+     *
+     * Finds consecutive cells of 00001011101 or 10111010000, and gives penalty
+     * to them. If we find patterns like 000010111010000, we give penalties
+     * twice (i.e. 40 * 2).
+     */
+    public static function applyMaskPenaltyRule3(ByteMatrix $matrix) : int
+    {
+        $penalty = 0;
+        $array = $matrix->getArray();
+        $width = $matrix->getWidth();
+        $height = $matrix->getHeight();
+
+        for ($y = 0; $y < $height; ++$y) {
+            for ($x = 0; $x < $width; ++$x) {
+                if ($x + 6 < $width
+                    && 1 === $array[$y][$x]
+                    && 0 === $array[$y][$x + 1]
+                    && 1 === $array[$y][$x + 2]
+                    && 1 === $array[$y][$x + 3]
+                    && 1 === $array[$y][$x + 4]
+                    && 0 === $array[$y][$x + 5]
+                    && 1 === $array[$y][$x + 6]
+                    && (
+                        (
+                            $x + 10 < $width
+                            && 0 === $array[$y][$x + 7]
+                            && 0 === $array[$y][$x + 8]
+                            && 0 === $array[$y][$x + 9]
+                            && 0 === $array[$y][$x + 10]
+                        )
+                        || (
+                            $x - 4 >= 0
+                            && 0 === $array[$y][$x - 1]
+                            && 0 === $array[$y][$x - 2]
+                            && 0 === $array[$y][$x - 3]
+                            && 0 === $array[$y][$x - 4]
+                        )
+                    )
+                ) {
+                    $penalty += self::N3;
+                }
+
+                if ($y + 6 < $height
+                    && 1 === $array[$y][$x]
+                    && 0 === $array[$y + 1][$x]
+                    && 1 === $array[$y + 2][$x]
+                    && 1 === $array[$y + 3][$x]
+                    && 1 === $array[$y + 4][$x]
+                    && 0 === $array[$y + 5][$x]
+                    && 1 === $array[$y + 6][$x]
+                    && (
+                        (
+                            $y + 10 < $height
+                            && 0 === $array[$y + 7][$x]
+                            && 0 === $array[$y + 8][$x]
+                            && 0 === $array[$y + 9][$x]
+                            && 0 === $array[$y + 10][$x]
+                        )
+                        || (
+                            $y - 4 >= 0
+                            && 0 === $array[$y - 1][$x]
+                            && 0 === $array[$y - 2][$x]
+                            && 0 === $array[$y - 3][$x]
+                            && 0 === $array[$y - 4][$x]
+                        )
+                    )
+                ) {
+                    $penalty += self::N3;
+                }
+            }
+        }
+
+        return $penalty;
+    }
+
+    /**
+     * Applies mask penalty rule 4 and returns the penalty.
+     *
+     * Calculates the ratio of dark cells and gives penalty if the ratio is far
+     * from 50%. It gives 10 penalty for 5% distance.
+     */
+    public static function applyMaskPenaltyRule4(ByteMatrix $matrix) : int
+    {
+        $numDarkCells = 0;
+
+        $array = $matrix->getArray();
+        $width = $matrix->getWidth();
+        $height = $matrix->getHeight();
+
+        for ($y = 0; $y < $height; ++$y) {
+            $arrayY = $array[$y];
+
+            for ($x = 0; $x < $width; ++$x) {
+                if (1 === $arrayY[$x]) {
+                    ++$numDarkCells;
+                }
+            }
+        }
+
+        $numTotalCells = $height * $width;
+        $darkRatio = $numDarkCells / $numTotalCells;
+        $fixedPercentVariances = (int) (abs($darkRatio - 0.5) * 20);
+
+        return $fixedPercentVariances * self::N4;
+    }
+
+    /**
+     * Returns the mask bit for "getMaskPattern" at "x" and "y".
+     *
+     * See 8.8 of JISX0510:2004 for mask pattern conditions.
+     *
+     * @throws InvalidArgumentException if an invalid mask pattern was supplied
+     */
+    public static function getDataMaskBit(int $maskPattern, int $x, int $y) : bool
+    {
+        switch ($maskPattern) {
+            case 0:
+                $intermediate = ($y + $x) & 0x1;
+                break;
+
+            case 1:
+                $intermediate = $y & 0x1;
+                break;
+
+            case 2:
+                $intermediate = $x % 3;
+                break;
+
+            case 3:
+                $intermediate = ($y + $x) % 3;
+                break;
+
+            case 4:
+                $intermediate = (BitUtils::unsignedRightShift($y, 1) + (int) ($x / 3)) & 0x1;
+                break;
+
+            case 5:
+                $temp = $y * $x;
+                $intermediate = ($temp & 0x1) + ($temp % 3);
+                break;
+
+            case 6:
+                $temp = $y * $x;
+                $intermediate = (($temp & 0x1) + ($temp % 3)) & 0x1;
+                break;
+
+            case 7:
+                $temp = $y * $x;
+                $intermediate = (($temp % 3) + (($y + $x) & 0x1)) & 0x1;
+                break;
+
+            default:
+                throw new InvalidArgumentException('Invalid mask pattern: ' . $maskPattern);
+        }
+
+        return 0 == $intermediate;
+    }
+
+    /**
+     * Helper function for applyMaskPenaltyRule1.
+     *
+     * We need this for doing this calculation in both vertical and horizontal
+     * orders respectively.
+     */
+    private static function applyMaskPenaltyRule1Internal(ByteMatrix $matrix, bool $isHorizontal) : int
+    {
+        $penalty = 0;
+        $iLimit = $isHorizontal ? $matrix->getHeight() : $matrix->getWidth();
+        $jLimit = $isHorizontal ? $matrix->getWidth() : $matrix->getHeight();
+        $array = $matrix->getArray();
+
+        for ($i = 0; $i < $iLimit; ++$i) {
+            $numSameBitCells = 0;
+            $prevBit = -1;
+
+            for ($j = 0; $j < $jLimit; $j++) {
+                $bit = $isHorizontal ? $array[$i][$j] : $array[$j][$i];
+
+                if ($bit === $prevBit) {
+                    ++$numSameBitCells;
+                } else {
+                    if ($numSameBitCells >= 5) {
+                        $penalty += self::N1 + ($numSameBitCells - 5);
+                    }
+
+                    $numSameBitCells = 1;
+                    $prevBit = $bit;
+                }
+            }
+
+            if ($numSameBitCells >= 5) {
+                $penalty += self::N1 + ($numSameBitCells - 5);
+            }
+        }
+
+        return $penalty;
+    }
+}

+ 513 - 0
htdocs/includes/bacon/bacon-qr-code/src/Encoder/MatrixUtil.php

@@ -0,0 +1,513 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Encoder;
+
+use BaconQrCode\Common\BitArray;
+use BaconQrCode\Common\ErrorCorrectionLevel;
+use BaconQrCode\Common\Version;
+use BaconQrCode\Exception\RuntimeException;
+use BaconQrCode\Exception\WriterException;
+
+/**
+ * Matrix utility.
+ */
+final class MatrixUtil
+{
+    /**
+     * Position detection pattern.
+     */
+    private const POSITION_DETECTION_PATTERN = [
+        [1, 1, 1, 1, 1, 1, 1],
+        [1, 0, 0, 0, 0, 0, 1],
+        [1, 0, 1, 1, 1, 0, 1],
+        [1, 0, 1, 1, 1, 0, 1],
+        [1, 0, 1, 1, 1, 0, 1],
+        [1, 0, 0, 0, 0, 0, 1],
+        [1, 1, 1, 1, 1, 1, 1],
+    ];
+
+    /**
+     * Position adjustment pattern.
+     */
+    private const POSITION_ADJUSTMENT_PATTERN = [
+        [1, 1, 1, 1, 1],
+        [1, 0, 0, 0, 1],
+        [1, 0, 1, 0, 1],
+        [1, 0, 0, 0, 1],
+        [1, 1, 1, 1, 1],
+    ];
+
+    /**
+     * Coordinates for position adjustment patterns for each version.
+     */
+    private const POSITION_ADJUSTMENT_PATTERN_COORDINATE_TABLE = [
+        [null, null, null, null, null, null, null], // Version 1
+        [   6,   18, null, null, null, null, null], // Version 2
+        [   6,   22, null, null, null, null, null], // Version 3
+        [   6,   26, null, null, null, null, null], // Version 4
+        [   6,   30, null, null, null, null, null], // Version 5
+        [   6,   34, null, null, null, null, null], // Version 6
+        [   6,   22,   38, null, null, null, null], // Version 7
+        [   6,   24,   42, null, null, null, null], // Version 8
+        [   6,   26,   46, null, null, null, null], // Version 9
+        [   6,   28,   50, null, null, null, null], // Version 10
+        [   6,   30,   54, null, null, null, null], // Version 11
+        [   6,   32,   58, null, null, null, null], // Version 12
+        [   6,   34,   62, null, null, null, null], // Version 13
+        [   6,   26,   46,   66, null, null, null], // Version 14
+        [   6,   26,   48,   70, null, null, null], // Version 15
+        [   6,   26,   50,   74, null, null, null], // Version 16
+        [   6,   30,   54,   78, null, null, null], // Version 17
+        [   6,   30,   56,   82, null, null, null], // Version 18
+        [   6,   30,   58,   86, null, null, null], // Version 19
+        [   6,   34,   62,   90, null, null, null], // Version 20
+        [   6,   28,   50,   72,   94, null, null], // Version 21
+        [   6,   26,   50,   74,   98, null, null], // Version 22
+        [   6,   30,   54,   78,  102, null, null], // Version 23
+        [   6,   28,   54,   80,  106, null, null], // Version 24
+        [   6,   32,   58,   84,  110, null, null], // Version 25
+        [   6,   30,   58,   86,  114, null, null], // Version 26
+        [   6,   34,   62,   90,  118, null, null], // Version 27
+        [   6,   26,   50,   74,   98,  122, null], // Version 28
+        [   6,   30,   54,   78,  102,  126, null], // Version 29
+        [   6,   26,   52,   78,  104,  130, null], // Version 30
+        [   6,   30,   56,   82,  108,  134, null], // Version 31
+        [   6,   34,   60,   86,  112,  138, null], // Version 32
+        [   6,   30,   58,   86,  114,  142, null], // Version 33
+        [   6,   34,   62,   90,  118,  146, null], // Version 34
+        [   6,   30,   54,   78,  102,  126,  150], // Version 35
+        [   6,   24,   50,   76,  102,  128,  154], // Version 36
+        [   6,   28,   54,   80,  106,  132,  158], // Version 37
+        [   6,   32,   58,   84,  110,  136,  162], // Version 38
+        [   6,   26,   54,   82,  110,  138,  166], // Version 39
+        [   6,   30,   58,   86,  114,  142,  170], // Version 40
+    ];
+
+    /**
+     * Type information coordinates.
+     */
+    private const TYPE_INFO_COORDINATES = [
+        [8, 0],
+        [8, 1],
+        [8, 2],
+        [8, 3],
+        [8, 4],
+        [8, 5],
+        [8, 7],
+        [8, 8],
+        [7, 8],
+        [5, 8],
+        [4, 8],
+        [3, 8],
+        [2, 8],
+        [1, 8],
+        [0, 8],
+    ];
+
+    /**
+     * Version information polynomial.
+     */
+    private const VERSION_INFO_POLY = 0x1f25;
+
+    /**
+     * Type information polynomial.
+     */
+    private const TYPE_INFO_POLY = 0x537;
+
+    /**
+     * Type information mask pattern.
+     */
+    private const TYPE_INFO_MASK_PATTERN = 0x5412;
+
+    /**
+     * Clears a given matrix.
+     */
+    public static function clearMatrix(ByteMatrix $matrix) : void
+    {
+        $matrix->clear(-1);
+    }
+
+    /**
+     * Builds a complete matrix.
+     */
+    public static function buildMatrix(
+        BitArray $dataBits,
+        ErrorCorrectionLevel $level,
+        Version $version,
+        int $maskPattern,
+        ByteMatrix $matrix
+    ) : void {
+        self::clearMatrix($matrix);
+        self::embedBasicPatterns($version, $matrix);
+        self::embedTypeInfo($level, $maskPattern, $matrix);
+        self::maybeEmbedVersionInfo($version, $matrix);
+        self::embedDataBits($dataBits, $maskPattern, $matrix);
+    }
+
+    /**
+     * Removes the position detection patterns from a matrix.
+     *
+     * This can be useful if you need to render those patterns separately.
+     */
+    public static function removePositionDetectionPatterns(ByteMatrix $matrix) : void
+    {
+        $pdpWidth = count(self::POSITION_DETECTION_PATTERN[0]);
+
+        self::removePositionDetectionPattern(0, 0, $matrix);
+        self::removePositionDetectionPattern($matrix->getWidth() - $pdpWidth, 0, $matrix);
+        self::removePositionDetectionPattern(0, $matrix->getWidth() - $pdpWidth, $matrix);
+    }
+
+    /**
+     * Embeds type information into a matrix.
+     */
+    private static function embedTypeInfo(ErrorCorrectionLevel $level, int $maskPattern, ByteMatrix $matrix) : void
+    {
+        $typeInfoBits = new BitArray();
+        self::makeTypeInfoBits($level, $maskPattern, $typeInfoBits);
+
+        $typeInfoBitsSize = $typeInfoBits->getSize();
+
+        for ($i = 0; $i < $typeInfoBitsSize; ++$i) {
+            $bit = $typeInfoBits->get($typeInfoBitsSize - 1 - $i);
+
+            $x1 = self::TYPE_INFO_COORDINATES[$i][0];
+            $y1 = self::TYPE_INFO_COORDINATES[$i][1];
+
+            $matrix->set($x1, $y1, (int) $bit);
+
+            if ($i < 8) {
+                $x2 = $matrix->getWidth() - $i - 1;
+                $y2 = 8;
+            } else {
+                $x2 = 8;
+                $y2 = $matrix->getHeight() - 7 + ($i - 8);
+            }
+
+            $matrix->set($x2, $y2, (int) $bit);
+        }
+    }
+
+    /**
+     * Generates type information bits and appends them to a bit array.
+     *
+     * @throws RuntimeException if bit array resulted in invalid size
+     */
+    private static function makeTypeInfoBits(ErrorCorrectionLevel $level, int $maskPattern, BitArray $bits) : void
+    {
+        $typeInfo = ($level->getBits() << 3) | $maskPattern;
+        $bits->appendBits($typeInfo, 5);
+
+        $bchCode = self::calculateBchCode($typeInfo, self::TYPE_INFO_POLY);
+        $bits->appendBits($bchCode, 10);
+
+        $maskBits = new BitArray();
+        $maskBits->appendBits(self::TYPE_INFO_MASK_PATTERN, 15);
+        $bits->xorBits($maskBits);
+
+        if (15 !== $bits->getSize()) {
+            throw new RuntimeException('Bit array resulted in invalid size: ' . $bits->getSize());
+        }
+    }
+
+    /**
+     * Embeds version information if required.
+     */
+    private static function maybeEmbedVersionInfo(Version $version, ByteMatrix $matrix) : void
+    {
+        if ($version->getVersionNumber() < 7) {
+            return;
+        }
+
+        $versionInfoBits = new BitArray();
+        self::makeVersionInfoBits($version, $versionInfoBits);
+
+        $bitIndex = 6 * 3 - 1;
+
+        for ($i = 0; $i < 6; ++$i) {
+            for ($j = 0; $j < 3; ++$j) {
+                $bit = $versionInfoBits->get($bitIndex);
+                --$bitIndex;
+
+                $matrix->set($i, $matrix->getHeight() - 11 + $j, (int) $bit);
+                $matrix->set($matrix->getHeight() - 11 + $j, $i, (int) $bit);
+            }
+        }
+    }
+
+    /**
+     * Generates version information bits and appends them to a bit array.
+     *
+     * @throws RuntimeException if bit array resulted in invalid size
+     */
+    private static function makeVersionInfoBits(Version $version, BitArray $bits) : void
+    {
+        $bits->appendBits($version->getVersionNumber(), 6);
+
+        $bchCode = self::calculateBchCode($version->getVersionNumber(), self::VERSION_INFO_POLY);
+        $bits->appendBits($bchCode, 12);
+
+        if (18 !== $bits->getSize()) {
+            throw new RuntimeException('Bit array resulted in invalid size: ' . $bits->getSize());
+        }
+    }
+
+    /**
+     * Calculates the BCH code for a value and a polynomial.
+     */
+    private static function calculateBchCode(int $value, int $poly) : int
+    {
+        $msbSetInPoly = self::findMsbSet($poly);
+        $value <<= $msbSetInPoly - 1;
+
+        while (self::findMsbSet($value) >= $msbSetInPoly) {
+            $value ^= $poly << (self::findMsbSet($value) - $msbSetInPoly);
+        }
+
+        return $value;
+    }
+
+    /**
+     * Finds and MSB set.
+     */
+    private static function findMsbSet(int $value) : int
+    {
+        $numDigits = 0;
+
+        while (0 !== $value) {
+            $value >>= 1;
+            ++$numDigits;
+        }
+
+        return $numDigits;
+    }
+
+    /**
+     * Embeds basic patterns into a matrix.
+     */
+    private static function embedBasicPatterns(Version $version, ByteMatrix $matrix) : void
+    {
+        self::embedPositionDetectionPatternsAndSeparators($matrix);
+        self::embedDarkDotAtLeftBottomCorner($matrix);
+        self::maybeEmbedPositionAdjustmentPatterns($version, $matrix);
+        self::embedTimingPatterns($matrix);
+    }
+
+    /**
+     * Embeds position detection patterns and separators into a byte matrix.
+     */
+    private static function embedPositionDetectionPatternsAndSeparators(ByteMatrix $matrix) : void
+    {
+        $pdpWidth = count(self::POSITION_DETECTION_PATTERN[0]);
+
+        self::embedPositionDetectionPattern(0, 0, $matrix);
+        self::embedPositionDetectionPattern($matrix->getWidth() - $pdpWidth, 0, $matrix);
+        self::embedPositionDetectionPattern(0, $matrix->getWidth() - $pdpWidth, $matrix);
+
+        $hspWidth = 8;
+
+        self::embedHorizontalSeparationPattern(0, $hspWidth - 1, $matrix);
+        self::embedHorizontalSeparationPattern($matrix->getWidth() - $hspWidth, $hspWidth - 1, $matrix);
+        self::embedHorizontalSeparationPattern(0, $matrix->getWidth() - $hspWidth, $matrix);
+
+        $vspSize = 7;
+
+        self::embedVerticalSeparationPattern($vspSize, 0, $matrix);
+        self::embedVerticalSeparationPattern($matrix->getHeight() - $vspSize - 1, 0, $matrix);
+        self::embedVerticalSeparationPattern($vspSize, $matrix->getHeight() - $vspSize, $matrix);
+    }
+
+    /**
+     * Embeds a single position detection pattern into a byte matrix.
+     */
+    private static function embedPositionDetectionPattern(int $xStart, int $yStart, ByteMatrix $matrix) : void
+    {
+        for ($y = 0; $y < 7; ++$y) {
+            for ($x = 0; $x < 7; ++$x) {
+                $matrix->set($xStart + $x, $yStart + $y, self::POSITION_DETECTION_PATTERN[$y][$x]);
+            }
+        }
+    }
+
+    private static function removePositionDetectionPattern(int $xStart, int $yStart, ByteMatrix $matrix) : void
+    {
+        for ($y = 0; $y < 7; ++$y) {
+            for ($x = 0; $x < 7; ++$x) {
+                $matrix->set($xStart + $x, $yStart + $y, 0);
+            }
+        }
+    }
+
+    /**
+     * Embeds a single horizontal separation pattern.
+     *
+     * @throws RuntimeException if a byte was already set
+     */
+    private static function embedHorizontalSeparationPattern(int $xStart, int $yStart, ByteMatrix $matrix) : void
+    {
+        for ($x = 0; $x < 8; $x++) {
+            if (-1 !== $matrix->get($xStart + $x, $yStart)) {
+                throw new RuntimeException('Byte already set');
+            }
+
+            $matrix->set($xStart + $x, $yStart, 0);
+        }
+    }
+
+    /**
+     * Embeds a single vertical separation pattern.
+     *
+     * @throws RuntimeException if a byte was already set
+     */
+    private static function embedVerticalSeparationPattern(int $xStart, int $yStart, ByteMatrix $matrix) : void
+    {
+        for ($y = 0; $y < 7; $y++) {
+            if (-1 !== $matrix->get($xStart, $yStart + $y)) {
+                throw new RuntimeException('Byte already set');
+            }
+
+            $matrix->set($xStart, $yStart + $y, 0);
+        }
+    }
+
+    /**
+     * Embeds a dot at the left bottom corner.
+     *
+     * @throws RuntimeException if a byte was already set to 0
+     */
+    private static function embedDarkDotAtLeftBottomCorner(ByteMatrix $matrix) : void
+    {
+        if (0 === $matrix->get(8, $matrix->getHeight() - 8)) {
+            throw new RuntimeException('Byte already set to 0');
+        }
+
+        $matrix->set(8, $matrix->getHeight() - 8, 1);
+    }
+
+    /**
+     * Embeds position adjustment patterns if required.
+     */
+    private static function maybeEmbedPositionAdjustmentPatterns(Version $version, ByteMatrix $matrix) : void
+    {
+        if ($version->getVersionNumber() < 2) {
+            return;
+        }
+
+        $index = $version->getVersionNumber() - 1;
+
+        $coordinates = self::POSITION_ADJUSTMENT_PATTERN_COORDINATE_TABLE[$index];
+        $numCoordinates = count($coordinates);
+
+        for ($i = 0; $i < $numCoordinates; ++$i) {
+            for ($j = 0; $j < $numCoordinates; ++$j) {
+                $y = $coordinates[$i];
+                $x = $coordinates[$j];
+
+                if (null === $x || null === $y) {
+                    continue;
+                }
+
+                if (-1 === $matrix->get($x, $y)) {
+                    self::embedPositionAdjustmentPattern($x - 2, $y - 2, $matrix);
+                }
+            }
+        }
+    }
+
+    /**
+     * Embeds a single position adjustment pattern.
+     */
+    private static function embedPositionAdjustmentPattern(int $xStart, int $yStart, ByteMatrix $matrix) : void
+    {
+        for ($y = 0; $y < 5; $y++) {
+            for ($x = 0; $x < 5; $x++) {
+                $matrix->set($xStart + $x, $yStart + $y, self::POSITION_ADJUSTMENT_PATTERN[$y][$x]);
+            }
+        }
+    }
+
+    /**
+     * Embeds timing patterns into a matrix.
+     */
+    private static function embedTimingPatterns(ByteMatrix $matrix) : void
+    {
+        $matrixWidth = $matrix->getWidth();
+
+        for ($i = 8; $i < $matrixWidth - 8; ++$i) {
+            $bit = ($i + 1) % 2;
+
+            if (-1 === $matrix->get($i, 6)) {
+                $matrix->set($i, 6, $bit);
+            }
+
+            if (-1 === $matrix->get(6, $i)) {
+                $matrix->set(6, $i, $bit);
+            }
+        }
+    }
+
+    /**
+     * Embeds "dataBits" using "getMaskPattern".
+     *
+     * For debugging purposes, it skips masking process if "getMaskPattern" is -1. See 8.7 of JISX0510:2004 (p.38) for
+     * how to embed data bits.
+     *
+     * @throws WriterException if not all bits could be consumed
+     */
+    private static function embedDataBits(BitArray $dataBits, int $maskPattern, ByteMatrix $matrix) : void
+    {
+        $bitIndex = 0;
+        $direction = -1;
+
+        // Start from the right bottom cell.
+        $x = $matrix->getWidth() - 1;
+        $y = $matrix->getHeight() - 1;
+
+        while ($x > 0) {
+            // Skip vertical timing pattern.
+            if (6 === $x) {
+                --$x;
+            }
+
+            while ($y >= 0 && $y < $matrix->getHeight()) {
+                for ($i = 0; $i < 2; $i++) {
+                    $xx = $x - $i;
+
+                    // Skip the cell if it's not empty.
+                    if (-1 !== $matrix->get($xx, $y)) {
+                        continue;
+                    }
+
+                    if ($bitIndex < $dataBits->getSize()) {
+                        $bit = $dataBits->get($bitIndex);
+                        ++$bitIndex;
+                    } else {
+                        // Padding bit. If there is no bit left, we'll fill the
+                        // left cells with 0, as described in 8.4.9 of
+                        // JISX0510:2004 (p. 24).
+                        $bit = false;
+                    }
+
+                    // Skip masking if maskPattern is -1.
+                    if (-1 !== $maskPattern && MaskUtil::getDataMaskBit($maskPattern, $xx, $y)) {
+                        $bit = ! $bit;
+                    }
+
+                    $matrix->set($xx, $y, (int) $bit);
+                }
+
+                $y += $direction;
+            }
+
+            $direction  = -$direction;
+            $y += $direction;
+            $x -= 2;
+        }
+
+        // All bits should be consumed
+        if ($dataBits->getSize() !== $bitIndex) {
+            throw new WriterException('Not all bits consumed (' . $bitIndex . ' out of ' . $dataBits->getSize() .')');
+        }
+    }
+}

+ 141 - 0
htdocs/includes/bacon/bacon-qr-code/src/Encoder/QrCode.php

@@ -0,0 +1,141 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Encoder;
+
+use BaconQrCode\Common\ErrorCorrectionLevel;
+use BaconQrCode\Common\Mode;
+use BaconQrCode\Common\Version;
+
+/**
+ * QR code.
+ */
+final class QrCode
+{
+    /**
+     * Number of possible mask patterns.
+     */
+    public const NUM_MASK_PATTERNS = 8;
+
+    /**
+     * Mode of the QR code.
+     *
+     * @var Mode
+     */
+    private $mode;
+
+    /**
+     * EC level of the QR code.
+     *
+     * @var ErrorCorrectionLevel
+     */
+    private $errorCorrectionLevel;
+
+    /**
+     * Version of the QR code.
+     *
+     * @var Version
+     */
+    private $version;
+
+    /**
+     * Mask pattern of the QR code.
+     *
+     * @var int
+     */
+    private $maskPattern = -1;
+
+    /**
+     * Matrix of the QR code.
+     *
+     * @var ByteMatrix
+     */
+    private $matrix;
+
+    public function __construct(
+        Mode $mode,
+        ErrorCorrectionLevel $errorCorrectionLevel,
+        Version $version,
+        int $maskPattern,
+        ByteMatrix $matrix
+    ) {
+        $this->mode = $mode;
+        $this->errorCorrectionLevel = $errorCorrectionLevel;
+        $this->version = $version;
+        $this->maskPattern = $maskPattern;
+        $this->matrix = $matrix;
+    }
+
+    /**
+     * Gets the mode.
+     */
+    public function getMode() : Mode
+    {
+        return $this->mode;
+    }
+
+    /**
+     * Gets the EC level.
+     */
+    public function getErrorCorrectionLevel() : ErrorCorrectionLevel
+    {
+        return $this->errorCorrectionLevel;
+    }
+
+    /**
+     * Gets the version.
+     */
+    public function getVersion() : Version
+    {
+        return $this->version;
+    }
+
+    /**
+     * Gets the mask pattern.
+     */
+    public function getMaskPattern() : int
+    {
+        return $this->maskPattern;
+    }
+
+    /**
+     * Gets the matrix.
+     *
+     * @return ByteMatrix
+     */
+    public function getMatrix()
+    {
+        return $this->matrix;
+    }
+
+    /**
+     * Validates whether a mask pattern is valid.
+     */
+    public static function isValidMaskPattern(int $maskPattern) : bool
+    {
+        return $maskPattern > 0 && $maskPattern < self::NUM_MASK_PATTERNS;
+    }
+
+    /**
+     * Returns a string representation of the QR code.
+     */
+    public function __toString() : string
+    {
+        $result = "<<\n"
+                . ' mode: ' . $this->mode . "\n"
+                . ' ecLevel: ' . $this->errorCorrectionLevel . "\n"
+                . ' version: ' . $this->version . "\n"
+                . ' maskPattern: ' . $this->maskPattern . "\n";
+
+        if ($this->matrix === null) {
+            $result .= " matrix: null\n";
+        } else {
+            $result .= " matrix:\n";
+            $result .= $this->matrix;
+        }
+
+        $result .= ">>\n";
+
+        return $result;
+    }
+}

+ 10 - 0
htdocs/includes/bacon/bacon-qr-code/src/Exception/ExceptionInterface.php

@@ -0,0 +1,10 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Exception;
+
+use Throwable;
+
+interface ExceptionInterface extends Throwable
+{
+}

+ 8 - 0
htdocs/includes/bacon/bacon-qr-code/src/Exception/InvalidArgumentException.php

@@ -0,0 +1,8 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Exception;
+
+final class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
+{
+}

+ 8 - 0
htdocs/includes/bacon/bacon-qr-code/src/Exception/OutOfBoundsException.php

@@ -0,0 +1,8 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Exception;
+
+final class OutOfBoundsException extends \OutOfBoundsException implements ExceptionInterface
+{
+}

+ 8 - 0
htdocs/includes/bacon/bacon-qr-code/src/Exception/RuntimeException.php

@@ -0,0 +1,8 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Exception;
+
+final class RuntimeException extends \RuntimeException implements ExceptionInterface
+{
+}

+ 8 - 0
htdocs/includes/bacon/bacon-qr-code/src/Exception/UnexpectedValueException.php

@@ -0,0 +1,8 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Exception;
+
+final class UnexpectedValueException extends \UnexpectedValueException implements ExceptionInterface
+{
+}

+ 8 - 0
htdocs/includes/bacon/bacon-qr-code/src/Exception/WriterException.php

@@ -0,0 +1,8 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Exception;
+
+final class WriterException extends \RuntimeException implements ExceptionInterface
+{
+}

+ 57 - 0
htdocs/includes/bacon/bacon-qr-code/src/Renderer/Color/Alpha.php

@@ -0,0 +1,57 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Renderer\Color;
+
+use BaconQrCode\Exception;
+
+final class Alpha implements ColorInterface
+{
+    /**
+     * @var int
+     */
+    private $alpha;
+
+    /**
+     * @var ColorInterface
+     */
+    private $baseColor;
+
+    /**
+     * @param int $alpha the alpha value, 0 to 100
+     */
+    public function __construct(int $alpha, ColorInterface $baseColor)
+    {
+        if ($alpha < 0 || $alpha > 100) {
+            throw new Exception\InvalidArgumentException('Alpha must be between 0 and 100');
+        }
+
+        $this->alpha = $alpha;
+        $this->baseColor = $baseColor;
+    }
+
+    public function getAlpha() : int
+    {
+        return $this->alpha;
+    }
+
+    public function getBaseColor() : ColorInterface
+    {
+        return $this->baseColor;
+    }
+
+    public function toRgb() : Rgb
+    {
+        return $this->baseColor->toRgb();
+    }
+
+    public function toCmyk() : Cmyk
+    {
+        return $this->baseColor->toCmyk();
+    }
+
+    public function toGray() : Gray
+    {
+        return $this->baseColor->toGray();
+    }
+}

+ 103 - 0
htdocs/includes/bacon/bacon-qr-code/src/Renderer/Color/Cmyk.php

@@ -0,0 +1,103 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Renderer\Color;
+
+use BaconQrCode\Exception;
+
+final class Cmyk implements ColorInterface
+{
+    /**
+     * @var int
+     */
+    private $cyan;
+
+    /**
+     * @var int
+     */
+    private $magenta;
+
+    /**
+     * @var int
+     */
+    private $yellow;
+
+    /**
+     * @var int
+     */
+    private $black;
+
+    /**
+     * @param int $cyan the cyan amount, 0 to 100
+     * @param int $magenta the magenta amount, 0 to 100
+     * @param int $yellow the yellow amount, 0 to 100
+     * @param int $black the black amount, 0 to 100
+     */
+    public function __construct(int $cyan, int $magenta, int $yellow, int $black)
+    {
+        if ($cyan < 0 || $cyan > 100) {
+            throw new Exception\InvalidArgumentException('Cyan must be between 0 and 100');
+        }
+
+        if ($magenta < 0 || $magenta > 100) {
+            throw new Exception\InvalidArgumentException('Magenta must be between 0 and 100');
+        }
+
+        if ($yellow < 0 || $yellow > 100) {
+            throw new Exception\InvalidArgumentException('Yellow must be between 0 and 100');
+        }
+
+        if ($black < 0 || $black > 100) {
+            throw new Exception\InvalidArgumentException('Black must be between 0 and 100');
+        }
+
+        $this->cyan = $cyan;
+        $this->magenta = $magenta;
+        $this->yellow = $yellow;
+        $this->black = $black;
+    }
+
+    public function getCyan() : int
+    {
+        return $this->cyan;
+    }
+
+    public function getMagenta() : int
+    {
+        return $this->magenta;
+    }
+
+    public function getYellow() : int
+    {
+        return $this->yellow;
+    }
+
+    public function getBlack() : int
+    {
+        return $this->black;
+    }
+
+    public function toRgb() : Rgb
+    {
+        $k = $this->black / 100;
+        $c = (-$k * $this->cyan + $k * 100 + $this->cyan) / 100;
+        $m = (-$k * $this->magenta + $k * 100 + $this->magenta) / 100;
+        $y = (-$k * $this->yellow + $k * 100 + $this->yellow) / 100;
+
+        return new Rgb(
+            (int) (-$c * 255 + 255),
+            (int) (-$m * 255 + 255),
+            (int) (-$y * 255 + 255)
+        );
+    }
+
+    public function toCmyk() : Cmyk
+    {
+        return $this;
+    }
+
+    public function toGray() : Gray
+    {
+        return $this->toRgb()->toGray();
+    }
+}

+ 22 - 0
htdocs/includes/bacon/bacon-qr-code/src/Renderer/Color/ColorInterface.php

@@ -0,0 +1,22 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Renderer\Color;
+
+interface ColorInterface
+{
+    /**
+     * Converts the color to RGB.
+     */
+    public function toRgb() : Rgb;
+
+    /**
+     * Converts the color to CMYK.
+     */
+    public function toCmyk() : Cmyk;
+
+    /**
+     * Converts the color to gray.
+     */
+    public function toGray() : Gray;
+}

+ 46 - 0
htdocs/includes/bacon/bacon-qr-code/src/Renderer/Color/Gray.php

@@ -0,0 +1,46 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Renderer\Color;
+
+use BaconQrCode\Exception;
+
+final class Gray implements ColorInterface
+{
+    /**
+     * @var int
+     */
+    private $gray;
+
+    /**
+     * @param int $gray the gray value between 0 (black) and 100 (white)
+     */
+    public function __construct(int $gray)
+    {
+        if ($gray < 0 || $gray > 100) {
+            throw new Exception\InvalidArgumentException('Gray must be between 0 and 100');
+        }
+
+        $this->gray = (int) $gray;
+    }
+
+    public function getGray() : int
+    {
+        return $this->gray;
+    }
+
+    public function toRgb() : Rgb
+    {
+        return new Rgb((int) ($this->gray * 2.55), (int) ($this->gray * 2.55), (int) ($this->gray * 2.55));
+    }
+
+    public function toCmyk() : Cmyk
+    {
+        return new Cmyk(0, 0, 0, 100 - $this->gray);
+    }
+
+    public function toGray() : Gray
+    {
+        return $this;
+    }
+}

+ 88 - 0
htdocs/includes/bacon/bacon-qr-code/src/Renderer/Color/Rgb.php

@@ -0,0 +1,88 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Renderer\Color;
+
+use BaconQrCode\Exception;
+
+final class Rgb implements ColorInterface
+{
+    /**
+     * @var int
+     */
+    private $red;
+
+    /**
+     * @var int
+     */
+    private $green;
+
+    /**
+     * @var int
+     */
+    private $blue;
+
+    /**
+     * @param int $red the red amount of the color, 0 to 255
+     * @param int $green the green amount of the color, 0 to 255
+     * @param int $blue the blue amount of the color, 0 to 255
+     */
+    public function __construct(int $red, int $green, int $blue)
+    {
+        if ($red < 0 || $red > 255) {
+            throw new Exception\InvalidArgumentException('Red must be between 0 and 255');
+        }
+
+        if ($green < 0 || $green > 255) {
+            throw new Exception\InvalidArgumentException('Green must be between 0 and 255');
+        }
+
+        if ($blue < 0 || $blue > 255) {
+            throw new Exception\InvalidArgumentException('Blue must be between 0 and 255');
+        }
+
+        $this->red = $red;
+        $this->green = $green;
+        $this->blue = $blue;
+    }
+
+    public function getRed() : int
+    {
+        return $this->red;
+    }
+
+    public function getGreen() : int
+    {
+        return $this->green;
+    }
+
+    public function getBlue() : int
+    {
+        return $this->blue;
+    }
+
+    public function toRgb() : Rgb
+    {
+        return $this;
+    }
+
+    public function toCmyk() : Cmyk
+    {
+        $c = 1 - ($this->red / 255);
+        $m = 1 - ($this->green / 255);
+        $y = 1 - ($this->blue / 255);
+        $k = min($c, $m, $y);
+
+        return new Cmyk(
+            (int) (100 * ($c - $k) / (1 - $k)),
+            (int) (100 * ($m - $k) / (1 - $k)),
+            (int) (100 * ($y - $k) / (1 - $k)),
+            (int) (100 * $k)
+        );
+    }
+
+    public function toGray() : Gray
+    {
+        return new Gray((int) (($this->red * 0.21 + $this->green * 0.71 + $this->blue * 0.07) / 2.55));
+    }
+}

+ 38 - 0
htdocs/includes/bacon/bacon-qr-code/src/Renderer/Eye/CompositeEye.php

@@ -0,0 +1,38 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Renderer\Eye;
+
+use BaconQrCode\Renderer\Path\Path;
+
+/**
+ * Combines the style of two different eyes.
+ */
+final class CompositeEye implements EyeInterface
+{
+    /**
+     * @var EyeInterface
+     */
+    private $externalEye;
+
+    /**
+     * @var EyeInterface
+     */
+    private $internalEye;
+
+    public function __construct(EyeInterface $externalEye, EyeInterface $internalEye)
+    {
+        $this->externalEye = $externalEye;
+        $this->internalEye = $internalEye;
+    }
+
+    public function getExternalPath() : Path
+    {
+        return $this->externalEye->getExternalPath();
+    }
+
+    public function getInternalPath() : Path
+    {
+        return $this->internalEye->getInternalPath();
+    }
+}

+ 26 - 0
htdocs/includes/bacon/bacon-qr-code/src/Renderer/Eye/EyeInterface.php

@@ -0,0 +1,26 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Renderer\Eye;
+
+use BaconQrCode\Renderer\Path\Path;
+
+/**
+ * Interface for describing the look of an eye.
+ */
+interface EyeInterface
+{
+    /**
+     * Returns the path of the external eye element.
+     *
+     * The path origin point (0, 0) must be anchored at the middle of the path.
+     */
+    public function getExternalPath() : Path;
+
+    /**
+     * Returns the path of the internal eye element.
+     *
+     * The path origin point (0, 0) must be anchored at the middle of the path.
+     */
+    public function getInternalPath() : Path;
+}

+ 54 - 0
htdocs/includes/bacon/bacon-qr-code/src/Renderer/Eye/ModuleEye.php

@@ -0,0 +1,54 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Renderer\Eye;
+
+use BaconQrCode\Encoder\ByteMatrix;
+use BaconQrCode\Renderer\Module\ModuleInterface;
+use BaconQrCode\Renderer\Path\Path;
+
+/**
+ * Renders an eye based on a module renderer.
+ */
+final class ModuleEye implements EyeInterface
+{
+    /**
+     * @var ModuleInterface
+     */
+    private $module;
+
+    public function __construct(ModuleInterface $module)
+    {
+        $this->module = $module;
+    }
+
+    public function getExternalPath() : Path
+    {
+        $matrix = new ByteMatrix(7, 7);
+
+        for ($x = 0; $x < 7; ++$x) {
+            $matrix->set($x, 0, 1);
+            $matrix->set($x, 6, 1);
+        }
+
+        for ($y = 1; $y < 6; ++$y) {
+            $matrix->set(0, $y, 1);
+            $matrix->set(6, $y, 1);
+        }
+
+        return $this->module->createPath($matrix)->translate(-3.5, -3.5);
+    }
+
+    public function getInternalPath() : Path
+    {
+        $matrix = new ByteMatrix(3, 3);
+
+        for ($x = 0; $x < 3; ++$x) {
+            for ($y = 0; $y < 3; ++$y) {
+                $matrix->set($x, $y, 1);
+            }
+        }
+
+        return $this->module->createPath($matrix)->translate(-1.5, -1.5);
+    }
+}

+ 54 - 0
htdocs/includes/bacon/bacon-qr-code/src/Renderer/Eye/SimpleCircleEye.php

@@ -0,0 +1,54 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Renderer\Eye;
+
+use BaconQrCode\Renderer\Path\Path;
+
+/**
+ * Renders the inner eye as a circle.
+ */
+final class SimpleCircleEye implements EyeInterface
+{
+    /**
+     * @var self|null
+     */
+    private static $instance;
+
+    private function __construct()
+    {
+    }
+
+    public static function instance() : self
+    {
+        return self::$instance ?: self::$instance = new self();
+    }
+
+    public function getExternalPath() : Path
+    {
+        return (new Path())
+            ->move(-3.5, -3.5)
+            ->line(3.5, -3.5)
+            ->line(3.5, 3.5)
+            ->line(-3.5, 3.5)
+            ->close()
+            ->move(-2.5, -2.5)
+            ->line(-2.5, 2.5)
+            ->line(2.5, 2.5)
+            ->line(2.5, -2.5)
+            ->close()
+        ;
+    }
+
+    public function getInternalPath() : Path
+    {
+        return (new Path())
+            ->move(1.5, 0)
+            ->ellipticArc(1.5, 1.5, 0., false, true, 0., 1.5)
+            ->ellipticArc(1.5, 1.5, 0., false, true, -1.5, 0.)
+            ->ellipticArc(1.5, 1.5, 0., false, true, 0., -1.5)
+            ->ellipticArc(1.5, 1.5, 0., false, true, 1.5, 0.)
+            ->close()
+        ;
+    }
+}

+ 53 - 0
htdocs/includes/bacon/bacon-qr-code/src/Renderer/Eye/SquareEye.php

@@ -0,0 +1,53 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Renderer\Eye;
+
+use BaconQrCode\Renderer\Path\Path;
+
+/**
+ * Renders the eyes in their default square shape.
+ */
+final class SquareEye implements EyeInterface
+{
+    /**
+     * @var self|null
+     */
+    private static $instance;
+
+    private function __construct()
+    {
+    }
+
+    public static function instance() : self
+    {
+        return self::$instance ?: self::$instance = new self();
+    }
+
+    public function getExternalPath() : Path
+    {
+        return (new Path())
+            ->move(-3.5, -3.5)
+            ->line(3.5, -3.5)
+            ->line(3.5, 3.5)
+            ->line(-3.5, 3.5)
+            ->close()
+            ->move(-2.5, -2.5)
+            ->line(-2.5, 2.5)
+            ->line(2.5, 2.5)
+            ->line(2.5, -2.5)
+            ->close()
+        ;
+    }
+
+    public function getInternalPath() : Path
+    {
+        return (new Path())
+            ->move(-1.5, -1.5)
+            ->line(1.5, -1.5)
+            ->line(1.5, 1.5)
+            ->line(-1.5, 1.5)
+            ->close()
+        ;
+    }
+}

+ 376 - 0
htdocs/includes/bacon/bacon-qr-code/src/Renderer/Image/EpsImageBackEnd.php

@@ -0,0 +1,376 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Renderer\Image;
+
+use BaconQrCode\Exception\RuntimeException;
+use BaconQrCode\Renderer\Color\Alpha;
+use BaconQrCode\Renderer\Color\Cmyk;
+use BaconQrCode\Renderer\Color\ColorInterface;
+use BaconQrCode\Renderer\Color\Gray;
+use BaconQrCode\Renderer\Color\Rgb;
+use BaconQrCode\Renderer\Path\Close;
+use BaconQrCode\Renderer\Path\Curve;
+use BaconQrCode\Renderer\Path\EllipticArc;
+use BaconQrCode\Renderer\Path\Line;
+use BaconQrCode\Renderer\Path\Move;
+use BaconQrCode\Renderer\Path\Path;
+use BaconQrCode\Renderer\RendererStyle\Gradient;
+use BaconQrCode\Renderer\RendererStyle\GradientType;
+
+final class EpsImageBackEnd implements ImageBackEndInterface
+{
+    private const PRECISION = 3;
+
+    /**
+     * @var string|null
+     */
+    private $eps;
+
+    public function new(int $size, ColorInterface $backgroundColor) : void
+    {
+        $this->eps = "%!PS-Adobe-3.0 EPSF-3.0\n"
+            . "%%Creator: BaconQrCode\n"
+            . sprintf("%%%%BoundingBox: 0 0 %d %d \n", $size, $size)
+            . "%%BeginProlog\n"
+            . "save\n"
+            . "50 dict begin\n"
+            . "/q { gsave } bind def\n"
+            . "/Q { grestore } bind def\n"
+            . "/s { scale } bind def\n"
+            . "/t { translate } bind def\n"
+            . "/r { rotate } bind def\n"
+            . "/n { newpath } bind def\n"
+            . "/m { moveto } bind def\n"
+            . "/l { lineto } bind def\n"
+            . "/c { curveto } bind def\n"
+            . "/z { closepath } bind def\n"
+            . "/f { eofill } bind def\n"
+            . "/rgb { setrgbcolor } bind def\n"
+            . "/cmyk { setcmykcolor } bind def\n"
+            . "/gray { setgray } bind def\n"
+            . "%%EndProlog\n"
+            . "1 -1 s\n"
+            . sprintf("0 -%d t\n", $size);
+
+        if ($backgroundColor instanceof Alpha && 0 === $backgroundColor->getAlpha()) {
+            return;
+        }
+
+        $this->eps .= wordwrap(
+            '0 0 m'
+            . sprintf(' %s 0 l', (string) $size)
+            . sprintf(' %s %s l', (string) $size, (string) $size)
+            . sprintf(' 0 %s l', (string) $size)
+            . ' z'
+            . ' ' .$this->getColorSetString($backgroundColor) . " f\n",
+            75,
+            "\n "
+        );
+    }
+
+    public function scale(float $size) : void
+    {
+        if (null === $this->eps) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        $this->eps .= sprintf("%1\$s %1\$s s\n", round($size, self::PRECISION));
+    }
+
+    public function translate(float $x, float $y) : void
+    {
+        if (null === $this->eps) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        $this->eps .= sprintf("%s %s t\n", round($x, self::PRECISION), round($y, self::PRECISION));
+    }
+
+    public function rotate(int $degrees) : void
+    {
+        if (null === $this->eps) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        $this->eps .= sprintf("%d r\n", $degrees);
+    }
+
+    public function push() : void
+    {
+        if (null === $this->eps) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        $this->eps .= "q\n";
+    }
+
+    public function pop() : void
+    {
+        if (null === $this->eps) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        $this->eps .= "Q\n";
+    }
+
+    public function drawPathWithColor(Path $path, ColorInterface $color) : void
+    {
+        if (null === $this->eps) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        $fromX = 0;
+        $fromY = 0;
+        $this->eps .= wordwrap(
+            'n '
+            . $this->drawPathOperations($path, $fromX, $fromY)
+            . ' ' . $this->getColorSetString($color) . " f\n",
+            75,
+            "\n "
+        );
+    }
+
+    public function drawPathWithGradient(
+        Path $path,
+        Gradient $gradient,
+        float $x,
+        float $y,
+        float $width,
+        float $height
+    ) : void {
+        if (null === $this->eps) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        $fromX = 0;
+        $fromY = 0;
+        $this->eps .= wordwrap(
+            'q n ' . $this->drawPathOperations($path, $fromX, $fromY) . "\n",
+            75,
+            "\n "
+        );
+
+        $this->createGradientFill($gradient, $x, $y, $width, $height);
+    }
+
+    public function done() : string
+    {
+        if (null === $this->eps) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        $this->eps .= "%%TRAILER\nend restore\n%%EOF";
+        $blob = $this->eps;
+        $this->eps = null;
+
+        return $blob;
+    }
+
+    private function drawPathOperations(Iterable $ops, &$fromX, &$fromY) : string
+    {
+        $pathData = [];
+
+        foreach ($ops as $op) {
+            switch (true) {
+                case $op instanceof Move:
+                    $fromX = $toX = round($op->getX(), self::PRECISION);
+                    $fromY = $toY = round($op->getY(), self::PRECISION);
+                    $pathData[] = sprintf('%s %s m', $toX, $toY);
+                    break;
+
+                case $op instanceof Line:
+                    $fromX = $toX = round($op->getX(), self::PRECISION);
+                    $fromY = $toY = round($op->getY(), self::PRECISION);
+                    $pathData[] = sprintf('%s %s l', $toX, $toY);
+                    break;
+
+                case $op instanceof EllipticArc:
+                    $pathData[] = $this->drawPathOperations($op->toCurves($fromX, $fromY), $fromX, $fromY);
+                    break;
+
+                case $op instanceof Curve:
+                    $x1 = round($op->getX1(), self::PRECISION);
+                    $y1 = round($op->getY1(), self::PRECISION);
+                    $x2 = round($op->getX2(), self::PRECISION);
+                    $y2 = round($op->getY2(), self::PRECISION);
+                    $fromX = $x3 = round($op->getX3(), self::PRECISION);
+                    $fromY = $y3 = round($op->getY3(), self::PRECISION);
+                    $pathData[] = sprintf('%s %s %s %s %s %s c', $x1, $y1, $x2, $y2, $x3, $y3);
+                    break;
+
+                case $op instanceof Close:
+                    $pathData[] = 'z';
+                    break;
+
+                default:
+                    throw new RuntimeException('Unexpected draw operation: ' . get_class($op));
+            }
+        }
+
+        return implode(' ', $pathData);
+    }
+
+    private function createGradientFill(Gradient $gradient, float $x, float $y, float $width, float $height) : void
+    {
+        $startColor = $gradient->getStartColor();
+        $endColor = $gradient->getEndColor();
+
+        if ($startColor instanceof Alpha) {
+            $startColor = $startColor->getBaseColor();
+        }
+
+        $startColorType = get_class($startColor);
+
+        if (! in_array($startColorType, [Rgb::class, Cmyk::class, Gray::class])) {
+            $startColorType = Cmyk::class;
+            $startColor = $startColor->toCmyk();
+        }
+
+        if (get_class($endColor) !== $startColorType) {
+            switch ($startColorType) {
+                case Cmyk::class:
+                    $endColor = $endColor->toCmyk();
+                    break;
+
+                case Rgb::class:
+                    $endColor = $endColor->toRgb();
+                    break;
+
+                case Gray::class:
+                    $endColor = $endColor->toGray();
+                    break;
+            }
+        }
+
+        $this->eps .= "eoclip\n<<\n";
+
+        if ($gradient->getType() === GradientType::RADIAL()) {
+            $this->eps .= " /ShadingType 3\n";
+        } else {
+            $this->eps .= " /ShadingType 2\n";
+        }
+
+        $this->eps .= " /Extend [ true true ]\n"
+            . " /AntiAlias true\n";
+
+        switch ($startColorType) {
+            case Cmyk::class:
+                $this->eps .= " /ColorSpace /DeviceCMYK\n";
+                break;
+
+            case Rgb::class:
+                $this->eps .= " /ColorSpace /DeviceRGB\n";
+                break;
+
+            case Gray::class:
+                $this->eps .= " /ColorSpace /DeviceGray\n";
+                break;
+        }
+
+        switch ($gradient->getType()) {
+            case GradientType::HORIZONTAL():
+                $this->eps .= sprintf(
+                    " /Coords [ %s %s %s %s ]\n",
+                    round($x, self::PRECISION),
+                    round($y, self::PRECISION),
+                    round($x + $width, self::PRECISION),
+                    round($y, self::PRECISION)
+                );
+                break;
+
+            case GradientType::VERTICAL():
+                $this->eps .= sprintf(
+                    " /Coords [ %s %s %s %s ]\n",
+                    round($x, self::PRECISION),
+                    round($y, self::PRECISION),
+                    round($x, self::PRECISION),
+                    round($y + $height, self::PRECISION)
+                );
+                break;
+
+            case GradientType::DIAGONAL():
+                $this->eps .= sprintf(
+                    " /Coords [ %s %s %s %s ]\n",
+                    round($x, self::PRECISION),
+                    round($y, self::PRECISION),
+                    round($x + $width, self::PRECISION),
+                    round($y + $height, self::PRECISION)
+                );
+                break;
+
+            case GradientType::INVERSE_DIAGONAL():
+                $this->eps .= sprintf(
+                    " /Coords [ %s %s %s %s ]\n",
+                    round($x, self::PRECISION),
+                    round($y + $height, self::PRECISION),
+                    round($x + $width, self::PRECISION),
+                    round($y, self::PRECISION)
+                );
+                break;
+
+            case GradientType::RADIAL():
+                $centerX = ($x + $width) / 2;
+                $centerY = ($y + $height) / 2;
+
+                $this->eps .= sprintf(
+                    " /Coords [ %s %s 0 %s %s %s ]\n",
+                    round($centerX, self::PRECISION),
+                    round($centerY, self::PRECISION),
+                    round($centerX, self::PRECISION),
+                    round($centerY, self::PRECISION),
+                    round(max($width, $height) / 2, self::PRECISION)
+                );
+                break;
+        }
+
+        $this->eps .= " /Function\n"
+            . " <<\n"
+            . "  /FunctionType 2\n"
+            . "  /Domain [ 0 1 ]\n"
+            . sprintf("  /C0 [ %s ]\n", $this->getColorString($startColor))
+            . sprintf("  /C1 [ %s ]\n", $this->getColorString($endColor))
+            . "  /N 1\n"
+            . " >>\n>>\nshfill\nQ\n";
+    }
+
+    private function getColorSetString(ColorInterface $color) : string
+    {
+        if ($color instanceof Rgb) {
+            return $this->getColorString($color) . ' rgb';
+        }
+
+        if ($color instanceof Cmyk) {
+            return $this->getColorString($color) . ' cmyk';
+        }
+
+        if ($color instanceof Gray) {
+            return $this->getColorString($color) . ' gray';
+        }
+
+        return $this->getColorSetString($color->toCmyk());
+    }
+
+    private function getColorString(ColorInterface $color) : string
+    {
+        if ($color instanceof Rgb) {
+            return sprintf('%s %s %s', $color->getRed() / 255, $color->getGreen() / 255, $color->getBlue() / 255);
+        }
+
+        if ($color instanceof Cmyk) {
+            return sprintf(
+                '%s %s %s %s',
+                $color->getCyan() / 100,
+                $color->getMagenta() / 100,
+                $color->getYellow() / 100,
+                $color->getBlack() / 100
+            );
+        }
+
+        if ($color instanceof Gray) {
+            return sprintf('%s', $color->getGray() / 100);
+        }
+
+        return $this->getColorString($color->toCmyk());
+    }
+}

+ 87 - 0
htdocs/includes/bacon/bacon-qr-code/src/Renderer/Image/ImageBackEndInterface.php

@@ -0,0 +1,87 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Renderer\Image;
+
+use BaconQrCode\Exception\RuntimeException;
+use BaconQrCode\Renderer\Color\ColorInterface;
+use BaconQrCode\Renderer\Path\Path;
+use BaconQrCode\Renderer\RendererStyle\Gradient;
+
+/**
+ * Interface for back ends able to to produce path based images.
+ */
+interface ImageBackEndInterface
+{
+    /**
+     * Starts a new image.
+     *
+     * If a previous image was already started, previous data get erased.
+     */
+    public function new(int $size, ColorInterface $backgroundColor) : void;
+
+    /**
+     * Transforms all following drawing operation coordinates by scaling them by a given factor.
+     *
+     * @throws RuntimeException if no image was started yet.
+     */
+    public function scale(float $size) : void;
+
+    /**
+     * Transforms all following drawing operation coordinates by translating them by a given amount.
+     *
+     * @throws RuntimeException if no image was started yet.
+     */
+    public function translate(float $x, float $y) : void;
+
+    /**
+     * Transforms all following drawing operation coordinates by rotating them by a given amount.
+     *
+     * @throws RuntimeException if no image was started yet.
+     */
+    public function rotate(int $degrees) : void;
+
+    /**
+     * Pushes the current coordinate transformation onto a stack.
+     *
+     * @throws RuntimeException if no image was started yet.
+     */
+    public function push() : void;
+
+    /**
+     * Pops the last coordinate transformation from a stack.
+     *
+     * @throws RuntimeException if no image was started yet.
+     */
+    public function pop() : void;
+
+    /**
+     * Draws a path with a given color.
+     *
+     * @throws RuntimeException if no image was started yet.
+     */
+    public function drawPathWithColor(Path $path, ColorInterface $color) : void;
+
+    /**
+     * Draws a path with a given gradient which spans the box described by the position and size.
+     *
+     * @throws RuntimeException if no image was started yet.
+     */
+    public function drawPathWithGradient(
+        Path $path,
+        Gradient $gradient,
+        float $x,
+        float $y,
+        float $width,
+        float $height
+    ) : void;
+
+    /**
+     * Ends the image drawing operation and returns the resulting blob.
+     *
+     * This should reset the state of the back end and thus this method should only be callable once per image.
+     *
+     * @throws RuntimeException if no image was started yet.
+     */
+    public function done() : string;
+}

+ 336 - 0
htdocs/includes/bacon/bacon-qr-code/src/Renderer/Image/ImagickImageBackEnd.php

@@ -0,0 +1,336 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Renderer\Image;
+
+use BaconQrCode\Exception\RuntimeException;
+use BaconQrCode\Renderer\Color\Alpha;
+use BaconQrCode\Renderer\Color\Cmyk;
+use BaconQrCode\Renderer\Color\ColorInterface;
+use BaconQrCode\Renderer\Color\Gray;
+use BaconQrCode\Renderer\Color\Rgb;
+use BaconQrCode\Renderer\Path\Close;
+use BaconQrCode\Renderer\Path\Curve;
+use BaconQrCode\Renderer\Path\EllipticArc;
+use BaconQrCode\Renderer\Path\Line;
+use BaconQrCode\Renderer\Path\Move;
+use BaconQrCode\Renderer\Path\Path;
+use BaconQrCode\Renderer\RendererStyle\Gradient;
+use BaconQrCode\Renderer\RendererStyle\GradientType;
+use Imagick;
+use ImagickDraw;
+use ImagickPixel;
+
+final class ImagickImageBackEnd implements ImageBackEndInterface
+{
+    /**
+     * @var string
+     */
+    private $imageFormat;
+
+    /**
+     * @var int
+     */
+    private $compressionQuality;
+
+    /**
+     * @var Imagick|null
+     */
+    private $image;
+
+    /**
+     * @var ImagickDraw|null
+     */
+    private $draw;
+
+    /**
+     * @var int|null
+     */
+    private $gradientCount;
+
+    /**
+     * @var TransformationMatrix[]|null
+     */
+    private $matrices;
+
+    /**
+     * @var int|null
+     */
+    private $matrixIndex;
+
+    public function __construct(string $imageFormat = 'png', int $compressionQuality = 100)
+    {
+        if (! class_exists(Imagick::class)) {
+            throw new RuntimeException('You need to install the imagick extension to use this back end');
+        }
+
+        $this->imageFormat = $imageFormat;
+        $this->compressionQuality = $compressionQuality;
+    }
+
+    public function new(int $size, ColorInterface $backgroundColor) : void
+    {
+        $this->image = new Imagick();
+        $this->image->newImage($size, $size, $this->getColorPixel($backgroundColor));
+        $this->image->setImageFormat($this->imageFormat);
+        $this->image->setCompressionQuality($this->compressionQuality);
+        $this->draw = new ImagickDraw();
+        $this->gradientCount = 0;
+        $this->matrices = [new TransformationMatrix()];
+        $this->matrixIndex = 0;
+    }
+
+    public function scale(float $size) : void
+    {
+        if (null === $this->draw) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        $this->draw->scale($size, $size);
+        $this->matrices[$this->matrixIndex] = $this->matrices[$this->matrixIndex]
+            ->multiply(TransformationMatrix::scale($size));
+    }
+
+    public function translate(float $x, float $y) : void
+    {
+        if (null === $this->draw) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        $this->draw->translate($x, $y);
+        $this->matrices[$this->matrixIndex] = $this->matrices[$this->matrixIndex]
+            ->multiply(TransformationMatrix::translate($x, $y));
+    }
+
+    public function rotate(int $degrees) : void
+    {
+        if (null === $this->draw) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        $this->draw->rotate($degrees);
+        $this->matrices[$this->matrixIndex] = $this->matrices[$this->matrixIndex]
+            ->multiply(TransformationMatrix::rotate($degrees));
+    }
+
+    public function push() : void
+    {
+        if (null === $this->draw) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        $this->draw->push();
+        $this->matrices[++$this->matrixIndex] = $this->matrices[$this->matrixIndex - 1];
+    }
+
+    public function pop() : void
+    {
+        if (null === $this->draw) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        $this->draw->pop();
+        unset($this->matrices[$this->matrixIndex--]);
+    }
+
+    public function drawPathWithColor(Path $path, ColorInterface $color) : void
+    {
+        if (null === $this->draw) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        $this->draw->setFillColor($this->getColorPixel($color));
+        $this->drawPath($path);
+    }
+
+    public function drawPathWithGradient(
+        Path $path,
+        Gradient $gradient,
+        float $x,
+        float $y,
+        float $width,
+        float $height
+    ) : void {
+        if (null === $this->draw) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        $this->draw->setFillPatternURL('#' . $this->createGradientFill($gradient, $x, $y, $width, $height));
+        $this->drawPath($path);
+    }
+
+    public function done() : string
+    {
+        if (null === $this->draw) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        $this->image->drawImage($this->draw);
+        $blob = $this->image->getImageBlob();
+        $this->draw->clear();
+        $this->image->clear();
+        $this->draw = null;
+        $this->image = null;
+        $this->gradientCount = null;
+
+        return $blob;
+    }
+
+    private function drawPath(Path $path) : void
+    {
+        $this->draw->pathStart();
+
+        foreach ($path as $op) {
+            switch (true) {
+                case $op instanceof Move:
+                    $this->draw->pathMoveToAbsolute($op->getX(), $op->getY());
+                    break;
+
+                case $op instanceof Line:
+                    $this->draw->pathLineToAbsolute($op->getX(), $op->getY());
+                    break;
+
+                case $op instanceof EllipticArc:
+                    $this->draw->pathEllipticArcAbsolute(
+                        $op->getXRadius(),
+                        $op->getYRadius(),
+                        $op->getXAxisAngle(),
+                        $op->isLargeArc(),
+                        $op->isSweep(),
+                        $op->getX(),
+                        $op->getY()
+                    );
+                    break;
+
+                case $op instanceof Curve:
+                    $this->draw->pathCurveToAbsolute(
+                        $op->getX1(),
+                        $op->getY1(),
+                        $op->getX2(),
+                        $op->getY2(),
+                        $op->getX3(),
+                        $op->getY3()
+                    );
+                    break;
+
+                case $op instanceof Close:
+                    $this->draw->pathClose();
+                    break;
+
+                default:
+                    throw new RuntimeException('Unexpected draw operation: ' . get_class($op));
+            }
+        }
+
+        $this->draw->pathFinish();
+    }
+
+    private function createGradientFill(Gradient $gradient, float $x, float $y, float $width, float $height) : string
+    {
+        list($width, $height) = $this->matrices[$this->matrixIndex]->apply($width, $height);
+
+        $startColor = $this->getColorPixel($gradient->getStartColor())->getColorAsString();
+        $endColor = $this->getColorPixel($gradient->getEndColor())->getColorAsString();
+        $gradientImage = new Imagick();
+
+        switch ($gradient->getType()) {
+            case GradientType::HORIZONTAL():
+                $gradientImage->newPseudoImage((int) $height, (int) $width, sprintf(
+                    'gradient:%s-%s',
+                    $startColor,
+                    $endColor
+                ));
+                $gradientImage->rotateImage('transparent', -90);
+                break;
+
+            case GradientType::VERTICAL():
+                $gradientImage->newPseudoImage((int) $width, (int) $height, sprintf(
+                    'gradient:%s-%s',
+                    $startColor,
+                    $endColor
+                ));
+                break;
+
+            case GradientType::DIAGONAL():
+            case GradientType::INVERSE_DIAGONAL():
+                $gradientImage->newPseudoImage((int) ($width * sqrt(2)), (int) ($height * sqrt(2)), sprintf(
+                    'gradient:%s-%s',
+                    $startColor,
+                    $endColor
+                ));
+
+                if (GradientType::DIAGONAL() === $gradient->getType()) {
+                    $gradientImage->rotateImage('transparent', -45);
+                } else {
+                    $gradientImage->rotateImage('transparent', -135);
+                }
+
+                $rotatedWidth = $gradientImage->getImageWidth();
+                $rotatedHeight = $gradientImage->getImageHeight();
+
+                $gradientImage->setImagePage($rotatedWidth, $rotatedHeight, 0, 0);
+                $gradientImage->cropImage(
+                    intdiv($rotatedWidth, 2) - 2,
+                    intdiv($rotatedHeight, 2) - 2,
+                    intdiv($rotatedWidth, 4) + 1,
+                    intdiv($rotatedWidth, 4) + 1
+                );
+                break;
+
+            case GradientType::RADIAL():
+                $gradientImage->newPseudoImage((int) $width, (int) $height, sprintf(
+                    'radial-gradient:%s-%s',
+                    $startColor,
+                    $endColor
+                ));
+                break;
+        }
+
+        $id = sprintf('g%d', ++$this->gradientCount);
+        $this->draw->pushPattern($id, 0, 0, $width, $height);
+        $this->draw->composite(Imagick::COMPOSITE_COPY, 0, 0, $width, $height, $gradientImage);
+        $this->draw->popPattern();
+        return $id;
+    }
+
+    private function getColorPixel(ColorInterface $color) : ImagickPixel
+    {
+        $alpha = 100;
+
+        if ($color instanceof Alpha) {
+            $alpha = $color->getAlpha();
+            $color = $color->getBaseColor();
+        }
+
+        if ($color instanceof Rgb) {
+            return new ImagickPixel(sprintf(
+                'rgba(%d, %d, %d, %F)',
+                $color->getRed(),
+                $color->getGreen(),
+                $color->getBlue(),
+                $alpha / 100
+            ));
+        }
+
+        if ($color instanceof Cmyk) {
+            return new ImagickPixel(sprintf(
+                'cmyka(%d, %d, %d, %d, %F)',
+                $color->getCyan(),
+                $color->getMagenta(),
+                $color->getYellow(),
+                $color->getBlack(),
+                $alpha / 100
+            ));
+        }
+
+        if ($color instanceof Gray) {
+            return new ImagickPixel(sprintf(
+                'graya(%d%%, %F)',
+                $color->getGray(),
+                $alpha / 100
+            ));
+        }
+
+        return $this->getColorPixel(new Alpha($alpha, $color->toRgb()));
+    }
+}

+ 369 - 0
htdocs/includes/bacon/bacon-qr-code/src/Renderer/Image/SvgImageBackEnd.php

@@ -0,0 +1,369 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Renderer\Image;
+
+use BaconQrCode\Exception\RuntimeException;
+use BaconQrCode\Renderer\Color\Alpha;
+use BaconQrCode\Renderer\Color\ColorInterface;
+use BaconQrCode\Renderer\Path\Close;
+use BaconQrCode\Renderer\Path\Curve;
+use BaconQrCode\Renderer\Path\EllipticArc;
+use BaconQrCode\Renderer\Path\Line;
+use BaconQrCode\Renderer\Path\Move;
+use BaconQrCode\Renderer\Path\Path;
+use BaconQrCode\Renderer\RendererStyle\Gradient;
+use BaconQrCode\Renderer\RendererStyle\GradientType;
+use XMLWriter;
+
+final class SvgImageBackEnd implements ImageBackEndInterface
+{
+    private const PRECISION = 3;
+
+    /**
+     * @var XMLWriter|null
+     */
+    private $xmlWriter;
+
+    /**
+     * @var int[]|null
+     */
+    private $stack;
+
+    /**
+     * @var int|null
+     */
+    private $currentStack;
+
+    /**
+     * @var int|null
+     */
+    private $gradientCount;
+
+    public function __construct()
+    {
+        if (! class_exists(XMLWriter::class)) {
+            throw new RuntimeException('You need to install the libxml extension to use this back end');
+        }
+    }
+
+    public function new(int $size, ColorInterface $backgroundColor) : void
+    {
+        $this->xmlWriter = new XMLWriter();
+        $this->xmlWriter->openMemory();
+
+        $this->xmlWriter->startDocument('1.0', 'UTF-8');
+        $this->xmlWriter->startElement('svg');
+        $this->xmlWriter->writeAttribute('xmlns', 'http://www.w3.org/2000/svg');
+        $this->xmlWriter->writeAttribute('version', '1.1');
+        $this->xmlWriter->writeAttribute('width', (string) $size);
+        $this->xmlWriter->writeAttribute('height', (string) $size);
+        $this->xmlWriter->writeAttribute('viewBox', '0 0 '. $size . ' ' . $size);
+
+        $this->gradientCount = 0;
+        $this->currentStack = 0;
+        $this->stack[0] = 0;
+
+        $alpha = 1;
+
+        if ($backgroundColor instanceof Alpha) {
+            $alpha = $backgroundColor->getAlpha() / 100;
+        }
+
+        if (0 === $alpha) {
+            return;
+        }
+
+        $this->xmlWriter->startElement('rect');
+        $this->xmlWriter->writeAttribute('x', '0');
+        $this->xmlWriter->writeAttribute('y', '0');
+        $this->xmlWriter->writeAttribute('width', (string) $size);
+        $this->xmlWriter->writeAttribute('height', (string) $size);
+        $this->xmlWriter->writeAttribute('fill', $this->getColorString($backgroundColor));
+
+        if ($alpha < 1) {
+            $this->xmlWriter->writeAttribute('fill-opacity', (string) $alpha);
+        }
+
+        $this->xmlWriter->endElement();
+    }
+
+    public function scale(float $size) : void
+    {
+        if (null === $this->xmlWriter) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        $this->xmlWriter->startElement('g');
+        $this->xmlWriter->writeAttribute(
+            'transform',
+            sprintf('scale(%s)', round($size, self::PRECISION))
+        );
+        ++$this->stack[$this->currentStack];
+    }
+
+    public function translate(float $x, float $y) : void
+    {
+        if (null === $this->xmlWriter) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        $this->xmlWriter->startElement('g');
+        $this->xmlWriter->writeAttribute(
+            'transform',
+            sprintf('translate(%s,%s)', round($x, self::PRECISION), round($y, self::PRECISION))
+        );
+        ++$this->stack[$this->currentStack];
+    }
+
+    public function rotate(int $degrees) : void
+    {
+        if (null === $this->xmlWriter) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        $this->xmlWriter->startElement('g');
+        $this->xmlWriter->writeAttribute('transform', sprintf('rotate(%d)', $degrees));
+        ++$this->stack[$this->currentStack];
+    }
+
+    public function push() : void
+    {
+        if (null === $this->xmlWriter) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        $this->xmlWriter->startElement('g');
+        $this->stack[] = 1;
+        ++$this->currentStack;
+    }
+
+    public function pop() : void
+    {
+        if (null === $this->xmlWriter) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        for ($i = 0; $i < $this->stack[$this->currentStack]; ++$i) {
+            $this->xmlWriter->endElement();
+        }
+
+        array_pop($this->stack);
+        --$this->currentStack;
+    }
+
+    public function drawPathWithColor(Path $path, ColorInterface $color) : void
+    {
+        if (null === $this->xmlWriter) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        $alpha = 1;
+
+        if ($color instanceof Alpha) {
+            $alpha = $color->getAlpha() / 100;
+        }
+
+        $this->startPathElement($path);
+        $this->xmlWriter->writeAttribute('fill', $this->getColorString($color));
+
+        if ($alpha < 1) {
+            $this->xmlWriter->writeAttribute('fill-opacity', (string) $alpha);
+        }
+
+        $this->xmlWriter->endElement();
+    }
+
+    public function drawPathWithGradient(
+        Path $path,
+        Gradient $gradient,
+        float $x,
+        float $y,
+        float $width,
+        float $height
+    ) : void {
+        if (null === $this->xmlWriter) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        $gradientId = $this->createGradientFill($gradient, $x, $y, $width, $height);
+        $this->startPathElement($path);
+        $this->xmlWriter->writeAttribute('fill', 'url(#' . $gradientId . ')');
+        $this->xmlWriter->endElement();
+    }
+
+    public function done() : string
+    {
+        if (null === $this->xmlWriter) {
+            throw new RuntimeException('No image has been started');
+        }
+
+        foreach ($this->stack as $openElements) {
+            for ($i = $openElements; $i > 0; --$i) {
+                $this->xmlWriter->endElement();
+            }
+        }
+
+        $this->xmlWriter->endDocument();
+        $blob = $this->xmlWriter->outputMemory(true);
+        $this->xmlWriter = null;
+        $this->stack = null;
+        $this->currentStack = null;
+        $this->gradientCount = null;
+
+        return $blob;
+    }
+
+    private function startPathElement(Path $path) : void
+    {
+        $pathData = [];
+
+        foreach ($path as $op) {
+            switch (true) {
+                case $op instanceof Move:
+                    $pathData[] = sprintf(
+                        'M%s %s',
+                        round($op->getX(), self::PRECISION),
+                        round($op->getY(), self::PRECISION)
+                    );
+                    break;
+
+                case $op instanceof Line:
+                    $pathData[] = sprintf(
+                        'L%s %s',
+                        round($op->getX(), self::PRECISION),
+                        round($op->getY(), self::PRECISION)
+                    );
+                    break;
+
+                case $op instanceof EllipticArc:
+                    $pathData[] = sprintf(
+                        'A%s %s %s %u %u %s %s',
+                        round($op->getXRadius(), self::PRECISION),
+                        round($op->getYRadius(), self::PRECISION),
+                        round($op->getXAxisAngle(), self::PRECISION),
+                        $op->isLargeArc(),
+                        $op->isSweep(),
+                        round($op->getX(), self::PRECISION),
+                        round($op->getY(), self::PRECISION)
+                    );
+                    break;
+
+                case $op instanceof Curve:
+                    $pathData[] = sprintf(
+                        'C%s %s %s %s %s %s',
+                        round($op->getX1(), self::PRECISION),
+                        round($op->getY1(), self::PRECISION),
+                        round($op->getX2(), self::PRECISION),
+                        round($op->getY2(), self::PRECISION),
+                        round($op->getX3(), self::PRECISION),
+                        round($op->getY3(), self::PRECISION)
+                    );
+                    break;
+
+                case $op instanceof Close:
+                    $pathData[] = 'Z';
+                    break;
+
+                default:
+                    throw new RuntimeException('Unexpected draw operation: ' . get_class($op));
+            }
+        }
+
+        $this->xmlWriter->startElement('path');
+        $this->xmlWriter->writeAttribute('fill-rule', 'evenodd');
+        $this->xmlWriter->writeAttribute('d', implode('', $pathData));
+    }
+
+    private function createGradientFill(Gradient $gradient, float $x, float $y, float $width, float $height) : string
+    {
+        $this->xmlWriter->startElement('defs');
+
+        $startColor = $gradient->getStartColor();
+        $endColor = $gradient->getEndColor();
+
+        if ($gradient->getType() === GradientType::RADIAL()) {
+            $this->xmlWriter->startElement('radialGradient');
+        } else {
+            $this->xmlWriter->startElement('linearGradient');
+        }
+
+        $this->xmlWriter->writeAttribute('gradientUnits', 'userSpaceOnUse');
+
+        switch ($gradient->getType()) {
+            case GradientType::HORIZONTAL():
+                $this->xmlWriter->writeAttribute('x1', (string) round($x, self::PRECISION));
+                $this->xmlWriter->writeAttribute('y1', (string) round($y, self::PRECISION));
+                $this->xmlWriter->writeAttribute('x2', (string) round($x + $width, self::PRECISION));
+                $this->xmlWriter->writeAttribute('y2', (string) round($y, self::PRECISION));
+                break;
+
+            case GradientType::VERTICAL():
+                $this->xmlWriter->writeAttribute('x1', (string) round($x, self::PRECISION));
+                $this->xmlWriter->writeAttribute('y1', (string) round($y, self::PRECISION));
+                $this->xmlWriter->writeAttribute('x2', (string) round($x, self::PRECISION));
+                $this->xmlWriter->writeAttribute('y2', (string) round($y + $height, self::PRECISION));
+                break;
+
+            case GradientType::DIAGONAL():
+                $this->xmlWriter->writeAttribute('x1', (string) round($x, self::PRECISION));
+                $this->xmlWriter->writeAttribute('y1', (string) round($y, self::PRECISION));
+                $this->xmlWriter->writeAttribute('x2', (string) round($x + $width, self::PRECISION));
+                $this->xmlWriter->writeAttribute('y2', (string) round($y + $height, self::PRECISION));
+                break;
+
+            case GradientType::INVERSE_DIAGONAL():
+                $this->xmlWriter->writeAttribute('x1', (string) round($x, self::PRECISION));
+                $this->xmlWriter->writeAttribute('y1', (string) round($y + $height, self::PRECISION));
+                $this->xmlWriter->writeAttribute('x2', (string) round($x + $width, self::PRECISION));
+                $this->xmlWriter->writeAttribute('y2', (string) round($y, self::PRECISION));
+                break;
+
+            case GradientType::RADIAL():
+                $this->xmlWriter->writeAttribute('cx', (string) round(($x + $width) / 2, self::PRECISION));
+                $this->xmlWriter->writeAttribute('cy', (string) round(($y + $height) / 2, self::PRECISION));
+                $this->xmlWriter->writeAttribute('r', (string) round(max($width, $height) / 2, self::PRECISION));
+                break;
+        }
+
+        $id = sprintf('g%d', ++$this->gradientCount);
+        $this->xmlWriter->writeAttribute('id', $id);
+
+        $this->xmlWriter->startElement('stop');
+        $this->xmlWriter->writeAttribute('offset', '0%');
+        $this->xmlWriter->writeAttribute('stop-color', $this->getColorString($startColor));
+
+        if ($startColor instanceof Alpha) {
+            $this->xmlWriter->writeAttribute('stop-opacity', (string) $startColor->getAlpha());
+        }
+
+        $this->xmlWriter->endElement();
+
+        $this->xmlWriter->startElement('stop');
+        $this->xmlWriter->writeAttribute('offset', '100%');
+        $this->xmlWriter->writeAttribute('stop-color', $this->getColorString($endColor));
+
+        if ($endColor instanceof Alpha) {
+            $this->xmlWriter->writeAttribute('stop-opacity', (string) $endColor->getAlpha());
+        }
+
+        $this->xmlWriter->endElement();
+
+        $this->xmlWriter->endElement();
+        $this->xmlWriter->endElement();
+
+        return $id;
+    }
+
+    private function getColorString(ColorInterface $color) : string
+    {
+        $color = $color->toRgb();
+
+        return sprintf(
+            '#%02x%02x%02x',
+            $color->getRed(),
+            $color->getGreen(),
+            $color->getBlue()
+        );
+    }
+}

+ 68 - 0
htdocs/includes/bacon/bacon-qr-code/src/Renderer/Image/TransformationMatrix.php

@@ -0,0 +1,68 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Renderer\Image;
+
+final class TransformationMatrix
+{
+    /**
+     * @var float[]
+     */
+    private $values;
+
+    public function __construct()
+    {
+        $this->values = [1, 0, 0, 1, 0, 0];
+    }
+
+    public function multiply(self $other) : self
+    {
+        $matrix = new self();
+        $matrix->values[0] = $this->values[0] * $other->values[0] + $this->values[2] * $other->values[1];
+        $matrix->values[1] = $this->values[1] * $other->values[0] + $this->values[3] * $other->values[1];
+        $matrix->values[2] = $this->values[0] * $other->values[2] + $this->values[2] * $other->values[3];
+        $matrix->values[3] = $this->values[1] * $other->values[2] + $this->values[3] * $other->values[3];
+        $matrix->values[4] = $this->values[0] * $other->values[4] + $this->values[2] * $other->values[5]
+            + $this->values[4];
+        $matrix->values[5] = $this->values[1] * $other->values[4] + $this->values[3] * $other->values[5]
+            + $this->values[5];
+
+        return $matrix;
+    }
+
+    public static function scale(float $size) : self
+    {
+        $matrix = new self();
+        $matrix->values = [$size, 0, 0, $size, 0, 0];
+        return $matrix;
+    }
+
+    public static function translate(float $x, float $y) : self
+    {
+        $matrix = new self();
+        $matrix->values = [1, 0, 0, 1, $x, $y];
+        return $matrix;
+    }
+
+    public static function rotate(int $degrees) : self
+    {
+        $matrix = new self();
+        $rad = deg2rad($degrees);
+        $matrix->values = [cos($rad), sin($rad), -sin($rad), cos($rad), 0, 0];
+        return $matrix;
+    }
+
+
+    /**
+     * Applies this matrix onto a point and returns the resulting viewport point.
+     *
+     * @return float[]
+     */
+    public function apply(float $x, float $y) : array
+    {
+        return [
+            $x * $this->values[0] + $y * $this->values[2] + $this->values[4],
+            $x * $this->values[1] + $y * $this->values[3] + $this->values[5],
+        ];
+    }
+}

+ 152 - 0
htdocs/includes/bacon/bacon-qr-code/src/Renderer/ImageRenderer.php

@@ -0,0 +1,152 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Renderer;
+
+use BaconQrCode\Encoder\MatrixUtil;
+use BaconQrCode\Encoder\QrCode;
+use BaconQrCode\Exception\InvalidArgumentException;
+use BaconQrCode\Renderer\Image\ImageBackEndInterface;
+use BaconQrCode\Renderer\Path\Path;
+use BaconQrCode\Renderer\RendererStyle\EyeFill;
+use BaconQrCode\Renderer\RendererStyle\RendererStyle;
+
+final class ImageRenderer implements RendererInterface
+{
+    /**
+     * @var RendererStyle
+     */
+    private $rendererStyle;
+
+    /**
+     * @var ImageBackEndInterface
+     */
+    private $imageBackEnd;
+
+    public function __construct(RendererStyle $rendererStyle, ImageBackEndInterface $imageBackEnd)
+    {
+        $this->rendererStyle = $rendererStyle;
+        $this->imageBackEnd = $imageBackEnd;
+    }
+
+    /**
+     * @throws InvalidArgumentException if matrix width doesn't match height
+     */
+    public function render(QrCode $qrCode) : string
+    {
+        $size = $this->rendererStyle->getSize();
+        $margin = $this->rendererStyle->getMargin();
+        $matrix = $qrCode->getMatrix();
+        $matrixSize = $matrix->getWidth();
+
+        if ($matrixSize !== $matrix->getHeight()) {
+            throw new InvalidArgumentException('Matrix must have the same width and height');
+        }
+
+        $totalSize = $matrixSize + ($margin * 2);
+        $moduleSize = $size / $totalSize;
+        $fill = $this->rendererStyle->getFill();
+
+        $this->imageBackEnd->new($size, $fill->getBackgroundColor());
+        $this->imageBackEnd->scale((float) $moduleSize);
+        $this->imageBackEnd->translate((float) $margin, (float) $margin);
+
+        $module = $this->rendererStyle->getModule();
+        $moduleMatrix = clone $matrix;
+        MatrixUtil::removePositionDetectionPatterns($moduleMatrix);
+        $modulePath = $this->drawEyes($matrixSize, $module->createPath($moduleMatrix));
+
+        if ($fill->hasGradientFill()) {
+            $this->imageBackEnd->drawPathWithGradient(
+                $modulePath,
+                $fill->getForegroundGradient(),
+                0,
+                0,
+                $matrixSize,
+                $matrixSize
+            );
+        } else {
+            $this->imageBackEnd->drawPathWithColor($modulePath, $fill->getForegroundColor());
+        }
+
+        return $this->imageBackEnd->done();
+    }
+
+    private function drawEyes(int $matrixSize, Path $modulePath) : Path
+    {
+        $fill = $this->rendererStyle->getFill();
+
+        $eye = $this->rendererStyle->getEye();
+        $externalPath = $eye->getExternalPath();
+        $internalPath = $eye->getInternalPath();
+
+        $modulePath = $this->drawEye(
+            $externalPath,
+            $internalPath,
+            $fill->getTopLeftEyeFill(),
+            3.5,
+            3.5,
+            0,
+            $modulePath
+        );
+        $modulePath = $this->drawEye(
+            $externalPath,
+            $internalPath,
+            $fill->getTopRightEyeFill(),
+            $matrixSize - 3.5,
+            3.5,
+            90,
+            $modulePath
+        );
+        $modulePath = $this->drawEye(
+            $externalPath,
+            $internalPath,
+            $fill->getBottomLeftEyeFill(),
+            3.5,
+            $matrixSize - 3.5,
+            -90,
+            $modulePath
+        );
+
+        return $modulePath;
+    }
+
+    private function drawEye(
+        Path $externalPath,
+        Path $internalPath,
+        EyeFill $fill,
+        float $xTranslation,
+        float $yTranslation,
+        int $rotation,
+        Path $modulePath
+    ) : Path {
+        if ($fill->inheritsBothColors()) {
+            return $modulePath
+                ->append($externalPath->translate($xTranslation, $yTranslation))
+                ->append($internalPath->translate($xTranslation, $yTranslation));
+        }
+
+        $this->imageBackEnd->push();
+        $this->imageBackEnd->translate($xTranslation, $yTranslation);
+
+        if (0 !== $rotation) {
+            $this->imageBackEnd->rotate($rotation);
+        }
+
+        if ($fill->inheritsExternalColor()) {
+            $modulePath = $modulePath->append($externalPath->translate($xTranslation, $yTranslation));
+        } else {
+            $this->imageBackEnd->drawPathWithColor($externalPath, $fill->getExternalColor());
+        }
+
+        if ($fill->inheritsInternalColor()) {
+            $modulePath = $modulePath->append($internalPath->translate($xTranslation, $yTranslation));
+        } else {
+            $this->imageBackEnd->drawPathWithColor($internalPath, $fill->getInternalColor());
+        }
+
+        $this->imageBackEnd->pop();
+
+        return $modulePath;
+    }
+}

+ 63 - 0
htdocs/includes/bacon/bacon-qr-code/src/Renderer/Module/DotsModule.php

@@ -0,0 +1,63 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Renderer\Module;
+
+use BaconQrCode\Encoder\ByteMatrix;
+use BaconQrCode\Exception\InvalidArgumentException;
+use BaconQrCode\Renderer\Path\Path;
+
+/**
+ * Renders individual modules as dots.
+ */
+final class DotsModule implements ModuleInterface
+{
+    public const LARGE = 1;
+    public const MEDIUM = .8;
+    public const SMALL = .6;
+
+    /**
+     * @var float
+     */
+    private $size;
+
+    public function __construct(float $size)
+    {
+        if ($size <= 0 || $size > 1) {
+            throw new InvalidArgumentException('Size must between 0 (exclusive) and 1 (inclusive)');
+        }
+
+        $this->size = $size;
+    }
+
+    public function createPath(ByteMatrix $matrix) : Path
+    {
+        $width = $matrix->getWidth();
+        $height = $matrix->getHeight();
+        $path = new Path();
+        $halfSize = $this->size / 2;
+        $margin = (1 - $this->size) / 2;
+
+        for ($y = 0; $y < $height; ++$y) {
+            for ($x = 0; $x < $width; ++$x) {
+                if (! $matrix->get($x, $y)) {
+                    continue;
+                }
+
+                $pathX = $x + $margin;
+                $pathY = $y + $margin;
+
+                $path = $path
+                    ->move($pathX + $this->size, $pathY + $halfSize)
+                    ->ellipticArc($halfSize, $halfSize, 0, false, true, $pathX + $halfSize, $pathY + $this->size)
+                    ->ellipticArc($halfSize, $halfSize, 0, false, true, $pathX, $pathY + $halfSize)
+                    ->ellipticArc($halfSize, $halfSize, 0, false, true, $pathX + $halfSize, $pathY)
+                    ->ellipticArc($halfSize, $halfSize, 0, false, true, $pathX + $this->size, $pathY + $halfSize)
+                    ->close()
+                ;
+            }
+        }
+
+        return $path;
+    }
+}

+ 100 - 0
htdocs/includes/bacon/bacon-qr-code/src/Renderer/Module/EdgeIterator/Edge.php

@@ -0,0 +1,100 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Renderer\Module\EdgeIterator;
+
+final class Edge
+{
+    /**
+     * @var bool
+     */
+    private $positive;
+
+    /**
+     * @var array<int[]>
+     */
+    private $points = [];
+
+    /**
+     * @var array<int[]>|null
+     */
+    private $simplifiedPoints;
+
+    /**
+     * @var int
+     */
+    private $minX = PHP_INT_MAX;
+
+    /**
+     * @var int
+     */
+    private $minY = PHP_INT_MAX;
+
+    /**
+     * @var int
+     */
+    private $maxX = -1;
+
+    /**
+     * @var int
+     */
+    private $maxY = -1;
+
+    public function __construct(bool $positive)
+    {
+        $this->positive = $positive;
+    }
+
+    public function addPoint(int $x, int $y) : void
+    {
+        $this->points[] = [$x, $y];
+        $this->minX = min($this->minX, $x);
+        $this->minY = min($this->minY, $y);
+        $this->maxX = max($this->maxX, $x);
+        $this->maxY = max($this->maxY, $y);
+    }
+
+    public function isPositive() : bool
+    {
+        return $this->positive;
+    }
+
+    /**
+     * @return array<int[]>
+     */
+    public function getPoints() : array
+    {
+        return $this->points;
+    }
+
+    public function getMaxX() : int
+    {
+        return $this->maxX;
+    }
+
+    public function getSimplifiedPoints() : array
+    {
+        if (null !== $this->simplifiedPoints) {
+            return $this->simplifiedPoints;
+        }
+
+        $points = [];
+        $length = count($this->points);
+
+        for ($i = 0; $i < $length; ++$i) {
+            $previousPoint = $this->points[(0 === $i ? $length : $i) - 1];
+            $nextPoint = $this->points[($length - 1 === $i ? -1 : $i) + 1];
+            $currentPoint = $this->points[$i];
+
+            if (($previousPoint[0] === $currentPoint[0] && $currentPoint[0] === $nextPoint[0])
+                || ($previousPoint[1] === $currentPoint[1] && $currentPoint[1] === $nextPoint[1])
+            ) {
+                continue;
+            }
+
+            $points[] = $currentPoint;
+        }
+
+        return $this->simplifiedPoints = $points;
+    }
+}

+ 169 - 0
htdocs/includes/bacon/bacon-qr-code/src/Renderer/Module/EdgeIterator/EdgeIterator.php

@@ -0,0 +1,169 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Renderer\Module\EdgeIterator;
+
+use BaconQrCode\Encoder\ByteMatrix;
+use IteratorAggregate;
+use Traversable;
+
+/**
+ * Edge iterator based on potrace.
+ */
+final class EdgeIterator implements IteratorAggregate
+{
+    /**
+     * @var int[]
+     */
+    private $bytes = [];
+
+    /**
+     * @var int
+     */
+    private $size;
+
+    /**
+     * @var int
+     */
+    private $width;
+
+    /**
+     * @var int
+     */
+    private $height;
+
+    public function __construct(ByteMatrix $matrix)
+    {
+        $this->bytes = iterator_to_array($matrix->getBytes());
+        $this->size = count($this->bytes);
+        $this->width = $matrix->getWidth();
+        $this->height = $matrix->getHeight();
+    }
+
+    /**
+     * @return Traversable<Edge>
+     */
+    public function getIterator() : Traversable
+    {
+        $originalBytes = $this->bytes;
+        $point = $this->findNext(0, 0);
+
+        while (null !== $point) {
+            $edge = $this->findEdge($point[0], $point[1]);
+            $this->xorEdge($edge);
+
+            yield $edge;
+
+            $point = $this->findNext($point[0], $point[1]);
+        }
+
+        $this->bytes = $originalBytes;
+    }
+
+    /**
+     * @return int[]|null
+     */
+    private function findNext(int $x, int $y) : ?array
+    {
+        $i = $this->width * $y + $x;
+
+        while ($i < $this->size && 1 !== $this->bytes[$i]) {
+            ++$i;
+        }
+
+        if ($i < $this->size) {
+            return $this->pointOf($i);
+        }
+
+        return null;
+    }
+
+    private function findEdge(int $x, int $y) : Edge
+    {
+        $edge = new Edge($this->isSet($x, $y));
+        $startX = $x;
+        $startY = $y;
+        $dirX = 0;
+        $dirY = 1;
+
+        while (true) {
+            $edge->addPoint($x, $y);
+            $x += $dirX;
+            $y += $dirY;
+
+            if ($x === $startX && $y === $startY) {
+                break;
+            }
+
+            $left = $this->isSet($x + ($dirX + $dirY - 1 ) / 2, $y + ($dirY - $dirX - 1) / 2);
+            $right = $this->isSet($x + ($dirX - $dirY - 1) / 2, $y + ($dirY + $dirX - 1) / 2);
+
+            if ($right && ! $left) {
+                $tmp = $dirX;
+                $dirX = -$dirY;
+                $dirY = $tmp;
+            } elseif ($right) {
+                $tmp = $dirX;
+                $dirX = -$dirY;
+                $dirY = $tmp;
+            } elseif (! $left) {
+                $tmp = $dirX;
+                $dirX = $dirY;
+                $dirY = -$tmp;
+            }
+        }
+
+        return $edge;
+    }
+
+    private function xorEdge(Edge $path) : void
+    {
+        $points = $path->getPoints();
+        $y1 = $points[0][1];
+        $length = count($points);
+        $maxX = $path->getMaxX();
+
+        for ($i = 1; $i < $length; ++$i) {
+            $y = $points[$i][1];
+
+            if ($y === $y1) {
+                continue;
+            }
+
+            $x = $points[$i][0];
+            $minY = min($y1, $y);
+
+            for ($j = $x; $j < $maxX; ++$j) {
+                $this->flip($j, $minY);
+            }
+
+            $y1 = $y;
+        }
+    }
+
+    private function isSet(int $x, int $y) : bool
+    {
+        return (
+            $x >= 0
+            && $x < $this->width
+            && $y >= 0
+            && $y < $this->height
+        ) && 1 === $this->bytes[$this->width * $y + $x];
+    }
+
+    /**
+     * @return int[]
+     */
+    private function pointOf(int $i) : array
+    {
+        $y = intdiv($i, $this->width);
+        return [$i - $y * $this->width, $y];
+    }
+
+    private function flip(int $x, int $y) : void
+    {
+        $this->bytes[$this->width * $y + $x] = (
+            $this->isSet($x, $y) ? 0 : 1
+        );
+    }
+}

+ 18 - 0
htdocs/includes/bacon/bacon-qr-code/src/Renderer/Module/ModuleInterface.php

@@ -0,0 +1,18 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Renderer\Module;
+
+use BaconQrCode\Encoder\ByteMatrix;
+use BaconQrCode\Renderer\Path\Path;
+
+/**
+ * Interface describing how modules should be rendered.
+ *
+ * A module always receives a byte matrix (with values either being 1 or 0). It returns a path, where the origin
+ * coordinate (0, 0) equals the top left corner of the first matrix value.
+ */
+interface ModuleInterface
+{
+    public function createPath(ByteMatrix $matrix) : Path;
+}

+ 129 - 0
htdocs/includes/bacon/bacon-qr-code/src/Renderer/Module/RoundnessModule.php

@@ -0,0 +1,129 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Renderer\Module;
+
+use BaconQrCode\Encoder\ByteMatrix;
+use BaconQrCode\Exception\InvalidArgumentException;
+use BaconQrCode\Renderer\Module\EdgeIterator\EdgeIterator;
+use BaconQrCode\Renderer\Path\Path;
+
+/**
+ * Rounds the corners of module groups.
+ */
+final class RoundnessModule implements ModuleInterface
+{
+    public const STRONG = 1;
+    public const MEDIUM = .5;
+    public const SOFT = .25;
+
+    /**
+     * @var float
+     */
+    private $intensity;
+
+    public function __construct(float $intensity)
+    {
+        if ($intensity <= 0 || $intensity > 1) {
+            throw new InvalidArgumentException('Intensity must between 0 (exclusive) and 1 (inclusive)');
+        }
+
+        $this->intensity = $intensity / 2;
+    }
+
+    public function createPath(ByteMatrix $matrix) : Path
+    {
+        $path = new Path();
+
+        foreach (new EdgeIterator($matrix) as $edge) {
+            $points = $edge->getSimplifiedPoints();
+            $length = count($points);
+
+            $currentPoint = $points[0];
+            $nextPoint = $points[1];
+            $horizontal = ($currentPoint[1] === $nextPoint[1]);
+
+            if ($horizontal) {
+                $right = $nextPoint[0] > $currentPoint[0];
+                $path = $path->move(
+                    $currentPoint[0] + ($right ? $this->intensity : -$this->intensity),
+                    $currentPoint[1]
+                );
+            } else {
+                $up = $nextPoint[0] < $currentPoint[0];
+                $path = $path->move(
+                    $currentPoint[0],
+                    $currentPoint[1] + ($up ? -$this->intensity : $this->intensity)
+                );
+            }
+
+            for ($i = 1; $i <= $length; ++$i) {
+                if ($i === $length) {
+                    $previousPoint = $points[$length - 1];
+                    $currentPoint = $points[0];
+                    $nextPoint = $points[1];
+                } else {
+                    $previousPoint = $points[(0 === $i ? $length : $i) - 1];
+                    $currentPoint = $points[$i];
+                    $nextPoint = $points[($length - 1 === $i ? -1 : $i) + 1];
+                }
+
+                $horizontal = ($previousPoint[1] === $currentPoint[1]);
+
+                if ($horizontal) {
+                    $right = $previousPoint[0] < $currentPoint[0];
+                    $up = $nextPoint[1] < $currentPoint[1];
+                    $sweep = ($up xor $right);
+
+                    if ($this->intensity < 0.5
+                        || ($right && $previousPoint[0] !== $currentPoint[0] - 1)
+                        || (! $right && $previousPoint[0] - 1 !== $currentPoint[0])
+                    ) {
+                        $path = $path->line(
+                            $currentPoint[0] + ($right ? -$this->intensity : $this->intensity),
+                            $currentPoint[1]
+                        );
+                    }
+
+                    $path = $path->ellipticArc(
+                        $this->intensity,
+                        $this->intensity,
+                        0,
+                        false,
+                        $sweep,
+                        $currentPoint[0],
+                        $currentPoint[1] + ($up ? -$this->intensity : $this->intensity)
+                    );
+                } else {
+                    $up = $previousPoint[1] > $currentPoint[1];
+                    $right = $nextPoint[0] > $currentPoint[0];
+                    $sweep = ! ($up xor $right);
+
+                    if ($this->intensity < 0.5
+                        || ($up && $previousPoint[1] !== $currentPoint[1] + 1)
+                        || (! $up && $previousPoint[0] + 1 !== $currentPoint[0])
+                    ) {
+                        $path = $path->line(
+                            $currentPoint[0],
+                            $currentPoint[1] + ($up ? $this->intensity : -$this->intensity)
+                        );
+                    }
+
+                    $path = $path->ellipticArc(
+                        $this->intensity,
+                        $this->intensity,
+                        0,
+                        false,
+                        $sweep,
+                        $currentPoint[0] + ($right ? $this->intensity : -$this->intensity),
+                        $currentPoint[1]
+                    );
+                }
+            }
+
+            $path = $path->close();
+        }
+
+        return $path;
+    }
+}

+ 47 - 0
htdocs/includes/bacon/bacon-qr-code/src/Renderer/Module/SquareModule.php

@@ -0,0 +1,47 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Renderer\Module;
+
+use BaconQrCode\Encoder\ByteMatrix;
+use BaconQrCode\Renderer\Module\EdgeIterator\EdgeIterator;
+use BaconQrCode\Renderer\Path\Path;
+
+/**
+ * Groups modules together to a single path.
+ */
+final class SquareModule implements ModuleInterface
+{
+    /**
+     * @var self|null
+     */
+    private static $instance;
+
+    private function __construct()
+    {
+    }
+
+    public static function instance() : self
+    {
+        return self::$instance ?: self::$instance = new self();
+    }
+
+    public function createPath(ByteMatrix $matrix) : Path
+    {
+        $path = new Path();
+
+        foreach (new EdgeIterator($matrix) as $edge) {
+            $points = $edge->getSimplifiedPoints();
+            $length = count($points);
+            $path = $path->move($points[0][0], $points[0][1]);
+
+            for ($i = 1; $i < $length; ++$i) {
+                $path = $path->line($points[$i][0], $points[$i][1]);
+            }
+
+            $path = $path->close();
+        }
+
+        return $path;
+    }
+}

+ 29 - 0
htdocs/includes/bacon/bacon-qr-code/src/Renderer/Path/Close.php

@@ -0,0 +1,29 @@
+<?php
+declare(strict_types = 1);
+
+namespace BaconQrCode\Renderer\Path;
+
+final class Close implements OperationInterface
+{
+    /**
+     * @var self|null
+     */
+    private static $instance;
+
+    private function __construct()
+    {
+    }
+
+    public static function instance() : self
+    {
+        return self::$instance ?: self::$instance = new self();
+    }
+
+    /**
+     * @return self
+     */
+    public function translate(float $x, float $y) : OperationInterface
+    {
+        return $this;
+    }
+}

Some files were not shown because too many files changed in this diff