<?php
/* Copyright (C) 2005-2009 Laurent Destailleur  <eldy@users.sourceforge.net>
 * Copyright (C) 2006      Rodolphe Quiedeville <rodolphe@quiedeville.org>
 * Copyright (C) 2010      Regis Houssin        <regis.houssin@inodbox.com>
 * 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/interfaces.class.php
 *   \ingroup		core
 *   \brief			Fichier de la class de gestion des triggers
 */

require_once DOL_DOCUMENT_ROOT.'/core/triggers/dolibarrtriggers.class.php';


/**
 *   Class to manage triggers
 */
class Interfaces
{
	/**
	 * @var DoliDB Database handler.
	 */
	public $db;

	/**
	 * @var string
	 */
	public $dir; // Directory with all core and external triggers files

	/**
	 * @var string		Last module name in error
	 */
	public $lastmoduleerror;

	/**
	 * @var string[] Error codes (or messages)
	 */
	public $errors = array();

	/**
	 *	Constructor
	 *
	 *  @param		DoliDB		$db      Database handler
	 */
	public function __construct($db)
	{
		$this->db = $db;
	}

	// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
	/**
	 *   Function called when a Dolibarr business event occurs
	 *   This function call all qualified triggers.
	 *
	 *   @param		string		$action     Trigger event code
	 *   @param     Object		$object     Object concerned. Some context information may also be provided into array property object->context.
	 *   @param     User		$user       Object user
	 *   @param     Translate	$langs      Object lang
	 *   @param     Conf		$conf       Object conf
	 *   @return    int         			Nb of triggers ran if no error, -Nb of triggers with errors otherwise.
	 */
	public function run_triggers($action, $object, $user, $langs, $conf)
	{
		// phpcs:enable

		if (getDolGlobalInt('MAIN_TRIGGER_DEBUG')) {
			// This his too much verbose, enabled if const enabled only
			dol_syslog(get_class($this)."::run_triggers action=".$action." Launch run_triggers", LOG_DEBUG);
		}

		// Check parameters
		if (!is_object($object) || !is_object($conf)) {	// Error
			$error = 'function run_triggers called with wrong parameters action='.$action.' object='.((string) (int) is_object($object)).' user='.((string) (int) is_object($user)).' langs='.((string) (int) is_object($langs)).' conf='.((string) (int) is_object($conf));
			dol_syslog(get_class($this).'::run_triggers '.$error, LOG_ERR);
			$this->errors[] = $error;
			return -1;
		}
		if (!is_object($langs)) {	// Warning
			dol_syslog(get_class($this).'::run_triggers was called with wrong parameters action='.$action.' object='.((string) (int) is_object($object)).' user='.((string) (int) is_object($user)).' langs='.((string) (int) is_object($langs)).' conf='.((string) (int) is_object($conf)), LOG_WARNING);
		}
		if (!is_object($user)) {	    // Warning
			dol_syslog(get_class($this).'::run_triggers was called with wrong parameters action='.$action.' object='.((string) (int) is_object($object)).' user='.((string) (int) is_object($user)).' langs='.((string) (int) is_object($langs)).' conf='.((string) (int) is_object($conf)), LOG_WARNING);
			$user = new User($this->db);
		}

		$nbfile = $nbtotal = $nbok = $nbko = 0;
		$this->lastmoduleerror = '';

		$files = array();
		$modules = array();
		$orders = array();
		$i = 0;

		$dirtriggers = array_merge(array('/core/triggers'), $conf->modules_parts['triggers']);
		foreach ($dirtriggers as $reldir) {
			$dir = dol_buildpath($reldir, 0);
			$newdir = dol_osencode($dir);
			//print "xx".$dir;exit;

			// Check if directory exists (we do not use dol_is_dir to avoir loading files.lib.php at each call)
			if (!is_dir($newdir)) {
				continue;
			}

			$handle = opendir($newdir);
			if (is_resource($handle)) {
				$fullpathfiles = array();
				'@phan-var-force array<string,string> $fullpathfiles';
				while (($file = readdir($handle)) !== false) {
					$reg = array();
					if (is_readable($newdir."/".$file) && preg_match('/^interface_([0-9]+)_([^_]+)_(.+)\.class\.php$/i', $file, $reg)) {
						$part1 = $reg[1];
						$part2 = $reg[2];
						$part3 = $reg[3];

						$nbfile++;

						// Check if trigger file is disabled by name
						if (preg_match('/NORUN$/i', $file)) {
							continue;
						}
						// Check if trigger file is for a particular module
						$qualified = true;
						if (strtolower($reg[2]) != 'all') {
							$module = preg_replace('/^mod/i', '', $reg[2]);
							if (!isModEnabled(strtolower($module))) {
								$qualified = false;
							}
						}

						if (!$qualified) {
							//dol_syslog(get_class($this)."::run_triggers action=".$action." Triggers for file '".$file."' need module to be enabled", LOG_DEBUG);
							continue;
						}

						$modName = "Interface".ucfirst($reg[3]);
						//print "file=$file - modName=$modName\n";
						if (array_key_exists($modName, $fullpathfiles)) {    // $modules = list of modName already loaded, fullpathfiles[$modName] is alsoset
							$langs->load("errors");
							dol_syslog(get_class($this)."::run_triggers action=".$action." ".$langs->trans("ErrorDuplicateTrigger", $newdir."/".$file, $fullpathfiles[$modName]), LOG_WARNING);
							continue;
						}

						try {
							//print 'Todo for '.$modName." : ".$newdir.'/'.$file."\n";
							include_once $newdir.'/'.$file;
							//print 'Done for '.$modName."\n";
						} catch (Exception $e) {
							dol_syslog('ko for '.$modName." ".$e->getMessage()."\n", LOG_ERR);
						}

						$modules[$i] = $modName;
						$files[$i] = $file;
						$fullpathfiles[$modName] = $newdir.'/'.$file;
						$orders[$i] = $part1.'_'.$part2.'_'.$part3; // Set sort criteria value

						$i++;
					}
				}

				closedir($handle);
			}
		}

		asort($orders, SORT_NATURAL);

		// Loop on each trigger
		foreach ($orders as $key => $value) {
			$modName = $modules[$key];
			if (empty($modName)) {
				continue;
			}

			$objMod = new $modName($this->db);
			'@phan-var-force DolibarrTriggers $objMod';
			if ($objMod) {
				$dblevelbefore = $this->db->transaction_opened;

				$result = 0;

				if (method_exists($objMod, 'runTrigger')) {	// New method to implement
					//dol_syslog(get_class($this)."::run_triggers action=".$action." Launch runTrigger for file '".$files[$key]."'", LOG_DEBUG);
					$result = $objMod->runTrigger($action, $object, $user, $langs, $conf);
				} elseif (method_exists($objMod, 'run_trigger')) {	// Deprecated method
					dol_syslog(get_class($this)."::run_triggers action=".$action." Launch old method run_trigger (rename your trigger into runTrigger) for file '".$files[$key]."'", LOG_WARNING);
					// @phan-suppress-next-line PhanUndeclaredMethod
					$result = $objMod->run_trigger($action, $object, $user, $langs, $conf);
				} else {
					dol_syslog(get_class($this)."::run_triggers action=".$action." A trigger was declared for class ".get_class($objMod)." but method runTrigger was not found", LOG_ERR);
				}

				$dblevelafter = $this->db->transaction_opened;

				if ($dblevelbefore != $dblevelafter) {
					$errormessage = "Error, the balance begin/close of db transactions has been broken into trigger ".$modName." with action=".$action." before=".$dblevelbefore." after=".$dblevelafter;
					$this->errors[] = $errormessage;
					dol_syslog($errormessage, LOG_ERR);
					$result = -1;
				}

				if ($result > 0) {
					// Action OK
					$nbtotal++;
					$nbok++;
				}
				if ($result == 0) {
					// Aucune action faite
					$nbtotal++;
				}
				if ($result < 0) {
					// Action KO
					//dol_syslog("Error in trigger ".$action." - result = ".$result." - Nb of error string returned = ".count($objMod->errors), LOG_ERR);
					$nbtotal++;
					$nbko++;
					$this->lastmoduleerror = $modName;
					if (!empty($objMod->errors)) {
						$this->errors = array_merge($this->errors, $objMod->errors);
					} elseif (!empty($objMod->error)) {
						$this->errors[] = $objMod->error;
					}
					//dol_syslog("Error in trigger ".$action." - Nb of error string returned = ".count($this->errors), LOG_ERR);
				}
			} else {
				dol_syslog(get_class($this)."::run_triggers action=".$action." Failed to instantiate trigger for file '".$files[$key]."'", LOG_ERR);
			}
		}

		if ($nbko) {
			dol_syslog(get_class($this)."::run_triggers action=".$action." Files found: ".$nbfile.", Files launched: ".$nbtotal.", Done: ".$nbok.", Failed: ".$nbko.($this->lastmoduleerror ? " - Last module in error: ".$this->lastmoduleerror : "")." - Nb of error string returned in this->errors = ".count($this->errors), LOG_ERR);
			return -$nbko;
		} else {
			//dol_syslog(get_class($this)."::run_triggers Files found: ".$nbfile.", Files launched: ".$nbtotal.", Done: ".$nbok.", Failed: ".$nbko, LOG_DEBUG);
			return $nbok;
		}
	}

