hookmanager.class.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. <?php
  2. /* Copyright (C) 2010-2016 Laurent Destailleur <eldy@users.sourceforge.net>
  3. * Copyright (C) 2010-2014 Regis Houssin <regis.houssin@inodbox.com>
  4. * Copyright (C) 2010-2011 Juanjo Menent <jmenent@2byte.es>
  5. *
  6. * This program is free software; you can redistribute it and/or modify
  7. * it under the terms of the GNU General Public License as published by
  8. * the Free Software Foundation; either version 3 of the License, or
  9. * (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU General Public License
  17. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  18. */
  19. /**
  20. * \file htdocs/core/class/hookmanager.class.php
  21. * \ingroup core
  22. * \brief File of class to manage hooks
  23. */
  24. /**
  25. * Class to manage hooks
  26. */
  27. class HookManager
  28. {
  29. /**
  30. * @var DoliDB Database handler.
  31. */
  32. public $db;
  33. /**
  34. * @var string Error code (or message)
  35. */
  36. public $error = '';
  37. /**
  38. * @var string[] Error codes (or messages)
  39. */
  40. public $errors = array();
  41. // Context hookmanager was created for ('thirdpartycard', 'thirdpartydao', ...)
  42. public $contextarray = array();
  43. // Array with instantiated classes
  44. public $hooks = array();
  45. // Array result
  46. public $resArray = array();
  47. // Printable result
  48. public $resPrint = '';
  49. // Nb of qualified hook ran
  50. public $resNbOfHooks = 0;
  51. /**
  52. * Constructor
  53. *
  54. * @param DoliDB $db Database handler
  55. */
  56. public function __construct($db)
  57. {
  58. $this->db = $db;
  59. }
  60. /**
  61. * Init array $this->hooks with instantiated action controlers.
  62. * 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.
  63. * This makes $conf->hooks_modules loaded with an entry ('modulename'=>array(nameofcontext1,nameofcontext2,...))
  64. * When initHooks function is called, with initHooks(list_of_contexts), an array $this->hooks is defined with instance of controler
  65. * class found into file /mymodule/class/actions_mymodule.class.php (if module has declared the context as a managed context).
  66. * Then when a hook executeHooks('aMethod'...) is called, the method aMethod found into class will be executed.
  67. *
  68. * @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), ...
  69. * @return int Always 1
  70. */
  71. public function initHooks($arraycontext)
  72. {
  73. global $conf;
  74. // Test if there is hooks to manage
  75. if (!is_array($conf->modules_parts['hooks']) || empty($conf->modules_parts['hooks'])) {
  76. return;
  77. }
  78. // For backward compatibility
  79. if (!is_array($arraycontext)) {
  80. $arraycontext = array($arraycontext);
  81. }
  82. $this->contextarray = array_unique(array_merge($arraycontext, $this->contextarray)); // All contexts are concatenated
  83. $arraytolog = array();
  84. foreach ($conf->modules_parts['hooks'] as $module => $hooks) { // Loop on each module that brings hooks
  85. if (empty($conf->$module->enabled)) {
  86. continue;
  87. }
  88. //dol_syslog(get_class($this).'::initHooks module='.$module.' arraycontext='.join(',',$arraycontext));
  89. foreach ($arraycontext as $context) {
  90. if (is_array($hooks)) {
  91. $arrayhooks = $hooks; // New system
  92. } else {
  93. $arrayhooks = explode(':', $hooks); // Old system (for backward compatibility)
  94. }
  95. if (in_array($context, $arrayhooks) || in_array('all', $arrayhooks)) { // We instantiate action class only if initialized hook is handled by module
  96. // Include actions class overwriting hooks
  97. 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.
  98. $path = '/'.$module.'/class/';
  99. $actionfile = 'actions_'.$module.'.class.php';
  100. $arraytolog[] = 'context='.$context.'-path='.$path.$actionfile;
  101. $resaction = dol_include_once($path.$actionfile);
  102. if ($resaction) {
  103. $controlclassname = 'Actions'.ucfirst($module);
  104. $actionInstance = new $controlclassname($this->db);
  105. $priority = empty($actionInstance->priority) ? 50 : $actionInstance->priority;
  106. $this->hooks[$context][$priority.':'.$module] = $actionInstance;
  107. }
  108. }
  109. }
  110. }
  111. }
  112. // Log the init of hook but only for hooks thare are declared to be managed
  113. if (count($arraytolog) > 0) {
  114. dol_syslog(get_class($this)."::initHooks Loading hooks: ".join(', ', $arraytolog), LOG_DEBUG);
  115. }
  116. foreach ($arraycontext as $context) {
  117. if (!empty($this->hooks[$context])) {
  118. ksort($this->hooks[$context], SORT_NATURAL);
  119. }
  120. }
  121. return 1;
  122. }
  123. /**
  124. * Execute hooks (if they were initialized) for the given method
  125. *
  126. * @param string $method Name of method hooked ('doActions', 'printSearchForm', 'showInputField', ...)
  127. * @param array $parameters Array of parameters
  128. * @param Object $object Object to use hooks on
  129. * @param string $action Action code on calling page ('create', 'edit', 'view', 'add', 'update', 'delete'...)
  130. * @return mixed 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.
  131. * 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.
  132. * All types can also return some values into an array ->results that will be finaly merged into this->resArray for caller.
  133. * $this->error or this->errors are also defined by class called by this function if error.
  134. */
  135. public function executeHooks($method, $parameters = array(), &$object = '', &$action = '')
  136. {
  137. if (!is_array($this->hooks) || empty($this->hooks)) {
  138. return 0; // No hook available, do nothing.
  139. }
  140. $parameters['context'] = join(':', $this->contextarray);
  141. //dol_syslog(get_class($this).'::executeHooks method='.$method." action=".$action." context=".$parameters['context']);
  142. // Define type of hook ('output' or 'addreplace').
  143. // TODO Remove hooks with type 'output' (exemple getNomUrl). All hooks must be converted into 'addreplace' hooks.
  144. $hooktype = 'output';
  145. if (in_array(
  146. $method,
  147. array(
  148. 'addCalendarChoice',
  149. 'addCalendarView',
  150. 'addMoreActionsButtons',
  151. 'addMoreMassActions',
  152. 'addSearchEntry',
  153. 'addStatisticLine',
  154. 'addSectionECMAuto',
  155. 'checkSecureAccess',
  156. 'createDictionaryFieldlist',
  157. 'editDictionaryFieldlist',
  158. 'getFormMail',
  159. 'deleteFile',
  160. 'doActions',
  161. 'doMassActions',
  162. 'formatEvent',
  163. 'formConfirm',
  164. 'formCreateThirdpartyOptions',
  165. 'formObjectOptions',
  166. 'formattachOptions',
  167. 'formBuilddocLineOptions',
  168. 'formatNotificationMessage',
  169. 'formConfirm',
  170. 'getAccessForbiddenMessage',
  171. 'getDirList',
  172. 'hookGetEntity',
  173. 'getFormMail',
  174. 'getFormatedCustomerRef',
  175. 'getFormatedSupplierRef',
  176. 'getIdProfUrl',
  177. 'getInputIdProf',
  178. 'menuDropdownQuickaddItems',
  179. 'menuLeftMenuItems',
  180. 'moveUploadedFile',
  181. 'moreHtmlStatus',
  182. 'pdf_build_address',
  183. 'pdf_writelinedesc',
  184. 'pdf_getlinenum',
  185. 'pdf_getlineref',
  186. 'pdf_getlineref_supplier',
  187. 'pdf_getlinevatrate',
  188. 'pdf_getlineupexcltax',
  189. 'pdf_getlineupwithtax',
  190. 'pdf_getlineqty',
  191. 'pdf_getlineqty_asked',
  192. 'pdf_getlineqty_shipped',
  193. 'pdf_getlineqty_keeptoship',
  194. 'pdf_getlineunit',
  195. 'pdf_getlineremisepercent',
  196. 'pdf_getlineprogress',
  197. 'pdf_getlinetotalexcltax',
  198. 'pdf_getlinetotalwithtax',
  199. 'paymentsupplierinvoices',
  200. 'printAddress',
  201. 'printEmail',
  202. 'printSearchForm',
  203. 'printTabsHead',
  204. 'printObjectLine',
  205. 'printObjectSubLine',
  206. 'restrictedArea',
  207. 'sendMail',
  208. 'sendMailAfter',
  209. 'showOptionals',
  210. 'showLinkToObjectBlock',
  211. 'setContentSecurityPolicy',
  212. 'setHtmlTitle',
  213. 'completeTabsHead',
  214. 'formDolBanner',
  215. 'displayMarginInfos',
  216. )
  217. )) {
  218. $hooktype = 'addreplace';
  219. }
  220. // Init return properties
  221. $this->resPrint = '';
  222. $this->resArray = array();
  223. $this->resNbOfHooks = 0;
  224. // Here, the value for $method and $hooktype are given.
  225. // Loop on each hook to qualify modules that have declared context
  226. $modulealreadyexecuted = array();
  227. $resaction = 0;
  228. $error = 0;
  229. 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
  230. if (!empty($modules)) {
  231. // Loop on each active hooks of module for this context
  232. foreach ($modules as $module => $actionclassinstance) {
  233. $module = preg_replace('/^\d+:/', '', $module);
  234. //print "Before hook ".get_class($actionclassinstance)." method=".$method." module=".$module." hooktype=".$hooktype." results=".count($actionclassinstance->results)." resprints=".count($actionclassinstance->resprints)." resaction=".$resaction."<br>\n";
  235. // test to avoid running twice a hook, when a module implements several active contexts
  236. if (in_array($module, $modulealreadyexecuted)) {
  237. continue;
  238. }
  239. // jump to next module/class if method does not exist
  240. if (!method_exists($actionclassinstance, $method)) {
  241. continue;
  242. }
  243. $this->resNbOfHooks++;
  244. $modulealreadyexecuted[$module] = $module;
  245. // Clean class (an error may have been set from a previous call of another method for same module/hook)
  246. $actionclassinstance->error = 0;
  247. $actionclassinstance->errors = array();
  248. if (getDolGlobalInt('MAIN_DEBUG_SHOW_EACH_QUALIFIED_HOOK_CALL') >= 2) {
  249. // This his too much verbose, enabled in develop only
  250. 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);
  251. }
  252. // Add current context to avoid method execution in bad context, you can add this test in your method : eg if($currentcontext != 'formfile') return;
  253. // Note: The hook can use the $currentcontext in its code to avoid to be ran twice or be ran for one given context only
  254. $parameters['currentcontext'] = $context;
  255. // Hooks that must return int (hooks with type 'addreplace')
  256. if ($hooktype == 'addreplace') {
  257. $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)
  258. $resaction += $resactiontmp;
  259. if ($resactiontmp < 0 || !empty($actionclassinstance->error) || (!empty($actionclassinstance->errors) && count($actionclassinstance->errors) > 0)) {
  260. $error++;
  261. $this->error = $actionclassinstance->error;
  262. $this->errors = array_merge($this->errors, (array) $actionclassinstance->errors);
  263. dol_syslog("Error on hook module=".$module.", method ".$method.", class ".get_class($actionclassinstance).", hooktype=".$hooktype.(empty($this->error) ? '' : " ".$this->error).(empty($this->errors) ? '' : " ".join(",", $this->errors)), LOG_ERR);
  264. }
  265. if (isset($actionclassinstance->results) && is_array($actionclassinstance->results)) {
  266. if ($resactiontmp > 0) {
  267. $this->resArray = $actionclassinstance->results;
  268. } else {
  269. $this->resArray = array_merge($this->resArray, $actionclassinstance->results);
  270. }
  271. }
  272. if (!empty($actionclassinstance->resprints)) {
  273. if ($resactiontmp > 0) {
  274. $this->resPrint = $actionclassinstance->resprints;
  275. } else {
  276. $this->resPrint .= $actionclassinstance->resprints;
  277. }
  278. }
  279. } else {
  280. // Generic hooks that return a string or array (printLeftBlock, formAddObjectLine, formBuilddocOptions, ...)
  281. // TODO. this test should be done into the method of hook by returning nothing
  282. if (is_array($parameters) && !empty($parameters['special_code']) && $parameters['special_code'] > 3 && $parameters['special_code'] != $actionclassinstance->module_number) {
  283. continue;
  284. }
  285. //dol_syslog("Call method ".$method." of class ".get_class($actionclassinstance).", module=".$module.", hooktype=".$hooktype, LOG_DEBUG);
  286. $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)
  287. $resaction += $resactiontmp;
  288. if (!empty($actionclassinstance->results) && is_array($actionclassinstance->results)) {
  289. $this->resArray = array_merge($this->resArray, $actionclassinstance->results);
  290. }
  291. if (!empty($actionclassinstance->resprints)) {
  292. $this->resPrint .= $actionclassinstance->resprints;
  293. }
  294. if (is_numeric($resactiontmp) && $resactiontmp < 0) {
  295. $error++;
  296. $this->error = $actionclassinstance->error;
  297. $this->errors = array_merge($this->errors, (array) $actionclassinstance->errors);
  298. dol_syslog("Error on hook module=".$module.", method ".$method.", class ".get_class($actionclassinstance).", hooktype=".$hooktype.(empty($this->error) ? '' : " ".$this->error).(empty($this->errors) ? '' : " ".join(",", $this->errors)), LOG_ERR);
  299. }
  300. // 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
  301. if (!is_array($resactiontmp) && !is_numeric($resactiontmp)) {
  302. 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);
  303. if (empty($actionclassinstance->resprints)) {
  304. $this->resPrint .= $resactiontmp;
  305. }
  306. }
  307. }
  308. //print "After hook context=".$context." ".get_class($actionclassinstance)." method=".$method." hooktype=".$hooktype." results=".count($actionclassinstance->results)." resprints=".count($actionclassinstance->resprints)." resaction=".$resaction."<br>\n";
  309. unset($actionclassinstance->results);
  310. unset($actionclassinstance->resprints);
  311. }
  312. }
  313. }
  314. return ($error ? -1 : $resaction);
  315. }
  316. }