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