* Copyright (C) 2017 Laurent Destailleur * * 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 . * * See https://medium.com/@lhartikk/a-blockchain-in-200-lines-of-code-963cc1cc0e54 */ /** * Class to manage Blocked Log */ class BlockedLog { /** * Id of the log * @var int */ public $id; /** * Entity * @var int */ public $entity; public $error = ''; public $errors = array(); /** * Unique fingerprint of the log * @var string */ public $signature = ''; /** * Unique fingerprint of the line log content * @var string */ public $signature_line = ''; public $amounts = null; /** * trigger action * @var string */ public $action = ''; /** * 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; public $date_object = 0; public $ref_object = ''; public $object_data = null; /** * Array of tracked event codes * @var string[] */ public $trackedevents = array(); /** * Constructor * * @param DoliDB $db Database handler */ public function __construct(DoliDB $db) { global $conf; $this->db = $db; $this->trackedevents = array(); if ($conf->facture->enabled) $this->trackedevents['BILL_VALIDATE']='BillValidate'; if ($conf->facture->enabled) $this->trackedevents['BILL_DELETE']='BillDelete'; if ($conf->facture->enabled) $this->trackedevents['BILL_SENTBYMAIL']='BillSentByEmail'; if ($conf->facture->enabled) $this->trackedevents['DOC_DOWNLOAD']='BillDownload'; if ($conf->facture->enabled) $this->trackedevents['DOC_PREVIEW']='BillPreview'; if ($conf->facture->enabled) $this->trackedevents['PAYMENT_CUSTOMER_CREATE']='BillPaymentCreate'; if ($conf->facture->enabled) $this->trackedevents['PAYMENT_CUSTOMER_DELETE']='BillPaymentDelete'; /* Supplier if ($conf->fournisseur->enabled) $this->trackedevents['BILL_SUPPLIER_VALIDATE']='SupplierBillValidate'; if ($conf->fournisseur->enabled) $this->trackedevents['BILL_SUPPLIER_DELETE']='SupplierBillDelete'; if ($conf->fournisseur->enabled) $this->trackedevents['BILL_SUPPLIER_SENTBYMAIL']='SupplierBillSentByEmail'; // Trigger key does not exists, we want just into array to list it as done if ($conf->fournisseur->enabled) $this->trackedevents['SUPPLIER_DOC_DOWNLOAD']='SupplierBillDownload'; // Trigger key does not exists, we want just into array to list it as done if ($conf->fournisseur->enabled) $this->trackedevents['SUPPLIER_DOC_PREVIEW']='SupplierBillPreview'; // Trigger key does not exists, we want just into array to list it as done if ($conf->fournisseur->enabled) $this->trackedevents['PAYMENT_SUPPLIER_CREATE']='SupplierBillPaymentCreate'; if ($conf->fournisseur->enabled) $this->trackedevents['PAYMENT_SUPPLIER_DELETE']='supplierBillPaymentCreate'; */ if ($conf->don->enabled) $this->trackedevents['DON_CREATE']='DonationCreate'; if ($conf->don->enabled) $this->trackedevents['DON_MODIFY']='DonationModify'; if ($conf->don->enabled) $this->trackedevents['DON_DELETE']='DonationDelete'; /* if ($conf->salary->enabled) $this->trackedevents['PAYMENT_SALARY_CREATE']='SalaryPaymentCreate'; if ($conf->salary->enabled) $this->trackedevents['PAYMENT_SALARY_MODIFY']='SalaryPaymentCreate'; if ($conf->salary->enabled) $this->trackedevents['PAYMENT_SALARY_DELETE']='SalaryPaymentCreate'; */ if ($conf->adherent->enabled) $this->trackedevents['MEMBER_SUBSCRIPTION']='MemberSubscription'; /* $trackedevents['PAYMENT_VARIOUS_CREATE']='VariousPaymentCreate'; $trackedevents['PAYMENT_VARIOUS_MODIFY']='VariousPaymentModify'; $trackedevents['PAYMENT_VARIOUS_DELETE']='VariousPaymentDelete'; */ } /** * try to retrieve logged object link */ 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++; } } 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++; } } else if($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++; } } else if($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++; } } else if ($this->action == 'MODULE_SET') { return 'System to track events into unalterable logs were enabled'; } else if ($this->action == 'MODULE_RESET') { if ($this->signature == '0000000000') { return 'System to track events into unalterable logs were disabled after some recording were done. We saved a special Fingerprint to track the chain as broken.'; } else { return 'System to track events into unalterable logs were disabled. This is possible because no record were done yet.'; } } return ''.$langs->trans('ImpossibleToReloadObject', $this->element, $this->fk_object).''; } /** * try to retrieve user author */ 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 log from object data * * @param Object $object object to store * @param string $action action * @param string $amounts amounts * @return int >0 if OK, <0 if KO */ public function setObjectData(&$object, $action, $amounts) { global $langs, $user, $mysoc; // Generic fields // action $this->action = $action; // amount $this->amounts= $amounts; // date if ($object->element == 'payment' || $object->element == 'payment_supplier') { $this->date_object = $object->datepaye; } elseif ($object->element=='payment_salary') { $this->date_object = $object->datev; } else { $this->date_object = $object->date; } // ref $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; $this->object_data=new stdClass(); // Add thirdparty info if (empty($object->thirdparty) && method_exists($object, 'fetch_thirdparty')) $object->fetch_thirdparty(); if (! empty($object->thirdparty)) { $this->object_data->thirdparty = new stdClass(); foreach($object->thirdparty as $key=>$value) { if (in_array($key, array('fields'))) continue; // Discard some properties 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', '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)) $this->object_data->thirdparty->{$key} = $value; } } // Add company info if (! empty($mysoc)) { $this->object_data->mycompany = new stdClass(); foreach($mysoc as $key=>$value) { if (in_array($key, array('fields'))) continue; // Discard some properties 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', '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)) $this->object_data->mycompany->{$key} = $value; } } // Add user info $this->fk_user = $user->id; $this->user_fullname = $user->getFullName($langs); // Field specific to object if ($this->element == 'facture') { foreach($object as $key=>$value) { if (in_array($key, array('fields'))) continue; // Discard some properties if (! in_array($key, array( 'ref','facnumber','ref_client','ref_supplier','datef','type','total_ht','total_tva','total_ttc','localtax1','localtax2','revenuestamp','datepointoftax','note_public' ))) continue; // Discard if not into a dedicated list if (!is_object($value)) $this->object_data->{$key} = $value; } } elseif ($this->element == 'invoice_supplier') { foreach($object as $key=>$value) { if (in_array($key, array('fields'))) continue; // Discard some properties if (! in_array($key, array( 'ref','facnumber','ref_client','ref_supplier','datef','type','total_ht','total_tva','total_ttc','localtax1','localtax2','revenuestamp','datepointoftax','note_public' ))) continue; // Discard if not into a dedicated list if (!is_object($value)) $this->object_data->{$key} = $value; } } elseif ($this->element == 'payment'|| $object->element == 'payment_supplier') { //var_dump($object); $this->object_data->ref = $object->ref; $this->object_data->date = $object->datepaye; $this->object_data->type_code = dol_getIdFromCode($this->db, $object->paiementid, 'c_paiement', 'id', 'code'); $this->object_data->payment_num = $object->num_paiement; //$this->object_data->fk_account = $object->fk_account; $this->object_data->note = $object->note; //var_dump($this->object_data);exit; $totalamount=0; $paymentpartnumber=0; foreach($object->amounts as $invoiceid => $amount) { if (empty($amount)) continue; $totalamount += $amount; if ($this->element == 'payment_supplier') { $tmpinvoice = new FactureFournisseur($this->db); } else { $tmpinvoice = new Facture($this->db); } $result = $tmpinvoice->fetch($invoiceid); if ($result <= 0) { $this->error = $tmpinvoice->error; $this->errors = $tmpinvoice->errors; return -1; } $result = $tmpinvoice->fetch_thirdparty(); if ($result <= 0) { $this->error = $tmpinvoice->error; $this->errors = $tmpinvoice->errors; return -1; } $paymentpart = new stdClass(); $paymentpart->amount = $amount; $paymentpart->thirdparty = new stdClass(); foreach($tmpinvoice->thirdparty as $key=>$value) { if (in_array($key, array('fields'))) continue; // Discard some properties 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', '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)) $paymentpart->thirdparty->{$key} = $value; } $paymentpart->invoice = new stdClass(); foreach($tmpinvoice as $key=>$value) { if (in_array($key, array('fields'))) continue; // Discard some properties if (! in_array($key, array( 'ref','facnumber','ref_client','ref_supplier','datef','type','total_ht','total_tva','total_ttc','localtax1','localtax2','revenuestamp','datepointoftax','note_public' ))) continue; // Discard if not into a dedicated list if (!is_object($value)) $paymentpart->invoice->{$key} = $value; } $paymentpartnumber++; $this->object_data->payment_part[$paymentpartnumber] = $paymentpart; } $this->object_data->amount = $totalamount; } elseif($this->element == 'payment_salary') { $this->object_data->amounts = array($object->amount); } return 1; } /** * Get object from database * * @param int $id Id of object to load * @return int >0 if OK, <0 if KO, 0 if not found */ public function fetch($id) { global $langs; dol_syslog(get_class($this)."::fetch id=".$id, LOG_DEBUG); if (empty($id)) { $this->error='BadParameter'; return -1; } $langs->load("blockedlog"); $sql = "SELECT b.rowid, b.date_creation, b.signature, b.signature_line, b.amounts, b.action, 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.object_data"; $sql.= " FROM ".MAIN_DB_PREFIX."blockedlog as b"; if ($id) $sql.= " WHERE b.rowid = ". $id; $resql=$this->db->query($sql); if ($resql) { if ($this->db->num_rows($resql)) { $obj = $this->db->fetch_object($resql); $this->id = $obj->rowid; $this->entity = $obj->entity; $this->ref = $obj->rowid; $this->date_creation = $this->db->jdate($obj->date_creation); $this->tms = $this->db->jdate($obj->tms); $this->amounts = (double) $obj->amounts; $this->action = $obj->action; $this->element = $obj->element; $this->fk_object = $obj->fk_object; $this->date_object = $this->db->jdate($obj->date_object); $this->ref_object = $obj->ref_object; $this->fk_user = $obj->fk_user; $this->user_fullname = $obj->user_fullname; $this->object_data = unserialize($obj->object_data); $this->signature = $obj->signature; $this->signature_line = $obj->signature_line; $this->certified = ($obj->certified == 1); return 1; } else { $this->error=$langs->trans("RecordNotFound"); return 0; } } else { $this->error=$this->db->error(); return -1; } } /** * Set block certified by authority * * @return boolean */ public function setCertified() { $res = $this->db->query("UPDATE ".MAIN_DB_PREFIX."blockedlog SET certified=1 WHERE rowid=".$this->id); if($res===false) return false; return true; } /** * Create blocked log in database. * * @param User $user Object user that create * @param int $forcesignature Force signature (for example '0000000000' when we disabled the module) * @return int <0 if KO, >0 if OK */ public function create($user, $forcesignature='') { global $conf,$langs,$hookmanager; $langs->load('blockedlog'); $error=0; // Clean data $this->amounts=(double) $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) { $this->error=$langs->trans("BlockLogNeedAmountsValue"); dol_syslog($this->error, LOG_WARNING); return -1; } if (empty($this->element)) { $this->error=$langs->trans("BlockLogNeedElement"); dol_syslog($this->error, LOG_WARNING); return -2; } if (empty($this->action) || empty($this->fk_user) || empty($this->user_fullname)) { $this->error=$langs->trans("BadParameterWhenCallingCreateOfBlockedLog"); dol_syslog($this->error, LOG_WARNING); return -3; } $this->date_creation = dol_now(); $this->db->begin(); $previoushash = $this->getPreviousHash(1); // This get last record and lock database until insert is done $keyforsignature = $this->buildKeyForSignature(); $this->signature_line = dol_hash($keyforsignature, '5'); // Not really usefull $this->signature = dol_hash($previoushash . $keyforsignature, '5'); if ($forcesignature) $this->signature = $forcesignature; //var_dump($keyforsignature);var_dump($previoushash);var_dump($this->signature_line);var_dump($this->signature); $sql = "INSERT INTO ".MAIN_DB_PREFIX."blockedlog ("; $sql.= " date_creation,"; $sql.= " action,"; $sql.= " amounts,"; $sql.= " signature,"; $sql.= " signature_line,"; $sql.= " element,"; $sql.= " fk_object,"; $sql.= " date_object,"; $sql.= " ref_object,"; $sql.= " object_data,"; $sql.= " certified,"; $sql.= " fk_user,"; $sql.= " user_fullname,"; $sql.= " entity"; $sql.= ") VALUES ("; $sql.= "'".$this->db->idate($this->date_creation)."',"; $sql.= "'".$this->db->escape($this->action)."',"; $sql.= $this->amounts.","; $sql.= "'".$this->db->escape($this->signature)."',"; $sql.= "'".$this->db->escape($this->signature_line)."',"; $sql.= "'".$this->db->escape($this->element)."',"; $sql.= $this->fk_object.","; $sql.= "'".$this->db->idate($this->date_object)."',"; $sql.= "'".$this->db->escape($this->ref_object)."',"; $sql.= "'".$this->db->escape(serialize($this->object_data))."',"; $sql.= "0,"; $sql.= $this->fk_user.","; $sql.= "'".$this->db->escape($this->user_fullname)."',"; $sql.= ($this->entity ? $this->entity : $conf->entity); $sql.= ")"; $res = $this->db->query($sql); if ($res) { $id = $this->db->last_insert_id(MAIN_DB_PREFIX."blockedlog"); if ($id > 0) { $this->id = $id; $this->db->commit(); return $this->id; } else { $this->db->rollback(); return -2; } } else { $this->error=$this->db->error(); $this->db->rollback(); return -1; } // The commit will release the lock so we can insert nex record } /** * Check if current signature still correct compare to the chain * * @return boolean True if OK, False if KO */ public function checkSignature() { //$oldblockedlog = new BlockedLog($this->db); //$previousrecord = $oldblockedlog->fetch($this->id - 1); $previoushash = $this->getPreviousHash(0, $this->id); // Recalculate hash $keyforsignature = $this->buildKeyForSignature(); $signature_line = dol_hash($keyforsignature, '5'); // Not really usefull $signature = dol_hash($previoushash . $keyforsignature, '5'); //var_dump($previoushash); var_dump($keyforsignature); var_dump($signature_line); var_dump($signature); $res = ($signature === $this->signature); if (!$res) { $this->error = 'Signature KO'; } return $res; } /** * Return a string for signature * * @return string Key for signature */ private function buildKeyForSignature() { //print_r($this->object_data); return $this->date_creation.'|'.$this->action.'|'.$this->amounts.'|'.$this->ref_object.'|'.$this->date_object.'|'.$this->user_fullname.'|'.print_r($this->object_data, true); } /** * Get previous signature/hash in chain * * @param int $withlock 1=With a lock * @param int $beforeid Before id * @return string Hash of last record */ private function getPreviousHash($withlock=0, $beforeid=0) { global $conf; $previoussignature=''; $sql = "SELECT rowid, signature FROM ".MAIN_DB_PREFIX."blockedlog"; $sql.= " WHERE entity=".$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) { $previoussignature = $obj->signature; } } else { dol_print_error($this->db); exit; } if (empty($previoussignature)) { // First signature line (line 0) $previoussignature = $this->getSignature(); } return $previoussignature; } /** * Return array of log objects (with criterias) * * @param string $element element to search * @param int $fk_object id of object to search * @param int $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 * @return array array of object log */ public function getLog($element, $fk_object, $limit = 0, $sortfield = '', $sortorder = '', $search_fk_user = -1, $search_start = -1, $search_end = -1, $search_ref='', $search_amount='') { global $conf, $cachedlogs; /* $cachedlogs allow fastest search */ if (empty($cachedlogs)) $cachedlogs=array(); if ($element=='all') { $sql="SELECT rowid FROM ".MAIN_DB_PREFIX."blockedlog WHERE entity=".$conf->entity; } else if ($element=='not_certified') { $sql="SELECT rowid FROM ".MAIN_DB_PREFIX."blockedlog WHERE entity=".$conf->entity." AND certified = 0"; } else if ($element=='just_certified') { $sql="SELECT rowid FROM ".MAIN_DB_PREFIX."blockedlog WHERE entity=".$conf->entity." AND certified = 1"; } else{ $sql="SELECT rowid FROM ".MAIN_DB_PREFIX."blockedlog WHERE entity=".$conf->entity." AND element='".$element."' AND fk_object=".(int) $fk_object; } if ($search_fk_user > 0) $sql.=" AND fk_user IN (".$this->db->escape($search_fk_user).")"; if ($search_start > 0) $sql.=" AND date_creation >= '".$this->db->idate($search_start)."'"; if ($search_end > 0) $sql.=" AND date_creation <= '".$this->db->idate($search_end)."'"; if ($search_ref != '') $sql.=natural_search("ref_object", $search_ref); if ($search_amount != '') $sql.=natural_search("amounts", $search_amount, 1); $sql.=$this->db->order($sortfield, $sortorder); if($limit > 0 )$sql.=' LIMIT '.$limit; $res = $this->db->query($sql); if($res) { $results=array(); while ($obj = $this->db->fetch_object($res)) { if (!isset($cachedlogs[$obj->rowid])) { $b=new BlockedLog($this->db); $b->fetch($obj->rowid); $cachedlogs[$obj->rowid] = $b; } $results[] = $cachedlogs[$obj->rowid]; } return $results; } else{ return false; } } /** * Return the signature (hash) of the "genesis-block" (Block 0). * * @return string Signature of genesis-block for current conf->entity */ public function getSignature() { global $db,$conf,$mysoc; if (empty($conf->global->BLOCKEDLOG_ENTITY_FINGERPRINT)) { // creation of a unique fingerprint require_once DOL_DOCUMENT_ROOT.'/core/lib/admin.lib.php'; $fingerprint = dol_hash(print_r($mysoc,true).getRandomPassword(1), '5'); dolibarr_set_const($db, 'BLOCKEDLOG_ENTITY_FINGERPRINT', $fingerprint, 'chaine',0,'Numeric Unique Fingerprint', $conf->entity); $conf->global->BLOCKEDLOG_ENTITY_FINGERPRINT=$fingerprint; } return $conf->global->BLOCKEDLOG_ENTITY_FINGERPRINT; } /** * Check if module was already used or not for at least one recording. * * @param int $ignoresystem Ignore system events for the test */ function alreadyUsed($ignoresystem=0) { global $conf; $result = false; $sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."blockedlog"; $sql.= " WHERE entity = ".$conf->entity; if ($ignoresystem) $sql.=" AND action not in ('MODULE_SET','MODULE_RESET')"; $sql.= $this->db->plimit(1); $res = $this->db->query($sql); if ($res!==false) { $obj = $this->db->fetch_object($res); if ($obj) $result = true; } else dol_print_error($this->db); dol_syslog("Module Blockedlog alreadyUsed with ignoresystem=".$ignoresystem." is ".$result); return $result; } }