site-accueil-insa/matomo/core/Common.php

1238 lines
40 KiB
PHP

<?php
/**
* Matomo - free/libre analytics platform
*
* @link https://matomo.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik;
use Exception;
use Piwik\CliMulti\Process;
use Piwik\Container\StaticContainer;
use Piwik\Intl\Data\Provider\LanguageDataProvider;
use Piwik\Intl\Data\Provider\RegionDataProvider;
use Piwik\Tracker\Cache as TrackerCache;
/**
* Contains helper methods used by both Piwik Core and the Piwik Tracking engine.
*
* This is the only non-Tracker class loaded by the **\/piwik.php** file.
*/
class Common
{
// constants used to map the referrer type to an integer in the log_visit table
const REFERRER_TYPE_DIRECT_ENTRY = 1;
const REFERRER_TYPE_SEARCH_ENGINE = 2;
const REFERRER_TYPE_WEBSITE = 3;
const REFERRER_TYPE_CAMPAIGN = 6;
const REFERRER_TYPE_SOCIAL_NETWORK = 7;
// Flag used with htmlspecialchar. See php.net/htmlspecialchars.
const HTML_ENCODING_QUOTE_STYLE = ENT_QUOTES;
public static $isCliMode = null;
/*
* Database
*/
const LANGUAGE_CODE_INVALID = 'xx';
/**
* Hashes a string into an integer which should be very low collision risks
* @param string $string String to hash
* @return int Resulting int hash
*/
public static function hashStringToInt($string)
{
$stringHash = substr(md5($string), 0, 8);
return base_convert($stringHash, 16, 10);
}
/**
* Returns a prefixed table name.
*
* The table prefix is determined by the `[database] tables_prefix` INI config
* option.
*
* @param string $table The table name to prefix, ie "log_visit"
* @return string The prefixed name, ie "piwik-production_log_visit".
* @api
*/
public static function prefixTable($table)
{
$prefix = Config::getInstance()->database['tables_prefix'];
return $prefix . $table;
}
/**
* Returns an array containing the prefixed table names of every passed argument.
*
* @param string ... The table names to prefix, ie "log_visit"
* @return array The prefixed names in an array.
*/
public static function prefixTables()
{
$result = array();
foreach (func_get_args() as $table) {
$result[] = self::prefixTable($table);
}
return $result;
}
/**
* Removes the prefix from a table name and returns the result.
*
* The table prefix is determined by the `[database] tables_prefix` INI config
* option.
*
* @param string $table The prefixed table name, eg "piwik-production_log_visit".
* @return string The unprefixed table name, eg "log_visit".
* @api
*/
public static function unprefixTable($table)
{
static $prefixTable = null;
if (is_null($prefixTable)) {
$prefixTable = Config::getInstance()->database['tables_prefix'];
}
if (empty($prefixTable)
|| strpos($table, $prefixTable) !== 0
) {
return $table;
}
$count = 1;
return str_replace($prefixTable, '', $table, $count);
}
/*
* Tracker
*/
public static function isGoalPluginEnabled()
{
return Plugin\Manager::getInstance()->isPluginActivated('Goals');
}
public static function isActionsPluginEnabled()
{
return Plugin\Manager::getInstance()->isPluginActivated('Actions');
}
/**
* Returns true if PHP was invoked from command-line interface (shell)
*
* @since added in 0.4.4
* @return bool true if PHP invoked as a CGI or from CLI
*/
public static function isPhpCliMode()
{
if (is_bool(self::$isCliMode)) {
return self::$isCliMode;
}
if(PHP_SAPI === 'cli'){
return true;
}
if(self::isPhpCgiType() && (!isset($_SERVER['REMOTE_ADDR']) || empty($_SERVER['REMOTE_ADDR']))){
return true;
}
return false;
}
/**
* Returns true if PHP is executed as CGI type.
*
* @since added in 0.4.4
* @return bool true if PHP invoked as a CGI
*/
public static function isPhpCgiType()
{
$sapiType = php_sapi_name();
return substr($sapiType, 0, 3) === 'cgi';
}
/**
* Returns true if the current request is a console command, eg.
* ./console xx:yy
* or
* php console xx:yy
*
* @return bool
*/
public static function isRunningConsoleCommand()
{
$searched = 'console';
$consolePos = strpos($_SERVER['SCRIPT_NAME'], $searched);
$expectedConsolePos = strlen($_SERVER['SCRIPT_NAME']) - strlen($searched);
$isScriptIsConsole = ($consolePos === $expectedConsolePos);
return self::isPhpCliMode() && $isScriptIsConsole;
}
/*
* String operations
*/
/**
* Multi-byte substr() - works with UTF-8.
*
* Calls `mb_substr` if available and falls back to `substr` if it's not.
*
* @param string $string
* @param int $start
* @param int|null $length optional length
* @return string
* @deprecated since 4.4 - directly use mb_substr instead
*/
public static function mb_substr($string, $start, $length = null)
{
return mb_substr($string, $start, $length, 'UTF-8');
}
/**
* Gets the current process ID.
* Note: If getmypid is disabled, a random ID will be generated once and used throughout the request. There is a
* small chance that two processes at the same time may generated the same random ID. If you need to rely on the
* value being 100% unique, then you may need to use `getmypid` directly or some other logic. Eg in CliMulti it is
* fine to use `getmypid` directly as the logic won't be used if getmypid is disabled...
* If you are wanting to use the pid to check if the process is running eg using `ps`, then you also have to use
* getmypid directly.
*
* @return int|null
*/
public static function getProcessId()
{
static $pid;
if (!isset($pid)) {
if (Process::isMethodDisabled('getmypid')) {
$pid = Common::getRandomInt(12);
} else {
$pid = getmypid();
}
}
return $pid;
}
/**
* Multi-byte strlen() - works with UTF-8
*
* Calls `mb_substr` if available and falls back to `substr` if not.
*
* @param string $string
* @return int
* @deprecated since 4.4 - directly use mb_strlen instead
*/
public static function mb_strlen($string)
{
return mb_strlen($string, 'UTF-8');
}
/**
* Multi-byte strtolower() - works with UTF-8.
*
* Calls `mb_strtolower` if available and falls back to `strtolower` if not.
*
* @param string $string
* @return string
* @deprecated since 4.4 - directly use mb_strtolower instead
*/
public static function mb_strtolower($string)
{
return mb_strtolower($string, 'UTF-8');
}
/**
* Multi-byte strtoupper() - works with UTF-8.
*
* Calls `mb_strtoupper` if available and falls back to `strtoupper` if not.
*
* @param string $string
* @return string
* @deprecated since 4.4 - directly use mb_strtoupper instead
*/
public static function mb_strtoupper($string)
{
return mb_strtoupper($string, 'UTF-8');
}
/**
* Timing attack safe string comparison.
*
* @param string $stringA
* @param string $stringB
* @return bool
*/
public static function hashEquals(string $stringA, string $stringB)
{
if (function_exists('hash_equals')) {
return hash_equals($stringA, $stringB);
}
if (strlen($stringA) !== strlen($stringB)) {
return false;
}
$result = "\0";
$stringA^= $stringB;
for ($i = 0; $i < strlen($stringA); $i++) {
$result|= $stringA[$i];
}
return $result === "\0";
}
/**
* Secure wrapper for unserialize, which by default disallows unserializing classes
*
* @param string $string String to unserialize
* @param array $allowedClasses Class names that should be allowed to unserialize
* @param bool $rethrow Whether to rethrow exceptions or not.
* @return mixed
*/
public static function safe_unserialize($string, $allowedClasses = [], $rethrow = false)
{
try {
// phpcs:ignore Generic.PHP.ForbiddenFunctions
return unserialize($string ?? '', ['allowed_classes' => empty($allowedClasses) ? false : $allowedClasses]);
} catch (\Throwable $e) {
if ($rethrow) {
throw $e;
}
$logger = StaticContainer::get('Psr\Log\LoggerInterface');
$logger->debug('Unable to unserialize a string: {exception} (string = {string})', [
'exception' => $e,
'string' => $string,
]);
return false;
}
}
/*
* Escaping input
*/
/**
* Sanitizes a string to help avoid XSS vulnerabilities.
*
* This function is automatically called when {@link getRequestVar()} is called,
* so you should not normally have to use it.
*
* This function should be used when outputting data that isn't escaped and was
* obtained from the user (for example when using the `|raw` twig filter on goal names).
*
* _NOTE: Sanitized input should not be used directly in an SQL query; SQL placeholders
* should still be used._
*
* **Implementation Details**
*
* - [htmlspecialchars](http://php.net/manual/en/function.htmlspecialchars.php) is used to escape text.
* - Single quotes are not escaped so **Piwik's amazing community** will still be
* **Piwik's amazing community**.
* - Use of the `magic_quotes` setting will not break this method.
* - Boolean, numeric and null values are not modified.
*
* @param mixed $value The variable to be sanitized. If an array is supplied, the contents
* of the array will be sanitized recursively. The keys of the array
* will also be sanitized.
* @param bool $alreadyStripslashed Implementation detail, ignore.
* @throws Exception If `$value` is of an incorrect type.
* @return mixed The sanitized value.
* @api
*/
public static function sanitizeInputValues($value, $alreadyStripslashed = false)
{
if (is_numeric($value)) {
return $value;
} elseif (is_string($value)) {
$value = self::sanitizeString($value);
} elseif (is_array($value)) {
foreach (array_keys($value) as $key) {
$newKey = $key;
$newKey = self::sanitizeInputValues($newKey, $alreadyStripslashed);
if ($key !== $newKey) {
$value[$newKey] = $value[$key];
unset($value[$key]);
}
$value[$newKey] = self::sanitizeInputValues($value[$newKey], $alreadyStripslashed);
}
} elseif (!is_null($value)
&& !is_bool($value)
) {
throw new Exception("The value to escape has not a supported type. Value = " . var_export($value, true));
}
return $value;
}
/**
* Sanitize a single input value and removes line breaks, tabs and null characters.
*
* @param string $value
* @return string sanitized input
*/
public static function sanitizeInputValue($value)
{
$value = self::sanitizeLineBreaks($value);
$value = self::sanitizeString($value);
return $value;
}
/**
* Sanitize a single input value
*
* @param $value
* @return string
*/
private static function sanitizeString($value)
{
// $_GET and $_REQUEST already urldecode()'d
// decode
// note: before php 5.2.7, htmlspecialchars() double encodes &#x hex items
$value = html_entity_decode($value, self::HTML_ENCODING_QUOTE_STYLE, 'UTF-8');
$value = self::sanitizeNullBytes($value);
// escape
$tmp = @htmlspecialchars($value, self::HTML_ENCODING_QUOTE_STYLE, 'UTF-8');
// note: php 5.2.5 and above, htmlspecialchars is destructive if input is not UTF-8
if ($value !== '' && $tmp === '') {
// convert and escape
$value = utf8_encode($value);
$tmp = htmlspecialchars($value, self::HTML_ENCODING_QUOTE_STYLE, 'UTF-8');
return $tmp;
}
return $tmp;
}
/**
* Unsanitizes a single input value and returns the result.
*
* @param string $value
* @return string unsanitized input
* @api
*/
public static function unsanitizeInputValue($value)
{
return htmlspecialchars_decode($value ?? '', self::HTML_ENCODING_QUOTE_STYLE);
}
/**
* Unsanitizes one or more values and returns the result.
*
* This method should be used when you need to unescape data that was obtained from
* the user.
*
* Some data in Piwik is stored sanitized (such as site name). In this case you may
* have to use this method to unsanitize it in order to, for example, output it in JSON.
*
* @param string|array $value The data to unsanitize. If an array is passed, the
* array is sanitized recursively. Key values are not unsanitized.
* @return string|array The unsanitized data.
* @api
*/
public static function unsanitizeInputValues($value)
{
if (is_array($value)) {
$result = array();
foreach ($value as $key => $arrayValue) {
$result[$key] = self::unsanitizeInputValues($arrayValue);
}
return $result;
} else {
return self::unsanitizeInputValue($value);
}
}
/**
* @param string $value
* @return string Line breaks and line carriage removed
*/
public static function sanitizeLineBreaks($value)
{
return is_null($value) ? '' : str_replace(array("\n", "\r"), '', $value);
}
/**
* @param string $value
* @return string Null bytes removed
*/
public static function sanitizeNullBytes($value)
{
return str_replace(array("\0"), '', $value);
}
/**
* Gets a sanitized request parameter by name from the `$_GET` and `$_POST` superglobals.
*
* Use this function to get request parameter values. **_NEVER use `$_GET` and `$_POST` directly._**
*
* If the variable cannot be found, and a default value was not provided, an exception is raised.
*
* _See {@link sanitizeInputValues()} to learn more about sanitization._
*
* @param string $varName Name of the request parameter to get. By default, we look in `$_GET[$varName]`
* and `$_POST[$varName]` for the value.
* @param string|null $varDefault The value to return if the request parameter cannot be found or has an empty value.
* @param string|null $varType Expected type of the request variable. This parameters value must be one of the following:
* `'array'`, `'int'`, `'integer'`, `'string'`, `'json'`.
*
* If `'json'`, the string value will be `json_decode`-d and then sanitized.
* @param array|null $requestArrayToUse The array to use instead of `$_GET` and `$_POST`.
* @throws Exception If the request parameter doesn't exist and there is no default value, or if the request parameter
* exists but has an incorrect type.
* @return mixed The sanitized request parameter.
* @api
*/
public static function getRequestVar($varName, $varDefault = null, $varType = null, $requestArrayToUse = null)
{
if (is_null($requestArrayToUse)) {
$requestArrayToUse = $_GET + $_POST;
}
$varDefault = self::sanitizeInputValues($varDefault);
if ($varType === 'int') {
// settype accepts only integer
// 'int' is simply a shortcut for 'integer'
$varType = 'integer';
}
// there is no value $varName in the REQUEST so we try to use the default value
if (empty($varName)
|| !isset($requestArrayToUse[$varName])
|| (!is_array($requestArrayToUse[$varName])
&& strlen($requestArrayToUse[$varName]) === 0
)
) {
if (is_null($varDefault)) {
throw new Exception("The parameter '$varName' isn't set in the Request, and a default value wasn't provided.");
} else {
if (!is_null($varType)
&& in_array($varType, array('string', 'integer', 'array'))
) {
settype($varDefault, $varType);
}
return $varDefault;
}
}
// Normal case, there is a value available in REQUEST for the requested varName:
// we deal w/ json differently
if ($varType === 'json') {
$value = $requestArrayToUse[$varName];
$value = json_decode($value, $assoc = true);
return self::sanitizeInputValues($value, $alreadyStripslashed = true);
}
$value = self::sanitizeInputValues($requestArrayToUse[$varName]);
if (isset($varType)) {
$ok = false;
if ($varType === 'string') {
if (is_string($value) || is_int($value)) {
$ok = true;
} elseif (is_float($value)) {
$value = Common::forceDotAsSeparatorForDecimalPoint($value);
$ok = true;
}
} elseif ($varType === 'integer') {
if ($value == (string)(int)$value) {
$ok = true;
}
} elseif ($varType === 'float') {
$valueToCompare = (string)(float)$value;
$valueToCompare = Common::forceDotAsSeparatorForDecimalPoint($valueToCompare);
if ($value == $valueToCompare) {
$ok = true;
}
} elseif ($varType === 'array') {
if (is_array($value)) {
$ok = true;
}
} else {
throw new Exception("\$varType specified is not known. It should be one of the following: array, int, integer, float, string");
}
// The type is not correct
if ($ok === false) {
if ($varDefault === null) {
throw new Exception("The parameter '$varName' doesn't have a correct type, and a default value wasn't provided.");
} // we return the default value with the good type set
else {
settype($varDefault, $varType);
return $varDefault;
}
}
settype($value, $varType);
}
return $value;
}
/**
* Replaces lbrace with an encoded entity to prevent angular from parsing the content
*
* @deprecated Will be removed, once the vue js migration is done
*
* @param $string
* @return array|string|string[]|null
*/
public static function fixLbrace($string)
{
$chars = array('{', '&#x7B;', '&#123;', '&lcub;', '&lbrace;', '&#x0007B;');
static $search;
static $replace;
if (!isset($search)) {
$search = array_map(function ($val) { return $val . $val; }, $chars);
}
if (!isset($replace)) {
$replace = array_map(function ($val) { return $val . '&#8291;' . $val; }, $chars);
}
$replacedString = is_null($string) ? $string : str_replace($search, $replace, $string);
// try to replace characters until there are no changes
if ($string !== $replacedString) {
return self::fixLbrace($replacedString);
}
return $string;
}
/*
* Generating unique strings
*/
/**
* Generates a random integer
*
* @param int $min
* @param null|int $max Defaults to max int value
* @return int
*/
public static function getRandomInt($min = 0, $max = null)
{
if (!isset($max)) {
$max = PHP_INT_MAX;
}
return random_int($min, $max);
}
/**
* Returns a 32 characters long uniq ID
*
* @return string 32 chars
*/
public static function generateUniqId()
{
return bin2hex(random_bytes(16));
}
/**
* Configurable hash() algorithm (defaults to md5)
*
* @param string $str String to be hashed
* @param bool $raw_output
* @return string Hash string
*/
public static function hash($str, $raw_output = false)
{
static $hashAlgorithm = null;
if (is_null($hashAlgorithm)) {
$hashAlgorithm = @Config::getInstance()->General['hash_algorithm'];
}
if ($hashAlgorithm) {
$hash = @hash($hashAlgorithm, $str, $raw_output);
if ($hash !== false) {
return $hash;
}
}
return md5($str, $raw_output);
}
/**
* Generate random string.
*
* @param int $length string length
* @param string $alphabet characters allowed in random string
* @return string random string with given length
*/
public static function getRandomString($length = 16, $alphabet = "abcdefghijklmnoprstuvwxyz0123456789")
{
$chars = $alphabet;
$str = '';
for ($i = 0; $i < $length; $i++) {
$rand_key = self::getRandomInt(0, strlen($chars) - 1);
$str .= substr($chars, $rand_key, 1);
}
return str_shuffle($str);
}
/*
* Conversions
*/
/**
* Convert hexadecimal representation into binary data.
* !! Will emit warning if input string is not hex!!
*
* @see http://php.net/bin2hex
*
* @param string $str Hexadecimal representation
* @return string
*/
public static function hex2bin($str)
{
return pack("H*", $str);
}
/**
* This function will convert the input string to the binary representation of the ID
* but it will throw an Exception if the specified input ID is not correct
*
* This is used when building segments containing visitorId which could be an invalid string
* therefore throwing Unexpected PHP error [pack(): Type H: illegal hex digit i] severity [E_WARNING]
*
* It would be simply to silent fail the pack() call above but in all other cases, we don't expect an error,
* so better be safe and get the php error when something unexpected is happening
* @param string $id
* @throws Exception
* @return string binary string
*/
public static function convertVisitorIdToBin($id)
{
if (strlen($id) !== Tracker::LENGTH_HEX_ID_STRING
|| @bin2hex(self::hex2bin($id)) != $id
) {
throw new Exception("visitorId is expected to be a " . Tracker::LENGTH_HEX_ID_STRING . " hex char string");
}
return self::hex2bin($id);
}
/**
* Converts a User ID string to the Visitor ID Binary representation.
*
* @param $userId
* @return string
*/
public static function convertUserIdToVisitorIdBin($userId)
{
$userIdHashed = \MatomoTracker::getUserIdHashed($userId);
return self::convertVisitorIdToBin($userIdHashed);
}
/**
* Detects whether an error occurred during the last json encode/decode.
* @return bool
*/
public static function hasJsonErrorOccurred()
{
return json_last_error() != JSON_ERROR_NONE;
}
/**
* Returns a human readable error message in case an error occurred during the last json encode/decode.
* Returns an empty string in case there was no error.
*
* @return string
*/
public static function getLastJsonError()
{
switch (json_last_error()) {
case JSON_ERROR_NONE:
return '';
case JSON_ERROR_DEPTH:
return 'Maximum stack depth exceeded';
case JSON_ERROR_STATE_MISMATCH:
return 'Underflow or the modes mismatch';
case JSON_ERROR_CTRL_CHAR:
return 'Unexpected control character found';
case JSON_ERROR_SYNTAX:
return 'Syntax error, malformed JSON';
case JSON_ERROR_UTF8:
return 'Malformed UTF-8 characters, possibly incorrectly encoded';
}
return 'Unknown error';
}
public static function stringEndsWith($haystack, $needle)
{
if (strlen(strval($needle)) === 0) {
return true;
}
if (strlen(strval($haystack)) === 0) {
return false;
}
$lastCharacters = substr($haystack, -strlen($needle));
return $lastCharacters === $needle;
}
/**
* Returns the list of parent classes for the given class.
*
* @param string $class A class name.
* @return string[] The list of parent classes in order from highest ancestor to the descended class.
*/
public static function getClassLineage($class)
{
$classes = array_merge(array($class), array_values(class_parents($class, $autoload = false)));
return array_reverse($classes);
}
/*
* DataFiles
*/
/**
* Returns list of provider names
*
* @see core/DataFiles/Providers.php
*
* @return array Array of ( dnsName => providerName )
*/
public static function getProviderNames()
{
require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/Providers.php';
$providers = $GLOBALS['Piwik_ProviderNames'];
return $providers;
}
/*
* Language, country, continent
*/
/**
* Returns the browser language code, eg. "en-gb,en;q=0.5"
*
* @param string|null $browserLang Optional browser language, otherwise taken from the request header
* @return string
*/
public static function getBrowserLanguage($browserLang = null)
{
static $replacementPatterns = array(
// extraneous bits of RFC 3282 that we ignore
'/(\\\\.)/', // quoted-pairs
'/(\s+)/', // CFWcS white space
'/(\([^)]*\))/', // CFWS comments
'/(;q=[0-9.]+)/', // quality
// found in the LANG environment variable
'/\.(.*)/', // charset (e.g., en_CA.UTF-8)
'/^C$/', // POSIX 'C' locale
);
if (is_null($browserLang)) {
$browserLang = self::sanitizeInputValues(@$_SERVER['HTTP_ACCEPT_LANGUAGE']);
if (empty($browserLang) && self::isPhpCliMode()) {
$browserLang = @getenv('LANG');
}
}
if (empty($browserLang)) {
// a fallback might be to infer the language in HTTP_USER_AGENT (i.e., localized build)
$browserLang = "";
} else {
// language tags are case-insensitive per HTTP/1.1 s3.10 but the region may be capitalized per ISO3166-1;
// underscores are not permitted per RFC 4646 or 4647 (which obsolete RFC 1766 and 3066),
// but we guard against a bad user agent which naively uses its locale
$browserLang = strtolower(str_replace('_', '-', $browserLang));
// filters
$browserLang = preg_replace($replacementPatterns, '', $browserLang);
$browserLang = preg_replace('/((^|,)chrome:.*)/', '', $browserLang, 1); // Firefox bug
$browserLang = preg_replace('/(,)(?:en-securid,)|(?:(^|,)en-securid(,|$))/', '$1', $browserLang, 1); // unregistered language tag
$browserLang = str_replace('sr-sp', 'sr-rs', $browserLang); // unofficial (proposed) code in the wild
}
return $browserLang;
}
/**
* Returns the visitor country based on the Browser 'accepted language'
* information, but provides a hook for geolocation via IP address.
*
* @param string $lang browser lang
* @param bool $enableLanguageToCountryGuess If set to true, some assumption will be made and detection guessed more often, but accuracy could be affected
* @param string $ip
* @return string 2 letter ISO code
*/
public static function getCountry($lang, $enableLanguageToCountryGuess, $ip)
{
if (empty($lang) || strlen($lang) < 2 || $lang === self::LANGUAGE_CODE_INVALID) {
return self::LANGUAGE_CODE_INVALID;
}
/** @var RegionDataProvider $dataProvider */
$dataProvider = StaticContainer::get('Piwik\Intl\Data\Provider\RegionDataProvider');
$validCountries = $dataProvider->getCountryList();
return self::extractCountryCodeFromBrowserLanguage($lang, $validCountries, $enableLanguageToCountryGuess);
}
/**
* Returns list of valid country codes
*
* @param string $browserLanguage
* @param array $validCountries Array of valid countries
* @param bool $enableLanguageToCountryGuess (if true, will guess country based on language that lacks region information)
* @return array Array of 2 letter ISO codes
*/
public static function extractCountryCodeFromBrowserLanguage($browserLanguage, $validCountries, $enableLanguageToCountryGuess)
{
/** @var LanguageDataProvider $dataProvider */
$dataProvider = StaticContainer::get('Piwik\Intl\Data\Provider\LanguageDataProvider');
$langToCountry = $dataProvider->getLanguageToCountryList();
if ($enableLanguageToCountryGuess) {
if (preg_match('/^([a-z]{2,3})(?:,|;|$)/', $browserLanguage, $matches)) {
// match language (without region) to infer the country of origin
if (array_key_exists($matches[1], $langToCountry)) {
return $langToCountry[$matches[1]];
}
}
}
if (!empty($validCountries) && preg_match_all('/[-]([a-z]{2})/', $browserLanguage, $matches, PREG_SET_ORDER)) {
foreach ($matches as $parts) {
// match location; we don't make any inferences from the language
if (array_key_exists($parts[1], $validCountries)) {
return $parts[1];
}
}
}
return self::LANGUAGE_CODE_INVALID;
}
/**
* Returns the language and region string, based only on the Browser 'accepted language' information.
* * The language tag is defined by ISO 639-1
*
* @param string $browserLanguage Browser's accepted language header
* @param array $validLanguages array of valid language codes
* @return string 2 letter ISO 639 code 'es' (Spanish)
*/
public static function extractLanguageCodeFromBrowserLanguage($browserLanguage, $validLanguages = array())
{
$validLanguages = self::checkValidLanguagesIsSet($validLanguages);
$languageRegionCode = self::extractLanguageAndRegionCodeFromBrowserLanguage($browserLanguage, $validLanguages);
if (strlen($languageRegionCode) === 2) {
$languageCode = $languageRegionCode;
} else {
$languageCode = substr($languageRegionCode, 0, 2);
}
if (in_array($languageCode, $validLanguages)) {
return $languageCode;
}
return self::LANGUAGE_CODE_INVALID;
}
/**
* Returns the language and region string, based only on the Browser 'accepted language' information.
* * The language tag is defined by ISO 639-1
* * The region tag is defined by ISO 3166-1
*
* @param string $browserLanguage Browser's accepted language header
* @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.
* @return string 2 letter ISO 639 code 'es' (Spanish) or if found, includes the region as well: 'es-ar'
*/
public static function extractLanguageAndRegionCodeFromBrowserLanguage($browserLanguage, $validLanguages = array())
{
$validLanguages = self::checkValidLanguagesIsSet($validLanguages);
if (!preg_match_all('/(?:^|,)([a-z]{2,3})([-][a-z]{2})?/', $browserLanguage, $matches, PREG_SET_ORDER)) {
return self::LANGUAGE_CODE_INVALID;
}
foreach ($matches as $parts) {
$langIso639 = $parts[1];
if (empty($langIso639)) {
continue;
}
// If a region tag is found eg. "fr-ca"
if (count($parts) === 3) {
$regionIso3166 = $parts[2]; // eg. "-ca"
if (in_array($langIso639 . $regionIso3166, $validLanguages)) {
return $langIso639 . $regionIso3166;
}
if (in_array($langIso639, $validLanguages)) {
return $langIso639 . $regionIso3166;
}
}
// eg. "fr" or "es"
if (in_array($langIso639, $validLanguages)) {
return $langIso639;
}
}
return self::LANGUAGE_CODE_INVALID;
}
/**
* Returns the continent of a given country
*
* @param string $country 2 letters iso code
*
* @return string Continent (3 letters code : afr, asi, eur, amn, ams, oce)
*/
public static function getContinent($country)
{
/** @var RegionDataProvider $dataProvider */
$dataProvider = StaticContainer::get('Piwik\Intl\Data\Provider\RegionDataProvider');
$countryList = $dataProvider->getCountryList();
if ($country === 'ti') {
$country = 'cn';
}
return isset($countryList[$country]) ? $countryList[$country] : 'unk';
}
/*
* Campaign
*/
/**
* Returns the list of Campaign parameter names that will be read to classify
* a visit as coming from a Campaign
*
* @return array array(
* 0 => array( ... ) // campaign names parameters
* 1 => array( ... ) // campaign keyword parameters
* );
*/
public static function getCampaignParameters()
{
$return = array(
Config::getInstance()->Tracker['campaign_var_name'],
Config::getInstance()->Tracker['campaign_keyword_var_name'],
);
foreach ($return as &$list) {
if (strpos($list, ',') !== false) {
$list = explode(',', $list);
} else {
$list = array($list);
}
$list = array_map('trim', $list);
}
return $return;
}
/*
* Referrer
*/
/**
* Returns a string with a comma separated list of placeholders for use in an SQL query. Used mainly
* to fill the `IN (...)` part of a query.
*
* @param array|string $fields The names of the mysql table fields to bind, e.g.
* `array(fieldName1, fieldName2, fieldName3)`.
*
* _Note: The content of the array isn't important, just its length._
* @return string The placeholder string, e.g. `"?, ?, ?"`.
* @api
*/
public static function getSqlStringFieldsArray($fields)
{
if (is_string($fields)) {
$fields = array($fields);
}
$count = count($fields);
if ($count === 0) {
return "''";
}
return '?' . str_repeat(',?', $count - 1);
}
/**
* Force the separator for decimal point to be a dot. See https://github.com/piwik/piwik/issues/6435
* If for instance a German locale is used it would be a comma otherwise.
*
* @param float|string $value
* @return string
*/
public static function forceDotAsSeparatorForDecimalPoint($value)
{
if (null === $value || false === $value) {
return $value;
}
return str_replace(',', '.', $value);
}
/**
* Sets outgoing header.
*
* @param string $header The header.
* @param bool $replace Whether to replace existing or not.
*/
public static function sendHeader($header, $replace = true)
{
// don't send header in CLI mode
if (!Common::isPhpCliMode() and !headers_sent()) {
header($header, $replace);
}
}
/**
* Strips outgoing header.
*
* @param string $name The header name.
*/
public static function stripHeader($name)
{
// don't strip header in CLI mode
if (!Common::isPhpCliMode() and !headers_sent()) {
header_remove($name);
}
}
/**
* Sends the given response code if supported.
*
* @param int $code Eg 204
*
* @throws Exception
*/
public static function sendResponseCode($code)
{
$messages = array(
200 => 'Ok',
204 => 'No Response',
301 => 'Moved Permanently',
302 => 'Found',
304 => 'Not Modified',
400 => 'Bad Request',
401 => 'Unauthorized',
403 => 'Forbidden',
404 => 'Not Found',
429 => 'Too Many Requests',
500 => 'Internal Server Error',
503 => 'Service Unavailable',
);
if (!array_key_exists($code, $messages)) {
throw new Exception('Response code not supported: ' . $code);
}
if (strpos(PHP_SAPI, '-fcgi') === false) {
$key = 'HTTP/1.1';
if (array_key_exists('SERVER_PROTOCOL', $_SERVER)
&& strlen($_SERVER['SERVER_PROTOCOL']) < 15
&& strlen($_SERVER['SERVER_PROTOCOL']) > 1) {
$key = $_SERVER['SERVER_PROTOCOL'];
}
} else {
// FastCGI
$key = 'Status:';
}
$message = $messages[$code];
Common::sendHeader($key . ' ' . $code . ' ' . $message);
}
/**
* Returns the ID of the current LocationProvider (see UserCountry plugin code) from
* the Tracker cache.
*/
public static function getCurrentLocationProviderId()
{
$cache = TrackerCache::getCacheGeneral();
return empty($cache['currentLocationProviderId'])
? Plugins\UserCountry\LocationProvider::getDefaultProviderId()
: $cache['currentLocationProviderId'];
}
/**
* Marks an orphaned object for garbage collection.
*
* For more information: {@link https://github.com/piwik/piwik/issues/374}
* @param mixed $var The object to destroy.
* @api
*/
public static function destroy(&$var)
{
if (is_object($var) && method_exists($var, '__destruct')) {
$var->__destruct();
}
unset($var);
$var = null;
}
/**
* @deprecated Use the logger directly instead.
*/
public static function printDebug($info = '')
{
if (is_object($info)) {
$info = var_export($info, true);
}
$logger = StaticContainer::get('Psr\Log\LoggerInterface');
if (is_array($info) || is_object($info)) {
$out = var_export($info, true);
$logger->debug($out);
} else {
$logger->debug($info);
}
}
/**
* Returns true if the request is an AJAX request.
*
* @return bool
*/
public static function isXmlHttpRequest()
{
return isset($_SERVER['HTTP_X_REQUESTED_WITH'])
&& (strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest');
}
/**
* @param $validLanguages
* @return array
*/
protected static function checkValidLanguagesIsSet($validLanguages)
{
/** @var LanguageDataProvider $dataProvider */
$dataProvider = StaticContainer::get('Piwik\Intl\Data\Provider\LanguageDataProvider');
if (empty($validLanguages)) {
$validLanguages = array_keys($dataProvider->getLanguageList());
return $validLanguages;
}
return $validLanguages;
}
}