* Copyright (C) 2003 Jean-Louis Bergamo * Copyright (C) 2004-2025 Laurent Destailleur * Copyright (C) 2004 Sebastien Di Cintio * Copyright (C) 2004 Benoit Mortier * Copyright (C) 2004 Christophe Combelles * Copyright (C) 2005-2019 Regis Houssin * Copyright (C) 2008 Raphael Bertrand (Resultic) * Copyright (C) 2010-2018 Juanjo Menent * Copyright (C) 2013 Cédric Salvador * Copyright (C) 2013-2026 Alexandre Spangaro * Copyright (C) 2014 Cédric GROSS * Copyright (C) 2014-2015 Marcos García * Copyright (C) 2015 Jean-François Ferry * Copyright (C) 2018-2026 Frédéric France * Copyright (C) 2019-2023 Thibault Foucart * Copyright (C) 2020 Open-Dsi * Copyright (C) 2021 Gauthier VERDOL * Copyright (C) 2022-2026 Anthony Berton * Copyright (C) 2022 Ferran Marcet * Copyright (C) 2022-2026 Charlene Benke * Copyright (C) 2024-2026 MDW * Copyright (C) 2023-2024 Joachim Kueter * Copyright (C) 2024 Lenin Rivas * Copyright (C) 2024 Josep Lluís Amador Teruel * Copyright (C) 2024 Benoît PASCAL * Copyright (C) 2025 Vincent Maury * Copyright (C) 2026 Benjamin Falière * Copyright (C) 2026 Pierre Ardoin * * 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 . * or see https://www.gnu.org/ */ /** * \file htdocs/core/lib/functions.lib.php * \brief A set of functions for Dolibarr * This file contains all frequently used functions. */ //include_once DOL_DOCUMENT_ROOT.'/core/lib/json.lib.php'; // Function for better PHP x compatibility if (!function_exists('utf8_encode')) { /** * Implement utf8_encode for PHP that does not support it. * * @param mixed $elements PHP Object to json encode * @return string Json encoded string * @phan-suppress PhanRedefineFunctionInternal */ function utf8_encode($elements) { return mb_convert_encoding($elements, 'UTF-8', 'ISO-8859-1'); } } if (!function_exists('utf8_decode')) { /** * Implement utf8_decode for PHP that does not support it. * * @param mixed $elements PHP Object to json encode * @return string Json encoded string * @phan-suppress PhanRedefineFunctionInternal */ function utf8_decode($elements) { return mb_convert_encoding($elements, 'ISO-8859-1', 'UTF-8'); } } if (!function_exists('str_starts_with')) { /** * str_starts_with * * @param string $haystack haystack * @param string $needle needle * @return boolean * @phan-suppress PhanRedefineFunctionInternal */ function str_starts_with($haystack, $needle) { return (string) $needle !== '' && strncmp($haystack, $needle, strlen($needle)) === 0; } } if (!function_exists('str_ends_with')) { /** * str_ends_with * * @param string $haystack haystack * @param string $needle needle * @return boolean * @phan-suppress PhanRedefineFunctionInternal */ function str_ends_with($haystack, $needle) { return $needle !== '' && substr($haystack, -strlen($needle)) === (string) $needle; } } if (!function_exists('str_contains')) { /** * str_contains * * @param string $haystack haystack * @param string $needle needle * @return boolean * @phan-suppress PhanRedefineFunctionInternal */ function str_contains($haystack, $needle) { return $needle !== '' && mb_strpos($haystack, $needle) !== false; } } /** * Return a string serialized to be output on log with dol_syslog() * An option allow to output log in one line instead of a structured human tree * * @param mixed $data Any PHP object * @return string Serialized data for loh */ function formatLogObject($data) { if (getDolGlobalInt("MAIN_LOG_ON_ONE_LINE")) { return json_encode($data); } return var_export($data, true); } /** * Return the full path of the directory where a module (or an object of a module) stores its files. * Path may depends on the entity if a multicompany module is enabled. * * @param CommonObject|BlockedLog|null $object Dolibarr common object. * @param string $module Override object element, for example to use 'mycompany' instead of 'societe' * @param int $forobject Use 1 to return the more complete path for the given object (including ref) instead of for the module only. * @param string $mode 'output' (full main dir) or 'outputrel' (relative dir) or 'temp' (full dir for temporary files) or 'version' (full dir for archived files) * @return string|null The path of the relative directory of the module, ending with / * @since Dolibarr V18 */ function getMultidirOutput($object, $module = '', $forobject = 0, $mode = 'output') { global $conf; $subdirectory = ''; if (!is_object($object) && empty($module)) { return null; } if (empty($module) && !empty($object->element)) { $module = $object->element; } // Special case for backward compatibility switch ($module) { case 'fichinter': $module = 'ficheinter'; break; case 'invoice_supplier': $module = 'supplier_invoice'; break; case 'order_supplier': $module = 'supplier_order'; break; case 'recruitmentjobposition': $module = 'recruitment'; $subdirectory = '/recruitmentjobposition'; break; case 'recruitmentcandidature': $module = 'recruitment'; $subdirectory = '/recruitmentcandidature'; break; case 'knowledgerecord': $module = 'knowledgemanagement'; $subdirectory = '/knowledgerecord'; break; case 'partnership': $subdirectory = '/partnership'; break; case 'stocktransfer': $subdirectory = '/stocktransfer'; break; case 'commande_fournisseur': $module = 'fournisseur'; $subdirectory = '/commande'; break; case 'expedition': case 'shipment': case 'shipping': $module = 'expedition'; $subdirectory = '/sending'; break; case 'company': $module = 'societe'; break; case 'service': case 'produit': $module = 'product'; break; case 'project_task': $module = 'projet'; // Fetch the project to build the correct path. The signature of this function accepts an object // that is not a CommonObject, and even a null when a module is given, so we must not call a method // that only a CommonObject owns without testing it exists. if (is_object($object) && method_exists($object, 'fetchProject')) { $object->fetchProject(); } // The ref must be sanitized with dol_sanitizeFileName() and not only with dol_sanitizePathName() // done at the end of this function, because a project ref is a user input that may contain a '/', // a ':' or an accented char. dol_sanitizePathName() keeps them, so we would not return the // directory used by projet/tasks/document.php, that sanitizes the ref with dol_sanitizeFileName(). if (!empty($object->project->ref)) { $subdirectory = '/'.dol_sanitizeFileName($object->project->ref); } break; case 'action': case 'actioncomm': case 'event': $module = 'agenda'; break; default: break; } // Get the relative path of directory if ($mode == 'output' || $mode == 'outputrel' || $mode == 'version') { if (isset($conf->$module) && property_exists($conf->$module, 'multidir_output')) { $s = ''; if ($mode != 'outputrel') { // An entity with no declared directory returned an undefined index, so an empty path that // made the caller read or write a relative path under the web root. Answer the error instead. $entity = (int) (empty($object->entity) ? $conf->entity : $object->entity); if (!isset($conf->$module->multidir_output[$entity])) { return 'error-diroutput-not-defined-for-this-entity-and-object='.$module; } $s = $conf->$module->multidir_output[$entity] . $subdirectory; } if ($forobject && $object->id > 0) { $s .= ($mode != 'outputrel' ? '/' : '') . get_exdir(0, 0, 0, 0, $object); } return dol_sanitizePathName($s); } elseif (isset($conf->$module) && property_exists($conf->$module, 'dir_output')) { $s = ''; if ($mode != 'outputrel') { $s = $conf->$module->dir_output . $subdirectory; } if ($forobject && $object->id > 0) { $s .= ($mode != 'outputrel' ? '/' : '') . get_exdir(0, 0, 0, 0, $object); } return dol_sanitizePathName($s); } else { return 'error-diroutput-not-defined-for-this-entity-and-object='.$module; } } elseif ($mode == 'temp') { if (isset($conf->$module) && property_exists($conf->$module, 'multidir_temp')) { // Same guard as the 'output' mode above, see the comment there $entity = (int) (empty($object->entity) ? $conf->entity : $object->entity); if (!isset($conf->$module->multidir_temp[$entity])) { return 'error-dirtemp-not-defined-for-this-entity-and-object='.$module; } return dol_sanitizePathName($conf->$module->multidir_temp[$entity]); } elseif (isset($conf->$module) && property_exists($conf->$module, 'dir_temp')) { return dol_sanitizePathName($conf->$module->dir_temp); } else { return 'error-dirtemp-not-defined-for-this-entity-and-object='.$module; } } else { return 'error-bad-value-for-mode'; } } /** * Return the full path of the directory where a module (or an object of a module) stores its temporary files. * Path may depends on the entity if a multicompany module is enabled. * * @param CommonObject $object Dolibarr common object * @param string $module Override object element, for example to use 'mycompany' instead of 'societe' * @param int $forobject Return the more complete path for the given object instead of for the module only. * @return string|null The path of the relative temp directory of the module */ function getMultidirTemp($object, $module = '', $forobject = 0) { return getMultidirOutput($object, $module, $forobject, 'temp'); } /** * Return the full path of the directory where a module (or an object of a module) stores its versioned files. * Path may depends on the entity if a multicompany module is enabled. * * @param CommonObject $object Dolibarr common object * @param string $module Override object element, for example to use 'mycompany' instead of 'societe' * @param int $forobject Return the more complete path for the given object instead of for the module only. * @return string|null The path of the relative version directory of the module */ function getMultidirVersion($object, $module = '', $forobject = 0) { return getMultidirOutput($object, $module, $forobject, 'version'); } /** * Return a Dolibarr global constant string value * * @param string $key Key to return value, return $default if not set * @param string|int|float $default Value to return if not defined * @return string Value returned * @see getDolUserString() */ function getDolGlobalString($key, $default = '') { global $conf; return (string) (isset($conf->global->$key) ? $conf->global->$key : $default); } /** * Return the list of unauthorized characters in user logins. * * @return string */ function getDolGlobalLoginBadCharUnauthorized() { global $dolibarr_login_badcharunauthorized; if (isset($dolibarr_login_badcharunauthorized)) { if ($dolibarr_login_badcharunauthorized === 'MAIN_LOGIN_BADCHARUNAUTHORIZED') { return getDolGlobalString('MAIN_LOGIN_BADCHARUNAUTHORIZED', ',@<>"\''); } return (string) $dolibarr_login_badcharunauthorized; } return ',@<>"\''; } /** * Return a Dolibarr global constant int value. * The constants $conf->global->xxx are loaded by the script master.inc.php included at begin of any PHP page. * * @param string $key Key to return value, return $default if not set * @param int $default Value to return if not defined * @return int Value returned * @see getDolUserInt() */ function getDolGlobalInt($key, $default = 0) { global $conf; return (int) (isset($conf->global->$key) ? $conf->global->$key : $default); } /** * Return a Dolibarr global constant float value. * The constants $conf->global->xxx are loaded by the script master.inc.php included at begin of any PHP page. * * @param string $key Key to return value, return $default if not set * @param float $default Value to return if not defined * @return float Value returned * @see getDolUserInt() */ function getDolGlobalFloat($key, $default = 0) { global $conf; return (float) (isset($conf->global->$key) ? $conf->global->$key : $default); } /** * Return a Dolibarr global constant boolean value. * The constants $conf->global->xxx are loaded by the script master.inc.php included at begin of any PHP page. * * @param string $key Key to return value, return $default if not set * @param bool $default Value to return if not defined * @return bool Value returned */ function getDolGlobalBool($key, $default = false) { global $conf; return (bool) ($conf->global->$key ?? $default); } /** * Return the main currency ('EUR', 'USD', ...) * * @return string Value returned */ function getDolCurrency() { global $conf; return (string) $conf->currency; } /** * Return if render must be optimized for small screen * * @return string Value returned */ function getDolOptimizeSmallScreen() { global $conf; return (string) $conf->dol_optimize_smallscreen; } /** * Return the current entity * * @return int Value returned */ function getDolEntity() { global $conf; return (int) $conf->entity; } /** * Return the current entity * * @return string Value returned */ function getDolDBType() { global $conf; return $conf->db->type; } /** * Return the default context page string * * @param string $s Page path * @return string Value returned */ function getDolDefaultContextPage($s) { return str_replace('_', '', basename(dirname($s)).basename($s, '.php')); } /** * Return Dolibarr user constant string value * * @param string $key Key to return value, return '' if not set * @param string|int|float $default Value to return * @param User $tmpuser To get another user than current user * @return string * @see getDolGlobalString() */ function getDolUserString($key, $default = '', $tmpuser = null) { if (empty($tmpuser)) { global $user; $tmpuser = $user; } return (string) (isset($tmpuser->conf->$key) ? $tmpuser->conf->$key : $default); } /** * Return Dolibarr user constant int value * * @param string $key Key to return value, return 0 if not set * @param int $default Value to return * @param User $tmpuser To get another user than current user * @return int */ function getDolUserInt($key, $default = 0, $tmpuser = null) { if (empty($tmpuser)) { global $user; $tmpuser = $user; } return (int) (isset($tmpuser->conf->$key) ? $tmpuser->conf->$key : $default); } /** * This mapping defines the conversion to the current internal * names from the alternative allowed names (including effectively deprecated * and future new names not yet used as internal names). * * This allows to map any temporary or future name to the effective internal name. * The value is typically the name of module's root directory. */ define( 'MODULE_MAPPING', array( // Map deprecated names to new names 'adherent' => 'member', // Has new directory 'member_type' => 'adherent_type', // No directory, but file called adherent_type 'banque' => 'bank', // Has new directory 'contrat' => 'contract', // Has new directory 'entrepot' => 'stock', // Has new directory 'projet' => 'project', // Has new directory 'categorie' => 'category', // Has old directory 'commande' => 'order', // Has old directory 'expedition' => 'shipping', // Has old directory 'facture' => 'invoice', // Has old directory 'fichinter' => 'intervention', // Has old directory 'ficheinter' => 'intervention', // Backup for 'fichinter' 'propale' => 'propal', // Has old directory 'societe' => 'thirdparty', // Has old directory 'socpeople' => 'contact', // Has old directory 'fournisseur' => 'supplier', // Has old directory 'actioncomm' => 'agenda', // NO module directory (public dir agenda) 'product_price' => 'productprice', // NO directory 'product_fournisseur_price' => 'productsupplierprice', // NO directory ) ); /** * Is Dolibarr module enabled * * @param string $module Module name to check * @return boolean True if module is enabled */ function isModEnabled($module) { global $conf; if (!empty($conf->modules[$module])) { return true; // Most calls use the real name of the module: no need to look at the old/new names mapping } // Fix old names (map to new names). The mappings are constant for the request, so they are built once: this function is // called thousands of times per page (hooks, rights, logs...), and array_flip() on each call was most of its cost. static $arrayconv = null; static $arrayconvbis = null; if ($arrayconv === null) { $arrayconv = MODULE_MAPPING; $arrayconvbis = array_flip(MODULE_MAPPING); if (!getDolGlobalString('MAIN_USE_NEW_SUPPLIERMOD')) { // Special cases: both use the same module. $arrayconv['supplier_order'] = 'fournisseur'; $arrayconv['supplier_invoice'] = 'fournisseur'; } } if (!empty($arrayconv[$module]) && !empty($conf->modules[$arrayconv[$module]])) { return true; } if (!empty($arrayconvbis[$module]) && !empty($conf->modules[$arrayconvbis[$module]])) { return true; } return false; } /** * Return a warning delay * You can use it like this: if (getWarningDelay('module', 'paramlevel1')) * It replaces old syntax: if ($conf->module->user->rights->module->level1) * * @param string $module Module of permission to check (Example: 'bank', 'contrat', ...) * @param string $parmlevel1 Parameter level1 (Example: 'rappro', 'services', ...) * @param string $parmlevel2 Parameter level2 (Example: 'inactifs', ...) * @return int Return the warning delay */ function getWarningDelay($module, $parmlevel1, $parmlevel2 = '') { global $conf; // For compatibility with bad naming on module $moduletomoduletouse = array( 'invoice' => 'facture', ); $moduleParmsMapping = array( 'product' => 'produit', ); if (!empty($moduletomoduletouse[$module])) { $module = $moduletomoduletouse[$module]; } $warningDelayPath = $parmlevel1; if (!empty($moduleParmsMapping[$warningDelayPath])) { $warningDelayPath = $moduleParmsMapping[$warningDelayPath]; } if ($parmlevel2) { if (!empty($conf->$module) && !empty($conf->$module->$warningDelayPath) && !empty($conf->$module->$warningDelayPath->$parmlevel2) && !empty($conf->$module->$warningDelayPath->$parmlevel2->warning_delay)) { return (int) $conf->$module->$warningDelayPath->$parmlevel2->warning_delay; } } else { if (!empty($conf->$module) && !empty($conf->$module->$warningDelayPath) && !empty($conf->$module->$warningDelayPath->warning_delay)) { return (int) $conf->$module->$warningDelayPath->warning_delay; } } return 0; } /** * isDolTms check if a timestamp is valid. * * @param int|string|null $timestamp timestamp to check * @return bool */ function isDolTms($timestamp) { if ($timestamp === '') { dol_syslog('Using empty string for a timestamp is deprecated, prefer use of null when calling page ' . $_SERVER["PHP_SELF"] . getCallerInfoString(), LOG_DEBUG); return false; } if (is_null($timestamp) || !is_numeric($timestamp)) { return false; } return true; } /** * Return a DoliDB instance (database handler). * * @param string $type Type of database (mysql, pgsql...) * @param string $host Address of database server * @param string $user Authorized username * @param string $pass Password (clear) * @param string $name Name of database * @param int $port Port of database server * @param bool $forcenew Force opening of a genuinely new connection instead of reusing one already opened to the same server/database in this process (only meaningful for drivers, like pgsql, that may otherwise transparently reuse a matching connection) * @return DoliDB A DoliDB instance */ function getDoliDBInstance($type, $host, $user, $pass, $name, $port, $forcenew = false) { require_once DOL_DOCUMENT_ROOT . "/core/db/" . $type . '.class.php'; $class = 'DoliDB' . ucfirst($type); $db = new $class($type, $host, $user, $pass, $name, $port, $forcenew); return $db; } /** * Get list of entity id to use. * * @param string $element Current element. The value is usually coming from $object->element property. Example: * 'societe', 'socpeople', 'actioncomm', 'agenda', 'resource', * 'product', 'productprice', 'stock', 'bom', 'mo', * 'propal', 'supplier_proposal', 'invoice', 'supplier_invoice', 'payment_various', * 'categorie', 'bank_account', 'bank_account', 'adherent', 'user', * 'commande', 'supplier_order', 'expedition', 'intervention', 'survey', * 'contract', 'tax', 'expensereport', 'holiday', 'multicurrency', 'project', * 'email_template', 'event', 'donation' * 'c_paiement', 'c_payment_term', ... * @param int<0,1> $shared 0=Return id of current entity only, * 1=Return id of current entity + shared entities (default) * @param ?CommonObject $currentobject Current object if needed * @return string Entity id(s) to use ( eg. entity IN ('.getEntity(elementname).')' ) */ function getEntity($element, $shared = 1, $currentobject = null) { global $conf, $mc, $hookmanager, $object, $action, $db; if (!is_object($hookmanager)) { include_once DOL_DOCUMENT_ROOT . '/core/class/hookmanager.class.php'; $hookmanager = new HookManager($db); } // fix different element names (France to English) switch ($element) { case 'projet': $element = 'project'; break; case 'contrat': $element = 'contract'; break; // "/contrat/class/contrat.class.php" case 'order_supplier': $element = 'supplier_order'; break; // "/fourn/class/fournisseur.commande.class.php" case 'invoice_supplier': $element = 'supplier_invoice'; break; // "/fourn/class/fournisseur.facture.class.php" } if (is_object($mc)) { $out = $mc->getEntity($element, $shared, $currentobject); } else { $out = ''; $addzero = array('user', 'usergroup', 'cronjob', 'c_email_templates', 'email_template', 'default_values', 'overwrite_trans', 'extrafields'); if (getDolGlobalString('HOLIDAY_ALLOW_ZERO_IN_DIC')) { // this constant break the dictionary admin without Multicompany $addzero[] = 'c_holiday_types'; } if (in_array($element, $addzero)) { $out .= '0,'; } $out .= ((int) $conf->entity); } // Manipulate entities to query on the fly $parameters = array( 'element' => $element, 'shared' => $shared, 'object' => $object, 'currentobject' => $currentobject, 'out' => $out ); // @phan-suppress-next-line PhanTypeMismatchArgumentNullable $reshook = $hookmanager->executeHooks('hookGetEntity', $parameters, $currentobject, $action); // Note that $action and $object may have been modified by some hooks if (is_numeric($reshook)) { if ($reshook == 0 && !empty($hookmanager->resPrint)) { $out .= ',' . $hookmanager->resPrint; // add } elseif ($reshook == 1) { $out = $hookmanager->resPrint; // replace } } return $out; } /** * Set entity id to use when to create an object * * @param CommonObject $currentobject Current object * @return int Entity id to use ( eg. entity = '.setEntity($object) ) */ function setEntity($currentobject) { global $conf, $mc; if (is_object($mc) && method_exists($mc, 'setEntity')) { return $mc->setEntity($currentobject); } else { return ((is_object($currentobject) && $currentobject->id > 0 && ((int) $currentobject->entity) > 0) ? (int) $currentobject->entity : $conf->entity); } } /** * Return if string has a name dedicated to store a secret * * @param string $keyname Name of key to test * @return boolean True if key is used to store a secret */ function isASecretKey($keyname) { return preg_match('/(_pass|password|_pw|_key|securekey|serverkey|secret\d?|p12key|exportkey|_PW_[a-z]+|token)$/i', $keyname); } /** * Return a numeric value into an Excel like column number. So 0 return 'A', 1 returns 'B'..., 26 return 'AA' * * @param int|string $n Numeric value * @return string Column in Excel format */ function num2Alpha($n) { $r = ''; for ($r = ""; $n >= 0; $n = intval($n / 26) - 1) { $r = chr($n % 26 + 0x41) . $r; } return $r; } /** * Return information about user browser * * Returns array with the following format: * array( * 'browsername' => Browser name (firefox|chrome|iceweasel|epiphany|safari|opera|ie|unknown) * 'browserversion' => Browser version. Empty if unknown * 'browseros' => Set with mobile OS (android|blackberry|ios|palm|symbian|webos|maemo|windows|unknown) * 'layout' => (tablet|phone|classic) * 'phone' => empty if not mobile, (android|blackberry|ios|palm|unknown) if mobile * 'tablet' => true/false * ) * * @param string $user_agent Content of $_SERVER["HTTP_USER_AGENT"] variable * @return array{browsername:string,browserversion:string,browseros:string,browserua:string,layout:string,phone:string,tablet:bool} Check function documentation */ function getBrowserInfo($user_agent) { include_once DOL_DOCUMENT_ROOT . '/includes/mobiledetect/mobiledetectlib/Mobile_Detect.php'; $name = 'unknown'; $version = ''; $os = 'unknown'; $phone = ''; $user_agent = substr($user_agent, 0, 512); // Avoid to process too large user agent // @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal Bad definition of Mobile_Detect function $detectmobile = new Mobile_Detect(null, $user_agent); $tablet = $detectmobile->isTablet(); if ($detectmobile->isMobile()) { $phone = 'unknown'; // If phone/smartphone, we set phone os name. if ($detectmobile->is('AndroidOS')) { $os = $phone = 'android'; } elseif ($detectmobile->is('BlackBerryOS')) { $os = $phone = 'blackberry'; } elseif ($detectmobile->is('iOS')) { $os = 'ios'; $phone = 'iphone'; } elseif ($detectmobile->is('PalmOS')) { $os = $phone = 'palm'; } elseif ($detectmobile->is('SymbianOS')) { $os = 'symbian'; } elseif ($detectmobile->is('webOS')) { $os = 'webos'; } elseif ($detectmobile->is('MaemoOS')) { $os = 'maemo'; } elseif ($detectmobile->is('WindowsMobileOS') || $detectmobile->is('WindowsPhoneOS')) { $os = 'windows'; } } // OS if (preg_match('/linux/i', $user_agent)) { $os = 'linux'; } elseif (preg_match('/macintosh/i', $user_agent)) { $os = 'macintosh'; } elseif (preg_match('/windows/i', $user_agent)) { $os = 'windows'; } // Name $reg = array(); if (preg_match('/firefox(\/|\s)([\d\.]*)/i', $user_agent, $reg)) { $name = 'firefox'; $version = empty($reg[2]) ? '' : $reg[2]; } elseif (preg_match('/edge(\/|\s)([\d\.]*)/i', $user_agent, $reg)) { $name = 'edge'; $version = empty($reg[2]) ? '' : $reg[2]; } elseif (preg_match('/chrome(\/|\s)([\d\.]+)/i', $user_agent, $reg)) { $name = 'chrome'; $version = empty($reg[2]) ? '' : $reg[2]; } elseif (preg_match('/chrome/i', $user_agent, $reg)) { // we can have 'chrome (Mozilla...) chrome x.y' in one string $name = 'chrome'; } elseif (preg_match('/iceweasel/i', $user_agent)) { $name = 'iceweasel'; } elseif (preg_match('/epiphany/i', $user_agent)) { $name = 'epiphany'; } elseif (preg_match('/safari(\/|\s)([\d\.]*)/i', $user_agent, $reg)) { $name = 'safari'; $version = empty($reg[2]) ? '' : $reg[2]; } elseif (preg_match('/opera(\/|\s)([\d\.]*)/i', $user_agent, $reg)) { // Safari is often present in string for mobile but its not. $name = 'opera'; $version = empty($reg[2]) ? '' : $reg[2]; } elseif (preg_match('/(MSIE\s([0-9]+\.[0-9]))|.*(Trident\/[0-9]+.[0-9];.*rv:([0-9]+\.[0-9]+))/i', $user_agent, $reg)) { $name = 'ie'; $version = end($reg); } elseif (preg_match('/(Windows NT\s([0-9]+\.[0-9])).*(Trident\/[0-9]+.[0-9];.*rv:([0-9]+\.[0-9]+))/i', $user_agent, $reg)) { // MS products at end $name = 'ie'; $version = end($reg); } elseif (preg_match('/l[iy]n(x|ks)(\(|\/|\s)*([\d\.]+)/i', $user_agent, $reg)) { // MS products at end $name = 'textbrowser'; $version = empty($reg[3]) ? '' : $reg[3]; } elseif (preg_match('/w3m\/([\d\.]+)/i', $user_agent, $reg)) { // MS products at end $name = 'textbrowser'; $version = empty($reg[1]) ? '' : $reg[1]; } if ($tablet) { $layout = 'tablet'; } elseif ($phone) { $layout = 'phone'; } else { $layout = 'classic'; } return array( 'browsername' => $name, 'browserversion' => $version, 'browseros' => $os, 'browserua' => $user_agent, 'layout' => $layout, // tablet, phone, classic 'phone' => $phone, // deprecated 'tablet' => $tablet // deprecated ); } /** * Function called at end of web php process * * @return void */ function dol_shutdown() { global $db; $disconnectdone = false; $depth = 0; if (is_object($db) && !empty($db->connected)) { $depth = $db->transaction_opened; $disconnectdone = $db->close(); } dol_syslog("--- End access to " . (empty($_SERVER["PHP_SELF"]) ? 'unknown' : $_SERVER["PHP_SELF"]) . (($disconnectdone && $depth) ? ' (Warn: db disconnection forced, transaction depth was ' . $depth . ')' : ''), (($disconnectdone && $depth) ? LOG_WARNING : LOG_INFO)); } /** * Return true if we are in a context of submitting the parameter $paramname from a POST of a form. * Warning: * For action=add, use: $var = GETPOST('var'); // No GETPOSTISSET, so GETPOST always called and default value is retrieved if not a form POST, and value of form is retrieved if it is a form POST. * For action=update, use: $var = GETPOSTISSET('var') ? GETPOST('var') : $object->var; * * @param string $paramname Name or parameter to test * @return boolean True if we have just submit a POST or GET request with the parameter provided (even if param is empty) */ function GETPOSTISSET($paramname) { $isset = false; $relativepathstring = $_SERVER["PHP_SELF"]; // Clean $relativepathstring if (constant('DOL_URL_ROOT')) { $relativepathstring = preg_replace('/^' . preg_quote(constant('DOL_URL_ROOT'), '/') . '/', '', $relativepathstring); } $relativepathstring = ltrim($relativepathstring, '/'); $relativepathstring = preg_replace('/^custom\//', '', $relativepathstring); // Code for search criteria persistence. // Retrieve values if restore_lastsearch_values if (!empty($_GET['restore_lastsearch_values'])) { // Use $_GET here and not GETPOST if (!empty($_SESSION['lastsearch_values_' . $relativepathstring])) { // If there is saved values $tmp = json_decode($_SESSION['lastsearch_values_' . $relativepathstring], true); if (is_array($tmp)) { foreach ($tmp as $key => $val) { if ($key == $paramname) { // We are on the requested parameter $isset = true; break; } } } } // If there is saved contextpage, limit, page or mode if ($paramname == 'contextpage' && !empty($_SESSION['lastsearch_contextpage_' . $relativepathstring])) { $isset = true; } elseif ($paramname == 'limit' && !empty($_SESSION['lastsearch_limit_' . $relativepathstring])) { $isset = true; } elseif ($paramname == 'page' && !empty($_SESSION['lastsearch_page_' . $relativepathstring])) { $isset = true; } elseif ($paramname == 'mode' && !empty($_SESSION['lastsearch_mode_' . $relativepathstring])) { $isset = true; } } else { $isset = (isset($_POST[$paramname]) || isset($_GET[$paramname])); // We must keep $_POST and $_GET here } return $isset; } /** * Return true if the parameter $paramname is submit from a POST OR GET as an array. * Can be used before GETPOST to know if the $check param of GETPOST need to check an array or a string * * @param string $paramname Name or parameter to test * @param int<0,3> $method Type of method (0 = get then post, 1 = only get, 2 = only post, 3 = post then get) * @return bool True if we have just submit a POST or GET request with the parameter provided (even if param is empty) */ function GETPOSTISARRAY($paramname, $method = 0) { // for $method test need return the same $val as GETPOST if (empty($method)) { $val = isset($_GET[$paramname]) ? $_GET[$paramname] : (isset($_POST[$paramname]) ? $_POST[$paramname] : ''); } elseif ($method == 1) { $val = isset($_GET[$paramname]) ? $_GET[$paramname] : ''; } elseif ($method == 2) { $val = isset($_POST[$paramname]) ? $_POST[$paramname] : ''; } elseif ($method == 3) { $val = isset($_POST[$paramname]) ? $_POST[$paramname] : (isset($_GET[$paramname]) ? $_GET[$paramname] : ''); } else { $val = 'BadFirstParameterForGETPOST'; } return is_array($val); } /** * Return the value of a $_GET or $_POST supervariable, converted into integer. * Use the property $user->default_values[path]['creatform'] and/or $user->default_values[path]['filters'] and/or $user->default_values[path]['sortorder'] * Note: The property $user->default_values is loaded by main.php when loading the user. * * @param string $paramname Name of the $_GET or $_POST parameter * @param int<0,3> $method Type of method (0 = $_GET then $_POST, 1 = only $_GET, 2 = only $_POST, 3 = $_POST then $_GET) * @param int $nodefault Force disable of default values set from the admin setup page * @return int Value converted into integer */ function GETPOSTINT($paramname, $method = 0, $nodefault = 0) { return (int) GETPOST($paramname, 'int', $method, null, null, 0, $nodefault); } /** * Return the value of a $_GET or $_POST supervariable, converted into float. * Warning: This function assumes by default that the input is a number entered by end user in user format in local language (with possible thousands separator and decimal separator). * If it is not the case, use the parameter $option = 1 instead. * * @param string $paramname Name of the $_GET or $_POST parameter * @param ''|'MU'|'MT'|'MS'|'CU'|'CT'|int $rounding Type of rounding ('', 'MU', 'MT, 'MS', 'CU', 'CT', integer) {@see price2num()} * @param int<0,2> $option Put 1 if you know that content is already universal format number (so no correction on decimal will be done) * Put 2 if you know that number is a user input (so we know we have to fix decimal separator). * Use 0 if unknown (never use this anymore, automatic detection is not reliable with some languages). * @return float Value converted into float * @since Dolibarr V20 */ function GETPOSTFLOAT($paramname, $rounding = '', $option = 2) { // price2num() can be used to round to an expected accuracy and/or to sanitize any valid user input (such as "1 234.5", "1 234,5", "1'234,5", "1·234,5", "1,234.5", etc.) return (float) price2num(GETPOST($paramname), $rounding, $option); } /** * Helper function that combines values of a dolibarr DatePicker (such as Form::selectDate) for year, month, day (and * optionally hour, minute, second) fields to return a timestamp. * * @param string $prefix Prefix used to build the date selector (for instance using Form::selectDate). Example: 'select_datec' * @param string $hourTime 'getpost' or 'getpostend' to include hour, minute, second values from the HTTP request, * or 'XX:YY:ZZ' to set hour, minute, second respectively, for example '23:59:59' * or 'end' means '23:59:59' * or '' means '00:00:00' (default) * @param int|string $gm Passed to dol_mktime. In most cases, when used with 'getpost' or 'getpostend', it should be 'tzuserrel'. Use 'auto' if you need dates related to 'tzserver' (like in accountancy). * @param string $saverestore Use a string family context to save retrieved date so it will be used on the next retrieval for the same family context (if value not already defined in parameters). * @return int|string Date as a timestamp, '' or false if error * * @see dol_mktime() */ function GETPOSTDATE($prefix, $hourTime = '', $gm = 'auto', $saverestore = '') { $m = array(); if ($hourTime === 'getpost' || $hourTime === 'getpostend') { $hour = (GETPOSTISSET($prefix . 'hour') && GETPOSTINT($prefix . 'hour') >= 0) ? GETPOSTINT($prefix . 'hour') : ($hourTime === 'getpostend' ? 23 : 0); $minute = (GETPOSTISSET($prefix . 'min') && GETPOSTINT($prefix . 'min') >= 0) ? GETPOSTINT($prefix . 'min') : ($hourTime === 'getpostend' ? 59 : 0); $second = (GETPOSTISSET($prefix . 'sec') && GETPOSTINT($prefix . 'sec') >= 0) ? GETPOSTINT($prefix . 'sec') : ($hourTime === 'getpostend' ? 59 : 0); } elseif (preg_match('/^(\d\d):(\d\d):(\d\d)$/', $hourTime, $m)) { $hour = intval($m[1]); $minute = intval($m[2]); $second = intval($m[3]); } elseif ($hourTime === 'end') { $hour = 23; $minute = 59; $second = 59; } else { $hour = $minute = $second = 0; } if ( $saverestore && !GETPOSTISSET($prefix . 'day') && !GETPOSTISSET($prefix . 'month') && !GETPOSTISSET($prefix . 'year') && isset($_SESSION['DOLDATE_' . $saverestore . '_day']) && isset($_SESSION['DOLDATE_' . $saverestore . '_month']) && isset($_SESSION['DOLDATE_' . $saverestore . '_year']) ) { $day = $_SESSION['DOLDATE_' . $saverestore . '_day']; $month = $_SESSION['DOLDATE_' . $saverestore . '_month']; $year = $_SESSION['DOLDATE_' . $saverestore . '_year']; } else { $month = GETPOSTINT($prefix . 'month'); $day = GETPOSTINT($prefix . 'day'); $year = GETPOSTINT($prefix . 'year'); } // normalize out of range values $hour = (int) min($hour, 23); $minute = (int) min($minute, 59); $second = (int) min($second, 59); if ($saverestore) { $_SESSION['DOLDATE_' . $saverestore . '_day'] = $day; $_SESSION['DOLDATE_' . $saverestore . '_month'] = $month; $_SESSION['DOLDATE_' . $saverestore . '_year'] = $year; } //print "$hour, $minute, $second, $month, $day, $year, $gm
"; return dol_mktime($hour, $minute, $second, $month, $day, $year, $gm); } /** * Return value of a param into GET or POST supervariable. * Use the property $user->default_values[path]['createform'] and/or $user->default_values[path]['filters'] and/or $user->default_values[path]['sortorder'] * Note: The property $user->default_values is loaded by main.php when loading the user. * * @param string $paramname Name of parameter to found * @param 'int'|'intcomma'|'array'|'array:int'|'array:intcomma'|'array:alpha'|'array:alphanohtml'|'array:aZ09'|'array:restricthtml'|'password'|'email'|'alpha'|'alphanohtml'|'nohtml'|'restricthtml'|'alphawithlgt'|'aZ09'|'aZ'|'aZ09arobase'|'aZ09comma'|'url'|'san_alpha'|'custom'|'none'|'restricthtmlallowclass'|'restricthtmlallowunvalid'|'restricthtmlallowiframe'|'restricthtmlallowlinkscript'|'' $check Type of check * '' or 'none'=no check (deprecated) * 'password'=allow characters for a password * 'email'=allow characters for an email "email@domain.com" * 'url'=allow characters for an url * 'array', 'array:restricthtml' or 'array:aZ09' to check it's an array * 'int'=check it's numeric (integer or float) * 'intcomma'=check it's integer+comma ('1,2,3,4...') * 'alphanohtml'=check there is no html content and no " and no ../ ('alpha' is an alias of 'alphanohtml') * 'alphawithlgt'=alpha with lgt and no " and no ../ (Can be used for email string like "Name ") * 'aZ'=check it's a-Z only * 'aZ09'=check it's simple alpha string (recommended for keys, it includes a-Z0-9_\-\.) * 'aZ09arobase'=check it's a string for an element type ('myobject@mymodule') * 'aZ09comma'=check it's a string for a sortfield or sortorder * 'san_alpha'=Use filter_var with FILTER_SANITIZE_STRING (do not use this for free text string) * 'nohtml'=check there is no html content * 'restricthtml'=check html content is restricted to some tags only * 'custom'= custom filter specify $filter and $options) * 'restricthtmlallowclass' * 'restricthtmlallowunvalid' * 'restricthtmlallowiframe' * 'restricthtmlallowlinkscript' * @param int $method Type of method (0 = get then post, 1 = only get, 2 = only post, 3 = post then get) * @param ?int $filter Filter to apply when $check is set to 'custom'. (See http://php.net/manual/en/filter.filters.php for details) * @param mixed $options Options to pass to filter_var when $check is set to 'custom' * @param int $noreplace Force disable of replacement of __xxx__ strings. * @param int $nodefault Force disable of default values set from the admin setup page * @return string|array Value found (string or array), or '' if check fails * @phpstan-return ( * $check is 'int' ? numeric-string|'' : * $check is 'array:int' ? numeric-string[]|array{} : * $check is 'array' | 'array:aZ09' | 'array:alpha' | 'array:intcomma' | 'array:restricthtml' ? string[] : * $check is 'alpha' | 'aZ' | 'aZ09' | 'aZ09arobase' | 'aZ09comma' | 'password' | 'email' | 'url' | 'alphanohtml' |'nohtml' | 'restricthtml' | 'alphawithlgt' | 'intcomma' | 'restricthtmlallowclass' | 'restricthtmlallowunvalid' | 'restricthtmlallowiframe' | 'restricthtmlallowlinkscript' ? string : string|array * ) */ function GETPOST($paramname, $check = 'alphanohtml', $method = 0, $filter = null, $options = null, $noreplace = 0, $nodefault = 0) { global $langs, $user; if (empty($paramname)) { // Explicit test for null for phan. return 'BadFirstParameterForGETPOST'; } if (empty($check)) { dol_syslog("Deprecated use of GETPOST, called with 1st param = " . $paramname . " and a 2nd param that is '', when calling page " . $_SERVER["PHP_SELF"], LOG_WARNING); // Enable this line to know who call the GETPOST with '' $check parameter. //var_dump(getCallerInfoString()); } if (in_array($paramname, array('sortfield', 'sortorder'))) { // Force the $check to a more appropriated value $check = 'aZ09comma'; } if (empty($method)) { $out = isset($_GET[$paramname]) ? $_GET[$paramname] : (isset($_POST[$paramname]) ? $_POST[$paramname] : ''); } elseif ($method == 1) { $out = isset($_GET[$paramname]) ? $_GET[$paramname] : ''; } elseif ($method == 2) { $out = isset($_POST[$paramname]) ? $_POST[$paramname] : ''; } elseif ($method == 3) { $out = isset($_POST[$paramname]) ? $_POST[$paramname] : (isset($_GET[$paramname]) ? $_GET[$paramname] : ''); } else { return 'BadThirdParameterForGETPOST'; } $relativepathstring = ''; // For static analysis - looks possibly undefined if not set. if (empty($method) || $method == 3 || $method == 4) { $relativepathstring = (empty($_SERVER["PHP_SELF"]) ? '' : $_SERVER["PHP_SELF"]); // Clean $relativepathstring if (constant('DOL_URL_ROOT')) { $relativepathstring = preg_replace('/^' . preg_quote(constant('DOL_URL_ROOT'), '/') . '/', '', $relativepathstring); } $relativepathstring = ltrim($relativepathstring, '/'); $relativepathstring = preg_replace('/^custom\//', '', $relativepathstring); // Code for search criteria persistence. // Retrieve saved values if restore_lastsearch_values is set if (!empty($_GET['restore_lastsearch_values'])) { // Use $_GET here and not GETPOST if (!empty($_SESSION['lastsearch_values_' . $relativepathstring])) { // If there is saved values $tmp = json_decode($_SESSION['lastsearch_values_' . $relativepathstring], true); if (is_array($tmp)) { foreach ($tmp as $key => $val) { if ($key == $paramname) { // We are on the requested parameter $out = $val; break; } } } } // If there is saved contextpage, page or limit if ($paramname == 'contextpage' && !empty($_SESSION['lastsearch_contextpage_' . $relativepathstring])) { $out = $_SESSION['lastsearch_contextpage_' . $relativepathstring]; } elseif ($paramname == 'limit' && !empty($_SESSION['lastsearch_limit_' . $relativepathstring])) { $out = $_SESSION['lastsearch_limit_' . $relativepathstring]; } elseif ($paramname == 'page' && !empty($_SESSION['lastsearch_page_' . $relativepathstring])) { $out = $_SESSION['lastsearch_page_' . $relativepathstring]; } elseif ($paramname == 'mode' && !empty($_SESSION['lastsearch_mode_' . $relativepathstring])) { $out = $_SESSION['lastsearch_mode_' . $relativepathstring]; } } elseif (!isset($_GET['sortfield'])) { // Else, retrieve default values if we are not doing a sort // If we did a click on a field to sort, we do no apply default values. Same if option MAIN_ENABLE_DEFAULT_VALUES is not set if (!empty($_GET['action']) && $_GET['action'] == 'create' && !isset($_GET[$paramname]) && !isset($_POST[$paramname])) { // Search default value from $object->field global $object; '@phan-var-force CommonObject $object'; // Suppose it's a CommonObject for analysis, but other objects have the $fields field as well if (is_object($object) && isset($object->fields[$paramname]['default'])) { // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset $out = $object->fields[$paramname]['default']; } } if (getDolGlobalString('MAIN_ENABLE_DEFAULT_VALUES')) { if (!empty($_GET['action']) && (preg_match('/^create/', $_GET['action']) || preg_match('/^presend/', $_GET['action'])) && !isset($_GET[$paramname]) && !isset($_POST[$paramname])) { // Now search in setup to overwrite default values if (!empty($user->default_values)) { // $user->default_values defined from menu 'Setup - Default values' if (isset($user->default_values[$relativepathstring]['createform'])) { foreach ($user->default_values[$relativepathstring]['createform'] as $defkey => $defval) { $qualified = 0; if ($defkey != '_noquery_') { $tmpqueryarraytohave = explode('&', $defkey); $tmpqueryarraywehave = explode('&', dol_string_nohtmltag($_SERVER['QUERY_STRING'])); $foundintru = 0; foreach ($tmpqueryarraytohave as $tmpquerytohave) { if (!in_array($tmpquerytohave, $tmpqueryarraywehave)) { $foundintru = 1; } } if (!$foundintru) { $qualified = 1; } } else { $qualified = 1; } if ($qualified) { if (isset($user->default_values[$relativepathstring]['createform'][$defkey][$paramname])) { $out = $user->default_values[$relativepathstring]['createform'][$defkey][$paramname]; break; } } } } } } elseif (!empty($paramname) && !isset($_GET[$paramname]) && !isset($_POST[$paramname]) && empty($nodefault)) { // Management of default search_filters and sort order if (!empty($user->default_values)) { // $user->default_values defined from menu 'Setup - Default values' //var_dump($user->default_values[$relativepathstring]); if ($paramname == 'sortfield' || $paramname == 'sortorder') { // Sorted on which fields ? ASC or DESC ? if (isset($user->default_values[$relativepathstring]['sortorder'])) { // Even if paramname is sortfield, data are stored into ['sortorder...'] foreach ($user->default_values[$relativepathstring]['sortorder'] as $defkey => $defval) { $qualified = 0; if ($defkey != '_noquery_') { $tmpqueryarraytohave = explode('&', $defkey); $tmpqueryarraywehave = explode('&', dol_string_nohtmltag($_SERVER['QUERY_STRING'])); $foundintru = 0; foreach ($tmpqueryarraytohave as $tmpquerytohave) { if (!in_array($tmpquerytohave, $tmpqueryarraywehave)) { $foundintru = 1; } } if (!$foundintru) { $qualified = 1; } } else { $qualified = 1; } if ($qualified) { $forbidden_chars_to_replace = array(" ", "'", "/", "\\", ":", "*", "?", "\"", "<", ">", "|", "[", "]", ";", "="); // we accept _, -, . and , foreach ($user->default_values[$relativepathstring]['sortorder'][$defkey] as $key => $val) { if ($out) { $out .= ', '; } if ($paramname == 'sortfield') { $out .= dol_string_nospecial($key, '', $forbidden_chars_to_replace); } if ($paramname == 'sortorder') { $out .= dol_string_nospecial($val, '', $forbidden_chars_to_replace); } } //break; // No break for sortfield and sortorder so we can cumulate fields (is it really useful ?) } } } } elseif (isset($user->default_values[$relativepathstring]['filters'])) { foreach ($user->default_values[$relativepathstring]['filters'] as $defkey => $defval) { // $defkey is a querystring like 'a=b&c=d', $defval is key of user if (!empty($_GET['disabledefaultvalues'])) { // If set of default values has been disabled by a request parameter continue; } $qualified = 0; if ($defkey != '_noquery_') { $tmpqueryarraytohave = explode('&', $defkey); $tmpqueryarraywehave = explode('&', dol_string_nohtmltag($_SERVER['QUERY_STRING'])); $foundintru = 0; foreach ($tmpqueryarraytohave as $tmpquerytohave) { if (!in_array($tmpquerytohave, $tmpqueryarraywehave)) { $foundintru = 1; } } if (!$foundintru) { $qualified = 1; } } else { $qualified = 1; } if ($qualified && isset($user->default_values[$relativepathstring]['filters'][$defkey][$paramname])) { // We must keep $_POST and $_GET here if (isset($_POST['search_all']) || isset($_GET['search_all'])) { // We made a search from quick search menu, do we still use default filter ? if (!getDolGlobalString('MAIN_DISABLE_DEFAULT_FILTER_FOR_QUICK_SEARCH')) { $forbidden_chars_to_replace = array(" ", "'", "/", "\\", ":", "*", "?", "\"", "<", ">", "|", "[", "]", ";", "="); // we accept _, -, . and , $out = dol_string_nospecial($user->default_values[$relativepathstring]['filters'][$defkey][$paramname], '', $forbidden_chars_to_replace); } } else { $forbidden_chars_to_replace = array(" ", "'", "/", "\\", ":", "*", "?", "\"", "<", ">", "|", "[", "]", ";", "="); // we accept _, -, . and , $out = dol_string_nospecial($user->default_values[$relativepathstring]['filters'][$defkey][$paramname], '', $forbidden_chars_to_replace); } break; } } } } } } } } // Replace substitution variables for GETPOST (used to get final url with variable parameters or final default value, when using variable parameters __XXX__ in the GET URL) // Example of variables: __DAY__, __MONTH__, __YEAR__, __MYCOMPANY_COUNTRY_ID__, __USER_ID__, ... // We do this only if var is a GET. If it is a POST, may be we want to post the text with vars as the setup text. '@phan-var-force string $paramname'; if (!is_array($out) && empty($_POST[$paramname]) && empty($noreplace)) { if (preg_match('/__([A-Z0-9]+(?:_[A-Z0-9]+){0,3})__/i', $out)) { // If there is at least one substitution key, we try to replace all known substitution keys $substitutionarray = getCommonSubstitutionArray($langs, 0, null, $user, array('mycompany', 'date', 'system', 'user')); complete_substitutions_array($substitutionarray, $langs, $user); $out = make_substitutions($out, $substitutionarray, $langs); } } // Check type of variable and make sanitization according to this if (preg_match('/^array/', $check)) { // If 'array' or 'array:restricthtml' or 'array:aZ09' or 'array:int' $tmpcheck = 'alphanohtml'; if ($out === null || $out === '') { $out = array(); } elseif (!is_array($out)) { $out = explode(',', $out); } else { $tmparray = explode(':', $check); if (!empty($tmparray[1])) { $tmpcheck = $tmparray[1]; } } foreach ($out as $outkey => $outval) { $out[$outkey] = sanitizeVal($outval, $tmpcheck, $filter, $options); } } else { // If field name is 'search_xxx' then we force the add of space after each < and > (when following char is numeric) because it means // we use the < or > to make a search on a numeric value to do higher or lower so we can add a space to break html tags if (strpos($paramname, 'search_') === 0) { $out = preg_replace('/([<>])([-+]?\d)/', '\1 \2', $out); } // @phan-suppress-next-line UnknownSanitizeType $out = sanitizeVal($out, $check, $filter, $options); } // Sanitizing for special parameters. // Note: There is no reason to allow the backtopage/backtopageforcancel/backtopagejs, backtolist or backtourl parameter to contains an external URL. Only relative URLs are allowed. // @TODO Merge backtopage with backtourl // @TODO Rename backtolist into backtopagelist // @TODO Merge urlfrom into backtourl if (preg_match('/^backto/i', $paramname) || preg_match('/^urlfrom/i', $paramname)) { $out = str_replace('\\', '/', $out); // Can be before the loop because only 1 char is replaced. No risk to get it after other replacements. $out = str_replace(array(':', ';', '@', "\t", ' '), '', $out); // Can be before the loop because only 1 char is replaced. No risk to retrieve it after other replacements. do { $oldstringtoclean = $out; $out = str_ireplace(array('javascript', 'vbscript', '&colon', '&#'), '', $out); $out = preg_replace(array('/^[^\?]*%/'), '', $out); // We remove any % chars before the ?. Example in url: '/product/stock/card.php?action=create&backtopage=%2Fdolibarr_dev%2Fhtdocs%2Fpro%25duct%2Fcard.php%3Fid%3Dabc' $out = preg_replace(array('/^[a-z]*\/\s*\/+/i'), '', $out); // We remove schema*// to remove external URL } while ($oldstringtoclean != $out); } // Code for search criteria persistence. // Save data into session if key start with 'search_' if (empty($method) || $method == 3 || $method == 4) { if (preg_match('/^search_/', $paramname) || in_array($paramname, array('sortorder', 'sortfield'))) { //var_dump($paramname.' - '.$out.' '.$user->default_values[$relativepathstring]['filters'][$paramname]); // We save search key only if $out not empty that means: // - posted value not empty, or // - if posted value is empty and a default value exists that is not empty (it means we did a filter to an empty value when default was not). if ($out != '' && isset($user)) { // $out = '0' or 'abc', it is a search criteria to keep $user->lastsearch_values_tmp[$relativepathstring][$paramname] = $out; } } } if ($paramname == 'hashp' && $out == 'shared') { $out = ''; // We refuse to have hashp=shared as a parameter } return $out; } /** * Return a sanitized or empty value after checking value against a rule. * * @param string|mixed[]|null $out Value to check/clear. * @param string $check Type of check/sanitizing * @param ?int $filter Filter to apply when $check is set to 'custom' (deprecated). (See http://php.net/manual/en/filter.filters.php for details) * @param ?mixed $options Options to pass to filter_var when $check is set to 'custom' * @return string|array Value sanitized (string or array). It may be '' if format check fails. */ function sanitizeVal($out = '', $check = 'alphanohtml', $filter = null, $options = null) { // TODO : use class "Validate" to perform tests (and add missing tests) if needed for factorize // Check is done after replacement if ($out === null) { $out = ''; } switch ($check) { case 'none': case 'password': break; case 'int': // Check param is a numeric value (integer but also float or hexadecimal) if (!is_numeric($out)) { $out = ''; } break; case 'intcomma': if (is_array($out)) { $out = implode(',', $out); } if (preg_match('/[^0-9,-]+/i', $out)) { $out = ''; } break; case 'san_alpha': dol_syslog("Use of parameter value 'san_alpha' in GETPOST is deprecated. Use 'alphanohtml', 'aZ09comma', ...", LOG_WARNING); $out = filter_var($out, FILTER_SANITIZE_STRING); break; case 'email': $out = filter_var($out, FILTER_SANITIZE_EMAIL); break; case 'url': //$out = filter_var($out, FILTER_SANITIZE_URL); // Not reliable, replaced with FILTER_VALIDATE_URL $out = preg_replace('/[^:\/\[\]a-z0-9@\$\'\*\~\.\-_,;\?\!=%&+#]+/i', '', $out); // TODO Allow ( ) but only into password of https://login:password@domain... break; case 'aZ': if (!is_array($out)) { $out = trim($out); if (preg_match('/[^a-z]+/i', $out)) { $out = ''; } } break; case 'aZ09': if (!is_array($out)) { $out = trim($out); if (preg_match('/[^a-z0-9_\-\.]+/i', $out)) { $out = ''; } } break; case 'aZ09arobase': // great to sanitize $objecttype parameter if (!is_array($out)) { $out = trim($out); if (preg_match('/[^a-z0-9_\-\.@]+/i', $out)) { $out = ''; } } break; case 'aZ09comma': // great to sanitize $sortfield or $sortorder params that can be 't.abc,t.def_gh' if (!is_array($out)) { $out = trim($out); if (preg_match('/[^a-z0-9_\-\.,]+/i', $out)) { $out = ''; } } break; case 'alpha': // No html and no ../ and " case 'alphanohtml': // Recommended for most scalar parameters and search parameters. Not valid for json string. if (!is_array($out)) { $out = trim($out); do { $oldstringtoclean = $out; // Remove html tags $out = dol_string_nohtmltag($out, 0); // Refuse octal syntax \999, hexa syntax \x999 and unicode syntax \u{999} by replacing the \ into / (so if it is a \ for a windows path, it is still ok). $out = preg_replace('/\\\([0-9xu])/', '/\1', $out); // Remove also other dangerous string sequences // '../' or '..\' is dangerous because it allows dir transversals // '&', '&', '&'... is a the char '&' alone but there is no reason to accept such way to encode input char // '"' = '"' = '"' = '"' is dangerous because param in url can close the href= or src= and add javascript functions. // '/', '/', '/' is the char '/' but there is no reason to accept such way to encode this input char // '\' = '\' = '\' is the char '\' but there is no reason to accept such way to encode this input char $out = str_ireplace(array('../', '..\\', '&', '&', '&', '"', '"', '"', '"', '"', '/', '/', '/', '\', '\', '\'), '', $out); } while ($oldstringtoclean != $out); // keep lines feed } break; case 'alphawithlgt': // No " and no ../ but we keep balanced < > tags with no special chars inside. Can be used for email string like "Name ". Less secured than 'alphanohtml' if (!is_array($out)) { $out = trim($out); do { $oldstringtoclean = $out; // Decode html entities $out = dol_html_entity_decode($out, ENT_COMPAT | ENT_HTML5, 'UTF-8'); // Refuse octal syntax \999, hexa syntax \x999 and unicode syntax \u{999} by replacing the \ into / (so if it is a \ for a windows path, it is still ok). $out = preg_replace('/\\\([0-9xu])/', '/\1', $out); // Remove also other dangerous string sequences // '../' or '..\' is dangerous because it allows dir transversals // '&', '&', '&'... is a the char '&' alone but there is no reason to accept such way to encode input char // '"' = '"' = '"' = '"' is dangerous because param in url can close the href= or src= and add javascript functions. // '/', '/', '/' is the char '/' but there is no reason to accept such way to encode this input char // '\' = '\' = '\' is the char '\' but there is no reason to accept such way to encode this input char $out = str_ireplace(array('../', '..\\', '&', '&', '&', '"', '"', '"', '"', '"', '/', '/', '/', '\', '\', '\'), '', $out); } while ($oldstringtoclean != $out); } break; case 'nohtml': // No html. Valid for JSON strings. $out = dol_string_nohtmltag($out, 0); break; case 'restricthtmlnolink': case 'restricthtml': // Recommended for most html textarea case 'restricthtmlallowclass': case 'restricthtmlallowiframe': case 'restricthtmlallowlinkscript': // Allow link and script tag for head section. case 'restricthtmlallowunvalid': $out = dol_htmlwithnojs($out, 1, $check); break; case 'custom': if (!empty($out)) { if (empty($filter)) { return 'BadParameterForGETPOST - Param 3 of sanitizeVal()'; } if (is_null($options)) { $options = 0; } $out = filter_var($out, $filter, $options); } break; default: dol_syslog("Error, you call sanitizeVal() with a bad value for the check type. Data will be sanitized with alphanohtml.", LOG_ERR); $out = GETPOST($out, 'alphanohtml'); break; } return $out; } /** * Set a cookie * * @param string $cookiename Cookie name * @param string $cookievalue Cookie value * @param int $expire Expire delay. If 0, expire at end of session. -1 means 1 year. * @return void */ function dolSetCookie(string $cookiename, string $cookievalue, int $expire = -1) { include_once DOL_DOCUMENT_ROOT.'/blockedlog/lib/securitycore.lib.php'; global $dolibarr_main_force_https; if ($expire == -1) { $expire = (time() + (86400 * 354)); // keep cookie 1 year. } if (PHP_VERSION_ID < 70300) { setcookie($cookiename, empty($cookievalue) ? '' : $cookievalue, empty($cookievalue) ? 0 : $expire, '/', '', !(empty($dolibarr_main_force_https) && isHTTPS() === false), true); // add tag httponly } else { // Only available for php >= 7.3 $cookieparams = array( 'expires' => empty($cookievalue) ? 0 : $expire, 'path' => '/', //'domain' => '.mywebsite.com', // the dot at the beginning allows compatibility with subdomains 'secure' => !(empty($dolibarr_main_force_https) && isHTTPS() === false), 'httponly' => true, 'samesite' => 'Lax' // None || Lax || Strict ); setcookie($cookiename, empty($cookievalue) ? '' : $cookievalue, $cookieparams); } if (empty($cookievalue)) { unset($_COOKIE[$cookiename]); } } if (!function_exists('dol_getprefix')) { /** * Return a prefix to use for this Dolibarr instance, for session/cookie names or email id. * The prefix is unique for instance and avoid conflict between multi-instances, even when having two instances with same root dir * or two instances in same virtual servers. * This function must not use dol_hash (that is used for password hash) and need to have all context $conf loaded. * * @param string $mode '' (prefix for session name) or 'email' (prefix for email id) * @return string A calculated prefix * @phan-suppress PhanRedefineFunction - Also defined in webportal.main.inc.php */ function dol_getprefix($mode = '') { // If prefix is for email (we need to have $conf already loaded for this case) if ($mode == 'email') { global $conf; if (getDolGlobalString('MAIL_PREFIX_FOR_EMAIL_ID')) { // If MAIL_PREFIX_FOR_EMAIL_ID is set if (getDolGlobalString('MAIL_PREFIX_FOR_EMAIL_ID') != 'SERVER_NAME') { return getDolGlobalString('MAIL_PREFIX_FOR_EMAIL_ID'); } elseif (isset($_SERVER["SERVER_NAME"])) { // If MAIL_PREFIX_FOR_EMAIL_ID is set to 'SERVER_NAME' return $_SERVER["SERVER_NAME"]; } } // The recommended value if MAIL_PREFIX_FOR_EMAIL_ID is not defined (may be not defined for old versions) if (!empty($conf->file->instance_unique_id)) { return sha1('dolibarr' . $conf->file->instance_unique_id); } // For backward compatibility when instance_unique_id is not set return sha1(DOL_DOCUMENT_ROOT . DOL_URL_ROOT); } // If prefix is for session (no need to have $conf loaded) global $dolibarr_main_instance_unique_id, $dolibarr_main_cookie_cryptkey; // This is loaded by filefunc.inc.php $tmp_instance_unique_id = empty($dolibarr_main_instance_unique_id) ? (empty($dolibarr_main_cookie_cryptkey) ? '' : $dolibarr_main_cookie_cryptkey) : $dolibarr_main_instance_unique_id; // Unique id of instance // The recommended value (may be not defined for old versions) if (!empty($tmp_instance_unique_id)) { return sha1('dolibarr' . $tmp_instance_unique_id); } // For backward compatibility when instance_unique_id is not set if (isset($_SERVER["SERVER_NAME"]) && isset($_SERVER["DOCUMENT_ROOT"])) { return sha1($_SERVER["SERVER_NAME"] . $_SERVER["DOCUMENT_ROOT"] . DOL_DOCUMENT_ROOT . DOL_URL_ROOT); } else { return sha1(DOL_DOCUMENT_ROOT . DOL_URL_ROOT); } } } /** * Make an include_once using default root and alternate root if it fails. * To link to a core file, use include(DOL_DOCUMENT_ROOT.'/pathtofile') * To link to a module file from a module file, use include './mymodulefile'; * To link to a module file from a core file, then this function can be used (call by hook / trigger / speciales pages) * * @param string $relpath Relative path to file (Ie: mydir/myfile, ./myfile, ...) * @param string $classname Class name (deprecated) * @return bool True if load is a success, False if it fails */ function dol_include_once($relpath, $classname = '') { global $conf, $langs, $user, $mysoc; // Do not remove this. They must be defined for files we make "include". Other globals var must be retrieved with $GLOBALS['var'] if (strpos($relpath, '..') !== false) { // Found a not valid path dol_syslog('functions::dol_include_once Tried to load a file with a path including a forbidden sequence ".." : ' . $relpath, LOG_WARNING); return false; } if (!preg_match('/\.php$/', $relpath)) { // Found a not valid path dol_syslog('functions::dol_include_once Tried to load a file that is not a PHP file : ' . $relpath, LOG_WARNING); return false; } $fullpath = dol_buildpath($relpath); if (!file_exists($fullpath)) { dol_syslog('functions::dol_include_once Tried to load unexisting file: ' . $relpath, LOG_WARNING); return false; } if (!empty($classname) && !class_exists($classname)) { return include $fullpath; } else { return include_once $fullpath; } } /** * Return path of url or filesystem. Can check into alternate dir or alternate dir + main dir depending on value of $returnemptyifnotfound. * * @param string $path Relative path to file (if mode=0) or relative url (if mode=1). Ie: '/mymoduledir/myfile' * @param int $type 0=Used for a Filesystem path, * 1=Used for an URL path (output relative), * 2=Used for an URL path (output full path using same host that current url), * 3=Used for an URL path (output full path using host defined into $dolibarr_main_url_root of conf file, for an access from internet) * @param int $returnemptyifnotfound 0:If $type==0 and if file was not found into alternate dir, return default path into main dir (no test on it) * 1:If $type==0 and if file was not found into alternate dir, return empty string * 2:If $type==0 and if file was not found into alternate dir, test into main dir, return default path if found, empty string if not found * @return string Full filesystem path (if path=0) or '' if file not found, Full url path (if mode=1) */ function dol_buildpath($path, $type = 0, $returnemptyifnotfound = 0) { global $conf; $path = preg_replace('/^\//', '', $path); if (empty($type)) { // For a filesystem path $res = DOL_DOCUMENT_ROOT . '/' . $path; // Standard default path if (is_array($conf->file->dol_document_root)) { foreach ($conf->file->dol_document_root as $key => $dirroot) { // ex: array("main"=>"/home/main/htdocs", "alt0"=>"/home/dirmod/htdocs", ...) if ($key == 'main') { continue; } // if (@file_exists($dirroot.'/'.$path)) { if (@file_exists($dirroot . '/' . $path)) { // avoid [php:warn] if ($key != 'main' && preg_match('/^core\//', $path)) { // When searching into an alternative custom path, we don't want path like 'core/...' because path should be 'modulename/core/...' continue; } $res = $dirroot . '/' . $path; return $res; } } } if ($returnemptyifnotfound) { // Not found into alternate dir if ($returnemptyifnotfound == 1 || !file_exists($res)) { return ''; } } } else { // For an url path // We try to get local path of file on filesystem from url // Note that trying to know if a file on disk exist by forging path on disk from url // works only for some web server and some setup. This is bugged when // using proxy, rewriting, virtual path, etc... $res = ''; if ($type == 1) { $res = DOL_URL_ROOT . '/' . $path; // Standard value } if ($type == 2) { $res = DOL_MAIN_URL_ROOT . '/' . $path; // Standard value } if ($type == 3) { $res = DOL_URL_ROOT . '/' . $path; } foreach ($conf->file->dol_document_root as $key => $dirroot) { // ex: array(["main"]=>"/home/main/htdocs", ["alt0"]=>"/home/dirmod/htdocs", ...) if ($key == 'main') { if ($type == 3) { /*global $dolibarr_main_url_root;*/ // Define $urlwithroot $urlwithouturlroot = preg_replace('/' . preg_quote(DOL_URL_ROOT, '/') . '$/i', '', trim($conf->file->dol_main_url_root)); $urlwithroot = $urlwithouturlroot . DOL_URL_ROOT; // This is to use external domain name found into config file //$urlwithroot=DOL_MAIN_URL_ROOT; // This is to use same domain name than current $res = (preg_match('/^http/i', $conf->file->dol_url_root[$key]) ? '' : $urlwithroot) . '/' . $path; // Test on start with http is for old conf syntax } continue; } $regs = array(); preg_match('/^([^\?]+(\.css\.php|\.css|\.js\.php|\.js|\.png|\.jpg|\.php)?)/i', $path, $regs); // Take part before '?' if (!empty($regs[1])) { //print $key.' - '.$dirroot.' - '.$path.' - '.$conf->file->dol_url_root[$key].' - '.$regs[1].'
'."\n"; //if (file_exists($dirroot.'/'.$regs[1])) { if (@file_exists($dirroot . '/' . $regs[1])) { // avoid [php:warn] if ($type == 1) { $res = (preg_match('/^http/i', $conf->file->dol_url_root[$key]) ? '' : DOL_URL_ROOT) . $conf->file->dol_url_root[$key] . '/' . $path; } elseif ($type == 2) { $res = (preg_match('/^http/i', $conf->file->dol_url_root[$key]) ? '' : DOL_MAIN_URL_ROOT) . $conf->file->dol_url_root[$key] . '/' . $path; } elseif ($type == 3) { /*global $dolibarr_main_url_root;*/ // Define $urlwithroot $urlwithouturlroot = preg_replace('/' . preg_quote(DOL_URL_ROOT, '/') . '$/i', '', trim($conf->file->dol_main_url_root)); $urlwithroot = $urlwithouturlroot . DOL_URL_ROOT; // This is to use external domain name found into config file //$urlwithroot=DOL_MAIN_URL_ROOT; // This is to use same domain name than current $res = (preg_match('/^http/i', $conf->file->dol_url_root[$key]) ? '' : $urlwithroot) . $conf->file->dol_url_root[$key] . '/' . $path; // Test on start with http is for old conf syntax } break; } } } } return $res; } /** * Return the full filesystem path of a file located in the currently selected theme directory. * The standard theme directory (DOL_DOCUMENT_ROOT/theme/) is searched first, then the theme * directories provided by modules (registered into $conf->modules_parts['theme']). This allows a * theme shipped inside an external module to be found the same way as a native theme. * When no module registers a theme directory (the usual case), the native path is returned as-is * without any file_exists() check. * * @param string $file Relative file name to look for into the theme directory (ex: 'theme_vars.inc.php') * @param string $theme Theme name to use. Default is $conf->theme. * @return string Full filesystem path to the file, or '' if it was not found. * @see dol_buildpath() */ function dol_getThemeFilePath($file, $theme = '') { global $conf; if (empty($theme)) { $theme = $conf->theme; } $file = '/theme/'.$theme.'/'.preg_replace('/^\//', '', $file); // No module registers a theme directory: the file can only be the native one. // Return it directly without an extra file_exists() call, like the historical code. if (empty($conf->modules_parts['theme'])) { return DOL_DOCUMENT_ROOT.$file; } // A module may provide or override the theme: look into the native directory // first, then into the module-provided theme directories. if (file_exists(DOL_DOCUMENT_ROOT.$file)) { return DOL_DOCUMENT_ROOT.$file; } foreach ($conf->modules_parts['theme'] as $reldir) { $tmp = dol_buildpath($reldir.$file, 0, 1); if ($tmp) { return $tmp; } } return ''; } /** * Return path of url. * * @param string $url Relative path to file * @param array $params params for the http query * @param bool $addtoken does we need to add token * @param string $anchor Add an anchor #xxx at end of URL * @return string path */ function dolBuildUrl($url, $params = [], $addtoken = false, $anchor = '') { global $db, $hookmanager; if (!is_object($hookmanager)) { include_once DOL_DOCUMENT_ROOT . '/core/class/hookmanager.class.php'; $hookmanager = new HookManager($db); } if ((!isset($params['mainmenu']) || empty($params['mainmenu'])) && GETPOSTISSET('mainmenu')) { $params = array_merge($params, ['mainmenu' => (GETPOST('mainmenu', 'restricthtml'))]); } if ((!isset($params['leftmenu'])/* || empty($params['leftmenu']) */) && GETPOSTISSET('leftmenu')) { // do not fill leftmenu if we have leftmenu= $params = array_merge($params, ['leftmenu' => (GETPOST('leftmenu', 'restricthtml'))]); } $parameters = [ 'path' => &$url, 'params' => &$params, 'addtoken' => &$addtoken, ]; $hookmanager->executeHooks('buildurl', $parameters); if ($addtoken) { $params = array_merge($params, ['token' => newToken()]); } if ($params) { $url .= '?' . http_build_query($params); } if ($anchor) { $url .= '#' . preg_replace('/[^a-z]/i', '', $anchor); } return $url; } /** * Get value of properties for an object - including magic properties when requested * * By default, returns all public properties. If a list of properties is provided, only returns the ones that exist. * * @param object $obj Object to get properties from * @param string[] $properties Optional list of properties to get. * When empty, only gets public properties. * @return array Hash for retrieved values (key=name) */ function dol_get_object_properties($obj, $properties = []) { // Get real properties using get_object_vars() if $properties is empty if (empty($properties)) { return get_object_vars($obj); } $existingProperties = []; $realProperties = get_object_vars($obj); // Get the real or magic property values foreach ($properties as $property) { if (array_key_exists($property, $realProperties)) { // Real property, add the value $existingProperties[$property] = $obj->{$property}; } elseif (property_exists($obj, $property)) { // Magic property $existingProperties[$property] = $obj->{$property}; } } return $existingProperties; } /** * Create a clone of instance of object (new instance with same value for each properties) * With native = 0: Deprecated. Property that are references are different memory area in the new object (full isolation clone). This means $this->objectproperty of the new object may not be valid (except this->db that is voluntarly kept). * With native = 1: Use PHP clone. Property that are reference are same pointer. This means $this->db of new object is still valid but point to same this->db than original object. * With native = 2: Property that are reference are different memory area in the new object (full isolation clone). Only scalar and array values are cloned. This means that the methods are not available and $this->db of new object is not valid. * You can use it with: $object->oldcopy = dol_clone($object, 2); // @phan-suppress-current-line PhanTypeMismatchProperty * * * @template T * * @param T $srcobject Object to clone * @param int $native 0=Full isolation method, 1=Native PHP method, 2=Full isolation method keeping only scalar and array properties (recommended) * @return T Clone object * * @see https://php.net/manual/language.oop5.cloning.php * @phan-suppress PhanTypeExpectedObjectPropAccess */ function dol_clone($srcobject, $native = 2) { if ($native == 0) { // deprecated method, use the method with native = 2 instead dol_syslog("Warning, call to dol_clone() with the deprecated parameter native=0, use 2 instead", LOG_WARNING); $tmpsavdb = null; if (isset($srcobject->db) && isset($srcobject->db->db) && is_object($srcobject->db->db) && get_class($srcobject->db->db) == 'PgSql\Connection') { $tmpsavdb = $srcobject->db; unset($srcobject->db); // Such property can not be serialized with pgsl (when object->db->db = 'PgSql\Connection') } $myclone = unserialize(serialize($srcobject)); // serialize then unserialize is a hack to be sure to have a new object for all fields if (!empty($tmpsavdb)) { $srcobject->db = $tmpsavdb; } } elseif ($native == 2) { // recommended method to have a full secured isolated cloned object $myclone = new stdClass(); $tmparray = get_object_vars($srcobject); // return only public properties if (is_array($tmparray)) { foreach ($tmparray as $propertykey => $propertyval) { if (is_scalar($propertyval) || is_array($propertyval)) { $myclone->$propertykey = $propertyval; } } } } else { $myclone = clone $srcobject; // PHP clone is a shallow copy only, not a real clone, so properties of references will keep the reference (referring to the same target/variable) } return $myclone; } /** * Create a clone of instance of object into a full array, using recursive call. * It also cleans some properties. * * @param Object $srcobject Object to clone * @param int $startlevel Start level to track recursive depth * @return array Array */ function dol_clone_in_array($srcobject, $startlevel = 0) { if (is_object($srcobject)) { $srcobject = get_object_vars($srcobject); // exclude private/protected properties } if (is_array($srcobject)) { $result = []; foreach ($srcobject as $key => $value) { if (in_array($key, array('db', 'fields', 'error', 'errorhidden', 'errors', 'oldcopy', 'linkedObjects', 'linked_objects'))) { continue; } $result[$key] = dol_clone_in_array($value, $startlevel + 1); } return $result; } return $srcobject; } /** * Optimize a size for some browsers (phone, smarphone...) * * @param int $size Size we want * @param string $type Type of optimizing: * '' = function used to define a size for truncation * 'width' = function is used to define a width * @return int New size after optimizing */ function dol_size($size, $type = '') { global $conf; if (empty($conf->dol_optimize_smallscreen)) { return $size; } if ($type == 'width' && $size > 250) { return 250; } else { return 10; } } /** * Clean a string to use it as a file name. * Replace also '--' and ' -' strings, they are used for parameters separation (Note: ' - ' is allowed). * * @param string|null $str String to clean * @param string $newstr String to replace bad chars with. * @param int $unaccent 1=Remove also accent (default), 0 do not remove them * @param int $includequotes 1=Include simple quotes (double is already included by default) * @param int $allowdash 1=Allow dash char after a space and before a string, 0 do not allow * @return string String cleaned * * @see dol_string_nospecial(), dol_string_unaccent(), dol_sanitizePathName() */ function dol_sanitizeFileName($str, $newstr = '_', $unaccent = 1, $includequotes = 0, $allowdash = 0) { $str = (string) $str; // List of special chars for filenames in windows are defined on page https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file // Char '>' '<' '|' '$' and ';' are special chars for shells. // Char '/' and '\' are file delimiters. // Chars '--' can be used into filename to inject special parameters like --use-compress-program to make command with file as parameter making remote execution of command $filesystem_forbidden_chars = array('<', '>', '/', '\\', '?', '*', '|', '"', ':', '°', '$', ';', '`'); if ($includequotes) { $filesystem_forbidden_chars[] = "'"; } $tmp = dol_string_nospecial($unaccent ? dol_string_unaccent($str) : $str, $newstr, $filesystem_forbidden_chars); $tmp = preg_replace('/\-\-+/', '_', $tmp); if (empty($allowdash)) { $tmp = preg_replace('/\s+\-([^\s])/', ' _$1', $tmp); $tmp = preg_replace('/\s+\-$/', '', $tmp); } $tmp = str_replace('..', '', $tmp); $tmp = str_replace('~', $newstr, $tmp); $tmp = preg_replace('/\s{2,}/', ' ', $tmp); return $tmp; } /** * Clean a string to use it as a path name. Similar to dol_sanitizeFileName but accept / and \ chars. * Replace also '--' and ' -' strings, they are used for parameters separation (Note: ' - ' and 'x-y' is allowed). * * @param string $str String to clean * @param string $newstr String to replace bad chars with * @param int $unaccent 1=Remove also accent, 0 do not remove them * @param int $allowdash 1=Allow dash char after a space and before a string, 0 do not allow * @return string String cleaned * * @see dol_string_nospecial(), dol_string_unaccent(), dol_sanitizeFileName() */ function dol_sanitizePathName($str, $newstr = '_', $unaccent = 0, $allowdash = 0) { // List of special chars for filenames in windows are defined on page https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file // Char '>' '<' '|' '$' ';' and '`' are special chars for shells. // Char '?' and '*' are for wild card chars. // Char '"' is dangerous. // Char '°' is just not expected. // Chars '-' and '--' can be used into filename to inject special parameters like --use-compress-program to make command with file as parameter making remote execution of command // Chars '--' and '~' can be used for path transversal $filesystem_forbidden_chars = array('<', '>', '?', '*', '|', '"', '°', '$', ';', '`'); $tmp = $str; if ($unaccent) { $tmp = dol_string_unaccent($tmp); } $tmp = dol_string_nospecial($tmp, $newstr, $filesystem_forbidden_chars); $tmp = preg_replace('/\-\-+/', $newstr, $tmp); if (empty($allowdash)) { $tmp = preg_replace('/\s+\-([^\s])/', ' '.$newstr.'$1', $tmp); $tmp = preg_replace('/\s+\-$/', '', $tmp); } $tmp = str_replace('..', $newstr, $tmp); $tmp = str_replace('~', $newstr, $tmp); $tmp = preg_replace('/\s{2,}/', ' ', $tmp); return $tmp; } /** * Clean a string to use it as an URL (into a href or src attribute, or into a js string that is a location). * Raw '<' and '>' are url encoded (a browser always sends them encoded, so a real URL never holds them). * * @param string $stringtoclean String to clean * @param int $type 0=Accept all Url, 1=Clean external Url (keep only relative Url) * @return string Escaped string. */ function dol_sanitizeUrl($stringtoclean, $type = 1) { // We clean string because some hacks try to obfuscate evil strings by inserting non printable chars. Example: 'java(ascci09)scr(ascii00)ipt' is processed like 'javascript' (whatever is place of evil ascii char) // We should use dol_string_nounprintableascii but function may not be yet loaded/available $stringtoclean = preg_replace('/[\x00-\x1F\x7F]/u', '', $stringtoclean); // /u operator makes UTF8 valid characters being ignored so are not included into the replace // We clean html comments because some hacks try to obfuscate evil strings by inserting HTML comments. Example: onerror=alert(1) $stringtoclean = preg_replace('//', '', $stringtoclean); $stringtoclean = str_replace('\\', '/', $stringtoclean); if ($type == 1) { // removing : should disable links to external url like http:aaa) // removing ';' should disable "named" html entities encode into an url (we should not have this into an url) $stringtoclean = str_replace(array(':', ';', '@'), '', $stringtoclean); } do { $oldstringtoclean = $stringtoclean; // removing '&colon' should disable links to external url like http:aaa) // removing '&#' should disable "numeric" html entities encode into an url (we should not have this into an url) $stringtoclean = str_ireplace(array('javascript', 'vbscript', '&colon', '&#'), '', $stringtoclean); } while ($oldstringtoclean != $stringtoclean); if ($type == 1) { // removing '//' should disable links to external url like //aaa or http//) $stringtoclean = preg_replace(array('/^[a-z]*\/\/+/i'), '', $stringtoclean); } // A raw < or > can not be part of a valid URL. We encode them, so the result can not open an html tag or close an inline script block (). $stringtoclean = str_replace(array('<', '>'), array('%3C', '%3E'), $stringtoclean); return $stringtoclean; } /** * Clean a string to use it as an Email. * * @param string $stringtoclean String to clean. Example 'abc@mycompany.com ' * @return string Escaped string. */ function dol_sanitizeEmail($stringtoclean) { do { $oldstringtoclean = $stringtoclean; $stringtoclean = str_ireplace(array('"', ':', '[', ']', "\n", "\r", '\\', '\/'), '', $stringtoclean); } while ($oldstringtoclean != $stringtoclean); return $stringtoclean; } /** * Clean a string to use it as a key or code. So only char a-Z, A-Z, _ and 0-9 is kept. * * @param string $str String to clean * @return string String cleaned (a-zA-Z_) * * @see dol_string_nospecial(), dol_string_unaccent(), dol_sanitize...() */ function dol_sanitizeKeyCode($str) { return preg_replace('/[^\w]+/', '', $str); } /** * Clean a string from all accent characters to be used as ref, login or by dol_sanitizeFileName * * @param string $str String to clean. Must be an ascii or utf8 string without any htmlentities. * @return string Cleaned string * * @see dol_sanitizeFilename(), dol_string_nospecial() */ function dol_string_unaccent($str) { if (is_null($str)) { return ''; } if (utf8_check($str)) { if (extension_loaded('intl') && getDolGlobalString('MAIN_UNACCENT_USE_TRANSLITERATOR')) { $transliterator = Transliterator::createFromRules(':: Any-Latin; :: Latin-ASCII; :: NFD; :: [:Nonspacing Mark:] Remove; :: NFC;', Transliterator::FORWARD); return $transliterator->transliterate($str); } // See http://www.utf8-chartable.de/ $string = rawurlencode($str); $replacements = array( '%C3%80' => 'A', '%C3%81' => 'A', '%C3%82' => 'A', '%C3%83' => 'A', '%C3%84' => 'A', '%C3%85' => 'A', '%C3%87' => 'C', '%C3%88' => 'E', '%C3%89' => 'E', '%C3%8A' => 'E', '%C3%8B' => 'E', '%C3%8C' => 'I', '%C3%8D' => 'I', '%C3%8E' => 'I', '%C3%8F' => 'I', '%C3%91' => 'N', '%C3%92' => 'O', '%C3%93' => 'O', '%C3%94' => 'O', '%C3%95' => 'O', '%C3%96' => 'O', '%C5%90' => 'O', '%C5%A0' => 'S', '%C3%99' => 'U', '%C3%9A' => 'U', '%C3%9B' => 'U', '%C3%9C' => 'U', '%C5%B0' => 'U', '%C3%9D' => 'Y', '%C5%B8' => 'y', '%C3%A0' => 'a', '%C3%A1' => 'a', '%C3%A2' => 'a', '%C3%A3' => 'a', '%C3%A4' => 'a', '%C3%A5' => 'a', '%C3%A7' => 'c', '%C3%A8' => 'e', '%C3%A9' => 'e', '%C3%AA' => 'e', '%C3%AB' => 'e', '%C3%AC' => 'i', '%C3%AD' => 'i', '%C3%AE' => 'i', '%C3%AF' => 'i', '%C3%B1' => 'n', '%C3%B2' => 'o', '%C3%B3' => 'o', '%C3%B4' => 'o', '%C3%B5' => 'o', '%C3%B6' => 'o', '%C5%91' => 'o', '%C5%A1' => 's', '%C3%B9' => 'u', '%C3%BA' => 'u', '%C3%BB' => 'u', '%C3%BC' => 'u', '%C5%B1' => 'u', '%C3%BD' => 'y', '%C3%BF' => 'y', '%CC%80' => '', '%CC%81' => '', '%CC%82' => '', '%CC%83' => '', '%CC%84' => '', '%CC%85' => '', '%CC%86' => '', '%CC%87' => '', '%CC%88' => '', '%CC%89' => '', '%CC%8A' => '', '%CC%8B' => '', '%CC%8C' => '', '%CC%8D' => '', '%CC%8E' => '', '%CC%8F' => '', '%CC%90' => '', '%CC%91' => '', '%CC%A7' => '' ); $string = strtr($string, $replacements); return rawurldecode($string); } else { // See http://www.ascii-code.com/ $string = strtr( $str, "\xC0\xC1\xC2\xC3\xC4\xC5\xC7 \xC8\xC9\xCA\xCB\xCC\xCD\xCE\xCF\xD0\xD1 \xD2\xD3\xD4\xD5\xD8\xD9\xDA\xDB\xDD \xE0\xE1\xE2\xE3\xE4\xE5\xE7\xE8\xE9\xEA\xEB \xEC\xED\xEE\xEF\xF0\xF1\xF2\xF3\xF4\xF5\xF8 \xF9\xFA\xFB\xFC\xFD\xFF", "AAAAAAC EEEEIIIIDN OOOOOUUUY aaaaaaceeee iiiidnooooo uuuuyy" ); $string = strtr($string, array("\xC4" => "Ae", "\xC6" => "AE", "\xD6" => "Oe", "\xDC" => "Ue", "\xDE" => "TH", "\xDF" => "ss", "\xE4" => "ae", "\xE6" => "ae", "\xF6" => "oe", "\xFC" => "ue", "\xFE" => "th")); return $string; } } /** * Clean a string from all punctuation characters to use it as a ref or login. * This is a more complete function than dol_sanitizeFileName(). * * @param string $str String to clean * @param string $newstr String to replace forbidden chars with * @param string[]|string $badcharstoreplace Array of forbidden characters to replace. Use '' to keep default list. * @param string[]|string $badcharstoremove Array of forbidden characters to remove. Use '' to keep default list. * @param int<0,1> $keepspaces 1=Do not treat space as a special char to replace or remove * @return string Cleaned string * * @see dol_sanitizeFilename(), dol_string_unaccent(), dol_string_nounprintableascii() */ function dol_string_nospecial($str, $newstr = '_', $badcharstoreplace = '', $badcharstoremove = '', $keepspaces = 0) { $forbidden_chars_to_replace = array("'", "/", "\\", ":", "*", "?", "\"", "<", ">", "|", "[", "]", ",", ";", "=", '°', '$', ';'); // more complete than dol_sanitizeFileName if (empty($keepspaces)) { $forbidden_chars_to_replace[] = " "; } $forbidden_chars_to_remove = array(); //$forbidden_chars_to_remove=array("(",")"); if (is_array($badcharstoreplace)) { $forbidden_chars_to_replace = $badcharstoreplace; } if (is_array($badcharstoremove)) { $forbidden_chars_to_remove = $badcharstoremove; } // @phan-suppress-next-line PhanPluginSuspiciousParamOrderInternal return str_replace($forbidden_chars_to_replace, $newstr, str_replace($forbidden_chars_to_remove, "", $str)); } /** * Clean a string from all non printable ASCII chars (0x00-0x1F and 0x7F). It can also removes also Tab-CR-LF. UTF8 chars remains. * This can be used to sanitize a string and view its real content. Some hacks try to obfuscate attacks by inserting non printable chars. * Note, for information: UTF8 on 1 byte are: \x00-\7F * 2 bytes are: byte 1 \xc0-\xdf, byte 2 = \x80-\xbf * 3 bytes are: byte 1 \xe0-\xef, byte 2 = \x80-\xbf, byte 3 = \x80-\xbf * 4 bytes are: byte 1 \xf0-\xf7, byte 2 = \x80-\xbf, byte 3 = \x80-\xbf, byte 4 = \x80-\xbf * @param string $str String to clean * @param int $removetabcrlf Remove also CR-LF * @return string Cleaned string * * @see dol_sanitizeFilename(), dol_string_unaccent(), dol_string_nospecial() */ function dol_string_nounprintableascii($str, $removetabcrlf = 1) { if ($removetabcrlf) { return preg_replace('/[\x00-\x1F\x7F]/u', '', $str); // /u operator makes UTF8 valid characters being ignored so are not included into the replace } else { return preg_replace('/[\x00-\x08\x11-\x12\x14-\x1F\x7F]/u', '', $str); // /u operator should make UTF8 valid characters being ignored so are not included into the replace } } /** * Returns text slugified (lowercase and no special char, separator is "-"). * * @param string $stringtoslugify String to slugify * @return string Slugified string */ function dolSlugify($stringtoslugify) { $slug = dol_string_unaccent($stringtoslugify); // Convert special characters to their ASCII equivalents if (function_exists('iconv')) { $slug = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $slug); } // Convert to lowercase $slug = strtolower($slug); // Replace non-alphanumeric characters with hyphens $slug = preg_replace('/[^a-z0-9]+/', '-', $slug); // Remove leading and trailing hyphens $slug = trim($slug, '-'); return $slug; } /** * Returns text escaped for inclusion into JavaScript code. * * @param int|string $stringtoescape String to escape * @param int $mode 0=Escape also \' and \" into \', 1=Escape \' but not \" for usage into 'string', 2=Escape \" but not \' for usage into "string", 3=Escape \' and \" with \\. * @param int $noescapebackslashn 0=Escape also \n. 1=Do not escape \n. * @return string Escaped string. Both \' and " are escaped into \' if they are escaped. */ function dol_escape_js($stringtoescape, $mode = 0, $noescapebackslashn = 0) { if (is_null($stringtoescape)) { return ''; } // escape quotes and backslashes, newlines, etc. $substitjs = array("'" => "\\'", "\r" => '\\r'); //$substitjs[' block that contains it ("<\/" is " $stringforquotes 2=String for doublequotes, 1=String for simple quotes * @return string Escaped string for PHP content. */ function dol_escape_php($stringtoescape, $stringforquotes = 2) { if (is_null($stringtoescape)) { return ''; } if ($stringforquotes == 2) { return str_replace('"', "'", $stringtoescape); } elseif ($stringforquotes == 1) { // We remove the \ char. // If we allow the \ char, we can have $stringtoescape = // abc\';phpcodedanger; so the escapement will become // abc\\';phpcodedanger; and injecting this into // $a='...' will give $ac='abc\\';phpcodedanger; $stringtoescape = str_replace('\\', '', $stringtoescape); return str_replace("'", "\'", str_replace('"', "'", $stringtoescape)); } return 'Bad parameter for stringforquotes in dol_escape_php'; } /** * Returns text escaped for all protocols (so only alpha chars and numbers) * * @param string $stringtoescape String to escape * @return string Escaped string for XML content. */ function dol_escape_all($stringtoescape) { return preg_replace('/[^a-z0-9_]/i', '', $stringtoescape); } /** * Returns text escaped for inclusion into a XML string * * @param string $stringtoescape String to escape * @return string Escaped string for XML content. */ function dol_escape_xml($stringtoescape) { $stringtoescape = (string) preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/', '', (string) $stringtoescape); // Characters not allowed in XML 1.0 return htmlspecialchars($stringtoescape, ENT_XML1 | ENT_QUOTES, 'UTF-8'); } /** * Convert a string to lower. Never use strtolower because it does not works with UTF8 strings. * * @param string $string String to encode * @param string $encoding Character set encoding * @return string String converted */ function dol_strtolower($string, $encoding = "UTF-8") { if (function_exists('mb_strtolower')) { return mb_strtolower($string, $encoding); } else { return strtolower($string); } } /** * Convert a string to upper. Never use strtolower because it does not works with UTF8 strings. * * @param string $string String to encode * @param string $encoding Character set encoding * @return string String converted * @see dol_ucfirst(), dol_ucwords() */ function dol_strtoupper($string, $encoding = "UTF-8") { if (function_exists('mb_strtoupper')) { return mb_strtoupper($string, $encoding); } else { return strtoupper($string); } } /** * Convert first character of the first word of a string to upper. Never use ucfirst because it does not works with UTF8 strings. * * @param string $string String to encode * @param string $encoding Character set encodign * @return string String converted * @see dol_strtoupper(), dol_ucwords() */ function dol_ucfirst($string, $encoding = "UTF-8") { if (function_exists('mb_substr')) { return mb_strtoupper(mb_substr($string, 0, 1, $encoding), $encoding) . mb_substr($string, 1, null, $encoding); } else { return ucfirst($string); } } /** * Convert first character of all the words of a string to upper. * * @param string $string String to encode * @param string $encoding Character set encodign * @return string String converted * @see dol_strtoupper(), dol_ucfirst() */ function dol_ucwords($string, $encoding = "UTF-8") { if (function_exists('mb_convert_case')) { return mb_convert_case($string, MB_CASE_TITLE, $encoding); } else { return ucwords($string); } } /** * Get caller info as a string that can be appended to a log message. * * @return string */ function getCallerInfoString() { $backtrace = debug_backtrace(); $msg = ""; if (count($backtrace) >= 1) { $pos = 1; if (count($backtrace) == 1) { $pos = 0; } $trace = $backtrace[$pos]; if (isset($trace['file'], $trace['line'])) { $msg = " From {$trace['file']}:{$trace['line']}."; } } return $msg; } /** * Write log message into outputs. Possible outputs can be: * SYSLOG_HANDLERS = ["mod_syslog_file"] file name is then defined by SYSLOG_FILE * SYSLOG_HANDLERS = ["mod_syslog_syslog"] facility is then defined by SYSLOG_FACILITY * Warning, syslog functions are bugged on Windows, generating memory protection faults. To solve * this, use logging to files instead of syslog (see setup of module). * Note: If constant 'SYSLOG_FILE_NO_ERROR' defined, we never output any error message when writing to log fails. * Note: You can get log message into html sources by adding parameter &logtohtml=1 (constant MAIN_LOGTOHTML must be set) * This function works only if syslog module is enabled. * This must not use any call to other function calling dol_syslog (avoid infinite loop). * * @param string $message Line to log. ''=Show nothing * @param int<0,7> $level Log level * On Windows LOG_ERR=4, LOG_WARNING=5, LOG_NOTICE=LOG_INFO=6, LOG_DEBUG=6 if define_syslog_variables ou PHP 5.3+, 7 if dolibarr * On Linux LOG_ERR=3, LOG_WARNING=4, LOG_NOTICE=5, LOG_INFO=6, LOG_DEBUG=7 * @param int<-1,1> $ident 1=Increase ident of 1 (after log), -1=Decrease ident of 1 (before log) * @param string $suffixinfilename When output is a file, append this suffix into default log filename. Example '_stripe', '_mail' * @param string $restricttologhandler Force output of log only to this log handler * @param ?array $logcontext If defined, an array with extra information (can be used by some log handlers) * @return void * @phan-suppress PhanPluginUnknownArrayFunctionParamType $logcontext is not defined in detail */ function dol_syslog($message, $level = LOG_INFO, $ident = 0, $suffixinfilename = '', $restricttologhandler = '', $logcontext = null) { global $conf, $user, $debugbar; // If syslog module enabled if (!isModEnabled('syslog')) { return; } // Check if we are into execution of code of a website if (defined('USEEXTERNALSERVER') && !defined('USEDOLIBARRSERVER') && !defined('USEDOLIBARREDITOR')) { global $website, $websitekey; if (is_object($website) && !empty($website->ref)) { $suffixinfilename .= '_website_' . $website->ref; } elseif (!empty($websitekey)) { $suffixinfilename .= '_website_' . $websitekey; } } // Check if we have a forced suffix if (defined('USESUFFIXINLOG')) { $suffixinfilename .= constant('USESUFFIXINLOG'); } if ($ident < 0) { foreach ($conf->loghandlers as $loghandlerinstance) { $loghandlerinstance->setIdent($ident); } } if (!empty($message)) { // Test log level // @phan-suppress-next-line PhanPluginDuplicateArrayKey $logLevels = array(LOG_EMERG => 'EMERG', LOG_ALERT => 'ALERT', LOG_CRIT => 'CRITICAL', LOG_ERR => 'ERR', LOG_WARNING => 'WARN', LOG_NOTICE => 'NOTICE', LOG_INFO => 'INFO', LOG_DEBUG => 'DEBUG'); if (!array_key_exists($level, $logLevels)) { dol_syslog('Error Bad Log Level ' . $level, LOG_ERR); $level = LOG_ERR; } if ($level > getDolGlobalInt('SYSLOG_LEVEL')) { return; } if (!getDolGlobalString('MAIN_SHOW_PASSWORD_INTO_LOG')) { $message = preg_replace('/password=\'[^\']*\'/', 'password=\'hidden\'', $message); // protection to avoid to have value of password in log } // If adding log inside HTML page is required if ((!empty($_REQUEST['logtohtml']) && getDolGlobalString('MAIN_ENABLE_LOG_TO_HTML')) || (is_object($user) && $user->hasRight('debugbar', 'read') && is_object($debugbar)) ) { $ospid = sprintf("%7s", dol_trunc((string) getmypid(), 7, 'right', 'UTF-8', 1)); $osuser = " " . sprintf("%6s", dol_trunc(function_exists('posix_getuid') ? posix_getuid() : '', 6, 'right', 'UTF-8', 1)); $conf->logbuffer[] = dol_print_date(dol_now(), "%Y-%m-%d %H:%M:%S") . " " . sprintf("%-7s", $logLevels[$level]) . " " . $ospid . " " . $osuser . " " . $message; } //TODO: Remove this. MAIN_ENABLE_LOG_INLINE_HTML should be deprecated and use a log handler dedicated to HTML output // If html log tag enabled and url parameter log defined, we show output log on HTML comments if (getDolGlobalString('MAIN_ENABLE_LOG_INLINE_HTML') && GETPOSTINT("log")) { print "\n\n\n"; } $data = array( 'message' => $message, 'script' => (isset($_SERVER['PHP_SELF']) ? basename($_SERVER['PHP_SELF'], '.php') : ''), 'level' => $level, 'user' => ((is_object($user) && $user->id) ? $user->login : ''), 'ip' => '', 'osuser' => function_exists('posix_getuid') ? (string) posix_getuid() : '', 'ospid' => (string) getmypid() // on linux, max value is defined into cat /proc/sys/kernel/pid_max ); // For log, we want the reliable IP first. $remoteip = getUserRemoteIP(1); // Get ip when page run on a web server if (!empty($remoteip)) { $data['ip'] = $remoteip; // This is when server run behind a reverse proxy // A HTTP_X_FORWARDED_FOR as format "ip real of user, ip of proxy1, ip of proxy2, ..." // $data['ip'] is last if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { $tmpips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']); $data['ip'] = ''; $foundremoteip = 0; $j = 0; foreach ($tmpips as $tmpip) { $tmpip = trim($tmpip); if (strtolower($tmpip) == strtolower($remoteip)) { $foundremoteip = 1; } if (empty($data['ip'])) { $data['ip'] = $tmpip; } else { $j++; $data['ip'] .= (($j == 1) ? ' [via ' : ',') . $tmpip; } } if (!$foundremoteip) { $j++; $data['ip'] .= (($j == 1) ? ' [via ' : ',') . $remoteip; } $data['ip'] .= (($j > 0) ? ']' : ''); } elseif (!empty($_SERVER['HTTP_CLIENT_IP'])) { $tmpips = explode(',', $_SERVER['HTTP_CLIENT_IP']); $data['ip'] = ''; $foundremoteip = 0; $j = 0; foreach ($tmpips as $tmpip) { $tmpip = trim($tmpip); if (strtolower($tmpip) == strtolower($remoteip)) { $foundremoteip = 1; } if (empty($data['ip'])) { $data['ip'] = $tmpip; } else { $j++; $data['ip'] .= (($j == 1) ? ' [via ' : ',') . $tmpip; } } if (!$foundremoteip) { $j++; $data['ip'] .= (($j == 1) ? ' [via ' : ',') . $remoteip; } $data['ip'] .= (($j > 0) ? ']' : ''); } } elseif (!empty($_SERVER['SERVER_ADDR'])) { // This is when PHP session is ran inside a web server but not inside a client request (example: init code of apache) $data['ip'] = (string) $_SERVER['SERVER_ADDR']; } elseif (!empty($_SERVER['COMPUTERNAME'])) { // This is when PHP session is ran outside a web server, like from Windows command line (Not always defined, but useful if OS defines it). $data['ip'] = (string) $_SERVER['COMPUTERNAME']; } else { $data['ip'] = '???'; } if (!empty($_SERVER['USERNAME'])) { // This is when PHP session is ran outside a web server, like from Linux command line (Not always defined, but useful if OS defines it). $data['osuser'] = (string) $_SERVER['USERNAME']; } elseif (!empty($_SERVER['LOGNAME'])) { // This is when PHP session is ran outside a web server, like from Linux command line (Not always defined, but useful if OS defines it). $data['osuser'] = (string) $_SERVER['LOGNAME']; } // Loop on each log handler and send output foreach ($conf->loghandlers as $loghandlerinstance) { if ($restricttologhandler && $loghandlerinstance->code != $restricttologhandler) { continue; } $loghandlerinstance->export($data, $suffixinfilename); } unset($data); } if ($ident > 0) { foreach ($conf->loghandlers as $loghandlerinstance) { $loghandlerinstance->setIdent($ident); } } } /** * Return a formatted address (part address/zip/town/state) according to country rules. * See https://en.wikipedia.org/wiki/Address * * @param Object $object A company or contact object * @param int $withcountry 1=Add country into address string * @param string $sep Separator to use to separate info when building string * @param ?Translate $outputlangs Object lang that contains language for text translation. * @param int $mode 0=Standard output, 1=Remove address * @param string $extralangcode User extralanguage $langcode as values for address, town * @return string Formatted string * @see dol_print_address() */ function dol_format_address($object, $withcountry = 0, $sep = "\n", $outputlangs = null, $mode = 0, $extralangcode = '') { global $langs, $hookmanager; $ret = ''; $countriesusingstate = array('AU', 'CA', 'US', 'IN', 'GB', 'ES', 'UK', 'TR', 'CN'); // See also MAIN_FORCE_STATE_INTO_ADDRESS // See format of addresses on https://en.wikipedia.org/wiki/Address // Address if (empty($mode)) { $ret .= (($extralangcode && !empty($object->array_languages['address'][$extralangcode])) ? $object->array_languages['address'][$extralangcode] : (empty($object->address) ? '' : preg_replace('/(\r\n|\r|\n)+/', $sep, $object->address))); } // Zip/Town/State if (isset($object->country_code) && in_array($object->country_code, array('AU', 'CA', 'US', 'CN')) || getDolGlobalString('MAIN_FORCE_STATE_INTO_ADDRESS')) { // US: title firstname name \n address lines \n town, state, zip \n country $town = ($extralangcode ? $object->array_languages['town'][$extralangcode] : (empty($object->town) ? '' : $object->town)); $ret .= (($ret && $town) ? $sep : '') . $town; if (!empty($object->state)) { $ret .= ($ret ? ($town ? ", " : $sep) : '') . $object->state; } if (!empty($object->zip)) { $ret .= ($ret ? (($town || $object->state) ? ", " : $sep) : '') . $object->zip; } } elseif (isset($object->country_code) && in_array($object->country_code, array('GB', 'UK'))) { // UK: title firstname name \n address lines \n town state \n zip \n country $town = ($extralangcode ? $object->array_languages['town'][$extralangcode] : (empty($object->town) ? '' : $object->town)); $ret .= ($ret ? $sep : '') . $town; if (!empty($object->state)) { $ret .= ($ret ? ", " : '') . $object->state; } if (!empty($object->zip)) { $ret .= ($ret ? $sep : '') . $object->zip; } } elseif (isset($object->country_code) && in_array($object->country_code, array('ES', 'TR'))) { // ES: title firstname name \n address lines \n zip town \n state \n country $ret .= ($ret ? $sep : '') . $object->zip; $town = ($extralangcode ? $object->array_languages['town'][$extralangcode] : (empty($object->town) ? '' : $object->town)); $ret .= ($town ? (($object->zip ? ' ' : '') . $town) : ''); if (!empty($object->state)) { $ret .= $sep . $object->state; } } elseif (isset($object->country_code) && in_array($object->country_code, array('JP'))) { // JP: In romaji, title firstname name\n address lines \n [state,] town zip \n country // See https://www.sljfaq.org/afaq/addresses.html $town = ($extralangcode ? $object->array_languages['town'][$extralangcode] : (empty($object->town) ? '' : $object->town)); $ret .= ($ret ? $sep : '') . ($object->state ? $object->state . ', ' : '') . $town . ($object->zip ? ' ' : '') . $object->zip; } elseif (isset($object->country_code) && in_array($object->country_code, array('IT'))) { // IT: title firstname name\n address lines \n zip town state_code \n country $ret .= ($ret ? $sep : '') . $object->zip; $town = ($extralangcode ? $object->array_languages['town'][$extralangcode] : (empty($object->town) ? '' : $object->town)); $ret .= ($town ? (($object->zip ? ' ' : '') . $town) : ''); $ret .= (empty($object->state_code) ? '' : (' ' . $object->state_code)); } else { // Other: title firstname name \n address lines \n zip town[, state] \n country $town = (($extralangcode && !empty($object->array_languages['address'][$extralangcode])) ? $object->array_languages['town'][$extralangcode] : (empty($object->town) ? '' : $object->town)); $ret .= !empty($object->zip) ? (($ret ? $sep : '') . $object->zip) : ''; $ret .= ($town ? (($object->zip ? ' ' : ($ret ? $sep : '')) . $town) : ''); if (!empty($object->state) && in_array($object->country_code, $countriesusingstate)) { $ret .= ($ret ? ", " : '') . $object->state; } } if (!is_object($outputlangs)) { $outputlangs = $langs; } if ($withcountry) { $langs->load("dict"); $ret .= (empty($object->country_code) ? '' : ($ret ? $sep : '') . $outputlangs->convToOutputCharset($outputlangs->transnoentitiesnoconv("Country" . $object->country_code))); } if ($hookmanager) { $parameters = array('withcountry' => $withcountry, 'sep' => $sep, 'outputlangs' => $outputlangs, 'mode' => $mode, 'extralangcode' => $extralangcode); $reshook = $hookmanager->executeHooks('formatAddress', $parameters, $object); if ($reshook > 0) { $ret = ''; } $ret .= $hookmanager->resPrint; } return $ret; } /** * Format a string. * * @param string $fmt Format of strftime function (http://php.net/manual/fr/function.strftime.php) * @param int|false $ts Timestamp (If is_gmt is true, timestamp is already includes timezone and daylight saving offset, if is_gmt is false, timestamp is a GMT timestamp and we must compensate with server PHP TZ) * @param bool $is_gmt See comment of timestamp parameter * @return string A formatted string * @see dol_stringtotime() */ function dol_strftime($fmt, $ts = false, $is_gmt = false) { if ((abs($ts) <= 0x7FFFFFFF)) { // check if number in 32-bit signed range return dol_print_date($ts, $fmt, $is_gmt); } else { return 'Error date outside supported range'; } } /** * Output date in a string format according to outputlangs (or langs if not defined). * Return charset is always UTF-8, except if encodetoouput is defined. In this case charset is output charset * * @param null|int|string $time GM Timestamps date * @param string $format Output date format (tag of strftime function) * "%d %b %Y", * "%d/%m/%Y %H:%M", * "%d/%m/%Y %H:%M:%S", * "%B"=Long text of month, "%A"=Long text of day, "%b"=Short text of month, "%a"=Short text of day * "day", "daytext", "dayhour", "dayhourldap", "dayhourtext", "dayrfc", "dayhourrfc", "...inputnoreduce", "...reduceformat" * @param string|bool $tzoutput true or 'gmt' => string is for Greenwich location * false or 'tzserver' => output string is for local PHP server TZ usage * 'tzuser' => output string is for user TZ (current browser TZ with current dst) => In a future, we should have same behaviour than 'tzuserrel' * 'tzuserrel' => output string is for user TZ (current browser TZ with dst or not, depending on date position) * @param ?Translate $outputlangs Object lang that contains language for text translation. * @param boolean $encodetooutput Use true to convert/encode string into the HTML rendering pagecode (false=keep UTF8 by default) * @param int|string $decorate Use 1 or a css to apply a HTML css style to decorate the date * @return string Formatted date or '' if time is null * * @see dol_mktime(), dol_stringtotime(), dol_getdate(), selectDate() */ function dol_print_date($time, $format = '', $tzoutput = 'auto', $outputlangs = null, $encodetooutput = false, $decorate = 0) { global $conf, $langs; // If date undefined or "", we return "" if (dol_strlen((string) $time) == 0) { return ''; // $time=0 allowed (it means 01/01/1970 00:00:00) } if ($tzoutput === 'auto') { $tzoutput = (empty($conf) ? 'tzserver' : (isset($conf->tzuserinputkey) ? $conf->tzuserinputkey : 'tzserver')); } // Clean parameters $to_gmt = false; // false if we want date in server timezone, true if we want to add offset $offsettz = $offsetdst = 0; if ($tzoutput) { $to_gmt = true; // For backward compatibility if (is_string($tzoutput)) { if ($tzoutput == 'tzserver') { $to_gmt = false; $offsettzstring = @date_default_timezone_get(); // Example 'Europe/Berlin' or 'Indian/Reunion' // @phan-suppress-next-line PhanPluginRedundantAssignment $offsettz = 0; // Timezone offset with server timezone (because to_gmt is false), so 0 // @phan-suppress-next-line PhanPluginRedundantAssignment $offsetdst = 0; // Dst offset with server timezone (because to_gmt is false), so 0 } elseif ($tzoutput == 'tzuser' || $tzoutput == 'tzuserrel') { $to_gmt = true; // if no session (by example in cron) may use MAIN_DOLIBARR_USER_TIMEZONE instead UTC $offsettzstring = (empty($_SESSION['dol_tz_string']) ? getDolGlobalString('MAIN_DOLIBARR_USER_TIMEZONE', 'UTC') : $_SESSION['dol_tz_string']); // Example 'Europe/Berlin' or 'Indian/Reunion' if (class_exists('DateTimeZone')) { try { $user_date_tz = new DateTimeZone($offsettzstring); } catch (Exception $e) { // Bad value for $offsettzstring dol_syslog("DateInvalidTimeZoneException for timezone string '".$offsettzstring."'. Falling back to UTC.", LOG_ERR); $user_date_tz = new DateTimeZone('UTC'); // Force valid timezone as UTC } $user_dt = new DateTime(); $user_dt->setTimezone($user_date_tz); $user_dt->setTimestamp($tzoutput == 'tzuser' ? dol_now() : (int) $time); $offsettz = $user_dt->getOffset(); // should include dst ? } else { // with old method (The 'tzuser' was processed like the 'tzuserrel') $offsettz = (empty($_SESSION['dol_tz']) ? 0 : $_SESSION['dol_tz']) * 60 * 60; // Will not be used anymore $offsetdst = (empty($_SESSION['dol_dst']) ? 0 : $_SESSION['dol_dst']) * 60 * 60; // Will not be used anymore } } } } if (!is_object($outputlangs)) { $outputlangs = $langs; } if (!$format) { $format = 'daytextshort'; } // Do we have to reduce the length of date (year on 2 chars) to save space. // Note: dayinputnoreduce is same than day but no reduction of year length will be done $reduceformat = (!empty($conf->dol_optimize_smallscreen) && in_array($format, array('day', 'dayhour', 'dayhoursec'))) ? 1 : 0; // Test on original $format param. $format = preg_replace('/inputnoreduce/', '', $format); // so format 'dayinputnoreduce' is processed like day $formatwithoutreduce = preg_replace('/reduceformat/', '', $format); if ($formatwithoutreduce != $format) { $format = $formatwithoutreduce; $reduceformat = 1; } // so format 'dayreduceformat' is processed like day // Change predefined format into computer format. If found translation in lang file we use it, otherwise we use default. // TODO Add format daysmallyear and dayhoursmallyear if ($format == 'day') { $format = ($outputlangs->trans("FormatDateShort") != "FormatDateShort" ? $outputlangs->trans("FormatDateShort") : $conf->format_date_short); } elseif ($format == 'hour') { $format = ($outputlangs->trans("FormatHourShort") != "FormatHourShort" ? $outputlangs->trans("FormatHourShort") : $conf->format_hour_short); } elseif ($format == 'hoursec') { $s1 = $outputlangs->trans("FormatDateShort"); $s2 = $outputlangs->trans("FormatDateHourSecShort"); $s3 = trim(preg_replace('/'.preg_quote($s1, '/').'/', '', $s2)); // Try to guess the format for FormatHourSecShort using FormatDateShort and FormatDateHourSecShort $format = $s3; //$format = ($outputlangs->trans("FormatHourSecShort") != "FormatHourSecShort" ? $outputlangs->trans("FormatHourSecShort") : ($s3 ? $s3 : $conf->format_hour_sec_short)); } elseif ($format == 'hourduration') { $format = ($outputlangs->trans("FormatHourShortDuration") != "FormatHourShortDuration" ? $outputlangs->trans("FormatHourShortDuration") : $conf->format_hour_short_duration); } elseif ($format == 'daytext') { $format = ($outputlangs->trans("FormatDateText") != "FormatDateText" ? $outputlangs->trans("FormatDateText") : $conf->format_date_text); } elseif ($format == 'daytextshort') { $format = ($outputlangs->trans("FormatDateTextShort") != "FormatDateTextShort" ? $outputlangs->trans("FormatDateTextShort") : $conf->format_date_text_short); } elseif ($format == 'dayhour') { $format = ($outputlangs->trans("FormatDateHourShort") != "FormatDateHourShort" ? $outputlangs->trans("FormatDateHourShort") : $conf->format_date_hour_short); } elseif ($format == 'dayhoursec') { $format = ($outputlangs->trans("FormatDateHourSecShort") != "FormatDateHourSecShort" ? $outputlangs->trans("FormatDateHourSecShort") : $conf->format_date_hour_sec_short); } elseif ($format == 'dayhourtext') { $format = ($outputlangs->trans("FormatDateHourText") != "FormatDateHourText" ? $outputlangs->trans("FormatDateHourText") : $conf->format_date_hour_text); } elseif ($format == 'dayhourtextshort') { $format = ($outputlangs->trans("FormatDateHourTextShort") != "FormatDateHourTextShort" ? $outputlangs->trans("FormatDateHourTextShort") : $conf->format_date_hour_text_short); } elseif ($format == 'dayhourlog') { // Format not sensitive to language $format = '%Y%m%d%H%M%S'; } elseif ($format == 'dayhourlogsmall') { // Format not sensitive to language $format = '%y%m%d%H%M'; } elseif ($format == 'dayhourldap') { $format = '%Y%m%d%H%M%SZ'; } elseif ($format == 'dayhourxcard') { $format = '%Y%m%dT%H%M%SZ'; } elseif ($format == 'dayxcard') { $format = '%Y%m%d'; } elseif ($format == 'dayrfc') { $format = '%Y-%m-%d'; // DATE_RFC3339 } elseif ($format == 'dayhourrfc') { $format = '%Y-%m-%dT%H:%M:%SZ'; // DATETIME RFC3339 } elseif ($format == 'standard') { $format = '%Y-%m-%d %H:%M:%S'; } if ($reduceformat) { $format = str_replace('%Y', '%y', $format); $format = str_replace('yyyy', 'yy', $format); } // Clean format if (preg_match('/%b/i', $format)) { // There is some text to translate // We inhibit translation to text made by strftime functions. We will use trans instead later. $format = str_replace('%b', '__b__', $format); $format = str_replace('%B', '__B__', $format); } if (preg_match('/%a/i', $format)) { // There is some text to translate // We inhibit translation to text made by strftime functions. We will use trans instead later. $format = str_replace('%a', '__a__', $format); $format = str_replace('%A', '__A__', $format); } // Analyze date $reg = array(); if (preg_match('/^([0-9][0-9][0-9][0-9])([0-9][0-9])([0-9][0-9])([0-9][0-9])([0-9][0-9])([0-9][0-9])$/i', (string) $time, $reg)) { // Deprecated. Ex: 1970-01-01, 1970-01-01 01:00:00, 19700101010000 dol_print_error(null, "Functions.lib::dol_print_date function called with a bad value" . getCallerInfoString()); return ''; } elseif (preg_match('/^([0-9]+)\-([0-9]+)\-([0-9]+) ?([0-9]+)?:?([0-9]+)?:?([0-9]+)?/i', (string) $time, $reg)) { // Still available to solve problems in extrafields of type date // This part of code should not be used anymore. dol_syslog("Functions.lib::dol_print_date function called with a bad value" . getCallerInfoString(), LOG_WARNING); // Date has format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS' $syear = (!empty($reg[1]) ? $reg[1] : ''); $smonth = (!empty($reg[2]) ? $reg[2] : ''); $sday = (!empty($reg[3]) ? $reg[3] : ''); $shour = (!empty($reg[4]) ? $reg[4] : ''); $smin = (!empty($reg[5]) ? $reg[5] : ''); $ssec = (!empty($reg[6]) ? $reg[6] : ''); $time = dol_mktime((int) $shour, (int) $smin, (int) $ssec, (int) $smonth, (int) $sday, (int) $syear, true); if ($to_gmt) { $tzo = new DateTimeZone('UTC'); // when to_gmt is true, base for offsettz and offsetdst (so timetouse) is UTC } else { $tzo = new DateTimeZone(date_default_timezone_get()); // when to_gmt is false, base for offsettz and offsetdst (so timetouse) is PHP server } $dtts = new DateTime(); $dtts->setTimestamp($time); $dtts->setTimezone($tzo); $newformat = str_replace( array('%Y', '%y', '%m', '%d', '%H', '%I', '%M', '%S', '%p', 'T', 'Z', '__a__', '__A__', '__b__', '__B__'), array('Y', 'y', 'm', 'd', 'H', 'h', 'i', 's', 'A', '__£__', '__$__', '__{__', '__}__', '__[__', '__]__'), $format ); $ret = $dtts->format($newformat); $ret = str_replace( array('__£__', '__$__', '__{__', '__}__', '__[__', '__]__'), array('T', 'Z', '__a__', '__A__', '__b__', '__B__'), $ret ); } else { // Date is a timestamps if ($time < 100000000000) { // Protection against bad date values $dtts = new DateTime(); //var_dump($tzoutput.' '.$offsettzstring.' '.$offsettz.$offsetdst.' x '.$to_gmt); if ($to_gmt) { // to_gmt means "not in php server timezone" (if tzoutput = 'gmt', offsets should be 0 but if = 'tzuser...', offsets may be defined $timetouse = (int) $time + $offsettz + $offsetdst; // TODO We could be able to disable use of offsettz and offsetdst to use only offsettzstring. $tzo = new DateTimeZone('UTC'); // when to_gmt is true, base for offsettz and offsetdst (so timetouse) is UTC $dtts->setTimezone($tzo); // important: must be before the setTimestamp $dtts->setTimestamp($timetouse); } else { $timetouse = (int) $time + $offsettz + $offsetdst; // TODO We could be able to disable use of offsettz and offsetdst to use only offsettzstring. $tzo = new DateTimeZone(date_default_timezone_get()); // when to_gmt is false, base for offsettz and offsetdst (so timetouse) is PHP server $dtts->setTimestamp($timetouse); // TODO May be we can invert setTimestamp and setTimezone $dtts->setTimezone($tzo); } $newformat = str_replace( array('%Y', '%y', '%m', '%d', '%H', '%I', '%M', '%S', '%p', '%w', 'T', 'Z', '__a__', '__A__', '__b__', '__B__'), array('Y', 'y', 'm', 'd', 'H', 'h', 'i', 's', 'A', 'w', '__£__', '__$__', '__{__', '__}__', '__[__', '__]__'), $format ); $ret = $dtts->format($newformat); //var_dump($timetouse, $offsettz, $offsetdst, $tzo, $newformat, $ret); $ret = str_replace( array('__£__', '__$__', '__{__', '__}__', '__[__', '__]__'), array('T', 'Z', '__a__', '__A__', '__b__', '__B__'), $ret ); } else { $ret = 'Bad value ' . $time . ' for date'; } } if (preg_match('/__b__/i', $format)) { $timetouse = $time + $offsettz + $offsetdst; // TODO We could be able to disable use of offsettz and offsetdst to use only offsettzstring. if ($to_gmt) { $tzo = new DateTimeZone('UTC'); // when to_gmt is true, base for offsettz and offsetdst (so timetouse) is UTC } else { $tzo = new DateTimeZone(date_default_timezone_get()); // when to_gmt is false, base for offsettz and offsetdst (so timetouse) is PHP server } $dtts = new DateTime(); $dtts->setTimestamp($timetouse); $dtts->setTimezone($tzo); $month = (int) $dtts->format("m"); $month = sprintf("%02d", $month); // $month may be return with format '06' on some installation and '6' on other, so we force it to '06'. if ($encodetooutput) { $monthtext = $outputlangs->transnoentities('Month' . $month); $monthtextshort = $outputlangs->transnoentities('MonthShort' . $month); } else { $monthtext = $outputlangs->transnoentitiesnoconv('Month' . $month); $monthtextshort = $outputlangs->transnoentitiesnoconv('MonthShort' . $month); } //print 'monthtext='.$monthtext.' monthtextshort='.$monthtextshort; $ret = str_replace('__b__', $monthtextshort, $ret); $ret = str_replace('__B__', $monthtext, $ret); //print 'x'.$outputlangs->charset_output.'-'.$ret.'x'; //return $ret; } if (preg_match('/__a__/i', $format)) { //print "time=$time offsettz=$offsettz offsetdst=$offsetdst offsettzstring=$offsettzstring"; $timetouse = $time + $offsettz + $offsetdst; // TODO Replace this with function Date PHP. We also should not use anymore offsettz and offsetdst but only offsettzstring. if ($to_gmt) { $tzo = new DateTimeZone('UTC'); } else { $tzo = new DateTimeZone(date_default_timezone_get()); } $dtts = new DateTime(); $dtts->setTimestamp($timetouse); $dtts->setTimezone($tzo); $w = $dtts->format("w"); $dayweek = $outputlangs->transnoentitiesnoconv('Day' . $w); $ret = str_replace('__A__', $dayweek, $ret); $ret = str_replace('__a__', dol_substr($dayweek, 0, 3), $ret); } if ($decorate) { $ret = preg_replace('/(\d\d:\d\d [AP]M)$/', '\1', $ret); $ret = preg_replace('/(\d\d:\d\d)$/', '\1', $ret); } return $ret; } /** * Return an array with locale date info. * WARNING: This function use PHP server timezone by default to return locale information. * Be aware to add the third parameter to "UTC" if you need to work on UTC. * * @param int $timestamp Timestamp * @param boolean $fast Fast mode. deprecated. * @param string $forcetimezone '' to use the PHP server timezone. Or use a form like 'gmt', 'tzserver', 'Europe/Paris' or '+0200' to force timezone. * @return array{}|array{seconds:int<0,59>,minutes:int<0,59>,hours:int<0,23>,mday:int<1,31>,wday:int<0,6>,mon:int<1,12>,year:int<0,9999>,yday:int<0,366>,0:int} Array of information * 'seconds' => $secs, * 'minutes' => $min, * 'hours' => $hour, * 'mday' => $day, * 'wday' => $dow, 0=sunday, 6=saturday * 'mon' => $month, * 'year' => $year, * 'yday' => floor($secsInYear/$_day_power) * '0' => original timestamp * @see dol_print_date(), dol_stringtotime(), dol_mktime() */ function dol_getdate($timestamp, $fast = false, $forcetimezone = '') { if ($timestamp === '') { return array(); } $datetimeobj = new DateTime(); $datetimeobj->setTimestamp($timestamp); // Use local PHP server timezone if ($forcetimezone && $forcetimezone != 'tzserver') { $datetimeobj->setTimezone(new DateTimeZone($forcetimezone == 'gmt' ? 'UTC' : $forcetimezone)); // (add timezone relative to the date entered) } $arrayinfo = array( 'year' => ((int) date_format($datetimeobj, 'Y')), 'mon' => ((int) date_format($datetimeobj, 'm')), 'mday' => ((int) date_format($datetimeobj, 'd')), 'wday' => ((int) date_format($datetimeobj, 'w')), 'yday' => ((int) date_format($datetimeobj, 'z')), 'hours' => ((int) date_format($datetimeobj, 'H')), 'minutes' => ((int) date_format($datetimeobj, 'i')), 'seconds' => ((int) date_format($datetimeobj, 's')), '0' => $timestamp ); return $arrayinfo; } /** * Return a timestamp date built from detailed information (by default a local PHP server timestamp) * Replace function mktime not available under Windows if year < 1970 * PHP mktime is restricted to the years 1901-2038 on Unix and 1970-2038 on Windows * * @param int $hour Hour (can be -1 for undefined) * @param int $minute Minute (can be -1 for undefined) * @param int $second Second (can be -1 for undefined) * @param int $month Month (1 to 12) * @param int $day Day (1 to 31) * @param int $year Year * @param bool|int|string $gm True or 1 or 'gmt'=Input information are GMT values * False or 0 or 'tzserver' = local to server TZ * 'auto' * 'tzuser' = local to user TZ taking dst into account at the current date. Not yet implemented. * 'tzuserrel' = local to user TZ taking dst into account at the given date. Use this one to convert date input from user into a GMT date. * 'tz,TimeZone' = use specified timezone * @param int $check 0=No check on parameters (Can use day 32, etc...) * @return int|'' Date as a timestamp, '' if error * @see dol_print_date(), dol_stringtotime(), dol_getdate() */ function dol_mktime($hour, $minute, $second, $month, $day, $year, $gm = 'auto', $check = 1) { global $conf; //print "- ".$hour.",".$minute.",".$second.",".$month.",".$day.",".$year.",".$_SERVER["WINDIR"]." -"; if ($gm === 'auto') { $gm = (empty($conf) ? 'tzserver' : $conf->tzuserinputkey); } //print 'gm:'.$gm.' gm === auto:'.($gm === 'auto').'
';exit; // Clean parameters if ($hour == -1 || empty($hour)) { $hour = 0; } if ($minute == -1 || empty($minute)) { $minute = 0; } if ($second == -1 || empty($second)) { $second = 0; } // Check parameters if ($check) { if (!$month || !$day) { return ''; } if ($day > 31) { return ''; } if ($month > 12) { return ''; } if ($hour < 0 || $hour > 24) { return ''; } if ($minute < 0 || $minute > 60) { return ''; } if ($second < 0 || $second > 60) { return ''; } } if (empty($gm) || ($gm === 'server' || $gm === 'tzserver')) { $default_timezone = @date_default_timezone_get(); // Example 'Europe/Berlin' $localtz = new DateTimeZone($default_timezone); } elseif ($gm === 'user' || $gm === 'tzuser' || $gm === 'tzuserrel') { // We use dol_tz_string first because it is more reliable. $default_timezone = (empty($_SESSION["dol_tz_string"]) ? @date_default_timezone_get() : $_SESSION["dol_tz_string"]); // Example 'Europe/Berlin' try { $localtz = new DateTimeZone($default_timezone); } catch (Exception $e) { dol_syslog("Warning dol_tz_string contains an invalid value " . json_encode($_SESSION["dol_tz_string"] ?? null), LOG_WARNING); $default_timezone = @date_default_timezone_get(); } } elseif (strrpos($gm, "tz,") !== false) { $timezone = (string) str_replace("tz,", "", $gm); // Example 'tz,Europe/Berlin' try { $localtz = new DateTimeZone($timezone); } catch (Exception $e) { dol_syslog("Warning passed timezone contains an invalid value " . $timezone, LOG_WARNING); } } if (empty($localtz)) { $localtz = new DateTimeZone('UTC'); } $dt = new DateTime('now', $localtz); $dt->setDate((int) $year, (int) $month, (int) $day); $dt->setTime((int) $hour, (int) $minute, (int) $second); $date = $dt->getTimestamp(); // should include daylight saving time return $date; } /** * Return date for now. In most cases, we use this function without parameters (that means GMT time). * * @param 'auto'|'gmt'|'tzserver'|'tzuser'|'tzuserrel' $mode 'auto' => for backward compatibility (avoid this), * 'gmt' => we return GMT timestamp, * 'tzserver' => we add the PHP server timezone * 'tzref' => we add the company timezone. Not implemented. * 'tzuser' or 'tzuserrel' => we add the user timezone * @return int $date Timestamp */ function dol_now($mode = 'gmt') { $ret = 0; if ($mode === 'auto') { $mode = 'gmt'; } if ($mode == 'gmt') { $ret = time(); // Time for now at greenwich. } elseif ($mode == 'tzserver') { // Time for now with PHP server timezone added require_once DOL_DOCUMENT_ROOT . '/core/lib/date.lib.php'; $tzsecond = getServerTimeZoneInt('now'); // Contains tz+dayling saving time $ret = (int) (dol_now('gmt') + ($tzsecond * 3600)); // } elseif ($mode == 'tzref') {// Time for now with parent company timezone is added // require_once DOL_DOCUMENT_ROOT.'/core/lib/date.lib.php'; // $tzsecond=getParentCompanyTimeZoneInt(); // Contains tz+dayling saving time // $ret=dol_now('gmt')+($tzsecond*3600); } elseif ($mode == 'tzuser' || $mode == 'tzuserrel') { // Time for now with user timezone added // print 'time: '.time(); $offsettz = (empty($_SESSION['dol_tz']) ? 0 : $_SESSION['dol_tz']) * 60 * 60; $offsetdst = (empty($_SESSION['dol_dst']) ? 0 : $_SESSION['dol_dst']) * 60 * 60; $ret = (int) (dol_now('gmt') + ($offsettz + $offsetdst)); } return $ret; } /** * Return string with formatted size * * @param int $size Size to print * @param int $shortvalue Tell if we want long value to use another unit (Ex: 1.5Kb instead of 1500b) * @param int $shortunit Use short label of size unit (for example 'b' instead of 'bytes') * @return string Link */ function dol_print_size($size, $shortvalue = 0, $shortunit = 0) { global $conf, $langs; $level = 1024; if (!empty($conf->dol_optimize_smallscreen)) { $shortunit = 1; } // Set value text if (empty($shortvalue) || $size < ($level * 10)) { $ret = $size; $textunitshort = $langs->trans("b"); $textunitlong = $langs->trans("Bytes"); } else { $ret = round($size / $level, 0); $textunitshort = $langs->trans("Kb"); $textunitlong = $langs->trans("KiloBytes"); } // Use long or short text unit if (empty($shortunit)) { $ret .= ' ' . $textunitlong; } else { $ret .= ' ' . $textunitshort; } return $ret; } /** * Show Url link * * @param string $url Url to show * @param string $target Target for link * @param int $max Max number of characters to show * @param int $withpicto With picto * @param string $morecss More CSS * @return string HTML Link */ function dol_print_url($url, $target = '_blank', $max = 32, $withpicto = 0, $morecss = '') { global $langs; if (empty($url)) { return ''; } $linkstart = 'trans("URL") . ': ' . $url . '"'; $linkstart .= '>'; $link = ''; if (!preg_match('/^http/i', $url)) { $link .= 'http://'; } $link .= dol_trunc($url, $max); $linkend = ''; if ($morecss == 'float') { // deprecated return '
' . ($withpicto ? img_picto($langs->trans("Url"), 'globe', 'class="paddingrightonly"') : '') . $link . '
'; } else { return $linkstart . '' . ($withpicto ? img_picto('', 'globe', 'class="paddingrightonly"') : '') . $link . '' . $linkend; } } /** * Show EMail link formatted for HTML output. * * @param string $email EMail to show (can be 'email' or 'Name of recipient ') * @param int $contactid Id of contact if known * @param int $socid Id of third party if known * @param int|string $addlink 0=no link, 1=email has a html email link (+ link to create action if constant AGENDA_ADDACTIONFOREMAIL is on), 'thirdparty'=link to the thirdparty presend email * @param int $max Max number of characters to show. Use -1 to hide the mail text and show only the picto. * @param int $showinvalid 0=no check on email, 1=Show warning if syntax email is wrong (fast), 2=Check also DNS domain (slow) * @param int|string $withpicto 0=Show email, 1=Show picto of email + email, 2=Show only picto * @param string $morecss More CSS * @return string HTML Link */ function dol_print_email($email, $contactid = 0, $socid = 0, $addlink = 0, $max = 0, $showinvalid = 2, $withpicto = 0, $morecss = 'paddingrightonly') { global $user, $langs, $hookmanager; //global $conf; $conf->global->AGENDA_ADDACTIONFOREMAIL = 1; //$showinvalid = 1; $email = 'rrrrr'; $newemail = dol_escape_htmltag($email); if (getDolGlobalString('MAIN_OPTIMIZEFORTEXTBROWSER') && $withpicto) { $withpicto = 0; } if (empty($email)) { return ' '; } if ($addlink == 1) { $newemail = ''; $newemail .= ($withpicto ? img_picto($langs->trans("EMail") . ' : ' . $email, (is_numeric($withpicto) ? 'email' : $withpicto), 'class="paddingrightonly"') : ''); if ($max > 0) { $newemail .= dol_trunc($email, $max); } else { $newemail .= $email; } $newemail .= ''; if ($showinvalid) { include_once DOL_DOCUMENT_ROOT.'/core/class/CMailFile.class.php'; include_once DOL_DOCUMENT_ROOT.'/core/lib/functions2.lib.php'; $emailonly = CMailFile::getValidAddress($email, 2); if (!isValidEmail($emailonly)) { $langs->load("errors"); $newemail .= img_warning($langs->transnoentitiesnoconv("ErrorBadEMail", $emailonly), '', 'paddingrightonly'); } elseif ($showinvalid == 2 && !isValidMailDomain($emailonly)) { $langs->load("errors"); $newemail .= img_warning($langs->transnoentitiesnoconv("ErrorBadMXDomain", $emailonly), '', 'paddingrightonly'); } } if (($contactid || $socid) && isModEnabled('agenda') && $user->hasRight("agenda", "myactions", "create")) { $type = 'AC_EMAIL'; $linktoaddaction = ''; if (getDolGlobalString('AGENDA_ADDACTIONFOREMAIL')) { $linktoaddaction = '' . img_object($langs->trans("AddAction"), "calendar") . ''; } if ($linktoaddaction) { $newemail = '
' . $newemail . ' ' . $linktoaddaction . '
'; } } } elseif ($addlink === 'thirdparty') { $tmpnewemail = ''; $tmpnewemail .= ($withpicto ? img_picto($langs->trans("EMail") . ' : ' . $email, (is_numeric($withpicto) ? 'email' : $withpicto), 'class="paddingrightonly"') : ''); if ($withpicto == 1) { $tmpnewemail .= $newemail; } $tmpnewemail .= ''; $newemail = $tmpnewemail; } else { $newemail = ($withpicto ? img_picto($langs->trans("EMail") . ' : ' . $email, (is_numeric($withpicto) ? 'email' : $withpicto), 'class="paddingrightonly"') : '') . $newemail; if ($showinvalid) { include_once DOL_DOCUMENT_ROOT.'/core/class/CMailFile.class.php'; include_once DOL_DOCUMENT_ROOT.'/core/lib/functions2.lib.php'; $emailonly = CMailFile::getValidAddress($email, 2); if (!isValidEmail($emailonly)) { $langs->load("errors"); $newemail .= img_warning($langs->transnoentitiesnoconv("ErrorBadEMail", $email)); } elseif ($showinvalid == 2 && !isValidMailDomain($emailonly)) { $langs->load("errors"); $newemail .= img_warning($langs->transnoentitiesnoconv("ErrorBadMXDomain", $emailonly)); } } } //$rep = '
'; //$rep = ($withpicto ? img_picto($langs->trans("EMail").' : '.$email, (is_numeric($withpicto) ? 'email' : $withpicto), 'class="paddingrightonly"') : '').$newemail; //$rep .= '
'; $rep = $newemail; if (getDolGlobalString('MAIN_MAIL_COPY_ON_CLICK')) { $rep .= showValueWithClipboardCPButton($newemail, 0, 'none'); } if ($hookmanager) { $parameters = array('cid' => $contactid, 'socid' => $socid, 'addlink' => $addlink, 'picto' => $withpicto); $reshook = $hookmanager->executeHooks('printEmail', $parameters, $email); if ($reshook > 0) { $rep = ''; } $rep .= $hookmanager->resPrint; } return $rep; } /** * Get array of social network dictionary * * @return array Array of Social Networks Dictionary */ function getArrayOfSocialNetworks() { global $db; $socialnetworks = array(); // Enable caching of array require_once DOL_DOCUMENT_ROOT . '/core/lib/memory.lib.php'; $cachekey = dol_sanitizeKeyCode(str_replace(',', '_', 'socialnetworks_'.getEntity('c_socialnetworks'))); $dataretrieved = dol_getcache($cachekey); if (!is_null($dataretrieved)) { $socialnetworks = $dataretrieved; } else { $sql = "SELECT rowid, code, label, url, icon, active FROM " . MAIN_DB_PREFIX . "c_socialnetworks"; $sql .= " WHERE entity IN (" . getEntity('c_socialnetworks').")"; $resql = $db->query($sql); if ($resql) { while ($obj = $db->fetch_object($resql)) { $socialnetworks[$obj->code] = array( 'rowid' => $obj->rowid, 'label' => $obj->label, 'url' => $obj->url, 'icon' => $obj->icon, 'active' => $obj->active, ); } } dol_setcache($cachekey, $socialnetworks); // If setting cache fails, this is not a problem, so we do not test result. } return (is_array($socialnetworks) ? $socialnetworks : array()); } /** * Show social network link * * @param string $value Social network ID to show (only skype, without 'Name of recipient' before) * @param int $contactid Id of contact if known * @param int $socid Id of third party if known * @param string $type Type of social network: 'skype','facebook',... * @param array $dictsocialnetworks List of socialnetworks available * @return string HTML Link */ function dol_print_socialnetworks($value, $contactid, $socid, $type, $dictsocialnetworks = array()) { global $hookmanager, $langs, $user; $htmllink = $value; if (empty($value)) { return ' '; } if (!empty($type)) { $htmllink = '
'; // Use dictionary definition for picto $dictsocialnetworks[$type]['icon'] $htmllink .= ''; if ($type == 'skype') { $htmllink .= dol_escape_htmltag($value); $htmllink .= '  '; $htmllink .= ''; $htmllink .= ''; $htmllink .= ''; $htmllink .= ''; if (($contactid || $socid) && isModEnabled('agenda') && $user->hasRight('agenda', 'myactions', 'create')) { $addlink = 'AC_SKYPE'; $link = ''; if (getDolGlobalString('AGENDA_ADDACTIONFORSKYPE')) { $link = '' . img_object($langs->trans("AddAction"), "calendar") . ''; } $htmllink .= ($link ? ' ' . $link : ''); } } else { if (!empty($dictsocialnetworks[$type]['url'])) { $tmpvirginurl = preg_replace('/\/?{socialid}/', '', $dictsocialnetworks[$type]['url']); if ($tmpvirginurl) { $value = preg_replace('/^www\.' . preg_quote($tmpvirginurl, '/') . '\/?/', '', $value); $value = preg_replace('/^' . preg_quote($tmpvirginurl, '/') . '\/?/', '', $value); $tmpvirginurl3 = preg_replace('/^https:\/\//i', 'https://www.', $tmpvirginurl); if ($tmpvirginurl3) { $value = preg_replace('/^www\.' . preg_quote($tmpvirginurl3, '/') . '\/?/', '', $value); $value = preg_replace('/^' . preg_quote($tmpvirginurl3, '/') . '\/?/', '', $value); } $tmpvirginurl2 = preg_replace('/^https?:\/\//i', '', $tmpvirginurl); if ($tmpvirginurl2) { $value = preg_replace('/^www\.' . preg_quote($tmpvirginurl2, '/') . '\/?/', '', $value); $value = preg_replace('/^' . preg_quote($tmpvirginurl2, '/') . '\/?/', '', $value); } } if (preg_match('/^https?:\/\//i', $value)) { $link = $value; } else { $link = str_replace('{socialid}', $value, $dictsocialnetworks[$type]['url']); } $valuetoshow = $value; $valuetoshow = preg_replace('/https:\/\/www\.(twitter|x|linkedin)\.com\/?/', '', $valuetoshow); if (preg_match('/^https?:\/\//i', $link)) { $htmllink .= '' . dol_escape_htmltag($valuetoshow) . ''; } else { $htmllink .= '' . dol_escape_htmltag($valuetoshow) . ''; } } else { $htmllink .= dol_escape_htmltag($value); } } $htmllink .= '
'; } else { $langs->load("errors"); $htmllink .= img_warning($langs->trans("ErrorBadSocialNetworkValue", $value)); } if ($hookmanager) { $parameters = array( 'value' => $value, 'cid' => $contactid, 'socid' => $socid, 'type' => $type, 'dictsocialnetworks' => $dictsocialnetworks, ); $reshook = $hookmanager->executeHooks('printSocialNetworks', $parameters); if ($reshook > 0) { $htmllink = ''; } $htmllink .= $hookmanager->resPrint; } return $htmllink; } /** * Format professional IDs according to their country * * @param string $profID Value of profID to format * @param string $profIDtype Type of profID to format ('1', '2', '3', '4', '5', '6' or 'VAT') * @param string $countrycode Country code to use for formatting * @param int<0,2> $addcpButton Add button to copy to clipboard (1 => show only on hoover ; 2 => always display ) * @return string Formatted profID */ function dol_print_profids($profID, $profIDtype, $countrycode = '', $addcpButton = 1) { global $mysoc; if (empty($profID) || empty($profIDtype)) { return ''; } if (empty($countrycode)) { $countrycode = (is_object($mysoc) ? $mysoc->country_code : ''); } $newProfID = $profID; $id = substr($profIDtype, -1); $ret = ''; if (strtoupper($countrycode) == 'FR') { // France // (see https://www.economie.gouv.fr/entreprises/numeros-identification-entreprise) if ($id == 1 && dol_strlen($newProfID) == 9) { // SIREN (ex: 123 123 123) $newProfID = substr($newProfID, 0, 3) . ' ' . substr($newProfID, 3, 3) . ' ' . substr($newProfID, 6, 3); } if ($id == 2 && dol_strlen($newProfID) == 14) { // SIRET (ex: 123 123 123 12345) $newProfID = substr($newProfID, 0, 3) . ' ' . substr($newProfID, 3, 3) . ' ' . substr($newProfID, 6, 3) . ' ' . substr($newProfID, 9, 5); } if ($id == 3 && dol_strlen($newProfID) == 5) { // NAF/APE (ex: 69.20Z) $newProfID = substr($newProfID, 0, 2) . '.' . substr($newProfID, 2, 3); } if ($profIDtype === 'VAT' && dol_strlen($newProfID) == 13) { // TVA intracommunautaire (ex: FR12 123 123 123) $newProfID = substr($newProfID, 0, 4) . ' ' . substr($newProfID, 4, 3) . ' ' . substr($newProfID, 7, 3) . ' ' . substr($newProfID, 10, 3); } } if (!empty($addcpButton)) { $ret = showValueWithClipboardCPButton(dol_escape_htmltag($profID), ($addcpButton == 1 ? 1 : 0), $newProfID); } else { $ret = $newProfID; } return $ret; } /** * Format phone numbers according to country * * @param string $phone Phone number to format * @param string $countrycode Country code to use for formatting * @param int $contactid Id of contact if known * @param int $socid Id of third party if known * @param string $addlink ''=no link to create action, 'AC_TEL'=add link to clicktodial (if module enabled) and add link to create event (if conf->global->AGENDA_ADDACTIONFORPHONE set), 'tel'=Force "tel:..." link * @param string $separ Separation between numbers for a better visibility example : xx.xx.xx.xx.xx. You can also use 'hidenum' to hide the number, keep only the picto. * @param string $withpicto Show picto ('fax', 'phone', 'mobile') * @param string $titlealt Text to show on alt * @param int $adddivfloat Add div float around phone. * @param string $morecss Add more css * @return string Formatted phone number */ function dol_print_phone($phone, $countrycode = '', $contactid = 0, $socid = 0, $addlink = '', $separ = " ", $withpicto = '', $titlealt = '', $adddivfloat = 0, $morecss = 'paddingright') { global $conf, $user, $langs, $mysoc, $hookmanager; // Clean phone parameter $phone = is_null($phone) ? '' : preg_replace("/[\s.-]/", "", trim($phone)); if (empty($phone)) { return ''; } if (getDolGlobalString('MAIN_PHONE_SEPAR')) { $separ = getDolGlobalString('MAIN_PHONE_SEPAR'); } if (empty($countrycode) && is_object($mysoc)) { $countrycode = $mysoc->country_code; } // Short format for small screens if (!empty($conf->dol_optimize_smallscreen) && $separ != 'hidenum') { $separ = ''; } $newphone = $phone; $newphonewa = $phone; if (strtoupper($countrycode) == "FR") { // France if (dol_strlen($phone) == 10) { $newphone = substr($newphone, 0, 2) . $separ . substr($newphone, 2, 2) . $separ . substr($newphone, 4, 2) . $separ . substr($newphone, 6, 2) . $separ . substr($newphone, 8, 2); } elseif (dol_strlen($phone) == 7) { $newphone = substr($newphone, 0, 3) . $separ . substr($newphone, 3, 2) . $separ . substr($newphone, 5, 2); } elseif (dol_strlen($phone) == 9) { $newphone = substr($newphone, 0, 2) . $separ . substr($newphone, 2, 3) . $separ . substr($newphone, 5, 2) . $separ . substr($newphone, 7, 2); } elseif (dol_strlen($phone) == 11) { $newphone = substr($newphone, 0, 3) . $separ . substr($newphone, 3, 2) . $separ . substr($newphone, 5, 2) . $separ . substr($newphone, 7, 2) . $separ . substr($newphone, 9, 2); } elseif (dol_strlen($phone) == 12) { $newphone = substr($newphone, 0, 3) . $separ . substr($newphone, 3, 1) . $separ . substr($newphone, 4, 2) . $separ . substr($newphone, 6, 2) . $separ . substr($newphone, 8, 2) . $separ . substr($newphone, 10, 2); } elseif (dol_strlen($phone) == 13) { $newphone = substr($newphone, 0, 4) . $separ . substr($newphone, 4, 2) . $separ . substr($newphone, 6, 2) . $separ . substr($newphone, 8, 3) . $separ . substr($newphone, 11, 2); } } elseif (strtoupper($countrycode) == "CA") { if (dol_strlen($phone) == 10) { $newphone = ($separ != '' ? '(' : '') . substr($newphone, 0, 3) . ($separ != '' ? ')' : '') . $separ . substr($newphone, 3, 3) . ($separ != '' ? '-' : '') . substr($newphone, 6, 4); } } elseif (strtoupper($countrycode) == "PT") { //Portugal if (dol_strlen($phone) == 13) { //ex: +351_ABC_DEF_GHI $newphone = substr($newphone, 0, 4) . $separ . substr($newphone, 4, 3) . $separ . substr($newphone, 7, 3) . $separ . substr($newphone, 10, 3); } } elseif (strtoupper($countrycode) == "SR") { //Suriname if (dol_strlen($phone) == 10) { //ex: +597_ABC_DEF $newphone = substr($newphone, 0, 4) . $separ . substr($newphone, 4, 3) . $separ . substr($newphone, 7, 3); } elseif (dol_strlen($phone) == 11) { //ex: +597_ABC_DEFG $newphone = substr($newphone, 0, 4) . $separ . substr($newphone, 4, 3) . $separ . substr($newphone, 7, 4); } } elseif (strtoupper($countrycode) == "DE") { //Deutschland if (dol_strlen($phone) == 14) { //ex: +49_ABCD_EFGH_IJK $newphone = substr($newphone, 0, 3) . $separ . substr($newphone, 3, 4) . $separ . substr($newphone, 7, 4) . $separ . substr($newphone, 11, 3); } elseif (dol_strlen($phone) == 13) { //ex: +49_ABC_DEFG_HIJ $newphone = substr($newphone, 0, 3) . $separ . substr($newphone, 3, 3) . $separ . substr($newphone, 6, 4) . $separ . substr($newphone, 10, 3); } } elseif (strtoupper($countrycode) == "ES") { //Spain if (dol_strlen($phone) == 12) { //ex: +34_ABC_DEF_GHI $newphone = substr($newphone, 0, 3) . $separ . substr($newphone, 3, 3) . $separ . substr($newphone, 6, 3) . $separ . substr($newphone, 9, 3); } } elseif (strtoupper($countrycode) == "BF") { // Burkina Faso if (dol_strlen($phone) == 12) { //ex : +22 A BC_DE_FG_HI $newphone = substr($newphone, 0, 3) . $separ . substr($newphone, 3, 1) . $separ . substr($newphone, 4, 2) . $separ . substr($newphone, 6, 2) . $separ . substr($newphone, 8, 2) . $separ . substr($newphone, 10, 2); } } elseif (strtoupper($countrycode) == "RO") { // Roumanie if (dol_strlen($phone) == 12) { //ex : +40 AB_CDE_FG_HI $newphone = substr($newphone, 0, 3) . $separ . substr($newphone, 3, 2) . $separ . substr($newphone, 5, 3) . $separ . substr($newphone, 8, 2) . $separ . substr($newphone, 10, 2); } } elseif (strtoupper($countrycode) == "TR") { //Turquie if (dol_strlen($phone) == 13) { //ex : +90 ABC_DEF_GHIJ $newphone = substr($newphone, 0, 3) . $separ . substr($newphone, 3, 3) . $separ . substr($newphone, 6, 3) . $separ . substr($newphone, 9, 4); } } elseif (strtoupper($countrycode) == "US") { //Etat-Unis if (dol_strlen($phone) == 12) { //ex: +1 ABC_DEF_GHIJ $newphone = substr($newphone, 0, 2) . $separ . substr($newphone, 2, 3) . $separ . substr($newphone, 5, 3) . $separ . substr($newphone, 8, 4); } } elseif (strtoupper($countrycode) == "MX") { //Mexique if (dol_strlen($phone) == 12) { //ex: +52 ABCD_EFG_HI $newphone = substr($newphone, 0, 3) . $separ . substr($newphone, 3, 4) . $separ . substr($newphone, 7, 3) . $separ . substr($newphone, 10, 2); } elseif (dol_strlen($phone) == 11) { //ex: +52 AB_CD_EF_GH $newphone = substr($newphone, 0, 3) . $separ . substr($newphone, 3, 2) . $separ . substr($newphone, 5, 2) . $separ . substr($newphone, 7, 2) . $separ . substr($newphone, 9, 2); } elseif (dol_strlen($phone) == 13) { //ex: +52 ABC_DEF_GHIJ $newphone = substr($newphone, 0, 3) . $separ . substr($newphone, 3, 3) . $separ . substr($newphone, 6, 3) . $separ . substr($newphone, 9, 4); } } elseif (strtoupper($countrycode) == "ML") { //Mali if (dol_strlen($phone) == 12) { //ex: +223 AB_CD_EF_GH $newphone = substr($newphone, 0, 4) . $separ . substr($newphone, 4, 2) . $separ . substr($newphone, 6, 2) . $separ . substr($newphone, 8, 2) . $separ . substr($newphone, 10, 2); } } elseif (strtoupper($countrycode) == "TH") { //Thailand if (dol_strlen($phone) == 11) { //ex: +66_ABC_DE_FGH $newphone = substr($newphone, 0, 3) . $separ . substr($newphone, 3, 3) . $separ . substr($newphone, 6, 2) . $separ . substr($newphone, 8, 3); } elseif (dol_strlen($phone) == 12) { //ex: +66_A_BCD_EF_GHI $newphone = substr($newphone, 0, 3) . $separ . substr($newphone, 3, 1) . $separ . substr($newphone, 4, 3) . $separ . substr($newphone, 7, 2) . $separ . substr($newphone, 9, 3); } } elseif (strtoupper($countrycode) == "MU") { //Maurice if (dol_strlen($phone) == 11) { //ex: +230_ABC_DE_FG $newphone = substr($newphone, 0, 4) . $separ . substr($newphone, 4, 3) . $separ . substr($newphone, 7, 2) . $separ . substr($newphone, 9, 2); } elseif (dol_strlen($phone) == 12) { //ex: +230_ABCD_EF_GH $newphone = substr($newphone, 0, 4) . $separ . substr($newphone, 4, 4) . $separ . substr($newphone, 8, 2) . $separ . substr($newphone, 10, 2); } } elseif (strtoupper($countrycode) == "ZA") { //Afrique du sud if (dol_strlen($phone) == 12) { //ex: +27_AB_CDE_FG_HI $newphone = substr($newphone, 0, 3) . $separ . substr($newphone, 3, 2) . $separ . substr($newphone, 5, 3) . $separ . substr($newphone, 8, 2) . $separ . substr($newphone, 10, 2); } } elseif (strtoupper($countrycode) == "SY") { //Syrie if (dol_strlen($phone) == 12) { //ex: +963_AB_CD_EF_GH $newphone = substr($newphone, 0, 4) . $separ . substr($newphone, 4, 2) . $separ . substr($newphone, 6, 2) . $separ . substr($newphone, 8, 2) . $separ . substr($newphone, 10, 2); } elseif (dol_strlen($phone) == 13) { //ex: +963_AB_CD_EF_GHI $newphone = substr($newphone, 0, 4) . $separ . substr($newphone, 4, 2) . $separ . substr($newphone, 6, 2) . $separ . substr($newphone, 8, 2) . $separ . substr($newphone, 10, 3); } } elseif (strtoupper($countrycode) == "AE") { //Emirats Arabes Unis if (dol_strlen($phone) == 12) { //ex: +971_ABC_DEF_GH $newphone = substr($newphone, 0, 4) . $separ . substr($newphone, 4, 3) . $separ . substr($newphone, 7, 3) . $separ . substr($newphone, 10, 2); } elseif (dol_strlen($phone) == 13) { //ex: +971_ABC_DEF_GHI $newphone = substr($newphone, 0, 4) . $separ . substr($newphone, 4, 3) . $separ . substr($newphone, 7, 3) . $separ . substr($newphone, 10, 3); } elseif (dol_strlen($phone) == 14) { //ex: +971_ABC_DEF_GHIK $newphone = substr($newphone, 0, 4) . $separ . substr($newphone, 4, 3) . $separ . substr($newphone, 7, 3) . $separ . substr($newphone, 10, 4); } } elseif (strtoupper($countrycode) == "DZ") { //Algeria if (dol_strlen($phone) == 13) { //ex: +213_ABC_DEF_GHI $newphone = substr($newphone, 0, 4) . $separ . substr($newphone, 4, 3) . $separ . substr($newphone, 7, 3) . $separ . substr($newphone, 10, 3); } } elseif (strtoupper($countrycode) == "BE") { //Belgique if (dol_strlen($phone) == 11) { //ex: +32_ABC_DE_FGH $newphone = substr($newphone, 0, 3) . $separ . substr($newphone, 3, 3) . $separ . substr($newphone, 6, 2) . $separ . substr($newphone, 8, 3); } elseif (dol_strlen($phone) == 12) { //ex: +32_ABC_DEF_GHI $newphone = substr($newphone, 0, 3) . $separ . substr($newphone, 3, 3) . $separ . substr($newphone, 6, 3) . $separ . substr($newphone, 9, 3); } } elseif (strtoupper($countrycode) == "PF") { //French Polynesia if (dol_strlen($phone) == 12) { //ex: +689_AB_CD_EF_GH $newphone = substr($newphone, 0, 4) . $separ . substr($newphone, 4, 2) . $separ . substr($newphone, 6, 2) . $separ . substr($newphone, 8, 2) . $separ . substr($newphone, 10, 2); } } elseif (strtoupper($countrycode) == "CO") { //Colombie if (dol_strlen($phone) == 13) { //ex: +57_ABC_DEF_GH_IJ $newphone = substr($newphone, 0, 3) . $separ . substr($newphone, 3, 3) . $separ . substr($newphone, 6, 3) . $separ . substr($newphone, 9, 2) . $separ . substr($newphone, 11, 2); } } elseif (strtoupper($countrycode) == "JO") { //Jordanie if (dol_strlen($phone) == 12) { //ex: +962_A_BCD_EF_GH $newphone = substr($newphone, 0, 4) . $separ . substr($newphone, 4, 1) . $separ . substr($newphone, 5, 3) . $separ . substr($newphone, 8, 2) . $separ . substr($newphone, 10, 2); } } elseif (strtoupper($countrycode) == "JM") { //Jamaica if (dol_strlen($newphone) == 12) { //ex: +1867_ABC_DEFG $newphone = substr($newphone, 0, 5) . $separ . substr($newphone, 5, 3) . $separ . substr($newphone, 8, 4); } } elseif (strtoupper($countrycode) == "MG") { //Madagascar if (dol_strlen($phone) == 13) { //ex: +261_AB_CD_EFG_HI $newphone = substr($newphone, 0, 4) . $separ . substr($newphone, 4, 2) . $separ . substr($newphone, 6, 2) . $separ . substr($newphone, 8, 3) . $separ . substr($newphone, 11, 2); } } elseif (strtoupper($countrycode) == "GB") { //Royaume uni if (dol_strlen($phone) == 13) { //ex: +44_ABCD_EFG_HIJ $newphone = substr($newphone, 0, 3) . $separ . substr($newphone, 3, 4) . $separ . substr($newphone, 7, 3) . $separ . substr($newphone, 10, 3); } } elseif (strtoupper($countrycode) == "CH") { //Suisse if (dol_strlen($phone) == 12) { //ex: +41_AB_CDE_FG_HI $newphone = substr($newphone, 0, 3) . $separ . substr($newphone, 3, 2) . $separ . substr($newphone, 5, 3) . $separ . substr($newphone, 8, 2) . $separ . substr($newphone, 10, 2); } elseif (dol_strlen($phone) == 15) { // +41_AB_CDE_FGH_IJKL $newphone = $newphone = substr($newphone, 0, 3) . $separ . substr($newphone, 3, 2) . $separ . substr($newphone, 5, 3) . $separ . substr($newphone, 8, 3) . $separ . substr($newphone, 11, 4); } } elseif (strtoupper($countrycode) == "TN") { //Tunisie if (dol_strlen($phone) == 12) { //ex: +216_AB_CDE_FGH $newphone = substr($newphone, 0, 4) . $separ . substr($newphone, 4, 2) . $separ . substr($newphone, 6, 3) . $separ . substr($newphone, 9, 3); } } elseif (strtoupper($countrycode) == "GF") { //Guyane francaise if (dol_strlen($phone) == 13) { //ex: +594_ABC_DE_FG_HI (ABC=594 de nouveau) $newphone = substr($newphone, 0, 4) . $separ . substr($newphone, 4, 3) . $separ . substr($newphone, 7, 2) . $separ . substr($newphone, 9, 2) . $separ . substr($newphone, 11, 2); } } elseif (strtoupper($countrycode) == "GP") { //Guadeloupe if (dol_strlen($phone) == 13) { //ex: +590_ABC_DE_FG_HI (ABC=590 de nouveau) $newphone = substr($newphone, 0, 4) . $separ . substr($newphone, 4, 3) . $separ . substr($newphone, 7, 2) . $separ . substr($newphone, 9, 2) . $separ . substr($newphone, 11, 2); } } elseif (strtoupper($countrycode) == "MQ") { //Martinique if (dol_strlen($phone) == 13) { //ex: +596_ABC_DE_FG_HI (ABC=596 de nouveau) $newphone = substr($newphone, 0, 4) . $separ . substr($newphone, 4, 3) . $separ . substr($newphone, 7, 2) . $separ . substr($newphone, 9, 2) . $separ . substr($newphone, 11, 2); } } elseif (strtoupper($countrycode) == "IT") { //Italie if (dol_strlen($phone) == 12) { //ex: +39_ABC_DEF_GHI $newphone = substr($newphone, 0, 3) . $separ . substr($newphone, 3, 3) . $separ . substr($newphone, 6, 3) . $separ . substr($newphone, 9, 3); } elseif (dol_strlen($phone) == 13) { //ex: +39_ABC_DEF_GH_IJ $newphone = substr($newphone, 0, 3) . $separ . substr($newphone, 3, 3) . $separ . substr($newphone, 6, 3) . $separ . substr($newphone, 9, 2) . $separ . substr($newphone, 11, 2); } } elseif (strtoupper($countrycode) == "AU") { //Australie if (dol_strlen($phone) == 12) { //ex: +61_A_BCDE_FGHI $newphone = substr($newphone, 0, 3) . $separ . substr($newphone, 3, 1) . $separ . substr($newphone, 4, 4) . $separ . substr($newphone, 8, 4); } } elseif (strtoupper($countrycode) == "LU") { // Luxembourg if (dol_strlen($phone) == 10) { // fix 6 digits +352_AA_BB_CC $newphone = substr($newphone, 0, 4) . $separ . substr($newphone, 4, 2) . $separ . substr($newphone, 6, 2) . $separ . substr($newphone, 8, 2); } elseif (dol_strlen($phone) == 11) { // fix 7 digits +352_AA_BB_CC_D $newphone = substr($newphone, 0, 4) . $separ . substr($newphone, 4, 2) . $separ . substr($newphone, 6, 2) . $separ . substr($newphone, 8, 2) . $separ . substr($newphone, 10, 1); } elseif (dol_strlen($phone) == 12) { // fix 8 digits +352_AA_BB_CC_DD $newphone = substr($newphone, 0, 4) . $separ . substr($newphone, 4, 2) . $separ . substr($newphone, 6, 2) . $separ . substr($newphone, 8, 2) . $separ . substr($newphone, 10, 2); } elseif (dol_strlen($phone) == 13) { // mobile +352_AAA_BB_CC_DD $newphone = substr($newphone, 0, 4) . $separ . substr($newphone, 4, 3) . $separ . substr($newphone, 7, 2) . $separ . substr($newphone, 9, 2) . $separ . substr($newphone, 11, 2); } } elseif (strtoupper($countrycode) == "PE") { // Peru if (dol_strlen($phone) == 7) { // fix 7 numbers without code AAA_BBBB $newphone = substr($newphone, 0, 3) . $separ . substr($newphone, 3, 4); } elseif (dol_strlen($phone) == 9) { // mobile add code and fix 9 numbers +51_AAA_BBB_CCC $newphonewa = '+51' . $newphone; $newphone = substr($newphone, 0, 3) . $separ . substr($newphone, 3, 3) . $separ . substr($newphone, 6, 3); } elseif (dol_strlen($phone) == 11) { // fix 11 numbers +511_AAA_BBBB $newphone = substr($newphone, 0, 4) . $separ . substr($newphone, 4, 3) . $separ . substr($newphone, 7, 4); } elseif (dol_strlen($phone) == 12) { // mobile +51_AAA_BBB_CCC $newphonewa = $newphone; $newphone = substr($newphone, 0, 3) . $separ . substr($newphone, 3, 3) . $separ . substr($newphone, 6, 3) . $separ . substr($newphone, 9, 3); } } elseif (strtoupper($countrycode) == "IN") { //India if (dol_strlen($phone) == 13) { if ($withpicto == 'phone') { //ex: +91_AB_CDEF_GHIJ $newphone = substr($newphone, 0, 3) . $separ . substr($newphone, 3, 2) . $separ . substr($newphone, 5, 4) . $separ . substr($newphone, 9, 4); } else { //ex: +91_ABCDE_FGHIJ $newphone = substr($newphone, 0, 3) . $separ . substr($newphone, 3, 5) . $separ . substr($newphone, 8, 5); } } } elseif (strtoupper($countrycode) == "CI") { //Ivory cost if (dol_strlen($phone) == 14) { //ex : +225_AB_CD_EF_GH_IJ $newphone = substr($newphone, 0, 4) . $separ.substr($newphone, 4, 2) . $separ.substr($newphone, 6, 2) . $separ.substr($newphone, 8, 2) . $separ.substr($newphone, 10, 2) . $separ.substr($newphone, 12, 2); } } $newphoneastart = $newphoneaend = ''; if (!empty($addlink)) { // Link on phone number (+ link to add action if conf->global->AGENDA_ADDACTIONFORPHONE set) if ($addlink == 'tel' || $conf->browser->layout == 'phone' || (isModEnabled('clicktodial') && getDolGlobalString('CLICKTODIAL_USE_TEL_LINK_ON_PHONE_NUMBERS'))) { // If phone or option for, we use link of phone $newphoneastart = ''; $newphoneaend .= ''; } elseif (isModEnabled('clicktodial') && $addlink == 'AC_TEL') { // If click to dial, we use click to dial url if (empty($user->clicktodial_loaded)) { $user->fetch_clicktodial(); } // Define urlmask $urlmask = getDolGlobalString('CLICKTODIAL_URL', 'ErrorClickToDialModuleNotConfigured'); if (!empty($user->clicktodial_url)) { $urlmask = $user->clicktodial_url; } $clicktodial_poste = (!empty($user->clicktodial_poste) ? urlencode($user->clicktodial_poste) : ''); $clicktodial_login = (!empty($user->clicktodial_login) ? urlencode($user->clicktodial_login) : ''); $clicktodial_password = (!empty($user->clicktodial_password) ? urlencode($user->clicktodial_password) : ''); // This line is for backward compatibility @phan-suppress-next-line PhanPluginPrintfVariableFormatString $url = sprintf($urlmask, urlencode($phone), $clicktodial_poste, $clicktodial_login, $clicktodial_password); // Those lines are for substitution $substitarray = array( '__PHONEFROM__' => $clicktodial_poste, '__PHONETO__' => urlencode($phone), '__LOGIN__' => $clicktodial_login, '__PASS__' => $clicktodial_password ); $url = make_substitutions($url, $substitarray); if (!getDolGlobalString('CLICKTODIAL_DO_NOT_USE_AJAX_CALL')) { // Default and recommended: New method using ajax without submitting a page making a javascript history.go(-1) back $newphoneastart = ''; // Call of ajax is handled by the lib_foot.js.php on class 'cssforclicktodial' $newphoneaend = ''; } else { // Old method $newphoneastart = 'hasRight('agenda', 'myactions', 'create')) if (isModEnabled('agenda') && $user->hasRight("agenda", "myactions", "create")) { $type = 'AC_TEL'; $addlinktoagenda = ''; if ($addlink == 'AC_FAX') { $type = 'AC_FAX'; } if (getDolGlobalString('AGENDA_ADDACTIONFORPHONE')) { $addlinktoagenda = '' . img_object($langs->trans("AddAction"), "calendar") . ''; } if ($addlinktoagenda) { $newphone = '' . $newphone . ' ' . $addlinktoagenda . ''; } } } if (getDolGlobalString('CONTACT_PHONEMOBILE_SHOW_LINK_TO_WHATSAPP') && $withpicto == 'mobile') { // Link to Whatsapp $newphone .= ' trans("Fax") : $langs->trans("Phone")); } $rep = ''; if ($hookmanager) { $parameters = array('countrycode' => $countrycode, 'cid' => $contactid, 'socid' => $socid, 'titlealt' => $titlealt, 'picto' => $withpicto); $reshook = $hookmanager->executeHooks('printPhone', $parameters, $phone); $rep .= $hookmanager->resPrint; } if (empty($reshook)) { $picto = ''; if ($withpicto) { if ($withpicto == 'fax') { $picto = 'phoning_fax'; } elseif ($withpicto == 'phone') { $picto = 'phone'; } elseif ($withpicto == 'mobile') { $picto = 'phoning_mobile'; } else { $picto = ''; } } if ($adddivfloat == 1) { $rep .= '
'; } elseif (empty($adddivfloat)) { $rep .= ''; } $rep .= $newphoneastart; $rep .= ($withpicto ? img_picto($titlealt, $picto) : ''); if ($separ != 'hidenum') { $rep .= ($withpicto ? ' ' : '') . $newphone; } $rep .= $newphoneaend; if ($adddivfloat == 1) { $rep .= '
'; } elseif (empty($adddivfloat)) { $rep .= ''; } } return $rep; } /** * Return an IP formatted to be shown on screen * * @param string $ip IP * @param int $mode 0=return IP + country/flag, 1=return only country/flag, 2=return only IP * @param int $showname 1=Show reverse domain name instead of IP * @return string Formatted IP, with country (and city, if the GeoIP datafile is a City database) if GeoIP module is enabled */ function dol_print_ip($ip, $mode = 0, $showname = 0) { global $conf; $ret = ''; if (!isset($conf->cache['resolveips'])) { $conf->cache['resolveips'] = array(); } if ($mode != 2) { $countrycode = dolGetCountryCodeFromIp($ip); if ($countrycode) { // If success, countrycode is us, fr, ... if (file_exists(DOL_DOCUMENT_ROOT . '/theme/common/flags/' . $countrycode . '.png')) { $ret .= picto_from_langcode($countrycode); } else { $ret .= '(' . $countrycode . ')'; } $ret .= ' '; } else { // Nothing } $cityname = dolGetCityFromIp($ip); if ($cityname) { // Only set if the GeoIP datafile in use is a City database $ret .= dol_escape_htmltag($cityname) . ' '; } } if (in_array($mode, [0, 2])) { $domain = ''; if ($showname) { if (!array_key_exists($ip, $conf->cache['resolveips'])) { $domain = gethostbyaddr($ip); $conf->cache['resolveips'][$ip] = $domain; // false or domain } else { $domain = $conf->cache['resolveips'][$ip]; } } if ($domain) { $ret .= $domain; } else { $ret .= $ip; } } return $ret; } /** * Return the real IP of remote user. * Take HTTP_X_FORWARDED_FOR (defined when using proxy) * Then HTTP_CLIENT_IP if defined (rare) * Then REMOTE_ADDR (the last seerver ip in proxy chain). REMOTE_ADDR can't be modified by client and may be the IP of a proxy. * Note: if Apache module "remoteip" module is on, Apache may replaced the $_SERVER["REMOTE_ADDR"] by HTTP_X_FORWARDED_FOR directly. For this * Apache is using the CF-Connecting-IP header (HTTP_CF_CONNECTING_IP), but only if the remote IP is in the trusted cloudflare list (Apache must enable module and conf remoteip). * * @param int $trusted 0=Default, 1=Trusted value (the last IP that was not altered by client) * @return string Real IP of remote user. * @see getBrowserInfo() */ function getUserRemoteIP($trusted = 0) { if ($trusted) { // Return only IP we can rely on (not spoofable by the client) $ip = (empty($_SERVER['REMOTE_ADDR']) ? '' : $_SERVER['REMOTE_ADDR']); // value may be the IP of a proxy // Note that if apache module remoteip has been enabled, REMOTE_ADDR can contain the real client (the value from cloudFlare HTTP_CF_CONNECTING_IP for example) // This can happen if the proxy were added in the list of trusted proxy. return $ip; } // Try to guess the real IP of client (but this may not be reliable) if (empty($_SERVER['HTTP_X_FORWARDED_FOR']) || preg_match('/[^0-9\.\:,\[\]\s]/', $_SERVER['HTTP_X_FORWARDED_FOR'])) { if (empty($_SERVER['HTTP_CLIENT_IP']) || preg_match('/[^0-9\.\:,\[\]\s]/', $_SERVER['HTTP_CLIENT_IP'])) { if (empty($_SERVER["HTTP_CF_CONNECTING_IP"])) { $ip = (empty($_SERVER['REMOTE_ADDR']) ? '' : $_SERVER['REMOTE_ADDR']); // value may be the IP of the proxy and not the client } else { $ip = $_SERVER["HTTP_CF_CONNECTING_IP"]; // value here may have been forged by client } } else { $ip = preg_replace('/,.*$/', '', $_SERVER['HTTP_CLIENT_IP']); // value is clean here but may have been forged by proxy } } else { $ip = preg_replace('/,.*$/', '', $_SERVER['HTTP_X_FORWARDED_FOR']); // value is clean here but may have been forged by proxy } return $ip; } /** * Return a country code from IP. Empty string if not found. * * @param string $ip IP * @return string Country code ('us', 'fr', ...) */ function dolGetCountryCodeFromIp($ip) { $countrycode = ''; if (isModEnabled('geoipmaxmind')) { if (getDolGlobalString('GEOIP_VERSION') == 'php') { $datafile = getDolGlobalString('GEOIPMAXMIND_COUNTRY_DATAFILE'); } else { $diroffile = getMultidirOutput(null, 'geoipmaxmind'); $datafile = $diroffile . '/' . getDolGlobalString('GEOIPMAXMIND_COUNTRY_DATAFILE_EMBEDDED'); } //$ip='24.24.24.24'; //$datafile='/usr/share/GeoIP/GeoIP.dat'; Note that this must be downloaded datafile (not same than datafile provided with ubuntu packages) if ($datafile) { try { include_once DOL_DOCUMENT_ROOT . '/core/class/dolgeoip.class.php'; $geoip = new DolGeoIP('country', $datafile); //print 'ip='.$ip.' databaseType='.$geoip->gi->databaseType." GEOIP_CITY_EDITION_REV1=".GEOIP_CITY_EDITION_REV1."\n"; $countrycode = $geoip->getCountryCodeFromIP($ip); } catch (Exception $e) { //print 'Error with GeoIP database: '.$e->getMessage(); } } } return $countrycode; } /** * Return a city name from IP. Empty string if not found or if the configured GeoIP * datafile is a Country-only database (no city record available in that case). * * @param string $ip IP * @return string City name, or '' */ function dolGetCityFromIp($ip) { $cityname = ''; if (isModEnabled('geoipmaxmind')) { if (getDolGlobalString('GEOIP_VERSION') == 'php') { $datafile = getDolGlobalString('GEOIPMAXMIND_COUNTRY_DATAFILE'); } else { $diroffile = getMultidirOutput(null, 'geoipmaxmind'); $datafile = $diroffile . '/' . getDolGlobalString('GEOIPMAXMIND_COUNTRY_DATAFILE_EMBEDDED'); } if ($datafile) { try { include_once DOL_DOCUMENT_ROOT . '/core/class/dolgeoip.class.php'; $geoip = new DolGeoIP('country', $datafile); $cityname = $geoip->getCityNameFromIP($ip); } catch (Exception $e) { //print 'Error with GeoIP database: '.$e->getMessage(); } } } return $cityname; } /** * Return country code for current user. * If software is used inside a local network, detection may fails (we need a public ip) * * @return string Country code (fr, es, it, us, ...) */ function dol_user_country() { //$ret=$user->xxx; $ret = ''; if (isModEnabled('geoipmaxmind')) { $ip = getUserRemoteIP(); $datafile = getDolGlobalString('GEOIPMAXMIND_COUNTRY_DATAFILE'); //$ip='24.24.24.24'; //$datafile='E:\Mes Sites\Web\Admin1\awstats\maxmind\GeoIP.dat'; include_once DOL_DOCUMENT_ROOT . '/core/class/dolgeoip.class.php'; $geoip = new DolGeoIP('country', $datafile); $countrycode = $geoip->getCountryCodeFromIP($ip); $ret = $countrycode; } return $ret; } /** * Format address string * * @param string $address Address string, already formatted with dol_format_address() * @param string $htmlid Html ID (for example 'gmap') * @param string $element 'thirdparty'|'contact'|'member'|'user'|'other' * @param int $id Id of object * @param int $noprint No output. Result is the function return * @param string $charfornl Char to use instead of nl2br. '' means we use a standad nl2br. * @return ?string Nothing if noprint is 0, formatted address if noprint is 1 * @see dol_format_address() */ function dol_print_address($address, $htmlid, $element, $id, $noprint = 0, $charfornl = '') { global $hookmanager; $out = ''; if ($address) { if ($hookmanager) { $parameters = array('element' => $element, 'id' => $id); $reshook = $hookmanager->executeHooks('printAddress', $parameters, $address); $out .= $hookmanager->resPrint; } if (empty($reshook)) { if (empty($charfornl)) { $out .= nl2br((string) $address); } else { $out .= preg_replace('/[\r\n]+/', $charfornl, (string) $address); } // TODO Remove this block, we can add this using the hook now $showgmap = $showomap = 0; if (($element == 'thirdparty' || $element == 'societe') && isModEnabled('google') && getDolGlobalString('GOOGLE_ENABLE_GMAPS')) { $showgmap = 1; } if ($element == 'contact' && isModEnabled('google') && getDolGlobalString('GOOGLE_ENABLE_GMAPS_CONTACTS')) { $showgmap = 1; } if ($element == 'member' && isModEnabled('google') && getDolGlobalString('GOOGLE_ENABLE_GMAPS_MEMBERS')) { $showgmap = 1; } if ($element == 'user' && isModEnabled('google') && getDolGlobalString('GOOGLE_ENABLE_GMAPS_USERS')) { $showgmap = 1; } if (($element == 'thirdparty' || $element == 'societe') && isModEnabled('openstreetmap') && getDolGlobalString('OPENSTREETMAP_ENABLE_MAPS')) { $showomap = 1; } if ($element == 'contact' && isModEnabled('openstreetmap') && getDolGlobalString('OPENSTREETMAP_ENABLE_MAPS_CONTACTS')) { $showomap = 1; } if ($element == 'member' && isModEnabled('openstreetmap') && getDolGlobalString('OPENSTREETMAP_ENABLE_MAPS_MEMBERS')) { $showomap = 1; } if ($element == 'user' && isModEnabled('openstreetmap') && getDolGlobalString('OPENSTREETMAP_ENABLE_MAPS_USERS')) { $showomap = 1; } if ($showgmap) { $url = dol_buildpath('/google/gmaps.php?mode=' . $element . '&id=' . $id, 1); $out .= '
'; } if ($showomap) { $url = dol_buildpath('/openstreetmap/maps.php?mode=' . $element . '&id=' . $id, 1); $out .= ' '; } } } if ($noprint) { return $out; } else { print $out; return null; } } /** * Return true if email syntax is ok. * * @param string $address email (Ex: "toto@example.com". Long form "John Do " will be false) * @param int $acceptsupervisorkey If 1, the special string '__SUPERVISOREMAIL__' is also accepted as valid * @param int $acceptuserkey If 1, the special string '__USER_EMAIL__' is also accepted as valid * @return boolean true if email syntax is OK, false if KO or empty string * @see isValidMXRecord() */ function isValidEmail($address, $acceptsupervisorkey = 0, $acceptuserkey = 0) { if ($acceptsupervisorkey && $address == '__SUPERVISOREMAIL__') { return true; } if ($acceptuserkey && $address == '__USER_EMAIL__') { return true; } if (filter_var($address, FILTER_VALIDATE_EMAIL)) { return true; } return false; } /** * Return if the domain name has a valid MX record. * WARNING: This need function idn_to_ascii, checkdnsrr and getmxrr * * @param string $domain Domain name (Ex: "yahoo.com", "yhaoo.com", "dolibarr.fr") * @return int -1 if error (function not available), 0=Not valid, 1=Valid * @see isValidEmail() * @suppress PhanDeprecatedFunctionInternal Error in Phan plugins incorrectly tags some functions here */ function isValidMXRecord($domain) { if (function_exists('idn_to_ascii') && function_exists('checkdnsrr')) { if (!checkdnsrr(idn_to_ascii($domain), 'MX')) { return 0; } if (function_exists('getmxrr')) { $mxhosts = array(); $weight = array(); getmxrr(idn_to_ascii($domain), $mxhosts, $weight); if (count($mxhosts) > 1) { return 1; } if (count($mxhosts) == 1 && !in_array((string) $mxhosts[0], array('', '.'))) { return 1; } return 0; } } // function idn_to_ascii or checkdnsrr or getmxrr does not exists return -1; } /** * Return true if phone number syntax is ok * TODO Decide what to do with this * * @param string $phone phone (Ex: "0601010101") * @return boolean true if phone syntax is OK, false if KO or empty string */ function isValidPhone($phone) { return true; } /** * Return first letters of a strings. * Example with nbofchar=1: 'ghi' will return 'g' but 'abc def' will return 'ad' * Example with nbofchar=2: 'ghi' will return 'gh' but 'abc def' will return 'abde' * * @param string $s String to truncate * @param int $nbofchar Nb of characters to keep * @return string Return first chars. */ function dolGetFirstLetters($s, $nbofchar = 1) { $ret = ''; $tmparray = explode(' ', $s); foreach ($tmparray as $tmps) { $ret .= dol_substr($tmps, 0, $nbofchar); } return $ret; } /** * Make a strlen call. Works even if mbstring module not enabled * * @param ?string $string String to calculate length * @param string $stringencoding Encoding of string * @param int $binarycount Do a binary count (count number of bytes in string, not number of human characters) * @return int Length of string */ function dol_strlen($string, $stringencoding = 'UTF-8', $binarycount = 0) { if (is_null($string)) { return 0; } if (function_exists('mb_strlen') && empty($binarycount)) { return mb_strlen($string, $stringencoding); } else { return strlen($string); } } /** * Make a substring. Works even if mbstring module is not enabled for better compatibility. * * @param string $string String to scan * @param int $start Start position (0 for first char) * @param int|null $length Length (in nb of characters or nb of bytes depending on trunconbytes param) * @param string $stringencoding Page code used for input string encoding * @param int $trunconbytes 1=Length is max of bytes instead of max of characters * @return string substring */ function dol_substr($string, $start, $length = null, $stringencoding = '', $trunconbytes = 0) { global $langs; if (empty($stringencoding)) { $stringencoding = (empty($langs) ? 'UTF-8' : $langs->charset_output); } $ret = ''; if (empty($trunconbytes)) { if (function_exists('mb_substr')) { $ret = mb_substr($string, $start, $length, $stringencoding); } else { $ret = substr($string, $start, $length); } } else { if (function_exists('mb_strcut')) { $ret = mb_strcut($string, $start, $length, $stringencoding); } else { $ret = substr($string, $start, $length); } } return $ret; } /** * Truncate a string to a particular length adding '...' if string larger than length. * If length = max length+1, we do no truncate to avoid having just 1 char replaced with '...'. * MAIN_DISABLE_TRUNC=1 can disable all truncings * * @param string $string String to truncate * @param int $size Max string size visible (excluding '...'). 0 for no limit. WARNING: Final string size can have 3 more chars (if we added '...', or if size was max+1 so it does not worse to replace with ...) * @param string $trunc Where to trunc: 'right', 'left', 'middle' (size must be a 2 power), 'wrap' * @param string $stringencoding Tell what is source string encoding * @param int $nodot Truncation do not add '...' after truncation. So it's an exact truncation. * @param int $display Trunc is used to display data and can be changed for small screen. TODO Remove this param (must be dealt with CSS) * @return string Truncated string. WARNING: length is never higher than $size if $nodot is set, but can be 3 chars higher otherwise. */ function dol_trunc($string, $size = 40, $trunc = 'right', $stringencoding = 'UTF-8', $nodot = 0, $display = 0) { global $conf; if (empty($size) || getDolGlobalString('MAIN_DISABLE_TRUNC')) { return $string; } if (empty($stringencoding)) { $stringencoding = 'UTF-8'; } // reduce for small screen if (!empty($conf->dol_optimize_smallscreen) && $conf->dol_optimize_smallscreen == 1 && $display == 1) { $size = round($size / 3); } // We go always here if ($trunc == 'right') { $newstring = dol_textishtml($string) ? dol_string_nohtmltag($string, 1) : $string; if (dol_strlen($newstring, $stringencoding) > ($size + ($nodot ? 0 : 1))) { // If nodot is 0 and size is 1 chars more, we don't trunc and don't add '...' return dol_substr($newstring, 0, $size, $stringencoding) . ($nodot ? '' : '…'); } else { //return 'u'.$size.'-'.$newstring.'-'.dol_strlen($newstring,$stringencoding).'-'.$string; return $string; } } elseif ($trunc == 'middle') { $newstring = dol_textishtml($string) ? dol_string_nohtmltag($string, 1) : $string; if (dol_strlen($newstring, $stringencoding) > 2 && dol_strlen($newstring, $stringencoding) > ($size + 1)) { $size1 = (int) round($size / 2); $size2 = (int) round($size / 2); return dol_substr($newstring, 0, $size1, $stringencoding) . '…' . dol_substr($newstring, dol_strlen($newstring, $stringencoding) - $size2, $size2, $stringencoding); } else { return $string; } } elseif ($trunc == 'left') { $newstring = dol_textishtml($string) ? dol_string_nohtmltag($string, 1) : $string; if (dol_strlen($newstring, $stringencoding) > ($size + ($nodot ? 0 : 1))) { // If nodot is 0 and size is 1 chars more, we don't trunc and don't add '...' return '…' . dol_substr($newstring, dol_strlen($newstring, $stringencoding) - $size, $size, $stringencoding); } else { return $string; } } elseif ($trunc == 'wrap') { $newstring = dol_textishtml($string) ? dol_string_nohtmltag($string, 1) : $string; if (dol_strlen($newstring, $stringencoding) > ($size + 1)) { return dol_substr($newstring, 0, $size, $stringencoding) . "\n" . dol_trunc(dol_substr($newstring, $size, dol_strlen($newstring, $stringencoding) - $size, $stringencoding), $size, $trunc); } else { return $string; } } else { return 'BadParam3CallingDolTrunc'; } } /** * Return a string with VAT rate label formatted for view output * Used into pdf and HTML pages * * @param string $rate Rate value to format ('19.6', '19,6', '19.6%', '19,6%', '19.6 (CODEX)', ...) * @param boolean $addpercent Add a percent % sign in output * @param int $info_bits Miscellaneous information on vat (0=Default, 1=French NPR vat) * @param int $usestarfornpr -1=Never show, 0 or 1=Use '*' for NPR vat rates * @param int $html Used for html output * @return string String with formatted amounts ('19,6' or '19,6%' or '8.5% (NPR)' or '8.5% *' or '19,6 (CODEX)') */ function vatrate($rate, $addpercent = false, $info_bits = 0, $usestarfornpr = 0, $html = 0) { $morelabel = ''; if (preg_match('/%/', $rate)) { $rate = str_replace('%', '', $rate); $addpercent = true; } $reg = array(); if (preg_match('/\((.*)\)/', $rate, $reg)) { $morelabel = ' (' . $reg[1] . ')'; $rate = preg_replace('/\s*' . preg_quote($morelabel, '/') . '/', '', $rate); $morelabel = ' ' . ($html ? '' : '') . '(' . $reg[1] . ')' . ($html ? '' : ''); } if (preg_match('/\*/', $rate)) { $rate = str_replace('*', '', $rate); $info_bits |= 1; } // If rate is '9/9/9' we don't change it. If rate is '9.000' we apply price() if (!preg_match('/\//', $rate)) { $ret = price($rate, 0, '', 0, 0) . ($addpercent ? '%' : ''); } else { // TODO Split on / and output with a price2num to have clean numbers without ton of 000. $ret = $rate . ($addpercent ? '%' : ''); } if (($info_bits & 1) && $usestarfornpr >= 0) { $ret .= ' *'; } $ret .= $morelabel; return $ret; } /** * Function to format a value into an amount for visual output * Function used into PDF and HTML pages * * @param int|float|string|null $amount Amount value to format * @param int<0,1> $form Type of formatting: 1=HTML, 0=no formatting (no by default) * @param Translate|string|null $outlangs Object langs for output. '' use default lang. 'none' use international separators. * @param int $trunc 1=Truncate if there is more decimals than MAIN_MAX_DECIMALS_SHOWN (default), 0=Does not truncate. Deprecated because amount are rounded (to unit or total amount accuracy) before being inserted into database or after a computation, so this parameter should be useless. * @param int<-1,max> $rounding MINIMUM number of decimal to show: 0=no change, -1=we use min(getDolGlobalString('MAIN_MAX_DECIMALS_UNIT'), getDolGlobalString('MAIN_MAX_DECIMALS_TOT')) * @param ''|'MU'|'MT'|'MS'|'CU'|'CT'|int<-2,max> $forcerounding MAXIMUM number of decimal to forcerounding decimal: -1=no change, -2=keep non zero part, 'MU' or 'MT' or a numeric to round to MU or MT or to a given number of decimal * @param string $currency_code To add currency symbol (''=add nothing, 'auto'=Use default currency, 'XXX'=add currency symbols for XXX currency) * @return string String with formatted amount * @see price2num() Revert function of price */ function price($amount, $form = 0, $outlangs = '', $trunc = 1, $rounding = -1, $forcerounding = -1, $currency_code = '') { global $langs, $conf; // Clean parameters if (empty($amount)) { $amount = 0; // To have a numeric value if amount not defined or = '' } $amount = (is_numeric($amount) ? $amount : 0); // Check if amount is numeric, for example, an error occurred when amount value = o (letter) instead 0 (number) if ($rounding == -1) { $curcode = $currency_code; if ($curcode === 'auto') { $curcode = $conf->currency; } $max_decimals_unit = getDolGlobalInt('MAIN_MAX_DECIMALS_UNIT'); $max_decimals_tot = getDolGlobalInt('MAIN_MAX_DECIMALS_TOT'); if (getDolGlobalString('MULTICURRENCY_USE_LIMIT_BY_CURRENCY') && !empty($curcode)) { if (getDolGlobalString('MAIN_MAX_DECIMALS_UNIT_'.$curcode) !== '') { $max_decimals_unit = getDolGlobalInt('MAIN_MAX_DECIMALS_UNIT_'.$curcode); } if (getDolGlobalString('MAIN_MAX_DECIMALS_TOT_'.$curcode) !== '') { $max_decimals_tot = getDolGlobalInt('MAIN_MAX_DECIMALS_TOT_'.$curcode); } } $rounding = min($max_decimals_unit, $max_decimals_tot); } $nbdecimal = $rounding; if ($outlangs === 'none') { // Use international separators $dec = '.'; $thousand = ''; } else { // Output separators by default (french) $dec = ','; $thousand = ' '; // If $outlangs not forced, we use use language if (!($outlangs instanceof Translate)) { $outlangs = $langs; } if ($outlangs->transnoentitiesnoconv("SeparatorDecimal") != "SeparatorDecimal") { $dec = $outlangs->transnoentitiesnoconv("SeparatorDecimal"); } if ($outlangs->transnoentitiesnoconv("SeparatorThousand") != "SeparatorThousand") { $thousand = $outlangs->transnoentitiesnoconv("SeparatorThousand"); } if ($thousand == 'None') { $thousand = ''; } elseif ($thousand == 'Space') { $thousand = ' '; } } //print "outlangs=".$outlangs->defaultlang." amount=".$amount." html=".$form." trunc=".$trunc." nbdecimal=".$nbdecimal." dec='".$dec."' thousand='".$thousand."'
"; //print "amount=".$amount."-"; $amount = str_replace(',', '.', $amount); // should be useless //print $amount."-"; $data = explode('.', $amount); $decpart = isset($data[1]) ? $data[1] : ''; $decpart = preg_replace('/0+$/i', '', $decpart); // Remove 0 at end of decimal part //print "decpart=".$decpart."
"; $end = ''; // We increase nbdecimal if there is more decimal than asked (to not loose information) if (dol_strlen($decpart) > $nbdecimal) { $nbdecimal = dol_strlen($decpart); } $curcode = $currency_code; if ($curcode === 'auto') { $curcode = $conf->currency; } $max_decimals_shown = getDolGlobalString('MAIN_MAX_DECIMALS_SHOWN'); if (getDolGlobalString('MULTICURRENCY_USE_LIMIT_BY_CURRENCY') && !empty($curcode) && getDolGlobalString('MAIN_MAX_DECIMALS_SHOWN_'.$curcode) !== '') { $max_decimals_shown = getDolGlobalString('MAIN_MAX_DECIMALS_SHOWN_'.$curcode); } // If nbdecimal is higher than max to show $nbdecimalmaxshown = (int) str_replace('...', '', $max_decimals_shown); if ($trunc && $nbdecimal > $nbdecimalmaxshown) { $nbdecimal = $nbdecimalmaxshown; if (preg_match('/\.\.\./i', $max_decimals_shown)) { // If output is truncated, we show ... $end = '...'; } } // If force rounding if ((string) $forcerounding != '-1' && (string) $forcerounding != '') { $max_decimals_unit = getDolGlobalInt('MAIN_MAX_DECIMALS_UNIT'); $max_decimals_tot = getDolGlobalInt('MAIN_MAX_DECIMALS_TOT'); if (getDolGlobalString('MULTICURRENCY_USE_LIMIT_BY_CURRENCY') && !empty($curcode)) { if (getDolGlobalString('MAIN_MAX_DECIMALS_UNIT_'.$curcode) !== '') { $max_decimals_unit = getDolGlobalInt('MAIN_MAX_DECIMALS_UNIT_'.$curcode); } if (getDolGlobalString('MAIN_MAX_DECIMALS_TOT_'.$curcode) !== '') { $max_decimals_tot = getDolGlobalInt('MAIN_MAX_DECIMALS_TOT_'.$curcode); } } if ($forcerounding === 'MU' || $forcerounding === 'CU') { $nbdecimal = $max_decimals_unit; } elseif ($forcerounding === 'MT' || $forcerounding === 'CT') { $nbdecimal = $max_decimals_tot; } elseif ($forcerounding >= 0) { $nbdecimal = (int) $forcerounding; } } // Format number $output = number_format((float) $amount, $nbdecimal, $dec, $thousand); // Add symbol of currency if requested $cursymbolbefore = $cursymbolafter = ''; if ($currency_code && is_object($outlangs)) { if ($currency_code == 'auto') { $currency_code = $conf->currency; } $listofcurrenciesbefore = array('AUD', 'CAD', 'CNY', 'COP', 'CLP', 'GBP', 'HKD', 'MXN', 'PEN', 'USD', 'CRC', 'ZAR'); $listoflanguagesbefore = array('nl_NL'); if (in_array($currency_code, $listofcurrenciesbefore) || in_array($outlangs->defaultlang, $listoflanguagesbefore)) { $cursymbolbefore .= $outlangs->getCurrencySymbol($currency_code); } else { $tmpcur = $outlangs->getCurrencySymbol($currency_code); $cursymbolafter .= ($tmpcur == $currency_code ? ' ' . $tmpcur : $tmpcur); } } if ($form) { $output = preg_replace('/\s/', ' ', $output); $output = $cursymbolbefore . $output . $end . ($cursymbolafter ? ' '.$cursymbolafter.'' : ''); $output = preg_replace('/\'/', ''', $output); } else { $output = $cursymbolbefore . $output . $end . ($cursymbolafter ? ' '.$cursymbolafter : ''); } return $output; } /** * Function that return a number with universal decimal format (decimal separator is '.') from an amount typed by a user. * Function to use on each input amount before any numeric test or database insert. A better name for this function * should be roundtext2num(). * * @param int|float|string|null $amount Amount to convert/clean or round * @param ''|'MU'|'MT'|'MS'|'CU'|'CT'|'CR'|int<0,max> $rounding ''=No rounding * 'MU'=Round to Max unit price (MAIN_MAX_DECIMALS_UNIT) * 'MT'=Round to Max for totals with Tax (MAIN_MAX_DECIMALS_TOT) * 'MS'=Round to Max for stock quantity (MAIN_MAX_DECIMALS_STOCK) * 'CU'=Round to Max unit price of foreign currency accuracy * 'CT'=Round to Max for totals with Tax of foreign currency accuracy * 'CR'=Round to Max accuracy for currency exchange rate (MAIN_MAX_DECIMALS_CURRENCY_RATE) * Numeric = Nb of digits for rounding (For example 2 for a percentage) * @param int<0,2> $option Put 1 if you know that content is already universal format number (so no correction on decimal will be done) * Put 2 if you know that number is a user input (so we know we have to fix decimal separator). * Default 0 if unknown. * @param string $currencycode Currency code * @return string Amount with universal numeric format (Example: '99.99999'), or error message. * If conversion fails to return a numeric, it returns: * - text unchanged or partial if ($rounding = ''): price2num('W9c', '', 0) => '9c', price2num('W9c', '', 1) => 'W9c', price2num('W9c', '', 2) => '9c' * - '0' if ($rounding is defined): price2num('W9c', 'MT', 0) => '9', price2num('W9c', 'MT', 1) => '0', price2num('W9c', 'MT', 2) => '9' * Note: The best way to guarantee a numeric value is to add a cast (float) before the price2num(). * If amount is null or '', it returns '' if $rounding = '', it returns '0' if $rounding is defined. * * @see price() Opposite function of price2num */ function price2num($amount, $rounding = '', $option = 0, $currencycode = '') { global $langs; // Clean parameters if (is_null($amount)) { $amount = ''; } // Round PHP function does not allow number like '1,234.56' nor '1.234,56' nor '1 234,56' // Numbers must be '1234.56' // Decimal delimiter for PHP and database SQL requests must be '.' $dec = ','; $thousand = ' '; if (is_null($langs)) { // $langs is not defined, we use english values. $dec = '.'; $thousand = ','; } else { if ($langs->transnoentitiesnoconv("SeparatorDecimal") != "SeparatorDecimal") { $dec = $langs->transnoentitiesnoconv("SeparatorDecimal"); } if ($langs->transnoentitiesnoconv("SeparatorThousand") != "SeparatorThousand") { $thousand = $langs->transnoentitiesnoconv("SeparatorThousand"); } } if ($thousand == 'None') { $thousand = ''; } elseif ($thousand == 'Space') { $thousand = ' '; } //print "amount=".$amount." html=".$form." trunc=".$trunc." nbdecimal=".$nbdecimal." dec='".$dec."' thousand='".$thousand."'
"; // Convert value to universal number format (no thousand separator, '.' as decimal separator) if ($option != 1) { // If not a PHP number or unknown, we change or clean format //print "\n".'PP'.$amount.' - '.$dec.' - '.$thousand.' - '.intval($amount).'
'; if (!is_numeric($amount)) { $amount = preg_replace('/[a-zA-Z\/\\\*\(\)\<\>\_]/', '', $amount); } if ($option == 2 && $thousand == '.' && preg_match('/\.(\d\d\d)$/', (string) $amount)) { // It means the . is used as a thousand separator and string come from input data, so 1.123 is 1123 $amount = str_replace($thousand, '', $amount); } // Convert amount to format with dolibarr dec and thousand (this is because PHP convert a number // to format defined by LC_NUMERIC after a calculation and we want source format to be like defined by Dolibarr setup. // So if number was already a good number, it is converted into local Dolibarr setup. if (is_numeric($amount)) { // We put in temps value of decimal ("0.00001"). Works with 0 and 2.0E-5 and 9999.10 $temps = sprintf("%10.10F", $amount - intval($amount)); // temps=0.0000000000 or 0.0000200000 or 9999.1000000000 $temps = preg_replace('/([\.1-9])0+$/', '\\1', $temps); // temps=0. or 0.00002 or 9999.1 $nbofdec = max(0, dol_strlen($temps) - 2); // -2 to remove "0." $amount = number_format($amount, $nbofdec, $dec, $thousand); } //print "QQ".$amount."
\n"; // Now make replaceents (the main goal of function) if ($thousand != ',' && $thousand != '.') { // Accept the two types of decimal points french users (i.e., using ' ' for thousands) // REGEX: Find the integral and decimal parts. // // We require that the decimal point only appears once in $amount. // The regex `/^(?[^,]*,|[^.]*\.)(?[^.,]*)$/u` can be broken down as follows: // - `(?[^,]*,|[^.]*\.)` is any accepted sequence up to the last potential decimal point '.' or ',' and named `int`. // It covers two cases: // - `[^,]*,`: Any sequence of characters that is not ',' with ',' accepted as the decimal point (from start of string because of earlier `^`); // - `[^.]*\.`: Any sequence of characters that is not a '.' with '.' accepted as the decimal point (from start of string. // - `(?[^.,]*)`: The sequence after the character accepted as the decimal point, not including it. $matches = array(); if (preg_match('/^(?[^,]*,|[^.]*\.)(?[^.,]*)$/u', $amount, $matches)) { $intPart = $matches['int']; $decPart = $matches['dec']; // Remove all commas and dots from intPart $intPart = str_replace(['.', ','], '', $intPart); // Combine intPart and decPart with a dot $amount = $intPart . $dec . $decPart; } } $amount = str_replace(' ', '', $amount); // To avoid spaces $amount = str_replace($thousand, '', $amount); // Replace of thousand before replace of dec to avoid pb if thousand is . $amount = str_replace($dec, '.', $amount); $amount = preg_replace('/[^0-9\-\.]/', '', $amount); // Clean non numeric chars (so it clean some UTF8 spaces for example. } //print ' XX'.$amount.' '.$rounding; // Now, $amount is a real PHP float number. We make a rounding if required. if ($rounding) { $nbofdectoround = ''; if ($rounding == 'MU') { if (getDolGlobalString('MULTICURRENCY_USE_LIMIT_BY_CURRENCY') && !empty($currencycode) && getDolGlobalString('MAIN_MAX_DECIMALS_UNIT_'.$currencycode) !== '') { $nbofdectoround = getDolGlobalInt('MAIN_MAX_DECIMALS_UNIT_'.$currencycode); } else { $nbofdectoround = getDolGlobalInt('MAIN_MAX_DECIMALS_UNIT'); // usually 5 } } elseif ($rounding == 'MT') { if (getDolGlobalString('MULTICURRENCY_USE_LIMIT_BY_CURRENCY') && !empty($currencycode) && getDolGlobalString('MAIN_MAX_DECIMALS_TOT_'.$currencycode) !== '') { $nbofdectoround = getDolGlobalInt('MAIN_MAX_DECIMALS_TOT_'.$currencycode); } else { $nbofdectoround = getDolGlobalInt('MAIN_MAX_DECIMALS_TOT'); // usually 2 or 3 } } elseif ($rounding == 'MS') { $nbofdectoround = getDolGlobalInt('MAIN_MAX_DECIMALS_STOCK', 5); } elseif ($rounding == 'CU' || preg_match('/^CU_([a-zA-Z]+)$/', (string) $rounding, $regscu)) { $cur = !empty($regscu[1]) ? $regscu[1] : $currencycode; if (getDolGlobalString('MULTICURRENCY_USE_LIMIT_BY_CURRENCY') && !empty($cur) && getDolGlobalString('MAIN_MAX_DECIMALS_UNIT_'.$cur) !== '') { $nbofdectoround = getDolGlobalInt('MAIN_MAX_DECIMALS_UNIT_'.$cur); } else { $nbofdectoround = getDolGlobalInt('MAIN_MAX_DECIMALS_CURRENCY_UNIT', getDolGlobalInt('MAIN_MAX_DECIMALS_UNIT')); } } elseif ($rounding == 'CT' || preg_match('/^CT_([a-zA-Z]+)$/', (string) $rounding, $regsct)) { $cur = !empty($regsct[1]) ? $regsct[1] : $currencycode; if (getDolGlobalString('MULTICURRENCY_USE_LIMIT_BY_CURRENCY') && !empty($cur) && getDolGlobalString('MAIN_MAX_DECIMALS_TOT_'.$cur) !== '') { $nbofdectoround = getDolGlobalInt('MAIN_MAX_DECIMALS_TOT_'.$cur); } else { $nbofdectoround = getDolGlobalInt('MAIN_MAX_DECIMALS_CURRENCY_TOT', getDolGlobalInt('MAIN_MAX_DECIMALS_TOT')); } } elseif ($rounding == 'CR' || preg_match('/^CR_([a-zA-Z]+)$/', (string) $rounding, $regscr)) { $cur = !empty($regscr[1]) ? $regscr[1] : $currencycode; if (empty($cur) && (string) getDolGlobalString('MULTICURRENCY_USE_LIMIT_BY_CURRENCY')) { $cur = getDolCurrency(); } $rounding_const = 'MAIN_MAX_DECIMALS_CURRENCY_RATE' . (!empty($cur) ? '_' . $cur : ''); $nbofdectoround = getDolGlobalInt($rounding_const, getDolGlobalInt('MAIN_MAX_DECIMALS_CURRENCY_RATE', 8)); } elseif (is_numeric($rounding)) { $nbofdectoround = (int) $rounding; } //print " RR".$amount.' - '.$nbofdectoround.'
'; if (dol_strlen($nbofdectoround)) { $amount = round(is_string($amount) ? (float) $amount : $amount, $nbofdectoround); // $nbofdectoround can be 0. } else { return 'ErrorBadParameterProvidedToFunction'; } //print ' SS'.$amount.' - '.$nbofdec.' - '.$dec.' - '.$thousand.' - '.$nbofdectoround.'
'; // Convert amount to format with dolibarr dec and thousand (this is because PHP convert a number // to format defined by LC_NUMERIC after a calculation and we want source format to be defined by Dolibarr setup. if (is_numeric($amount)) { // We put in temps value of decimal ("0.00001"). Works with 0 and 2.0E-5 and 9999.10 $temps = sprintf("%10.10F", $amount - intval($amount)); // temps=0.0000000000 or 0.0000200000 or 9999.1000000000 $temps = preg_replace('/([\.1-9])0+$/', '\\1', $temps); // temps=0. or 0.00002 or 9999.1 $nbofdec = max(0, dol_strlen($temps) - 2); // -2 to remove "0." $amount = number_format($amount, min($nbofdec, $nbofdectoround), $dec, $thousand); // Convert amount to format with dolibarr dec and thousand } //print "TT".$amount.'
'; // Always make replace because each math function (like round) replace // with local values and we want a number that has a SQL string format x.y if ($thousand != ',' && $thousand != '.') { $amount = str_replace(',', '.', $amount); // To accept 2 notations for french users } $amount = str_replace(' ', '', $amount); // To avoid spaces $amount = str_replace($thousand, '', $amount); // Replace of thousand before replace of dec to avoid pb if thousand is . $amount = str_replace($dec, '.', $amount); $amount = preg_replace('/[^0-9\-\.]/', '', $amount); // Clean non numeric chars (so it clean some UTF8 spaces for example. } return $amount; } /** * Return localtax rate for a particular VAT rate, when selling a product with vat $vatrate, from a $thirdparty_buyer to a $thirdparty_seller * Note: This function get information into the table llx_tva using the VAT rate as key. * * @param float|string $vatrate Vat rate. Can be '8.5' or '8.5 (VATCODEX)' for example * @param int $local Local tax to search and return (1 or 2 return only tax rate 1 or tax rate 2) * @param ?Societe $thirdparty_buyer Object of buying third party * @param ?Societe $thirdparty_seller Object of selling third party ($mysoc if not defined) * @param int<0,1> $vatnpr If vat rate is NPR or not * @return int<0,0>|string 0 if not found, localtax rate if found (Can be '20', '-19:-15:-9') * @see get_default_tva(), get_default_localtax() */ function get_localtax($vatrate, $local, $thirdparty_buyer = null, $thirdparty_seller = null, $vatnpr = 0) { global $db, $conf, $mysoc; if (empty($thirdparty_seller) || !is_object($thirdparty_seller)) { $thirdparty_seller = $mysoc; } // $mysoc may be null when called from a page with NOREQUIRESOC (like viewimage.php). With no seller, there is no localtax. if (!is_object($thirdparty_seller)) { return 0; } dol_syslog( "get_localtax tva=" . $vatrate . " local=" . $local ." thirdparty_buyer id=" . (is_object($thirdparty_buyer) ? $thirdparty_buyer->id : '') . "/country_code=" . (is_object($thirdparty_buyer) ? $thirdparty_buyer->country_code : '') ." thirdparty_seller id=" . $thirdparty_seller->id . "/country_code=" . $thirdparty_seller->country_code ." thirdparty_seller localtax1_assuj=" . $thirdparty_seller->localtax1_assuj . " thirdparty_seller localtax2_assuj=" . $thirdparty_seller->localtax2_assuj ); $vatratecleaned = $vatrate; $reg = array(); if (preg_match('/^(.*)\s*\((.*)\)$/', (string) $vatrate, $reg)) { // If vat is "xx (yy)" $vatratecleaned = trim($reg[1]); $vatratecode = $reg[2]; } /*if ($thirdparty_buyer->country_code != $thirdparty_seller->country_code) { return 0; }*/ // Some test to guess with no need to make database access if ($mysoc->country_code == 'ES') { // For spain localtaxes 1 and 2, tax is qualified if buyer use local tax if ($local == 1) { if (!$mysoc->localtax1_assuj || (string) $vatratecleaned == "0") { return 0; } if ($thirdparty_seller->id == $mysoc->id) { if (!$thirdparty_buyer->localtax1_assuj) { return 0; } } else { if (!$thirdparty_seller->localtax1_assuj) { return 0; } } } if ($local == 2) { //if (! $mysoc->localtax2_assuj || (string) $vatratecleaned == "0") return 0; if (!$mysoc->localtax2_assuj) { return 0; // If main vat is 0, IRPF may be different than 0. } if ($thirdparty_seller->id == $mysoc->id) { if (!$thirdparty_buyer->localtax2_assuj) { return 0; } } else { if (!$thirdparty_seller->localtax2_assuj) { return 0; } } } } else { if ($local == 1 && !$thirdparty_seller->localtax1_assuj) { return 0; } if ($local == 2 && !$thirdparty_seller->localtax2_assuj) { return 0; } } // For some country MAIN_GET_LOCALTAXES_VALUES_FROM_THIRDPARTY is forced to on. if (in_array($mysoc->country_code, array('ES'))) { $conf->global->MAIN_GET_LOCALTAXES_VALUES_FROM_THIRDPARTY = 1; } // Search local taxes if (getDolGlobalString('MAIN_GET_LOCALTAXES_VALUES_FROM_THIRDPARTY')) { if ($local == 1) { if ($thirdparty_seller != $mysoc) { if (!isOnlyOneLocalTax($local)) { // TODO We should provide $vatrate to search on correct line and not always on line with highest vat rate return $thirdparty_seller->localtax1_value; } } else { // i am the seller if (!isOnlyOneLocalTax($local)) { // TODO If seller is me, why not always returning this, even if there is only one locatax vat. return getDolGlobalString('MAIN_INFO_VALUE_LOCALTAX1'); } } } if ($local == 2) { if ($thirdparty_seller != $mysoc) { if (!isOnlyOneLocalTax($local)) { // TODO We should provide $vatrate to search on correct line and not always on line with highest vat rate // TODO We should also return value defined on thirdparty only if defined return $thirdparty_seller->localtax2_value; } } else { // i am the seller if (in_array($mysoc->country_code, array('ES'))) { return $thirdparty_buyer->localtax2_value; } else { return getDolGlobalString('MAIN_INFO_VALUE_LOCALTAX2'); } } } } // By default, search value of local tax on line of common tax $sql = "SELECT t.localtax1, t.localtax2, t.localtax1_type, t.localtax2_type"; $sql .= " FROM " . MAIN_DB_PREFIX . "c_tva as t, " . MAIN_DB_PREFIX . "c_country as c"; $sql .= " WHERE t.fk_pays = c.rowid AND c.code = '" . $db->escape($thirdparty_seller->country_code) . "'"; $sql .= " AND t.taux = " . ((float) $vatratecleaned) . " AND t.active = 1"; $sql .= " AND t.entity IN (" . getEntity('c_tva') . ")"; if (!empty($vatratecode)) { $sql .= " AND t.code ='" . $db->escape($vatratecode) . "'"; // If we have the code, we use it in priority } else { $sql .= " AND t.recuperableonly = '" . $db->escape((string) $vatnpr) . "'"; } $resql = $db->query($sql); if ($resql) { $obj = $db->fetch_object($resql); if ($obj) { if ($local == 1) { return $obj->localtax1; } elseif ($local == 2) { return $obj->localtax2; } } } return 0; } /** * Return true if LocalTax (1 or 2) is unique. * Example: If localtax1 is 5 on line with highest common vat rate, return true * Example: If localtax1 is 5:8:15 on line with highest common vat rate, return false * * @param int $local Local tax to test (1 or 2) * @return boolean True if LocalTax have multiple values, False if not */ function isOnlyOneLocalTax($local) { $tax = get_localtax_by_third($local); $valors = explode(":", $tax); if (count($valors) > 1) { return false; } else { return true; } } /** * Get values of localtaxes (1 or 2) for company country for the common vat with the highest value * * @param int<1,2> $local LocalTax to get * @return string Values of localtax (Can be '20', '-19:-15:-9') or 'Error' */ function get_localtax_by_third($local) { global $db, $mysoc; $sql = " SELECT t.localtax" . ((int) $local) . " as localtax"; $sql .= " FROM " . MAIN_DB_PREFIX . "c_tva as t INNER JOIN " . MAIN_DB_PREFIX . "c_country as c ON c.rowid = t.fk_pays"; $sql .= " WHERE c.code = '" . $db->escape($mysoc->country_code) . "' AND t.active = 1 AND t.entity IN (" . getEntity('c_tva') . ") AND t.taux = ("; $sql .= "SELECT MAX(tt.taux) FROM " . MAIN_DB_PREFIX . "c_tva as tt INNER JOIN " . MAIN_DB_PREFIX . "c_country as c ON c.rowid = tt.fk_pays"; $sql .= " WHERE c.code = '" . $db->escape($mysoc->country_code) . "' AND t.entity IN (" . getEntity('c_tva') . ") AND tt.active = 1)"; $sql .= " AND t.localtax" . ((int) $local) . "_type <> '0'"; $sql .= " ORDER BY t.rowid DESC"; $resql = $db->query($sql); if ($resql) { $obj = $db->fetch_object($resql); if ($obj) { return $obj->localtax; } else { return '0'; } } return 'Error'; } /** * Get tax (VAT) main information from Id. * You can also call getLocalTaxesFromRate() after to get only localtax fields. * * @param float|string $vatrate VAT ID or Rate. Value can be value or the string with code into parenthesis or rowid if $firstparamisid is 1. Example: '8.5' or '8.5 (8.5NPR)' or 123. * @param ?Societe $buyer Company object * @param ?Societe $seller Company object * @param int<0,1> $firstparamisid 1 if first param is id into table (use this if you can) * @return array{}|array{rowid:int,code:string,rate:float,localtax1:float,localtax1_type:string,localtax2:float,localtax2_type:string,npr:float,accountancy_code_sell:string,accountancy_code_buy:string} array('rowid'=> , 'code'=> ...) * @see getLocalTaxesFromRate() */ function getTaxesFromId($vatrate, $buyer = null, $seller = null, $firstparamisid = 1) { global $db; dol_syslog("getTaxesFromId vat id or rate = " . $vatrate); // Search local taxes $sql = "SELECT t.rowid, t.code, t.taux as rate, t.recuperableonly as npr, t.accountancy_code_sell, t.accountancy_code_buy,"; $sql .= " t.localtax1, t.localtax1_type, t.localtax2, t.localtax2_type"; $sql .= " FROM " . MAIN_DB_PREFIX . "c_tva as t"; if ($firstparamisid) { $sql .= " WHERE t.rowid = " . (int) $vatrate; } else { $vatratecleaned = $vatrate; $vatratecode = ''; $reg = array(); if (preg_match('/^(.*)\s*\((.*)\)$/', $vatrate, $reg)) { // If vat is "xx (yy)" $vatratecleaned = $reg[1]; $vatratecode = $reg[2]; } $sql .= ", " . MAIN_DB_PREFIX . "c_country as c"; /*if ($mysoc->country_code == 'ES') $sql.= " WHERE t.fk_pays = c.rowid AND c.code = '".$db->escape($buyer->country_code)."'"; // vat in spain use the buyer country ?? else $sql.= " WHERE t.fk_pays = c.rowid AND c.code = '".$db->escape($seller->country_code)."'";*/ $sql .= " WHERE t.fk_pays = c.rowid"; if (getDolGlobalString('SERVICE_ARE_ECOMMERCE_200238EC')) { $sql .= " AND c.code = '" . $db->escape($buyer->country_code) . "'"; } else { $sql .= " AND c.code = '" . $db->escape($seller->country_code) . "'"; } $sql .= " AND t.taux = " . ((float) $vatratecleaned) . " AND t.active = 1"; $sql .= " AND t.entity IN (" . getEntity('c_tva') . ")"; if ($vatratecode) { $sql .= " AND t.code = '" . $db->escape($vatratecode) . "'"; } } $resql = $db->query($sql); if ($resql) { $obj = $db->fetch_object($resql); if ($obj) { return array( 'rowid' => $obj->rowid, 'code' => $obj->code, 'rate' => $obj->rate, 'localtax1' => $obj->localtax1, 'localtax1_type' => $obj->localtax1_type, 'localtax2' => $obj->localtax2, 'localtax2_type' => $obj->localtax2_type, 'npr' => $obj->npr, 'accountancy_code_sell' => $obj->accountancy_code_sell, 'accountancy_code_buy' => $obj->accountancy_code_buy ); } else { return array(); } } else { dol_print_error($db); } return array(); } /** * Get type and rate of localtaxes for a particular vat rate/country of a thirdparty. * This does not take into account the seller setup if subject to vat or not, only country. * * TODO This function is ALSO called to retrieve type for building PDF. Such call of function must be removed. * Instead this function must be called when adding a line to get the array of possible values for localtax and type, and then * provide the selected value to the function calcul_price_total. * * @param float|string $vatrate VAT ID or Rate+Code. Value can be value or the string with code into parenthesis or rowid if $firstparamisid is 1. Example: '8.5' or '8.5 (8.5NPR)' or 123. * @param int<0,2> $local Number of localtax (1 or 2, or 0 to return 1 & 2) * @param ?Societe $buyer Company object * @param ?Societe $seller Company object * @param int<0,1> $firstparamisid 1 if first param is ID into table instead of Rate+code (use this if you can) * @return array{}|array{0:string,1:int|string,2:string,3:string}|array{0:string,1:int|string,2:string,3:int|string,4:string,5:string} array(localtax_type1('1-6' or '0' if not found), rate localtax1, localtax_type2, rate localtax2, accountancycodecust, accountancycodesupp) * @see getTaxesFromId() */ function getLocalTaxesFromRate($vatrate, $local, $buyer, $seller, $firstparamisid = 0) { global $db, $mysoc; dol_syslog("getLocalTaxesFromRate vatrate=" . $vatrate . " local=" . $local); // Search local taxes $sql = "SELECT t.taux as rate, t.code, t.localtax1, t.localtax1_type, t.localtax2, t.localtax2_type, t.accountancy_code_sell, t.accountancy_code_buy"; $sql .= " FROM " . MAIN_DB_PREFIX . "c_tva as t"; if ($firstparamisid) { $sql .= " WHERE t.rowid = " . (int) $vatrate; } else { $vatratecleaned = $vatrate; $vatratecode = ''; $reg = array(); if (preg_match('/^(.*)\s*\((.*)\)$/', $vatrate, $reg)) { // If vat is "x.x (yy)" $vatratecleaned = $reg[1]; $vatratecode = $reg[2]; } $sql .= ", " . MAIN_DB_PREFIX . "c_country as c"; if (!empty($mysoc) && $mysoc->country_code == 'ES') { $countrycodetouse = ((empty($buyer) || empty($buyer->country_code)) ? $mysoc->country_code : $buyer->country_code); $sql .= " WHERE t.fk_pays = c.rowid AND c.code = '" . $db->escape($countrycodetouse) . "'"; // local tax in spain use the buyer country ?? } else { $countrycodetouse = ((empty($seller) || empty($seller->country_code)) ? $mysoc->country_code : $seller->country_code); $sql .= " WHERE t.fk_pays = c.rowid AND c.code = '" . $db->escape($countrycodetouse) . "'"; } $sql .= " AND t.taux = " . ((float) $vatratecleaned) . " AND t.active = 1"; if ($vatratecode) { $sql .= " AND t.code = '" . $db->escape($vatratecode) . "'"; } } $resql = $db->query($sql); if ($resql) { $obj = $db->fetch_object($resql); if ($obj) { $vateratestring = $obj->rate . ($obj->code ? ' (' . $obj->code . ')' : ''); if ($local == 1) { return array($obj->localtax1_type, get_localtax($vateratestring, $local, $buyer, $seller), $obj->accountancy_code_sell, $obj->accountancy_code_buy); } elseif ($local == 2) { return array($obj->localtax2_type, get_localtax($vateratestring, $local, $buyer, $seller), $obj->accountancy_code_sell, $obj->accountancy_code_buy); } else { return array($obj->localtax1_type, get_localtax($vateratestring, 1, $buyer, $seller), $obj->localtax2_type, get_localtax($vateratestring, 2, $buyer, $seller), $obj->accountancy_code_sell, $obj->accountancy_code_buy); } } } return array(); } /** * Return vat rate of a product in a particular country, or default country vat if product is unknown. * Function called by get_default_tva(). Do not use this function directly, prefer to use get_default_tva(). * * @param int $idprod Id of product or 0 if not a predefined product * @param Societe $thirdpartytouseforcountry Thirdparty with a ->country_code defined (FR, US, IT, ...) * @param int $idprodfournprice Id product_fournisseur_price (for "supplier" proposal/order/invoice) * @return float|string Vat rate to use with format 5.0 or '5.0 (XXX)' * @see get_default_tva(), get_product_localtax_for_country() */ function get_product_vat_for_country($idprod, $thirdpartytouseforcountry, $idprodfournprice = 0) { global $db, $mysoc; require_once DOL_DOCUMENT_ROOT . '/product/class/product.class.php'; $ret = 0; $found = 0; if ($idprod > 0) { // Load product $product = new Product($db); $product->fetch($idprod); if (($mysoc->country_code == $thirdpartytouseforcountry->country_code) || (in_array($mysoc->country_code, array('FR', 'MC')) && in_array($thirdpartytouseforcountry->country_code, array('FR', 'MC'))) || (in_array($mysoc->country_code, array('MQ', 'GP')) && in_array($thirdpartytouseforcountry->country_code, array('MQ', 'GP'))) ) { // If country of thirdparty to consider is ours if ($idprodfournprice > 0) { // We want vat for product for a "supplier" object $result = $product->get_buyprice($idprodfournprice, 0, 0, ''); if ($result > 0) { $ret = $product->vatrate_supplier; if ($product->default_vat_code_supplier) { $ret .= ' (' . $product->default_vat_code_supplier . ')'; } $found = 1; } } if (!$found) { $ret = $product->tva_tx; // Default sales vat of product if ($product->default_vat_code) { $ret .= ' (' . $product->default_vat_code . ')'; } $found = 1; } } else { // TODO Read default product vat according to product and an other countrycode. // Vat for couple anothercountrycode/product is data that is not managed and store yet, so we will fallback on next rule. } } if (!$found) { if (!getDolGlobalString('MAIN_VAT_DEFAULT_IF_AUTODETECT_FAILS')) { // If vat of product for the country not found or not defined, we return the first rate found (sorting on use_default, then on higher vat of country). $sql = "SELECT t.taux as vat_rate, t.code as default_vat_code"; $sql .= " FROM " . MAIN_DB_PREFIX . "c_tva as t, " . MAIN_DB_PREFIX . "c_country as c"; $sql .= " WHERE t.active = 1 AND t.fk_pays = c.rowid AND c.code = '" . $db->escape($thirdpartytouseforcountry->country_code) . "'"; $sql .= " AND t.entity IN (" . getEntity('c_tva') . ")"; $sql .= " ORDER BY t.use_default DESC, t.taux DESC, t.code ASC, t.recuperableonly ASC"; $sql .= $db->plimit(1); $resql = $db->query($sql); if ($resql) { $obj = $db->fetch_object($resql); if ($obj) { $ret = $obj->vat_rate; if ($obj->default_vat_code) { $ret .= ' (' . $obj->default_vat_code . ')'; } } $db->free($resql); } else { dol_print_error($db); } } else { // Forced value if autodetect fails. MAIN_VAT_DEFAULT_IF_AUTODETECT_FAILS can be // '1.23' // or '1.23 (CODE)' $defaulttx = ''; if (getDolGlobalString('MAIN_VAT_DEFAULT_IF_AUTODETECT_FAILS') != 'none') { $defaulttx = getDolGlobalString('MAIN_VAT_DEFAULT_IF_AUTODETECT_FAILS'); } /*if (preg_match('/\((.*)\)/', $defaulttx, $reg)) { $defaultcode = $reg[1]; $defaulttx = preg_replace('/\s*\(.*\)/', '', $defaulttx); }*/ $ret = $defaulttx; } } dol_syslog("get_product_vat_for_country: ret=" . $ret); return $ret; } /** * Return localtax vat rate of a product in a particular country or default country vat if product is unknown * * @param int $idprod Id of product * @param int $local 1 for localtax1, 2 for localtax 2 * @param Societe $thirdpartytouseforcountry Thirdparty with a ->country_code defined (FR, US, IT, ...) * @return int Return integer <0 if KO, Vat rate if OK * @see get_product_vat_for_country() */ function get_product_localtax_for_country($idprod, $local, $thirdpartytouseforcountry) { global $db, $mysoc; if (!class_exists('Product')) { require_once DOL_DOCUMENT_ROOT . '/product/class/product.class.php'; } $ret = 0; $found = 0; if ($idprod > 0) { // Load product $product = new Product($db); $result = $product->fetch($idprod); if ($mysoc->country_code == $thirdpartytouseforcountry->country_code) { // If selling country is ours /* Not defined yet, so we don't use this if ($local==1) $ret=$product->localtax1_tx; elseif ($local==2) $ret=$product->localtax2_tx; $found=1; */ } else { // TODO Read default product vat according to product and another countrycode. // Vat for couple anothercountrycode/product is data that is not managed and store yet, so we will fallback on next rule. } } if (!$found) { // If vat of product for the country not found or not defined, we return higher vat of country. $sql = "SELECT taux as vat_rate, localtax1, localtax2"; $sql .= " FROM " . MAIN_DB_PREFIX . "c_tva as t, " . MAIN_DB_PREFIX . "c_country as c"; $sql .= " WHERE t.active=1 AND t.fk_pays = c.rowid AND c.code='" . $db->escape($thirdpartytouseforcountry->country_code) . "'"; $sql .= " AND t.entity IN (" . getEntity('c_tva') . ")"; $sql .= " ORDER BY t.taux DESC, t.recuperableonly ASC"; $sql .= $db->plimit(1); $resql = $db->query($sql); if ($resql) { $obj = $db->fetch_object($resql); if ($obj) { if ($local == 1) { $ret = $obj->localtax1; } elseif ($local == 2) { $ret = $obj->localtax2; } } } else { dol_print_error($db); } } dol_syslog("get_product_localtax_for_country: ret=" . $ret); return $ret; } /** * Function that return vat rate of a product line (according to seller, buyer and product vat rate) * VATRULE 0: If we are in mode SERVICE_ARE_ECOMMERCE_200238EC and customer is not a company with a vat id, we use default product VAT in buyer country * VATRULE 1: If seller does not use VAT, default VAT is 0. End of rule. * VATRULE 2: If buyer department has a VAT rule from vat rates dictionary then it's the default VAT rate. End of rule. * VATRULE 3: If the (seller country = buyer country) then the default VAT = VAT of the product sold. End of rule. * VATRULE 4: If (seller and buyer in the European Community) and (property sold = new means of transport such as car, boat, plane) then VAT by default = 0 (VAT must be paid by the buyer to the tax center of his country and not to the seller). End of rule. * VATRULE 5: If (seller and buyer in the European Community) and (buyer = individual) then VAT by default = VAT of the product sold. End of rule * VATRULE 6: If (seller and buyer in European Community) and (buyer = company) then VAT by default=0. End of rule * VATRULE 7: Otherwise the VAT proposed by default=0. End of rule. * * @param Societe $thirdparty_seller Object Seller company * @param Societe $thirdparty_buyer Object Buyer company * @param int $idprod Id product * @param int $idprodfournprice Id product_fournisseur_price (for supplier order/invoice) * @return float|string Vat rate to use with format 5.0 or '5.0 (XXX)', -1 if we can't guess it * @see get_default_localtax(), get_default_npr() */ function get_default_tva(Societe $thirdparty_seller, Societe $thirdparty_buyer, $idprod = 0, $idprodfournprice = 0) { global $mysoc, $db, $hookmanager; require_once DOL_DOCUMENT_ROOT . '/core/lib/company.lib.php'; // Note: possible values for tva_assuj are 0/1 or franchise/reel $seller_use_vat = ((is_numeric($thirdparty_seller->tva_assuj) && !$thirdparty_seller->tva_assuj) || (!is_numeric($thirdparty_seller->tva_assuj) && $thirdparty_seller->tva_assuj == 'franchise')) ? 0 : 1; if (empty($thirdparty_seller->country_code)) { $thirdparty_seller->country_code = $mysoc->country_code; } $seller_country_code = $thirdparty_seller->country_code; $seller_in_cee = isInEEC($thirdparty_seller); if (empty($thirdparty_buyer->country_code)) { $thirdparty_buyer->country_code = $mysoc->country_code; } $buyer_country_code = $thirdparty_buyer->country_code; $buyer_in_cee = isInEEC($thirdparty_buyer); dol_syslog( "get_default_tva: seller use vat=" . $seller_use_vat . ", seller country=" . $seller_country_code . ", seller in cee=" . ((string) (int) $seller_in_cee) .", buyer vat number=" . $thirdparty_buyer->tva_intra . " buyer country=" . $buyer_country_code . ", buyer state=" . $thirdparty_buyer->state_id . " buyer in cee=" . ((string) (int) $buyer_in_cee) .", idprod=" . $idprod . ", idprodfournprice=" . $idprodfournprice . ", SERVICE_ARE_ECOMMERCE_200238EC=" . getDolGlobalString('SERVICE_ARE_ECOMMERCE_200238EC') ); $vatvalue = 0; $vatrule = ''; // If services are eServices according to EU Council Directive 2002/38/EC (http://ec.europa.eu/taxation_customs/taxation/vat/traders/e-commerce/article_1610_en.htm) // we use the buyer VAT. if (getDolGlobalString('SERVICE_ARE_ECOMMERCE_200238EC')) { if ($seller_in_cee && $buyer_in_cee) { $isacompany = $thirdparty_buyer->isACompany(); if ($isacompany && !getDolGlobalString('MAIN_USE_VAT_ZERO_FOR_COMPANIES_IN_EEC_EVEN_IF_VAT_ID_UNKNOWN')) { require_once DOL_DOCUMENT_ROOT . '/core/lib/functions2.lib.php'; if (!isValidVATID($thirdparty_buyer)) { $isacompany = 0; } } if (!$isacompany) { $vatvalue = get_product_vat_for_country($idprod, $thirdparty_buyer, $idprodfournprice); $vatrule = 'VATRULE 0'; } } } // If seller does not use VAT, default VAT is 0. End of rule. if (empty($vatrule) && !$seller_use_vat) { //print 'VATRULE 1'; // TODO get the VAT Code of exemption asked into setup if country isInEEC (from an array list of possible // values like VATEX-EU-132-*, VATEX-FR-FRANCHISE, VATEX-EU-AE... // When we had recorded it, we also added a corresponding entry into table of vat code if it does not exists yet. // Here we test if entry for the VAT exemption code exists in llx_vat, we can return '0 (VATEX-EU-132-xx)' // If not, we add it and we return '0 (VATEX-EU-132-xx)' $vatvalue = 0; $vatrule = 'VATRULE 1'; } // 'VATRULE 2' - Force VAT if a buyer department is defined on vat rates dictionary if (empty($vatrule) && !empty($thirdparty_buyer->state_id)) { $sql = "SELECT d.rowid, t.taux as vat_default_rate, t.code as vat_default_code "; $sql .= " FROM " . $db->prefix() . "c_tva as t"; $sql .= " INNER JOIN " . $db->prefix() . "c_departements as d ON t.fk_department_buyer = d.rowid"; $sql .= " WHERE d.rowid = " . ((int) $thirdparty_buyer->state_id); $sql .= " AND t.active > 0"; $sql .= " AND t.entity IN (".getEntity('c_tva').")"; $sql .= " ORDER BY t.use_default DESC, t.taux DESC, t.code ASC, t.recuperableonly ASC"; $res = $db->query($sql); if ($res) { if ($db->num_rows($res)) { $obj = $db->fetch_object($res); $vatvalue = $obj->vat_default_rate . ' (' . $obj->vat_default_code . ')'; $vatrule = 'VATRULE 2'; } $db->free($res); } } // If the (seller country = buyer country) then the default VAT = VAT of the product sold. End of rule. if (empty($vatrule) && ( ($seller_country_code == $buyer_country_code) || (in_array($seller_country_code, array('FR', 'MC')) && in_array($buyer_country_code, array('FR', 'MC'))) || (in_array($seller_country_code, array('MQ', 'GP')) && in_array($buyer_country_code, array('MQ', 'GP'))) // We should be able to manage the case of MQ, GP, ... with a deicated vat rate at previous step. )) { // Warning ->country_code not always defined //print 'VATRULE 3'; $tmpvat = get_product_vat_for_country($idprod, $thirdparty_seller, $idprodfournprice); if ($seller_country_code == 'IN' && getDolGlobalString('MAIN_SALETAX_AUTOSWITCH_I_CS_FOR_INDIA')) { // Special case for india. //print 'VATRULE 3b'; $reg = array(); if (preg_match('/C+S-(\d+)/', $tmpvat, $reg) && $thirdparty_seller->state_id != $thirdparty_buyer->state_id) { // we must revert the C+S into I $tmpvat = str_replace("C+S", "I", $tmpvat); } elseif (preg_match('/I-(\d+)/', $tmpvat, $reg) && $thirdparty_seller->state_id == $thirdparty_buyer->state_id) { // we must revert the I into C+S $tmpvat = str_replace("I", "C+S", $tmpvat); } } $vatvalue = $tmpvat; $vatrule = 'VATRULE 3b'; } // If (seller and buyer in the European Community) and (property sold = new means of transport such as car, boat, plane) then VAT by default = 0 (VAT must be paid by the buyer to the tax center of his country and not to the seller). End of rule. // 'VATRULE 4' - Not supported // If (seller and buyer in the European Community) and (buyer = individual) then VAT by default = VAT of the product sold. End of rule // If (seller and buyer in European Community) and (buyer = company) then VAT by default=0. End of rule if (empty($vatrule) && ($seller_in_cee && $buyer_in_cee)) { $isacompany = $thirdparty_buyer->isACompany(); if ($isacompany && !getDolGlobalString('MAIN_USE_VAT_ZERO_FOR_COMPANIES_IN_EEC_EVEN_IF_VAT_ID_UNKNOWN')) { require_once DOL_DOCUMENT_ROOT . '/core/lib/functions2.lib.php'; if (!isValidVATID($thirdparty_buyer)) { $isacompany = 0; } } if (!$isacompany) { //print 'VATRULE 5'; $vatvalue = get_product_vat_for_country($idprod, $thirdparty_seller, $idprodfournprice); $vatrule = 'VATRULE 5'; } else { //print 'VATRULE 6'; // TODO This is the case of VAT exemption 'VATEX-EU-IC' // If entry for the VAT exemption code exists in llx_vat, we can return '0 (VATEX-EU-IC)' // If not, we add it and we return '0 (VATEX-EU-IC)' $vatvalue = 0; $vatrule = 'VATRULE 6'; } } // If (seller in the European Community and buyer outside the European Community and private buyer) then VAT by default = VAT of the product sold. End of rule // I don't see any use case that need this rule, this case is on only if MAIN_USE_VAT_OF_PRODUCT_FOR_INDIVIDUAL_CUSTOMER_OUT_OF_EEC set if (empty($vatrule) && getDolGlobalString('MAIN_USE_VAT_OF_PRODUCT_FOR_INDIVIDUAL_CUSTOMER_OUT_OF_EEC') && empty($buyer_in_cee)) { $isacompany = $thirdparty_buyer->isACompany(); if (!$isacompany) { $vatvalue = get_product_vat_for_country($idprod, $thirdparty_seller, $idprodfournprice); $vatrule = 'VATRULE extra'; //print 'VATRULE extra'; } } // Otherwise the VAT proposed by default=0. End of rule. // Rem: This means that at least one of the 2 is outside the European Community and the country differs //print 'VATRULE 7'; // TODO This is the case of VAT exemption 'VATEX-EU-G' // If entry for the VAT exemption code exists in llx_vat, we can return '0 (VATEX-xxx)' // If not, we add it and we return '0 (VATEX-xxx)' // Allow an external module to bypass the calculation of prices $parameters = array('vatvalue' => $vatvalue, 'vatrule' => $vatrule); $tmpobject = null; $tmpaction = ''; // @phan-suppress-next-line PhanPluginConstantVariableNull $reshook = $hookmanager->executeHooks('get_default_tva', $parameters, $tmpobject, $tmpaction); // @phan-suppress-current-line PhanPluginConstantVariableNull if ($reshook > 0 && !empty($hookmanager->resArray['vatvalue'])) { $vatvalue = $hookmanager->resArray['vatvalue']; $vatrule = $hookmanager->resArray['vatrule']; // For information } return $vatvalue; } /** * Function that returns whether VAT must be recoverable collected VAT (e.g.: VAT NPR in France) * * @param Societe $thirdparty_seller Thirdparty seller * @param Societe $thirdparty_buyer Thirdparty buyer * @param int $idprod Id product * @param int $idprodfournprice Id supplier price for product * @return int<0,1> 0 or 1 * @see get_default_tva(), get_default_localtax() */ function get_default_npr(Societe $thirdparty_seller, Societe $thirdparty_buyer, $idprod = 0, $idprodfournprice = 0) { global $db; if ($idprodfournprice > 0) { if (!class_exists('ProductFournisseur')) { require_once DOL_DOCUMENT_ROOT . '/fourn/class/fournisseur.product.class.php'; } $prodprice = new ProductFournisseur($db); $prodprice->fetch_product_fournisseur_price($idprodfournprice); return $prodprice->fourn_tva_npr; } elseif ($idprod > 0) { if (!class_exists('Product')) { require_once DOL_DOCUMENT_ROOT . '/product/class/product.class.php'; } $prod = new Product($db); $prod->fetch($idprod); return $prod->tva_npr; } return 0; } /** * Function that return localtax of a product line (according to seller, buyer and product vat rate) * If the seller is not subject to VAT, then default VAT=0. Rule/Test ends. * If (seller country == buyer country) default VAT=sold product VAT. Rule/Test ends. * Else, default VAT=0. Rule/Test ends * * @param Societe $thirdparty_seller Third party seller * @param Societe $thirdparty_buyer Third party buyer * @param int $local Localtax to process (1 or 2) * @param int $idprod Id product * @return int localtax, -1 if it can not be determined * @see get_default_tva(), get_default_npr() */ function get_default_localtax($thirdparty_seller, $thirdparty_buyer, $local, $idprod = 0) { global $mysoc; if (!is_object($thirdparty_seller)) { return -1; } if (!is_object($thirdparty_buyer)) { return -1; } if (empty($thirdparty_seller->country_code)) { $thirdparty_seller->country_code = $mysoc->country_code; } $seller_country_code = $thirdparty_seller->country_code; //$seller_in_cee = isInEEC($thirdparty_seller); if (empty($thirdparty_buyer->country_code)) { $thirdparty_buyer->country_code = $mysoc->country_code; } $buyer_country_code = $thirdparty_buyer->country_code; //$buyer_in_cee = isInEEC($thirdparty_buyer); if ($local == 1) { // Localtax 1 if ($mysoc->country_code == 'ES') { if (is_numeric($thirdparty_buyer->localtax1_assuj) && !$thirdparty_buyer->localtax1_assuj) { return 0; } } else { // Si vendeur non assujeti a Localtax1, localtax1 par default=0 if (is_numeric($thirdparty_seller->localtax1_assuj) && !$thirdparty_seller->localtax1_assuj) { return 0; } if (!is_numeric($thirdparty_seller->localtax1_assuj) && $thirdparty_seller->localtax1_assuj == 'localtax1off') { return 0; } } } elseif ($local == 2) { //I Localtax 2 // Si vendeur non assujeti a Localtax2, localtax2 par default=0 if (is_numeric($thirdparty_seller->localtax2_assuj) && !$thirdparty_seller->localtax2_assuj) { return 0; } if (!is_numeric($thirdparty_seller->localtax2_assuj) && $thirdparty_seller->localtax2_assuj == 'localtax2off') { return 0; } } if ($seller_country_code == $buyer_country_code) { return get_product_localtax_for_country($idprod, $local, $thirdparty_seller); } return 0; } /** * Return a path to have a the directory according to object where files are stored. * This function is called by getMultidirOutput * New usage: $conf->module->multidir_output[$object->entity ?? $conf->entity].'/'.get_exdir(0, 0, 0, 1, $object, '').'/' * or: $conf->module->dir_output.'/'.get_exdir(0, 0, 0, 0, $object, '') * * Example of output with new usage: $object is invoice -> 'INYYMM-ABCD' * Example of output with old usage: '015' with level 3->"5/1/0/", '015' with level 1->"5/", 'ABC-1' with level 3 ->"1/0/0/" * * @param string|int $num Id of object (deprecated, $object->id will be used in future) * @param int $level Level of subdirs to return (1, 2 or 3 levels). (deprecated, global setup will be used in future) * @param int $alpha 0=Keep number only to forge path, 1=Use alpha part after the - (By default, use 0). (deprecated, global option will be used in future) * @param int $withoutslash 0=With slash at end (except if '/', we return ''), 1=without slash at end * @param ?CommonObject $object Object to use to get ref to forge the path. * @param string $modulepart Type of object ('invoice_supplier, 'donation', 'invoice', ...'). Use '' for autodetect from $object. * @return string Sanitized Dir to use ending. Example '' or '1/' or '1/2/' * @see getMultidirOutput() */ function get_exdir($num, $level, $alpha, $withoutslash, $object, $modulepart = '') { if (empty($modulepart) && is_object($object)) { if (!empty($object->module)) { $modulepart = $object->module; } elseif (!empty($object->element)) { $modulepart = $object->element; } } $path = ''; // Define $arrayforoldpath that is module path using a hierarchy on more than 1 level. $arrayforoldpath = array('cheque' => 2, 'category' => 2, 'supplier_invoice' => 2, 'invoice_supplier' => 2, 'mailing' => 2, 'supplier_payment' => 2); if (getDolGlobalInt('PRODUCT_USE_OLD_PATH_FOR_PHOTO')) { $arrayforoldpath['product'] = 2; } if (empty($level) && array_key_exists($modulepart, $arrayforoldpath)) { $level = $arrayforoldpath[$modulepart]; } if (!empty($level) && array_key_exists($modulepart, $arrayforoldpath)) { // This part should be removed once all code is using "get_exdir" to forge path, with parameter $object and $modulepart provided. if (empty($num) && is_object($object)) { $num = ((int) $object->id); } if (empty($alpha)) { $num = preg_replace('/([^0-9])/i', '', $num); } else { $num = preg_replace('/^.*\-/i', '', $num); } $num = substr("000" . $num, -$level); if ($level == 1) { $path = substr($num, 0, 1); } if ($level == 2) { $path = substr($num, 1, 1) . '/' . substr($num, 0, 1); } if ($level == 3) { $path = substr($num, 2, 1) . '/' . substr($num, 1, 1) . '/' . substr($num, 0, 1); } } else { // We will enhance here a common way of forging path for document storage. // In a future, we may distribute directories on several levels depending on setup and object. // Here, $object->id, $object->ref and $modulepart are required. if (in_array($modulepart, array('societe', 'thirdparty')) && $object instanceof Societe) { // Special case for thirdparty, where the ref is a company name that is not unique so path on disk is using the ID instead of the ref $path = dol_sanitizeFileName((string) $object->id); } else { $path = dol_sanitizeFileName(empty($object->ref) ? (string) ((is_object($object) && property_exists($object, 'id')) ? ((int) $object->id) : '') : $object->ref); } } if (empty($withoutslash) && !empty($path)) { $path .= '/'; } return $path; } /** * Creation of a directory (this can create recursive subdir) * * @param string $dir Directory to create (Separator must be '/'. Example: '/mydir/mysubdir') * @param string $dataroot Data root directory (To avoid having the data root in the loop. Using this will also lost the warning, on first dir, saying PHP has no permission when open_basedir is used) * @param string $newmask Mask for new file (Defaults to $conf->global->MAIN_UMASK or 0755 if unavailable). Example: '0444' * @return int Return integer < 0 if KO, 0 = already exists, > 0 if OK */ function dol_mkdir($dir, $dataroot = '', $newmask = '') { dol_syslog("functions.lib::dol_mkdir: dir=" . $dir, LOG_INFO); $dir = dol_sanitizePathName($dir, '_', 0); $dir_osencoded = dol_osencode($dir); if (@is_dir($dir_osencoded)) { return 0; } $nberr = 0; $nbcreated = 0; $ccdir = ''; if (!empty($dataroot)) { // Remove data root from loop $dir = str_replace($dataroot . '/', '', $dir); $ccdir = $dataroot . '/'; } $cdir = explode("/", $dir); $num = count($cdir); for ($i = 0; $i < $num; $i++) { if ($i > 0) { $ccdir .= '/' . $cdir[$i]; } else { $ccdir .= $cdir[$i]; } $regs = array(); if (preg_match("/^.:$/", $ccdir, $regs)) { continue; // If the Windows path is incomplete, continue with next directory } // Attention, is_dir() can fail event if the directory exists // (i.e. according the open_basedir configuration) if ($ccdir) { $ccdir_osencoded = dol_osencode($ccdir); if (!@is_dir($ccdir_osencoded)) { dol_syslog("functions.lib::dol_mkdir: Directory '" . $ccdir . "' is not found (does not exists or is outside open_basedir PHP setting).", LOG_DEBUG); umask(0); $dirmaskdec = octdec((string) $newmask); if (empty($newmask)) { $dirmaskdec = octdec(getDolGlobalString('MAIN_UMASK', '0755')); } $dirmaskdec |= octdec('0111'); // Set x bit required for directories if (!@mkdir($ccdir_osencoded, $dirmaskdec)) { // If the is_dir has returned a false information, we arrive here dol_syslog("functions.lib::dol_mkdir: Fails to create directory '" . $ccdir . "' (no permission to write into parent or directory already exists).", LOG_WARNING); $nberr++; } else { dol_syslog("functions.lib::dol_mkdir: Directory '" . $ccdir . "' created", LOG_DEBUG); $nberr = 0; // At this point in the code, the previous failures can be ignored -> set $nberr to 0 $nbcreated++; } } else { $nberr = 0; // At this point in the code, the previous failures can be ignored -> set $nberr to 0 } } } return ($nberr ? -$nberr : $nbcreated); } /** * Change mod of a file * * @param string $filepath Full file path * @param string $newmask Force new mask. For example '0644' * @return void */ function dolChmod($filepath, $newmask = '') { if (!empty($newmask)) { @chmod($filepath, octdec($newmask)); } elseif (getDolGlobalString('MAIN_UMASK')) { @chmod($filepath, octdec(getDolGlobalString('MAIN_UMASK'))); } } /** * Return picto saying a field is required * * @return string String with the picto mandatory */ function picto_required() { return '*'; } /** * Clean a string from all HTML tags and entities. * This function differs from strip_tags because: * -
are replaced with \n if removelinefeed=0 or 1 * - if entities are found, they are decoded BEFORE the strip * - you can decide to convert line feed into a space * * @param string $stringtoclean String to clean * @param integer $removelinefeed 1=Replace all new lines strings ('
, "\n", "\r") by 1 space, 0=The last new line endings are removed and others '
' are replaced with "\n", 2=The last new line endings are removed and others '
\n' are replaced with "\n" with the same nb of "\n" than the nb of
when there is both "...
\n..." * @param string $pagecodeto Encoding of input/output string * @param integer $strip_tags 0=Use internal strip, 1=Use strip_tags() php function (bugged when text contains a < char that is not for a html tag or when tags is not closed like ']*>(\n|\r)+/ims', '
', $stringtoclean); } $temp = preg_replace('/]*>/i', "\n", $stringtoclean); // We remove entities BEFORE stripping (in case of an open separator char that is entity encoded and not the closing other, the strip will fails) $temp = dol_html_entity_decode($temp, ENT_COMPAT | ENT_HTML5, $pagecodeto); $temp = str_replace('< ', '__ltspace__', $temp); $temp = str_replace('<:', '__lttwopoints__', $temp); if ($strip_tags) { $temp = strip_tags($temp); } else { // Remove '<' into remaining, so remove non closing html tags like '0000-021 // pass 1 - $temp after pass 1: 0000-021 // pass 2 - $temp after pass 2: 0000-021 $tempbis = $temp; do { $temp = $tempbis; $tempbis = str_replace('<>', '', $temp); // No reason to have this into a text, except if value is to try bypass the next html cleaning $tempbis = preg_replace($pattern, '', $tempbis); //$idowhile++; print $temp.'-'.$tempbis."\n"; if ($idowhile > 100) break; } while ($tempbis != $temp); $temp = $tempbis; // Remove '<' into remaining, so remove non closing html tags like ' that are replaced into ... if ($allowiframe) { if (!in_array('iframe', $allowed_tags)) { $allowed_tags[] = "iframe"; } } if ($allowlink) { if (!in_array('link', $allowed_tags)) { $allowed_tags[] = "link"; } if (!in_array('meta', $allowed_tags)) { $allowed_tags[] = "meta"; } } if ($allowscript) { if (!in_array('script', $allowed_tags)) { $allowed_tags[] = "script"; } } if ($allowstyle) { if (!in_array('style', $allowed_tags)) { $allowed_tags[] = "style"; } } if (is_string($sav_allowed_tags)) { $tmptags = explode(',', $sav_allowed_tags); foreach ($tmptags as $tag) { if ($tag != 'common') { $allowed_tags[] = $tag; } } } $allowed_tags_string = implode("><", $allowed_tags); $allowed_tags_string = '<' . $allowed_tags_string . '>'; $stringtoclean = str_replace('', '__!DOCTYPE_HTML__', $stringtoclean); // Replace DOCTYPE to avoid to have it removed by the strip_tags $stringtoclean = dol_string_nounprintableascii($stringtoclean, 0); //$stringtoclean = preg_replace('//', '', $stringtoclean); $stringtoclean = preg_replace('//', '\1', $stringtoclean); if ($allowphp) { $allowed_tags[] = "commentphp"; $stringtoclean = preg_replace('/^<\?php([^"]+)\?>$/i', '\1__', $stringtoclean); // Note: is allowed only if on the same line $stringtoclean = preg_replace('/"<\?php([^"]+)\?>"/i', '"\1"', $stringtoclean); // Note: "" is allowed only if on the same line } $stringtoclean = preg_replace('/:/i', ':', $stringtoclean); $stringtoclean = preg_replace('/:|�+58|:/i', '', $stringtoclean); // refused string ':' encoded (no reason to have a : encoded like this) to disable 'javascript:...' // Remove all HTML tags $temp = strip_tags($stringtoclean, $allowed_tags_string); // Warning: This remove also undesired , so may changes string obfuscated with that pass the injection detection into a harmfull string if ($cleanalsosomestyles) { // Clean for remaining html tags //$temp = preg_replace('/position\s*:\s*(absolute|fixed)\s*!\s*important/i', '', $temp); // Note: If hacker try to introduce css comment into string to bypass this regex, the string must also be encoded by the dol_htmlentitiesbr during output so it become harmless $temp = preg_replace('/position\s*:\s*(absolute|fixed)/i', '', $temp); // Note: If hacker try to introduce css comment into string to bypass this regex, the string must also be encoded by the dol_htmlentitiesbr during output so it become harmless $temp = preg_replace('/z-index\s*:/i', '', $temp); // Note: If hacker try to introduce css comment into string to bypass this regex, the string must also be encoded by the dol_htmlentitiesbr during output so it become harmless } if ($removeclassattribute) { // Clean for remaining html tags $temp = preg_replace('/(<[^>]+)\s+class=((["\']).*?\\3|\\w*)/i', '\\1', $temp); } // Remove 'javascript:' that we should not find into a text // Warning: This is not reliable to fight against obfuscated javascript, there is a lot of other solution to include js into a common html tag (only filtered by a GETPOST(.., powerfullfilter)). if ($cleanalsojavascript) { $temp = preg_replace('/j\s*a\s*v\s*a\s*s\s*c\s*r\s*i\s*p\s*t\s*:/i', '', $temp); } $temp = str_replace('__!DOCTYPE_HTML__', '', $temp); // Restore the DOCTYPE if ($allowphp) { $temp = preg_replace('/(.*)<\/commentphp>/', '', $temp); // Restore php code } $temp = preg_replace('/([^>]*)<\/comment>/', '', $temp); // Restore html comments return $temp; } /** * Clean a string from some undesirable HTML tags. * Note: Complementary to dol_string_onlythesehtmltags(). * This method is used for example by dol_htmlwithnojs() when option MAIN_RESTRICTHTML_REMOVE_ALSO_BAD_ATTRIBUTES is set to 1. * * @param string $stringtoclean String to clean * @param string[]|null $allowed_attributes Array of attributes to keep (null = default list) * @param int|null $ishtml Use 1 if string is not an HTML content. 0 if it is, null if unknown. * @return string String cleaned * * @see dol_htmlwithnojs() dol_escape_htmltag() strip_tags() dol_string_nohtmltag() dol_string_onlythesehtmltags() dol_string_neverthesehtmltags() * @phan-suppress PhanUndeclaredProperty */ function dol_string_onlythesehtmlattributes($stringtoclean, $allowed_attributes = null, $ishtml = null) { if (is_null($allowed_attributes)) { $allowed_attributes = array( // HTML 4 "allow", "allowfullscreen", "alt", "async", "class", "contenteditable", "crossorigin", "data-html", "frameborder", "height", "href", "id", "name", "property", "rel", "src", "style", "target", "title", "type", "width", // HTML5 "footer", "header", "menu", "menuitem", "nav", "section" ); } // Always add content and http-equiv for meta tags, required to force encoding and keep html content in utf8 by load/saveHTML functions. if (!in_array("content", $allowed_attributes)) { $allowed_attributes[] = "content"; } if (!in_array("http-equiv", $allowed_attributes)) { $allowed_attributes[] = "http-equiv"; } if (class_exists('DOMDocument') && !empty($stringtoclean)) { if ($ishtml === 0) { $stringtoclean = str_replace('&', '__AMPINTEXT__', $stringtoclean); // Restore & char not entity } // Warning: loadHTML does not support HTML5 on old libxml versions. $dom = new DOMDocument('', 'UTF-8'); // If $stringtoclean is wrong, it will generates warnings. So we disable warnings and restore them later. $savwarning = error_reporting(); error_reporting(E_ALL & ~E_WARNING & ~E_NOTICE); $wrapperId = "dol_string_onlythesehtmlattributes___wrapper"; $dom->loadHTML('
' . $stringtoclean . '
', LIBXML_HTML_NODEFDTD | LIBXML_ERR_NONE | LIBXML_HTML_NOIMPLIED | LIBXML_NONET | LIBXML_NOWARNING | LIBXML_NOERROR | LIBXML_NOXMLDECL); error_reporting($savwarning); for ($els = $dom->getElementsByTagname('*'), $i = $els->length - 1; $i >= 0; $i--) { $el = $els->item($i); if (!$el instanceof DOMElement) { continue; } $attrs = $el->attributes; for ($ii = $attrs->length - 1; $ii >= 0; $ii--) { //var_dump($attrs->item($ii)); if (!empty($attrs->item($ii)->name)) { if (! in_array($attrs->item($ii)->name, $allowed_attributes)) { // Delete attribute if not into allowed_attributes @phan-suppress-next-line PhanUndeclaredMethod $els->item($i)->removeAttribute($attrs->item($ii)->name); } elseif (in_array($attrs->item($ii)->name, array('style'))) { // If attribute is 'style' $valuetoclean = $attrs->item($ii)->value; if ($valuetoclean !== null) { do { $oldvaluetoclean = $valuetoclean; $valuetoclean = preg_replace('/\/\*.*\*\//m', '', $valuetoclean); // clean css comments $valuetoclean = preg_replace('/position\s*:\s*[a-z]+/mi', '', $valuetoclean); if ($els->item($i)->tagName == 'a') { // more paranoiac cleaning for clickable tags. $valuetoclean = preg_replace('/display\s*:/mi', '', $valuetoclean); $valuetoclean = preg_replace('/z-index\s*:/mi', '', $valuetoclean); $valuetoclean = preg_replace('/\s+(top|left|right|bottom)\s*:/mi', '', $valuetoclean); } // We do not allow logout|passwordforgotten.php and action= into the content of a "style" tag $valuetoclean = preg_replace('/(logout|passwordforgotten)\.php/mi', '', $valuetoclean); $valuetoclean = preg_replace('/action=/mi', '', $valuetoclean); } while ($oldvaluetoclean != $valuetoclean); } $attrs->item($ii)->value = $valuetoclean; } } } } $dom->encoding = 'UTF-8'; $wrapper = $dom->getElementById($wrapperId); $return = ''; foreach ($wrapper->childNodes as $child) { $return .= $dom->saveHTML($child); } if ($ishtml === 0) { $return = str_replace('__AMPINTEXT__', '&', $return); // Restore & char not entity } return trim($return); } else { return $stringtoclean; } } /** * Clean a string from some undesirable HTML tags. * Note: You should use instead dol_string_onlythesehtmltags() that is more secured if you can. * * @param string $stringtoclean String to clean * @param string[] $disallowed_tags Array of tags not allowed * @param int<0,1> $cleanalsosomestyles Clean also some tags * @return string String cleaned * * @see dol_escape_htmltag() strip_tags() dol_string_nohtmltag() dol_string_onlythesehtmltags() dol_string_onlythesehtmlattributes() */ function dol_string_neverthesehtmltags($stringtoclean, $disallowed_tags = array('textarea'), $cleanalsosomestyles = 0) { $temp = $stringtoclean; foreach ($disallowed_tags as $tagtoremove) { $temp = preg_replace('/<\/?' . $tagtoremove . '>/', '', $temp); $temp = preg_replace('/<\/?' . $tagtoremove . '\s+[^>]*>/', '', $temp); } if ($cleanalsosomestyles) { $temp = preg_replace('/position\s*:\s*(absolute|fixed)\s*!\s*important/', '', $temp); // Note: If hacker try to introduce css comment into string to avoid this, string should be encoded by the dol_htmlentitiesbr so be harmless } return $temp; } /** * Close the HTML tags left open in a truncated HTML string. * Truncating HTML on a separator can cut inside a block, and an unclosed tag makes the browser nest * everything that follows inside it. Only tags really left open are closed, in reverse order. * * @param string $text HTML string, possibly with unclosed tags * @return string Same string with the missing closing tags appended * @see dolGetFirstLineOfText() */ function dolCloseUnclosedHtmlTags($text) { if (!is_string($text) || $text === '') { return $text; } // Tags that never carry a closing tag $selfclosing = array('br', 'hr', 'img', 'input', 'meta', 'link', 'source', 'col', 'area', 'base', 'embed', 'param', 'track', 'wbr'); $opened = array(); if (preg_match_all('/<\s*(\/?)([a-zA-Z][a-zA-Z0-9]*)[^>]*?(\/?)\s*>/', $text, $matches, PREG_SET_ORDER)) { foreach ($matches as $match) { $tag = strtolower($match[2]); if (in_array($tag, $selfclosing) || !empty($match[3])) { continue; } if (empty($match[1])) { $opened[] = $tag; } else { // Close the most recent matching opened tag, ignore a stray closing tag $idx = array_search($tag, array_reverse($opened, true), true); if ($idx !== false) { unset($opened[$idx]); } } } } foreach (array_reverse($opened) as $tag) { $text .= ''; } return $text; } /** * Return first line of text. Cut will depends if content is HTML or not. * * @param string $text Input text * @param int $nboflines Nb of lines to get (default is 1 = first line only) * @param string $charset Charset of $text string (UTF-8 by default) * @return string Output text * @see dol_nboflines_bis(), dol_string_nohtmltag(), dol_escape_htmltag() */ function dolGetFirstLineOfText($text, $nboflines = 1, $charset = 'UTF-8') { if ($nboflines == 1) { if (dol_textishtml($text)) { $firstline = preg_replace('/]*>.*$/s', '', $text); // The s pattern modifier means the . can match newline characters $firstline = preg_replace('/]*>.*$/s', '', $firstline); // The s pattern modifier means the . can match newline characters } else { if (isset($text)) { $firstline = preg_replace('/[\n\r].*/', '', $text); } else { $firstline = ''; } } return $firstline . (isset($firstline) && isset($text) && (strlen($firstline) != strlen($text)) ? '...' : ''); } else { $ishtml = 0; if (dol_textishtml($text)) { $text = preg_replace('/\n/', '', $text); $ishtml = 1; $repTable = array("\t" => " ", "\n" => " ", "\r" => " ", "\0" => " ", "\x0B" => " "); } else { $repTable = array("\t" => " ", "\n" => "
", "\r" => " ", "\0" => " ", "\x0B" => " "); } $text = strtr($text, $repTable); if ($charset == 'UTF-8') { $pattern = '/(]*>)/Uu'; } else { // /U is to have UNGREEDY regex to limit to one html tag. /u is for UTF8 support $pattern = '/(]*>)/U'; // /U is to have UNGREEDY regex to limit to one html tag. } $a = preg_split($pattern, $text, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); $firstline = ''; $i = 0; $countline = 0; $lastaddediscontent = 1; while ($countline < $nboflines && isset($a[$i])) { if (preg_match('/]*>/', $a[$i])) { if (array_key_exists($i + 1, $a) && !empty($a[$i + 1])) { $firstline .= ($ishtml ? "
\n" : "\n"); // Is it a br for a new line of after a printed line ? if (!$lastaddediscontent) { $countline++; } $lastaddediscontent = 0; } } else { $firstline .= $a[$i]; $lastaddediscontent = 1; $countline++; } $i++; } $adddots = (isset($a[$i]) && (!preg_match('/]*>/', $a[$i]) || (array_key_exists($i + 1, $a) && !empty($a[$i + 1])))); //unset($a); $ret = $firstline . ($adddots ? '...' : ''); //exit; return $ret; } } /** * Replace CRLF in string with a HTML BR tag. * WARNING: The content after operation contains some HTML tags (the
) so be sure to also have * encoded the special chars of stringtoencode into HTML before with dol_htmlentitiesbr(). * * @param string $stringtoencode String to encode * @param int $nl2brmode 0=Adding br before \n, 1=Replacing \n by br * @param bool $forxml false=Use
, true=Use
* @return string String encoded * @see dol_htmlentitiesbr(), dol_nboflines(), dolGetFirstLineOfText() */ function dol_nl2br($stringtoencode, $nl2brmode = 0, $forxml = false) { if (is_null($stringtoencode)) { return ''; } if (!$nl2brmode) { return nl2br($stringtoencode, $forxml); } else { $ret = preg_replace('/(\r\n|\r|\n)/i', ($forxml ? '
' : '
'), $stringtoencode); return $ret; } } /** * Sanitize a HTML to remove js, dangerous content and external links. * This function is used by dolPrintHTML... function for example. * This function is tested by test/phpunit/SecurityTest.php * * @param string $stringtoencode String to encode * @param int $nouseofiframesandbox 0=Default, 1=Allow use of option MAIN_SECURITY_USE_SANDBOX_FOR_HTMLWITHNOJS for html sanitizing (not yet working) * @param string $check 'restricthtmlnolink' or 'restricthtml' or 'restricthtmlallowclass' or 'restricthtmlallowiframe' or 'restricthtmlallowlinkscript' or 'restricthtmlallowunvalid' * @return string HTML sanitized */ function dol_htmlwithnojs($stringtoencode, $nouseofiframesandbox = 0, $check = 'restricthtml') { if (empty($nouseofiframesandbox) && getDolGlobalString('MAIN_SECURITY_USE_SANDBOX_FOR_HTMLWITHNOJS')) { // TODO using sandbox on inline html content is not possible yet with current browsers //$s = ''; return $stringtoencode; } else { $out = $stringtoencode; $antiinfinitloop = 0; // First clean HTML content do { if ($antiinfinitloop >= 20) { dol_print_error(null, "Infinite loop detected after ".$antiinfinitloop." iterations in dol_htmlwithnojs"); die; // We must not break and we must not return a string for security issue. This should never happen. } $antiinfinitloop++; $oldstringtoclean = $out; $outishtml = 0; if (dol_textishtml($out)) { $outishtml = 1; } // HTML sanitizer by DOMDocument if (!empty($out) && getDolGlobalInt('MAIN_RESTRICTHTML_ONLY_VALID_HTML') && $check != 'restricthtmlallowunvalid') { try { libxml_use_internal_errors(false); // Avoid to fill memory with xml errors if (LIBXML_VERSION < 20900) { // Avoid load of external entities (security problem). // Required only if LIBXML_VERSION < 20900 // @phan-suppress-next-line PhanDeprecatedFunctionInternal libxml_disable_entity_loader(true); } if (!$outishtml) { $out = preg_replace('/&(?![a-zA-Z0-9#]+;)/', '__AMPINTEXT__', $out); } $dom = new DOMDocument(); // Note:
is transformed into // We don't want that, so we protect __[xxx]__ by replacing [ and ] before loadHTML and restore them after saveHTML $out = preg_replace_callback( '/__\[([0-9a-zA-Z_]+)\]__/', /** * @param array $m Array of matches * @return string Translated string for the key */ function ($m) { return '__BRACKETSTART' . $m[1] . 'BRACKETEND__'; }, $out ); $wrapperId = "dol_htmlwithnojs___wrapper___toremove"; $dom->loadHTML('
' . $out . '
', LIBXML_HTML_NODEFDTD | LIBXML_ERR_NONE | LIBXML_HTML_NOIMPLIED | LIBXML_NONET | LIBXML_NOWARNING | LIBXML_NOERROR | LIBXML_NOXMLDECL); $dom->encoding = 'UTF-8'; // Add a layer to remove some styles if (getDolGlobalInt('MAIN_RESTRICTHTML_ONLY_VALID_HTML') == 2) { foreach ($dom->getElementsByTagName('*') as $el) { if (!$el instanceof DOMElement) { continue; } // @phan-suppress-next-line PhanPluginUnknownObjectMethodCall if ($el->hasAttribute('style')) { // @phan-suppress-next-line PhanPluginUnknownObjectMethodCall $style = $el->getAttribute('style'); // delete some styles $style = preg_replace('/z-index\s*:/i', '', $style); $style = preg_replace('/position\s*:/i', '', $style); $style = preg_replace('/top\s*:/i', '', $style); $style = preg_replace('/left\s*:/i', '', $style); $style = preg_replace('/background\s*:/i', '', $style); /* $style = preg_replace('/width\s*:/i', '', $style); $style = preg_replace('/height\s*:/i', '', $style); $style = preg_replace('/backdrop-filter\s*:/i', '', $style); */ if (trim($style) === '') { // @phan-suppress-next-line PhanPluginUnknownObjectMethodCall $el->removeAttribute('style'); } else { // @phan-suppress-next-line PhanPluginUnknownObjectMethodCall $el->setAttribute('style', $style); } } } } $wrapper = $dom->getElementById($wrapperId); $result = ''; foreach ($wrapper->childNodes as $child) { $result .= $dom->saveHTML($child); } $out = trim($result); // Restore [ and ] that were protected before loadHTML $out = preg_replace_callback( '/__BRACKETSTART([0-9a-zA-Z_]+)BRACKETEND__/', /** * @param array $m Array of matches * @return string Translated string for the key */ function ($m) { return '__[' . $m[1] . ']__'; }, $out ); if (!$outishtml) { // If $out was not HTML content we made before a dol_nl2br so we must do the opposite operation now $out = str_replace('__AMPINTEXT__', '&', $out); // Restore & char not entity $out = preg_replace('//i', "\n", $out); } } catch (Exception $e) { // If error, invalid HTML string with no way to clean it //print $e->getMessage(); $out = 'InvalidHTMLStringCantBeCleaned ' . $e->getMessage(); } } // HTML sanitizer by Tidy // Tidy can't be used for restricthtmlallowunvalid and restricthtmlallowlinkscript // Tidy can't be used for non html text content as it is corrupting the new lines fields. if (!empty($out) && getDolGlobalInt('MAIN_RESTRICTHTML_ONLY_VALID_HTML_TIDY') && !in_array($check, array('restricthtmlallowunvalid', 'restricthtmlallowlinkscript')) && $outishtml) { // TODO Try to implement a hack for restricthtmlallowlinkscript by renaming tag and