	/**
	 *  Return list of triggers. Function used by admin page htdoc/admin/triggers.
	 *  List is sorted by trigger filename so by priority to run.
	 *
	 *  @param	?array<int,string>	$forcedirtriggers	null=All default directories. This parameter is used by modulebuilder module only.
	 *	@return array<array{picto:string,file:string,fullpath:string,relpath:string,iscoreorexternal?:'internal'|'external',version?:string,status?:string,module?:string,info:string}>		Array list of triggers
	 */
	public function getTriggersList($forcedirtriggers = null)
	{
		global $conf, $langs, $db;

		$files = array();
		$fullpath = array();
		$relpath = array();
		$iscoreorexternal = array();
		$modules = array();
		$orders = array();
		$i = 0;

		$dirtriggers = array_merge(array('/core/triggers/'), $conf->modules_parts['triggers']);
		if (is_array($forcedirtriggers)) {
			$dirtriggers = $forcedirtriggers;
		}

		foreach ($dirtriggers as $reldir) {
			$dir = dol_buildpath($reldir, 0);
			$newdir = dol_osencode($dir);

			// Check if directory exists (we do not use dol_is_dir to avoid loading files.lib.php at each call)
			if (!is_dir($newdir)) {
				continue;
			}

			$handle = opendir($newdir);
			if (is_resource($handle)) {
				while (($file = readdir($handle)) !== false) {
					$reg = array();
					if (is_readable($newdir.'/'.$file) && preg_match('/^interface_([0-9]+)_([^_]+)_(.+)\.class\.php/', $file, $reg)) {
						if (preg_match('/\.back$/', $file)) {
							continue;
						}

						$part1 = $reg[1];
						$part2 = $reg[2];
						$part3 = $reg[3];

						$modName = 'Interface'.ucfirst($reg[3]);
						//print "file=$file"; print "modName=$modName"; exit;
						if (in_array($modName, $modules)) {
							$langs->load("errors");
							print '<div class="error">'.$langs->trans("Error").' : '.$langs->trans("ErrorDuplicateTrigger", $modName, "/htdocs/core/triggers/").'</div>';
						} else {
							include_once $newdir.'/'.$file;
						}

						$files[$i] = $file;
						$fullpath[$i] = $dir.'/'.$file;
						$relpath[$i] = preg_replace('/^\//', '', $reldir).'/'.$file;
						$iscoreorexternal[$i] = ($reldir == '/core/triggers/' ? 'internal' : 'external');
						$modules[$i] = $modName;
						$orders[$i] = $part1.'_'.$part2.'_'.$part3; // Set sort criteria value

						$i++;
					}
				}
				closedir($handle);
			}
		}

		asort($orders, SORT_NATURAL);

		$triggers = array();
		$j = 0;

		// Loop on each trigger
		foreach ($orders as $key => $value) {
			$modName = $modules[$key];
			if (empty($modName)) {
				continue;
			}

			if (!class_exists($modName)) {
				print 'Error: A trigger file was found but its class "'.$modName.'" was not found.'."<br>\n";
				continue;
			}

			$text = '';

			try {
				$objMod = new $modName($db);
				'@phan-var-force DolibarrTriggers $objMod';

				if (is_subclass_of($objMod, 'DolibarrTriggers')) {
					// Define disabledbyname and disabledbymodule
					$disabledbyname = 0;
					$disabledbymodule = 1;
					$module = '';

					// Check if trigger file is disabled by name
					if (preg_match('/NORUN$/i', $files[$key])) {
						$disabledbyname = 1;
					}
					// Check if trigger file is for a particular module
					if (preg_match('/^interface_([0-9]+)_([^_]+)_(.+)\.class\.php/i', $files[$key], $reg)) {
						$module = preg_replace('/^mod/i', '', $reg[2]);
						if (strtolower($module) == 'all') {
							$disabledbymodule = 0;
						} elseif (!isModEnabled(strtolower($module))) {
							$disabledbymodule = 2;
						}
						$triggers[$j]['module'] = strtolower($module);
					}

					// We set info of modules
					$triggers[$j]['picto'] = (!empty($objMod->picto)) ? img_object('', $objMod->picto, 'class="valignmiddle pictomodule "') : img_object('', 'generic', 'class="valignmiddle pictomodule "');
					$triggers[$j]['file'] = $files[$key];
					$triggers[$j]['fullpath'] = $fullpath[$key];
					$triggers[$j]['relpath'] = $relpath[$key];
					$triggers[$j]['iscoreorexternal'] = $iscoreorexternal[$key];
					$triggers[$j]['version'] = $objMod->getVersion();
					$triggers[$j]['status'] = img_picto($langs->trans("Active"), 'tick');
					if ($disabledbyname > 0 || $disabledbymodule > 1) {
						$triggers[$j]['status'] = '';
					}

					$text = '<b>'.$langs->trans("Description").':</b><br>';
					$text .= $objMod->getDesc().'<br>';
					$text .= '<br><b>'.$langs->trans("Status").':</b><br>';
					if ($disabledbyname == 1) {
						$text .= $langs->trans("TriggerDisabledByName").'<br>';
						if ($disabledbymodule == 2) {
							$text .= $langs->trans("TriggerDisabledAsModuleDisabled", $module).'<br>';
						}
					} else {
						if ($disabledbymodule == 0) {
							$text .= $langs->trans("TriggerAlwaysActive").'<br>';
						}
						if ($disabledbymodule == 1) {
							$text .= $langs->trans("TriggerActiveAsModuleActive", $module).'<br>';
						}
						if ($disabledbymodule == 2) {
							$text .= $langs->trans("TriggerDisabledAsModuleDisabled", $module).'<br>';
						}
					}
				} else {
					$triggers[$j]['picto'] = (!empty($objMod->picto)) ? img_object('', $objMod->picto, 'class="valignmiddle pictomodule "') : img_object('', 'generic', 'class="valignmiddle pictomodule "');
					$triggers[$j]['file'] = $files[$key];
					$triggers[$j]['fullpath'] = $fullpath[$key];
					$triggers[$j]['relpath'] = $relpath[$key];
					$triggers[$j]['status'] = img_picto('Error: Trigger '.$modName.' does not extends DolibarrTriggers', 'warning');

					//print 'Error: Trigger '.$modName.' does not extends DolibarrTriggers<br>';
					$text = 'Error: Trigger '.$modName.' does not extend DolibarrTriggers';
				}
			} catch (Exception $e) {
				print $e->getMessage();
			}

			$triggers[$j]['info'] = $text;
			$j++;
		}
		return $triggers;
	}
}
