# Qual: Fix 'SqlInjection' notices Batch of 25 files with changes to fix 'SqlInjection' Notices
2453 lines
92 KiB
PHP
2453 lines
92 KiB
PHP
<?php
|
|
/* Copyright (C) 2017 ATM Consulting <contact@atm-consulting.fr>
|
|
* Copyright (C) 2017-2020 Laurent Destailleur <eldy@destailleur.fr>
|
|
* Copyright (C) 2022 charlene benke <charlene@patas-monkey.com>
|
|
* Copyright (C) 2024-2026 MDW <mdeweerd@users.noreply.github.com>
|
|
* Copyright (C) 2024-2026 Frédéric France <frederic.france@free.fr>
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*
|
|
* See https://medium.com/@lhartikk/a-blockchain-in-200-lines-of-code-963cc1cc0e54
|
|
*/
|
|
|
|
include_once DOL_DOCUMENT_ROOT.'/blockedlog/versionmod.inc.php';
|
|
include_once DOL_DOCUMENT_ROOT.'/blockedlog/lib/securitycore.lib.php';
|
|
|
|
|
|
/**
|
|
* Class to manage Blocked Log
|
|
*/
|
|
class BlockedLog
|
|
{
|
|
/**
|
|
* @var DoliDB Database handler
|
|
*/
|
|
public $db;
|
|
|
|
/**
|
|
* Id of the log
|
|
* @var int
|
|
*/
|
|
public $id;
|
|
|
|
/**
|
|
* Entity
|
|
* @var int
|
|
*/
|
|
public $entity;
|
|
|
|
/**
|
|
* Picto
|
|
* @var string
|
|
*/
|
|
public $picto = 'blockedlog';
|
|
|
|
/**
|
|
* @var string Error message
|
|
*/
|
|
public $error = '';
|
|
|
|
/**
|
|
* @var string[] Error codes (or messages)
|
|
*/
|
|
public $errors = array();
|
|
|
|
/**
|
|
* Unique fingerprint of the log
|
|
* @var string
|
|
*/
|
|
public $signature = '';
|
|
|
|
/**
|
|
* @var float|string|null
|
|
*/
|
|
public $amounts = null;
|
|
|
|
/**
|
|
* @var float|string|null
|
|
*/
|
|
public $amounts_taxexcl = null;
|
|
|
|
/**
|
|
* trigger action
|
|
* @var string
|
|
*/
|
|
public $action = '';
|
|
|
|
/**
|
|
* @var string Module source
|
|
*/
|
|
public $module_source = '';
|
|
|
|
/**
|
|
* @var string Terminal nb
|
|
*/
|
|
public $pos_source = '';
|
|
|
|
/**
|
|
* @var string Example 'paymentofinvoice'
|
|
*/
|
|
public $linktype = '';
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
public $linktoref = '';
|
|
|
|
/**
|
|
* Object element
|
|
* @var string
|
|
*/
|
|
public $element = '';
|
|
|
|
/**
|
|
* Object id
|
|
* @var int
|
|
*/
|
|
public $fk_object = 0;
|
|
|
|
/**
|
|
* Log certified by remote authority or not
|
|
* @var boolean
|
|
*/
|
|
public $certified = false;
|
|
|
|
/**
|
|
* Author
|
|
* @var int
|
|
*/
|
|
public $fk_user = 0;
|
|
|
|
/**
|
|
* @var int|string Note we store in database in gmt time, not in server timezone time
|
|
*/
|
|
public $date_creation;
|
|
|
|
/**
|
|
* @var int|string
|
|
*/
|
|
public $date_modification;
|
|
|
|
/**
|
|
* @var int
|
|
*/
|
|
public $date_object = 0;
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
public $ref_object = '';
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
public $type_code = '';
|
|
|
|
/**
|
|
* @var ?stdClass
|
|
*/
|
|
public $object_data = null;
|
|
|
|
/**
|
|
* @var string Version of application
|
|
*/
|
|
public $object_version = '';
|
|
|
|
/**
|
|
* @var string Version of format of line ('', 'V1', ...).
|
|
*/
|
|
public $object_format = '';
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
public $user_fullname = '';
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
public $debuginfo;
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
public $note;
|
|
|
|
/**
|
|
* Array of tracked event codes. They are event codes that triggers a record in the unalterable log (and you can filter in list of events).
|
|
* @var array<string,string|mixed>
|
|
*/
|
|
public $trackedevents = array();
|
|
|
|
/**
|
|
* Array of controlled event codes. They are event the execute a control when they occurs. An error return will cancel the action.
|
|
* @var array<string,string|mixed>
|
|
*/
|
|
public $controlledevents = array();
|
|
|
|
/**
|
|
* Array of tracked modules (key => label). List of modules we can see in module_pos.
|
|
* @var array<int|string,string>
|
|
*/
|
|
public $trackedmodules = array();
|
|
|
|
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param DoliDB $db Database handler
|
|
*/
|
|
public function __construct(DoliDB $db)
|
|
{
|
|
global $conf;
|
|
|
|
$this->db = $db;
|
|
$this->entity = $conf->entity;
|
|
}
|
|
|
|
|
|
/**
|
|
* Load list of tracked and controlled events into $this->controlled, $this->trackedevents and $this->trackedmodules
|
|
*
|
|
* @return int<1,1> Always 1
|
|
*/
|
|
public function loadTrackedEvents()
|
|
{
|
|
global $langs;
|
|
|
|
$this->controlledevents = array();
|
|
$this->trackedevents = array();
|
|
$this->trackedmodules = array();
|
|
|
|
$sep = 0;
|
|
|
|
$this->controlledevents['BILL_MODIFY'] = array('id' => 'BILL_MODIFY', 'label' => 'logBILL_MODIFY');
|
|
|
|
$this->trackedmodules[0] = 'None';
|
|
if (isModEnabled('takepos')) {
|
|
$this->trackedmodules['takepos'] = 'TakePOS';
|
|
}
|
|
|
|
// Customer Invoice/Facture / Payment (For most VAT antifraud laws)
|
|
if (isModEnabled('invoice')) {
|
|
$sep++;
|
|
$this->trackedevents['separator_'.$sep] = array('id' => 'separator_'.$sep, 'label' => '----------', 'labelhtml' => '<span class="opacitymedium">----- '.$langs->trans("Invoices").' | '.$langs->trans("Payments").'</span>', 'disabled' => 1);
|
|
|
|
$this->trackedevents['BILL_VALIDATE'] = array('id' => 'BILL_VALIDATE', 'label' => 'logBILL_VALIDATE', 'labelhtml' => img_picto('', 'bill', 'class="pictofixedwidth").').$langs->trans('logBILL_VALIDATE'));
|
|
//$this->trackedevents['BILL_UPDATE'] = array('id' => 'BILL_VALIDATE', 'label' => 'logBILL_UPDATE', 'labelhtml' => img_picto('', 'bill', 'class="pictofixedwidth").').$langs->trans('logBILL_UPDATE'));
|
|
$this->trackedevents['BILL_SENTBYMAIL'] = array('id' => 'BILL_SENTBYMAIL', 'label' => 'logBILL_SENTBYMAIL', 'labelhtml' => img_picto('', 'bill', 'class="pictofixedwidth").').$langs->trans('logBILL_SENTBYMAIL'));
|
|
$this->trackedevents['DOC_DOWNLOAD'] = array('id' => 'DOC_DOWNLOAD', 'label' => 'BlockedLogBillDownload', 'labelhtml' => img_picto('', 'bill', 'class="pictofixedwidth").').$langs->trans('BlockedLogBillDownload'));
|
|
$this->trackedevents['DOC_PREVIEW'] = array('id' => 'DOC_PREVIEW', 'label' => 'BlockedLogBillPreview', 'labelhtml' => img_picto('', 'bill', 'class="pictofixedwidth").').$langs->trans('BlockedLogBillPreview'));
|
|
$this->trackedevents['PAYMENT_CUSTOMER_CREATE'] = array('id' => 'PAYMENT_CUSTOMER_CREATE', 'label' => 'logPAYMENT_CUSTOMER_CREATE', 'labelhtml' => img_picto('', 'bill', 'class="pictofixedwidth").').$langs->trans('logPAYMENT_CUSTOMER_CREATE'));
|
|
$this->trackedevents['PAYMENT_CUSTOMER_DELETE'] = array('id' => 'PAYMENT_CUSTOMER_DELETE', 'label' => 'logPAYMENT_CUSTOMER_DELETE', 'labelhtml' => img_picto('', 'bill', 'class="pictofixedwidth").').$langs->trans('logPAYMENT_CUSTOMER_DELETE'));
|
|
}
|
|
|
|
/* Supplier
|
|
// Supplier Invoice / Payment
|
|
if (isModEnabled("fournisseur")) {
|
|
$this->trackedevents['BILL_SUPPLIER_VALIDATE']='BlockedLogSupplierBillValidate';
|
|
$this->trackedevents['BILL_SUPPLIER_DELETE']='BlockedLogSupplierBillDelete';
|
|
$this->trackedevents['BILL_SUPPLIER_SENTBYMAIL']='BlockedLogSupplierBillSentByEmail'; // Trigger key does not exists, we want just into array to list it as done
|
|
$this->trackedevents['SUPPLIER_DOC_DOWNLOAD']='BlockedLogSupplierBillDownload'; // Trigger key does not exists, we want just into array to list it as done
|
|
$this->trackedevents['SUPPLIER_DOC_PREVIEW']='BlockedLogSupplierBillPreview'; // Trigger key does not exists, we want just into array to list it as done
|
|
$this->trackedevents['PAYMENT_SUPPLIER_CREATE']='BlockedLogSupplierBillPaymentCreate';
|
|
$this->trackedevents['PAYMENT_SUPPLIER_DELETE']='BlockedLogsupplierBillPaymentCreate';
|
|
}
|
|
*/
|
|
|
|
// Donation
|
|
if (isModEnabled('don') && getDolGlobalString('BLOCKEDLOG_ENABLE_DONATION')) { // For countries that need unalterable logs for donations
|
|
if (!empty($this->trackedevents)) {
|
|
$sep++;
|
|
$this->trackedevents['separator_'.$sep] = array('id' => 'separator_'.$sep, 'label' => '----------', 'labelhtml' => '<span class="opacitymedium">----- '.$langs->trans("Donations").' | '.$langs->trans("Payments").'</span>', 'disabled' => 1);
|
|
}
|
|
|
|
$this->trackedevents['DON_VALIDATE'] = array('id' => 'DON_VALIDATE', 'label' => 'logDON_VALIDATE', 'labelhtml' => img_picto('', 'donation', 'class="pictofixedwidth").').$langs->trans('logDON_VALIDATE'));
|
|
$this->trackedevents['DON_DELETE'] = array('id' => 'DON_DELETE', 'label' => 'logDON_DELETE', 'labelhtml' => img_picto('', 'donation', 'class="pictofixedwidth").').$langs->trans('logDON_DELETE'));
|
|
//$this->trackedevents['DON_SENTBYMAIL'] = array('id' => 'BILL_VALIDATE', img_picto('', 'don', 'class="pictofixedwidth").').$langs->trans('labelhtml' => 'logDON_SENTBYMAIL');
|
|
$this->trackedevents['DONATION_PAYMENT_CREATE'] = array('id' => 'DONATION_PAYMENT_CREATE', 'label' => 'logDONATION_PAYMENT_CREATE', 'labelhtml' => img_picto('', 'donation', 'class="pictofixedwidth").').$langs->trans('logDONATION_PAYMENT_CREATE'));
|
|
$this->trackedevents['DONATION_PAYMENT_DELETE'] = array('id' => 'DONATION_PAYMENT_DELETE', 'label' => 'logDONATION_PAYMENT_DELETE', 'labelhtml' => img_picto('', 'donation', 'class="pictofixedwidth").').$langs->trans('logDONATION_PAYMENT_DELETE'));
|
|
}
|
|
|
|
/*
|
|
// Salary
|
|
if (isModEnabled('salary')) {
|
|
$this->trackedevents['PAYMENT_SALARY_CREATE'] = 'BlockedLogSalaryPaymentCreate';
|
|
$this->trackedevents['PAYMENT_SALARY_MODIFY'] = 'BlockedLogSalaryPaymentCreate';
|
|
$this->trackedevents['PAYMENT_SALARY_DELETE'] = 'BlockedLogSalaryPaymentCreate';
|
|
}
|
|
*/
|
|
|
|
// Members
|
|
if (isModEnabled('member') && getDolGlobalString('BLOCKEDLOG_ENABLE_MEMBER')) { // For countries that need unalterable logs for membership management
|
|
if (!empty($this->trackedevents)) {
|
|
$sep++;
|
|
$this->trackedevents['separator_'.$sep] = array('id' => 'separator_'.$sep, 'label' => '----------', 'labelhtml' => '<span class="opacitymedium">----- '.$langs->trans("MenuMembers").'</span>', 'disabled' => 1);
|
|
}
|
|
|
|
$this->trackedevents['MEMBER_SUBSCRIPTION_CREATE'] = array('id' => 'MEMBER_SUBSCRIPTION_CREATE', 'label' => 'logMEMBER_SUBSCRIPTION_CREATE', 'labelhtml' => img_picto('', 'member', 'class="pictofixedwidth").').$langs->trans('logMEMBER_SUBSCRIPTION_CREATE'));
|
|
$this->trackedevents['MEMBER_SUBSCRIPTION_MODIFY'] = array('id' => 'MEMBER_SUBSCRIPTION_MODIFY', 'label' => 'logMEMBER_SUBSCRIPTION_MODIFY', 'labelhtml' => img_picto('', 'member', 'class="pictofixedwidth").').$langs->trans('logMEMBER_SUBSCRIPTION_MODIFY'));
|
|
$this->trackedevents['MEMBER_SUBSCRIPTION_DELETE'] = array('id' => 'MEMBER_SUBSCRIPTION_DELETE', 'label' => 'logMEMBER_SUBSCRIPTION_DELETE', 'labelhtml' => img_picto('', 'member', 'class="pictofixedwidth").').$langs->trans('logMEMBER_SUBSCRIPTION_DELETE'));
|
|
}
|
|
|
|
// Bank
|
|
/*
|
|
if (isModEnabled("bank")) {
|
|
if (!empty($this->trackedevents)) {
|
|
$sep++;
|
|
$this->trackedevents['separator_'.$sep] = array('id' => 'separator_'.$sep, 'label' => '----------', 'labelhtml' => '<span class="opacitymedium">----- '.$langs->trans("VariousPayment").'</span>', 'disabled' => 1);
|
|
}
|
|
|
|
$this->trackedevents['PAYMENT_VARIOUS_CREATE'] = array('id' => 'PAYMENT_VARIOUS_CREATE', 'label' => 'logPAYMENT_VARIOUS_CREATE', 'labelhtml' => img_picto('', 'bank', 'class="pictofixedwidth").').$langs->trans('logPAYMENT_VARIOUS_CREATE'));
|
|
$this->trackedevents['PAYMENT_VARIOUS_MODIFY'] = array('id' => 'PAYMENT_VARIOUS_MODIFY', 'label' => 'logPAYMENT_VARIOUS_MODIFY', 'labelhtml' => img_picto('', 'bank', 'class="pictofixedwidth").').$langs->trans('logPAYMENT_VARIOUS_MODIFY'));
|
|
$this->trackedevents['PAYMENT_VARIOUS_DELETE'] = array('id' => 'PAYMENT_VARIOUS_DELETE', 'label' => 'logPAYMENT_VARIOUS_DELETE', 'labelhtml' => img_picto('', 'bank', 'class="pictofixedwidth").').$langs->trans('logPAYMENT_VARIOUS_DELETE'));
|
|
}
|
|
*/
|
|
|
|
// Cash register closing
|
|
// $conf->global->BANK_ENABLE_POS_CASHCONTROL must be set to 1 by all external POS modules
|
|
$moduleposenabled = (isModEnabled('cashdesk') || isModEnabled('takepos') || getDolGlobalString('BANK_ENABLE_POS_CASHCONTROL'));
|
|
if ($moduleposenabled) {
|
|
if (!empty($this->trackedevents)) {
|
|
$sep++;
|
|
$this->trackedevents['separator_'.$sep] = array('id' => 'separator_'.$sep, 'label' => '----------', 'labelhtml' => '<span class="opacitymedium">----- '.$langs->trans("CashControl").'</span>', 'disabled' => 1);
|
|
}
|
|
if (getDolGlobalString('BLOCKEDLOG_ADD_OLD_CASHCONTROL_VALIDATE')) {
|
|
$this->trackedevents['CASHCONTROL_VALIDATE'] = array('id' => 'CASHCONTROL_VALIDATE', 'label' => 'logCASHCONTROL_VALIDATE', 'labelhtml' => img_picto('', 'pos', 'class="pictofixedwidth").').$langs->trans('logCASHCONTROL_VALIDATE'));
|
|
}
|
|
$this->trackedevents['CASHCONTROL_CLOSE'] = array('id' => 'CASHCONTROL_CLOSE', 'label' => 'logCASHCONTROL_CLOSE', 'labelhtml' => img_picto('', 'pos', 'class="pictofixedwidth").').$langs->trans('logCASHCONTROL_CLOSE'));
|
|
}
|
|
|
|
// Add more action to track from a conf variable. For the case we want to track other actions into the unalterable log.
|
|
// For example: STOCK_MOVEMENT, ...
|
|
if (getDolGlobalString('BLOCKEDLOG_ADD_ACTIONS_SUPPORTED')) {
|
|
if (!empty($this->trackedevents)) {
|
|
$sep++;
|
|
$this->trackedevents['separator_'.$sep] = array('id' => 'separator_'.$sep, 'label' => '----------', 'labelhtml' => '<span class="opacitymedium">----------</span>', 'disabled' => 1);
|
|
}
|
|
|
|
$tmparrayofmoresupportedevents = explode(',', getDolGlobalString('BLOCKEDLOG_ADD_ACTIONS_SUPPORTED'));
|
|
foreach ($tmparrayofmoresupportedevents as $val) {
|
|
$this->trackedevents[$val] = array('id' => $val, 'label' => 'log'.$val, 'labelhtml' => img_picto('', 'generic', 'class="pictofixedwidth").').$langs->trans('log'.$val));
|
|
}
|
|
}
|
|
|
|
if (!empty($this->trackedevents)) {
|
|
$sep++;
|
|
$this->trackedevents['separator_'.$sep] = array('id' => 'separator_'.$sep, 'label' => '----------', 'labelhtml' => '<span class="opacitymedium">----- '.$langs->trans("Other").'</span>', 'disabled' => 1);
|
|
}
|
|
$this->trackedevents['BLOCKEDLOG_EXPORT'] = array('id' => 'BLOCKEDLOG_EXPORT', 'label' => 'logBLOCKEDLOG_EXPORT', 'labelhtml' => img_picto('', $this->picto, 'class="pictofixedwidth").').$langs->trans('logBLOCKEDLOG_EXPORT'));
|
|
|
|
return 1;
|
|
}
|
|
|
|
/**
|
|
* Try to retrieve source object (it it still exists).
|
|
*
|
|
* @return string URL string of source object
|
|
*/
|
|
public function getObjectLink()
|
|
{
|
|
global $langs;
|
|
|
|
if ($this->element === 'facture') {
|
|
require_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
|
|
|
|
$object = new Facture($this->db);
|
|
if ($object->fetch($this->fk_object) > 0) {
|
|
return $object->getNomUrl(1);
|
|
} else {
|
|
$this->error = (string) (((int) $this->error) + 1);
|
|
}
|
|
}
|
|
if ($this->element === 'invoice_supplier') {
|
|
require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.facture.class.php';
|
|
|
|
$object = new FactureFournisseur($this->db);
|
|
if ($object->fetch($this->fk_object) > 0) {
|
|
return $object->getNomUrl(1);
|
|
} else {
|
|
$this->error = (string) (((int) $this->error) + 1);
|
|
}
|
|
} elseif ($this->element === 'payment') {
|
|
require_once DOL_DOCUMENT_ROOT.'/compta/paiement/class/paiement.class.php';
|
|
|
|
$object = new Paiement($this->db);
|
|
if ($object->fetch($this->fk_object) > 0) {
|
|
return $object->getNomUrl(1);
|
|
} else {
|
|
$this->error = (string) (((int) $this->error) + 1);
|
|
}
|
|
} elseif ($this->element === 'payment_supplier') {
|
|
require_once DOL_DOCUMENT_ROOT.'/fourn/class/paiementfourn.class.php';
|
|
|
|
$object = new PaiementFourn($this->db);
|
|
if ($object->fetch($this->fk_object) > 0) {
|
|
return $object->getNomUrl(1);
|
|
} else {
|
|
$this->error = (string) (((int) $this->error) + 1);
|
|
}
|
|
} elseif ($this->element === 'payment_donation') {
|
|
require_once DOL_DOCUMENT_ROOT.'/don/class/paymentdonation.class.php';
|
|
|
|
$object = new PaymentDonation($this->db);
|
|
if ($object->fetch($this->fk_object) > 0) {
|
|
return $object->getNomUrl(1);
|
|
} else {
|
|
$this->error = (string) (((int) $this->error) + 1);
|
|
}
|
|
} elseif ($this->element === 'payment_various') {
|
|
require_once DOL_DOCUMENT_ROOT.'/compta/bank/class/paymentvarious.class.php';
|
|
|
|
$object = new PaymentVarious($this->db);
|
|
if ($object->fetch($this->fk_object) > 0) {
|
|
return $object->getNomUrl(1);
|
|
} else {
|
|
$this->error = (string) (((int) $this->error) + 1);
|
|
}
|
|
} elseif ($this->element === 'don' || $this->element === 'donation') {
|
|
require_once DOL_DOCUMENT_ROOT.'/don/class/don.class.php';
|
|
|
|
$object = new Don($this->db);
|
|
if ($object->fetch($this->fk_object) > 0) {
|
|
return $object->getNomUrl(1);
|
|
} else {
|
|
$this->error = (string) (((int) $this->error) + 1);
|
|
}
|
|
} elseif ($this->element === 'subscription') {
|
|
require_once DOL_DOCUMENT_ROOT.'/adherents/class/subscription.class.php';
|
|
|
|
$object = new Subscription($this->db);
|
|
if ($object->fetch($this->fk_object) > 0) {
|
|
return $object->getNomUrl(1);
|
|
} else {
|
|
$this->error = (string) (((int) $this->error) + 1);
|
|
}
|
|
} elseif ($this->element === 'cashcontrol') {
|
|
require_once DOL_DOCUMENT_ROOT.'/compta/cashcontrol/class/cashcontrol.class.php';
|
|
|
|
$object = new CashControl($this->db);
|
|
if ($object->fetch($this->fk_object) > 0) {
|
|
return $object->getNomUrl(1);
|
|
} else {
|
|
$this->error = (string) (((int) $this->error) + 1);
|
|
}
|
|
} elseif ($this->element === 'stockmouvement') {
|
|
require_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php';
|
|
|
|
$object = new MouvementStock($this->db);
|
|
if ($object->fetch($this->fk_object) > 0) {
|
|
return $object->getNomUrl(1);
|
|
} else {
|
|
$this->error = (string) (((int) $this->error) + 1);
|
|
}
|
|
} elseif ($this->element === 'project') {
|
|
require_once DOL_DOCUMENT_ROOT.'/projet/class/project.class.php';
|
|
|
|
$object = new Project($this->db);
|
|
if ($object->fetch($this->fk_object) > 0) {
|
|
return $object->getNomUrl(1);
|
|
} else {
|
|
$this->error = (string) (((int) $this->error) + 1);
|
|
}
|
|
} elseif ($this->action == 'BLOCKEDLOG_EXPORT') {
|
|
return '<i class="opacitymedium">'.$langs->trans("logBLOCKEDLOG_EXPORT").'</i>';
|
|
} elseif ($this->action == 'MODULE_SET') {
|
|
return '<i class="opacitymedium">'.$langs->trans("BlockedLogEnabled").'</i>';
|
|
} elseif ($this->action == 'MODULE_RESET') { // This case should not happen. Paranoiac protection against possible bug that forces a record that will return a non valid entry.
|
|
if ($this->signature == '0000000000') {
|
|
return '<i class="opacitymedium">'.$langs->trans("BlockedLogDisabled").'</i>';
|
|
} else {
|
|
return '<i class="opacitymedium">'.$langs->trans("BlockedLogDisabledBis").'</i>';
|
|
}
|
|
}
|
|
|
|
return '<i class="opacitymedium">'.$langs->trans('ImpossibleToReloadObject', $this->element, $this->fk_object).'</i>';
|
|
}
|
|
|
|
/**
|
|
* Try to retrieve user author
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getUser()
|
|
{
|
|
global $langs, $cachedUser;
|
|
|
|
if (empty($cachedUser)) {
|
|
$cachedUser = array();
|
|
}
|
|
|
|
if (empty($cachedUser[$this->fk_user])) {
|
|
$u = new User($this->db);
|
|
if ($u->fetch($this->fk_user) > 0) {
|
|
$cachedUser[$this->fk_user] = $u;
|
|
}
|
|
}
|
|
|
|
if (!empty($cachedUser[$this->fk_user])) {
|
|
return $cachedUser[$this->fk_user]->getNomUrl(1);
|
|
}
|
|
|
|
return $langs->trans('ImpossibleToRetrieveUser', $this->fk_user);
|
|
}
|
|
|
|
/**
|
|
* Populate properties of an unalterable log entry from object data.
|
|
* This populates ->object_data but also other fields like ->action, ->module_source, ->amounts_taxexcl, ->amounts and ->linktoref and ->linktype
|
|
* It also populates some debug info like ->element and ->fk_object
|
|
*
|
|
* @param CommonObject|stdClass $object Object to store
|
|
* @param string $action Action code ('BILL_VALIDATE', 'BILL_SENTBYMAIL', ...)
|
|
* @param float|int $amounts amounts (incl tax)
|
|
* @param ?User $fuser User object (forced)
|
|
* @param float|int|null $amounts_taxexcl amounts (excl tax or null if not relevant)
|
|
* @return int<-1,-1>|int<1,1> Return >0 if OK, <0 if KO
|
|
*/
|
|
public function setObjectData(&$object, $action, $amounts, $fuser = null, $amounts_taxexcl = null)
|
|
{
|
|
global $langs, $user, $mysoc;
|
|
|
|
if (is_object($fuser)) {
|
|
$user = $fuser;
|
|
}
|
|
|
|
// Init object_data for JSON data
|
|
$this->object_data = new stdClass();
|
|
|
|
// Generic fields
|
|
|
|
// entity
|
|
$this->entity = $object->entity ?? getDolEntity();
|
|
|
|
// action
|
|
$this->action = $action;
|
|
|
|
// amount
|
|
$this->amounts_taxexcl = $amounts_taxexcl;
|
|
$this->amounts = $amounts;
|
|
if ($action === 'MEMBER_SUBSCRIPTION_DELETE' || $action === 'PAYMENT_CUSTOMER_DELETE' || $action === 'PAYMENT_SUPPLIER_DELETE' || $action === 'DONATION_PAYMENT_DELETE') {
|
|
$this->amounts_taxexcl = - $this->amounts_taxexcl;
|
|
$this->amounts = - $this->amounts;
|
|
}
|
|
|
|
// date
|
|
if ($object->element == 'payment' || $object->element == 'payment_supplier') {
|
|
'@phan-var-force Paiement|PaiementFourn $object';
|
|
$this->date_object = empty($object->datepaye) ? $object->date : $object->datepaye;
|
|
} elseif ($object->element == 'payment_salary') {
|
|
'@phan-var-force PaymentSalary $object';
|
|
$this->date_object = $object->datev;
|
|
} elseif ($object->element == 'payment_donation' || $object->element == 'payment_various') {
|
|
'@phan-var-force PaymentDonation $object';
|
|
$this->date_object = empty($object->datepaid) ? $object->datep : $object->datepaid;
|
|
} elseif ($object->element == 'subscription') {
|
|
'@phan-var-force Subscription $object';
|
|
$this->date_object = $object->dateh;
|
|
} elseif ($object->element == 'cashcontrol') {
|
|
/** var CashControl $object */
|
|
'@phan-var-force CashControl $object';
|
|
$this->date_object = $object->date_creation;
|
|
$this->module_source = $object->posmodule;
|
|
$this->pos_source = $object->posnumber;
|
|
} elseif (property_exists($object, 'date')) {
|
|
// Generic case
|
|
$this->date_object = $object->date; // @phan-suppress-current-line PhanUndeclaredProperty
|
|
} elseif (property_exists($object, 'datem')) {
|
|
// Generic case (second chance, for example for stock movement)
|
|
$this->date_object = $object->datem; // @phan-suppress-current-line PhanUndeclaredProperty
|
|
}
|
|
|
|
// In case of credit note, we add link to source invoice to have more tracking info when doing tracking later
|
|
if ($object->element == 'invoice_supplier') {
|
|
'@phan-var-force FactureFournisseur $object';
|
|
if ($object->type == FactureFournisseur::TYPE_CREDIT_NOTE) {
|
|
$invoice = new FactureFournisseur($this->db);
|
|
$invoice->fetch($object->fk_facture_source);
|
|
if ($invoice->id > 0) {
|
|
$this->linktype = 'credit_note_of';
|
|
$this->linktoref = $invoice->ref;
|
|
}
|
|
//$this->module_source = (string) $invoice->module_source;
|
|
//$this->pos_source = (string) $invoice->pos_source;
|
|
}
|
|
}
|
|
if ($object->element == 'facture') {
|
|
'@phan-var-force Facture $object';
|
|
if ($object->type == Facture::TYPE_CREDIT_NOTE) {
|
|
$invoice = new Facture($this->db);
|
|
$invoice->fetch($object->fk_facture_source);
|
|
if ($invoice->id > 0) {
|
|
$this->linktype = 'credit_note_of';
|
|
$this->linktoref = $invoice->ref;
|
|
|
|
$this->object_data->link = $this->linktype.' '.$this->linktoref;
|
|
}
|
|
$this->module_source = (string) $invoice->module_source;
|
|
$this->pos_source = (string) $invoice->pos_source;
|
|
}
|
|
}
|
|
|
|
// ref object
|
|
$this->ref_object = ((!empty($object->newref)) ? $object->newref : $object->ref); // newref is set when validating a draft, ref is set in other cases
|
|
// type of object
|
|
$this->element = $object->element;
|
|
// id of object
|
|
$this->fk_object = $object->id;
|
|
|
|
// Add thirdparty info if not yet done
|
|
if (empty($object->thirdparty) && method_exists($object, 'fetch_thirdparty')) {
|
|
$object->fetch_thirdparty();
|
|
}
|
|
|
|
|
|
// Add fields to exclude (this has become useless because we now use a list fields to keep later).
|
|
$arrayoffieldstoexclude = array(
|
|
'table_element', 'fields',
|
|
'ref_previous', 'ref_next',
|
|
'origin', 'origin_id',
|
|
'oldcopy', 'picto', 'error', 'errors',
|
|
'model_pdf', 'modelpdf', 'last_main_doc', 'civility_id', 'contact', 'contact_id',
|
|
'table_element_line', 'ismultientitymanaged', 'isextrafieldmanaged',
|
|
'array_languages',
|
|
'childtables',
|
|
'contact_ids',
|
|
'context',
|
|
'element',
|
|
'labelStatus',
|
|
'labelStatusShort',
|
|
'linkedObjectsIds',
|
|
'linkedObjects',
|
|
'fk_delivery_address',
|
|
'projet', // There is already ->fk_project
|
|
'restrictiononfksoc',
|
|
'specimen',
|
|
);
|
|
|
|
// Add more fields to exclude depending on object type
|
|
if ($this->element == 'cashcontrol') {
|
|
$arrayoffieldstoexclude = array_merge(
|
|
$arrayoffieldstoexclude,
|
|
array(
|
|
'name', 'lastname', 'firstname', 'region', 'region_id', 'region_code', 'state', 'state_id', 'state_code', 'country', 'country_id', 'country_code',
|
|
'total_ht', 'total_tva', 'total_ttc', 'total_localtax1', 'total_localtax2',
|
|
'barcode_type', 'barcode_type_code', 'barcode_type_label', 'barcode_type_coder', 'mode_reglement_id', 'cond_reglement_id', 'mode_reglement', 'cond_reglement', 'shipping_method_id',
|
|
'extraparams', 'fk_incoterms', 'fk_user_creat', 'fk_user_valid', 'label_incoterms', 'location_incoterms', 'lines', 'nb', 'tms', 'comments', 'array_options', 'warnings',
|
|
'opening', 'status', 'date_valid'
|
|
)
|
|
);
|
|
}
|
|
|
|
// For customer payment and supplier payment, the thirdparty can be added in payment detail
|
|
$addthirdpartyatpaymentlevel = 0;
|
|
if (!empty($object->thirdparty) && in_array($this->element, array('payment', 'payment_supplier'))) {
|
|
$addthirdpartyatpaymentlevel = 1;
|
|
}
|
|
|
|
if (!empty($object->thirdparty) && !$addthirdpartyatpaymentlevel) { // If $addthirdpartyatpaymentlevel is set, we will add thirdparty on payments later.
|
|
$this->object_data->thirdparty = new stdClass();
|
|
|
|
foreach ($object->thirdparty as $key => $value) {
|
|
if (in_array($key, $arrayoffieldstoexclude)) {
|
|
continue; // Discard some properties
|
|
}
|
|
// List of fields qualified
|
|
if (!in_array($key, array(
|
|
'name', 'name_alias', 'ref_ext', 'address', 'zip', 'town', 'state_code', 'country_code', 'idprof1', 'idprof2', 'idprof3', 'idprof4', 'idprof5', 'idprof6', 'phone', 'fax', 'email', 'barcode',
|
|
'tva_intra', 'tva_assuj', 'localtax1_assuj', 'localtax2_assuj', 'managers', 'capital', 'typent_code', 'forme_juridique_code', 'code_client', 'code_fournisseur'
|
|
))) {
|
|
continue; // Discard if not into this dedicated list
|
|
}
|
|
|
|
$valuequalifiedforstorage = false;
|
|
if (!is_object($value)) {
|
|
if (empty($value) && in_array($key, array('country_code', 'idprof1', 'idprof2', 'tva_intra'))) {
|
|
$valuequalifiedforstorage = true; // We accept '' value for some fields
|
|
$value = (string) $value;
|
|
}
|
|
if (!is_null($value) && empty($value) && in_array($key, array('tva_assuj', 'localtax1_assuj', 'localtax2_assuj'))) {
|
|
$valuequalifiedforstorage = true; // We accept zero value for amounts
|
|
}
|
|
if (!is_null($value) && (string) $value !== '') {
|
|
$valuequalifiedforstorage = true;
|
|
}
|
|
}
|
|
|
|
if ($valuequalifiedforstorage) {
|
|
$this->object_data->thirdparty->$key = $value;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add my company info (Only for customer invoice and payment)
|
|
if (!empty($mysoc) && in_array($object->element, array('facture', 'paiement'))) {
|
|
$this->object_data->mycompany = new stdClass();
|
|
|
|
foreach ($mysoc as $key => $value) {
|
|
if (in_array($key, $arrayoffieldstoexclude)) {
|
|
continue; // Discard some properties
|
|
}
|
|
// List of fields qualified to keep
|
|
if (!in_array($key, array(
|
|
'name', 'name_alias', 'ref_ext', 'address', 'zip', 'town', 'state_code', 'country_code', 'idprof1', 'idprof2', 'idprof3', 'idprof4', 'idprof5', 'idprof6', 'phone', 'fax', 'email', 'barcode',
|
|
'tva_assuj', 'tva_intra', 'localtax1_assuj', 'localtax1_value', 'localtax2_assuj', 'localtax2_value', 'managers', 'capital', 'typent_code', 'forme_juridique_code', 'code_client', 'code_fournisseur'
|
|
))) {
|
|
continue; // Discard if not into this dedicated list
|
|
}
|
|
|
|
$valuequalifiedforstorage = false;
|
|
if (!is_object($value)) {
|
|
if (empty($value) && in_array($key, array('country_code', 'idprof1', 'idprof2', 'tva_intra'))) {
|
|
$valuequalifiedforstorage = true; // We accept '' value for some fields
|
|
$value = (string) $value;
|
|
}
|
|
if (!is_null($value) && empty($value) && in_array($key, array('tva_assuj', 'localtax1_assuj', 'localtax2_assuj'))) {
|
|
$valuequalifiedforstorage = true; // We accept zero value for amounts
|
|
}
|
|
if (!is_null($value) && (string) $value !== '') {
|
|
$valuequalifiedforstorage = true;
|
|
}
|
|
}
|
|
|
|
if ($valuequalifiedforstorage) {
|
|
$this->object_data->mycompany->$key = $value;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add user info
|
|
if (!empty($user)) {
|
|
$this->fk_user = $user->id;
|
|
$this->user_fullname = $user->getFullName($langs);
|
|
}
|
|
|
|
// Field specific to object
|
|
if ($this->element == 'facture') {
|
|
'@phan-var-force Facture $object';
|
|
$this->module_source = (string) $object->module_source;
|
|
$this->pos_source = (string) $object->pos_source;
|
|
|
|
foreach ($object as $key => $value) {
|
|
if (in_array($key, $arrayoffieldstoexclude)) {
|
|
continue; // Discard some properties
|
|
}
|
|
// List of fields qualified
|
|
if (!in_array($key, array(
|
|
'ref', 'ref_client', 'ref_supplier', 'date', 'datef', 'datev', 'type',
|
|
//'vat_src_code', 'tva_tx', 'localtax1_tx', 'localtax2_tx', There is no rate at full doc level
|
|
'total_ht', 'total_tva', 'total_ttc', 'localtax1', 'localtax2',
|
|
'revenuestamp', 'datepointoftax', 'note_public',
|
|
'lines',
|
|
'module_source', 'pos_source', 'pos_print_counter', 'email_sent_counter'
|
|
))) {
|
|
continue; // Discarded if not into the dedicated list
|
|
}
|
|
if ($key == 'lines') {
|
|
$lineid = 0;
|
|
foreach ($value as $tmpline) { // $tmpline is object FactureLine
|
|
$lineid++;
|
|
foreach ($tmpline as $keyline => $valueline) {
|
|
if (!in_array($keyline, array(
|
|
'ref', 'product_type', 'product_label',
|
|
'qty', 'subprice',
|
|
'vat_src_code', 'tva_tx', 'localtax1_tx', 'localtax2_tx',
|
|
'total_ht', 'total_tva', 'total_ttc', 'total_localtax1', 'total_localtax2',
|
|
'multicurrency_code', 'multicurrency_total_ht', 'multicurrency_total_tva', 'multicurrency_total_ttc',
|
|
'info_bits', 'special_code', 'remise_percent'
|
|
))) {
|
|
continue; // Discard if not into a dedicated list
|
|
}
|
|
|
|
if (empty($this->object_data->invoiceline[$lineid]) || !is_object($this->object_data->invoiceline[$lineid])) { // To avoid warning
|
|
$this->object_data->invoiceline[$lineid] = new stdClass();
|
|
}
|
|
|
|
$valuequalifiedforstorage = false;
|
|
if (!is_object($valueline)) {
|
|
if (!is_null($valueline) && empty($valueline) && in_array($key, array('tva_tx', 'localtax1_tx', 'localtax2_tx', 'total_ht', 'total_tva', 'total_ttc', 'total_localtax1', 'total_localtax2'))) {
|
|
$valuequalifiedforstorage = true; // We accept zero value for amounts
|
|
}
|
|
if (!is_null($valueline) && (string) $valueline !== '') {
|
|
$valuequalifiedforstorage = true;
|
|
}
|
|
}
|
|
if ($keyline == 'product_label' && empty($valueline)) {
|
|
$valueline = dol_trunc(dolGetFirstLineOfText($tmpline->desc)); // Fallback on description if label is empty
|
|
$valuequalifiedforstorage = true;
|
|
}
|
|
|
|
if ($valuequalifiedforstorage) {
|
|
$this->object_data->invoiceline[$lineid]->$keyline = $valueline;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
$valuequalifiedforstorage = false;
|
|
if (!is_object($value)) {
|
|
if (empty($value) && in_array($key, array('pos_source', 'module_source'))) {
|
|
$valuequalifiedforstorage = true; // We accept '' value for some fields
|
|
$value = (string) $value;
|
|
}
|
|
if (!is_null($value) && empty($value) && in_array($key, array('total_ht', 'total_tva', 'total_ttc', 'localtax1', 'localtax2', 'pos_print_counter', 'email_sent_counter'))) {
|
|
$valuequalifiedforstorage = true; // We accept zero value for amounts
|
|
}
|
|
if (!is_null($value) && (string) $value !== '') {
|
|
$valuequalifiedforstorage = true;
|
|
}
|
|
}
|
|
|
|
if ($valuequalifiedforstorage) {
|
|
$this->object_data->$key = $value;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!empty($object->newref)) {
|
|
$this->object_data->ref = $object->newref;
|
|
}
|
|
|
|
// Add data for action emails
|
|
if ($action == 'BILL_SENTBYMAIL') {
|
|
$this->object_data->action_email_sent = array(
|
|
"email_from" => $object->context['email_from'],
|
|
"email_to" => $object->context['email_to'],
|
|
"email_msgid" => $object->context['email_msgid']
|
|
);
|
|
}
|
|
} elseif ($this->element == 'invoice_supplier') {
|
|
'@phan-var-force FactureFournisseur $object';
|
|
foreach ($object as $key => $value) {
|
|
if (in_array($key, $arrayoffieldstoexclude)) {
|
|
continue; // Discard some properties
|
|
}
|
|
// List of fields qualified
|
|
if (!in_array($key, array(
|
|
'ref', 'ref_client', 'ref_supplier', 'date', 'datef', 'type', 'total_ht', 'total_tva', 'total_ttc', 'localtax1', 'localtax2', 'revenuestamp', 'datepointoftax', 'note_public'
|
|
))) {
|
|
continue; // Discard if not into a dedicated list
|
|
}
|
|
|
|
$valuequalifiedforstorage = false;
|
|
if (!is_object($value)) {
|
|
if (empty($value) && in_array($key, array('pos_source', 'module_source'))) {
|
|
$valuequalifiedforstorage = true; // We accept '' value for some fields
|
|
$value = (string) $value;
|
|
}
|
|
if (!is_null($value) && empty($value) && in_array($key, array('total_ht', 'total_tva', 'total_ttc', 'localtax1', 'localtax2', 'pos_print_counter', 'email_sent_counter'))) {
|
|
$valuequalifiedforstorage = true; // We accept zero value for amounts
|
|
}
|
|
if (!is_null($value) && (string) $value !== '') {
|
|
$valuequalifiedforstorage = true;
|
|
}
|
|
}
|
|
|
|
if ($valuequalifiedforstorage) {
|
|
$this->object_data->$key = $value;
|
|
}
|
|
}
|
|
|
|
if (!empty($object->newref)) {
|
|
$this->object_data->ref = $object->newref;
|
|
}
|
|
} elseif ($this->element == 'payment' || $this->element == 'payment_supplier' || $this->element == 'payment_donation' || $this->element == 'payment_various') {
|
|
'@phan-var-force Paiement|PaiementFourn|PaymentDonation|PaymentVarious $object';
|
|
$datepayment = $object->datepaye ? $object->datepaye : ($object->datepaid ? $object->datepaid : $object->datep);
|
|
$paymenttypeid = $object->paiementid ? $object->paiementid : ($object->paymenttype ? $object->paymenttype : $object->type_payment);
|
|
|
|
$this->object_data->ref = $object->ref;
|
|
$this->object_data->date = $datepayment;
|
|
$this->object_data->type_code = dol_getIdFromCode($this->db, $paymenttypeid, 'c_paiement', 'id', 'code');
|
|
|
|
if (!empty($object->num_payment)) {
|
|
$this->object_data->payment_num = $object->num_payment;
|
|
}
|
|
if (!empty($object->note_private)) {
|
|
$this->object_data->note_private = $object->note_private;
|
|
}
|
|
//$this->object_data->fk_account = $object->fk_account;
|
|
//var_dump($this->object_data);exit;
|
|
|
|
$totalamount = 0;
|
|
|
|
$this->type_code = $this->object_data->type_code;
|
|
$this->linktype = $this->element;
|
|
$this->linktoref = '';
|
|
|
|
// If payment and $object->amounts is empty (for example when we delete), we complete the information
|
|
if ($this->element == 'payment' && empty($object->amounts) && $object instanceof Paiement) {
|
|
$amountsarray = $object->getAmountsArray();
|
|
$object->amounts = $amountsarray;
|
|
// Invert the sign of amount into the array ->amounts if it is a deletion
|
|
if ($action == 'PAYMENT_CUSTOMER_DELETE') {
|
|
foreach ($object->amounts as $amountkey => $amountval) {
|
|
$object->amounts[$amountkey] = - $amountval;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Loop on each invoice payment amount (the payment_part)
|
|
if (is_array($object->amounts) && !empty($object->amounts)) {
|
|
// Loop on each invoice the payment is part of to set the linktoref and the module_source and pos_source
|
|
$originofpayment = null;
|
|
$terminalofpayment = null;
|
|
$paymentpartnumber = 0;
|
|
foreach ($object->amounts as $objid => $amount) {
|
|
if (empty($amount)) {
|
|
continue;
|
|
}
|
|
|
|
$totalamount += $amount;
|
|
|
|
$tmpobject = null;
|
|
if ($this->element == 'payment_supplier') {
|
|
include_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.facture.class.php';
|
|
$tmpobject = new FactureFournisseur($this->db);
|
|
} elseif ($this->element == 'payment') {
|
|
include_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
|
|
$tmpobject = new Facture($this->db);
|
|
} elseif ($this->element == 'payment_donation') {
|
|
include_once DOL_DOCUMENT_ROOT.'/don/class/don.class.php';
|
|
$tmpobject = new Don($this->db);
|
|
} elseif ($this->element == 'payment_various') {
|
|
include_once DOL_DOCUMENT_ROOT.'/compta/bank/class/paymentvarious.class.php';
|
|
$tmpobject = new PaymentVarious($this->db);
|
|
}
|
|
|
|
if (!is_object($tmpobject)) {
|
|
continue;
|
|
}
|
|
|
|
$result = $tmpobject->fetch($objid);
|
|
|
|
if ($result <= 0) {
|
|
$this->error = $tmpobject->error;
|
|
$this->errors = $tmpobject->errors;
|
|
dol_syslog("Failed to fetch object with id ".$objid, LOG_ERR);
|
|
return -1;
|
|
}
|
|
|
|
$this->linktoref .= ($this->linktoref ? ',' : '').$tmpobject->ref;
|
|
|
|
// Set the ->module_source of payment from origin object if relevant
|
|
if (property_exists($tmpobject, 'module_source')) {
|
|
if (is_null($originofpayment)) {
|
|
$originofpayment = (string) $tmpobject->module_source;
|
|
} elseif ($originofpayment != $tmpobject->module_source) {
|
|
$originofpayment = 'mix'; // the payment is on several invoices with different origins of module
|
|
} else {
|
|
$originofpayment = (string) $tmpobject->module_source;
|
|
}
|
|
}
|
|
// Set the ->pos_source of payment from origin object if relevant
|
|
if (property_exists($tmpobject, 'pos_source')) {
|
|
if (is_null($terminalofpayment)) {
|
|
$terminalofpayment = (string) $tmpobject->pos_source;
|
|
} elseif ($terminalofpayment != $tmpobject->pos_source) {
|
|
$terminalofpayment = 'mix'; // the payment is on several invoices with same origin of module but different terminals
|
|
} else {
|
|
$terminalofpayment = (string) $tmpobject->pos_source;
|
|
}
|
|
}
|
|
$paymentpart = new stdClass();
|
|
$paymentpart->amount = $amount;
|
|
|
|
// If we want to add thirdparty on each payment level
|
|
// (seems not necessary as we have one thirdparty per payment on invoice level)
|
|
if ($addthirdpartyatpaymentlevel) {
|
|
$result = $tmpobject->fetch_thirdparty();
|
|
if ($result == 0) {
|
|
$this->error = 'Failed to fetch thirdparty for object with id '.$tmpobject->id;
|
|
$this->errors[] = $this->error;
|
|
dol_syslog("Failed to fetch thirdparty for object with id ".$tmpobject->id, LOG_ERR);
|
|
return -1;
|
|
} elseif ($result < 0) {
|
|
$this->error = $tmpobject->error;
|
|
$this->errors = $tmpobject->errors;
|
|
return -1;
|
|
}
|
|
|
|
$paymentpart->thirdparty = new stdClass();
|
|
foreach ($tmpobject->thirdparty as $key => $value) {
|
|
if (in_array($key, $arrayoffieldstoexclude)) {
|
|
continue; // Discard some properties
|
|
}
|
|
// List of thirdparty fields qualified
|
|
if (!in_array($key, array(
|
|
'name', 'name_alias', 'ref_ext', 'address', 'zip', 'town', 'state_code', 'country_code', 'idprof1', 'idprof2', 'idprof3', 'idprof4', 'idprof5', 'idprof6', 'phone', 'fax', 'email', 'barcode',
|
|
'tva_intra', 'tva_assuj', 'localtax1_assuj', 'localtax1_value', 'localtax2_assuj', 'localtax2_value', 'managers', 'capital', 'typent_code', 'forme_juridique_code', 'code_client', 'code_fournisseur'
|
|
))) {
|
|
continue; // Discard if not into a dedicated list
|
|
}
|
|
if (!is_object($value) && !is_null($value) && $value !== '') {
|
|
$paymentpart->thirdparty->$key = $value;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Init object to avoid warnings
|
|
if ($this->element == 'payment_donation') {
|
|
$paymentpart->donation = new stdClass();
|
|
} elseif ($this->element == 'payment_various') {
|
|
$paymentpart->various = new stdClass();
|
|
} else {
|
|
$paymentpart->invoice = new stdClass();
|
|
}
|
|
|
|
if ($this->element != 'payment_various') {
|
|
foreach ($tmpobject as $key => $value) {
|
|
if (in_array($key, $arrayoffieldstoexclude)) {
|
|
continue; // Discard some properties
|
|
}
|
|
// List of fields qualified
|
|
if (!in_array($key, array(
|
|
'ref', 'ref_client', 'ref_supplier', 'date', 'datef', 'type', 'total_ht', 'total_tva', 'total_ttc', 'localtax1', 'localtax2', 'revenuestamp', 'datepointoftax', 'note_public',
|
|
'pos_source', 'module_source', 'pos_print_counter', 'email_sent_counter'
|
|
))) {
|
|
continue; // Discard if not into a dedicated list
|
|
}
|
|
|
|
$valuequalifiedforstorage = false;
|
|
if (!is_object($value)) {
|
|
if (empty($value) && in_array($key, array('pos_source', 'module_source'))) {
|
|
$valuequalifiedforstorage = true; // We accept '' value for some fields
|
|
$value = (string) $value;
|
|
}
|
|
if (!is_null($value) && empty($value) && in_array($key, array('total_ht', 'total_tva', 'total_ttc', 'localtax1', 'localtax2', 'pos_print_counter', 'email_sent_counter'))) {
|
|
$valuequalifiedforstorage = true; // We accept zero value for amounts
|
|
}
|
|
if (!is_null($value) && (string) $value !== '') {
|
|
$valuequalifiedforstorage = true;
|
|
}
|
|
}
|
|
|
|
if ($valuequalifiedforstorage) {
|
|
if ($this->element == 'payment_donation') {
|
|
$paymentpart->donation->$key = $value;
|
|
} elseif ($this->element == 'payment_various') {
|
|
$paymentpart->various->$key = $value;
|
|
} else {
|
|
$paymentpart->invoice->$key = $value;
|
|
}
|
|
}
|
|
}
|
|
|
|
$paymentpartnumber++; // first payment will be 1
|
|
$this->object_data->payment_part[$paymentpartnumber] = $paymentpart;
|
|
}
|
|
}
|
|
|
|
$this->module_source = (string) $originofpayment;
|
|
$this->pos_source = (string) $terminalofpayment;
|
|
} elseif (!empty($object->amount)) {
|
|
$totalamount = $object->amount;
|
|
}
|
|
|
|
$this->object_data->amount = $totalamount;
|
|
|
|
if (!empty($object->newref)) {
|
|
$this->object_data->ref = $object->newref;
|
|
}
|
|
} elseif ($this->element == 'payment_salary') {
|
|
'@phan-var-force PaymentSalary $object';
|
|
$this->object_data->amounts = array($object->amount);
|
|
|
|
if (!empty($object->newref)) {
|
|
$this->object_data->ref = $object->newref;
|
|
}
|
|
} elseif ($this->element == 'subscription') {
|
|
'@phan-var-force Subscription $object';
|
|
foreach ($object as $key => $value) {
|
|
if (in_array($key, $arrayoffieldstoexclude)) {
|
|
continue; // Discard some properties
|
|
}
|
|
if (!in_array($key, array(
|
|
'id', 'datec', 'dateh', 'datef', 'fk_adherent', 'amount', 'import_key', 'statut', 'note'
|
|
))) {
|
|
continue; // Discard if not into a dedicated list
|
|
}
|
|
if (!is_object($value) && !is_null($value) && $value !== '') {
|
|
$this->object_data->$key = $value;
|
|
}
|
|
}
|
|
|
|
if (!empty($object->newref)) {
|
|
$this->object_data->ref = $object->newref;
|
|
}
|
|
} elseif ($this->element == 'stockmouvement') {
|
|
'@phan-var-force StockTransfer $object';
|
|
foreach ($object as $key => $value) {
|
|
if (in_array($key, $arrayoffieldstoexclude)) {
|
|
continue; // Discard some properties
|
|
}
|
|
if (!is_object($value) && !is_null($value) && $value !== '') {
|
|
$this->object_data->$key = $value;
|
|
}
|
|
}
|
|
} else {
|
|
if ($object->element == 'cashcontrol') {
|
|
$this->module_source = (string) $object->posmodule; // Module
|
|
$this->pos_source = (string) $object->posnumber; // Terminal
|
|
}
|
|
|
|
// Generic case
|
|
foreach ($object as $key => $value) {
|
|
if (in_array($key, $arrayoffieldstoexclude)) {
|
|
continue; // Discard some properties
|
|
}
|
|
if (!is_object($value) && !is_null($value) && $value !== '') {
|
|
$this->object_data->$key = $value;
|
|
}
|
|
}
|
|
|
|
if ($object->element == 'cashcontrol') {
|
|
$period = $object->year_close;
|
|
$period .= ($object->month_close ? "-".sprintf("%02d", $object->month_close) : "");
|
|
$period .= ($object->day_close ? "-".sprintf("%02d", $object->day_close) : "");
|
|
|
|
$this->object_data->period = $period;
|
|
}
|
|
|
|
if (!empty($object->newref)) {
|
|
$this->object_data->ref = $object->newref;
|
|
}
|
|
}
|
|
|
|
// A trick to be sure all the object_data is an associative array
|
|
// json_encode and json_decode are not able to manage mixed object (with array/object, only full arrays or full objects)
|
|
$this->object_data = json_decode(json_encode($this->object_data, JSON_FORCE_OBJECT), false);
|
|
|
|
return 1;
|
|
}
|
|
|
|
/**
|
|
* Get object from database
|
|
*
|
|
* @param int $id Id of object to load
|
|
* @return int<-1,1> >0 if OK, <0 if KO, 0 if not found
|
|
*/
|
|
public function fetch($id)
|
|
{
|
|
global $langs;
|
|
|
|
if (empty($id)) {
|
|
$this->error = 'BadParameter';
|
|
return -1;
|
|
}
|
|
|
|
$sql = "SELECT b.rowid, b.date_creation, b.action, b.module_source, b.pos_source, b.amounts_taxexcl, b.amounts, b.element, b.fk_object, b.entity,";
|
|
$sql .= " b.certified, b.tms, b.fk_user, b.user_fullname, b.date_object, b.ref_object, b.type_code, b.linktoref, b.linktype, b.object_data, b.object_version, b.object_format, b.signature,";
|
|
$sql .= " b.note";
|
|
$sql .= " FROM ".MAIN_DB_PREFIX."blockedlog as b";
|
|
$sql .= " WHERE b.rowid = ".((int) $id); // $id is not empty because of test above
|
|
|
|
|
|
$resql = $this->db->query($sql);
|
|
if ($resql) {
|
|
$obj = $this->db->fetch_object($resql);
|
|
if ($obj) {
|
|
$this->id = $obj->rowid;
|
|
$this->entity = $obj->entity;
|
|
|
|
// Must be at top
|
|
$tz = 'gmt';
|
|
if (empty($obj->object_format) || $obj->object_format == 'V1') {
|
|
$tz = 'tzserver';
|
|
}
|
|
|
|
$this->date_creation = $this->db->jdate($obj->date_creation, $tz); // jdate(date_creation)is UTC
|
|
// @phan-suppress-next-line PhanPluginSuspiciousParamOrder
|
|
$this->date_modification = $this->db->jdate($obj->tms, $tz); // jdate(tms) is UTC
|
|
|
|
|
|
$this->action = $obj->action;
|
|
$this->module_source = $obj->module_source;
|
|
$this->pos_source = $obj->pos_source;
|
|
|
|
$this->amounts_taxexcl = (is_null($obj->amounts_taxexcl) ? null : (float) $obj->amounts_taxexcl);
|
|
$this->amounts = (float) $obj->amounts;
|
|
|
|
$this->fk_object = $obj->fk_object;
|
|
$this->date_object = $this->db->jdate($obj->date_object, $tz); // jdate(date_object) is UTC
|
|
//var_dump($obj->date_object, dol_print_date($this->date_object, 'dayhour' , $tz));
|
|
//exit;
|
|
|
|
$this->ref_object = $obj->ref_object;
|
|
$this->type_code = $obj->type_code;
|
|
$this->linktoref = $obj->linktoref;
|
|
$this->linktype = $obj->linktype;
|
|
|
|
$this->fk_user = $obj->fk_user;
|
|
$this->user_fullname = $obj->user_fullname;
|
|
|
|
$this->object_data = $this->dolDecodeBlockedData($obj->object_data);
|
|
$this->object_version = $obj->object_version;
|
|
$this->object_format = $obj->object_format;
|
|
|
|
$this->element = $obj->element;
|
|
|
|
$this->signature = $obj->signature;
|
|
$this->certified = ($obj->certified == 1);
|
|
|
|
$this->note = $obj->note;
|
|
//$this->debuginfo = $obj->debuginfo; // We don't need this, sot we don't load it to save memory.
|
|
|
|
return 1;
|
|
} else {
|
|
$langs->load("errors");
|
|
$this->error = $langs->trans("ErrorRecordNotFound");
|
|
return 0;
|
|
}
|
|
} else {
|
|
$this->error = $this->db->error();
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Encode data
|
|
*
|
|
* @param ?stdClass $data Data to serialize
|
|
* @param int<0,1> $mode 0=serialize, 1=json_encode
|
|
* @return string Value serialized, an object (stdClass).
|
|
*/
|
|
public function dolEncodeBlockedData($data, $mode = 1)
|
|
{
|
|
$aaa = json_encode($data);
|
|
|
|
return $aaa;
|
|
}
|
|
|
|
|
|
/**
|
|
* Decode data
|
|
*
|
|
* @param string $data Data to unserialize
|
|
* @param int $mode 0=unserialize, 1=json_decode
|
|
* @return Object Value unserialized, an object (stdClass)
|
|
*/
|
|
public function dolDecodeBlockedData($data, $mode = 0)
|
|
{
|
|
$aaa = null;
|
|
try {
|
|
$aaa = (object) jsonOrUnserialize($data, false);
|
|
} catch (Exception $e) {
|
|
// print $e->getErrs);
|
|
}
|
|
|
|
return $aaa;
|
|
}
|
|
|
|
|
|
/**
|
|
* Set block certified by an external authority
|
|
*
|
|
* @return boolean
|
|
*/
|
|
public function setCertified()
|
|
{
|
|
$res = $this->db->query("UPDATE ".MAIN_DB_PREFIX."blockedlog SET certified = 1 WHERE rowid = ".((int) $this->id));
|
|
if (!$res) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Create blocked log in database.
|
|
*
|
|
* @param User $user Object user that create
|
|
* @param string $forcesignature Force signature (for example '0000000000' when we disabled the module, to force a non valid record, for test purpose for example)
|
|
* @return int<-3,-1>|int<1,1> Return integer <0 if KO, >0 if OK
|
|
*/
|
|
public function create($user, $forcesignature = '')
|
|
{
|
|
global $conf, $langs, $mysoc;
|
|
|
|
$langs->load('blockedlog');
|
|
|
|
// Clean data
|
|
$this->amounts = (float) $this->amounts;
|
|
|
|
dol_syslog(get_class($this).'::create action='.$this->action.' fk_user='.$this->fk_user.' user_fullname='.$this->user_fullname, LOG_DEBUG);
|
|
|
|
// Check parameters/properties
|
|
if (!isset($this->amounts)) { // amount can be 0 for some events (like when module is disabled)
|
|
$langs->load("errors");
|
|
$this->error = $langs->trans("ErrorBlockLogNeedAmountsValue");
|
|
dol_syslog($this->error, LOG_WARNING);
|
|
return -1;
|
|
}
|
|
|
|
if (empty($this->element)) {
|
|
$langs->load("errors");
|
|
$this->error = $langs->trans("ErrorBlockLogNeedElement");
|
|
dol_syslog($this->error, LOG_WARNING);
|
|
return -2;
|
|
}
|
|
|
|
if (empty($this->object_data)) {
|
|
$langs->load("errors");
|
|
$this->error = $langs->trans("ErrorBlockLogNeedObject");
|
|
dol_syslog($this->error, LOG_WARNING);
|
|
return -2;
|
|
}
|
|
|
|
if (empty($this->action)) {
|
|
$langs->load("errors");
|
|
$this->error = $langs->trans("ErrorBadParameterWhenCallingCreateOfBlockedLog");
|
|
dol_syslog($this->error, LOG_WARNING);
|
|
return -3;
|
|
}
|
|
if (empty($this->fk_user)) {
|
|
$this->user_fullname = '(Anonymous)';
|
|
}
|
|
|
|
include_once DOL_DOCUMENT_ROOT.'/core/lib/security.lib.php';
|
|
|
|
$this->db->begin();
|
|
|
|
$this->date_creation = dol_now();
|
|
|
|
$this->object_version = DOL_VERSION;
|
|
|
|
// The object_format defines the formatting rules and syntax into
|
|
// buildKeyForSignature and buildFirstPartOfKeyForSignature and buildFinalSignatureHash
|
|
// This may vary when the Immutable Log module version is modified, but only if algorithm has changed.
|
|
$this->object_format = 'V2';
|
|
|
|
$tz = 'gmt';
|
|
|
|
$previoushash = '';
|
|
$previousid = 0;
|
|
$previousdatecreation = 0;
|
|
|
|
try {
|
|
$tmparray = $this->getPreviousHash(1, 0); // This get last record and lock database until insert is done and transaction closed
|
|
|
|
$previoushash = $tmparray['previoushash'];
|
|
$previousid = $tmparray['previousid'];
|
|
$previousdatecreation = $tmparray['previousdatecreation'];
|
|
|
|
// The string of line to sign
|
|
$concatenateddata = $this->buildKeyForSignature(); // All the information for the hash (meta data + data saved)
|
|
|
|
// The new hash, including previous hash
|
|
$this->signature = $this->buildFinalSignatureHash($previoushash.$concatenateddata); // Build the hmac signature
|
|
|
|
// For debug info (we can clean this field later)
|
|
if (getDolGlobalString('BLOCKEDLOG_ADD_DEBUG_INFO')) {
|
|
$this->debuginfo = 'previoushash='.$previoushash.' concatenateddatafirstpart='.$this->buildFirstPartOfKeyForSignature().' => signature='.$this->signature; // Not used
|
|
}
|
|
} catch (Exception $e) {
|
|
$this->error = $e->getMessage();
|
|
|
|
dol_syslog($this->error, LOG_ERR);
|
|
|
|
$this->db->rollback();
|
|
return -1;
|
|
}
|
|
|
|
if ($forcesignature) {
|
|
$this->signature = $forcesignature;
|
|
}
|
|
//var_dump($previoushash, $concatenateddata, $this->signature);
|
|
|
|
$sql = "INSERT INTO ".MAIN_DB_PREFIX."blockedlog (";
|
|
$sql .= " date_creation,";
|
|
$sql .= " action,";
|
|
$sql .= " module_source,";
|
|
$sql .= " pos_source,";
|
|
$sql .= " amounts_taxexcl,";
|
|
$sql .= " amounts,";
|
|
$sql .= " signature,";
|
|
$sql .= " element,";
|
|
$sql .= " fk_object,";
|
|
$sql .= " date_object,";
|
|
$sql .= " ref_object,";
|
|
$sql .= " type_code,";
|
|
$sql .= " linktoref,";
|
|
$sql .= " linktype,";
|
|
$sql .= " object_data,";
|
|
$sql .= " object_version,";
|
|
$sql .= " object_format,";
|
|
$sql .= " certified,";
|
|
$sql .= " fk_user,";
|
|
$sql .= " user_fullname,";
|
|
$sql .= " entity,";
|
|
$sql .= " debuginfo"; // Only stored
|
|
$sql .= ") VALUES (";
|
|
$sql .= "'".$this->db->idate($this->date_creation, $tz)."',";
|
|
$sql .= "'".$this->db->escape($this->action)."',";
|
|
$sql .= "'".$this->db->escape((string) $this->module_source)."',";
|
|
$sql .= "'".$this->db->escape((string) $this->pos_source)."',";
|
|
$sql .= (is_null($this->amounts_taxexcl) ? "null" : (float) $this->amounts_taxexcl).",";
|
|
$sql .= (float) $this->amounts.",";
|
|
$sql .= "'".$this->db->escape($this->signature)."',";
|
|
$sql .= "'".$this->db->escape($this->element)."',";
|
|
$sql .= (int) $this->fk_object.",";
|
|
$sql .= "'".$this->db->idate($this->date_object, $tz)."',";
|
|
$sql .= "'".$this->db->escape($this->ref_object)."',";
|
|
$sql .= "'".$this->db->escape($this->type_code)."',";
|
|
$sql .= ($this->linktoref ? "'".$this->db->escape($this->linktoref)."'" : "null").",";
|
|
$sql .= ($this->linktype ? "'".$this->db->escape($this->linktype)."'" : "null").",";
|
|
$sql .= "'".$this->db->escape($this->dolEncodeBlockedData($this->object_data))."',";
|
|
$sql .= "'".$this->db->escape($this->object_version)."',";
|
|
$sql .= "'".$this->db->escape($this->object_format)."',";
|
|
$sql .= "0,";
|
|
$sql .= ((int) $this->fk_user).",";
|
|
$sql .= "'".$this->db->escape($this->user_fullname)."',";
|
|
$sql .= ((int) ($this->entity ? ((int) $this->entity) : ((int) $conf->entity))).",";
|
|
$sql .= "'".$this->db->escape($this->debuginfo)."'";
|
|
$sql .= ")";
|
|
|
|
/*
|
|
$a = serialize($this->object_data); $a2 = unserialize($a); $a4 = print_r($a2, true);
|
|
$b = json_encode($this->object_data); $b2 = json_decode($b); $b4 = print_r($b2, true);
|
|
var_dump($a4 == print_r($this->object_data, true) ? 'a=a' : 'a not = a');
|
|
var_dump($b4 == print_r($this->object_data, true) ? 'b=b' : 'b not = b');
|
|
exit;
|
|
*/
|
|
|
|
$res = $this->db->query($sql);
|
|
if ($res) {
|
|
$id = $this->db->last_insert_id(MAIN_DB_PREFIX."blockedlog");
|
|
|
|
if ($id > 0) {
|
|
// The new ID
|
|
$this->id = $id;
|
|
|
|
// Check and store the signature of this new line in the .end flag.
|
|
try {
|
|
$finalsignature = $this->signature;
|
|
$finalnote = '';
|
|
|
|
$lockfile = $this->getEndOfChainFlagFile();
|
|
|
|
// Load the .end flag.
|
|
// If not found (has been removed), we track the record as error.
|
|
if (defined('BLOCKEDLOG_END_FLAG_IN_A_FILE')) {
|
|
dol_mkdir(dirname($lockfile)); // Create at least directory for the lock file. Nothing if already exists.
|
|
|
|
if (!file_exists($lockfile)) {
|
|
//throw new Exception("The head file ".$lockfile." was not found or is not writable.");
|
|
|
|
$this->note = 'EndOfChainDeletionDetected [after '.dol_print_date($previousdatecreation, 'dayhourrfc', 'gmt').']';
|
|
|
|
// The string of line to sign
|
|
$concatenateddata = $this->buildKeyForSignature(); // All the information for the hash (meta data + data saved)
|
|
|
|
// The new hash, including previous hash
|
|
$finalsignature = $this->buildFinalSignatureHash($previoushash.$concatenateddata); // Build the hmac signature
|
|
$finalnote = $this->note;
|
|
|
|
$line = '';
|
|
} elseif (is_writable($lockfile)) {
|
|
$line = file_get_contents($lockfile);
|
|
} else {
|
|
// Go to the catch()
|
|
throw new Exception("Cannot write into the blockedlog .end flag ".$lockfile.' to update it. Is the file writable by the running user and not open by another process? Transaction aborted.');
|
|
}
|
|
} else {
|
|
$sql = "SELECT value from ".MAIN_DB_PREFIX."const";
|
|
$sql .= " WHERE name = '".$this->db->escape(basename($lockfile))."' AND entity = ".((int) $conf->entity);
|
|
$resql = $this->db->query($sql);
|
|
if ($resql) {
|
|
$obj = $this->db->fetch_object($resql);
|
|
if ($obj) {
|
|
$line = $obj->value;
|
|
} else {
|
|
//throw new Exception("The head file ".$lockfile." was not found or is not writable.");
|
|
|
|
$this->note = 'EndOfChainDeletionDetected [after '.dol_print_date($previousdatecreation, 'dayhourrfc', 'gmt').']';
|
|
|
|
// The string of line to sign
|
|
$concatenateddata = $this->buildKeyForSignature(); // All the information for the hash (meta data + data saved)
|
|
|
|
// The new hash, including previous hash
|
|
$finalsignature = $this->buildFinalSignatureHash($previoushash.$concatenateddata); // Build the hmac signature
|
|
$finalnote = $this->note;
|
|
|
|
$line = '';
|
|
}
|
|
} else {
|
|
// Go to the catch()
|
|
throw new Exception("Cannot read into the blockedlog .end flag ".basename($lockfile).' to update it. Transaction aborted.');
|
|
}
|
|
}
|
|
|
|
// Check the .end flag file.
|
|
$headstring = '';
|
|
$remoteobfuscationkey = '';
|
|
if (preg_match('/^dolcrypt/', $line)) { // Old method (does not happen after migration)
|
|
$headstring = dolDecrypt($line);
|
|
} elseif (preg_match('/^dolobfuscation/', $line)) {
|
|
$remoteobfuscationkey = $this->getObfuscationKey();
|
|
if (empty($remoteobfuscationkey)) {
|
|
throw new Exception("Failed to get the remote obfuscation key. We can't read the end of chain flag file so we abort the transaction.");
|
|
}
|
|
$headstring = dolDecrypt($line, $remoteobfuscationkey);
|
|
}
|
|
|
|
$reg = array();
|
|
if (preg_match('/^BLOCKEDLOGHEAD (\d+) ([^\s]+) ([a-zA-Z0-9\-]+)/', $headstring, $reg)) {
|
|
// We succeed in decypting the head
|
|
$previousidheadflag = $reg[1];
|
|
$previousdatecreationheadflag = $reg[2];
|
|
$previoushashheadflag = $reg[3];
|
|
|
|
// Check the signature of the previous line
|
|
if ($previousid < $previousidheadflag || $previoushash != $previoushashheadflag) {
|
|
// We detect that old record were removed. We force a non valid signature on the new record.
|
|
$this->note = 'EndOfChainDeletionDetected ['.dol_print_date($previousdatecreation, 'dayhourrfc', 'gmt').' - '.dol_print_date($previousdatecreationheadflag, 'dayhourrfc', 'gmt').']';
|
|
|
|
// The string of line to sign
|
|
$concatenateddata = $this->buildKeyForSignature(); // All the information for the hash (meta data + data saved)
|
|
|
|
// The new hash, including previous hash
|
|
$finalsignature = $this->buildFinalSignatureHash($previoushash.$concatenateddata); // Build the hmac signature
|
|
$finalnote = $this->note;
|
|
}
|
|
} elseif ($headstring != '') {
|
|
// Failed to decrypt the head
|
|
throw new Exception("Failed to decode the content of the .end flag ".basename($lockfile).", content = ".$line." (remote obfuscation key = ".$remoteobfuscationkey."), so we can't record the head file so we abort the transaction.");
|
|
}
|
|
|
|
// If a note has been added to track an anomaly (signature is also different in this case).
|
|
if ($finalsignature != $this->signature) {
|
|
// For debug info (we can clean this field later)
|
|
if (getDolGlobalString('BLOCKEDLOG_ADD_DEBUG_INFO')) {
|
|
$this->debuginfo = 'previoushash='.$previoushash.' concatenateddatafirstpart='.$this->buildFirstPartOfKeyForSignature().' => signature='.$this->signature; // Not used
|
|
}
|
|
|
|
// We update the record we have just inserted to record the new "anomaly" we have detected. Anomaly is also incrusted into the signature.
|
|
$sql = "UPDATE ".MAIN_DB_PREFIX."blockedlog";
|
|
$sql .= " SET signature = '".$this->db->escape($finalsignature)."',";
|
|
$sql .= " note = '".$this->db->escape($finalnote)."',";
|
|
$sql .= " debuginfo = '".$this->db->escape($this->debuginfo)."'";
|
|
$sql .= " WHERE rowid = ".((int) $this->id);
|
|
$resql = $this->db->query($sql);
|
|
if (!$resql) {
|
|
throw new Exception("End of chain deletion detected but we failed to update the signature of the record ".$this->id." to set the note and new signature ".$finalsignature." to track this.");
|
|
}
|
|
}
|
|
|
|
|
|
// We can now write the new .end file
|
|
$stringtowrite = 'BLOCKEDLOGHEAD '.$this->id." ".dol_print_date($this->date_creation, 'dayhourrfc', 'gmt')." ".(string) $finalsignature;
|
|
|
|
if (isALNERunningVersion(1, ($this->action == 'MODULE_SET' ? 1 : 0)) && $mysoc->country_code == 'FR') {
|
|
$remoteobfuscationkey = $this->getObfuscationKey();
|
|
if (empty($remoteobfuscationkey)) {
|
|
throw new Exception("Failed to get the remote obfuscation key. We can't record the end of chain flag file so we abort the transaction.");
|
|
}
|
|
$stringtowriteencoded = dolEncrypt($stringtowrite, $remoteobfuscationkey, '', '', 'dolobfuscationv1-'.$mysoc->idprof1.'-'.$this->id);
|
|
} else {
|
|
$stringtowriteencoded = dolEncrypt($stringtowrite, '', '', '', 'dolcrypt-'.$mysoc->idprof1.'-'.$this->id);
|
|
}
|
|
|
|
|
|
// Update or create the .end file.
|
|
if (defined('BLOCKEDLOG_END_FLAG_IN_A_FILE')) {
|
|
$lockhandle = fopen($lockfile, 'w+');
|
|
if ($lockhandle) {
|
|
if (fwrite($lockhandle, $stringtowriteencoded."\n") === false) {
|
|
throw new Exception("Cannot write to the blockedlog .end file ".$lockfile);
|
|
}
|
|
|
|
fclose($lockhandle); // Remove the lock
|
|
dolChmod($lockfile);
|
|
} else {
|
|
throw new Exception("Cannot open for writing the blockedlog .end file ".$lockfile);
|
|
}
|
|
} else {
|
|
$sql = "DELETE FROM ".MAIN_DB_PREFIX."const";
|
|
$sql .= " WHERE name = '".$this->db->escape(basename($lockfile))."' AND entity = ".((int) $conf->entity);
|
|
$resql = $this->db->query($sql);
|
|
|
|
$sql = "INSERT INTO ".MAIN_DB_PREFIX."const(name, value, type, visible, note, entity)";
|
|
$sql .= " VALUES('".$this->db->escape(basename($lockfile))."', '".$this->db->escape($stringtowriteencoded)."', 'chaine', 0, 'Blockedlog end of chain flag', ".((int) $conf->entity).")";
|
|
$resql = $this->db->query($sql);
|
|
if (!$resql) {
|
|
throw new Exception("Cannot update the blockedlog .end flag ".basename($lockfile));
|
|
}
|
|
}
|
|
} catch (Exception $e) {
|
|
$this->error = $e->getMessage();
|
|
|
|
dol_syslog($this->error, LOG_ERR);
|
|
|
|
$this->db->rollback();
|
|
return -1;
|
|
}
|
|
|
|
$this->db->commit();
|
|
|
|
// Call remote API service to record the last counter
|
|
/*
|
|
$error = 0;
|
|
|
|
include_once DOL_DOCUMENT_ROOT.'/blockedlog/lib/blockedlog.lib.php';
|
|
try {
|
|
$resultcall = callApiToPushCounter((int) $this->id, $this->signature, $this->date_creation, 0, (int) $previousid, $previoushash, $previousdatecreation);
|
|
} catch (Exception $e) {
|
|
$error++;
|
|
$this->error = $e->getMessage();
|
|
}
|
|
|
|
if (!$error) {
|
|
return $this->id;
|
|
} else {
|
|
return -3;
|
|
}
|
|
*/
|
|
return $this->id;
|
|
} else {
|
|
$this->db->rollback();
|
|
return -2;
|
|
}
|
|
} else {
|
|
$this->error = $this->db->error();
|
|
$this->db->rollback();
|
|
return -1;
|
|
}
|
|
|
|
// The commit or rollback will release the lock so app can insert other record now
|
|
}
|
|
|
|
/**
|
|
* Return path of end of chain flag file.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getEndOfChainFlagFile()
|
|
{
|
|
global $conf;
|
|
|
|
// Note: We must not use $conf->blockedlog->dir_output because we need this
|
|
// function to work even when module not yet enabled.
|
|
return DOL_DATA_ROOT.'/blockedlog/blockedlog-'.((int) $conf->entity).'.end';
|
|
}
|
|
|
|
/**
|
|
* Check if calculated signature still correct compared to the value in the chain
|
|
*
|
|
* @param string $previoushash If previous signature hash is known, we can provide it to avoid to make a search of it in database.
|
|
* @param int<0,2> $returnarray 1=Return array of details, 2=Return array of details including keyforsignature, 0=Return a boolean
|
|
* @return boolean|array{checkresult:bool,calculatedsignature:string,previoushash:string,keyforsignature?:string} Array or true if OK, false if KO
|
|
*/
|
|
public function checkSignature($previoushash = '', $returnarray = 0)
|
|
{
|
|
if (empty($previoushash)) {
|
|
$tmparray = $this->getPreviousHash(0, $this->id);
|
|
$previoushash = $tmparray['previoushash'];
|
|
}
|
|
|
|
$concatenateddata = '';
|
|
$signature = '';
|
|
|
|
// Recalculate the signature
|
|
try {
|
|
// Build the string for the signature
|
|
$concatenateddata = $this->buildKeyForSignature();
|
|
|
|
$signature = $this->buildFinalSignatureHash($previoushash.$concatenateddata);
|
|
|
|
//var_dump($previoushash, $concatenateddata, $this->object_format, $signature);
|
|
} catch (Exception $e) {
|
|
$res = ($signature === $this->signature);
|
|
$this->error = $e->getMessage();
|
|
|
|
dol_syslog($this->error, LOG_ERR);
|
|
|
|
if ($returnarray) {
|
|
return array('checkresult' => $res, 'calculatedsignature' => $signature, 'previoushash' => $previoushash, 'error' => $this->error);
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
$res = ($signature === $this->signature);
|
|
|
|
if (!$res) {
|
|
$this->error = 'Signature KO';
|
|
}
|
|
|
|
if ($returnarray) {
|
|
if ($returnarray == 1) {
|
|
unset($concatenateddata);
|
|
return array('checkresult' => $res, 'calculatedsignature' => $signature, 'previoushash' => $previoushash);
|
|
} else { // Consume much memory ($concatenateddata is a large var)
|
|
return array('checkresult' => $res, 'calculatedsignature' => $signature, 'previoushash' => $previoushash, 'keyforsignature' => $concatenateddata);
|
|
}
|
|
} else {
|
|
unset($concatenateddata);
|
|
return $res;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return first part of string for signature (clear data)
|
|
* Note: rowid of line not included as it is not a business data and this allow to make backup of a year
|
|
* and restore it into another database with different ids without comprimising checksums
|
|
*
|
|
* @param string $format Force format to use
|
|
* @return string First part of key for signature
|
|
*/
|
|
private function buildFirstPartOfKeyForSignature($format = '')
|
|
{
|
|
if (empty($format)) {
|
|
$format = $this->object_format;
|
|
}
|
|
|
|
// Note: $this->amounts can be '0', '1.1', '1.123'; // All 0 at end should have been removed already
|
|
if ($format == '') {
|
|
return $this->date_creation.'|'.$this->action.'|'.$this->amounts.'|'.$this->ref_object.'|'.$this->date_object.'|'.$this->user_fullname;
|
|
} elseif ($format == 'V1') { // Note: $this->amounts can be '0', '1.1', '1.123'; // All 0 at end should have been removed already
|
|
return $this->date_creation.'|'.$this->action.'|'.$this->amounts.'|'.$this->ref_object.'|'.$this->date_object.'|'.$this->user_fullname;
|
|
} elseif ($format == 'V2') {
|
|
$s = $this->entity;
|
|
$s .= '|'.$this->date_creation.'|'.$this->action.'|'.$this->module_source.'|'.$this->pos_source.'|'.$this->amounts_taxexcl;
|
|
$s .= '|'.$this->amounts.'|'.$this->ref_object.'|'.$this->date_object.'|'.$this->user_fullname;
|
|
if ($this->type_code) {
|
|
$s .= '|'.(string) $this->type_code;
|
|
}
|
|
$s .= '|'.(string) $this->linktoref;
|
|
$s .= '|'.(string) $this->linktype;
|
|
if ($this->note) {
|
|
$s .= '|'.(string) $this->note;
|
|
}
|
|
return $s;
|
|
} else {
|
|
throw new Exception('Error bad value "'.$this->object_format.'" for object_format');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return the string for signature (clear data).
|
|
*
|
|
* @param string $format Force format to use
|
|
* @return string Key for signature
|
|
*/
|
|
public function buildKeyForSignature($format = '')
|
|
{
|
|
//print_r($this->object_data);
|
|
if (empty($format)) {
|
|
$format = $this->object_format;
|
|
}
|
|
|
|
if ($format == '') {
|
|
return $this->buildFirstPartOfKeyForSignature($format).'|'.print_r($this->object_data, true);
|
|
} elseif ($format == 'V1') { // Note: $this->amounts can be '0', '1.1', '1.123'; // All 0 at end should have been removed already
|
|
return $this->buildFirstPartOfKeyForSignature($format).'|'.json_encode($this->object_data, JSON_FORCE_OBJECT);
|
|
} elseif ($format == 'V2') {
|
|
return $this->buildFirstPartOfKeyForSignature($format).'|'.json_encode($this->object_data, JSON_FORCE_OBJECT);
|
|
} else {
|
|
throw new Exception('Error bad value "'.$format.'" for object_format');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return a hash that is the signature of a line data $clearstring (hash_hmac SHA256 of data + secret key)
|
|
*
|
|
* @param string $clearstring Data string to sign
|
|
* @param string $format Force encryption format version to use ('V1', 'V2', ...)
|
|
* @return string Signature string
|
|
*/
|
|
private function buildFinalSignatureHash($clearstring, $format = '')
|
|
{
|
|
global $mysoc;
|
|
|
|
if (empty($format)) {
|
|
$format = $this->object_format;
|
|
}
|
|
|
|
if ($format == '') {
|
|
return dol_hash($clearstring, '5');
|
|
} elseif ($format == 'V1') {
|
|
return dol_hash($clearstring, '5');
|
|
} elseif ($format == 'V2') {
|
|
// BLOCKEDLOG_HMAC_KEY is a HMAC key starting with 'BLOCKEDLOGHMAC....'. It is not stored as a clear data but
|
|
// is a string dolcrypt:... or dolobfuscationv1... It will be decrypted later.
|
|
$hmac_encoded_secret_key = $this->getEncodedHMACSecretKey();
|
|
|
|
if (empty($hmac_encoded_secret_key)) {
|
|
throw new Exception('Error: BLOCKEDLOG_HMAC_KEY was not found. It should have been initialized to a value "BLOCKEDLOG_HMAC_...." during initialization of module BlockedLog or during migration of an old version');
|
|
}
|
|
|
|
// Here we have the obfuscated value of BLOCKEDLOG_HMAC_KEY in $hmac_encoded_secret_key. We need to unobfuscate it.
|
|
$hmac_secret_key = '';
|
|
try {
|
|
$hmac_secret_key = $this->getClearHMACSecretKey($hmac_encoded_secret_key); // Note: On network trouble, an Exception is thrown to the caller
|
|
} catch (Exception $e) {
|
|
$firsterrormessage = $e->getMessage();
|
|
|
|
// Another chance to get HMAC when saved with old obfuscation method (dolcrypt)
|
|
$hmac_encoded_secret_key_alt = $this->getEncodedHMACSecretKey(1, 1);
|
|
if (!empty($hmac_encoded_secret_key_alt)) {
|
|
try {
|
|
$hmac_secret_key_alt = $this->getClearHMACSecretKey($hmac_encoded_secret_key_alt); // Note: On network trouble, an Exception is thrown to the caller
|
|
if (preg_match('/^BLOCKEDLOGHMAC/', (string) $hmac_secret_key_alt)) { // Alternative is ok
|
|
$hmac_secret_key = $hmac_secret_key_alt;
|
|
}
|
|
} catch (Exception $e) {
|
|
throw new Exception($firsterrormessage);
|
|
}
|
|
} else {
|
|
throw new Exception($firsterrormessage);
|
|
}
|
|
}
|
|
|
|
// Last check on validity of key
|
|
if (!preg_match('/^BLOCKEDLOGHMAC/', (string) $hmac_secret_key)) {
|
|
//throw new Exception('Error: Failed to decode the crypted value of the parameter BLOCKEDLOG_HMAC_KEY using the obfuscation key. A value was found but decoding failed. May be the database data were restored onto another environment and the coding/decoding key $dolibarr_main_dolcrypt_key or $dolibarr_main_instance_unique_id was not restored with the same value in conf.php file.');
|
|
throw new Exception('buildFinalSignatureHash Error: Failed to decode the crypted value of the parameter BLOCKEDLOG_HMAC_KEY '.$hmac_encoded_secret_key.' using the obfuscation key. A value was found in database but decoding failed. May be you modified the SIREN used to get the obfuscation key from ping.dolibarr.org (or old config key $dolibarr_main_instance_unique_id).');
|
|
}
|
|
|
|
// Here the $hmac_secret_key is in memory with the correct value.
|
|
|
|
// On old versions, we must switch the data saving mode to use the new method.
|
|
// Live migration of the way the key is stored.
|
|
$needremoteobfuscation = (isALNERunningVersion(1) && $mysoc->country_code == 'FR');
|
|
|
|
if ($needremoteobfuscation && !preg_match('/^dolobfuscationv1/', $hmac_encoded_secret_key)) { // For old versions, we must switch the data saving mode to use the new method.
|
|
$obfuscationkey = '';
|
|
$errormsg = '';
|
|
try {
|
|
$obfuscationkey = $this->getObfuscationKey(); // Get obfuscation key providing $mysoc->idprof1 and $registrationnumber. Note: On network trouble, an Exception is thrown to the caller
|
|
} catch (Exception $e) {
|
|
$errormsg = $e->getMessage();
|
|
}
|
|
if (!$errormsg && $obfuscationkey) {
|
|
$this->saveHMACSecretKey((string) $hmac_secret_key, 'dolobfuscationv1-'.$mysoc->idprof1, $obfuscationkey); // gitleaks:allow
|
|
}
|
|
} elseif (!$needremoteobfuscation && !preg_match('/^dolcrypt/', $hmac_encoded_secret_key)) {
|
|
$this->saveHMACSecretKey((string) $hmac_secret_key, 'dolcrypt'); // gitleaks:allow
|
|
}
|
|
|
|
// Here HMAC secret key is a long string starting with BLOCKEDLOGHMAC..., we can use it to sign the data.
|
|
return hash_hmac('sha256', $clearstring, $hmac_secret_key);
|
|
} else {
|
|
throw new Exception('Error bad value "'.$this->object_format.'" for object_format');
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Save the HMAC secret key into database.
|
|
*
|
|
* @param string $hmac_secret_key HMAC secret key ('BLOCKEDLOG_HMAC_KEY...')
|
|
* @param string $obfuscationmode Obfuscation mode ('dolcrypt', 'dolobfuscationv1-SIREN')
|
|
* @param string $obfuscationkey Obfuscation key
|
|
* @return int Return <0 if KO, >0 if OK
|
|
*/
|
|
public function saveHMACSecretKey($hmac_secret_key, $obfuscationmode, $obfuscationkey = '')
|
|
{
|
|
global $conf;
|
|
|
|
//var_dump($hmac_secret_key, $obfuscationmode, $obfuscationkey);exit;
|
|
|
|
if (preg_match('/^dolobfuscationv1/', $obfuscationmode) && empty($obfuscationkey)) {
|
|
return -1;
|
|
}
|
|
|
|
$name = 'BLOCKEDLOG_HMAC_KEY'; // The name of the key to save in database. $hmac_secret_key is the value to save and $obfuscationkey the key to obfuscate the value.
|
|
|
|
$this->db->begin();
|
|
|
|
$sql = "DELETE FROM ".MAIN_DB_PREFIX."const";
|
|
$sql .= " WHERE name = '".$this->db->escape($name)."'";
|
|
if ($this->entity >= 0) {
|
|
$sql .= " AND entity = ".((int) $this->entity);
|
|
}
|
|
|
|
dol_syslog("saveHMACSecretKey", LOG_DEBUG);
|
|
|
|
$resql = $this->db->query($sql);
|
|
|
|
if (preg_match('/^dolobfuscationv1/', $obfuscationmode)) {
|
|
$newvalue = dolEncrypt($hmac_secret_key, $obfuscationkey, '', '', $obfuscationmode); // AES-256
|
|
} else {
|
|
// if ($obfuscationmode == 'dolcrypt')
|
|
$newvalue = dolEncrypt($hmac_secret_key); // AES-256
|
|
}
|
|
|
|
// Clear cache
|
|
unset($conf->cache['hmac_encoded_secret_key_'.((int) $this->entity)]);
|
|
|
|
// Save in database
|
|
$sql = "INSERT INTO ".MAIN_DB_PREFIX."const(name, value, type, visible, note, entity)";
|
|
$sql .= " VALUES (";
|
|
$sql .= "'".$this->db->escape($name)."'";
|
|
$sql .= ", '".$this->db->escape($newvalue)."'";
|
|
$sql .= ", 'chaine', '0', '', ".((int) $this->entity).")";
|
|
|
|
//print "xx".$db->escape($value);
|
|
$resql = $this->db->query($sql);
|
|
|
|
if ($resql) {
|
|
// Now reload it to check it was saved correctly for a paranoiac control.
|
|
$saved = $this->getEncodedHMACSecretKey(); // This also reload the cache
|
|
|
|
if ($saved == $newvalue) {
|
|
$this->db->commit();
|
|
|
|
return 1;
|
|
} else {
|
|
$this->error = 'Data read of HMAC key is not same than the one we expect to save.';
|
|
$this->db->rollback();
|
|
|
|
return -1;
|
|
}
|
|
} else {
|
|
$this->error = $this->db->lasterror();
|
|
$this->db->rollback();
|
|
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Return the remote obfuscation key from ping.dolibarr.org (used later to decode HMAC secret key).
|
|
* Use a memory cache to avoid repeated db access.
|
|
* This function can also be called just to store the remote obfuscation key into the cache so all next call will not depends on the obfuscation key server availability.
|
|
* Note: Avoid to call this function if you are not in acontext that need remote obfuscation key.
|
|
*
|
|
* @return string Obfuscation key or a coma-separated list of obfuscation keys, or "" if not found.
|
|
*/
|
|
public function getObfuscationKey()
|
|
{
|
|
global $conf, $mysoc;
|
|
|
|
|
|
// Uncomment the next line to emulate a network error to get the remote obfuscation key
|
|
//throw new Exception('Failed to get the remote obfuscation key - error emulated');
|
|
|
|
// If key found into the user session memory cache, we use it
|
|
if (!empty($_SESSION['obfuscationkey_'.((int) $this->entity)])) {
|
|
dol_syslog("getObfuscationKey remote obfuscation key found into session cache", LOG_DEBUG);
|
|
return (string) $_SESSION['obfuscationkey_'.((int) $this->entity)];
|
|
}
|
|
// If key found into the page memory cache, we use it
|
|
if (!empty($conf->cache['obfuscationkey_'.((int) $this->entity)])) {
|
|
dol_syslog("getObfuscationKey remote obfuscation key found into conf->cache", LOG_DEBUG);
|
|
return (string) $conf->cache['obfuscationkey_'.((int) $this->entity)];
|
|
}
|
|
|
|
$obfuscationkey = '';
|
|
|
|
include_once DOL_DOCUMENT_ROOT.'/blockedlog/lib/blockedlog.lib.php';
|
|
$registrationnumber = getHashUniqueIdOfRegistration();
|
|
|
|
// Value is not into cache, we must get it from ping.dolibarr.org
|
|
$obfuscationkey = callApiToGetObfuscationKey((string) $mysoc->idprof1, $registrationnumber);
|
|
if (empty($obfuscationkey)) {
|
|
dol_syslog("getObfuscationKey Failed to get the obfuscation key from ping.dolibarr.org (country='.$mysoc->country_code.', SIREN='.$mysoc->idprof1.'). May be the SIREN is not valid, the ping.dolibarr.org server is down or registration was not done (empty value returned). Re-try later.", LOG_DEBUG);
|
|
throw new Exception('Error: Failed to get the obfuscation key from ping.dolibarr.org (country='.$mysoc->country_code.', SIREN='.$mysoc->idprof1.'). May be the SIREN is not valid, the ping.dolibarr.org server is down or registration was not done (empty value returned). Re-try later.');
|
|
}
|
|
if (strpos($obfuscationkey, 'ERROR') === 0) {
|
|
dol_syslog('getObfuscationKey Error: Failed to get the obfuscation key from ping.dolibarr.org. May be the SIREN is not valid, the ping.dolibarr.org server is down or registration was not done (bad value returned). Re-try later. '.$obfuscationkey, LOG_DEBUG);
|
|
throw new Exception('Error: Failed to get the obfuscation key from ping.dolibarr.org. May be the SIREN is not valid, the ping.dolibarr.org server is down or registration was not done (bad value returned). Re-try later. '.$obfuscationkey);
|
|
}
|
|
|
|
// Now store value in cache ($obfuscationkey is not empty because of empty/throw above).
|
|
$_SESSION['obfuscationkey_'.((int) $this->entity)] = $obfuscationkey;
|
|
$conf->cache['obfuscationkey_'.((int) $this->entity)] = $obfuscationkey;
|
|
|
|
return (string) $obfuscationkey;
|
|
}
|
|
|
|
/**
|
|
* Get the encoded HMAC secret key.
|
|
* Use a memory cache to avoid repeated db access.
|
|
*
|
|
* @param int $nocache Use 1 to force to not use cache.
|
|
* @param int $noentity Use 1 to search without entity.
|
|
* @return string Encoded HMAC secret key.
|
|
*/
|
|
public function getEncodedHMACSecretKey($nocache = 0, $noentity = 0)
|
|
{
|
|
global $conf;
|
|
|
|
$hmac_encoded_secret_key = '';
|
|
|
|
// Get value of the $hmac_encoded_secret_key from the database
|
|
if ($nocache || empty($conf->cache['hmac_encoded_secret_key_'.((int) $this->entity)])) {
|
|
$sql = "SELECT value FROM ".MAIN_DB_PREFIX."const WHERE name = 'BLOCKEDLOG_HMAC_KEY'";
|
|
if ($noentity) {
|
|
$sql .= " AND entity IN (0)"; // To force to get value on old instances that may have been saved with entity = 0
|
|
} else {
|
|
$sql .= " AND entity IN (0, ".((int) $this->entity).")";
|
|
}
|
|
$sql .= " ORDER BY entity DESC LIMIT 1";
|
|
|
|
$resql = $this->db->query($sql);
|
|
if ($resql) {
|
|
$obj = $this->db->fetch_object($resql);
|
|
if ($obj) {
|
|
$hmac_encoded_secret_key = $obj->value;
|
|
|
|
// Save value in memory page cache (if we recall the same function in same page transaction, we will avoid db access).
|
|
if (empty($nocache)) {
|
|
$conf->cache['hmac_encoded_secret_key_'.((int) $this->entity)] = $hmac_encoded_secret_key;
|
|
}
|
|
}
|
|
} else {
|
|
return 'ERROR '.$this->db->lasterror();
|
|
}
|
|
} else {
|
|
$hmac_encoded_secret_key = $conf->cache['hmac_encoded_secret_key_'.((int) $this->entity)];
|
|
}
|
|
|
|
return $hmac_encoded_secret_key;
|
|
}
|
|
|
|
/**
|
|
* Get the HMAC secret key.
|
|
*
|
|
* @param string $hmac_encoded_secret_key HMAC encode string retrieved with getEncodedHMACSecretKey()
|
|
* @return string Encoded HMAC secret key.
|
|
*/
|
|
public function getClearHMACSecretKey($hmac_encoded_secret_key)
|
|
{
|
|
// Here we have the obfuscated value of BLOCKEDLOG_HMAC_KEY in $hmac_encoded_secret_key. We need to unobfuscate it.
|
|
$hmac_secret_key = '';
|
|
$errormsg = '';
|
|
|
|
// Get the obfuscation key from ping.dolibarr.org (to be used just after to decode HMAC secret key)
|
|
if (preg_match('/^dolobfuscation/', $hmac_encoded_secret_key)) {
|
|
$obfuscationkey = '';
|
|
try {
|
|
$obfuscationkey = $this->getObfuscationKey(); // Get obfuscation key providing $mysoc->idprof1 and $registrationnumber. Note: On network trouble, an Exception is thrown to the caller
|
|
} catch (Exception $e) {
|
|
$errormsg = $e->getMessage();
|
|
}
|
|
if (!$errormsg && $obfuscationkey) {
|
|
// Decode the encrypted parameter using the obfuscation key to get the HMAC key in memory.
|
|
$hmac_secret_key = dolDecrypt($hmac_encoded_secret_key, $obfuscationkey);
|
|
}
|
|
}
|
|
|
|
if (preg_match('/^dolcrypt/', $hmac_encoded_secret_key)) {
|
|
// Failed to get the clear HMAC value. May be we are using an old obfuscated HMAC key, so we retry with the old method (used by webhosting providers using the attestation with old versions).
|
|
// We test this. Note: we force a migration of data to use the new storage if this is the case in method buildFinalSignatureHash().
|
|
// Example with the old demo sample database:
|
|
// dolcrypt:AES-256-CTR:46cb611f00c4cff8:XVfEh15vX/JOYmpiw2QPNamcTQwdbBZJTcXBh9rMpzYJOpVPZubIWcgA8wHMXA==
|
|
// instance_unique_id=11f3c81e86fc9e3b3fd11d81c9a31bd0
|
|
// HMAC key=BLOCKEDLOGHMACY3Ewx37RXbSd8gL9JV8p7Wqw7qvq2K2A
|
|
|
|
$hmac_secret_key = dolDecrypt($hmac_encoded_secret_key); // Decode the encrypted parameter using the obfuscation key from ping.dolibarr.org to decode HMAC key
|
|
}
|
|
|
|
if (!preg_match('/^BLOCKEDLOGHMAC/', (string) $hmac_secret_key)) {
|
|
throw new Exception('getClearHMACSecretKey Error: Failed to decode the crypted value of the parameter BLOCKEDLOG_HMAC_KEY '.$hmac_encoded_secret_key.' using the obfuscation key. A value was found in database but decoding failed. May be you modified the SIREN used to get the obfuscation key from ping.dolibarr.org (or old config key $dolibarr_main_instance_unique_id).'.($errormsg ? ' Additional message: '.$errormsg : ''));
|
|
}
|
|
|
|
return $hmac_secret_key;
|
|
}
|
|
|
|
/**
|
|
* Get previous signature/hash in chain. If there is no previous line, return the init hash.
|
|
*
|
|
* @param int<0,1> $withlock 1=With a lock (Used in the ->create() transaction)
|
|
* @param int $beforeid ID of a record
|
|
* @return array<string, int|string> Hash of previous record (if beforeid is defined) or hash of last record (if beforeid is 0)
|
|
*/
|
|
public function getPreviousHash($withlock = 0, $beforeid = 0)
|
|
{
|
|
global $conf;
|
|
|
|
$previousid = 0;
|
|
$previoussignature = '';
|
|
$previousdatecreation = 0;
|
|
|
|
// Fast search of previous record by searching with beforeid - 1. This is very fast and will work 99% of time.
|
|
if ($beforeid) {
|
|
$sql = "SELECT rowid, signature, date_creation, object_format FROM ".MAIN_DB_PREFIX."blockedlog";
|
|
$sql .= " WHERE entity = ".((int) $conf->entity);
|
|
$sql .= " AND rowid = ".((int) $beforeid - 1);
|
|
$sql .= ($withlock ? " FOR UPDATE " : ""); // To be sure transaction the get last hash to generate the next one will be unlocked once transaction to create new record is finished
|
|
|
|
$resql = $this->db->query($sql);
|
|
if ($resql) {
|
|
$obj = $this->db->fetch_object($resql);
|
|
if ($obj) {
|
|
$previousid = $obj->rowid;
|
|
$previoussignature = $obj->signature;
|
|
$tz = 'gmt';
|
|
if (empty($obj->object_format) || $obj->object_format == 'V1') {
|
|
$tz = 'tzserver';
|
|
}
|
|
$previousdatecreation = $this->db->jdate($obj->date_creation, $tz);
|
|
}
|
|
} else {
|
|
dol_print_error($this->db);
|
|
exit;
|
|
}
|
|
}
|
|
|
|
if (empty($previoussignature)) {
|
|
dol_syslog("getPreviousHash: We did not found previous record with fast mode so we search with a select max", LOG_DEBUG);
|
|
|
|
// Note: a select max rowid and then a select to get signature seems not faster due to filter on entity
|
|
$sql = "SELECT rowid, signature, date_creation, object_format FROM ".MAIN_DB_PREFIX."blockedlog";
|
|
if ($beforeid) {
|
|
$sql .= $this->db->hintindex('entity_rowid', 1);
|
|
}
|
|
$sql .= " WHERE entity = ".((int) $conf->entity);
|
|
if ($beforeid) {
|
|
$sql .= " AND rowid < ".(int) $beforeid;
|
|
}
|
|
$sql .= " ORDER BY rowid DESC LIMIT 1";
|
|
$sql .= ($withlock ? " FOR UPDATE " : "");
|
|
|
|
$resql = $this->db->query($sql);
|
|
if ($resql) {
|
|
$obj = $this->db->fetch_object($resql);
|
|
if ($obj) {
|
|
$previousid = $obj->rowid;
|
|
$previoussignature = $obj->signature;
|
|
$tz = 'gmt';
|
|
if (empty($obj->object_format) || $obj->object_format == 'V1') {
|
|
$tz = 'tzserver';
|
|
}
|
|
$previousdatecreation = $this->db->jdate($obj->date_creation, $tz);
|
|
}
|
|
} else {
|
|
dol_print_error($this->db); // can happen after a deadlock when too many requests do create into blocked log happen at the same time.
|
|
http_response_code(503);
|
|
exit;
|
|
}
|
|
}
|
|
|
|
if (empty($previoussignature)) {
|
|
// First signature line (line 0)
|
|
$previousid = 0;
|
|
$previoussignature = $this->getOrInitFirstSignature();
|
|
}
|
|
|
|
return array('previousid' => $previousid, 'previoushash' => $previoussignature, 'previousdatecreation' => $previousdatecreation);
|
|
}
|
|
|
|
/**
|
|
* Return the last record in blocked log
|
|
*
|
|
* @param int $rowidafter Search record after this one
|
|
* @return array<string, int|string> Last record (id, date, signature)
|
|
*/
|
|
public function getNextRecord($rowidafter = 0)
|
|
{
|
|
global $conf;
|
|
|
|
$nextrecord = array('id' => 0, 'date' => 0, 'signature' => '');
|
|
|
|
// Get next record
|
|
$sql = "SELECT rowid, date_creation, signature FROM ".MAIN_DB_PREFIX."blockedlog";
|
|
$sql .= " WHERE entity = ".((int) $conf->entity);
|
|
$sql .= " AND rowid > ".((int) $rowidafter);
|
|
$sql .= " ORDER BY rowid ASC LIMIT 1";
|
|
|
|
$resql = $this->db->query($sql);
|
|
$obj = $this->db->fetch_object($resql);
|
|
if ($obj) {
|
|
$nextrecord['id'] = $obj->rowid;
|
|
$nextrecord['date'] = $this->db->jdate($obj->date_creation, 'gmt');
|
|
$nextrecord['signature'] = $obj->signature;
|
|
}
|
|
|
|
return $nextrecord;
|
|
}
|
|
|
|
/**
|
|
* Return the last record in blocked log
|
|
*
|
|
* @return array<string, int|string> Last record (id, date, signature)
|
|
*/
|
|
public function getLastRecord()
|
|
{
|
|
global $conf;
|
|
|
|
$lastrecord = array('id' => 0, 'date' => 0, 'signature' => '');
|
|
|
|
// Get last line
|
|
$sql = "SELECT rowid, date_creation, signature FROM ".MAIN_DB_PREFIX."blockedlog";
|
|
$sql .= " WHERE entity = ".((int) $conf->entity);
|
|
$sql .= " ORDER BY rowid DESC LIMIT 1";
|
|
$resql = $this->db->query($sql);
|
|
$obj = $this->db->fetch_object($resql);
|
|
if ($obj) {
|
|
$lastrecord['id'] = $obj->rowid;
|
|
$lastrecord['date'] = $this->db->jdate($obj->date_creation, 'gmt');
|
|
$lastrecord['signature'] = $obj->signature;
|
|
}
|
|
|
|
return $lastrecord;
|
|
}
|
|
|
|
/**
|
|
* Return array of unalterable log objects (filtered with criteria)
|
|
*
|
|
* @param string $element Element to search
|
|
* @param string|int $fk_object Id of object to search. Can be a UFS search criteria.
|
|
* @param int<0,max> $limit Max number of element, 0 for all
|
|
* @param string $sortfield Sort field
|
|
* @param string $sortorder Sort order
|
|
* @param int $search_fk_user Id of user(s)
|
|
* @param int $search_start Start time limit
|
|
* @param int $search_end End time limit
|
|
* @param string $search_ref Search ref
|
|
* @param string $search_amount Search amount
|
|
* @param string|string[] $search_code Search code
|
|
* @param string $search_signature Search signature
|
|
* @param string $search_module_source Search on module source
|
|
* @param string $search_pos_source Search on terminal
|
|
* @param string $search_type_code Search on type code
|
|
* @return BlockedLog[]|int<-2,-1> Array of object log or <0 if error
|
|
*/
|
|
public function getLog($element, $fk_object, $limit = 0, $sortfield = '', $sortorder = '', $search_fk_user = -1, $search_start = -1, $search_end = -1, $search_ref = '', $search_amount = '', $search_code = '', $search_signature = '', $search_module_source = '', $search_pos_source = '', $search_type_code = '')
|
|
{
|
|
global $conf;
|
|
//global $cachedlogs;
|
|
|
|
/* $cachedlogs allow fastest search */
|
|
//if (empty($cachedlogs)) $cachedlogs = array();
|
|
|
|
if ($element == 'all') {
|
|
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."blockedlog
|
|
WHERE entity = ".((int) $conf->entity);
|
|
} elseif ($element == 'not_certified') {
|
|
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."blockedlog
|
|
WHERE entity = ".((int) $conf->entity)." AND certified = 0";
|
|
} elseif ($element == 'just_certified') {
|
|
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."blockedlog
|
|
WHERE entity = ".((int) $conf->entity)." AND certified = 1";
|
|
} else {
|
|
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."blockedlog
|
|
WHERE entity = ".((int) $conf->entity)." AND element = '".$this->db->escape($element)."'";
|
|
}
|
|
|
|
if ($fk_object) {
|
|
$sql .= natural_search("rowid", (string) $fk_object, 1);
|
|
}
|
|
if ($search_fk_user > 0) {
|
|
$sql .= natural_search("fk_user", (string) $search_fk_user, 2);
|
|
}
|
|
if ($search_start > 0) {
|
|
$sql .= " AND date_creation >= '".$this->db->idate($search_start, 'gmt')."'";
|
|
}
|
|
if ($search_end > 0) {
|
|
$sql .= " AND date_creation <= '".$this->db->idate($search_end, 'gmt')."'";
|
|
}
|
|
if ($search_type_code) {
|
|
$sql .= natural_search("type_code", (string) $search_type_code);
|
|
}
|
|
if ($search_ref != '') {
|
|
$sql .= " AND (".natural_search("ref_object", $search_ref, 0, 1);
|
|
$sql .= " OR ".natural_search("linktoref", $search_ref, 0, 1).")";
|
|
}
|
|
if ($search_amount != '') {
|
|
$sql .= natural_search("amounts", $search_amount, 1);
|
|
}
|
|
if ($search_signature != '') {
|
|
$sql .= natural_search("signature", $search_signature, 0);
|
|
}
|
|
if (is_array($search_code)) {
|
|
if (!empty($search_code)) {
|
|
if (in_array('PAYMENT_CUSTOMER', $search_code)) { // If we ask codes PAYMENT_CUSTOMER, it means both PAYMENT_CUSTOMER_CREATE and PAYMENT_CUSTOMER_DELETE
|
|
$search_code[] = 'PAYMENT_CUSTOMER_CREATE';
|
|
$search_code[] = 'PAYMENT_CUSTOMER_DELETE';
|
|
}
|
|
|
|
$sql .= natural_search("action", implode(',', $search_code), 3);
|
|
}
|
|
} else {
|
|
if ($search_code != '' && $search_code != '-1') {
|
|
$sql .= natural_search("action", $search_code, 3);
|
|
}
|
|
}
|
|
if (is_array($search_module_source)) {
|
|
if (!empty($search_module_source)) {
|
|
$sql .= " AND (";
|
|
if (in_array('0', $search_module_source)) {
|
|
$sql .= "module_source = ''";
|
|
unset($search_module_source[0]);
|
|
if (!empty($search_module_source)) {
|
|
$sql .= " OR ";
|
|
}
|
|
}
|
|
if (!empty($search_module_source)) {
|
|
$sqlTmp = natural_search("module_source", implode(',', $search_module_source), 0, 1);
|
|
$sqlTmp = str_replace('%backoffice%', '', $sqlTmp);
|
|
$sql .= $sqlTmp;
|
|
}
|
|
$sql .= " OR module_source = 'mix'"; // When a payment was recorded and payment was on an invoice with different origins (pos and not pos)
|
|
$sql .= ")";
|
|
}
|
|
} else {
|
|
if ($search_module_source != '' && $search_module_source != '-1') {
|
|
$sql .= natural_search("module_source", $search_module_source, 3);
|
|
}
|
|
}
|
|
if ($search_pos_source != '') {
|
|
$sql .= " AND (";
|
|
$sql .= natural_search("pos_source", $search_pos_source, 0, 1);
|
|
$sql .= " OR pos_source = 'mix'"; // When a payment was recorded and payment was on an invoice with different terminal (pos and not pos)
|
|
$sql .= ")";
|
|
}
|
|
|
|
$sql .= $this->db->order($sortfield, $sortorder);
|
|
$sql .= $this->db->plimit($limit + 1); // We want more, because we will stop into loop later with error if we reach max
|
|
|
|
$res = $this->db->query($sql);
|
|
if ($res) {
|
|
$results = array();
|
|
|
|
$i = 0;
|
|
while ($obj = $this->db->fetch_object($res)) {
|
|
$i++;
|
|
if ($i > $limit) {
|
|
// Too many record, we will consume too much memory
|
|
return -2;
|
|
}
|
|
|
|
//if (!isset($cachedlogs[$obj->rowid]))
|
|
//{
|
|
$b = new BlockedLog($this->db);
|
|
$result = $b->fetch($obj->rowid);
|
|
//$b->loadTrackedEvents();
|
|
//$cachedlogs[$obj->rowid] = $b;
|
|
//}
|
|
|
|
//$results[] = $cachedlogs[$obj->rowid];
|
|
if ($result < 0) {
|
|
$this->error = $b->error;
|
|
$this->errors = $b->errors;
|
|
return -1;
|
|
}
|
|
|
|
$results[] = $b;
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* Return the signature (hash) of the "genesis-block" (Block 0).
|
|
*
|
|
* @return string Signature of genesis-block for current conf->entity
|
|
*/
|
|
public function getOrInitFirstSignature()
|
|
{
|
|
global $db, $conf;
|
|
|
|
if (!getDolGlobalString('BLOCKEDLOG_ENTITY_FINGERPRINT')) { // creation of a unique fingerprint
|
|
require_once DOL_DOCUMENT_ROOT.'/core/lib/admin.lib.php';
|
|
require_once DOL_DOCUMENT_ROOT.'/core/lib/security.lib.php';
|
|
require_once DOL_DOCUMENT_ROOT.'/core/lib/security2.lib.php';
|
|
|
|
$fingerprint = bin2hex(random_bytes(32)); // 64 char hex
|
|
|
|
dolibarr_set_const($db, 'BLOCKEDLOG_ENTITY_FINGERPRINT', $fingerprint, 'chaine', 0, 'Initial signature fingerprint', $conf->entity);
|
|
|
|
$conf->global->BLOCKEDLOG_ENTITY_FINGERPRINT = $fingerprint;
|
|
}
|
|
|
|
return getDolGlobalString('BLOCKEDLOG_ENTITY_FINGERPRINT');
|
|
}
|
|
|
|
|
|
/**
|
|
* Check if module was already used or not for at least one recording.
|
|
*
|
|
* @param int<0,1> $ignoresystem Ignore system events for the test
|
|
* @return bool
|
|
*/
|
|
public function alreadyUsed($ignoresystem = 0)
|
|
{
|
|
include_once DOL_DOCUMENT_ROOT.'/blockedlog/lib/blockedlog.lib.php';
|
|
return isBlockedLogUsed($ignoresystem);
|
|
}
|
|
|
|
|
|
/**
|
|
* Check if module can be enabled.
|
|
*
|
|
* @return string '' if ok, error message if not possible
|
|
*/
|
|
public function canBeEnabled()
|
|
{
|
|
include_once DOL_DOCUMENT_ROOT.'/blockedlog/lib/blockedlog.lib.php';
|
|
include_once DOL_DOCUMENT_ROOT.'/blockedlog/lib/securitycore.lib.php';
|
|
|
|
$isqualified = isALNEQualifiedVersion(0, 1);
|
|
|
|
if ($isqualified && ($isqualified != 'CERTIF_LNE_IS_2') && !isHTTPS()) {
|
|
return 'Error: The HTTPS must be enabled to allow the use of this module in France.';
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
|
|
/**
|
|
* Check if module can be disabled.
|
|
*
|
|
* @return int<0,1> 0=Can't be disabled, 1=Can be disabled
|
|
*/
|
|
public function canBeDisabled()
|
|
{
|
|
global $mysoc;
|
|
|
|
include_once DOL_DOCUMENT_ROOT.'/blockedlog/lib/blockedlog.lib.php';
|
|
|
|
$isqualified = isALNEQualifiedVersion();
|
|
|
|
$canbedisabled = 1;
|
|
// For france, we can never disable the module (except in debug mode)
|
|
if ($isqualified && ($isqualified != 'CERTIF_LNE_IS_2') && $mysoc->country_code == 'FR') {
|
|
$canbedisabled = 0;
|
|
}
|
|
|
|
return $canbedisabled;
|
|
}
|
|
|
|
|
|
/**
|
|
* Return current number of records.
|
|
*
|
|
* @return int Number of recor for all instances
|
|
*/
|
|
public function countRecord()
|
|
{
|
|
$nb = 0;
|
|
|
|
$sql = "SELECT COUNT(rowid) as nb FROM ".MAIN_DB_PREFIX."blockedlog";
|
|
$resql = $this->db->query($sql);
|
|
if ($resql) {
|
|
$obj = $this->db->fetch_object($resql);
|
|
$nb = $obj->nb;
|
|
} else {
|
|
dol_print_error($this->db);
|
|
}
|
|
$this->db->free($resql);
|
|
|
|
return $nb;
|
|
}
|
|
}
|