1<?php
2/**
3 * Matomo - free/libre analytics platform
4 *
5 * @link https://matomo.org
6 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
7 *
8 */
9namespace Piwik;
10
11use Exception;
12use Piwik\CliMulti\Process;
13use Piwik\Container\StaticContainer;
14use Piwik\Intl\Data\Provider\LanguageDataProvider;
15use Piwik\Intl\Data\Provider\RegionDataProvider;
16use Piwik\Plugins\UserCountry\LocationProvider\DefaultProvider;
17use Piwik\Tracker\Cache as TrackerCache;
18
19/**
20 * Contains helper methods used by both Piwik Core and the Piwik Tracking engine.
21 *
22 * This is the only non-Tracker class loaded by the **\/piwik.php** file.
23 */
24class Common
25{
26    // constants used to map the referrer type to an integer in the log_visit table
27    const REFERRER_TYPE_DIRECT_ENTRY = 1;
28    const REFERRER_TYPE_SEARCH_ENGINE = 2;
29    const REFERRER_TYPE_WEBSITE = 3;
30    const REFERRER_TYPE_CAMPAIGN = 6;
31    const REFERRER_TYPE_SOCIAL_NETWORK = 7;
32
33    // Flag used with htmlspecialchar. See php.net/htmlspecialchars.
34    const HTML_ENCODING_QUOTE_STYLE = ENT_QUOTES;
35
36    public static $isCliMode = null;
37
38    /*
39     * Database
40     */
41    const LANGUAGE_CODE_INVALID = 'xx';
42
43    /**
44     * Hashes a string into an integer which should be very low collision risks
45     * @param string $string String to hash
46     * @return int  Resulting int hash
47     */
48    public static function hashStringToInt($string)
49    {
50        $stringHash = substr(md5($string), 0, 8);
51        return base_convert($stringHash, 16, 10);
52    }
53
54    /**
55     * Returns a prefixed table name.
56     *
57     * The table prefix is determined by the `[database] tables_prefix` INI config
58     * option.
59     *
60     * @param string $table The table name to prefix, ie "log_visit"
61     * @return string  The prefixed name, ie "piwik-production_log_visit".
62     * @api
63     */
64    public static function prefixTable($table)
65    {
66        $prefix = Config::getInstance()->database['tables_prefix'];
67        return $prefix . $table;
68    }
69
70    /**
71     * Returns an array containing the prefixed table names of every passed argument.
72     *
73     * @param string ... The table names to prefix, ie "log_visit"
74     * @return array The prefixed names in an array.
75     */
76    public static function prefixTables()
77    {
78        $result = array();
79        foreach (func_get_args() as $table) {
80            $result[] = self::prefixTable($table);
81        }
82        return $result;
83    }
84
85    /**
86     * Removes the prefix from a table name and returns the result.
87     *
88     * The table prefix is determined by the `[database] tables_prefix` INI config
89     * option.
90     *
91     * @param string $table The prefixed table name, eg "piwik-production_log_visit".
92     * @return string The unprefixed table name, eg "log_visit".
93     * @api
94     */
95    public static function unprefixTable($table)
96    {
97        static $prefixTable = null;
98        if (is_null($prefixTable)) {
99            $prefixTable = Config::getInstance()->database['tables_prefix'];
100        }
101        if (empty($prefixTable)
102            || strpos($table, $prefixTable) !== 0
103        ) {
104            return $table;
105        }
106        $count = 1;
107        return str_replace($prefixTable, '', $table, $count);
108    }
109
110    /*
111     * Tracker
112     */
113    public static function isGoalPluginEnabled()
114    {
115        return Plugin\Manager::getInstance()->isPluginActivated('Goals');
116    }
117
118    public static function isActionsPluginEnabled()
119    {
120        return Plugin\Manager::getInstance()->isPluginActivated('Actions');
121    }
122
123    /**
124     * Returns true if PHP was invoked from command-line interface (shell)
125     *
126     * @since added in 0.4.4
127     * @return bool true if PHP invoked as a CGI or from CLI
128     */
129    public static function isPhpCliMode()
130    {
131        if (is_bool(self::$isCliMode)) {
132            return self::$isCliMode;
133        }
134
135        if(PHP_SAPI === 'cli'){
136            return true;
137        }
138
139        if(self::isPhpCgiType() && (!isset($_SERVER['REMOTE_ADDR']) || empty($_SERVER['REMOTE_ADDR']))){
140            return true;
141        }
142
143        return false;
144    }
145
146    /**
147     * Returns true if PHP is executed as CGI type.
148     *
149     * @since added in 0.4.4
150     * @return bool true if PHP invoked as a CGI
151     */
152    public static function isPhpCgiType()
153    {
154        $sapiType = php_sapi_name();
155
156        return substr($sapiType, 0, 3) === 'cgi';
157    }
158
159    /**
160     * Returns true if the current request is a console command, eg.
161     * ./console xx:yy
162     * or
163     * php console xx:yy
164     *
165     * @return bool
166     */
167    public static function isRunningConsoleCommand()
168    {
169        $searched = 'console';
170        $consolePos = strpos($_SERVER['SCRIPT_NAME'], $searched);
171        $expectedConsolePos = strlen($_SERVER['SCRIPT_NAME']) - strlen($searched);
172        $isScriptIsConsole = ($consolePos === $expectedConsolePos);
173        return self::isPhpCliMode() && $isScriptIsConsole;
174    }
175
176    /*
177     * String operations
178     */
179
180    /**
181     * Multi-byte substr() - works with UTF-8.
182     *
183     * Calls `mb_substr` if available and falls back to `substr` if it's not.
184     *
185     * @param string $string
186     * @param int $start
187     * @param int|null $length      optional length
188     * @return string
189     * @deprecated since 4.4 - directly use mb_substr instead
190     */
191    public static function mb_substr($string, $start, $length = null)
192    {
193        return mb_substr($string, $start, $length, 'UTF-8');
194    }
195
196    /**
197     * Gets the current process ID.
198     * Note: If getmypid is disabled, a random ID will be generated once and used throughout the request. There is a
199     * small chance that two processes at the same time may generated the same random ID. If you need to rely on the
200     * value being 100% unique, then you may need to use `getmypid` directly or some other logic. Eg in CliMulti it is
201     * fine to use `getmypid` directly as the logic won't be used if getmypid is disabled...
202     * If you are wanting to use the pid to check if the process is running eg using `ps`, then you also have to use
203     * getmypid directly.
204     *
205     * @return int|null
206     */
207    public static function getProcessId()
208    {
209        static $pid;
210        if (!isset($pid)) {
211            if (Process::isMethodDisabled('getmypid')) {
212                $pid = Common::getRandomInt(12);
213            } else {
214                $pid = getmypid();
215            }
216        }
217
218        return $pid;
219    }
220
221    /**
222     * Multi-byte strlen() - works with UTF-8
223     *
224     * Calls `mb_substr` if available and falls back to `substr` if not.
225     *
226     * @param string $string
227     * @return int
228     * @deprecated since 4.4 - directly use mb_strlen instead
229     */
230    public static function mb_strlen($string)
231    {
232        return mb_strlen($string, 'UTF-8');
233    }
234
235    /**
236     * Multi-byte strtolower() - works with UTF-8.
237     *
238     * Calls `mb_strtolower` if available and falls back to `strtolower` if not.
239     *
240     * @param string $string
241     * @return string
242     * @deprecated since 4.4 - directly use mb_strtolower instead
243     */
244    public static function mb_strtolower($string)
245    {
246        return mb_strtolower($string, 'UTF-8');
247    }
248
249    /**
250     * Multi-byte strtoupper() - works with UTF-8.
251     *
252     * Calls `mb_strtoupper` if available and falls back to `strtoupper` if not.
253     *
254     * @param string $string
255     * @return string
256     * @deprecated since 4.4 - directly use mb_strtoupper instead
257     */
258    public static function mb_strtoupper($string)
259    {
260        return mb_strtoupper($string, 'UTF-8');
261    }
262
263    /**
264     * Timing attack safe string comparison.
265     *
266     * @param string $stringA
267     * @param string $stringB
268     * @return bool
269     */
270    public static function hashEquals(string $stringA, string $stringB)
271    {
272        if (function_exists('hash_equals')) {
273            return hash_equals($stringA, $stringB);
274        }
275
276        if (strlen($stringA) !== strlen($stringB)) {
277            return false;
278        }
279
280        $result = "\0";
281        $stringA^= $stringB;
282        for ($i = 0; $i < strlen($stringA); $i++) {
283            $result|= $stringA[$i];
284        }
285
286        return $result === "\0";
287    }
288
289    /**
290     * Secure wrapper for unserialize, which by default disallows unserializing classes
291     *
292     * @param string $string String to unserialize
293     * @param array $allowedClasses Class names that should be allowed to unserialize
294     * @param bool $rethrow Whether to rethrow exceptions or not.
295     * @return mixed
296     */
297    public static function safe_unserialize($string, $allowedClasses = [], $rethrow = false)
298    {
299        try {
300            // phpcs:ignore Generic.PHP.ForbiddenFunctions
301            return unserialize($string ?? '', ['allowed_classes' => empty($allowedClasses) ? false : $allowedClasses]);
302        } catch (\Throwable $e) {
303            if ($rethrow) {
304                throw $e;
305            }
306
307            $logger = StaticContainer::get('Psr\Log\LoggerInterface');
308            $logger->debug('Unable to unserialize a string: {exception} (string = {string})', [
309                'exception' => $e,
310                'string' => $string,
311            ]);
312            return false;
313        }
314    }
315
316    /*
317     * Escaping input
318     */
319
320    /**
321     * Sanitizes a string to help avoid XSS vulnerabilities.
322     *
323     * This function is automatically called when {@link getRequestVar()} is called,
324     * so you should not normally have to use it.
325     *
326     * This function should be used when outputting data that isn't escaped and was
327     * obtained from the user (for example when using the `|raw` twig filter on goal names).
328     *
329     * _NOTE: Sanitized input should not be used directly in an SQL query; SQL placeholders
330     * should still be used._
331     *
332     * **Implementation Details**
333     *
334     * - [htmlspecialchars](http://php.net/manual/en/function.htmlspecialchars.php) is used to escape text.
335     * - Single quotes are not escaped so **Piwik's amazing community** will still be
336     *   **Piwik's amazing community**.
337     * - Use of the `magic_quotes` setting will not break this method.
338     * - Boolean, numeric and null values are not modified.
339     *
340     * @param mixed $value The variable to be sanitized. If an array is supplied, the contents
341     *                     of the array will be sanitized recursively. The keys of the array
342     *                     will also be sanitized.
343     * @param bool $alreadyStripslashed Implementation detail, ignore.
344     * @throws Exception If `$value` is of an incorrect type.
345     * @return mixed  The sanitized value.
346     * @api
347     */
348    public static function sanitizeInputValues($value, $alreadyStripslashed = false)
349    {
350        if (is_numeric($value)) {
351            return $value;
352        } elseif (is_string($value)) {
353            $value = self::sanitizeString($value);
354        } elseif (is_array($value)) {
355            foreach (array_keys($value) as $key) {
356                $newKey = $key;
357                $newKey = self::sanitizeInputValues($newKey, $alreadyStripslashed);
358                if ($key !== $newKey) {
359                    $value[$newKey] = $value[$key];
360                    unset($value[$key]);
361                }
362
363                $value[$newKey] = self::sanitizeInputValues($value[$newKey], $alreadyStripslashed);
364            }
365        } elseif (!is_null($value)
366            && !is_bool($value)
367        ) {
368            throw new Exception("The value to escape has not a supported type. Value = " . var_export($value, true));
369        }
370        return $value;
371    }
372
373    /**
374     * Sanitize a single input value and removes line breaks, tabs and null characters.
375     *
376     * @param string $value
377     * @return string  sanitized input
378     */
379    public static function sanitizeInputValue($value)
380    {
381        $value = self::sanitizeLineBreaks($value);
382        $value = self::sanitizeString($value);
383        return $value;
384    }
385
386    /**
387     * Sanitize a single input value
388     *
389     * @param $value
390     * @return string
391     */
392    private static function sanitizeString($value)
393    {
394        // $_GET and $_REQUEST already urldecode()'d
395        // decode
396        // note: before php 5.2.7, htmlspecialchars() double encodes &#x hex items
397        $value = html_entity_decode($value, self::HTML_ENCODING_QUOTE_STYLE, 'UTF-8');
398
399        $value = self::sanitizeNullBytes($value);
400
401        // escape
402        $tmp = @htmlspecialchars($value, self::HTML_ENCODING_QUOTE_STYLE, 'UTF-8');
403
404        // note: php 5.2.5 and above, htmlspecialchars is destructive if input is not UTF-8
405        if ($value !== '' && $tmp === '') {
406            // convert and escape
407            $value = utf8_encode($value);
408            $tmp = htmlspecialchars($value, self::HTML_ENCODING_QUOTE_STYLE, 'UTF-8');
409            return $tmp;
410        }
411        return $tmp;
412    }
413
414    /**
415     * Unsanitizes a single input value and returns the result.
416     *
417     * @param string $value
418     * @return string  unsanitized input
419     * @api
420     */
421    public static function unsanitizeInputValue($value)
422    {
423        return htmlspecialchars_decode($value ?? '', self::HTML_ENCODING_QUOTE_STYLE);
424    }
425
426    /**
427     * Unsanitizes one or more values and returns the result.
428     *
429     * This method should be used when you need to unescape data that was obtained from
430     * the user.
431     *
432     * Some data in Piwik is stored sanitized (such as site name). In this case you may
433     * have to use this method to unsanitize it in order to, for example, output it in JSON.
434     *
435     * @param string|array $value The data to unsanitize. If an array is passed, the
436     *                            array is sanitized recursively. Key values are not unsanitized.
437     * @return string|array The unsanitized data.
438     * @api
439     */
440    public static function unsanitizeInputValues($value)
441    {
442        if (is_array($value)) {
443            $result = array();
444            foreach ($value as $key => $arrayValue) {
445                $result[$key] = self::unsanitizeInputValues($arrayValue);
446            }
447            return $result;
448        } else {
449            return self::unsanitizeInputValue($value);
450        }
451    }
452
453    /**
454     * @param string $value
455     * @return string Line breaks and line carriage removed
456     */
457    public static function sanitizeLineBreaks($value)
458    {
459        return is_null($value) ? '' : str_replace(array("\n", "\r"), '', $value);
460    }
461
462    /**
463     * @param string $value
464     * @return string Null bytes removed
465     */
466    public static function sanitizeNullBytes($value)
467    {
468        return str_replace(array("\0"), '', $value);
469    }
470
471    /**
472     * Gets a sanitized request parameter by name from the `$_GET` and `$_POST` superglobals.
473     *
474     * Use this function to get request parameter values. **_NEVER use `$_GET` and `$_POST` directly._**
475     *
476     * If the variable cannot be found, and a default value was not provided, an exception is raised.
477     *
478     * _See {@link sanitizeInputValues()} to learn more about sanitization._
479     *
480     * @param string $varName Name of the request parameter to get. By default, we look in `$_GET[$varName]`
481     *                        and `$_POST[$varName]` for the value.
482     * @param string|null $varDefault The value to return if the request parameter cannot be found or has an empty value.
483     * @param string|null $varType Expected type of the request variable. This parameters value must be one of the following:
484     *                             `'array'`, `'int'`, `'integer'`, `'string'`, `'json'`.
485     *
486     *                             If `'json'`, the string value will be `json_decode`-d and then sanitized.
487     * @param array|null $requestArrayToUse The array to use instead of `$_GET` and `$_POST`.
488     * @throws Exception If the request parameter doesn't exist and there is no default value, or if the request parameter
489     *                   exists but has an incorrect type.
490     * @return mixed The sanitized request parameter.
491     * @api
492     */
493    public static function getRequestVar($varName, $varDefault = null, $varType = null, $requestArrayToUse = null)
494    {
495        if (is_null($requestArrayToUse)) {
496            $requestArrayToUse = $_GET + $_POST;
497        }
498
499        $varDefault = self::sanitizeInputValues($varDefault);
500        if ($varType === 'int') {
501            // settype accepts only integer
502            // 'int' is simply a shortcut for 'integer'
503            $varType = 'integer';
504        }
505
506        // there is no value $varName in the REQUEST so we try to use the default value
507        if (empty($varName)
508            || !isset($requestArrayToUse[$varName])
509            || (!is_array($requestArrayToUse[$varName])
510                && strlen($requestArrayToUse[$varName]) === 0
511            )
512        ) {
513            if (is_null($varDefault)) {
514                throw new Exception("The parameter '$varName' isn't set in the Request, and a default value wasn't provided.");
515            } else {
516                if (!is_null($varType)
517                    && in_array($varType, array('string', 'integer', 'array'))
518                ) {
519                    settype($varDefault, $varType);
520                }
521                return $varDefault;
522            }
523        }
524
525        // Normal case, there is a value available in REQUEST for the requested varName:
526
527        // we deal w/ json differently
528        if ($varType === 'json') {
529            $value = $requestArrayToUse[$varName];
530            $value = json_decode($value, $assoc = true);
531            return self::sanitizeInputValues($value, $alreadyStripslashed = true);
532        }
533
534        $value = self::sanitizeInputValues($requestArrayToUse[$varName]);
535        if (isset($varType)) {
536            $ok = false;
537
538            if ($varType === 'string') {
539                if (is_string($value) || is_int($value)) {
540                    $ok = true;
541                } elseif (is_float($value)) {
542                    $value = Common::forceDotAsSeparatorForDecimalPoint($value);
543                    $ok    = true;
544                }
545            } elseif ($varType === 'integer') {
546                if ($value == (string)(int)$value) {
547                    $ok = true;
548                }
549            } elseif ($varType === 'float') {
550                $valueToCompare = (string)(float)$value;
551                $valueToCompare = Common::forceDotAsSeparatorForDecimalPoint($valueToCompare);
552
553                if ($value == $valueToCompare) {
554                    $ok = true;
555                }
556            } elseif ($varType === 'array') {
557                if (is_array($value)) {
558                    $ok = true;
559                }
560            } else {
561                throw new Exception("\$varType specified is not known. It should be one of the following: array, int, integer, float, string");
562            }
563
564            // The type is not correct
565            if ($ok === false) {
566                if ($varDefault === null) {
567                    throw new Exception("The parameter '$varName' doesn't have a correct type, and a default value wasn't provided.");
568                } // we return the default value with the good type set
569                else {
570                    settype($varDefault, $varType);
571                    return $varDefault;
572                }
573            }
574            settype($value, $varType);
575        }
576
577        return $value;
578    }
579
580    /*
581     * Generating unique strings
582     */
583
584    /**
585     * Generates a random integer
586     *
587     * @param int $min
588     * @param null|int $max Defaults to max int value
589     * @return int
590     */
591    public static function getRandomInt($min = 0, $max = null)
592    {
593        if (!isset($max)) {
594            $max = PHP_INT_MAX;
595        }
596        return random_int($min, $max);
597    }
598
599    /**
600     * Returns a 32 characters long uniq ID
601     *
602     * @return string 32 chars
603     */
604    public static function generateUniqId()
605    {
606        return bin2hex(random_bytes(16));
607    }
608
609    /**
610     * Configurable hash() algorithm (defaults to md5)
611     *
612     * @param string $str String to be hashed
613     * @param bool $raw_output
614     * @return string Hash string
615     */
616    public static function hash($str, $raw_output = false)
617    {
618        static $hashAlgorithm = null;
619
620        if (is_null($hashAlgorithm)) {
621            $hashAlgorithm = @Config::getInstance()->General['hash_algorithm'];
622        }
623
624        if ($hashAlgorithm) {
625            $hash = @hash($hashAlgorithm, $str, $raw_output);
626            if ($hash !== false) {
627                return $hash;
628            }
629        }
630
631        return md5($str, $raw_output);
632    }
633
634    /**
635     * Generate random string.
636     *
637     * @param int $length string length
638     * @param string $alphabet characters allowed in random string
639     * @return string  random string with given length
640     */
641    public static function getRandomString($length = 16, $alphabet = "abcdefghijklmnoprstuvwxyz0123456789")
642    {
643        $chars = $alphabet;
644        $str   = '';
645
646        for ($i = 0; $i < $length; $i++) {
647            $rand_key = self::getRandomInt(0, strlen($chars) - 1);
648            $str .= substr($chars, $rand_key, 1);
649        }
650
651        return str_shuffle($str);
652    }
653
654    /*
655     * Conversions
656     */
657
658    /**
659     * Convert hexadecimal representation into binary data.
660     * !! Will emit warning if input string is not hex!!
661     *
662     * @see http://php.net/bin2hex
663     *
664     * @param string $str Hexadecimal representation
665     * @return string
666     */
667    public static function hex2bin($str)
668    {
669        return pack("H*", $str);
670    }
671
672    /**
673     * This function will convert the input string to the binary representation of the ID
674     * but it will throw an Exception if the specified input ID is not correct
675     *
676     * This is used when building segments containing visitorId which could be an invalid string
677     * therefore throwing Unexpected PHP error [pack(): Type H: illegal hex digit i] severity [E_WARNING]
678     *
679     * It would be simply to silent fail the pack() call above but in all other cases, we don't expect an error,
680     * so better be safe and get the php error when something unexpected is happening
681     * @param string $id
682     * @throws Exception
683     * @return string  binary string
684     */
685    public static function convertVisitorIdToBin($id)
686    {
687        if (strlen($id) !== Tracker::LENGTH_HEX_ID_STRING
688            || @bin2hex(self::hex2bin($id)) != $id
689        ) {
690            throw new Exception("visitorId is expected to be a " . Tracker::LENGTH_HEX_ID_STRING . " hex char string");
691        }
692
693        return self::hex2bin($id);
694    }
695
696    /**
697     * Converts a User ID string to the Visitor ID Binary representation.
698     *
699     * @param $userId
700     * @return string
701     */
702    public static function convertUserIdToVisitorIdBin($userId)
703    {
704        $userIdHashed = \MatomoTracker::getUserIdHashed($userId);
705
706        return self::convertVisitorIdToBin($userIdHashed);
707    }
708
709    /**
710     * Detects whether an error occurred during the last json encode/decode.
711     * @return bool
712     */
713    public static function hasJsonErrorOccurred()
714    {
715        return json_last_error() != JSON_ERROR_NONE;
716    }
717
718    /**
719     * Returns a human readable error message in case an error occcurred during the last json encode/decode.
720     * Returns an empty string in case there was no error.
721     *
722     * @return string
723     */
724    public static function getLastJsonError()
725    {
726        switch (json_last_error()) {
727            case JSON_ERROR_NONE:
728                return '';
729            case JSON_ERROR_DEPTH:
730                return 'Maximum stack depth exceeded';
731            case JSON_ERROR_STATE_MISMATCH:
732                return 'Underflow or the modes mismatch';
733            case JSON_ERROR_CTRL_CHAR:
734                return 'Unexpected control character found';
735            case JSON_ERROR_SYNTAX:
736                return 'Syntax error, malformed JSON';
737            case JSON_ERROR_UTF8:
738                return 'Malformed UTF-8 characters, possibly incorrectly encoded';
739        }
740
741        return 'Unknown error';
742    }
743
744    public static function stringEndsWith($haystack, $needle)
745    {
746        if (strlen(strval($needle)) === 0) {
747            return true;
748        }
749
750        if (strlen(strval($haystack)) === 0) {
751            return false;
752        }
753
754        $lastCharacters = substr($haystack, -strlen($needle));
755
756        return $lastCharacters === $needle;
757    }
758
759    /**
760     * Returns the list of parent classes for the given class.
761     *
762     * @param  string    $class   A class name.
763     * @return string[]  The list of parent classes in order from highest ancestor to the descended class.
764     */
765    public static function getClassLineage($class)
766    {
767        $classes = array_merge(array($class), array_values(class_parents($class, $autoload = false)));
768
769        return array_reverse($classes);
770    }
771
772    /*
773     * DataFiles
774     */
775
776    /**
777     * Returns list of provider names
778     *
779     * @see core/DataFiles/Providers.php
780     *
781     * @return array  Array of ( dnsName => providerName )
782     */
783    public static function getProviderNames()
784    {
785        require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/Providers.php';
786
787        $providers = $GLOBALS['Piwik_ProviderNames'];
788        return $providers;
789    }
790
791    /*
792     * Language, country, continent
793     */
794
795    /**
796     * Returns the browser language code, eg. "en-gb,en;q=0.5"
797     *
798     * @param string|null $browserLang Optional browser language, otherwise taken from the request header
799     * @return string
800     */
801    public static function getBrowserLanguage($browserLang = null)
802    {
803        static $replacementPatterns = array(
804            // extraneous bits of RFC 3282 that we ignore
805            '/(\\\\.)/', // quoted-pairs
806            '/(\s+)/', // CFWcS white space
807            '/(\([^)]*\))/', // CFWS comments
808            '/(;q=[0-9.]+)/', // quality
809
810            // found in the LANG environment variable
811            '/\.(.*)/', // charset (e.g., en_CA.UTF-8)
812            '/^C$/', // POSIX 'C' locale
813        );
814
815        if (is_null($browserLang)) {
816            $browserLang = self::sanitizeInputValues(@$_SERVER['HTTP_ACCEPT_LANGUAGE']);
817            if (empty($browserLang) && self::isPhpCliMode()) {
818                $browserLang = @getenv('LANG');
819            }
820        }
821
822        if (empty($browserLang)) {
823            // a fallback might be to infer the language in HTTP_USER_AGENT (i.e., localized build)
824            $browserLang = "";
825        } else {
826            // language tags are case-insensitive per HTTP/1.1 s3.10 but the region may be capitalized per ISO3166-1;
827            // underscores are not permitted per RFC 4646 or 4647 (which obsolete RFC 1766 and 3066),
828            // but we guard against a bad user agent which naively uses its locale
829            $browserLang = strtolower(str_replace('_', '-', $browserLang));
830
831            // filters
832            $browserLang = preg_replace($replacementPatterns, '', $browserLang);
833
834            $browserLang = preg_replace('/((^|,)chrome:.*)/', '', $browserLang, 1); // Firefox bug
835            $browserLang = preg_replace('/(,)(?:en-securid,)|(?:(^|,)en-securid(,|$))/', '$1', $browserLang, 1); // unregistered language tag
836
837            $browserLang = str_replace('sr-sp', 'sr-rs', $browserLang); // unofficial (proposed) code in the wild
838        }
839
840        return $browserLang;
841    }
842
843    /**
844     * Returns the visitor country based on the Browser 'accepted language'
845     * information, but provides a hook for geolocation via IP address.
846     *
847     * @param string $lang browser lang
848     * @param bool $enableLanguageToCountryGuess If set to true, some assumption will be made and detection guessed more often, but accuracy could be affected
849     * @param string $ip
850     * @return string  2 letter ISO code
851     */
852    public static function getCountry($lang, $enableLanguageToCountryGuess, $ip)
853    {
854        if (empty($lang) || strlen($lang) < 2 || $lang === self::LANGUAGE_CODE_INVALID) {
855            return self::LANGUAGE_CODE_INVALID;
856        }
857
858        /** @var RegionDataProvider $dataProvider */
859        $dataProvider = StaticContainer::get('Piwik\Intl\Data\Provider\RegionDataProvider');
860
861        $validCountries = $dataProvider->getCountryList();
862
863        return self::extractCountryCodeFromBrowserLanguage($lang, $validCountries, $enableLanguageToCountryGuess);
864    }
865
866    /**
867     * Returns list of valid country codes
868     *
869     * @param string $browserLanguage
870     * @param array $validCountries Array of valid countries
871     * @param bool $enableLanguageToCountryGuess (if true, will guess country based on language that lacks region information)
872     * @return array Array of 2 letter ISO codes
873     */
874    public static function extractCountryCodeFromBrowserLanguage($browserLanguage, $validCountries, $enableLanguageToCountryGuess)
875    {
876        /** @var LanguageDataProvider $dataProvider */
877        $dataProvider = StaticContainer::get('Piwik\Intl\Data\Provider\LanguageDataProvider');
878
879        $langToCountry = $dataProvider->getLanguageToCountryList();
880
881        if ($enableLanguageToCountryGuess) {
882            if (preg_match('/^([a-z]{2,3})(?:,|;|$)/', $browserLanguage, $matches)) {
883                // match language (without region) to infer the country of origin
884                if (array_key_exists($matches[1], $langToCountry)) {
885                    return $langToCountry[$matches[1]];
886                }
887            }
888        }
889
890        if (!empty($validCountries) && preg_match_all('/[-]([a-z]{2})/', $browserLanguage, $matches, PREG_SET_ORDER)) {
891            foreach ($matches as $parts) {
892                // match location; we don't make any inferences from the language
893                if (array_key_exists($parts[1], $validCountries)) {
894                    return $parts[1];
895                }
896            }
897        }
898        return self::LANGUAGE_CODE_INVALID;
899    }
900
901    /**
902     * Returns the language and region string, based only on the Browser 'accepted language' information.
903     * * The language tag is defined by ISO 639-1
904     *
905     * @param string $browserLanguage Browser's accepted language header
906     * @param array $validLanguages array of valid language codes
907     * @return string  2 letter ISO 639 code  'es' (Spanish)
908     */
909    public static function extractLanguageCodeFromBrowserLanguage($browserLanguage, $validLanguages = array())
910    {
911        $validLanguages = self::checkValidLanguagesIsSet($validLanguages);
912        $languageRegionCode = self::extractLanguageAndRegionCodeFromBrowserLanguage($browserLanguage, $validLanguages);
913
914        if (strlen($languageRegionCode) === 2) {
915            $languageCode = $languageRegionCode;
916        } else {
917            $languageCode = substr($languageRegionCode, 0, 2);
918        }
919        if (in_array($languageCode, $validLanguages)) {
920            return $languageCode;
921        }
922        return self::LANGUAGE_CODE_INVALID;
923    }
924
925    /**
926     * Returns the language and region string, based only on the Browser 'accepted language' information.
927     * * The language tag is defined by ISO 639-1
928     * * The region tag is defined by ISO 3166-1
929     *
930     * @param string $browserLanguage Browser's accepted language header
931     * @param array $validLanguages array of valid language codes. Note that if the array includes "fr" then it will consider all regional variants of this language valid, such as "fr-ca" etc.
932     * @return string 2 letter ISO 639 code 'es' (Spanish) or if found, includes the region as well: 'es-ar'
933     */
934    public static function extractLanguageAndRegionCodeFromBrowserLanguage($browserLanguage, $validLanguages = array())
935    {
936        $validLanguages = self::checkValidLanguagesIsSet($validLanguages);
937
938        if (!preg_match_all('/(?:^|,)([a-z]{2,3})([-][a-z]{2})?/', $browserLanguage, $matches, PREG_SET_ORDER)) {
939            return self::LANGUAGE_CODE_INVALID;
940        }
941        foreach ($matches as $parts) {
942            $langIso639 = $parts[1];
943            if (empty($langIso639)) {
944                continue;
945            }
946
947            // If a region tag is found eg. "fr-ca"
948            if (count($parts) === 3) {
949                $regionIso3166 = $parts[2]; // eg. "-ca"
950
951                if (in_array($langIso639 . $regionIso3166, $validLanguages)) {
952                    return $langIso639 . $regionIso3166;
953                }
954
955                if (in_array($langIso639, $validLanguages)) {
956                    return $langIso639 . $regionIso3166;
957                }
958            }
959            // eg. "fr" or "es"
960            if (in_array($langIso639, $validLanguages)) {
961                return $langIso639;
962            }
963        }
964        return self::LANGUAGE_CODE_INVALID;
965    }
966
967    /**
968     * Returns the continent of a given country
969     *
970     * @param string $country 2 letters iso code
971     *
972     * @return string  Continent (3 letters code : afr, asi, eur, amn, ams, oce)
973     */
974    public static function getContinent($country)
975    {
976        /** @var RegionDataProvider $dataProvider */
977        $dataProvider = StaticContainer::get('Piwik\Intl\Data\Provider\RegionDataProvider');
978
979        $countryList = $dataProvider->getCountryList();
980
981        if ($country === 'ti') {
982            $country = 'cn';
983        }
984
985        return isset($countryList[$country]) ? $countryList[$country] : 'unk';
986    }
987
988    /*
989     * Campaign
990     */
991
992    /**
993     * Returns the list of Campaign parameter names that will be read to classify
994     * a visit as coming from a Campaign
995     *
996     * @return array array(
997     *            0 => array( ... ) // campaign names parameters
998     *            1 => array( ... ) // campaign keyword parameters
999     * );
1000     */
1001    public static function getCampaignParameters()
1002    {
1003        $return = array(
1004            Config::getInstance()->Tracker['campaign_var_name'],
1005            Config::getInstance()->Tracker['campaign_keyword_var_name'],
1006        );
1007
1008        foreach ($return as &$list) {
1009            if (strpos($list, ',') !== false) {
1010                $list = explode(',', $list);
1011            } else {
1012                $list = array($list);
1013            }
1014            $list = array_map('trim', $list);
1015        }
1016
1017        return $return;
1018    }
1019
1020    /*
1021     * Referrer
1022     */
1023
1024    /**
1025     * Returns a string with a comma separated list of placeholders for use in an SQL query. Used mainly
1026     * to fill the `IN (...)` part of a query.
1027     *
1028     * @param array|string $fields The names of the mysql table fields to bind, e.g.
1029     *                             `array(fieldName1, fieldName2, fieldName3)`.
1030     *
1031     *                             _Note: The content of the array isn't important, just its length._
1032     * @return string The placeholder string, e.g. `"?, ?, ?"`.
1033     * @api
1034     */
1035    public static function getSqlStringFieldsArray($fields)
1036    {
1037        if (is_string($fields)) {
1038            $fields = array($fields);
1039        }
1040        $count = count($fields);
1041        if ($count === 0) {
1042            return "''";
1043        }
1044        return '?' . str_repeat(',?', $count - 1);
1045    }
1046
1047    /**
1048     * Force the separator for decimal point to be a dot. See https://github.com/piwik/piwik/issues/6435
1049     * If for instance a German locale is used it would be a comma otherwise.
1050     *
1051     * @param  float|string $value
1052     * @return string
1053     */
1054    public static function forceDotAsSeparatorForDecimalPoint($value)
1055    {
1056        if (null === $value || false === $value) {
1057            return $value;
1058        }
1059
1060        return str_replace(',', '.', $value);
1061    }
1062
1063    /**
1064     * Sets outgoing header.
1065     *
1066     * @param string $header The header.
1067     * @param bool $replace Whether to replace existing or not.
1068     */
1069    public static function sendHeader($header, $replace = true)
1070    {
1071        // don't send header in CLI mode
1072        if (!Common::isPhpCliMode() and !headers_sent()) {
1073            header($header, $replace);
1074        }
1075    }
1076
1077    /**
1078     * Strips outgoing header.
1079     *
1080     * @param string $name The header name.
1081     */
1082    public static function stripHeader($name)
1083    {
1084        // don't strip header in CLI mode
1085        if (!Common::isPhpCliMode() and !headers_sent()) {
1086            header_remove($name);
1087        }
1088    }
1089
1090    /**
1091     * Sends the given response code if supported.
1092     *
1093     * @param int $code  Eg 204
1094     *
1095     * @throws Exception
1096     */
1097    public static function sendResponseCode($code)
1098    {
1099        $messages = array(
1100            200 => 'Ok',
1101            204 => 'No Response',
1102            301 => 'Moved Permanently',
1103            302 => 'Found',
1104            304 => 'Not Modified',
1105            400 => 'Bad Request',
1106            401 => 'Unauthorized',
1107            403 => 'Forbidden',
1108            404 => 'Not Found',
1109            500 => 'Internal Server Error',
1110            503 => 'Service Unavailable',
1111        );
1112
1113        if (!array_key_exists($code, $messages)) {
1114            throw new Exception('Response code not supported: ' . $code);
1115        }
1116
1117        if (strpos(PHP_SAPI, '-fcgi') === false) {
1118            $key = 'HTTP/1.1';
1119
1120            if (array_key_exists('SERVER_PROTOCOL', $_SERVER)
1121                && strlen($_SERVER['SERVER_PROTOCOL']) < 15
1122                && strlen($_SERVER['SERVER_PROTOCOL']) > 1) {
1123                $key = $_SERVER['SERVER_PROTOCOL'];
1124            }
1125        } else {
1126            // FastCGI
1127            $key = 'Status:';
1128        }
1129
1130        $message = $messages[$code];
1131        Common::sendHeader($key . ' ' . $code . ' ' . $message);
1132    }
1133
1134    /**
1135     * Returns the ID of the current LocationProvider (see UserCountry plugin code) from
1136     * the Tracker cache.
1137     */
1138    public static function getCurrentLocationProviderId()
1139    {
1140        $cache = TrackerCache::getCacheGeneral();
1141        return empty($cache['currentLocationProviderId'])
1142            ? DefaultProvider::ID
1143            : $cache['currentLocationProviderId'];
1144    }
1145
1146    /**
1147     * Marks an orphaned object for garbage collection.
1148     *
1149     * For more information: {@link https://github.com/piwik/piwik/issues/374}
1150     * @param mixed $var The object to destroy.
1151     * @api
1152     */
1153    public static function destroy(&$var)
1154    {
1155        if (is_object($var) && method_exists($var, '__destruct')) {
1156            $var->__destruct();
1157        }
1158        unset($var);
1159        $var = null;
1160    }
1161
1162    /**
1163     * @deprecated Use the logger directly instead.
1164     */
1165    public static function printDebug($info = '')
1166    {
1167        if (is_object($info)) {
1168            $info = var_export($info, true);
1169        }
1170
1171        $logger = StaticContainer::get('Psr\Log\LoggerInterface');
1172        if (is_array($info) || is_object($info)) {
1173            $out = var_export($info, true);
1174            $logger->debug($out);
1175        } else {
1176            $logger->debug($info);
1177        }
1178    }
1179
1180    /**
1181     * Returns true if the request is an AJAX request.
1182     *
1183     * @return bool
1184     */
1185    public static function isXmlHttpRequest()
1186    {
1187        return isset($_SERVER['HTTP_X_REQUESTED_WITH'])
1188            && (strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest');
1189    }
1190
1191    /**
1192     * @param $validLanguages
1193     * @return array
1194     */
1195    protected static function checkValidLanguagesIsSet($validLanguages)
1196    {
1197        /** @var LanguageDataProvider $dataProvider */
1198        $dataProvider = StaticContainer::get('Piwik\Intl\Data\Provider\LanguageDataProvider');
1199
1200        if (empty($validLanguages)) {
1201            $validLanguages = array_keys($dataProvider->getLanguageList());
1202            return $validLanguages;
1203        }
1204        return $validLanguages;
1205    }
1206}
1207