1<?php
2/**
3 * Copyright since 2007 PrestaShop SA and Contributors
4 * PrestaShop is an International Registered Trademark & Property of PrestaShop SA
5 *
6 * NOTICE OF LICENSE
7 *
8 * This source file is subject to the Open Software License (OSL 3.0)
9 * that is bundled with this package in the file LICENSE.md.
10 * It is also available through the world-wide-web at this URL:
11 * https://opensource.org/licenses/OSL-3.0
12 * If you did not receive a copy of the license and are unable to
13 * obtain it through the world-wide-web, please send an email
14 * to license@prestashop.com so we can send you a copy immediately.
15 *
16 * DISCLAIMER
17 *
18 * Do not edit or add to this file if you wish to upgrade PrestaShop to newer
19 * versions in the future. If you wish to customize PrestaShop for your
20 * needs please refer to https://devdocs.prestashop.com/ for more information.
21 *
22 * @author    PrestaShop SA and Contributors <contact@prestashop.com>
23 * @copyright Since 2007 PrestaShop SA and Contributors
24 * @license   https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
25 */
26use Composer\CaBundle\CaBundle;
27use PHPSQLParser\PHPSQLParser;
28use PrestaShop\PrestaShop\Adapter\ContainerFinder;
29use PrestaShop\PrestaShop\Core\Exception\ContainerNotFoundException;
30use PrestaShop\PrestaShop\Core\Foundation\Filesystem\FileSystem as PsFileSystem;
31use PrestaShop\PrestaShop\Core\Localization\Exception\LocalizationException;
32use PrestaShop\PrestaShop\Core\Localization\Locale;
33use PrestaShop\PrestaShop\Core\Localization\Locale\Repository as LocaleRepository;
34use PrestaShop\PrestaShop\Core\String\CharacterCleaner;
35use PrestaShop\PrestaShop\Core\Util\ColorBrightnessCalculator;
36use Symfony\Component\Filesystem\Filesystem;
37use Symfony\Component\HttpFoundation\Request;
38
39class ToolsCore
40{
41    const CACERT_LOCATION = 'https://curl.haxx.se/ca/cacert.pem';
42    const SERVICE_LOCALE_REPOSITORY = 'prestashop.core.localization.locale.repository';
43    public const CACHE_LIFETIME_SECONDS = 604800;
44
45    protected static $file_exists_cache = [];
46    protected static $_forceCompile;
47    protected static $_caching;
48    protected static $_user_plateform;
49    protected static $_user_browser;
50    protected static $request;
51    protected static $cldr_cache = [];
52    protected static $colorBrightnessCalculator;
53    protected static $fallbackParameters = [];
54
55    public static $round_mode = null;
56
57    /**
58     * @param Request $request
59     */
60    public function __construct(Request $request = null)
61    {
62        if ($request) {
63            self::$request = $request;
64        }
65    }
66
67    /**
68     * Properly clean static cache
69     */
70    public static function resetStaticCache()
71    {
72        static::$cldr_cache = [];
73    }
74
75    /**
76     * Reset the request set during the first new Tools($request) call.
77     */
78    public static function resetRequest()
79    {
80        self::$request = null;
81    }
82
83    /**
84     * Random password generator.
85     *
86     * @param int $length Desired length (optional)
87     * @param string $flag Output type (NUMERIC, ALPHANUMERIC, NO_NUMERIC, RANDOM)
88     *
89     * @return bool|string Password
90     */
91    public static function passwdGen($length = 8, $flag = 'ALPHANUMERIC')
92    {
93        $length = (int) $length;
94
95        if ($length <= 0) {
96            return false;
97        }
98
99        switch ($flag) {
100            case 'NUMERIC':
101                $str = '0123456789';
102
103                break;
104            case 'NO_NUMERIC':
105                $str = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
106
107                break;
108            case 'RANDOM':
109                $num_bytes = ceil($length * 0.75);
110                $bytes = self::getBytes($num_bytes);
111
112                return substr(rtrim(base64_encode($bytes), '='), 0, $length);
113            case 'ALPHANUMERIC':
114            default:
115                $str = 'abcdefghijkmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
116
117                break;
118        }
119
120        $bytes = Tools::getBytes($length);
121        $position = 0;
122        $result = '';
123
124        for ($i = 0; $i < $length; ++$i) {
125            $position = ($position + ord($bytes[$i])) % strlen($str);
126            $result .= $str[$position];
127        }
128
129        return $result;
130    }
131
132    /**
133     * Random bytes generator.
134     *
135     * Limited to OpenSSL since 1.7.0.0
136     *
137     * @param int $length Desired length of random bytes
138     *
139     * @return bool|string Random bytes
140     */
141    public static function getBytes($length)
142    {
143        $length = (int) $length;
144
145        if ($length <= 0) {
146            return false;
147        }
148
149        $bytes = openssl_random_pseudo_bytes($length, $cryptoStrong);
150
151        if ($cryptoStrong === true) {
152            return $bytes;
153        }
154
155        return false;
156    }
157
158    /**
159     * Replace text within a portion of a string.
160     *
161     * Replaces a string matching a search, (optionally) string from a certain position
162     *
163     * @param string $search The string to search in the input string
164     * @param string $replace The replacement string
165     * @param string $subject The input string
166     * @param int $cur Starting position cursor for the search
167     *
168     * @return string the result string is returned
169     */
170    public static function strReplaceFirst($search, $replace, $subject, $cur = 0)
171    {
172        $strPos = strpos($subject, $search, $cur);
173
174        return $strPos !== false ? substr_replace($subject, $replace, (int) $strPos, strlen($search)) : $subject;
175    }
176
177    /**
178     * Redirect user to another page.
179     *
180     * Warning: uses exit
181     *
182     * @param string $url Desired URL
183     * @param string $base_uri Base URI (optional)
184     * @param Link $link
185     * @param string|array $headers A list of headers to send before redirection
186     */
187    public static function redirect($url, $base_uri = __PS_BASE_URI__, Link $link = null, $headers = null)
188    {
189        if (!$link) {
190            $link = Context::getContext()->link;
191        }
192
193        if (strpos($url, 'http://') === false && strpos($url, 'https://') === false && $link) {
194            if (strpos($url, $base_uri) === 0) {
195                $url = substr($url, strlen($base_uri));
196            }
197            if (strpos($url, 'index.php?controller=') !== false && strpos($url, 'index.php/') == 0) {
198                $url = substr($url, strlen('index.php?controller='));
199                if (Configuration::get('PS_REWRITING_SETTINGS')) {
200                    $url = Tools::strReplaceFirst('&', '?', $url);
201                }
202            }
203
204            $explode = explode('?', $url);
205            $url = $link->getPageLink($explode[0]);
206            if (isset($explode[1])) {
207                $url .= '?' . $explode[1];
208            }
209        }
210
211        // Send additional headers
212        if ($headers) {
213            if (!is_array($headers)) {
214                $headers = [$headers];
215            }
216
217            foreach ($headers as $header) {
218                header($header);
219            }
220        }
221
222        header('Location: ' . $url);
223        exit;
224    }
225
226    /**
227     * Redirect URLs already containing PS_BASE_URI.
228     *
229     * Warning: uses exit
230     *
231     * @param string $url Desired URL
232     */
233    public static function redirectLink($url)
234    {
235        if (!preg_match('@^https?://@i', $url)) {
236            if (strpos($url, __PS_BASE_URI__) !== false && strpos($url, __PS_BASE_URI__) == 0) {
237                $url = substr($url, strlen(__PS_BASE_URI__));
238            }
239            if (strpos($url, 'index.php?controller=') !== false && strpos($url, 'index.php/') == 0) {
240                $url = substr($url, strlen('index.php?controller='));
241            }
242            $explode = explode('?', $url);
243            $url = Context::getContext()->link->getPageLink($explode[0]);
244            if (isset($explode[1])) {
245                $url .= '?' . $explode[1];
246            }
247        }
248        header('Location: ' . $url);
249        exit;
250    }
251
252    /**
253     * Redirect user to another page (using header Location)
254     *
255     * Warning: uses exit
256     *
257     * @param string $url Desired URL
258     */
259    public static function redirectAdmin($url)
260    {
261        header('Location: ' . $url);
262        exit;
263    }
264
265    /**
266     * Returns the available protocol for the current shop in use
267     * SSL if Configuration is set on and available for the server.
268     *
269     * @return string
270     */
271    public static function getShopProtocol()
272    {
273        $protocol = (Configuration::get('PS_SSL_ENABLED') || (!empty($_SERVER['HTTPS'])
274            && Tools::strtolower($_SERVER['HTTPS']) != 'off')) ? 'https://' : 'http://';
275
276        return $protocol;
277    }
278
279    /**
280     * Returns the set protocol according to configuration (http[s]).
281     *
282     * @param bool $use_ssl true if require ssl
283     *
284     * @return string (http|https)
285     */
286    public static function getProtocol($use_ssl = null)
287    {
288        return null !== $use_ssl && $use_ssl ? 'https://' : 'http://';
289    }
290
291    /**
292     * Returns the <b>current</b> host used, with the protocol (http or https) if $http is true
293     * This function should not be used to choose http or https domain name.
294     * Use Tools::getShopDomain() or Tools::getShopDomainSsl instead.
295     *
296     * @param bool $http
297     * @param bool $entities
298     * @param bool $ignore_port
299     *
300     * @return string host
301     */
302    public static function getHttpHost($http = false, $entities = false, $ignore_port = false)
303    {
304        $httpHost = '';
305        if (array_key_exists('HTTP_HOST', $_SERVER)) {
306            $httpHost = $_SERVER['HTTP_HOST'];
307        }
308
309        $host = (isset($_SERVER['HTTP_X_FORWARDED_HOST']) ? $_SERVER['HTTP_X_FORWARDED_HOST'] : $httpHost);
310        if ($ignore_port && $pos = strpos($host, ':')) {
311            $host = substr($host, 0, $pos);
312        }
313        if ($entities) {
314            $host = htmlspecialchars($host, ENT_COMPAT, 'UTF-8');
315        }
316        if ($http) {
317            $host = (Configuration::get('PS_SSL_ENABLED') ? 'https://' : 'http://') . $host;
318        }
319
320        return $host;
321    }
322
323    /**
324     * Returns domain name according to configuration and ignoring ssl.
325     *
326     * @param bool $http if true, return domain name with protocol
327     * @param bool $entities if true, convert special chars to HTML entities
328     *
329     * @return string domain
330     */
331    public static function getShopDomain($http = false, $entities = false)
332    {
333        if (!$domain = ShopUrl::getMainShopDomain()) {
334            $domain = Tools::getHttpHost();
335        }
336        if ($entities) {
337            $domain = htmlspecialchars($domain, ENT_COMPAT, 'UTF-8');
338        }
339        if ($http) {
340            $domain = 'http://' . $domain;
341        }
342
343        return $domain;
344    }
345
346    /**
347     * Returns domain name according to configuration and depending on ssl activation.
348     *
349     * @param bool $http if true, return domain name with protocol
350     * @param bool $entities if true, convert special chars to HTML entities
351     *
352     * @return string domain
353     */
354    public static function getShopDomainSsl($http = false, $entities = false)
355    {
356        if (!$domain = ShopUrl::getMainShopDomainSSL()) {
357            $domain = Tools::getHttpHost();
358        }
359        if ($entities) {
360            $domain = htmlspecialchars($domain, ENT_COMPAT, 'UTF-8');
361        }
362        if ($http) {
363            $domain = (Configuration::get('PS_SSL_ENABLED') ? 'https://' : 'http://') . $domain;
364        }
365
366        return $domain;
367    }
368
369    /**
370     * Get the server variable SERVER_NAME.
371     * Relies on $_SERVER
372     *
373     * @return string server name
374     */
375    public static function getServerName()
376    {
377        if (isset($_SERVER['HTTP_X_FORWARDED_SERVER']) && $_SERVER['HTTP_X_FORWARDED_SERVER']) {
378            return $_SERVER['HTTP_X_FORWARDED_SERVER'];
379        }
380
381        return $_SERVER['SERVER_NAME'];
382    }
383
384    /**
385     * Get the server variable REMOTE_ADDR, or the first ip of HTTP_X_FORWARDED_FOR (when using proxy).
386     *
387     * @return string $remote_addr ip of client
388     */
389    public static function getRemoteAddr()
390    {
391        if (function_exists('apache_request_headers')) {
392            $headers = apache_request_headers();
393        } else {
394            $headers = $_SERVER;
395        }
396
397        if (array_key_exists('X-Forwarded-For', $headers)) {
398            $_SERVER['HTTP_X_FORWARDED_FOR'] = $headers['X-Forwarded-For'];
399        }
400
401        if (isset($_SERVER['HTTP_X_FORWARDED_FOR']) && $_SERVER['HTTP_X_FORWARDED_FOR'] && (!isset($_SERVER['REMOTE_ADDR'])
402            || preg_match('/^127\..*/i', trim($_SERVER['REMOTE_ADDR'])) || preg_match('/^172\.(1[6-9]|2\d|30|31)\..*/i', trim($_SERVER['REMOTE_ADDR']))
403            || preg_match('/^192\.168\.*/i', trim($_SERVER['REMOTE_ADDR'])) || preg_match('/^10\..*/i', trim($_SERVER['REMOTE_ADDR'])))) {
404            if (strpos($_SERVER['HTTP_X_FORWARDED_FOR'], ',')) {
405                $ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
406
407                return $ips[0];
408            } else {
409                return $_SERVER['HTTP_X_FORWARDED_FOR'];
410            }
411        } else {
412            return $_SERVER['REMOTE_ADDR'];
413        }
414    }
415
416    /**
417     * Check if the current page use SSL connection on not.
418     * Relies on $_SERVER global being filled
419     *
420     * @return bool true if SSL is used
421     */
422    public static function usingSecureMode()
423    {
424        if (isset($_SERVER['HTTPS'])) {
425            return in_array(Tools::strtolower($_SERVER['HTTPS']), [1, 'on']);
426        }
427        // $_SERVER['SSL'] exists only in some specific configuration
428        if (isset($_SERVER['SSL'])) {
429            return in_array(Tools::strtolower($_SERVER['SSL']), [1, 'on']);
430        }
431        // $_SERVER['REDIRECT_HTTPS'] exists only in some specific configuration
432        if (isset($_SERVER['REDIRECT_HTTPS'])) {
433            return in_array(Tools::strtolower($_SERVER['REDIRECT_HTTPS']), [1, 'on']);
434        }
435        if (isset($_SERVER['HTTP_SSL'])) {
436            return in_array(Tools::strtolower($_SERVER['HTTP_SSL']), [1, 'on']);
437        }
438        if (isset($_SERVER['HTTP_X_FORWARDED_PROTO'])) {
439            return Tools::strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == 'https';
440        }
441
442        return false;
443    }
444
445    /**
446     * Get the current url prefix protocol (https/http).
447     *
448     * @return string protocol
449     */
450    public static function getCurrentUrlProtocolPrefix()
451    {
452        if (Tools::usingSecureMode()) {
453            return 'https://';
454        } else {
455            return 'http://';
456        }
457    }
458
459    /**
460     * Returns a safe URL referrer.
461     *
462     * @param string $referrer URL referrer
463     *
464     * @return string secured referrer
465     */
466    public static function secureReferrer($referrer)
467    {
468        if (static::urlBelongsToShop($referrer)) {
469            return $referrer;
470        }
471
472        return __PS_BASE_URI__;
473    }
474
475    /**
476     * Indicates if the provided URL belongs to this shop (relative urls count as belonging to the shop).
477     *
478     * @param string $url
479     *
480     * @return bool
481     */
482    public static function urlBelongsToShop($url)
483    {
484        $urlHost = Tools::extractHost($url);
485
486        return empty($urlHost) || $urlHost === Tools::getServerName();
487    }
488
489    /**
490     * Safely extracts the host part from an URL.
491     *
492     * @param string $url
493     *
494     * @return string
495     */
496    public static function extractHost($url)
497    {
498        $parsed = parse_url($url);
499        if (!is_array($parsed)) {
500            return $url;
501        }
502        if (empty($parsed['host']) || empty($parsed['scheme'])) {
503            return '';
504        }
505
506        return $parsed['host'];
507    }
508
509    /**
510     * Get a value from $_POST / $_GET
511     * if unavailable, take a default value.
512     *
513     * @param string $key Value key
514     * @param mixed $default_value (optional)
515     *
516     * @return mixed Value
517     */
518    public static function getValue($key, $default_value = false)
519    {
520        if (empty($key) || !is_string($key)) {
521            return false;
522        }
523
524        if (getenv('kernel.environment') === 'test' && self::$request instanceof Request) {
525            $value = self::$request->request->get($key, self::$request->query->get($key, $default_value));
526        } elseif (isset($_POST[$key]) || isset($_GET[$key])) {
527            $value = isset($_POST[$key]) ? $_POST[$key] : $_GET[$key];
528        } elseif (isset(static::$fallbackParameters[$key])) {
529            $value = static::$fallbackParameters[$key];
530        }
531
532        if (!isset($value)) {
533            $value = $default_value;
534        }
535
536        if (is_string($value)) {
537            return urldecode(preg_replace('/((\%5C0+)|(\%00+))/i', '', urlencode($value)));
538        }
539
540        return $value;
541    }
542
543    /**
544     * Get all values from $_POST/$_GET.
545     *
546     * @return mixed
547     */
548    public static function getAllValues()
549    {
550        return $_POST + $_GET;
551    }
552
553    /**
554     * Checks if a key exists either in $_POST or $_GET.
555     *
556     * @param string $key
557     *
558     * @return bool
559     */
560    public static function getIsset($key)
561    {
562        if (!is_string($key)) {
563            return false;
564        }
565
566        return isset($_POST[$key]) || isset($_GET[$key]);
567    }
568
569    /**
570     * Change language in cookie while clicking on a flag.
571     *
572     * @return string iso code
573     */
574    public static function setCookieLanguage($cookie = null)
575    {
576        if (!$cookie) {
577            $cookie = Context::getContext()->cookie;
578        }
579        /* If language does not exist or is disabled, erase it */
580        if ($cookie->id_lang) {
581            $lang = new Language((int) $cookie->id_lang);
582            if (!Validate::isLoadedObject($lang) || !$lang->active || !$lang->isAssociatedToShop()) {
583                $cookie->id_lang = null;
584            }
585        }
586
587        if (!Configuration::get('PS_DETECT_LANG')) {
588            unset($cookie->detect_language);
589        }
590
591        /* Automatically detect language if not already defined, detect_language is set in Cookie::update */
592        if (!Tools::getValue('isolang') && !Tools::getValue('id_lang') && (!$cookie->id_lang || isset($cookie->detect_language))
593            && isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
594            $array = explode(',', Tools::strtolower($_SERVER['HTTP_ACCEPT_LANGUAGE']));
595            $string = $array[0];
596
597            if (Validate::isLanguageCode($string)) {
598                $lang = Language::getLanguageByIETFCode($string);
599                if (Validate::isLoadedObject($lang) && $lang->active && $lang->isAssociatedToShop()) {
600                    Context::getContext()->language = $lang;
601                    $cookie->id_lang = (int) $lang->id;
602                }
603            }
604        }
605
606        if (isset($cookie->detect_language)) {
607            unset($cookie->detect_language);
608        }
609
610        /* If language file not present, you must use default language file */
611        if (!$cookie->id_lang || !Validate::isUnsignedId($cookie->id_lang)) {
612            $cookie->id_lang = (int) Configuration::get('PS_LANG_DEFAULT');
613        }
614
615        $iso = Language::getIsoById((int) $cookie->id_lang);
616        @include_once _PS_THEME_DIR_ . 'lang/' . $iso . '.php';
617
618        return $iso;
619    }
620
621    /**
622     * If necessary change cookie language ID and context language.
623     *
624     * @param Context|null $context
625     *
626     * @throws PrestaShopDatabaseException
627     * @throws PrestaShopException
628     */
629    public static function switchLanguage(Context $context = null)
630    {
631        if (null === $context) {
632            $context = Context::getContext();
633        }
634
635        // On PrestaShop installations Dispatcher::__construct() gets called (and so Tools::switchLanguage())
636        // Stop in this case by checking the cookie
637        if (!isset($context->cookie)) {
638            return;
639        }
640
641        if (
642            ($iso = Tools::getValue('isolang')) &&
643            Validate::isLanguageIsoCode($iso) &&
644            ($id_lang = (int) Language::getIdByIso($iso))
645        ) {
646            $_GET['id_lang'] = $id_lang;
647        }
648
649        // Only switch if new ID is different from old ID
650        $newLanguageId = (int) Tools::getValue('id_lang');
651
652        if (
653            Validate::isUnsignedId($newLanguageId) &&
654            $newLanguageId !== 0 &&
655            $context->cookie->id_lang !== $newLanguageId
656        ) {
657            $context->cookie->id_lang = $newLanguageId;
658            $language = new Language($newLanguageId);
659            if (Validate::isLoadedObject($language) && $language->active && $language->isAssociatedToShop()) {
660                $context->language = $language;
661            }
662        }
663
664        Tools::setCookieLanguage($context->cookie);
665    }
666
667    public static function getCountry($address = null)
668    {
669        $countryId = Tools::getValue('id_country');
670        if (Validate::isInt($countryId)
671            && (int) $countryId > 0
672            && !empty(Country::getIsoById((int) $countryId))
673        ) {
674            return (int) $countryId;
675        }
676
677        if (!empty($address->id_country) && (int) $address->id_country > 0) {
678            return (int) $address->id_country;
679        }
680
681        return (int) Configuration::get('PS_COUNTRY_DEFAULT');
682    }
683
684    /**
685     * Set cookie currency from POST or default currency.
686     *
687     * @return Currency object
688     */
689    public static function setCurrency($cookie)
690    {
691        if (Tools::isSubmit('SubmitCurrency') && ($id_currency = Tools::getValue('id_currency'))) {
692            /** @var Currency $currency */
693            $currency = Currency::getCurrencyInstance((int) $id_currency);
694            if (is_object($currency) && $currency->id && !$currency->deleted && $currency->isAssociatedToShop()) {
695                $cookie->id_currency = (int) $currency->id;
696            }
697        }
698
699        $currency = null;
700        if ((int) $cookie->id_currency) {
701            $currency = Currency::getCurrencyInstance((int) $cookie->id_currency);
702        }
703        if (!Validate::isLoadedObject($currency) || (bool) $currency->deleted || !(bool) $currency->active) {
704            $currency = Currency::getCurrencyInstance(Configuration::get('PS_CURRENCY_DEFAULT'));
705        }
706
707        $cookie->id_currency = (int) $currency->id;
708        if ($currency->isAssociatedToShop()) {
709            return $currency;
710        } else {
711            // get currency from context
712            $currency = Shop::getEntityIds('currency', Context::getContext()->shop->id, true, true);
713            if (isset($currency[0]) && $currency[0]['id_currency']) {
714                $cookie->id_currency = $currency[0]['id_currency'];
715
716                return Currency::getCurrencyInstance((int) $cookie->id_currency);
717            }
718        }
719
720        return $currency;
721    }
722
723    /**
724     * Return the CLDR associated with the context or given language_code.
725     *
726     * @see Tools::getContextLocale
727     * @deprecated since PrestaShop 1.7.6.0
728     *
729     * @param Context|null $context
730     * @param null $language_code
731     *
732     * @throws PrestaShopException
733     */
734    public static function getCldr(Context $context = null, $language_code = null)
735    {
736        throw new PrestaShopException('This CLDR library has been removed. See Tools::getContextLocale instead.');
737    }
738
739    /**
740     * Return price with currency sign for a given product.
741     *
742     * @deprecated Since 1.7.6.0. Please use Locale::formatPrice() instead
743     * @see PrestaShop\PrestaShop\Core\Localization\Locale
744     *
745     * @param float $price Product price
746     * @param int|Currency|array|null $currency Current currency (object, id_currency, NULL => context currency)
747     * @param bool $no_utf8 Not used anymore
748     * @param Context|null $context
749     *
750     * @return string Price correctly formatted (sign, decimal separator...)
751     *                if you modify this function, don't forget to modify the Javascript function formatCurrency (in tools.js)
752     *
753     * @throws LocalizationException
754     */
755    public static function displayPrice($price, $currency = null, $no_utf8 = false, Context $context = null)
756    {
757        @trigger_error(
758            'Tools::displayPrice() is deprecated since version 1.7.6.0. '
759            . 'Use ' . Locale::class . '::formatPrice() instead.',
760            E_USER_DEPRECATED
761        );
762
763        if (!is_numeric($price)) {
764            return $price;
765        }
766
767        $context = $context ?: Context::getContext();
768        $currency = $currency ?: $context->currency;
769
770        if (is_int($currency)) {
771            $currency = Currency::getCurrencyInstance($currency);
772        }
773
774        $locale = static::getContextLocale($context);
775        $currencyCode = is_array($currency) ? $currency['iso_code'] : $currency->iso_code;
776
777        return $locale->formatPrice($price, $currencyCode);
778    }
779
780    /**
781     * Return current locale
782     *
783     * @param Context $context
784     *
785     * @return Locale
786     *
787     * @throws Exception
788     */
789    public static function getContextLocale(Context $context)
790    {
791        $locale = $context->getCurrentLocale();
792        if (null !== $locale) {
793            return $locale;
794        }
795
796        $containerFinder = new ContainerFinder($context);
797        $container = $containerFinder->getContainer();
798        if (null === $context->container) {
799            $context->container = $container;
800        }
801
802        /** @var LocaleRepository $localeRepository */
803        $localeRepository = $container->get(self::SERVICE_LOCALE_REPOSITORY);
804        $locale = $localeRepository->getLocale(
805            $context->language->getLocale()
806        );
807
808        return $locale;
809    }
810
811    /**
812     * Returns a well formatted number.
813     *
814     * @deprecated Since 1.7.6.0. Please use Locale::formatNumber() instead
815     * @see Locale
816     *
817     * @param float $number The number to format
818     * @param null $currency not used anymore
819     *
820     * @return string The formatted number
821     *
822     * @throws Exception
823     * @throws LocalizationException
824     */
825    public static function displayNumber($number, $currency = null)
826    {
827        @trigger_error(
828            'Tools::displayNumber() is deprecated since version 1.7.5.0. '
829            . 'Use ' . Locale::class . ' instead.',
830            E_USER_DEPRECATED
831        );
832
833        $context = Context::getContext();
834        $locale = static::getContextLocale($context);
835
836        return $locale->formatNumber($number);
837    }
838
839    public static function displayPriceSmarty($params, &$smarty)
840    {
841        $context = Context::getContext();
842        $locale = static::getContextLocale($context);
843        if (array_key_exists('currency', $params)) {
844            $currency = Currency::getCurrencyInstance((int) $params['currency']);
845            if (Validate::isLoadedObject($currency)) {
846                return $locale->formatPrice($params['price'], $currency->iso_code);
847            }
848        }
849
850        return $locale->formatPrice($params['price'], $context->currency->iso_code);
851    }
852
853    /**
854     * Return price converted.
855     *
856     * @deprecated since 1.7.4 use convertPriceToCurrency()
857     *
858     * @param float|null $price Product price
859     * @param object|array $currency Current currency object
860     * @param bool $to_currency convert to currency or from currency to default currency
861     * @param Context $context
862     *
863     * @return float|null Price
864     */
865    public static function convertPrice($price, $currency = null, $to_currency = true, Context $context = null)
866    {
867        $default_currency = (int) Configuration::get('PS_CURRENCY_DEFAULT');
868
869        if (!$context) {
870            $context = Context::getContext();
871        }
872        if ($currency === null) {
873            $currency = $context->currency;
874        } elseif (is_numeric($currency)) {
875            $currency = Currency::getCurrencyInstance($currency);
876        }
877
878        $c_id = (is_array($currency) ? $currency['id_currency'] : $currency->id);
879        $c_rate = (is_array($currency) ? $currency['conversion_rate'] : $currency->conversion_rate);
880
881        if ($c_id != $default_currency) {
882            if ($to_currency) {
883                $price *= $c_rate;
884            } else {
885                $price /= $c_rate;
886            }
887        }
888
889        return $price;
890    }
891
892    /**
893     * Implement array_replace for PHP <= 5.2.
894     *
895     * @return array|mixed|null
896     *
897     * @deprecated since version 1.7.4.0, to be removed.
898     */
899    public static function array_replace()
900    {
901        Tools::displayAsDeprecated('Use PHP\'s array_replace() instead');
902        if (!function_exists('array_replace')) {
903            $args = func_get_args();
904            $num_args = func_num_args();
905            $res = [];
906            for ($i = 0; $i < $num_args; ++$i) {
907                if (is_array($args[$i])) {
908                    foreach ($args[$i] as $key => $val) {
909                        $res[$key] = $val;
910                    }
911                } else {
912                    trigger_error(__FUNCTION__ . '(): Argument #' . ($i + 1) . ' is not an array', E_USER_WARNING);
913
914                    return null;
915                }
916            }
917
918            return $res;
919        } else {
920            return call_user_func_array('array_replace', func_get_args());
921        }
922    }
923
924    /**
925     * Convert amount from a currency to an other currency automatically.
926     *
927     * @param float $amount
928     * @param Currency $currency_from if null we used the default currency
929     * @param Currency $currency_to if null we used the default currency
930     */
931    public static function convertPriceFull($amount, Currency $currency_from = null, Currency $currency_to = null)
932    {
933        if ($currency_from == $currency_to) {
934            return $amount;
935        }
936
937        if ($currency_from === null) {
938            $currency_from = new Currency(Configuration::get('PS_CURRENCY_DEFAULT'));
939        }
940
941        if ($currency_to === null) {
942            $currency_to = new Currency(Configuration::get('PS_CURRENCY_DEFAULT'));
943        }
944
945        if ($currency_from->id == Configuration::get('PS_CURRENCY_DEFAULT')) {
946            $amount *= $currency_to->conversion_rate;
947        } else {
948            $conversion_rate = ($currency_from->conversion_rate == 0 ? 1 : $currency_from->conversion_rate);
949            // Convert amount to default currency (using the old currency rate)
950            $amount = $amount / $conversion_rate;
951            // Convert to new currency
952            $amount *= $currency_to->conversion_rate;
953        }
954
955        return Tools::ps_round($amount, Context::getContext()->getComputingPrecision());
956    }
957
958    /**
959     * Display date regarding to language preferences.
960     *
961     * @param array $params Date, format...
962     * @param object $smarty Smarty object for language preferences
963     *
964     * @return string Date
965     */
966    public static function dateFormat($params, &$smarty)
967    {
968        return Tools::displayDate($params['date'], null, (isset($params['full']) ? $params['full'] : false));
969    }
970
971    /**
972     * Display date regarding to language preferences.
973     *
974     * @param string $date Date to display format UNIX
975     * @param int $id_lang Language id DEPRECATED
976     * @param bool $full With time or not (optional)
977     * @param string $separator DEPRECATED
978     *
979     * @return string Date
980     */
981    public static function displayDate($date, $id_lang = null, $full = false, $separator = null)
982    {
983        if ($id_lang !== null) {
984            Tools::displayParameterAsDeprecated('id_lang');
985        }
986        if ($separator !== null) {
987            Tools::displayParameterAsDeprecated('separator');
988        }
989
990        if (!$date || !($time = strtotime($date))) {
991            return $date;
992        }
993
994        if ($date == '0000-00-00 00:00:00' || $date == '0000-00-00') {
995            return '';
996        }
997
998        if (!Validate::isDate($date) || !Validate::isBool($full)) {
999            throw new PrestaShopException('Invalid date');
1000        }
1001
1002        $context = Context::getContext();
1003        $date_format = ($full ? $context->language->date_format_full : $context->language->date_format_lite);
1004
1005        return date($date_format, $time);
1006    }
1007
1008    /**
1009     * Get localized date format.
1010     *
1011     * @return string Date format
1012     */
1013    public static function getDateFormat()
1014    {
1015        $format = Context::getContext()->language->date_format_lite;
1016        $search = ['d', 'm', 'Y'];
1017        $replace = ['DD', 'MM', 'YYYY'];
1018        $format = str_replace($search, $replace, $format);
1019
1020        return $format;
1021    }
1022
1023    /**
1024     * Get formatted date.
1025     *
1026     * @param string $date_str Date string
1027     * @param bool $full With time or not (optional)
1028     *
1029     * @return string Formatted date
1030     */
1031    public static function formatDateStr($date_str, $full = false)
1032    {
1033        $time = strtotime($date_str);
1034        $context = Context::getContext();
1035        $date_format = ($full ? $context->language->date_format_full : $context->language->date_format_lite);
1036        $date = date($date_format, $time);
1037
1038        return $date;
1039    }
1040
1041    /**
1042     * Sanitize a string.
1043     *
1044     * @param string $string String to sanitize
1045     * @param bool $full String contains HTML or not (optional)
1046     *
1047     * @return string Sanitized string
1048     */
1049    public static function safeOutput($string, $html = false)
1050    {
1051        if (!$html) {
1052            $string = strip_tags($string);
1053        }
1054
1055        return @Tools::htmlentitiesUTF8($string, ENT_QUOTES);
1056    }
1057
1058    public static function htmlentitiesUTF8($string, $type = ENT_QUOTES)
1059    {
1060        if (is_array($string)) {
1061            return array_map(['Tools', 'htmlentitiesUTF8'], $string);
1062        }
1063
1064        return htmlentities((string) $string, $type, 'utf-8');
1065    }
1066
1067    public static function htmlentitiesDecodeUTF8($string)
1068    {
1069        if (is_array($string)) {
1070            $string = array_map(['Tools', 'htmlentitiesDecodeUTF8'], $string);
1071
1072            return (string) array_shift($string);
1073        }
1074
1075        return html_entity_decode((string) $string, ENT_QUOTES, 'utf-8');
1076    }
1077
1078    public static function safePostVars()
1079    {
1080        if (!isset($_POST) || !is_array($_POST)) {
1081            $_POST = [];
1082        } else {
1083            $_POST = array_map(['Tools', 'htmlentitiesUTF8'], $_POST);
1084        }
1085    }
1086
1087    /**
1088     * Delete directory and subdirectories.
1089     *
1090     * @param string $dirname Directory name
1091     */
1092    public static function deleteDirectory($dirname, $delete_self = true)
1093    {
1094        $dirname = rtrim($dirname, '/') . '/';
1095        if (file_exists($dirname)) {
1096            if ($files = scandir($dirname, SCANDIR_SORT_NONE)) {
1097                foreach ($files as $file) {
1098                    if ($file != '.' && $file != '..' && $file != '.svn') {
1099                        if (is_dir($dirname . $file)) {
1100                            Tools::deleteDirectory($dirname . $file);
1101                        } elseif (file_exists($dirname . $file)) {
1102                            unlink($dirname . $file);
1103                        }
1104                    }
1105                }
1106
1107                if ($delete_self && file_exists($dirname)) {
1108                    if (!rmdir($dirname)) {
1109                        return false;
1110                    }
1111                }
1112
1113                return true;
1114            }
1115        }
1116
1117        return false;
1118    }
1119
1120    /**
1121     * Delete file.
1122     *
1123     * @param string $file File path
1124     * @param array $exclude_files Excluded files
1125     *
1126     * @return bool
1127     */
1128    public static function deleteFile($file, $exclude_files = [])
1129    {
1130        if (isset($exclude_files) && !is_array($exclude_files)) {
1131            $exclude_files = [$exclude_files];
1132        }
1133
1134        if (file_exists($file) && is_file($file) && array_search(basename($file), $exclude_files) === false) {
1135            return unlink($file);
1136        }
1137
1138        return false;
1139    }
1140
1141    /**
1142     * Clear XML cache folder.
1143     */
1144    public static function clearXMLCache()
1145    {
1146        foreach (scandir(_PS_ROOT_DIR_ . '/config/xml', SCANDIR_SORT_NONE) as $file) {
1147            $path_info = pathinfo($file, PATHINFO_EXTENSION);
1148            if (($path_info == 'xml') && ($file != 'default.xml')) {
1149                self::deleteFile(_PS_ROOT_DIR_ . '/config/xml/' . $file);
1150            }
1151        }
1152    }
1153
1154    /**
1155     * Depending on _PS_MODE_DEV_ throws an exception or returns a error message.
1156     *
1157     * @param string|null $errorMessage Error message (defaults to "Fatal error")
1158     * @param bool $htmlentities DEPRECATED since 1.7.4.0
1159     * @param Context|null $context DEPRECATED since 1.7.4.0
1160     *
1161     * @return string
1162     *
1163     * @throws PrestaShopException If _PS_MODE_DEV_ is enabled
1164     */
1165    public static function displayError($errorMessage = null, $htmlentities = null, Context $context = null)
1166    {
1167        header('HTTP/1.1 500 Internal Server Error', true, 500);
1168        if (null !== $htmlentities) {
1169            self::displayParameterAsDeprecated('htmlentities');
1170        }
1171        if (null !== $context) {
1172            self::displayParameterAsDeprecated('context');
1173        }
1174
1175        if (null === $errorMessage) {
1176            $errorMessage = Context::getContext()
1177                ->getTranslator()
1178                ->trans('Fatal error', [], 'Admin.Notifications.Error');
1179        }
1180
1181        if (_PS_MODE_DEV_) {
1182            throw new PrestaShopException($errorMessage);
1183        }
1184
1185        return $errorMessage;
1186    }
1187
1188    /**
1189     * Display an error with detailed object.
1190     *
1191     * @param mixed $object
1192     * @param bool $kill
1193     *
1194     * @return $object if $kill = false;
1195     */
1196    public static function dieObject($object, $kill = true)
1197    {
1198        dump($object);
1199
1200        if ($kill) {
1201            die('END');
1202        }
1203
1204        return $object;
1205    }
1206
1207    public static function debug_backtrace($start = 0, $limit = null)
1208    {
1209        $backtrace = debug_backtrace();
1210        array_shift($backtrace);
1211        for ($i = 0; $i < $start; ++$i) {
1212            array_shift($backtrace);
1213        }
1214
1215        echo '
1216        <div style="margin:10px;padding:10px;border:1px solid #666666">
1217            <ul>';
1218        $i = 0;
1219        foreach ($backtrace as $id => $trace) {
1220            if ((int) $limit && (++$i > $limit)) {
1221                break;
1222            }
1223            $relative_file = (isset($trace['file'])) ? 'in /' . ltrim(str_replace([_PS_ROOT_DIR_, '\\'], ['', '/'], $trace['file']), '/') : '';
1224            $current_line = (isset($trace['line'])) ? ':' . $trace['line'] : '';
1225
1226            echo '<li>
1227                <b>' . ((isset($trace['class'])) ? $trace['class'] : '') . ((isset($trace['type'])) ? $trace['type'] : '') . $trace['function'] . '</b>
1228                ' . $relative_file . $current_line . '
1229            </li>';
1230        }
1231        echo '</ul>
1232        </div>';
1233    }
1234
1235    /**
1236     * Prints object information into error log.
1237     *
1238     * @see error_log()
1239     *
1240     * @param mixed $object
1241     * @param int|null $message_type
1242     * @param string|null $destination
1243     * @param string|null $extra_headers
1244     *
1245     * @return bool
1246     */
1247    public static function error_log($object, $message_type = null, $destination = null, $extra_headers = null)
1248    {
1249        return error_log(print_r($object, true), $message_type, $destination, $extra_headers);
1250    }
1251
1252    /**
1253     * Check if submit has been posted.
1254     *
1255     * @param string $submit submit name
1256     */
1257    public static function isSubmit($submit)
1258    {
1259        return
1260            isset($_POST[$submit]) || isset($_POST[$submit . '_x']) || isset($_POST[$submit . '_y'])
1261            || isset($_GET[$submit]) || isset($_GET[$submit . '_x']) || isset($_GET[$submit . '_y']);
1262    }
1263
1264    /**
1265     * Hash password.
1266     *
1267     * @param string $passwd String to hash
1268     *
1269     * @return string Hashed password
1270     *
1271     * @deprecated 1.7.0
1272     */
1273    public static function encrypt($passwd)
1274    {
1275        return self::hash($passwd);
1276    }
1277
1278    /**
1279     * Hash password.
1280     *
1281     * @param string $passwd String to has
1282     *
1283     * @return string Hashed password
1284     *
1285     * @since 1.7.0
1286     */
1287    public static function hash($passwd)
1288    {
1289        return md5(_COOKIE_KEY_ . $passwd);
1290    }
1291
1292    /**
1293     * Hash data string.
1294     *
1295     * @param string $data String to encrypt
1296     *
1297     * @return string Hashed IV
1298     *
1299     * @deprecated 1.7.0
1300     */
1301    public static function encryptIV($data)
1302    {
1303        return self::hashIV($data);
1304    }
1305
1306    /**
1307     * Hash data string.
1308     *
1309     * @param string $data String to encrypt
1310     *
1311     * @return string Hashed IV
1312     *
1313     * @since 1.7.0
1314     */
1315    public static function hashIV($data)
1316    {
1317        return md5(_COOKIE_IV_ . $data);
1318    }
1319
1320    /**
1321     * Get token to prevent CSRF.
1322     *
1323     * @param string $token token to encrypt
1324     *
1325     * @return string
1326     */
1327    public static function getToken($page = true, Context $context = null)
1328    {
1329        if (!$context) {
1330            $context = Context::getContext();
1331        }
1332        if ($page === true) {
1333            return Tools::hash($context->customer->id . $context->customer->passwd . $_SERVER['SCRIPT_NAME']);
1334        } else {
1335            return Tools::hash($context->customer->id . $context->customer->passwd . $page);
1336        }
1337    }
1338
1339    /**
1340     * Tokenize a string.
1341     *
1342     * @param string $string String to encrypt
1343     *
1344     * @return string|bool false if given string is empty
1345     */
1346    public static function getAdminToken($string)
1347    {
1348        return !empty($string) ? Tools::hash($string) : false;
1349    }
1350
1351    /**
1352     * @param string $tab
1353     * @param Context $context
1354     *
1355     * @return bool|string
1356     */
1357    public static function getAdminTokenLite($tab, Context $context = null)
1358    {
1359        if (!$context) {
1360            $context = Context::getContext();
1361        }
1362
1363        return Tools::getAdminToken($tab . (int) Tab::getIdFromClassName($tab) . (int) $context->employee->id);
1364    }
1365
1366    /**
1367     * @param array $params
1368     * @param $smarty unused parameter, please ignore (@todo: remove in next major)
1369     *
1370     * @return bool|string
1371     */
1372    public static function getAdminTokenLiteSmarty($params, &$smarty = null)
1373    {
1374        $context = Context::getContext();
1375
1376        return Tools::getAdminToken($params['tab'] . (int) Tab::getIdFromClassName($params['tab']) . (int) $context->employee->id);
1377    }
1378
1379    /**
1380     * Get a valid URL to use from BackOffice.
1381     *
1382     * @param string $url An URL to use in BackOffice
1383     * @param bool $entities Set to true to use htmlentities function on URL param
1384     *
1385     * @return string
1386     */
1387    public static function getAdminUrl($url = null, $entities = false)
1388    {
1389        $link = Tools::getHttpHost(true) . __PS_BASE_URI__;
1390
1391        if (isset($url)) {
1392            $link .= ($entities ? Tools::htmlentitiesUTF8($url) : $url);
1393        }
1394
1395        return $link;
1396    }
1397
1398    /**
1399     * Get a valid image URL to use from BackOffice.
1400     *
1401     * @param string $image Image name
1402     * @param bool $entities Set to true to use htmlentities function on image param
1403     *
1404     * @return string
1405     */
1406    public static function getAdminImageUrl($image = null, $entities = false)
1407    {
1408        return Tools::getAdminUrl(basename(_PS_IMG_DIR_) . '/' . $image, $entities);
1409    }
1410
1411    /**
1412     * Return the friendly url from the provided string.
1413     *
1414     * @param string $str
1415     * @param bool $utf8_decode (deprecated)
1416     *
1417     * @return string
1418     */
1419    public static function link_rewrite($str, $utf8_decode = null)
1420    {
1421        if ($utf8_decode !== null) {
1422            Tools::displayParameterAsDeprecated('utf8_decode');
1423        }
1424
1425        return Tools::str2url($str);
1426    }
1427
1428    /**
1429     * Return a friendly url made from the provided string
1430     * If the mbstring library is available, the output is the same as the js function of the same name.
1431     *
1432     * @param string $str
1433     *
1434     * @return string|bool
1435     */
1436    public static function str2url($str)
1437    {
1438        static $array_str = [];
1439        static $allow_accented_chars = null;
1440        static $has_mb_strtolower = null;
1441
1442        if ($has_mb_strtolower === null) {
1443            $has_mb_strtolower = function_exists('mb_strtolower');
1444        }
1445
1446        if (!is_string($str)) {
1447            return false;
1448        }
1449
1450        if (isset($array_str[$str])) {
1451            return $array_str[$str];
1452        }
1453
1454        if ($str == '') {
1455            return '';
1456        }
1457
1458        if ($allow_accented_chars === null) {
1459            $allow_accented_chars = Configuration::get('PS_ALLOW_ACCENTED_CHARS_URL');
1460        }
1461
1462        $return_str = trim($str);
1463
1464        if ($has_mb_strtolower) {
1465            $return_str = mb_strtolower($return_str, 'utf-8');
1466        }
1467        if (!$allow_accented_chars) {
1468            $return_str = Tools::replaceAccentedChars($return_str);
1469        }
1470
1471        // Remove all non-whitelist chars.
1472        if ($allow_accented_chars) {
1473            $return_str = preg_replace('/[^a-zA-Z0-9\s\'\:\/\[\]\-\p{L}]/u', '', $return_str);
1474        } else {
1475            $return_str = preg_replace('/[^a-zA-Z0-9\s\'\:\/\[\]\-]/', '', $return_str);
1476        }
1477
1478        $return_str = preg_replace('/[\s\'\:\/\[\]\-]+/', ' ', $return_str);
1479        $return_str = str_replace([' ', '/'], '-', $return_str);
1480
1481        // If it was not possible to lowercase the string with mb_strtolower, we do it after the transformations.
1482        // This way we lose fewer special chars.
1483        if (!$has_mb_strtolower) {
1484            $return_str = Tools::strtolower($return_str);
1485        }
1486
1487        $array_str[$str] = $return_str;
1488
1489        return $return_str;
1490    }
1491
1492    /**
1493     * Replace all accented chars by their equivalent non accented chars.
1494     *
1495     * @param string $str
1496     *
1497     * @return string
1498     */
1499    public static function replaceAccentedChars($str)
1500    {
1501        /* One source among others:
1502            http://www.tachyonsoft.com/uc0000.htm
1503            http://www.tachyonsoft.com/uc0001.htm
1504            http://www.tachyonsoft.com/uc0004.htm
1505        */
1506        $patterns = [
1507            /* Lowercase */
1508            /* a  */ '/[\x{00E0}\x{00E1}\x{00E2}\x{00E3}\x{00E4}\x{00E5}\x{0101}\x{0103}\x{0105}\x{0430}\x{00C0}-\x{00C3}\x{1EA0}-\x{1EB7}]/u',
1509            /* b  */ '/[\x{0431}]/u',
1510            /* c  */ '/[\x{00E7}\x{0107}\x{0109}\x{010D}\x{0446}]/u',
1511            /* d  */ '/[\x{010F}\x{0111}\x{0434}\x{0110}\x{00F0}]/u',
1512            /* e  */ '/[\x{00E8}\x{00E9}\x{00EA}\x{00EB}\x{0113}\x{0115}\x{0117}\x{0119}\x{011B}\x{0435}\x{044D}\x{00C8}-\x{00CA}\x{1EB8}-\x{1EC7}]/u',
1513            /* f  */ '/[\x{0444}]/u',
1514            /* g  */ '/[\x{011F}\x{0121}\x{0123}\x{0433}\x{0491}]/u',
1515            /* h  */ '/[\x{0125}\x{0127}]/u',
1516            /* i  */ '/[\x{00EC}\x{00ED}\x{00EE}\x{00EF}\x{0129}\x{012B}\x{012D}\x{012F}\x{0131}\x{0438}\x{0456}\x{00CC}\x{00CD}\x{1EC8}-\x{1ECB}\x{0128}]/u',
1517            /* j  */ '/[\x{0135}\x{0439}]/u',
1518            /* k  */ '/[\x{0137}\x{0138}\x{043A}]/u',
1519            /* l  */ '/[\x{013A}\x{013C}\x{013E}\x{0140}\x{0142}\x{043B}]/u',
1520            /* m  */ '/[\x{043C}]/u',
1521            /* n  */ '/[\x{00F1}\x{0144}\x{0146}\x{0148}\x{0149}\x{014B}\x{043D}]/u',
1522            /* o  */ '/[\x{00F2}\x{00F3}\x{00F4}\x{00F5}\x{00F6}\x{00F8}\x{014D}\x{014F}\x{0151}\x{043E}\x{00D2}-\x{00D5}\x{01A0}\x{01A1}\x{1ECC}-\x{1EE3}]/u',
1523            /* p  */ '/[\x{043F}]/u',
1524            /* r  */ '/[\x{0155}\x{0157}\x{0159}\x{0440}]/u',
1525            /* s  */ '/[\x{015B}\x{015D}\x{015F}\x{0161}\x{0441}]/u',
1526            /* ss */ '/[\x{00DF}]/u',
1527            /* t  */ '/[\x{0163}\x{0165}\x{0167}\x{0442}]/u',
1528            /* u  */ '/[\x{00F9}\x{00FA}\x{00FB}\x{00FC}\x{0169}\x{016B}\x{016D}\x{016F}\x{0171}\x{0173}\x{0443}\x{00D9}-\x{00DA}\x{0168}\x{01AF}\x{01B0}\x{1EE4}-\x{1EF1}]/u',
1529            /* v  */ '/[\x{0432}]/u',
1530            /* w  */ '/[\x{0175}]/u',
1531            /* y  */ '/[\x{00FF}\x{0177}\x{00FD}\x{044B}\x{1EF2}-\x{1EF9}\x{00DD}]/u',
1532            /* z  */ '/[\x{017A}\x{017C}\x{017E}\x{0437}]/u',
1533            /* ae */ '/[\x{00E6}]/u',
1534            /* ch */ '/[\x{0447}]/u',
1535            /* kh */ '/[\x{0445}]/u',
1536            /* oe */ '/[\x{0153}]/u',
1537            /* sh */ '/[\x{0448}]/u',
1538            /* shh*/ '/[\x{0449}]/u',
1539            /* ya */ '/[\x{044F}]/u',
1540            /* ye */ '/[\x{0454}]/u',
1541            /* yi */ '/[\x{0457}]/u',
1542            /* yo */ '/[\x{0451}]/u',
1543            /* yu */ '/[\x{044E}]/u',
1544            /* zh */ '/[\x{0436}]/u',
1545
1546            /* Uppercase */
1547            /* A  */ '/[\x{0100}\x{0102}\x{0104}\x{00C0}\x{00C1}\x{00C2}\x{00C3}\x{00C4}\x{00C5}\x{0410}]/u',
1548            /* B  */ '/[\x{0411}]/u',
1549            /* C  */ '/[\x{00C7}\x{0106}\x{0108}\x{010A}\x{010C}\x{0426}]/u',
1550            /* D  */ '/[\x{010E}\x{0110}\x{0414}\x{00D0}]/u',
1551            /* E  */ '/[\x{00C8}\x{00C9}\x{00CA}\x{00CB}\x{0112}\x{0114}\x{0116}\x{0118}\x{011A}\x{0415}\x{042D}]/u',
1552            /* F  */ '/[\x{0424}]/u',
1553            /* G  */ '/[\x{011C}\x{011E}\x{0120}\x{0122}\x{0413}\x{0490}]/u',
1554            /* H  */ '/[\x{0124}\x{0126}]/u',
1555            /* I  */ '/[\x{0128}\x{012A}\x{012C}\x{012E}\x{0130}\x{0418}\x{0406}]/u',
1556            /* J  */ '/[\x{0134}\x{0419}]/u',
1557            /* K  */ '/[\x{0136}\x{041A}]/u',
1558            /* L  */ '/[\x{0139}\x{013B}\x{013D}\x{0139}\x{0141}\x{041B}]/u',
1559            /* M  */ '/[\x{041C}]/u',
1560            /* N  */ '/[\x{00D1}\x{0143}\x{0145}\x{0147}\x{014A}\x{041D}]/u',
1561            /* O  */ '/[\x{00D3}\x{014C}\x{014E}\x{0150}\x{041E}]/u',
1562            /* P  */ '/[\x{041F}]/u',
1563            /* R  */ '/[\x{0154}\x{0156}\x{0158}\x{0420}]/u',
1564            /* S  */ '/[\x{015A}\x{015C}\x{015E}\x{0160}\x{0421}]/u',
1565            /* T  */ '/[\x{0162}\x{0164}\x{0166}\x{0422}]/u',
1566            /* U  */ '/[\x{00D9}\x{00DA}\x{00DB}\x{00DC}\x{0168}\x{016A}\x{016C}\x{016E}\x{0170}\x{0172}\x{0423}]/u',
1567            /* V  */ '/[\x{0412}]/u',
1568            /* W  */ '/[\x{0174}]/u',
1569            /* Y  */ '/[\x{0176}\x{042B}]/u',
1570            /* Z  */ '/[\x{0179}\x{017B}\x{017D}\x{0417}]/u',
1571            /* AE */ '/[\x{00C6}]/u',
1572            /* CH */ '/[\x{0427}]/u',
1573            /* KH */ '/[\x{0425}]/u',
1574            /* OE */ '/[\x{0152}]/u',
1575            /* SH */ '/[\x{0428}]/u',
1576            /* SHH*/ '/[\x{0429}]/u',
1577            /* YA */ '/[\x{042F}]/u',
1578            /* YE */ '/[\x{0404}]/u',
1579            /* YI */ '/[\x{0407}]/u',
1580            /* YO */ '/[\x{0401}]/u',
1581            /* YU */ '/[\x{042E}]/u',
1582            /* ZH */ '/[\x{0416}]/u',
1583        ];
1584
1585        // ö to oe
1586        // å to aa
1587        // ä to ae
1588
1589        $replacements = [
1590            'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'r', 's', 'ss', 't', 'u', 'v', 'w', 'y', 'z', 'ae', 'ch', 'kh', 'oe', 'sh', 'shh', 'ya', 'ye', 'yi', 'yo', 'yu', 'zh',
1591            'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'R', 'S', 'T', 'U', 'V', 'W', 'Y', 'Z', 'AE', 'CH', 'KH', 'OE', 'SH', 'SHH', 'YA', 'YE', 'YI', 'YO', 'YU', 'ZH',
1592        ];
1593
1594        return preg_replace($patterns, $replacements, $str);
1595    }
1596
1597    /**
1598     * Truncate strings.
1599     *
1600     * @param string $str
1601     * @param int $max_length Max length
1602     * @param string $suffix Suffix optional
1603     *
1604     * @return string $str truncated
1605     */
1606    /* CAUTION : Use it only on module hookEvents.
1607    ** For other purposes use the smarty function instead */
1608    public static function truncate($str, $max_length, $suffix = '...')
1609    {
1610        if (Tools::strlen($str) <= $max_length) {
1611            return $str;
1612        }
1613        $str = utf8_decode($str);
1614
1615        return utf8_encode(substr($str, 0, $max_length - Tools::strlen($suffix)) . $suffix);
1616    }
1617
1618    /*Copied from CakePHP String utility file*/
1619    public static function truncateString($text, $length = 120, $options = [])
1620    {
1621        $default = [
1622            'ellipsis' => '...', 'exact' => true, 'html' => true,
1623        ];
1624
1625        $options = array_merge($default, $options);
1626        extract($options);
1627        /**
1628         * @var string
1629         * @var bool $exact
1630         * @var bool $html
1631         */
1632        if ($html) {
1633            if (Tools::strlen(preg_replace('/<.*?>/', '', $text)) <= $length) {
1634                return $text;
1635            }
1636
1637            $total_length = Tools::strlen(strip_tags($ellipsis));
1638            $open_tags = [];
1639            $truncate = '';
1640            preg_match_all('/(<\/?([\w+]+)[^>]*>)?([^<>]*)/', $text, $tags, PREG_SET_ORDER);
1641
1642            foreach ($tags as $tag) {
1643                if (!preg_match('/img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param/s', $tag[2])) {
1644                    if (preg_match('/<[\w]+[^>]*>/s', $tag[0])) {
1645                        array_unshift($open_tags, $tag[2]);
1646                    } elseif (preg_match('/<\/([\w]+)[^>]*>/s', $tag[0], $close_tag)) {
1647                        $pos = array_search($close_tag[1], $open_tags);
1648                        if ($pos !== false) {
1649                            array_splice($open_tags, $pos, 1);
1650                        }
1651                    }
1652                }
1653                $truncate .= $tag[1];
1654                $content_length = Tools::strlen(preg_replace('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', ' ', $tag[3]));
1655
1656                if ($content_length + $total_length > $length) {
1657                    $left = $length - $total_length;
1658                    $entities_length = 0;
1659
1660                    if (preg_match_all('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', $tag[3], $entities, PREG_OFFSET_CAPTURE)) {
1661                        foreach ($entities[0] as $entity) {
1662                            if ($entity[1] + 1 - $entities_length <= $left) {
1663                                --$left;
1664                                $entities_length += Tools::strlen($entity[0]);
1665                            } else {
1666                                break;
1667                            }
1668                        }
1669                    }
1670
1671                    $truncate .= Tools::substr($tag[3], 0, $left + $entities_length);
1672
1673                    break;
1674                } else {
1675                    $truncate .= $tag[3];
1676                    $total_length += $content_length;
1677                }
1678
1679                if ($total_length >= $length) {
1680                    break;
1681                }
1682            }
1683        } else {
1684            if (Tools::strlen($text) <= $length) {
1685                return $text;
1686            }
1687
1688            $truncate = Tools::substr($text, 0, $length - Tools::strlen($ellipsis));
1689        }
1690
1691        if (!$exact) {
1692            $spacepos = Tools::strrpos($truncate, ' ');
1693            if ($html) {
1694                $truncate_check = Tools::substr($truncate, 0, $spacepos);
1695                $last_open_tag = Tools::strrpos($truncate_check, '<');
1696                $last_close_tag = Tools::strrpos($truncate_check, '>');
1697
1698                if ($last_open_tag > $last_close_tag) {
1699                    preg_match_all('/<[\w]+[^>]*>/s', $truncate, $last_tag_matches);
1700                    $last_tag = array_pop($last_tag_matches[0]);
1701                    $spacepos = Tools::strrpos($truncate, $last_tag) + Tools::strlen($last_tag);
1702                }
1703
1704                $bits = Tools::substr($truncate, $spacepos);
1705                preg_match_all('/<\/([a-z]+)>/', $bits, $dropped_tags, PREG_SET_ORDER);
1706
1707                if (!empty($dropped_tags)) {
1708                    if (!empty($open_tags)) {
1709                        foreach ($dropped_tags as $closing_tag) {
1710                            if (!in_array($closing_tag[1], $open_tags)) {
1711                                array_unshift($open_tags, $closing_tag[1]);
1712                            }
1713                        }
1714                    } else {
1715                        foreach ($dropped_tags as $closing_tag) {
1716                            $open_tags[] = $closing_tag[1];
1717                        }
1718                    }
1719                }
1720            }
1721
1722            $truncate = Tools::substr($truncate, 0, $spacepos);
1723        }
1724
1725        $truncate .= $ellipsis;
1726
1727        if ($html) {
1728            foreach ($open_tags as $tag) {
1729                $truncate .= '</' . $tag . '>';
1730            }
1731        }
1732
1733        return $truncate;
1734    }
1735
1736    public static function normalizeDirectory($directory)
1737    {
1738        return rtrim($directory, '/\\') . DIRECTORY_SEPARATOR;
1739    }
1740
1741    /**
1742     * Generate date form.
1743     *
1744     * @param int $year Year to select
1745     * @param int $month Month to select
1746     * @param int $day Day to select
1747     *
1748     * @return array $tab html data with 3 cells :['days'], ['months'], ['years']
1749     */
1750    public static function dateYears()
1751    {
1752        $tab = [];
1753        for ($i = date('Y'); $i >= 1900; --$i) {
1754            $tab[] = $i;
1755        }
1756
1757        return $tab;
1758    }
1759
1760    public static function dateDays()
1761    {
1762        $tab = [];
1763        for ($i = 1; $i != 32; ++$i) {
1764            $tab[] = $i;
1765        }
1766
1767        return $tab;
1768    }
1769
1770    public static function dateMonths()
1771    {
1772        $tab = [];
1773        for ($i = 1; $i != 13; ++$i) {
1774            $tab[$i] = date('F', mktime(0, 0, 0, $i, date('m'), date('Y')));
1775        }
1776
1777        return $tab;
1778    }
1779
1780    public static function hourGenerate($hours, $minutes, $seconds)
1781    {
1782        return implode(':', [$hours, $minutes, $seconds]);
1783    }
1784
1785    public static function dateFrom($date)
1786    {
1787        $tab = explode(' ', $date);
1788        if (!isset($tab[1])) {
1789            $date .= ' ' . Tools::hourGenerate(0, 0, 0);
1790        }
1791
1792        return $date;
1793    }
1794
1795    public static function dateTo($date)
1796    {
1797        $tab = explode(' ', $date);
1798        if (!isset($tab[1])) {
1799            $date .= ' ' . Tools::hourGenerate(23, 59, 59);
1800        }
1801
1802        return $date;
1803    }
1804
1805    public static function strtolower($str)
1806    {
1807        if (is_array($str)) {
1808            return false;
1809        }
1810        if (function_exists('mb_strtolower')) {
1811            return mb_strtolower($str, 'utf-8');
1812        }
1813
1814        return strtolower($str);
1815    }
1816
1817    public static function strlen($str, $encoding = 'UTF-8')
1818    {
1819        if (is_array($str)) {
1820            return false;
1821        }
1822        $str = html_entity_decode($str, ENT_COMPAT, 'UTF-8');
1823        if (function_exists('mb_strlen')) {
1824            return mb_strlen($str, $encoding);
1825        }
1826
1827        return strlen($str);
1828    }
1829
1830    public static function stripslashes($string)
1831    {
1832        return $string;
1833    }
1834
1835    public static function strtoupper($str)
1836    {
1837        if (is_array($str)) {
1838            return false;
1839        }
1840        if (function_exists('mb_strtoupper')) {
1841            return mb_strtoupper($str, 'utf-8');
1842        }
1843
1844        return strtoupper($str);
1845    }
1846
1847    public static function substr($str, $start, $length = false, $encoding = 'utf-8')
1848    {
1849        if (is_array($str)) {
1850            return false;
1851        }
1852        if (function_exists('mb_substr')) {
1853            return mb_substr($str, (int) $start, ($length === false ? Tools::strlen($str) : (int) $length), $encoding);
1854        }
1855
1856        return substr($str, $start, ($length === false ? Tools::strlen($str) : (int) $length));
1857    }
1858
1859    public static function strpos($str, $find, $offset = 0, $encoding = 'UTF-8')
1860    {
1861        if (function_exists('mb_strpos')) {
1862            return mb_strpos($str, $find, $offset, $encoding);
1863        }
1864
1865        return strpos($str, $find, $offset);
1866    }
1867
1868    public static function strrpos($str, $find, $offset = 0, $encoding = 'utf-8')
1869    {
1870        if (function_exists('mb_strrpos')) {
1871            return mb_strrpos($str, $find, $offset, $encoding);
1872        }
1873
1874        return strrpos($str, $find, $offset);
1875    }
1876
1877    public static function ucfirst($str)
1878    {
1879        return Tools::strtoupper(Tools::substr($str, 0, 1)) . Tools::substr($str, 1);
1880    }
1881
1882    public static function ucwords($str)
1883    {
1884        if (function_exists('mb_convert_case')) {
1885            return mb_convert_case($str, MB_CASE_TITLE);
1886        }
1887
1888        return ucwords(Tools::strtolower($str));
1889    }
1890
1891    public static function orderbyPrice(&$array, $order_way)
1892    {
1893        foreach ($array as &$row) {
1894            $row['price_tmp'] = Product::getPriceStatic($row['id_product'], true, ((isset($row['id_product_attribute']) && !empty($row['id_product_attribute'])) ? (int) $row['id_product_attribute'] : null), 2);
1895        }
1896        unset($row);
1897
1898        if (Tools::strtolower($order_way) == 'desc') {
1899            uasort($array, 'cmpPriceDesc');
1900        } else {
1901            uasort($array, 'cmpPriceAsc');
1902        }
1903        foreach ($array as &$row) {
1904            unset($row['price_tmp']);
1905        }
1906    }
1907
1908    public static function iconv($from, $to, $string)
1909    {
1910        if (function_exists('iconv')) {
1911            return iconv($from, $to . '//TRANSLIT', str_replace('¥', '&yen;', str_replace('£', '&pound;', str_replace('€', '&euro;', $string))));
1912        }
1913
1914        return html_entity_decode(htmlentities($string, ENT_NOQUOTES, $from), ENT_NOQUOTES, $to);
1915    }
1916
1917    public static function isEmpty($field)
1918    {
1919        return $field === '' || $field === null;
1920    }
1921
1922    /**
1923     * returns the rounded value of $value to specified precision, according to your configuration;.
1924     *
1925     * @note : PHP 5.3.0 introduce a 3rd parameter mode in round function
1926     *
1927     * @param float $value
1928     * @param int $precision
1929     *
1930     * @return float
1931     */
1932    public static function ps_round($value, $precision = 0, $round_mode = null)
1933    {
1934        if ($round_mode === null) {
1935            if (Tools::$round_mode == null) {
1936                Tools::$round_mode = (int) Configuration::get('PS_PRICE_ROUND_MODE');
1937            }
1938
1939            $round_mode = Tools::$round_mode;
1940        }
1941
1942        switch ($round_mode) {
1943            case PS_ROUND_UP:
1944                return Tools::ceilf($value, $precision);
1945            case PS_ROUND_DOWN:
1946                return Tools::floorf($value, $precision);
1947            case PS_ROUND_HALF_DOWN:
1948            case PS_ROUND_HALF_EVEN:
1949            case PS_ROUND_HALF_ODD:
1950                return Tools::math_round($value, $precision, $round_mode);
1951            case PS_ROUND_HALF_UP:
1952            default:
1953                return Tools::math_round($value, $precision, PS_ROUND_HALF_UP);
1954        }
1955    }
1956
1957    /**
1958     * @param $value
1959     * @param $places
1960     * @param int $mode
1961     *
1962     * @return false|float
1963     */
1964    public static function math_round($value, $places, $mode = PS_ROUND_HALF_UP)
1965    {
1966        //If PHP_ROUND_HALF_UP exist (PHP 5.3) use it and pass correct mode value (PrestaShop define - 1)
1967        if (defined('PHP_ROUND_HALF_UP')) {
1968            return round($value, $places, $mode - 1);
1969        }
1970
1971        $precision_places = 14 - floor(log10(abs($value)));
1972        $f1 = 10.0 ** (float) abs($places);
1973
1974        /* If the decimal precision guaranteed by FP arithmetic is higher than
1975        * the requested places BUT is small enough to make sure a non-zero value
1976        * is returned, pre-round the result to the precision */
1977        if ($precision_places > $places && $precision_places - $places < 15) {
1978            $f2 = 10.0 ** (float) abs($precision_places);
1979
1980            if ($precision_places >= 0) {
1981                $tmp_value = $value * $f2;
1982            } else {
1983                $tmp_value = $value / $f2;
1984            }
1985
1986            /* preround the result (tmp_value will always be something * 1e14,
1987            * thus never larger than 1e15 here) */
1988            $tmp_value = Tools::round_helper($tmp_value, $mode);
1989            /* now correctly move the decimal point */
1990            $f2 = 10.0 ** (float) abs($places - $precision_places);
1991            /* because places < precision_places */
1992            $tmp_value = $tmp_value / $f2;
1993        } else {
1994            /* adjust the value */
1995            if ($places >= 0) {
1996                $tmp_value = $value * $f1;
1997            } else {
1998                $tmp_value = $value / $f1;
1999            }
2000
2001            /* This value is beyond our precision, so rounding it is pointless */
2002            if (abs($tmp_value) >= 1e15) {
2003                return $value;
2004            }
2005        }
2006
2007        /* round the temp value */
2008        $tmp_value = Tools::round_helper($tmp_value, $mode);
2009
2010        /* see if it makes sense to use simple division to round the value */
2011        if (abs($places) < 23) {
2012            if ($places > 0) {
2013                $tmp_value /= $f1;
2014            } else {
2015                $tmp_value *= $f1;
2016            }
2017        }
2018
2019        return $tmp_value;
2020    }
2021
2022    /**
2023     * @param $value
2024     * @param $mode
2025     *
2026     * @return float
2027     */
2028    public static function round_helper($value, $mode)
2029    {
2030        if ($value >= 0.0) {
2031            $tmp_value = floor($value + 0.5);
2032
2033            if (($mode == PS_ROUND_HALF_DOWN && $value == (-0.5 + $tmp_value)) ||
2034                ($mode == PS_ROUND_HALF_EVEN && $value == (0.5 + 2 * floor($tmp_value / 2.0))) ||
2035                ($mode == PS_ROUND_HALF_ODD && $value == (0.5 + 2 * floor($tmp_value / 2.0) - 1.0))) {
2036                $tmp_value = $tmp_value - 1.0;
2037            }
2038        } else {
2039            $tmp_value = ceil($value - 0.5);
2040
2041            if (($mode == PS_ROUND_HALF_DOWN && $value == (0.5 + $tmp_value)) ||
2042                ($mode == PS_ROUND_HALF_EVEN && $value == (-0.5 + 2 * ceil($tmp_value / 2.0))) ||
2043                ($mode == PS_ROUND_HALF_ODD && $value == (-0.5 + 2 * ceil($tmp_value / 2.0) + 1.0))) {
2044                $tmp_value = $tmp_value + 1.0;
2045            }
2046        }
2047
2048        return $tmp_value;
2049    }
2050
2051    /**
2052     * Returns the rounded value up of $value to specified precision.
2053     *
2054     * @param float $value
2055     * @param int $precision
2056     *
2057     * @return float
2058     */
2059    public static function ceilf($value, $precision = 0)
2060    {
2061        $precision_factor = $precision == 0 ? 1 : 10 ** $precision;
2062        $tmp = $value * $precision_factor;
2063        $tmp2 = (string) $tmp;
2064        // If the current value has already the desired precision
2065        if (strpos($tmp2, '.') === false) {
2066            return $value;
2067        }
2068        if ($tmp2[strlen($tmp2) - 1] == 0) {
2069            return $value;
2070        }
2071
2072        return ceil($tmp) / $precision_factor;
2073    }
2074
2075    /**
2076     * Returns the rounded value down of $value to specified precision.
2077     *
2078     * @param float $value
2079     * @param int $precision
2080     *
2081     * @return float
2082     */
2083    public static function floorf($value, $precision = 0)
2084    {
2085        $precision_factor = $precision == 0 ? 1 : 10 ** $precision;
2086        $tmp = $value * $precision_factor;
2087        $tmp2 = (string) $tmp;
2088        // If the current value has already the desired precision
2089        if (strpos($tmp2, '.') === false) {
2090            return $value;
2091        }
2092        if ($tmp2[strlen($tmp2) - 1] == 0) {
2093            return $value;
2094        }
2095
2096        return floor($tmp) / $precision_factor;
2097    }
2098
2099    /**
2100     * file_exists() wrapper with cache to speedup performance.
2101     *
2102     * @param string $filename File name
2103     *
2104     * @return bool Cached result of file_exists($filename)
2105     */
2106    public static function file_exists_cache($filename)
2107    {
2108        if (!isset(self::$file_exists_cache[$filename])) {
2109            self::$file_exists_cache[$filename] = file_exists($filename);
2110        }
2111
2112        return self::$file_exists_cache[$filename];
2113    }
2114
2115    /**
2116     * file_exists() wrapper with a call to clearstatcache prior.
2117     *
2118     * @param string $filename File name
2119     *
2120     * @return bool Cached result of file_exists($filename)
2121     */
2122    public static function file_exists_no_cache($filename)
2123    {
2124        clearstatcache();
2125
2126        return file_exists($filename);
2127    }
2128
2129    /**
2130     * Refresh local CACert file.
2131     */
2132    public static function refreshCACertFile()
2133    {
2134        if ((time() - @filemtime(_PS_CACHE_CA_CERT_FILE_) > 1296000)) {
2135            $stream_context = @stream_context_create(
2136                [
2137                    'http' => ['timeout' => 3],
2138                    'ssl' => [
2139                        'cafile' => CaBundle::getBundledCaBundlePath(),
2140                    ],
2141                ]
2142            );
2143
2144            $ca_cert_content = @file_get_contents(Tools::CACERT_LOCATION, false, $stream_context);
2145            if (empty($ca_cert_content)) {
2146                $ca_cert_content = @file_get_contents(CaBundle::getBundledCaBundlePath());
2147            }
2148
2149            if (
2150                preg_match('/(.*-----BEGIN CERTIFICATE-----.*-----END CERTIFICATE-----){50}$/Uims', $ca_cert_content) &&
2151                substr(rtrim($ca_cert_content), -1) == '-'
2152            ) {
2153                file_put_contents(_PS_CACHE_CA_CERT_FILE_, $ca_cert_content);
2154            }
2155        }
2156    }
2157
2158    /**
2159     * @param string $url
2160     * @param int $curl_timeout
2161     * @param array $opts
2162     *
2163     * @return bool|string
2164     *
2165     * @throws Exception
2166     */
2167    private static function file_get_contents_curl(
2168        $url,
2169        $curl_timeout,
2170        $opts
2171    ) {
2172        $content = false;
2173
2174        if (function_exists('curl_init')) {
2175            Tools::refreshCACertFile();
2176            $curl = curl_init();
2177
2178            curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
2179            curl_setopt($curl, CURLOPT_URL, $url);
2180            curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 5);
2181            curl_setopt($curl, CURLOPT_TIMEOUT, $curl_timeout);
2182            curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, true);
2183            curl_setopt($curl, CURLOPT_CAINFO, _PS_CACHE_CA_CERT_FILE_);
2184            curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
2185            curl_setopt($curl, CURLOPT_MAXREDIRS, 5);
2186
2187            if ($opts != null) {
2188                if (isset($opts['http']['method']) && Tools::strtolower($opts['http']['method']) == 'post') {
2189                    curl_setopt($curl, CURLOPT_POST, true);
2190                    if (isset($opts['http']['content'])) {
2191                        parse_str($opts['http']['content'], $post_data);
2192                        curl_setopt($curl, CURLOPT_POSTFIELDS, $post_data);
2193                    }
2194                }
2195            }
2196
2197            $content = curl_exec($curl);
2198
2199            if (false === $content && _PS_MODE_DEV_) {
2200                $errorMessage = sprintf('file_get_contents_curl failed to download %s : (error code %d) %s',
2201                    $url,
2202                    curl_errno($curl),
2203                    curl_error($curl)
2204                );
2205
2206                throw new \Exception($errorMessage);
2207            }
2208
2209            curl_close($curl);
2210        }
2211
2212        return $content;
2213    }
2214
2215    private static function file_get_contents_fopen(
2216        $url,
2217        $use_include_path,
2218        $stream_context
2219    ) {
2220        $content = false;
2221
2222        if (in_array(ini_get('allow_url_fopen'), ['On', 'on', '1'])) {
2223            $content = @file_get_contents($url, $use_include_path, $stream_context);
2224        }
2225
2226        return $content;
2227    }
2228
2229    /**
2230     * This method allows to get the content from either a URL or a local file.
2231     *
2232     * @param string $url the url to get the content from
2233     * @param bool $use_include_path second parameter of http://php.net/manual/en/function.file-get-contents.php
2234     * @param resource $stream_context third parameter of http://php.net/manual/en/function.file-get-contents.php
2235     * @param int $curl_timeout
2236     * @param bool $fallback whether or not to use the fallback if the main solution fails
2237     *
2238     * @return bool|string false or the string content
2239     */
2240    public static function file_get_contents(
2241        $url,
2242        $use_include_path = false,
2243        $stream_context = null,
2244        $curl_timeout = 5,
2245        $fallback = false
2246    ) {
2247        $is_local_file = !preg_match('/^https?:\/\//', $url);
2248        $require_fopen = false;
2249        $opts = null;
2250
2251        if ($stream_context) {
2252            $opts = stream_context_get_options($stream_context);
2253            if (isset($opts['http'])) {
2254                $require_fopen = true;
2255                $opts_layer = array_diff_key($opts, ['http' => null]);
2256                $http_layer = array_diff_key($opts['http'], ['method' => null, 'content' => null]);
2257                if (empty($opts_layer) && empty($http_layer)) {
2258                    $require_fopen = false;
2259                }
2260            }
2261        } elseif (!$is_local_file) {
2262            $stream_context = @stream_context_create(
2263                [
2264                    'http' => ['timeout' => $curl_timeout],
2265                    'ssl' => [
2266                        'verify_peer' => true,
2267                        'cafile' => CaBundle::getBundledCaBundlePath(),
2268                    ],
2269                ]
2270            );
2271        }
2272
2273        if ($is_local_file) {
2274            $content = @file_get_contents($url, $use_include_path, $stream_context);
2275        } else {
2276            if ($require_fopen) {
2277                $content = Tools::file_get_contents_fopen($url, $use_include_path, $stream_context);
2278            } else {
2279                $content = Tools::file_get_contents_curl($url, $curl_timeout, $opts);
2280                if (empty($content) && $fallback) {
2281                    $content = Tools::file_get_contents_fopen($url, $use_include_path, $stream_context);
2282                }
2283            }
2284        }
2285
2286        return $content;
2287    }
2288
2289    /**
2290     * Create a local file from url
2291     * required because ZipArchive is unable to extract from remote files.
2292     *
2293     * @param string $url the remote location
2294     *
2295     * @return bool|string false if failure, else the local filename
2296     */
2297    public static function createFileFromUrl($url)
2298    {
2299        $remoteFile = fopen($url, 'rb');
2300        if (!$remoteFile) {
2301            return false;
2302        }
2303        $localFile = fopen(basename($url), 'wb');
2304        if (!$localFile) {
2305            return false;
2306        }
2307
2308        while (!feof($remoteFile)) {
2309            $data = fread($remoteFile, 1024);
2310            fwrite($localFile, $data, 1024);
2311        }
2312
2313        fclose($remoteFile);
2314        fclose($localFile);
2315
2316        return basename($url);
2317    }
2318
2319    public static function simplexml_load_file($url, $class_name = null)
2320    {
2321        $cache_id = 'Tools::simplexml_load_file' . $url;
2322        if (!Cache::isStored($cache_id)) {
2323            $result = @simplexml_load_string(Tools::file_get_contents($url), $class_name);
2324            Cache::store($cache_id, $result);
2325
2326            return $result;
2327        }
2328
2329        return Cache::retrieve($cache_id);
2330    }
2331
2332    public static function copy($source, $destination, $stream_context = null)
2333    {
2334        if (null === $stream_context && !preg_match('/^https?:\/\//', $source)) {
2335            return @copy($source, $destination);
2336        }
2337
2338        return @file_put_contents($destination, Tools::file_get_contents($source, false, $stream_context));
2339    }
2340
2341    /**
2342     * Translates a string with underscores into camel case (e.g. first_name -> firstName).
2343     *
2344     * @prototype string public static function toCamelCase(string $str[, bool $capitalise_first_char = false])
2345     *
2346     * @param string $str Source string to convert in camel case
2347     * @param bool $capitaliseFirstChar Optionnal parameters to transform the first letter in upper case
2348     *
2349     * @return string The string in camel case
2350     */
2351    public static function toCamelCase($str, $capitaliseFirstChar = false)
2352    {
2353        $str = Tools::strtolower($str);
2354        $str = str_replace(' ', '', ucwords(str_replace(['-', '_'], ' ', $str)));
2355        if (!$capitaliseFirstChar) {
2356            $str = lcfirst($str);
2357        }
2358
2359        return $str;
2360    }
2361
2362    /**
2363     * Transform a CamelCase string to underscore_case string.
2364     *
2365     * 'CMSCategories' => 'cms_categories'
2366     * 'RangePrice' => 'range_price'
2367     *
2368     * @param string $string
2369     *
2370     * @return string
2371     */
2372    public static function toUnderscoreCase($string)
2373    {
2374        return Tools::strtolower(trim(preg_replace('/([A-Z][a-z])/', '_$1', $string), '_'));
2375    }
2376
2377    /**
2378     * Converts SomethingLikeThis to something-like-this
2379     *
2380     * @param string $string
2381     *
2382     * @return string
2383     */
2384    public static function camelCaseToKebabCase($string)
2385    {
2386        return Tools::strtolower(
2387            preg_replace('/([a-z])([A-Z])/', '$1-$2', $string)
2388        );
2389    }
2390
2391    /**
2392     * @param string $hex
2393     *
2394     * @return float|int|string
2395     */
2396    public static function getBrightness($hex)
2397    {
2398        if (Tools::strtolower($hex) == 'transparent') {
2399            return '129';
2400        }
2401
2402        $hex = str_replace('#', '', $hex);
2403
2404        if (Tools::strlen($hex) == 3) {
2405            $hex .= $hex;
2406        }
2407
2408        $r = hexdec(substr($hex, 0, 2));
2409        $g = hexdec(substr($hex, 2, 2));
2410        $b = hexdec(substr($hex, 4, 2));
2411
2412        return (($r * 299) + ($g * 587) + ($b * 114)) / 1000;
2413    }
2414
2415    public static function isBright($hex)
2416    {
2417        if (null === self::$colorBrightnessCalculator) {
2418            self::$colorBrightnessCalculator = new ColorBrightnessCalculator();
2419        }
2420
2421        return self::$colorBrightnessCalculator->isBright($hex);
2422    }
2423
2424    public static function parserSQL($sql)
2425    {
2426        if (strlen($sql) > 0) {
2427            $parser = new PHPSQLParser($sql);
2428
2429            return $parser->parsed;
2430        }
2431
2432        return false;
2433    }
2434
2435    public static function replaceByAbsoluteURL($matches)
2436    {
2437        Tools::displayAsDeprecated('Use Media::replaceByAbsoluteURL($matches) instead');
2438
2439        return Media::replaceByAbsoluteURL($matches);
2440    }
2441
2442    protected static $_cache_nb_media_servers = null;
2443
2444    /**
2445     * @return bool
2446     */
2447    public static function hasMediaServer(): bool
2448    {
2449        if (self::$_cache_nb_media_servers === null && defined('_MEDIA_SERVER_1_') && defined('_MEDIA_SERVER_2_') && defined('_MEDIA_SERVER_3_')) {
2450            if (_MEDIA_SERVER_1_ == '') {
2451                self::$_cache_nb_media_servers = 0;
2452            } elseif (_MEDIA_SERVER_2_ == '') {
2453                self::$_cache_nb_media_servers = 1;
2454            } elseif (_MEDIA_SERVER_3_ == '') {
2455                self::$_cache_nb_media_servers = 2;
2456            } else {
2457                self::$_cache_nb_media_servers = 3;
2458            }
2459        }
2460
2461        return self::$_cache_nb_media_servers > 0;
2462    }
2463
2464    /**
2465     * @param string $filename
2466     *
2467     * @return string
2468     */
2469    public static function getMediaServer(string $filename): string
2470    {
2471        if (self::hasMediaServer() && ($id_media_server = (abs(crc32($filename)) % self::$_cache_nb_media_servers + 1))) {
2472            return constant('_MEDIA_SERVER_' . $id_media_server . '_');
2473        }
2474
2475        return Tools::usingSecureMode() ? Tools::getShopDomainSsl() : Tools::getShopDomain();
2476    }
2477
2478    /**
2479     * Get domains information with physical and virtual paths
2480     *
2481     * e.g: [
2482     *  prestashop.localhost => [
2483     *    physical => "/",
2484     *    virtual => "",
2485     *    id_shop => "1",
2486     *  ]
2487     * ]
2488     *
2489     * @return array
2490     */
2491    public static function getDomains()
2492    {
2493        $domains = [];
2494        foreach (ShopUrl::getShopUrls() as $shop_url) {
2495            /** @var ShopUrl $shop_url */
2496            if (!isset($domains[$shop_url->domain])) {
2497                $domains[$shop_url->domain] = [];
2498            }
2499
2500            $domains[$shop_url->domain][] = [
2501                'physical' => $shop_url->physical_uri,
2502                'virtual' => $shop_url->virtual_uri,
2503                'id_shop' => $shop_url->id_shop,
2504            ];
2505
2506            if ($shop_url->domain == $shop_url->domain_ssl) {
2507                continue;
2508            }
2509
2510            if (!isset($domains[$shop_url->domain_ssl])) {
2511                $domains[$shop_url->domain_ssl] = [];
2512            }
2513
2514            $domains[$shop_url->domain_ssl][] = [
2515                'physical' => $shop_url->physical_uri,
2516                'virtual' => $shop_url->virtual_uri,
2517                'id_shop' => $shop_url->id_shop,
2518            ];
2519        }
2520
2521        return $domains;
2522    }
2523
2524    public static function generateHtaccess($path = null, $rewrite_settings = null, $cache_control = null, $specific = '', $disable_multiviews = null, $medias = false, $disable_modsec = null)
2525    {
2526        if (defined('_PS_IN_TEST_')
2527            || (defined('PS_INSTALLATION_IN_PROGRESS') && $rewrite_settings === null)
2528        ) {
2529            return true;
2530        }
2531
2532        // Default values for parameters
2533        if (null === $path) {
2534            $path = _PS_ROOT_DIR_ . '/.htaccess';
2535        }
2536
2537        if (null === $cache_control) {
2538            $cache_control = (int) Configuration::get('PS_HTACCESS_CACHE_CONTROL');
2539        }
2540        if (null === $disable_multiviews) {
2541            $disable_multiviews = (bool) Configuration::get('PS_HTACCESS_DISABLE_MULTIVIEWS');
2542        }
2543
2544        if ($disable_modsec === null) {
2545            $disable_modsec = (int) Configuration::get('PS_HTACCESS_DISABLE_MODSEC');
2546        }
2547
2548        // Check current content of .htaccess and save all code outside of prestashop comments
2549        $specific_before = $specific_after = '';
2550        if (file_exists($path)) {
2551            $content = file_get_contents($path);
2552            if (preg_match('#^(.*)\# ~~start~~.*\# ~~end~~[^\n]*(.*)$#s', $content, $m)) {
2553                $specific_before = $m[1];
2554                $specific_after = $m[2];
2555            } else {
2556                // For retrocompatibility
2557                if (preg_match('#\# http://www\.prestashop\.com - http://www\.prestashop\.com/forums\s*(.*)<IfModule mod_rewrite\.c>#si', $content, $m)) {
2558                    $specific_before = $m[1];
2559                } else {
2560                    $specific_before = $content;
2561                }
2562            }
2563        }
2564
2565        // Write .htaccess data
2566        if (!$write_fd = @fopen($path, 'wb')) {
2567            return false;
2568        }
2569        if ($specific_before) {
2570            fwrite($write_fd, trim($specific_before) . "\n\n");
2571        }
2572
2573        $domains = self::getDomains();
2574
2575        // Write data in .htaccess file
2576        fwrite($write_fd, "# ~~start~~ Do not remove this comment, Prestashop will keep automatically the code outside this comment when .htaccess will be generated again\n");
2577        fwrite($write_fd, "# .htaccess automaticaly generated by PrestaShop e-commerce open-source solution\n");
2578        fwrite($write_fd, "# https://www.prestashop.com - https://www.prestashop.com/forums\n\n");
2579
2580        if ($disable_modsec) {
2581            fwrite($write_fd, "<IfModule mod_security.c>\nSecFilterEngine Off\nSecFilterScanPOST Off\n</IfModule>\n\n");
2582        }
2583
2584        // RewriteEngine
2585        fwrite($write_fd, "<IfModule mod_rewrite.c>\n");
2586
2587        // Ensure HTTP_MOD_REWRITE variable is set in environment
2588        fwrite($write_fd, "<IfModule mod_env.c>\n");
2589        fwrite($write_fd, "SetEnv HTTP_MOD_REWRITE On\n");
2590        fwrite($write_fd, "</IfModule>\n\n");
2591
2592        // Disable multiviews ?
2593        if ($disable_multiviews) {
2594            fwrite($write_fd, "\n# Disable Multiviews\nOptions -Multiviews\n\n");
2595        }
2596
2597        fwrite($write_fd, "RewriteEngine on\n");
2598
2599        if (!$medias && Configuration::getMultiShopValues('PS_MEDIA_SERVER_1')
2600            && Configuration::getMultiShopValues('PS_MEDIA_SERVER_2')
2601            && Configuration::getMultiShopValues('PS_MEDIA_SERVER_3')
2602        ) {
2603            $medias = [
2604                Configuration::getMultiShopValues('PS_MEDIA_SERVER_1'),
2605                Configuration::getMultiShopValues('PS_MEDIA_SERVER_2'),
2606                Configuration::getMultiShopValues('PS_MEDIA_SERVER_3'),
2607            ];
2608        }
2609
2610        $media_domains = '';
2611        foreach ($medias as $media) {
2612            foreach ($media as $media_url) {
2613                if ($media_url) {
2614                    $media_domains .= 'RewriteCond %{HTTP_HOST} ^' . $media_url . '$ [OR]' . PHP_EOL;
2615                }
2616            }
2617        }
2618
2619        if (Configuration::get('PS_WEBSERVICE_CGI_HOST')) {
2620            fwrite($write_fd, "RewriteCond %{HTTP:Authorization} ^(.*)\nRewriteRule . - [E=HTTP_AUTHORIZATION:%1]\n\n");
2621        }
2622
2623        foreach ($domains as $domain => $list_uri) {
2624            // As we use regex in the htaccess, ipv6 surrounded by brackets must be escaped
2625            $domain = str_replace(['[', ']'], ['\[', '\]'], $domain);
2626
2627            foreach ($list_uri as $uri) {
2628                fwrite($write_fd, PHP_EOL . PHP_EOL . '#Domain: ' . $domain . PHP_EOL);
2629                if (Shop::isFeatureActive()) {
2630                    fwrite($write_fd, 'RewriteCond %{HTTP_HOST} ^' . $domain . '$' . PHP_EOL);
2631                }
2632                fwrite($write_fd, 'RewriteRule . - [E=REWRITEBASE:' . $uri['physical'] . ']' . PHP_EOL);
2633
2634                // Webservice
2635                fwrite($write_fd, 'RewriteRule ^api(?:/(.*))?$ %{ENV:REWRITEBASE}webservice/dispatcher.php?url=$1 [QSA,L]' . "\n\n");
2636
2637                if (!$rewrite_settings) {
2638                    $rewrite_settings = (int) Configuration::get('PS_REWRITING_SETTINGS', null, null, (int) $uri['id_shop']);
2639                }
2640
2641                $domain_rewrite_cond = 'RewriteCond %{HTTP_HOST} ^' . $domain . '$' . PHP_EOL;
2642                // Rewrite virtual multishop uri
2643                if ($uri['virtual']) {
2644                    if (!$rewrite_settings) {
2645                        fwrite($write_fd, $media_domains);
2646                        fwrite($write_fd, $domain_rewrite_cond);
2647                        fwrite($write_fd, 'RewriteRule ^' . trim($uri['virtual'], '/') . '/?$ ' . $uri['physical'] . $uri['virtual'] . "index.php [L,R]\n");
2648                    } else {
2649                        fwrite($write_fd, $media_domains);
2650                        fwrite($write_fd, $domain_rewrite_cond);
2651                        fwrite($write_fd, 'RewriteRule ^' . trim($uri['virtual'], '/') . '$ ' . $uri['physical'] . $uri['virtual'] . " [L,R]\n");
2652                    }
2653                    fwrite($write_fd, $media_domains);
2654                    fwrite($write_fd, $domain_rewrite_cond);
2655                    fwrite($write_fd, 'RewriteRule ^' . ltrim($uri['virtual'], '/') . '(.*) ' . $uri['physical'] . "$1 [L]\n\n");
2656                }
2657
2658                if ($rewrite_settings) {
2659                    // Compatibility with the old image filesystem
2660                    fwrite($write_fd, "# Images\n");
2661                    if (Configuration::get('PS_LEGACY_IMAGES')) {
2662                        fwrite($write_fd, $media_domains);
2663                        fwrite($write_fd, $domain_rewrite_cond);
2664                        fwrite($write_fd, 'RewriteRule ^([a-z0-9]+)\-([a-z0-9]+)(\-[_a-zA-Z0-9-]*)(-[0-9]+)?/.+\.jpg$ %{ENV:REWRITEBASE}img/p/$1-$2$3$4.jpg [L]' . PHP_EOL);
2665                        fwrite($write_fd, $media_domains);
2666                        fwrite($write_fd, $domain_rewrite_cond);
2667                        fwrite($write_fd, 'RewriteRule ^([0-9]+)\-([0-9]+)(-[0-9]+)?/.+\.jpg$ %{ENV:REWRITEBASE}img/p/$1-$2$3.jpg [L]' . PHP_EOL);
2668                    }
2669
2670                    // Rewrite product images < 10 millions
2671                    for ($i = 1; $i <= 7; ++$i) {
2672                        $img_path = $img_name = '';
2673                        for ($j = 1; $j <= $i; ++$j) {
2674                            $img_path .= '$' . $j . '/';
2675                            $img_name .= '$' . $j;
2676                        }
2677                        $img_name .= '$' . $j;
2678                        fwrite($write_fd, $media_domains);
2679                        fwrite($write_fd, $domain_rewrite_cond);
2680                        fwrite($write_fd, 'RewriteRule ^' . str_repeat('([0-9])', $i) . '(\-[_a-zA-Z0-9-]*)?(-[0-9]+)?/.+\.jpg$ %{ENV:REWRITEBASE}img/p/' . $img_path . $img_name . '$' . ($j + 1) . ".jpg [L]\n");
2681                    }
2682                    fwrite($write_fd, $media_domains);
2683                    fwrite($write_fd, $domain_rewrite_cond);
2684                    fwrite($write_fd, 'RewriteRule ^c/([0-9]+)(\-[\.*_a-zA-Z0-9-]*)(-[0-9]+)?/.+\.jpg$ %{ENV:REWRITEBASE}img/c/$1$2$3.jpg [L]' . PHP_EOL);
2685                    fwrite($write_fd, $media_domains);
2686                    fwrite($write_fd, $domain_rewrite_cond);
2687                    fwrite($write_fd, 'RewriteRule ^c/([a-zA-Z_-]+)(-[0-9]+)?/.+\.jpg$ %{ENV:REWRITEBASE}img/c/$1$2.jpg [L]' . PHP_EOL);
2688                }
2689
2690                fwrite($write_fd, "# AlphaImageLoader for IE and fancybox\n");
2691                if (Shop::isFeatureActive()) {
2692                    fwrite($write_fd, $domain_rewrite_cond);
2693                }
2694                fwrite($write_fd, 'RewriteRule ^images_ie/?([^/]+)\.(jpe?g|png|gif)$ js/jquery/plugins/fancybox/images/$1.$2 [L]' . PHP_EOL);
2695            }
2696            // Redirections to dispatcher
2697            if ($rewrite_settings) {
2698                fwrite($write_fd, "\n# Dispatcher\n");
2699                fwrite($write_fd, "RewriteCond %{REQUEST_FILENAME} -s [OR]\n");
2700                fwrite($write_fd, "RewriteCond %{REQUEST_FILENAME} -l [OR]\n");
2701                fwrite($write_fd, "RewriteCond %{REQUEST_FILENAME} -d\n");
2702                if (Shop::isFeatureActive()) {
2703                    fwrite($write_fd, $domain_rewrite_cond);
2704                }
2705                fwrite($write_fd, "RewriteRule ^.*$ - [NC,L]\n");
2706                if (Shop::isFeatureActive()) {
2707                    fwrite($write_fd, $domain_rewrite_cond);
2708                }
2709                fwrite($write_fd, "RewriteRule ^.*\$ %{ENV:REWRITEBASE}index.php [NC,L]\n");
2710            }
2711        }
2712
2713        fwrite($write_fd, "</IfModule>\n\n");
2714
2715        fwrite($write_fd, "AddType application/vnd.ms-fontobject .eot\n");
2716        fwrite($write_fd, "AddType font/ttf .ttf\n");
2717        fwrite($write_fd, "AddType font/otf .otf\n");
2718        fwrite($write_fd, "AddType application/font-woff .woff\n");
2719        fwrite($write_fd, "AddType font/woff2 .woff2\n");
2720        fwrite($write_fd, "<IfModule mod_headers.c>
2721	<FilesMatch \"\.(ttf|ttc|otf|eot|woff|woff2|svg)$\">
2722		Header set Access-Control-Allow-Origin \"*\"
2723	</FilesMatch>
2724
2725    <FilesMatch \"\.pdf$\">
2726      Header set Content-Disposition \"Attachment\"
2727      Header set X-Content-Type-Options \"nosniff\"
2728    </FilesMatch>
2729</IfModule>\n\n");
2730        fwrite($write_fd, '<Files composer.lock>
2731    # Apache 2.2
2732    <IfModule !mod_authz_core.c>
2733        Order deny,allow
2734        Deny from all
2735    </IfModule>
2736
2737    # Apache 2.4
2738    <IfModule mod_authz_core.c>
2739        Require all denied
2740    </IfModule>
2741</Files>
2742');
2743        // Cache control
2744        if ($cache_control) {
2745            $cache_control = "<IfModule mod_expires.c>
2746	ExpiresActive On
2747	ExpiresByType image/gif \"access plus 1 month\"
2748	ExpiresByType image/jpeg \"access plus 1 month\"
2749	ExpiresByType image/png \"access plus 1 month\"
2750	ExpiresByType text/css \"access plus 1 week\"
2751	ExpiresByType text/javascript \"access plus 1 week\"
2752	ExpiresByType application/javascript \"access plus 1 week\"
2753	ExpiresByType application/x-javascript \"access plus 1 week\"
2754	ExpiresByType image/x-icon \"access plus 1 year\"
2755	ExpiresByType image/svg+xml \"access plus 1 year\"
2756	ExpiresByType image/vnd.microsoft.icon \"access plus 1 year\"
2757	ExpiresByType application/font-woff \"access plus 1 year\"
2758	ExpiresByType application/x-font-woff \"access plus 1 year\"
2759	ExpiresByType font/woff2 \"access plus 1 year\"
2760	ExpiresByType application/vnd.ms-fontobject \"access plus 1 year\"
2761	ExpiresByType font/opentype \"access plus 1 year\"
2762	ExpiresByType font/ttf \"access plus 1 year\"
2763	ExpiresByType font/otf \"access plus 1 year\"
2764	ExpiresByType application/x-font-ttf \"access plus 1 year\"
2765	ExpiresByType application/x-font-otf \"access plus 1 year\"
2766</IfModule>
2767
2768<IfModule mod_headers.c>
2769    Header unset Etag
2770</IfModule>
2771FileETag none
2772<IfModule mod_deflate.c>
2773    <IfModule mod_filter.c>
2774        AddOutputFilterByType DEFLATE text/html text/css text/javascript application/javascript application/x-javascript font/ttf application/x-font-ttf font/otf application/x-font-otf font/opentype image/svg+xml
2775    </IfModule>
2776</IfModule>\n\n";
2777            fwrite($write_fd, $cache_control);
2778        }
2779
2780        // In case the user hasn't rewrite mod enabled
2781        fwrite($write_fd, "#If rewrite mod isn't enabled\n");
2782
2783        // Do not remove ($domains is already iterated upper)
2784        reset($domains);
2785        $domain = current($domains);
2786        fwrite($write_fd, 'ErrorDocument 404 ' . $domain[0]['physical'] . "index.php?controller=404\n\n");
2787
2788        fwrite($write_fd, '# ~~end~~ Do not remove this comment, Prestashop will keep automatically the code outside this comment when .htaccess will be generated again');
2789        if ($specific_after) {
2790            fwrite($write_fd, "\n\n" . trim($specific_after));
2791        }
2792        fclose($write_fd);
2793
2794        if (!defined('PS_INSTALLATION_IN_PROGRESS')) {
2795            Hook::exec('actionHtaccessCreate');
2796        }
2797
2798        return true;
2799    }
2800
2801    /**
2802     * @param bool $executeHook
2803     *
2804     * @return bool
2805     */
2806    public static function generateRobotsFile($executeHook = false)
2807    {
2808        $robots_file = _PS_ROOT_DIR_ . '/robots.txt';
2809
2810        if (!$write_fd = @fopen($robots_file, 'wb')) {
2811            return false;
2812        }
2813
2814        $robots_content = static::getRobotsContent();
2815        $languagesIsoIds = Language::getIsoIds();
2816
2817        if (true === $executeHook) {
2818            Hook::exec('actionAdminMetaBeforeWriteRobotsFile', [
2819                'rb_data' => &$robots_content,
2820            ]);
2821        }
2822
2823        // PS Comments
2824        fwrite($write_fd, "# robots.txt automatically generated by PrestaShop e-commerce open-source solution\n");
2825        fwrite($write_fd, "# https://www.prestashop.com - https://www.prestashop.com/forums\n");
2826        fwrite($write_fd, "# This file is to prevent the crawling and indexing of certain parts\n");
2827        fwrite($write_fd, "# of your site by web crawlers and spiders run by sites like Yahoo!\n");
2828        fwrite($write_fd, "# and Google. By telling these \"robots\" where not to go on your site,\n");
2829        fwrite($write_fd, "# you save bandwidth and server resources.\n");
2830        fwrite($write_fd, "# For more information about the robots.txt standard, see:\n");
2831        fwrite($write_fd, "# https://www.robotstxt.org/robotstxt.html\n");
2832
2833        // User-Agent
2834        fwrite($write_fd, "User-agent: *\n");
2835
2836        // Allow Directives
2837        if (count($robots_content['Allow'])) {
2838            fwrite($write_fd, "# Allow Directives\n");
2839            foreach ($robots_content['Allow'] as $allow) {
2840                fwrite($write_fd, 'Allow: ' . $allow . PHP_EOL);
2841            }
2842        }
2843
2844        // Private pages
2845        if (count($robots_content['GB'])) {
2846            fwrite($write_fd, "# Private pages\n");
2847            foreach ($robots_content['GB'] as $gb) {
2848                fwrite($write_fd, 'Disallow: /*' . $gb . PHP_EOL);
2849            }
2850        }
2851
2852        // Directories
2853        if (count($robots_content['Directories'])) {
2854            foreach (self::getDomains() as $domain => $uriList) {
2855                fwrite(
2856                    $write_fd,
2857                    sprintf(
2858                        '# Directories for %s%s',
2859                        $domain,
2860                        PHP_EOL
2861                    )
2862                );
2863                // Disallow multishop directories
2864                foreach ($uriList as $uri) {
2865                    foreach ($robots_content['Directories'] as $dir) {
2866                        fwrite($write_fd, 'Disallow: ' . $uri['physical'] . $dir . PHP_EOL);
2867                    }
2868                }
2869
2870                // Disallow multilang directories
2871                if (!empty($languagesIsoIds)) {
2872                    foreach ($languagesIsoIds as $language) {
2873                        foreach ($robots_content['Directories'] as $dir) {
2874                            fwrite(
2875                                $write_fd,
2876                                sprintf(
2877                                    'Disallow: /%s/%s%s',
2878                                    $language['iso_code'],
2879                                    $dir,
2880                                    PHP_EOL
2881                                )
2882                            );
2883                        }
2884                    }
2885                }
2886            }
2887        }
2888
2889        // Files
2890        if (count($robots_content['Files'])) {
2891            fwrite($write_fd, "# Files\n");
2892            foreach ($robots_content['Files'] as $iso_code => $files) {
2893                foreach ($files as $file) {
2894                    if (!empty($languagesIsoIds)) {
2895                        fwrite($write_fd, 'Disallow: /*' . $iso_code . '/' . $file . PHP_EOL);
2896                    } else {
2897                        fwrite($write_fd, 'Disallow: /' . $file . PHP_EOL);
2898                    }
2899                }
2900            }
2901        }
2902
2903        if (null === Context::getContext()) {
2904            $sitemap_file = _PS_ROOT_DIR_ . DIRECTORY_SEPARATOR . 'index_sitemap.xml';
2905        } else {
2906            $sitemap_file = _PS_ROOT_DIR_ . DIRECTORY_SEPARATOR . Context::getContext()->shop->id . '_index_sitemap.xml';
2907        }
2908
2909        // Sitemap
2910        if (file_exists($sitemap_file) && filesize($sitemap_file)) {
2911            fwrite($write_fd, "# Sitemap\n");
2912            $sitemap_filename = basename($sitemap_file);
2913            fwrite($write_fd, 'Sitemap: ' . (Configuration::get('PS_SSL_ENABLED') ? 'https://' : 'http://') . $_SERVER['SERVER_NAME']
2914                . __PS_BASE_URI__ . $sitemap_filename . PHP_EOL);
2915        }
2916
2917        if (true === $executeHook) {
2918            Hook::exec('actionAdminMetaAfterWriteRobotsFile', [
2919                'rb_data' => $robots_content,
2920                'write_fd' => &$write_fd,
2921            ]);
2922        }
2923
2924        fclose($write_fd);
2925
2926        return true;
2927    }
2928
2929    /**
2930     * @return array
2931     */
2932    public static function getRobotsContent()
2933    {
2934        $tab = [];
2935
2936        // Special allow directives
2937        $tab['Allow'] = [
2938            '*/modules/*.css',
2939            '*/modules/*.js',
2940            '*/modules/*.png',
2941            '*/modules/*.jpg',
2942            '/js/jquery/*',
2943        ];
2944
2945        // Directories
2946        $tab['Directories'] = [
2947            'app/', 'cache/', 'classes/', 'config/', 'controllers/',
2948            'download/', 'js/', 'localization/', 'log/', 'mails/', 'modules/', 'override/',
2949            'pdf/', 'src/', 'tools/', 'translations/', 'upload/', 'var/', 'vendor/', 'webservice/',
2950        ];
2951
2952        // Files
2953        $disallow_controllers = [
2954            'addresses', 'address', 'authentication', 'cart', 'discount', 'footer',
2955            'get-file', 'header', 'history', 'identity', 'images.inc', 'init', 'my-account', 'order',
2956            'order-slip', 'order-detail', 'order-follow', 'order-return', 'order-confirmation', 'pagination', 'password',
2957            'pdf-invoice', 'pdf-order-return', 'pdf-order-slip', 'product-sort', 'search', 'statistics', 'attachment', 'guest-tracking',
2958        ];
2959
2960        // Rewrite files
2961        $tab['Files'] = [];
2962        if (Configuration::get('PS_REWRITING_SETTINGS')) {
2963            $sql = 'SELECT DISTINCT ml.url_rewrite, l.iso_code
2964                FROM ' . _DB_PREFIX_ . 'meta m
2965                INNER JOIN ' . _DB_PREFIX_ . 'meta_lang ml ON ml.id_meta = m.id_meta
2966                INNER JOIN ' . _DB_PREFIX_ . 'lang l ON l.id_lang = ml.id_lang
2967                WHERE l.active = 1 AND m.page IN (\'' . implode('\', \'', $disallow_controllers) . '\')';
2968            if ($results = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql)) {
2969                foreach ($results as $row) {
2970                    $tab['Files'][$row['iso_code']][] = $row['url_rewrite'];
2971                }
2972            }
2973        }
2974
2975        $tab['GB'] = [
2976            '?order=', '?tag=', '?id_currency=', '?search_query=', '?back=', '?n=',
2977            '&order=', '&tag=', '&id_currency=', '&search_query=', '&back=', '&n=',
2978        ];
2979
2980        foreach ($disallow_controllers as $controller) {
2981            $tab['GB'][] = 'controller=' . $controller;
2982        }
2983
2984        return $tab;
2985    }
2986
2987    public static function generateIndex()
2988    {
2989        PrestaShopAutoload::getInstance()->generateIndex();
2990    }
2991
2992    /**
2993     * @return string php file to be run
2994     */
2995    public static function getDefaultIndexContent()
2996    {
2997        return '<?php
2998/**
2999 * Copyright since 2007 PrestaShop SA and Contributors
3000 * PrestaShop is an International Registered Trademark & Property of PrestaShop SA
3001 *
3002 * NOTICE OF LICENSE
3003 *
3004 * This source file is subject to the Open Software License (OSL 3.0)
3005 * that is bundled with this package in the file LICENSE.md.
3006 * It is also available through the world-wide-web at this URL:
3007 * https://opensource.org/licenses/OSL-3.0
3008 * If you did not receive a copy of the license and are unable to
3009 * obtain it through the world-wide-web, please send an email
3010 * to license@prestashop.com so we can send you a copy immediately.
3011 *
3012 * DISCLAIMER
3013 *
3014 * Do not edit or add to this file if you wish to upgrade PrestaShop to newer
3015 * versions in the future. If you wish to customize PrestaShop for your
3016 * needs please refer to https://devdocs.prestashop.com/ for more information.
3017 *
3018 * @author    PrestaShop SA and Contributors <contact@prestashop.com>
3019 * @copyright Since 2007 PrestaShop SA and Contributors
3020 * @license   https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
3021 */
3022
3023header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
3024header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT");
3025
3026header("Cache-Control: no-store, no-cache, must-revalidate");
3027header("Cache-Control: post-check=0, pre-check=0", false);
3028header("Pragma: no-cache");
3029
3030header("Location: ../");
3031exit;
3032';
3033    }
3034
3035    /**
3036     * Return the directory list from the given $path.
3037     *
3038     * @param string $path
3039     *
3040     * @return array
3041     */
3042    public static function getDirectories($path)
3043    {
3044        if (function_exists('glob')) {
3045            return self::getDirectoriesWithGlob($path);
3046        }
3047
3048        return self::getDirectoriesWithReaddir($path);
3049    }
3050
3051    /**
3052     * Return the directory list from the given $path using php glob function.
3053     *
3054     * @param string $path
3055     *
3056     * @return array
3057     */
3058    public static function getDirectoriesWithGlob($path)
3059    {
3060        $directoryList = glob($path . '/*', GLOB_ONLYDIR | GLOB_NOSORT);
3061        array_walk(
3062            $directoryList,
3063            function (&$absolutePath, $key) {
3064                $absolutePath = substr($absolutePath, strrpos($absolutePath, '/') + 1);
3065            }
3066        );
3067
3068        return $directoryList;
3069    }
3070
3071    /**
3072     * Return the directory list from the given $path using php readdir function.
3073     *
3074     * @param string $path
3075     *
3076     * @return array
3077     */
3078    public static function getDirectoriesWithReaddir($path)
3079    {
3080        $directoryList = [];
3081        $dh = @opendir($path);
3082        if ($dh) {
3083            while (($file = @readdir($dh)) !== false) {
3084                if (is_dir($path . DIRECTORY_SEPARATOR . $file) && $file[0] != '.') {
3085                    $directoryList[] = $file;
3086                }
3087            }
3088            @closedir($dh);
3089        }
3090
3091        return $directoryList;
3092    }
3093
3094    /**
3095     * @deprecated Deprecated since 1.7.0
3096     * Use json_decode instead
3097     * jsonDecode convert json string to php array / object
3098     *
3099     * @param string $data
3100     * @param bool $assoc (since 1.4.2.4) if true, convert to associative array
3101     * @param int $depth
3102     * @param int $options
3103     *
3104     * @return array
3105     */
3106    public static function jsonDecode($data, $assoc = false, $depth = 512, $options = 0)
3107    {
3108        return json_decode($data, $assoc, $depth, $options);
3109    }
3110
3111    /**
3112     * @deprecated Deprecated since 1.7.0
3113     * Use json_encode instead
3114     * Convert an array to json string
3115     *
3116     * @param array $data
3117     * @param int $depth
3118     * @param int $options
3119     *
3120     * @return string json
3121     */
3122    public static function jsonEncode($data, $options = 0, $depth = 512)
3123    {
3124        return json_encode($data, $options, $depth);
3125    }
3126
3127    /**
3128     * Display a warning message indicating that the method is deprecated.
3129     *
3130     * @param string $message
3131     */
3132    public static function displayAsDeprecated($message = null)
3133    {
3134        $backtrace = debug_backtrace();
3135        $callee = next($backtrace);
3136        $class = isset($callee['class']) ? $callee['class'] : null;
3137
3138        if ($message === null) {
3139            $message = 'The function ' . $callee['function'] . ' (Line ' . $callee['line'] . ') is deprecated and will be removed in the next major version.';
3140        }
3141
3142        $error = 'Function <b>' . $callee['function'] . '()</b> is deprecated in <b>' . $callee['file'] . '</b> on line <b>' . $callee['line'] . '</b><br />';
3143
3144        Tools::throwDeprecated($error, $message, $class);
3145    }
3146
3147    /**
3148     * Display a warning message indicating that the parameter is deprecated.
3149     */
3150    public static function displayParameterAsDeprecated($parameter)
3151    {
3152        $backtrace = debug_backtrace();
3153        $callee = next($backtrace);
3154        $error = 'Parameter <b>' . $parameter . '</b> in function <b>' . (isset($callee['function']) ? $callee['function'] : '') . '()</b> is deprecated in <b>' . $callee['file'] . '</b> on line <b>' . (isset($callee['line']) ? $callee['line'] : '(undefined)') . '</b><br />';
3155        $message = 'The parameter ' . $parameter . ' in function ' . $callee['function'] . ' (Line ' . (isset($callee['line']) ? $callee['line'] : 'undefined') . ') is deprecated and will be removed in the next major version.';
3156        $class = isset($callee['class']) ? $callee['class'] : null;
3157
3158        Tools::throwDeprecated($error, $message, $class);
3159    }
3160
3161    public static function displayFileAsDeprecated()
3162    {
3163        $backtrace = debug_backtrace();
3164        $callee = current($backtrace);
3165        $error = 'File <b>' . $callee['file'] . '</b> is deprecated<br />';
3166        $message = 'The file ' . $callee['file'] . ' is deprecated and will be removed in the next major version.';
3167        $class = isset($callee['class']) ? $callee['class'] : null;
3168
3169        Tools::throwDeprecated($error, $message, $class);
3170    }
3171
3172    protected static function throwDeprecated($error, $message, $class)
3173    {
3174        if (_PS_DISPLAY_COMPATIBILITY_WARNING_) {
3175            @trigger_error($error, E_USER_DEPRECATED);
3176            PrestaShopLogger::addLog($message, 3, $class);
3177        }
3178    }
3179
3180    public static function enableCache($level = 1, Context $context = null)
3181    {
3182        if (!$context) {
3183            $context = Context::getContext();
3184        }
3185        $smarty = $context->smarty;
3186        if (!Configuration::get('PS_SMARTY_CACHE')) {
3187            return;
3188        }
3189        if ($smarty->force_compile == 0 && $smarty->caching == $level) {
3190            return;
3191        }
3192        self::$_forceCompile = (int) $smarty->force_compile;
3193        self::$_caching = (int) $smarty->caching;
3194        $smarty->force_compile = 0;
3195        $smarty->caching = (int) $level;
3196        $smarty->cache_lifetime = 31536000; // 1 Year
3197    }
3198
3199    public static function restoreCacheSettings(Context $context = null)
3200    {
3201        if (!$context) {
3202            $context = Context::getContext();
3203        }
3204
3205        if (isset(self::$_forceCompile)) {
3206            $context->smarty->force_compile = (int) self::$_forceCompile;
3207        }
3208        if (isset(self::$_caching)) {
3209            $context->smarty->caching = (int) self::$_caching;
3210        }
3211    }
3212
3213    public static function isCallable($function)
3214    {
3215        $disabled = explode(',', ini_get('disable_functions'));
3216
3217        return !in_array($function, $disabled) && is_callable($function);
3218    }
3219
3220    public static function pRegexp($s, $delim)
3221    {
3222        $s = str_replace($delim, '\\' . $delim, $s);
3223        foreach (['?', '[', ']', '(', ')', '{', '}', '-', '.', '+', '*', '^', '$', '`', '"', '%'] as $char) {
3224            $s = str_replace($char, '\\' . $char, $s);
3225        }
3226
3227        return $s;
3228    }
3229
3230    public static function str_replace_once($needle, $replace, $haystack)
3231    {
3232        $pos = false;
3233        if ($needle) {
3234            $pos = strpos($haystack, $needle);
3235        }
3236        if ($pos === false) {
3237            return $haystack;
3238        }
3239
3240        return substr_replace($haystack, $replace, $pos, strlen($needle));
3241    }
3242
3243    /**
3244     * Identify the version of php
3245     *
3246     * @return string
3247     */
3248    public static function checkPhpVersion()
3249    {
3250        $version = null;
3251
3252        if (defined('PHP_VERSION')) {
3253            $version = PHP_VERSION;
3254        } else {
3255            $version = phpversion('');
3256        }
3257
3258        // Specific ubuntu usecase: php version returns 5.2.4-2ubuntu5.2
3259        if (strpos($version, '-') !== false) {
3260            $version = substr($version, 0, strpos($version, '-'));
3261        }
3262
3263        return $version;
3264    }
3265
3266    /**
3267     * Try to open a zip file in order to check if it's valid
3268     *
3269     * @param string $from_file
3270     *
3271     * @return bool success
3272     */
3273    public static function ZipTest($from_file)
3274    {
3275        $zip = new ZipArchive();
3276
3277        return $zip->open($from_file, ZipArchive::CHECKCONS) === true;
3278    }
3279
3280    /**
3281     * @deprecated Deprecated since 1.7.0
3282     *
3283     * @return bool
3284     */
3285    public static function getSafeModeStatus()
3286    {
3287        return false;
3288    }
3289
3290    /**
3291     * Extract a zip file to the given directory
3292     *
3293     * @param string $from_file
3294     * @param string $to_dir
3295     *
3296     * @return bool
3297     */
3298    public static function ZipExtract($from_file, $to_dir)
3299    {
3300        if (!file_exists($to_dir)) {
3301            mkdir($to_dir, PsFileSystem::DEFAULT_MODE_FOLDER);
3302        }
3303
3304        $zip = new ZipArchive();
3305        if ($zip->open($from_file) === true && $zip->extractTo($to_dir) && $zip->close()) {
3306            return true;
3307        }
3308
3309        return false;
3310    }
3311
3312    /**
3313     * @param string $path
3314     * @param int $filemode
3315     *
3316     * @return bool
3317     */
3318    public static function chmodr($path, $filemode)
3319    {
3320        if (!is_dir($path)) {
3321            return @chmod($path, $filemode);
3322        }
3323        $dh = opendir($path);
3324        while (($file = readdir($dh)) !== false) {
3325            if ($file != '.' && $file != '..') {
3326                $fullpath = $path . '/' . $file;
3327                if (is_link($fullpath)) {
3328                    return false;
3329                } elseif (!is_dir($fullpath) && !@chmod($fullpath, $filemode)) {
3330                    return false;
3331                } elseif (!Tools::chmodr($fullpath, $filemode)) {
3332                    return false;
3333                }
3334            }
3335        }
3336        closedir($dh);
3337        if (@chmod($path, $filemode)) {
3338            return true;
3339        } else {
3340            return false;
3341        }
3342    }
3343
3344    /**
3345     * Get products order field name for queries.
3346     *
3347     * @param string $type by|way
3348     * @param string $value If no index given, use default order from admin -> pref -> products
3349     * @param bool|\bool(false)|string $prefix
3350     *
3351     * @return string Order by sql clause
3352     */
3353    public static function getProductsOrder($type, $value = null, $prefix = false)
3354    {
3355        switch ($type) {
3356            case 'by':
3357                $list = [0 => 'name', 1 => 'price', 2 => 'date_add', 3 => 'date_upd', 4 => 'position', 5 => 'manufacturer_name', 6 => 'quantity', 7 => 'reference'];
3358                $value = (null === $value || $value === false || $value === '') ? (int) Configuration::get('PS_PRODUCTS_ORDER_BY') : $value;
3359                $value = (isset($list[$value])) ? $list[$value] : ((in_array($value, $list)) ? $value : 'position');
3360                $order_by_prefix = '';
3361                if ($prefix) {
3362                    if ($value == 'id_product' || $value == 'date_add' || $value == 'date_upd' || $value == 'price') {
3363                        $order_by_prefix = 'p.';
3364                    } elseif ($value == 'name') {
3365                        $order_by_prefix = 'pl.';
3366                    } elseif ($value == 'manufacturer_name' && $prefix) {
3367                        $order_by_prefix = 'm.';
3368                        $value = 'name';
3369                    } elseif ($value == 'position' || empty($value)) {
3370                        $order_by_prefix = 'cp.';
3371                    }
3372                }
3373
3374                return $order_by_prefix . $value;
3375
3376            break;
3377
3378            case 'way':
3379                $value = (null === $value || $value === false || $value === '') ? (int) Configuration::get('PS_PRODUCTS_ORDER_WAY') : $value;
3380                $list = [0 => 'asc', 1 => 'desc'];
3381
3382                return (isset($list[$value])) ? $list[$value] : ((in_array($value, $list)) ? $value : 'asc');
3383
3384            break;
3385        }
3386    }
3387
3388    /**
3389     * Convert a shorthand byte value from a PHP configuration directive to an integer value.
3390     *
3391     * @param string $value value to convert
3392     *
3393     * @return int
3394     */
3395    public static function convertBytes($value)
3396    {
3397        if (is_numeric($value)) {
3398            return $value;
3399        } else {
3400            $value_length = strlen($value);
3401            $qty = (int) substr($value, 0, $value_length - 1);
3402            $unit = Tools::strtolower(substr($value, $value_length - 1));
3403            switch ($unit) {
3404                case 'k':
3405                    $qty *= 1024;
3406
3407                    break;
3408                case 'm':
3409                    $qty *= 1048576;
3410
3411                    break;
3412                case 'g':
3413                    $qty *= 1073741824;
3414
3415                    break;
3416            }
3417
3418            return $qty;
3419        }
3420    }
3421
3422    /**
3423     * @deprecated as of 1.5 use Controller::getController('PageNotFoundController')->run();
3424     */
3425    public static function display404Error()
3426    {
3427        header('HTTP/1.1 404 Not Found');
3428        header('Status: 404 Not Found');
3429        include __DIR__ . '/../404.php';
3430        die;
3431    }
3432
3433    /**
3434     * Concat $begin and $end, add ? or & between strings.
3435     *
3436     * @since 1.5.0
3437     *
3438     * @param string $begin
3439     * @param string $end
3440     *
3441     * @return string
3442     */
3443    public static function url($begin, $end)
3444    {
3445        return $begin . ((strpos($begin, '?') !== false) ? '&' : '?') . $end;
3446    }
3447
3448    /**
3449     * Display error and dies or silently log the error.
3450     *
3451     * @param string $msg
3452     * @param bool $die
3453     *
3454     * @return bool success of logging
3455     */
3456    public static function dieOrLog($msg, $die = true)
3457    {
3458        if ($die || (defined('_PS_MODE_DEV_') && _PS_MODE_DEV_)) {
3459            header('HTTP/1.1 500 Internal Server Error', true, 500);
3460            die($msg);
3461        }
3462
3463        return PrestaShopLogger::addLog($msg);
3464    }
3465
3466    /**
3467     * Convert \n and \r\n and \r to <br />.
3468     *
3469     * @param string $str String to transform
3470     *
3471     * @return string New string
3472     */
3473    public static function nl2br($str)
3474    {
3475        return str_replace(["\r\n", "\r", "\n", AddressFormat::FORMAT_NEW_LINE, PHP_EOL], '<br />', $str);
3476    }
3477
3478    /**
3479     * Clear cache for Smarty.
3480     *
3481     * @param Smarty $smarty
3482     * @param bool $tpl
3483     * @param string $cache_id
3484     * @param string $compile_id
3485     *
3486     * @return int|null number of cache files deleted
3487     */
3488    public static function clearCache($smarty = null, $tpl = false, $cache_id = null, $compile_id = null)
3489    {
3490        if ($smarty === null) {
3491            $smarty = Context::getContext()->smarty;
3492        }
3493
3494        if ($smarty === null) {
3495            return null;
3496        }
3497
3498        if (!$tpl && $cache_id === null && $compile_id === null) {
3499            return $smarty->clearAllCache();
3500        }
3501
3502        $ret = $smarty->clearCache($tpl, $cache_id, $compile_id);
3503
3504        Hook::exec('actionClearCache');
3505
3506        return $ret;
3507    }
3508
3509    /**
3510     * Clear compile for Smarty.
3511     *
3512     * @param Smarty $smarty
3513     *
3514     * @return int|null number of template files deleted
3515     */
3516    public static function clearCompile($smarty = null)
3517    {
3518        if ($smarty === null) {
3519            $smarty = Context::getContext()->smarty;
3520        }
3521
3522        if ($smarty === null) {
3523            return null;
3524        }
3525
3526        $ret = $smarty->clearCompiledTemplate();
3527
3528        Hook::exec('actionClearCompileCache');
3529
3530        return $ret;
3531    }
3532
3533    /**
3534     * Clear Smarty cache and compile folders.
3535     */
3536    public static function clearSmartyCache()
3537    {
3538        $smarty = Context::getContext()->smarty;
3539        Tools::clearCache($smarty);
3540        Tools::clearCompile($smarty);
3541    }
3542
3543    /**
3544     * Clear Symfony cache.
3545     *
3546     * @param string $env
3547     */
3548    public static function clearSf2Cache($env = null)
3549    {
3550        if (null === $env) {
3551            $env = _PS_ENV_;
3552        }
3553
3554        $dir = _PS_ROOT_DIR_ . '/var/cache/' . $env . '/';
3555
3556        register_shutdown_function(function () use ($dir) {
3557            $fs = new Filesystem();
3558            $fs->remove($dir);
3559            Hook::exec('actionClearSf2Cache');
3560        });
3561    }
3562
3563    /**
3564     * Clear both Smarty and Symfony cache.
3565     */
3566    public static function clearAllCache()
3567    {
3568        Tools::clearSmartyCache();
3569        Tools::clearSf2Cache();
3570    }
3571
3572    /**
3573     * @param int|bool $id_product
3574     */
3575    public static function clearColorListCache($id_product = false)
3576    {
3577        // Change template dir if called from the BackOffice
3578        $current_template_dir = Context::getContext()->smarty->getTemplateDir();
3579        Context::getContext()->smarty->setTemplateDir(_PS_THEME_DIR_);
3580        Tools::clearCache(null, _PS_THEME_DIR_ . 'product-list-colors.tpl', Product::getColorsListCacheId((int) $id_product, false));
3581        Context::getContext()->smarty->setTemplateDir($current_template_dir);
3582    }
3583
3584    /**
3585     * Allow to get the memory limit in octets.
3586     *
3587     * @since 1.4.5.0
3588     *
3589     * @return int the memory limit value in octet
3590     */
3591    public static function getMemoryLimit()
3592    {
3593        $memory_limit = @ini_get('memory_limit');
3594
3595        return Tools::getOctets($memory_limit);
3596    }
3597
3598    /**
3599     * Gets the value of a configuration option in octets.
3600     *
3601     * @since 1.5.0
3602     *
3603     * @return int the value of a configuration option in octets
3604     */
3605    public static function getOctets($option)
3606    {
3607        if (preg_match('/[0-9]+k/i', $option)) {
3608            return 1024 * (int) $option;
3609        }
3610
3611        if (preg_match('/[0-9]+m/i', $option)) {
3612            return 1024 * 1024 * (int) $option;
3613        }
3614
3615        if (preg_match('/[0-9]+g/i', $option)) {
3616            return 1024 * 1024 * 1024 * (int) $option;
3617        }
3618
3619        return $option;
3620    }
3621
3622    /**
3623     * @return bool true if the server use 64bit arch
3624     */
3625    public static function isX86_64arch()
3626    {
3627        return PHP_INT_MAX == '9223372036854775807';
3628    }
3629
3630    /**
3631     * @return bool true if php-cli is used
3632     */
3633    public static function isPHPCLI()
3634    {
3635        return defined('STDIN') || (Tools::strtolower(PHP_SAPI) == 'cli' && (!isset($_SERVER['REMOTE_ADDR']) || empty($_SERVER['REMOTE_ADDR'])));
3636    }
3637
3638    public static function argvToGET($argc, $argv)
3639    {
3640        if ($argc <= 1) {
3641            return;
3642        }
3643
3644        // get the first argument and parse it like a query string
3645        parse_str($argv[1], $args);
3646        if (!is_array($args) || !count($args)) {
3647            return;
3648        }
3649        $_GET = array_merge($args, $_GET);
3650        $_SERVER['QUERY_STRING'] = $argv[1];
3651    }
3652
3653    /**
3654     * Get max file upload size considering server settings and optional max value.
3655     *
3656     * @param int $max_size optional max file size
3657     *
3658     * @return int max file size in bytes
3659     */
3660    public static function getMaxUploadSize($max_size = 0)
3661    {
3662        $values = [Tools::convertBytes(ini_get('upload_max_filesize'))];
3663
3664        if ($max_size > 0) {
3665            $values[] = $max_size;
3666        }
3667
3668        $post_max_size = Tools::convertBytes(ini_get('post_max_size'));
3669        if ($post_max_size > 0) {
3670            $values[] = $post_max_size;
3671        }
3672
3673        return min($values);
3674    }
3675
3676    /**
3677     * apacheModExists return true if the apache module $name is loaded.
3678     *
3679     * @TODO move this method in class Information (when it will exist)
3680     *
3681     * Notes: This method requires either apache_get_modules or phpinfo()
3682     * to be available. With CGI mod, we cannot get php modules
3683     *
3684     * @param string $name module name
3685     *
3686     * @return bool true if exists
3687     *
3688     * @since 1.4.5.0
3689     */
3690    public static function apacheModExists($name)
3691    {
3692        if (function_exists('apache_get_modules')) {
3693            static $apache_module_list = null;
3694
3695            if (!is_array($apache_module_list)) {
3696                $apache_module_list = apache_get_modules();
3697            }
3698
3699            // we need strpos (example, evasive can be evasive20)
3700            foreach ($apache_module_list as $module) {
3701                if (strpos($module, $name) !== false) {
3702                    return true;
3703                }
3704            }
3705        }
3706
3707        return false;
3708    }
3709
3710    /**
3711     * Fix native uasort see: http://php.net/manual/en/function.uasort.php#114535.
3712     *
3713     * @param $array
3714     * @param $cmp_function
3715     */
3716    public static function uasort(&$array, $cmp_function)
3717    {
3718        if (count($array) < 2) {
3719            return;
3720        }
3721        $halfway = count($array) / 2;
3722        $array1 = array_slice($array, 0, $halfway, true);
3723        $array2 = array_slice($array, $halfway, null, true);
3724
3725        self::uasort($array1, $cmp_function);
3726        self::uasort($array2, $cmp_function);
3727        if (call_user_func($cmp_function, end($array1), reset($array2)) < 1) {
3728            $array = $array1 + $array2;
3729
3730            return;
3731        }
3732        $array = [];
3733        reset($array1);
3734        reset($array2);
3735        while (current($array1) && current($array2)) {
3736            if (call_user_func($cmp_function, current($array1), current($array2)) < 1) {
3737                $array[key($array1)] = current($array1);
3738                next($array1);
3739            } else {
3740                $array[key($array2)] = current($array2);
3741                next($array2);
3742            }
3743        }
3744        while (current($array1)) {
3745            $array[key($array1)] = current($array1);
3746            next($array1);
3747        }
3748        while (current($array2)) {
3749            $array[key($array2)] = current($array2);
3750            next($array2);
3751        }
3752    }
3753
3754    /**
3755     * Copy the folder $src into $dst, $dst is created if it do not exist.
3756     *
3757     * @param $src
3758     * @param $dst
3759     * @param bool $del if true, delete the file after copy
3760     */
3761    public static function recurseCopy($src, $dst, $del = false)
3762    {
3763        if (!Tools::file_exists_cache($src)) {
3764            return false;
3765        }
3766        $dir = opendir($src);
3767
3768        if (!Tools::file_exists_cache($dst)) {
3769            mkdir($dst);
3770        }
3771        while (false !== ($file = readdir($dir))) {
3772            if (($file != '.') && ($file != '..')) {
3773                if (is_dir($src . DIRECTORY_SEPARATOR . $file)) {
3774                    self::recurseCopy($src . DIRECTORY_SEPARATOR . $file, $dst . DIRECTORY_SEPARATOR . $file, $del);
3775                } else {
3776                    copy($src . DIRECTORY_SEPARATOR . $file, $dst . DIRECTORY_SEPARATOR . $file);
3777                    if ($del && is_writable($src . DIRECTORY_SEPARATOR . $file)) {
3778                        unlink($src . DIRECTORY_SEPARATOR . $file);
3779                    }
3780                }
3781            }
3782        }
3783        closedir($dir);
3784        if ($del && is_writable($src)) {
3785            rmdir($src);
3786        }
3787    }
3788
3789    /**
3790     * @param string $path Path to scan
3791     * @param string $ext Extention to filter files
3792     * @param string $dir Add this to prefix output for example /path/dir/*
3793     *
3794     * @return array List of file found
3795     *
3796     * @since 1.5.0
3797     */
3798    public static function scandir($path, $ext = 'php', $dir = '', $recursive = false)
3799    {
3800        $path = rtrim(rtrim($path, '\\'), '/') . '/';
3801        $real_path = rtrim(rtrim($path . $dir, '\\'), '/') . '/';
3802        $files = scandir($real_path, SCANDIR_SORT_NONE);
3803        if (!$files) {
3804            return [];
3805        }
3806
3807        $filtered_files = [];
3808
3809        $real_ext = false;
3810        if (!empty($ext)) {
3811            $real_ext = '.' . $ext;
3812        }
3813        $real_ext_length = strlen($real_ext);
3814
3815        $subdir = ($dir) ? $dir . '/' : '';
3816        foreach ($files as $file) {
3817            if (!$real_ext || (strpos($file, $real_ext) && strpos($file, $real_ext) == (strlen($file) - $real_ext_length))) {
3818                $filtered_files[] = $subdir . $file;
3819            }
3820
3821            if ($recursive && $file[0] != '.' && is_dir($real_path . $file)) {
3822                foreach (Tools::scandir($path, $ext, $subdir . $file, $recursive) as $subfile) {
3823                    $filtered_files[] = $subfile;
3824                }
3825            }
3826        }
3827
3828        return $filtered_files;
3829    }
3830
3831    /**
3832     * Align version sent and use internal function.
3833     *
3834     * @param $v1
3835     * @param $v2
3836     * @param string $operator
3837     *
3838     * @return mixed
3839     */
3840    public static function version_compare($v1, $v2, $operator = '<')
3841    {
3842        Tools::alignVersionNumber($v1, $v2);
3843
3844        return version_compare($v1, $v2, $operator);
3845    }
3846
3847    /**
3848     * Align 2 version with the same number of sub version
3849     * version_compare will work better for its comparison :)
3850     * (Means: '1.8' to '1.9.3' will change '1.8' to '1.8.0').
3851     *
3852     * @param $v1
3853     * @param $v2
3854     */
3855    public static function alignVersionNumber(&$v1, &$v2)
3856    {
3857        $len1 = count(explode('.', trim($v1, '.')));
3858        $len2 = count(explode('.', trim($v2, '.')));
3859        $len = 0;
3860        $str = '';
3861
3862        if ($len1 > $len2) {
3863            $len = $len1 - $len2;
3864            $str = &$v2;
3865        } elseif ($len2 > $len1) {
3866            $len = $len2 - $len1;
3867            $str = &$v1;
3868        }
3869
3870        for ($len; $len > 0; --$len) {
3871            $str .= '.0';
3872        }
3873    }
3874
3875    public static function modRewriteActive()
3876    {
3877        if (Tools::apacheModExists('mod_rewrite')) {
3878            return true;
3879        }
3880        if ((isset($_SERVER['HTTP_MOD_REWRITE']) && Tools::strtolower($_SERVER['HTTP_MOD_REWRITE']) == 'on') || Tools::strtolower(getenv('HTTP_MOD_REWRITE')) == 'on') {
3881            return true;
3882        }
3883
3884        return false;
3885    }
3886
3887    public static function unSerialize($serialized, $object = false)
3888    {
3889        if (is_string($serialized) && (strpos($serialized, 'O:') === false || !preg_match('/(^|;|{|})O:[0-9]+:"/', $serialized)) && !$object || $object) {
3890            return @unserialize($serialized);
3891        }
3892
3893        return false;
3894    }
3895
3896    /**
3897     * Reproduce array_unique working before php version 5.2.9.
3898     *
3899     * @param array $array
3900     *
3901     * @return array
3902     */
3903    public static function arrayUnique($array)
3904    {
3905        return array_unique($array, SORT_REGULAR);
3906    }
3907
3908    /**
3909     * Delete unicode class from regular expression patterns.
3910     *
3911     * @deprecated Use PrestaShop\PrestaShop\Core\String\CharacterCleaner::cleanNonUnicodeSupport() instead
3912     *
3913     * @param string $pattern
3914     *
3915     * @return string pattern
3916     *
3917     * @throws Exception
3918     */
3919    public static function cleanNonUnicodeSupport($pattern)
3920    {
3921        $context = Context::getContext();
3922        $containerFinder = new ContainerFinder($context);
3923        try {
3924            $container = $containerFinder->getContainer();
3925            $characterCleaner = $container->get('prestashop.core.string.character_cleaner');
3926        } catch (ContainerNotFoundException $e) {
3927            // Used when the container is not generated
3928            $characterCleaner = new CharacterCleaner();
3929        }
3930
3931        return $characterCleaner->cleanNonUnicodeSupport($pattern);
3932    }
3933
3934    protected static $is_addons_up = true;
3935
3936    public static function addonsRequest($request, $params = [])
3937    {
3938        if (!self::$is_addons_up) {
3939            return false;
3940        }
3941
3942        $post_query_data = [
3943            'version' => isset($params['version']) ? $params['version'] : _PS_VERSION_,
3944            'iso_lang' => Tools::strtolower(isset($params['iso_lang']) ? $params['iso_lang'] : Context::getContext()->language->iso_code),
3945            'iso_code' => Tools::strtolower(isset($params['iso_country']) ? $params['iso_country'] : Country::getIsoById(Configuration::get('PS_COUNTRY_DEFAULT'))),
3946            'shop_url' => isset($params['shop_url']) ? $params['shop_url'] : Tools::getShopDomain(),
3947            'mail' => isset($params['email']) ? $params['email'] : Configuration::get('PS_SHOP_EMAIL'),
3948            'format' => isset($params['format']) ? $params['format'] : 'xml',
3949        ];
3950        if (isset($params['source'])) {
3951            $post_query_data['source'] = $params['source'];
3952        }
3953
3954        $post_data = http_build_query($post_query_data);
3955
3956        $end_point = 'api.addons.prestashop.com';
3957
3958        switch ($request) {
3959            case 'native':
3960                $post_data .= '&method=listing&action=native';
3961
3962                break;
3963            case 'partner':
3964                $post_data .= '&method=listing&action=partner';
3965
3966                break;
3967            case 'service':
3968                $post_data .= '&method=listing&action=service';
3969
3970                break;
3971            case 'native_all':
3972                $post_data .= '&method=listing&action=native&iso_code=all';
3973
3974                break;
3975            case 'must-have':
3976                $post_data .= '&method=listing&action=must-have';
3977
3978                break;
3979            case 'must-have-themes':
3980                $post_data .= '&method=listing&action=must-have-themes';
3981
3982                break;
3983            case 'customer':
3984                $post_data .= '&method=listing&action=customer&username=' . urlencode(trim(Context::getContext()->cookie->username_addons))
3985                    . '&password=' . urlencode(trim(Context::getContext()->cookie->password_addons));
3986
3987                break;
3988            case 'customer_themes':
3989                $post_data .= '&method=listing&action=customer-themes&username=' . urlencode(trim(Context::getContext()->cookie->username_addons))
3990                    . '&password=' . urlencode(trim(Context::getContext()->cookie->password_addons));
3991
3992                break;
3993            case 'check_customer':
3994                $post_data .= '&method=check_customer&username=' . urlencode($params['username_addons']) . '&password=' . urlencode($params['password_addons']);
3995
3996                break;
3997            case 'check_module':
3998                $post_data .= '&method=check&module_name=' . urlencode($params['module_name']) . '&module_key=' . urlencode($params['module_key']);
3999
4000                break;
4001            case 'module':
4002                $post_data .= '&method=module&id_module=' . urlencode($params['id_module']);
4003                if (isset($params['username_addons'], $params['password_addons'])) {
4004                    $post_data .= '&username=' . urlencode($params['username_addons']) . '&password=' . urlencode($params['password_addons']);
4005                }
4006
4007                break;
4008            case 'hosted_module':
4009                $post_data .= '&method=module&id_module=' . urlencode((int) $params['id_module']) . '&username=' . urlencode($params['hosted_email'])
4010                    . '&password=' . urlencode($params['password_addons'])
4011                    . '&shop_url=' . urlencode(isset($params['shop_url']) ? $params['shop_url'] : Tools::getShopDomain())
4012                    . '&mail=' . urlencode(isset($params['email']) ? $params['email'] : Configuration::get('PS_SHOP_EMAIL'));
4013
4014                break;
4015            case 'install-modules':
4016                $post_data .= '&method=listing&action=install-modules';
4017                $post_data .= defined('_PS_HOST_MODE_') ? '-od' : '';
4018
4019                break;
4020            default:
4021                return false;
4022        }
4023
4024        $context = stream_context_create([
4025            'http' => [
4026                'method' => 'POST',
4027                'content' => $post_data,
4028                'header' => 'Content-type: application/x-www-form-urlencoded',
4029                'timeout' => 5,
4030            ],
4031        ]);
4032
4033        if ($content = Tools::file_get_contents('https://' . $end_point, false, $context)) {
4034            return $content;
4035        }
4036
4037        self::$is_addons_up = false;
4038
4039        return false;
4040    }
4041
4042    /**
4043     * Returns an array containing information about
4044     * HTTP file upload variable ($_FILES).
4045     *
4046     * @param string $input File upload field name
4047     * @param bool $return_content If true, returns uploaded file contents
4048     *
4049     * @return array|null
4050     */
4051    public static function fileAttachment($input = 'fileUpload', $return_content = true)
4052    {
4053        $file_attachment = null;
4054        if (isset($_FILES[$input]['name']) && !empty($_FILES[$input]['name']) && !empty($_FILES[$input]['tmp_name'])) {
4055            $file_attachment['rename'] = uniqid() . Tools::strtolower(substr($_FILES[$input]['name'], -5));
4056            if ($return_content) {
4057                $file_attachment['content'] = file_get_contents($_FILES[$input]['tmp_name']);
4058            }
4059            $file_attachment['tmp_name'] = $_FILES[$input]['tmp_name'];
4060            $file_attachment['name'] = $_FILES[$input]['name'];
4061            $file_attachment['mime'] = $_FILES[$input]['type'];
4062            $file_attachment['error'] = $_FILES[$input]['error'];
4063            $file_attachment['size'] = $_FILES[$input]['size'];
4064        }
4065
4066        return $file_attachment;
4067    }
4068
4069    public static function changeFileMTime($file_name)
4070    {
4071        @touch($file_name);
4072    }
4073
4074    public static function waitUntilFileIsModified($file_name, $timeout = 180)
4075    {
4076        @ini_set('max_execution_time', $timeout);
4077        if (($time_limit = ini_get('max_execution_time')) === null) {
4078            $time_limit = 30;
4079        }
4080
4081        $time_limit -= 5;
4082        $start_time = microtime(true);
4083        $last_modified = @filemtime($file_name);
4084
4085        while (true) {
4086            if (((microtime(true) - $start_time) > $time_limit) || @filemtime($file_name) > $last_modified) {
4087                break;
4088            }
4089            clearstatcache();
4090            usleep(300);
4091        }
4092    }
4093
4094    /**
4095     * Delete a substring from another one starting from the right.
4096     *
4097     * @param string $str
4098     * @param string $str_search
4099     *
4100     * @return string
4101     */
4102    public static function rtrimString($str, $str_search)
4103    {
4104        $length_str = strlen($str_search);
4105        if (strlen($str) >= $length_str && substr($str, -$length_str) == $str_search) {
4106            $str = substr($str, 0, -$length_str);
4107        }
4108
4109        return $str;
4110    }
4111
4112    /**
4113     * Format a number into a human readable format
4114     * e.g. 24962496 => 23.81M.
4115     *
4116     * @param $size
4117     * @param int $precision
4118     *
4119     * @return string
4120     */
4121    public static function formatBytes($size, $precision = 2)
4122    {
4123        if (!$size) {
4124            return '0';
4125        }
4126        $base = log($size) / log(1024);
4127        $suffixes = ['', 'KB', 'MB', 'GB', 'TB'];
4128
4129        return round(1024 ** ($base - floor($base)), $precision) . Context::getContext()->getTranslator()->trans($suffixes[floor($base)], [], 'Shop.Theme.Catalog');
4130    }
4131
4132    public static function boolVal($value)
4133    {
4134        if (empty($value)) {
4135            $value = false;
4136        }
4137
4138        return (bool) $value;
4139    }
4140
4141    public static function getUserPlatform()
4142    {
4143        if (isset(self::$_user_plateform)) {
4144            return self::$_user_plateform;
4145        }
4146
4147        $user_agent = $_SERVER['HTTP_USER_AGENT'];
4148        self::$_user_plateform = 'unknown';
4149
4150        if (preg_match('/linux/i', $user_agent)) {
4151            self::$_user_plateform = 'Linux';
4152        } elseif (preg_match('/macintosh|mac os x/i', $user_agent)) {
4153            self::$_user_plateform = 'Mac';
4154        } elseif (preg_match('/windows|win32/i', $user_agent)) {
4155            self::$_user_plateform = 'Windows';
4156        }
4157
4158        return self::$_user_plateform;
4159    }
4160
4161    public static function getUserBrowser()
4162    {
4163        if (isset(self::$_user_browser)) {
4164            return self::$_user_browser;
4165        }
4166
4167        $user_agent = $_SERVER['HTTP_USER_AGENT'];
4168        self::$_user_browser = 'unknown';
4169
4170        if (preg_match('/MSIE/i', $user_agent) && !preg_match('/Opera/i', $user_agent)) {
4171            self::$_user_browser = 'Internet Explorer';
4172        } elseif (preg_match('/Firefox/i', $user_agent)) {
4173            self::$_user_browser = 'Mozilla Firefox';
4174        } elseif (preg_match('/Chrome/i', $user_agent)) {
4175            self::$_user_browser = 'Google Chrome';
4176        } elseif (preg_match('/Safari/i', $user_agent)) {
4177            self::$_user_browser = 'Apple Safari';
4178        } elseif (preg_match('/Opera/i', $user_agent)) {
4179            self::$_user_browser = 'Opera';
4180        } elseif (preg_match('/Netscape/i', $user_agent)) {
4181            self::$_user_browser = 'Netscape';
4182        }
4183
4184        return self::$_user_browser;
4185    }
4186
4187    /**
4188     * Allows to display the category description without HTML tags and slashes.
4189     *
4190     * @return string
4191     */
4192    public static function getDescriptionClean($description)
4193    {
4194        return strip_tags(stripslashes($description));
4195    }
4196
4197    public static function purifyHTML($html, $uri_unescape = null, $allow_style = false)
4198    {
4199        static $use_html_purifier = null;
4200        static $purifier = null;
4201
4202        if (defined('PS_INSTALLATION_IN_PROGRESS') || !Configuration::configurationIsLoaded()) {
4203            return $html;
4204        }
4205
4206        if ($use_html_purifier === null) {
4207            $use_html_purifier = (bool) Configuration::get('PS_USE_HTMLPURIFIER');
4208        }
4209
4210        if ($use_html_purifier) {
4211            if ($purifier === null) {
4212                $config = HTMLPurifier_Config::createDefault();
4213
4214                $config->set('Attr.EnableID', true);
4215                $config->set('Attr.AllowedRel', ['nofollow']);
4216                $config->set('HTML.Trusted', true);
4217                $config->set('Cache.SerializerPath', _PS_CACHE_DIR_ . 'purifier');
4218                $config->set('Attr.AllowedFrameTargets', ['_blank', '_self', '_parent', '_top']);
4219                if (is_array($uri_unescape)) {
4220                    $config->set('URI.UnescapeCharacters', implode('', $uri_unescape));
4221                }
4222
4223                if (Configuration::get('PS_ALLOW_HTML_IFRAME')) {
4224                    $config->set('HTML.SafeIframe', true);
4225                    $config->set('HTML.SafeObject', true);
4226                    $config->set('URI.SafeIframeRegexp', '/.*/');
4227                }
4228
4229                /** @var HTMLPurifier_HTMLDefinition|HTMLPurifier_HTMLModule $def */
4230                // http://developers.whatwg.org/the-video-element.html#the-video-element
4231                if ($def = $config->getHTMLDefinition(true)) {
4232                    $def->addElement('video', 'Block', 'Optional: (source, Flow) | (Flow, source) | Flow', 'Common', [
4233                        'src' => 'URI',
4234                        'type' => 'Text',
4235                        'width' => 'Length',
4236                        'height' => 'Length',
4237                        'poster' => 'URI',
4238                        'preload' => 'Enum#auto,metadata,none',
4239                        'controls' => 'Bool',
4240                    ]);
4241                    $def->addElement('source', 'Block', 'Flow', 'Common', [
4242                        'src' => 'URI',
4243                        'type' => 'Text',
4244                    ]);
4245                    if ($allow_style) {
4246                        $def->addElement('style', 'Block', 'Flow', 'Common', ['type' => 'Text']);
4247                    }
4248                }
4249
4250                $purifier = new HTMLPurifier($config);
4251            }
4252
4253            $html = $purifier->purify($html);
4254        }
4255
4256        return $html;
4257    }
4258
4259    /**
4260     * Check if a constant was already defined.
4261     *
4262     * @param string $constant Constant name
4263     * @param mixed $value Default value to set if not defined
4264     */
4265    public static function safeDefine($constant, $value)
4266    {
4267        if (!defined($constant)) {
4268            define($constant, $value);
4269        }
4270    }
4271
4272    /**
4273     * Spread an amount on lines, adjusting the $column field,
4274     * with the biggest adjustments going to the rows having the
4275     * highest $sort_column.
4276     *
4277     * E.g.:
4278     *
4279     * $rows = [['a' => 5.1], ['a' => 8.2]];
4280     *
4281     * spreadAmount(0.3, 1, $rows, 'a');
4282     *
4283     * => $rows is [['a' => 8.4], ['a' => 5.2]]
4284     *
4285     * @param $amount float  The amount to spread across the rows
4286     * @param $precision int Rounding precision
4287     *                       e.g. if $amount is 1, $precision is 0 and $rows = [['a' => 2], ['a' => 1]]
4288     *                       then the resulting $rows will be [['a' => 3], ['a' => 1]]
4289     *                       But if $precision were 1, then the resulting $rows would be [['a' => 2.5], ['a' => 1.5]]
4290     * @param &$rows array   An array, associative or not, containing arrays that have at least $column and $sort_column fields
4291     * @param $column string The column on which to perform adjustments
4292     */
4293    public static function spreadAmount($amount, $precision, &$rows, $column)
4294    {
4295        if (!is_array($rows) || empty($rows)) {
4296            return;
4297        }
4298
4299        $sort_function = function ($a, $b) use ($column) { return $b[$column] > $a[$column] ? 1 : -1; };
4300
4301        uasort($rows, $sort_function);
4302
4303        $unit = 10 ** $precision;
4304
4305        $int_amount = (int) round($unit * $amount);
4306
4307        $remainder = $int_amount % count($rows);
4308        $amount_to_spread = ($int_amount - $remainder) / count($rows) / $unit;
4309
4310        $sign = ($amount >= 0 ? 1 : -1);
4311        $position = 0;
4312        foreach ($rows as &$row) {
4313            $adjustment_factor = $amount_to_spread;
4314
4315            if ($position < abs($remainder)) {
4316                $adjustment_factor += $sign * 1 / $unit;
4317            }
4318
4319            $row[$column] += $adjustment_factor;
4320
4321            ++$position;
4322        }
4323        unset($row);
4324    }
4325
4326    /**
4327     * Replaces elements from passed arrays into the first array recursively.
4328     *
4329     * @param array $base the array in which elements are replaced
4330     * @param array $replacements the array from which elements will be extracted
4331     */
4332    public static function arrayReplaceRecursive($base, $replacements)
4333    {
4334        if (function_exists('array_replace_recursive')) {
4335            return array_replace_recursive($base, $replacements);
4336        }
4337
4338        foreach (array_slice(func_get_args(), 1) as $replacements) {
4339            $bref_stack = [&$base];
4340            $head_stack = [$replacements];
4341
4342            do {
4343                end($bref_stack);
4344
4345                $bref = &$bref_stack[key($bref_stack)];
4346                $head = array_pop($head_stack);
4347                unset($bref_stack[key($bref_stack)]);
4348                foreach (array_keys($head) as $key) {
4349                    if (isset($key, $bref) && is_array($bref[$key]) && is_array($head[$key])) {
4350                        $bref_stack[] = &$bref[$key];
4351                        $head_stack[] = $head[$key];
4352                    } else {
4353                        $bref[$key] = $head[$key];
4354                    }
4355                }
4356            } while (count($head_stack));
4357        }
4358
4359        return $base;
4360    }
4361
4362    /**
4363     * Return path to a Product or a CMS category.
4364     *
4365     * @param string $url_base Start URL
4366     * @param int $id_category Start category
4367     * @param string $path Current path
4368     * @param string $highlight String to highlight (in XHTML/CSS)
4369     * @param string $type Category type (products/cms)
4370     */
4371    public static function getPath($url_base, $id_category, $path = '', $highlight = '', $category_type = 'catalog', $home = false)
4372    {
4373        $context = Context::getContext();
4374        if ($category_type == 'catalog') {
4375            $category = Db::getInstance()->getRow('
4376		SELECT id_category, level_depth, nleft, nright
4377		FROM ' . _DB_PREFIX_ . 'category
4378		WHERE id_category = ' . (int) $id_category);
4379            if (isset($category['id_category'])) {
4380                $sql = 'SELECT c.id_category, cl.name, cl.link_rewrite
4381					FROM ' . _DB_PREFIX_ . 'category c
4382					LEFT JOIN ' . _DB_PREFIX_ . 'category_lang cl ON (cl.id_category = c.id_category' . Shop::addSqlRestrictionOnLang('cl') . ')
4383					WHERE c.nleft <= ' . (int) $category['nleft'] . '
4384						AND c.nright >= ' . (int) $category['nright'] . '
4385						AND cl.id_lang = ' . (int) $context->language->id .
4386                       ($home ? ' AND c.id_category=' . (int) $id_category : '') . '
4387						AND c.id_category != ' . (int) Category::getTopCategory()->id . '
4388					GROUP BY c.id_category
4389					ORDER BY c.level_depth ASC
4390					LIMIT ' . (!$home ? (int) $category['level_depth'] + 1 : 1);
4391                $categories = Db::getInstance()->executeS($sql);
4392                $full_path = '';
4393                $n = 1;
4394                $n_categories = (int) count($categories);
4395                foreach ($categories as $category) {
4396                    $action = (($category['id_category'] == (int) Configuration::get('PS_HOME_CATEGORY') || $home) ? 'index' : 'updatecategory');
4397                    $link_params = ['action' => $action, 'id_category' => (int) $category['id_category']];
4398                    $edit_link = Context::getContext()->link->getAdminLink('AdminCategories', true, $link_params);
4399                    $link_params['action'] = 'index';
4400                    $index_link = Context::getContext()->link->getAdminLink('AdminCategories', true, $link_params);
4401                    $edit = '<a href="' . Tools::safeOutput($edit_link) . '" title="' . ($category['id_category'] == Category::getRootCategory()->id_category ? 'Home' : 'Modify') . '"><i class="icon-' . (($category['id_category'] == Category::getRootCategory()->id_category || $home) ? 'home' : 'pencil') . '"></i></a> ';
4402                    $full_path .= $edit .
4403                                  ($n < $n_categories ? '<a href="' . Tools::safeOutput($index_link) . '" title="' . htmlentities($category['name'], ENT_NOQUOTES, 'UTF-8') . '">' : '') .
4404                                  (!empty($highlight) ? str_ireplace($highlight, '<span class="highlight">' . htmlentities($highlight, ENT_NOQUOTES, 'UTF-8') . '</span>', $category['name']) : $category['name']) .
4405                                  ($n < $n_categories ? '</a>' : '') .
4406                                  (($n++ != $n_categories || !empty($path)) ? ' > ' : '');
4407                }
4408
4409                return $full_path . $path;
4410            }
4411        } elseif ($category_type == 'cms') {
4412            $category = new CMSCategory($id_category, $context->language->id);
4413            if (!$category->id) {
4414                return $path;
4415            }
4416            $name = ($highlight != null) ? str_ireplace($highlight, '<span class="highlight">' . $highlight . '</span>', CMSCategory::hideCMSCategoryPosition($category->name)) : CMSCategory::hideCMSCategoryPosition($category->name);
4417            $edit = '<a href="' . Tools::safeOutput($url_base . '&id_cms_category=' . $category->id . '&updatecms_category&token=' . Tools::getAdminToken('AdminCmsContent' . (int) Tab::getIdFromClassName('AdminCmsContent') . (int) $context->employee->id)) . '">
4418				<i class="icon-pencil"></i></a> ';
4419            if ($category->id == 1) {
4420                $edit = '<li><a href="' . Tools::safeOutput($url_base . '&id_cms_category=' . $category->id . '&viewcategory&token=' . Tools::getAdminToken('AdminCmsContent' . (int) Tab::getIdFromClassName('AdminCmsContent') . (int) $context->employee->id)) . '">
4421					<i class="icon-home"></i></a></li> ';
4422            }
4423            $path = $edit . '<li><a href="' . Tools::safeOutput($url_base . '&id_cms_category=' . $category->id . '&viewcategory&token=' . Tools::getAdminToken('AdminCmsContent' . (int) Tab::getIdFromClassName('AdminCmsContent') . (int) $context->employee->id)) . '">
4424		' . $name . '</a></li> > ' . $path;
4425            if ($category->id == 1) {
4426                return substr($path, 0, strlen($path) - 3);
4427            }
4428
4429            return Tools::getPath($url_base, $category->id_parent, $path, '', 'cms');
4430        }
4431    }
4432
4433    public static function redirectToInstall()
4434    {
4435        if (file_exists(__DIR__ . '/../install')) {
4436            if (defined('_PS_ADMIN_DIR_')) {
4437                header('Location: ../install/');
4438            } else {
4439                header('Location: install/');
4440            }
4441        } elseif (file_exists(__DIR__ . '/../install-dev')) {
4442            if (defined('_PS_ADMIN_DIR_')) {
4443                header('Location: ../install-dev/');
4444            } else {
4445                header('Location: install-dev/');
4446            }
4447        } else {
4448            die('Error: "install" directory is missing');
4449        }
4450        exit;
4451    }
4452
4453    /**
4454     * @param array $fallbackParameters
4455     */
4456    public static function setFallbackParameters(array $fallbackParameters): void
4457    {
4458        static::$fallbackParameters = $fallbackParameters;
4459    }
4460
4461    /**
4462     * @param string $file_to_refresh
4463     * @param string $external_file
4464     *
4465     * @return bool
4466     */
4467    public static function refreshFile(string $file_to_refresh, string $external_file): bool
4468    {
4469        return (bool) static::copy($external_file, _PS_ROOT_DIR_ . $file_to_refresh);
4470    }
4471
4472    /**
4473     * @param string $file
4474     * @param int $timeout
4475     *
4476     * @return bool
4477     */
4478    public static function isFileFresh(string $file, int $timeout = self::CACHE_LIFETIME_SECONDS): bool
4479    {
4480        if (($time = @filemtime(_PS_ROOT_DIR_ . $file)) && filesize(_PS_ROOT_DIR_ . $file) > 0) {
4481            return (time() - $time) < $timeout;
4482        }
4483
4484        return false;
4485    }
4486}
4487
4488/**
4489 * Compare 2 prices to sort products.
4490 *
4491 * @param float $a
4492 * @param float $b
4493 *
4494 * @return int
4495 */
4496/* Externalized because of a bug in PHP 5.1.6 when inside an object */
4497function cmpPriceAsc($a, $b)
4498{
4499    if ((float) $a['price_tmp'] < (float) $b['price_tmp']) {
4500        return -1;
4501    } elseif ((float) $a['price_tmp'] > (float) $b['price_tmp']) {
4502        return 1;
4503    }
4504
4505    return 0;
4506}
4507
4508/**
4509 * @param array $a
4510 * @param array $b
4511 *
4512 * @return int
4513 */
4514function cmpPriceDesc($a, $b)
4515{
4516    if ((float) $a['price_tmp'] < (float) $b['price_tmp']) {
4517        return 1;
4518    } elseif ((float) $a['price_tmp'] > (float) $b['price_tmp']) {
4519        return -1;
4520    }
4521
4522    return 0;
4523}
4524