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; } /** * Returns the table name, after removing the table prefix * * @param string $table * @return string */ 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 \Piwik\PluginsManager::getInstance()->isPluginActivated('Goals'); } /* * URLs */ /** * Returns the path and query part from a URL. * Eg. http://piwik.org/test/index.php?module=CoreHome will return /test/index.php?module=CoreHome * * @param string $url either http://piwik.org/test or / * @return string */ public static function getPathAndQueryFromUrl($url) { $parsedUrl = parse_url($url); $result = ''; if (isset($parsedUrl['path'])) { $result .= substr($parsedUrl['path'], 1); } if (isset($parsedUrl['query'])) { $result .= '?' . $parsedUrl['query']; } return $result; } /** * Returns the value of a GET parameter $parameter in an URL query $urlQuery * * @param string $urlQuery result of parse_url()['query'] and htmlentitied (& is &) eg. module=test&action=toto or ?page=test * @param string $parameter * @return string|bool Parameter value if found (can be the empty string!), null if not found */ public static function getParameterFromQueryString($urlQuery, $parameter) { $nameToValue = self::getArrayFromQueryString($urlQuery); if (isset($nameToValue[$parameter])) { return $nameToValue[$parameter]; } return null; } /** * Returns an URL query string in an array format * * @param string $urlQuery * @return array array( param1=> value1, param2=>value2) */ public static function getArrayFromQueryString($urlQuery) { if (strlen($urlQuery) == 0) { return array(); } if ($urlQuery[0] == '?') { $urlQuery = substr($urlQuery, 1); } $separator = '&'; $urlQuery = $separator . $urlQuery; // $urlQuery = str_replace(array('%20'), ' ', $urlQuery); $refererQuery = trim($urlQuery); $values = explode($separator, $refererQuery); $nameToValue = array(); foreach ($values as $value) { $pos = strpos($value, '='); if ($pos !== false) { $name = substr($value, 0, $pos); $value = substr($value, $pos + 1); if ($value === false) { $value = ''; } } else { $name = $value; $value = false; } if (!empty($name)) { $name = self::sanitizeInputValue($name); } if (!empty($value)) { $value = self::sanitizeInputValue($value); } // if array without indexes $count = 0; $tmp = preg_replace('/(\[|%5b)(]|%5d)$/i', '', $name, -1, $count); if (!empty($tmp) && $count) { $name = $tmp; if (isset($nameToValue[$name]) == false || is_array($nameToValue[$name]) == false) { $nameToValue[$name] = array(); } array_push($nameToValue[$name], $value); } else if (!empty($name)) { $nameToValue[$name] = $value; } } return $nameToValue; } /** * Builds a URL from the result of parse_url function * Copied from the PHP comments at http://php.net/parse_url * @param array $parsed * @return bool|string */ public static function getParseUrlReverse($parsed) { if (!is_array($parsed)) { return false; } $uri = !empty($parsed['scheme']) ? $parsed['scheme'] . ':' . (!strcasecmp($parsed['scheme'], 'mailto') ? '' : '//') : ''; $uri .= !empty($parsed['user']) ? $parsed['user'] . (!empty($parsed['pass']) ? ':' . $parsed['pass'] : '') . '@' : ''; $uri .= !empty($parsed['host']) ? $parsed['host'] : ''; $uri .= !empty($parsed['port']) ? ':' . $parsed['port'] : ''; if (!empty($parsed['path'])) { $uri .= (!strncmp($parsed['path'], '/', 1)) ? $parsed['path'] : ((!empty($uri) ? '/' : '') . $parsed['path']); } $uri .= !empty($parsed['query']) ? '?' . $parsed['query'] : ''; $uri .= !empty($parsed['fragment']) ? '#' . $parsed['fragment'] : ''; return $uri; } /** * Returns true if the string passed may be a URL. * We don't need a precise test here because the value comes from the website * tracked source code and the URLs may look very strange. * * @param string $url * @return bool */ public static function isLookLikeUrl($url) { return preg_match('~^(ftp|news|http|https)?://(.*)$~D', $url, $matches) !== 0 && strlen($matches[2]) > 0; } /* * File operations */ /** * ending WITHOUT slash * * @return string */ public static function getPathToPiwikRoot() { return realpath(dirname(__FILE__) . "/.."); } /** * Create directory if permitted * * @param string $path * @param bool $denyAccess */ public static function mkdir($path, $denyAccess = true) { if (!is_dir($path)) { // the mode in mkdir is modified by the current umask @mkdir($path, $mode = 0755, $recursive = true); } // try to overcome restrictive umask (mis-)configuration if (!is_writable($path)) { @chmod($path, 0755); if (!is_writable($path)) { @chmod($path, 0775); // enough! we're not going to make the directory world-writeable } } if ($denyAccess) { self::createHtAccess($path, $overwrite = false); } } /** * Create .htaccess file in specified directory * * Apache-specific; for IIS @see web.config * * @param string $path without trailing slash * @param bool $overwrite whether to overwrite an existing file or not * @param string $content */ public static function createHtAccess($path, $overwrite = true, $content = "\n\nDeny from all\n\n\n\nDeny from all\n\n\n\nDeny from all\n\n\n") { if (self::isApache()) { $file = $path . '/.htaccess'; if ($overwrite || !file_exists($file)) { @file_put_contents($file, $content); } } } /** * Get canonicalized absolute path * See http://php.net/realpath * * @param string $path * @return string canonicalized absolute path */ public static function realpath($path) { if (file_exists($path)) { return realpath($path); } return $path; } /** * Returns true if the string is a valid filename * File names that start with a-Z or 0-9 and contain a-Z, 0-9, underscore(_), dash(-), and dot(.) will be accepted. * File names beginning with anything but a-Z or 0-9 will be rejected (including .htaccess for example). * File names containing anything other than above mentioned will also be rejected (file names with spaces won't be accepted). * * @param string $filename * @return bool * */ public static function isValidFilename($filename) { return (0 !== preg_match('/(^[a-zA-Z0-9]+([a-zA-Z_0-9.-]*))$/D', $filename)); } /* * String operations */ /** * byte-oriented substr() - ASCII * * @param string $string * @param int $start * @param int ... optional length * @return string */ public static function substr($string, $start) { // in case mbstring overloads substr function $substr = function_exists('mb_orig_substr') ? 'mb_orig_substr' : 'substr'; $length = func_num_args() > 2 ? func_get_arg(2) : self::strlen($string); return $substr($string, $start, $length); } /** * byte-oriented strlen() - ASCII * * @param string $string * @return int */ public static function strlen($string) { // in case mbstring overloads strlen function $strlen = function_exists('mb_orig_strlen') ? 'mb_orig_strlen' : 'strlen'; return $strlen($string); } /** * multi-byte substr() - UTF-8 * * @param string $string * @param int $start * @param int ... optional length * @return string */ public static function mb_substr($string, $start) { $length = func_num_args() > 2 ? func_get_arg(2) : self::mb_strlen($string); if (function_exists('mb_substr')) { return mb_substr($string, $start, $length, 'UTF-8'); } return substr($string, $start, $length); } /** * multi-byte strlen() - UTF-8 * * @param string $string * @return int */ public static function mb_strlen($string) { if (function_exists('mb_strlen')) { return mb_strlen($string, 'UTF-8'); } return strlen($string); } /** * multi-byte strtolower() - UTF-8 * * @param string $string * @return string */ public static function mb_strtolower($string) { if (function_exists('mb_strtolower')) { return mb_strtolower($string, 'UTF-8'); } return strtolower($string); } /* * Escaping input */ /** * Returns the variable after cleaning operations. * NB: The variable still has to be escaped before going into a SQL Query! * * If an array is passed the cleaning is done recursively on all the sub-arrays. * The array's keys are filtered as well! * * How this method works: * - The variable returned has been htmlspecialchars to avoid the XSS security problem. * - The single quotes are not protected so "Piwik's amazing" will still be "Piwik's amazing". * * - Transformations are: * - '&' (ampersand) becomes '&' * - '"'(double quote) becomes '"' * - '<' (less than) becomes '<' * - '>' (greater than) becomes '>' * - It handles the magic_quotes setting. * - A non string value is returned without modification * * @param mixed $value The variable to be cleaned * @param bool $alreadyStripslashed * @throws Exception * @return mixed The variable after cleaning */ public static function sanitizeInputValues($value, $alreadyStripslashed = false) { if (is_numeric($value)) { return $value; } elseif (is_string($value)) { $value = self::sanitizeInputValue($value); if (!$alreadyStripslashed) // a JSON array was already stripslashed, don't do it again for each value { $value = self::undoMagicQuotes($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 * * @param string $value * @return string sanitized input */ public static function sanitizeInputValue($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, \Piwik\Common::HTML_ENCODING_QUOTE_STYLE, 'UTF-8'); // filter $value = str_replace(array("\n", "\r", "\0"), '', $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; } /** * Unsanitize a single input value * * @param string $value * @return string unsanitized input */ public static function unsanitizeInputValue($value) { return htmlspecialchars_decode($value, self::HTML_ENCODING_QUOTE_STYLE); } /** * Unsanitize one or more values. * * @param string|array $value * @return string|array unsanitized input */ 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); } } /** * Undo the damage caused by magic_quotes; deprecated in php 5.3 but not removed until php 5.4 * * @param string * @return string modified or not */ public static function undoMagicQuotes($value) { return version_compare(PHP_VERSION, '5.4', '<') && get_magic_quotes_gpc() ? stripslashes($value) : $value; } /** * Returns a sanitized variable value from the $_GET and $_POST superglobal. * If the variable doesn't have a value or an empty value, returns the defaultValue if specified. * If the variable doesn't have neither a value nor a default value provided, an exception is raised. * * @see sanitizeInputValues() for the applied sanitization * * @param string $varName name of the variable * @param string $varDefault default value. If '', and if the type doesn't match, exit() ! * @param string $varType Expected type, the value must be one of the following: array, int, integer, string, json * @param array $requestArrayToUse * * @throws Exception if the variable type is not known * or if the variable we want to read doesn't have neither a value nor a default value specified * * @return mixed The variable after cleaning */ 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 = self::undoMagicQuotes($requestArrayToUse[$varName]); $value = self::json_decode($value, $assoc = true); return self::sanitizeInputValues($value, $alreadyStripslashed = true); } $value = self::sanitizeInputValues($requestArrayToUse[$varName]); if (!is_null($varType)) { $ok = false; if ($varType === 'string') { if (is_string($value)) $ok = true; } elseif ($varType === 'integer') { if ($value == (string)(int)$value) $ok = true; } elseif ($varType === 'float') { if ($value == (string)(float)$value) $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; } /* * Generating unique strings */ /** * Returns a 32 characters long uniq ID * * @return string 32 chars */ public static function generateUniqId() { return md5(uniqid(rand(), true)); } /** * Get salt from [superuser] section * * @return string */ public static function getSalt() { static $salt = null; if (is_null($salt)) { $salt = @Config::getInstance()->superuser['salt']; } return $salt; } /** * Configureable 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 = ''; list($usec, $sec) = explode(" ", microtime()); $seed = ((float)$sec + (float)$usec) * 100000; mt_srand($seed); for ($i = 0; $i < $length; $i++) { $rand_key = mt_rand(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) !== Piwik_Tracker::LENGTH_HEX_ID_STRING || @bin2hex(self::hex2bin($id)) != $id ) { throw new Exception("visitorId is expected to be a " . Piwik_Tracker::LENGTH_HEX_ID_STRING . " hex char string"); } return self::hex2bin($id); } /** * Convert IP address (in network address format) to presentation format. * This is a backward compatibility function for code that only expects * IPv4 addresses (i.e., doesn't support IPv6). * * @see Piwik_IP::N2P() * * This function does not support the long (or its string representation) * returned by the built-in ip2long() function, from Piwik 1.3 and earlier. * * @deprecated 1.4 * * @param string $ip IP address in network address format * @return string */ public static function long2ip($ip) { return Piwik_IP::long2ip($ip); } /** * Should we use the replacement json_encode/json_decode functions? * * @return bool True if broken; false otherwise */ private static function useJsonLibrary() { static $useLib; if (!isset($useLib)) { /* * 5.1.x - doesn't have json extension; we use lib/upgradephp instead * 5.2 to 5.2.4 - broken in various ways, including: * * @see https://bugs.php.net/bug.php?id=38680 'json_decode cannot decode basic types' * @see https://bugs.php.net/bug.php?id=41403 'json_decode cannot decode floats' * @see https://bugs.php.net/bug.php?id=42785 'json_encode outputs numbers according to locale' */ $useLib = false; if (version_compare(PHP_VERSION, '5.2.1') < 0) { $useLib = true; } else if (version_compare(PHP_VERSION, '5.2.5') < 0) { $info = localeconv(); $useLib = $info['decimal_point'] != '.'; } } return $useLib; } /** * JSON encode wrapper * - missing or broken in some php 5.x versions * * @param mixed $value * @return string */ public static function json_encode($value) { if (self::useJsonLibrary()) { return _json_encode($value); } return @json_encode($value); } /** * JSON decode wrapper * - missing or broken in some php 5.x versions * * @param string $json * @param bool $assoc * @return mixed */ public static function json_decode($json, $assoc = false) { if (self::useJsonLibrary()) { return _json_decode($json, $assoc); } return json_decode($json, $assoc); } /* * DataFiles */ /** * Returns list of continent codes * * @see core/DataFiles/Countries.php * * @return array Array of 3 letter continent codes */ public static function getContinentsList() { require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/Countries.php'; $continentsList = $GLOBALS['Piwik_ContinentList']; return $continentsList; } /** * Returns list of valid country codes * * @see core/DataFiles/Countries.php * * @param bool $includeInternalCodes * @return array Array of (2 letter ISO codes => 3 letter continent code) */ public static function getCountriesList($includeInternalCodes = false) { require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/Countries.php'; $countriesList = $GLOBALS['Piwik_CountryList']; $extras = $GLOBALS['Piwik_CountryList_Extras']; if ($includeInternalCodes) { return array_merge($countriesList, $extras); } return $countriesList; } /** * Returns list of valid language codes * * @see core/DataFiles/Languages.php * * @return array Array of 2 letter ISO codes => Language name (in English) */ public static function getLanguagesList() { require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/Languages.php'; $languagesList = $GLOBALS['Piwik_LanguageList']; return $languagesList; } /** * Returns list of language to country mappings * * @see core/DataFiles/LanguageToCountry.php * * @return array Array of ( 2 letter ISO language codes => 2 letter ISO country codes ) */ public static function getLanguageToCountryList() { require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/LanguageToCountry.php'; $languagesList = $GLOBALS['Piwik_LanguageToCountry']; return $languagesList; } /** * Returns list of search engines by URL * * @see core/DataFiles/SearchEngines.php * * @return array Array of ( URL => array( searchEngineName, keywordParameter, path, charset ) ) */ public static function getSearchEngineUrls() { require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/SearchEngines.php'; $searchEngines = $GLOBALS['Piwik_SearchEngines']; return $searchEngines; } /** * Returns list of search engines by name * * @see core/DataFiles/SearchEngines.php * * @return array Array of ( searchEngineName => URL ) */ public static function getSearchEngineNames() { require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/SearchEngines.php'; $searchEngines = $GLOBALS['Piwik_SearchEngines_NameToUrl']; return $searchEngines; } /** * 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 (is_null($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) { $country = null; Piwik_PostEvent('Common.getCountry', array(&$country, $ip)); if (!empty($country)) { return strtolower($country); } if (empty($lang) || strlen($lang) < 2 || $lang == 'xx') { return 'xx'; } $validCountries = self::getCountriesList(); 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) { $langToCountry = self::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 'xx'; } /** * Returns the visitor language based only on the Browser 'accepted language' information * * @param string $browserLanguage Browser's accepted langauge header * @param array $validLanguages array of valid language codes * @return string 2 letter ISO 639 code */ public static function extractLanguageCodeFromBrowserLanguage($browserLanguage, $validLanguages) { // assumes language preference is sorted; // does not handle language-script-region tags or language range (*) if (!empty($validLanguages) && preg_match_all('/(?:^|,)([a-z]{2,3})([-][a-z]{2})?/', $browserLanguage, $matches, PREG_SET_ORDER)) { foreach ($matches as $parts) { if (count($parts) == 3) { // match locale (language and location) if (in_array($parts[1] . $parts[2], $validLanguages)) { return $parts[1] . $parts[2]; } } // match language only (where no region provided) if (in_array($parts[1], $validLanguages)) { return $parts[1]; } } } return 'xx'; } /** * Returns the continent of a given country * * @param string $country 2 letters isocode * * @return string Continent (3 letters code : afr, asi, eur, amn, ams, oce) */ public static function getContinent($country) { $countryList = self::getCountriesList(); if (isset($countryList[$country])) { return $countryList[$country]; } return '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); } } array_walk_recursive($return, 'trim'); return $return; } /* * Referrer */ /** * Reduce URL to more minimal form. 2 letter country codes are * replaced by '{}', while other parts are simply removed. * * Examples: * www.example.com -> example.com * search.example.com -> example.com * m.example.com -> example.com * de.example.com -> {}.example.com * example.de -> example.{} * example.co.uk -> example.{} * * @param string $url * @return string */ public static function getLossyUrl($url) { static $countries; if (!isset($countries)) { $countries = implode('|', array_keys(self::getCountriesList(true))); } return preg_replace( array( '/^(w+[0-9]*|search)\./', '/(^|\.)m\./', '/(\.(com|org|net|co|it|edu))?\.(' . $countries . ')(\/|$)/', '/(^|\.)(' . $countries . ')\./', ), array( '', '$1', '.{}$4', '$1{}.', ), $url); } /** * Extracts a keyword from a raw not encoded URL. * Will only extract keyword if a known search engine has been detected. * Returns the keyword: * - in UTF8: automatically converted from other charsets when applicable * - strtolowered: "QUErY test!" will return "query test!" * - trimmed: extra spaces before and after are removed * * Lists of supported search engines can be found in /core/DataFiles/SearchEngines.php * The function returns false when a keyword couldn't be found. * eg. if the url is "http://www.google.com/partners.html" this will return false, * as the google keyword parameter couldn't be found. * * @see unit tests in /tests/core/Common.test.php * @param string $referrerUrl URL referer URL, eg. $_SERVER['HTTP_REFERER'] * @return array|false false if a keyword couldn't be extracted, * or array( * 'name' => 'Google', * 'keywords' => 'my searched keywords') */ public static function extractSearchEngineInformationFromUrl($referrerUrl) { $refererParsed = @parse_url($referrerUrl); $refererHost = ''; if (isset($refererParsed['host'])) { $refererHost = $refererParsed['host']; } if (empty($refererHost)) { return false; } // some search engines (eg. Bing Images) use the same domain // as an existing search engine (eg. Bing), we must also use the url path $refererPath = ''; if (isset($refererParsed['path'])) { $refererPath = $refererParsed['path']; } // no search query if (!isset($refererParsed['query'])) { $refererParsed['query'] = ''; } $query = $refererParsed['query']; // Google Referrers URLs sometimes have the fragment which contains the keyword if (!empty($refererParsed['fragment'])) { $query .= '&' . $refererParsed['fragment']; } $searchEngines = self::getSearchEngineUrls(); $hostPattern = self::getLossyUrl($refererHost); if (array_key_exists($refererHost . $refererPath, $searchEngines)) { $refererHost = $refererHost . $refererPath; } elseif (array_key_exists($hostPattern . $refererPath, $searchEngines)) { $refererHost = $hostPattern . $refererPath; } elseif (array_key_exists($hostPattern, $searchEngines)) { $refererHost = $hostPattern; } elseif (!array_key_exists($refererHost, $searchEngines)) { if (!strncmp($query, 'cx=partner-pub-', 15)) { // Google custom search engine $refererHost = 'google.com/cse'; } elseif (!strncmp($refererPath, '/pemonitorhosted/ws/results/', 28)) { // private-label search powered by InfoSpace Metasearch $refererHost = 'wsdsold.infospace.com'; } elseif (strpos($refererHost, '.images.search.yahoo.com') != false) { // Yahoo! Images $refererHost = 'images.search.yahoo.com'; } elseif (strpos($refererHost, '.search.yahoo.com') != false) { // Yahoo! $refererHost = 'search.yahoo.com'; } else { return false; } } $searchEngineName = $searchEngines[$refererHost][0]; $variableNames = null; if (isset($searchEngines[$refererHost][1])) { $variableNames = $searchEngines[$refererHost][1]; } if (!$variableNames) { $searchEngineNames = self::getSearchEngineNames(); $url = $searchEngineNames[$searchEngineName]; $variableNames = $searchEngines[$url][1]; } if (!is_array($variableNames)) { $variableNames = array($variableNames); } $key = null; if ($searchEngineName === 'Google Images' || ($searchEngineName === 'Google' && strpos($referrerUrl, '/imgres') !== false) ) { if (strpos($query, '&prev') !== false) { $query = urldecode(trim(self::getParameterFromQueryString($query, 'prev'))); $query = str_replace('&', '&', strstr($query, '?')); } $searchEngineName = 'Google Images'; } else if ($searchEngineName === 'Google' && (strpos($query, '&as_') !== false || strpos($query, 'as_') === 0) ) { $keys = array(); $key = self::getParameterFromQueryString($query, 'as_q'); if (!empty($key)) { array_push($keys, $key); } $key = self::getParameterFromQueryString($query, 'as_oq'); if (!empty($key)) { array_push($keys, str_replace('+', ' OR ', $key)); } $key = self::getParameterFromQueryString($query, 'as_epq'); if (!empty($key)) { array_push($keys, "\"$key\""); } $key = self::getParameterFromQueryString($query, 'as_eq'); if (!empty($key)) { array_push($keys, "-$key"); } $key = trim(urldecode(implode(' ', $keys))); } if ($searchEngineName === 'Google') { // top bar menu $tbm = self::getParameterFromQueryString($query, 'tbm'); switch ($tbm) { case 'isch': $searchEngineName = 'Google Images'; break; case 'vid': $searchEngineName = 'Google Video'; break; case 'shop': $searchEngineName = 'Google Shopping'; break; } } if (empty($key)) { foreach ($variableNames as $variableName) { if ($variableName[0] == '/') { // regular expression match if (preg_match($variableName, $referrerUrl, $matches)) { $key = trim(urldecode($matches[1])); break; } } else { // search for keywords now &vname=keyword $key = self::getParameterFromQueryString($query, $variableName); $key = trim(urldecode($key)); // Special case: Google & empty q parameter if (empty($key) && $variableName == 'q' && ( // Google search with no keyword ($searchEngineName == 'Google' && ( // First, they started putting an empty q= parameter strpos($query, '&q=') !== false || strpos($query, '?q=') !== false // then they started sending the full host only (no path/query string) || (empty($query) && (empty($refererPath) || $refererPath == '/') && empty($refererParsed['fragment'])) ) ) // search engines with no keyword || $searchEngineName == 'Google Images' || $searchEngineName == 'DuckDuckGo') ) { $key = false; } if (!empty($key) || $key === false ) { break; } } } } // $key === false is the special case "No keyword provided" which is a Search engine match if ($key === null || $key === '' ) { return false; } if (!empty($key)) { if (function_exists('iconv') && isset($searchEngines[$refererHost][3]) ) { // accepts string, array, or comma-separated list string in preferred order $charsets = $searchEngines[$refererHost][3]; if (!is_array($charsets)) { $charsets = explode(',', $charsets); } if (!empty($charsets)) { $charset = $charsets[0]; if (count($charsets) > 1 && function_exists('mb_detect_encoding') ) { $charset = mb_detect_encoding($key, $charsets); if ($charset === false) { $charset = $charsets[0]; } } $newkey = @iconv($charset, 'UTF-8//IGNORE', $key); if (!empty($newkey)) { $key = $newkey; } } } $key = self::mb_strtolower($key); } return array( 'name' => $searchEngineName, 'keywords' => $key, ); } /* * System environment */ /** * 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() { $remoteAddr = @$_SERVER['REMOTE_ADDR']; return PHP_SAPI == 'cli' || (!strncmp(PHP_SAPI, 'cgi', 3) && empty($remoteAddr)); } /** * Is the current script execution triggered by misc/cron/archive.php ? * * Helpful for error handling: directly throw error without HTML (eg. when DB is down) * @return bool */ public static function isArchivePhpTriggered() { return !empty($_GET['trigger']) && $_GET['trigger'] == 'archivephp'; } /** * Assign CLI parameters as if they were REQUEST or GET parameters. * You can trigger Piwik from the command line by * # /usr/bin/php5 /path/to/piwik/index.php -- "module=API&method=Actions.getActions&idSite=1&period=day&date=previous8&format=php" */ public static function assignCliParametersToRequest() { if (isset($_SERVER['argc']) && $_SERVER['argc'] > 0 ) { for ($i = 1; $i < $_SERVER['argc']; $i++) { parse_str($_SERVER['argv'][$i], $tmp); $_GET = array_merge($_GET, $tmp); } } } /** * Returns true if running on a Windows operating system * * @since 0.6.5 * @return bool true if PHP detects it is running on Windows; else false */ public static function isWindows() { return DIRECTORY_SEPARATOR === '\\'; } /** * Returns true if running on MacOS * * @return bool true if PHP detects it is running on MacOS; else false */ public static function isMacOS() { return PHP_OS === 'Darwin'; } /** * Returns true if running on an Apache web server * * @return bool */ public static function isApache() { $apache = isset($_SERVER['SERVER_SOFTWARE']) && !strncmp($_SERVER['SERVER_SOFTWARE'], 'Apache', 6); return $apache; } /** * Returns true if running on Microsoft IIS 7 (or above) * * @return bool */ public static function isIIS() { $iis = isset($_SERVER['SERVER_SOFTWARE']) && preg_match('/^Microsoft-IIS\/(.+)/', $_SERVER['SERVER_SOFTWARE'], $matches) && version_compare($matches[1], '7') >= 0; return $iis; } /** * Takes a list of fields defining numeric values and returns the corresponding * unnamed parameters to be bound to the field names in the where clause of a SQL query * * @param array|string $fields array( fieldName1, fieldName2, fieldName3) Names of the mysql table fields to load * @return string "?, ?, ?" */ public static function getSqlStringFieldsArray($fields) { if (is_string($fields)) { $fields = array($fields); } $count = count($fields); if ($count == 0) { return "''"; } return '?' . str_repeat(',?', $count - 1); } /** * Sets outgoing header. * * @param string $header The header. * @param bool $replace Whether to replace existing or not. */ public static function sendHeader($header, $replace = true) { if (isset($GLOBALS['PIWIK_TRACKER_LOCAL_TRACKING']) && $GLOBALS['PIWIK_TRACKER_LOCAL_TRACKING']) { @header($header, $replace); } else { header($header, $replace); } } /** * Returns the ID of the current LocationProvider (see UserCountry plugin code) from * the Tracker cache. */ public static function getCurrentLocationProviderId() { $cache = Piwik_Tracker_Cache::getCacheGeneral(); return empty($cache['currentLocationProviderId']) ? Piwik_UserCountry_LocationProvider_Default::ID : $cache['currentLocationProviderId']; } /** * Unprefix class name (if needed) * * @param string $class * @return string */ public static function unprefixClass($class) { $lenPrefix = strlen(self::CLASSES_PREFIX); if (!strncmp($class, self::CLASSES_PREFIX, $lenPrefix)) { return substr($class, $lenPrefix); } return $class; } /** * Mark orphaned object for garbage collection * * For more information: @link http://dev.piwik.org/trac/ticket/374 * @param $var */ static public function destroy(&$var) { if (is_object($var) && method_exists($var, '__destruct')) { $var->__destruct(); } unset($var); $var = null; } static public function printDebug($info = '') { if (isset($GLOBALS['PIWIK_TRACKER_DEBUG']) && $GLOBALS['PIWIK_TRACKER_DEBUG']) { if (is_array($info) || is_object($info)) { print("
");
                print(htmlspecialchars(var_export($info, true), ENT_QUOTES));
                print("
"); } else { print(htmlspecialchars($info, ENT_QUOTES) . "
\n"); } } } }