mirror of
https://github.com/Dolibarr/dolibarr.git
synced 2025-12-08 18:48:22 +01:00
Change $object parameter default value from '' to null to fix issue when using type declarations in custom hooks.
Mirrored change from commit 0453b3e.
376 lines
16 KiB
PHP
376 lines
16 KiB
PHP
<?php
|
|
/* Copyright (C) 2010-2016 Laurent Destailleur <eldy@users.sourceforge.net>
|
|
* Copyright (C) 2010-2014 Regis Houssin <regis.houssin@inodbox.com>
|
|
* Copyright (C) 2010-2011 Juanjo Menent <jmenent@2byte.es>
|
|
* Copyright (C) 2024 MDW <mdeweerd@users.noreply.github.com>
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
/**
|
|
* \file htdocs/core/class/hookmanager.class.php
|
|
* \ingroup core
|
|
* \brief File of class to manage hooks
|
|
*/
|
|
|
|
|
|
/**
|
|
* Class to manage hooks
|
|
*/
|
|
class HookManager
|
|
{
|
|
/**
|
|
* @var DoliDB Database handler.
|
|
*/
|
|
public $db;
|
|
|
|
/**
|
|
* @var string Error code (or message)
|
|
*/
|
|
public $error = '';
|
|
|
|
/**
|
|
* @var string[] Error codes (or messages)
|
|
*/
|
|
public $errors = array();
|
|
|
|
/**
|
|
* @var string[] Context hookmanager was created for ('thirdpartycard', 'thirdpartydao', ...)
|
|
*/
|
|
public $contextarray = array();
|
|
|
|
/**
|
|
* array<string,array<string,null|string|CommonHookActions>> Array with instantiated classes
|
|
*/
|
|
public $hooks = array();
|
|
|
|
/**
|
|
* @var array<string,array{name:string,contexts:string[],file:string,line:string,count:int}> List of hooks called during this request (key = hash)
|
|
*/
|
|
public $hooksHistory = [];
|
|
|
|
/**
|
|
* @var mixed[] Result
|
|
*/
|
|
public $resArray = array();
|
|
|
|
/**
|
|
* @var string Printable result
|
|
*/
|
|
public $resPrint = '';
|
|
|
|
/**
|
|
* @var int Nb of qualified hook ran
|
|
*/
|
|
public $resNbOfHooks = 0;
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param DoliDB $db Database handler
|
|
* @return void
|
|
*/
|
|
public function __construct($db)
|
|
{
|
|
$this->db = $db;
|
|
}
|
|
|
|
|
|
/**
|
|
* Init array $this->hooks with instantiated action controllers.
|
|
* First, a hook is declared by a module by adding a constant MAIN_MODULE_MYMODULENAME_HOOKS with value 'nameofcontext1:nameofcontext2:...' into $this->const of module descriptor file.
|
|
* This makes $conf->hooks_modules loaded with an entry ('modulename'=>array(nameofcontext1,nameofcontext2,...))
|
|
* When initHooks function is called, with initHooks(list_of_contexts), an array $this->hooks is defined with instance of controller
|
|
* class found into file /mymodule/class/actions_mymodule.class.php (if module has declared the context as a managed context).
|
|
* Then when a hook executeHooks('aMethod'...) is called, the method aMethod found into class will be executed.
|
|
*
|
|
* @param string[] $arraycontext Array list of searched hooks tab/features. For example: 'thirdpartycard' (for hook methods into page card thirdparty), 'thirdpartydao' (for hook methods into Societe), ...
|
|
* @return int<0,1> 0 or 1
|
|
*/
|
|
public function initHooks($arraycontext)
|
|
{
|
|
global $conf;
|
|
|
|
// Test if there is hooks to manage
|
|
if (!is_array($conf->modules_parts['hooks']) || empty($conf->modules_parts['hooks'])) {
|
|
return 0;
|
|
}
|
|
|
|
// For backward compatibility
|
|
if (!is_array($arraycontext)) {
|
|
$arraycontext = array($arraycontext);
|
|
}
|
|
|
|
$this->contextarray = array_unique(array_merge($arraycontext, $this->contextarray)); // All contexts are concatenated
|
|
|
|
$arraytolog = array();
|
|
foreach ($conf->modules_parts['hooks'] as $module => $hooks) { // Loop on each module that brings hooks
|
|
if (empty($conf->$module->enabled)) {
|
|
continue;
|
|
}
|
|
|
|
//dol_syslog(get_class($this).'::initHooks module='.$module.' arraycontext='.join(',',$arraycontext));
|
|
foreach ($arraycontext as $context) {
|
|
if (is_array($hooks)) {
|
|
$arrayhooks = $hooks; // New system
|
|
} else {
|
|
$arrayhooks = explode(':', $hooks); // Old system (for backward compatibility)
|
|
}
|
|
|
|
if (in_array($context, $arrayhooks) || in_array('all', $arrayhooks)) { // We instantiate action class only if initialized hook is handled by module
|
|
// Include actions class overwriting hooks
|
|
if (empty($this->hooks[$context][$module]) || !is_object($this->hooks[$context][$module])) { // If set to an object value, class was already loaded so we do nothing.
|
|
$path = '/'.$module.'/class/';
|
|
$actionfile = 'actions_'.$module.'.class.php';
|
|
|
|
$arraytolog[] = 'context='.$context.'-path='.$path.$actionfile;
|
|
$resaction = dol_include_once($path.$actionfile);
|
|
if ($resaction) {
|
|
$controlclassname = 'Actions'.ucfirst($module);
|
|
$actionInstance = new $controlclassname($this->db);
|
|
'@phan-var-force CommonHookActions $actionInstance';
|
|
$priority = empty($actionInstance->priority) ? 50 : $actionInstance->priority;
|
|
$this->hooks[$context][$priority.':'.$module] = $actionInstance;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Log the init of hook but only for hooks there are declared to be managed
|
|
if (count($arraytolog) > 0) {
|
|
dol_syslog(get_class($this)."::initHooks Loading hooks: ".implode(', ', $arraytolog), LOG_DEBUG);
|
|
}
|
|
|
|
foreach ($arraycontext as $context) {
|
|
if (!empty($this->hooks[$context])) {
|
|
ksort($this->hooks[$context], SORT_NATURAL);
|
|
}
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
/**
|
|
* Execute hooks (if they were initialized) for the given method
|
|
*
|
|
* @param string $method Name of method hooked ('doActions', 'printSearchForm', 'showInputField', ...)
|
|
* @param array<string,mixed> $parameters Array of parameters
|
|
* @param object $object Object to use hooks on
|
|
* @param string $action Action code on calling page ('create', 'edit', 'view', 'add', 'update', 'delete'...)
|
|
* @return int<-1,1> For 'addreplace' hooks (doActions, formConfirm, formObjectOptions, pdf_xxx,...): Return 0 if we want to keep standard actions, >0 if we want to stop/replace standard actions, <0 if KO. Things to print are returned into ->resprints and set into ->resPrint. Things to return are returned into ->results by hook and set into ->resArray for caller.
|
|
* For 'output' hooks (printLeftBlock, formAddObjectLine, formBuilddocOptions, ...): Return 0 if we want to keep standard actions, >0 uf we want to stop/replace standard actions (at least one > 0 and replacement will be done), <0 if KO. Things to print are returned into ->resprints and set into ->resPrint. Things to return are returned into ->results by hook and set into ->resArray for caller.
|
|
* All types can also return some values into an array ->results that will be merged into this->resArray for caller.
|
|
* $this->error or this->errors are also defined by class called by this function if error.
|
|
*/
|
|
public function executeHooks($method, $parameters = array(), &$object = null, &$action = '')
|
|
{
|
|
//global $debugbar;
|
|
//if (is_object($debugbar) && get_class($debugbar) === 'DolibarrDebugBar') {
|
|
if (isModEnabled('debugbar') && function_exists('debug_backtrace')) {
|
|
$trace = debug_backtrace();
|
|
if (isset($trace[0])) {
|
|
$hookInformations = [
|
|
'name' => $method,
|
|
'contexts' => $this->contextarray,
|
|
'file' => $trace[0]['file'],
|
|
'line' => $trace[0]['line'],
|
|
'count' => 0,
|
|
];
|
|
$hash = md5(json_encode($hookInformations));
|
|
if (!empty($this->hooksHistory[$hash])) {
|
|
$this->hooksHistory[$hash]['count']++;
|
|
} else {
|
|
$hookInformations['count'] = 1;
|
|
$this->hooksHistory[$hash] = $hookInformations;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!is_array($this->hooks) || empty($this->hooks)) {
|
|
return 0; // No hook available, do nothing.
|
|
}
|
|
if (!is_array($parameters)) {
|
|
dol_syslog('executeHooks was called with a non array $parameters. Surely a bug.', LOG_WARNING);
|
|
$parameters = array();
|
|
}
|
|
|
|
$parameters['context'] = implode(':', $this->contextarray);
|
|
//dol_syslog(get_class($this).'::executeHooks method='.$method." action=".$action." context=".$parameters['context']);
|
|
|
|
// Define type of hook ('output' or 'addreplace').
|
|
$hooktype = 'addreplace';
|
|
// TODO Remove hooks with type 'output' (example createFrom). All hooks must be converted into 'addreplace' hooks.
|
|
if (in_array($method, array(
|
|
'createFrom',
|
|
'dashboardAccountancy',
|
|
'dashboardActivities',
|
|
'dashboardCommercials',
|
|
'dashboardContracts',
|
|
'dashboardDonation',
|
|
'dashboardEmailings',
|
|
'dashboardExpenseReport',
|
|
'dashboardHRM',
|
|
'dashboardInterventions',
|
|
'dashboardMRP',
|
|
'dashboardMembers',
|
|
'dashboardOpensurvey',
|
|
'dashboardOrders',
|
|
'dashboardOrdersSuppliers',
|
|
'dashboardProductServices',
|
|
'dashboardProjects',
|
|
'dashboardPropals',
|
|
'dashboardSpecialBills',
|
|
'dashboardSupplierProposal',
|
|
'dashboardThirdparties',
|
|
'dashboardTickets',
|
|
'dashboardUsersGroups',
|
|
'dashboardWarehouse',
|
|
'dashboardWarehouseReceptions',
|
|
'dashboardWarehouseSendings',
|
|
'insertExtraHeader',
|
|
'insertExtraFooter',
|
|
'printLeftBlock',
|
|
'formAddObjectLine',
|
|
'formBuilddocOptions',
|
|
'showSocinfoOnPrint'
|
|
))) {
|
|
$hooktype = 'output';
|
|
}
|
|
|
|
// Init return properties
|
|
$localResPrint = '';
|
|
$localResArray = array();
|
|
|
|
$this->resNbOfHooks = 0;
|
|
|
|
// Here, the value for $method and $hooktype are given.
|
|
// Loop on each hook to qualify modules that have declared context
|
|
$modulealreadyexecuted = array();
|
|
$resaction = 0;
|
|
$error = 0;
|
|
foreach ($this->hooks as $context => $modules) { // $this->hooks is an array with context as key and value is an array of modules that handle this context
|
|
if (!empty($modules)) {
|
|
'@phan-var-force array<string,CommonHookActions> $modules';
|
|
// Loop on each active hooks of module for this context
|
|
foreach ($modules as $module => $actionclassinstance) {
|
|
$module = preg_replace('/^\d+:/', '', $module);
|
|
//print "Before hook ".get_class($actionclassinstance)." method=".$method." module=".$module." hooktype=".$hooktype." results=".count($actionclassinstance->results)." resprints=".count($actionclassinstance->resprints)." resaction=".$resaction."<br>\n";
|
|
|
|
// test to avoid running twice a hook, when a module implements several active contexts
|
|
if (in_array($module, $modulealreadyexecuted)) {
|
|
continue;
|
|
}
|
|
|
|
// jump to next module/class if method does not exist
|
|
if (!method_exists($actionclassinstance, $method)) {
|
|
continue;
|
|
}
|
|
|
|
$this->resNbOfHooks++;
|
|
|
|
$modulealreadyexecuted[$module] = $module;
|
|
|
|
// Clean class (an error may have been set from a previous call of another method for same module/hook)
|
|
$actionclassinstance->error = '';
|
|
$actionclassinstance->errors = array();
|
|
|
|
if (getDolGlobalInt('MAIN_HOOK_DEBUG')) {
|
|
// This his too much verbose, enabled if const enabled only
|
|
dol_syslog(get_class($this)."::executeHooks Qualified hook found (hooktype=".$hooktype."). We call method ".get_class($actionclassinstance).'->'.$method.", context=".$context.", module=".$module.", action=".$action.((is_object($object) && property_exists($object, 'id')) ? ', object id='.$object->id : '').((is_object($object) && property_exists($object, 'element')) ? ', object element='.$object->element : ''), LOG_DEBUG);
|
|
}
|
|
|
|
// Add current context to avoid method execution in bad context, you can add this test in your method : eg if($currentcontext != 'formfile') return;
|
|
// Note: The hook can use the $currentcontext in its code to avoid to be ran twice or be ran for one given context only
|
|
$parameters['currentcontext'] = $context;
|
|
// Hooks that must return int (hooks with type 'addreplace')
|
|
if ($hooktype == 'addreplace') {
|
|
// @phan-suppress-next-line PhanUndeclaredMethod The method's existence is tested above.
|
|
$resactiontmp = $actionclassinstance->$method($parameters, $object, $action, $this); // $object and $action can be changed by method ($object->id during creation for example or $action to go back to other action for example)
|
|
$resaction += $resactiontmp;
|
|
|
|
if ($resactiontmp < 0 || !empty($actionclassinstance->error) || (!empty($actionclassinstance->errors) && count($actionclassinstance->errors) > 0)) {
|
|
$error++;
|
|
$this->error = $actionclassinstance->error;
|
|
$this->errors = array_merge($this->errors, (array) $actionclassinstance->errors);
|
|
dol_syslog("Error on hook module=".$module.", method ".$method.", class ".get_class($actionclassinstance).", hooktype=".$hooktype.(empty($this->error) ? '' : " ".$this->error).(empty($this->errors) ? '' : " ".implode(",", $this->errors)), LOG_ERR);
|
|
}
|
|
|
|
if (isset($actionclassinstance->results) && is_array($actionclassinstance->results)) {
|
|
if ($resactiontmp > 0) {
|
|
$localResArray = $actionclassinstance->results;
|
|
} else {
|
|
$localResArray = array_merge_recursive($localResArray, $actionclassinstance->results);
|
|
}
|
|
}
|
|
|
|
if (!empty($actionclassinstance->resprints)) {
|
|
if ($resactiontmp > 0) {
|
|
$localResPrint = (string) $actionclassinstance->resprints;
|
|
} else {
|
|
$localResPrint .= (string) $actionclassinstance->resprints;
|
|
}
|
|
}
|
|
} else {
|
|
// Generic hooks that return a string or array (printLeftBlock, formAddObjectLine, formBuilddocOptions, ...)
|
|
|
|
// TODO. this test should be done into the method of hook by returning nothing
|
|
if (is_array($parameters) && !empty($parameters['special_code']) && $parameters['special_code'] > 3 && $parameters['special_code'] != $actionclassinstance->module_number) {
|
|
continue;
|
|
}
|
|
|
|
if (getDolGlobalInt('MAIN_HOOK_DEBUG')) {
|
|
dol_syslog("Call method ".$method." of class ".get_class($actionclassinstance).", module=".$module.", hooktype=".$hooktype, LOG_DEBUG);
|
|
}
|
|
|
|
// @phan-suppress-next-line PhanUndeclaredMethod The method's existence is tested above.
|
|
$resactiontmp = $actionclassinstance->$method($parameters, $object, $action, $this); // $object and $action can be changed by method ($object->id during creation for example or $action to go back to other action for example)
|
|
$resaction += $resactiontmp;
|
|
|
|
if (!empty($actionclassinstance->results) && is_array($actionclassinstance->results)) {
|
|
$localResArray = array_merge_recursive($localResArray, $actionclassinstance->results);
|
|
}
|
|
if (!empty($actionclassinstance->resprints)) {
|
|
$localResPrint .= (string) $actionclassinstance->resprints;
|
|
}
|
|
if (is_numeric($resactiontmp) && $resactiontmp < 0) {
|
|
$error++;
|
|
$this->error = $actionclassinstance->error;
|
|
$this->errors = array_merge($this->errors, (array) $actionclassinstance->errors);
|
|
dol_syslog("Error on hook module=".$module.", method ".$method.", class ".get_class($actionclassinstance).", hooktype=".$hooktype.(empty($this->error) ? '' : " ".$this->error).(empty($this->errors) ? '' : " ".implode(",", $this->errors)), LOG_ERR);
|
|
}
|
|
|
|
// TODO dead code to remove (do not disable this, but fix your hook instead): result must not be a string but an int. you must use $actionclassinstance->resprints to return a string
|
|
if (!is_array($resactiontmp) && !is_numeric($resactiontmp)) {
|
|
dol_syslog('Error: Bug into hook '.$method.' of module class '.get_class($actionclassinstance).'. Method must not return a string but an int (0=OK, 1=Replace, -1=KO) and set string into ->resprints', LOG_ERR);
|
|
if (empty($actionclassinstance->resprints)) {
|
|
$localResPrint .= $resactiontmp;
|
|
}
|
|
}
|
|
}
|
|
|
|
//print "After hook context=".$context." ".get_class($actionclassinstance)." method=".$method." hooktype=".$hooktype." results=".count($actionclassinstance->results)." resprints=".count($actionclassinstance->resprints)." resaction=".$resaction."<br>\n";
|
|
|
|
$actionclassinstance->results = array();
|
|
$actionclassinstance->resprints = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
$this->resPrint = $localResPrint;
|
|
$this->resArray = $localResArray;
|
|
|
|
return ($error ? -1 : $resaction);
|
|
}
|
|
}
|