Teknik is a suite of services with attractive and functional interfaces. https://www.teknik.io/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Common.php 35KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113
  1. <?php
  2. /**
  3. * Piwik - free/libre analytics platform
  4. *
  5. * @link http://piwik.org
  6. * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  7. *
  8. */
  9. namespace Piwik;
  10. use Exception;
  11. use Piwik\Plugins\UserCountry\LocationProvider\DefaultProvider;
  12. use Piwik\Tracker;
  13. use Piwik\Tracker\Cache;
  14. /**
  15. * Contains helper methods used by both Piwik Core and the Piwik Tracking engine.
  16. *
  17. * This is the only non-Tracker class loaded by the **\/piwik.php** file.
  18. */
  19. class Common
  20. {
  21. // constants used to map the referrer type to an integer in the log_visit table
  22. const REFERRER_TYPE_DIRECT_ENTRY = 1;
  23. const REFERRER_TYPE_SEARCH_ENGINE = 2;
  24. const REFERRER_TYPE_WEBSITE = 3;
  25. const REFERRER_TYPE_CAMPAIGN = 6;
  26. // Flag used with htmlspecialchar. See php.net/htmlspecialchars.
  27. const HTML_ENCODING_QUOTE_STYLE = ENT_QUOTES;
  28. public static $isCliMode = null;
  29. /*
  30. * Database
  31. */
  32. /**
  33. * Hashes a string into an integer which should be very low collision risks
  34. * @param string $string String to hash
  35. * @return int Resulting int hash
  36. */
  37. public static function hashStringToInt($string)
  38. {
  39. $stringHash = substr(md5($string), 0, 8);
  40. return base_convert($stringHash, 16, 10);
  41. }
  42. /**
  43. * Returns a prefixed table name.
  44. *
  45. * The table prefix is determined by the `[database] tables_prefix` INI config
  46. * option.
  47. *
  48. * @param string $table The table name to prefix, ie "log_visit"
  49. * @return string The prefixed name, ie "piwik-production_log_visit".
  50. * @api
  51. */
  52. public static function prefixTable($table)
  53. {
  54. $prefix = Config::getInstance()->database['tables_prefix'];
  55. return $prefix . $table;
  56. }
  57. /**
  58. * Returns an array containing the prefixed table names of every passed argument.
  59. *
  60. * @param string ... The table names to prefix, ie "log_visit"
  61. * @return array The prefixed names in an array.
  62. */
  63. public static function prefixTables()
  64. {
  65. $result = array();
  66. foreach (func_get_args() as $table) {
  67. $result[] = self::prefixTable($table);
  68. }
  69. return $result;
  70. }
  71. /**
  72. * Removes the prefix from a table name and returns the result.
  73. *
  74. * The table prefix is determined by the `[database] tables_prefix` INI config
  75. * option.
  76. *
  77. * @param string $table The prefixed table name, eg "piwik-production_log_visit".
  78. * @return string The unprefixed table name, eg "log_visit".
  79. * @api
  80. */
  81. public static function unprefixTable($table)
  82. {
  83. static $prefixTable = null;
  84. if (is_null($prefixTable)) {
  85. $prefixTable = Config::getInstance()->database['tables_prefix'];
  86. }
  87. if (empty($prefixTable)
  88. || strpos($table, $prefixTable) !== 0
  89. ) {
  90. return $table;
  91. }
  92. $count = 1;
  93. return str_replace($prefixTable, '', $table, $count);
  94. }
  95. /*
  96. * Tracker
  97. */
  98. public static function isGoalPluginEnabled()
  99. {
  100. return \Piwik\Plugin\Manager::getInstance()->isPluginActivated('Goals');
  101. }
  102. public static function isActionsPluginEnabled()
  103. {
  104. return \Piwik\Plugin\Manager::getInstance()->isPluginActivated('Actions');
  105. }
  106. /**
  107. * Returns true if PHP was invoked from command-line interface (shell)
  108. *
  109. * @since added in 0.4.4
  110. * @return bool true if PHP invoked as a CGI or from CLI
  111. */
  112. public static function isPhpCliMode()
  113. {
  114. if (is_bool(self::$isCliMode)) {
  115. return self::$isCliMode;
  116. }
  117. $remoteAddr = @$_SERVER['REMOTE_ADDR'];
  118. return PHP_SAPI == 'cli' ||
  119. (self::isPhpCgiType() && empty($remoteAddr));
  120. }
  121. /**
  122. * Returns true if PHP is executed as CGI type.
  123. *
  124. * @since added in 0.4.4
  125. * @return bool true if PHP invoked as a CGI
  126. */
  127. public static function isPhpCgiType()
  128. {
  129. $sapiType = php_sapi_name();
  130. return substr($sapiType, 0, 3) === 'cgi';
  131. }
  132. /**
  133. * Returns true if the current request is a console command, eg.
  134. * ./console xx:yy
  135. * or
  136. * php console xx:yy
  137. *
  138. * @return bool
  139. */
  140. public static function isRunningConsoleCommand()
  141. {
  142. $searched = 'console';
  143. $consolePos = strpos($_SERVER['SCRIPT_NAME'], $searched);
  144. $expectedConsolePos = strlen($_SERVER['SCRIPT_NAME']) - strlen($searched);
  145. $isScriptIsConsole = ($consolePos === $expectedConsolePos);
  146. return self::isPhpCliMode() && $isScriptIsConsole;
  147. }
  148. /*
  149. * String operations
  150. */
  151. /**
  152. * Multi-byte substr() - works with UTF-8.
  153. *
  154. * Calls `mb_substr` if available and falls back to `substr` if it's not.
  155. *
  156. * @param string $string
  157. * @param int $start
  158. * @param int ... optional length
  159. * @return string
  160. * @api
  161. */
  162. public static function mb_substr($string, $start)
  163. {
  164. $length = func_num_args() > 2
  165. ? func_get_arg(2)
  166. : self::mb_strlen($string);
  167. if (function_exists('mb_substr')) {
  168. return mb_substr($string, $start, $length, 'UTF-8');
  169. }
  170. return substr($string, $start, $length);
  171. }
  172. /**
  173. * Multi-byte strlen() - works with UTF-8
  174. *
  175. * Calls `mb_substr` if available and falls back to `substr` if not.
  176. *
  177. * @param string $string
  178. * @return int
  179. * @api
  180. */
  181. public static function mb_strlen($string)
  182. {
  183. if (function_exists('mb_strlen')) {
  184. return mb_strlen($string, 'UTF-8');
  185. }
  186. return strlen($string);
  187. }
  188. /**
  189. * Multi-byte strtolower() - works with UTF-8.
  190. *
  191. * Calls `mb_strtolower` if available and falls back to `strtolower` if not.
  192. *
  193. * @param string $string
  194. * @return string
  195. * @api
  196. */
  197. public static function mb_strtolower($string)
  198. {
  199. if (function_exists('mb_strtolower')) {
  200. return mb_strtolower($string, 'UTF-8');
  201. }
  202. return strtolower($string);
  203. }
  204. /*
  205. * Escaping input
  206. */
  207. /**
  208. * Sanitizes a string to help avoid XSS vulnerabilities.
  209. *
  210. * This function is automatically called when {@link getRequestVar()} is called,
  211. * so you should not normally have to use it.
  212. *
  213. * This function should be used when outputting data that isn't escaped and was
  214. * obtained from the user (for example when using the `|raw` twig filter on goal names).
  215. *
  216. * _NOTE: Sanitized input should not be used directly in an SQL query; SQL placeholders
  217. * should still be used._
  218. *
  219. * **Implementation Details**
  220. *
  221. * - [htmlspecialchars](http://php.net/manual/en/function.htmlspecialchars.php) is used to escape text.
  222. * - Single quotes are not escaped so **Piwik's amazing community** will still be
  223. * **Piwik's amazing community**.
  224. * - Use of the `magic_quotes` setting will not break this method.
  225. * - Boolean, numeric and null values are not modified.
  226. *
  227. * @param mixed $value The variable to be sanitized. If an array is supplied, the contents
  228. * of the array will be sanitized recursively. The keys of the array
  229. * will also be sanitized.
  230. * @param bool $alreadyStripslashed Implementation detail, ignore.
  231. * @throws Exception If `$value` is of an incorrect type.
  232. * @return mixed The sanitized value.
  233. * @api
  234. */
  235. public static function sanitizeInputValues($value, $alreadyStripslashed = false)
  236. {
  237. if (is_numeric($value)) {
  238. return $value;
  239. } elseif (is_string($value)) {
  240. $value = self::sanitizeInputValue($value);
  241. if (!$alreadyStripslashed) // a JSON array was already stripslashed, don't do it again for each value
  242. {
  243. $value = self::undoMagicQuotes($value);
  244. }
  245. } elseif (is_array($value)) {
  246. foreach (array_keys($value) as $key) {
  247. $newKey = $key;
  248. $newKey = self::sanitizeInputValues($newKey, $alreadyStripslashed);
  249. if ($key != $newKey) {
  250. $value[$newKey] = $value[$key];
  251. unset($value[$key]);
  252. }
  253. $value[$newKey] = self::sanitizeInputValues($value[$newKey], $alreadyStripslashed);
  254. }
  255. } elseif (!is_null($value)
  256. && !is_bool($value)
  257. ) {
  258. throw new Exception("The value to escape has not a supported type. Value = " . var_export($value, true));
  259. }
  260. return $value;
  261. }
  262. /**
  263. * Sanitize a single input value
  264. *
  265. * @param string $value
  266. * @return string sanitized input
  267. */
  268. public static function sanitizeInputValue($value)
  269. {
  270. // $_GET and $_REQUEST already urldecode()'d
  271. // decode
  272. // note: before php 5.2.7, htmlspecialchars() double encodes &#x hex items
  273. $value = html_entity_decode($value, self::HTML_ENCODING_QUOTE_STYLE, 'UTF-8');
  274. // filter
  275. $value = self::sanitizeLineBreaks($value);
  276. // escape
  277. $tmp = @htmlspecialchars($value, self::HTML_ENCODING_QUOTE_STYLE, 'UTF-8');
  278. // note: php 5.2.5 and above, htmlspecialchars is destructive if input is not UTF-8
  279. if ($value != '' && $tmp == '') {
  280. // convert and escape
  281. $value = utf8_encode($value);
  282. $tmp = htmlspecialchars($value, self::HTML_ENCODING_QUOTE_STYLE, 'UTF-8');
  283. }
  284. return $tmp;
  285. }
  286. /**
  287. * Unsanitizes a single input value and returns the result.
  288. *
  289. * @param string $value
  290. * @return string unsanitized input
  291. */
  292. public static function unsanitizeInputValue($value)
  293. {
  294. return htmlspecialchars_decode($value, self::HTML_ENCODING_QUOTE_STYLE);
  295. }
  296. /**
  297. * Unsanitizes one or more values and returns the result.
  298. *
  299. * This method should be used when you need to unescape data that was obtained from
  300. * the user.
  301. *
  302. * Some data in Piwik is stored sanitized (such as site name). In this case you may
  303. * have to use this method to unsanitize it in order to, for example, output it in JSON.
  304. *
  305. * @param string|array $value The data to unsanitize. If an array is passed, the
  306. * array is sanitized recursively. Key values are not unsanitized.
  307. * @return string|array The unsanitized data.
  308. * @api
  309. */
  310. public static function unsanitizeInputValues($value)
  311. {
  312. if (is_array($value)) {
  313. $result = array();
  314. foreach ($value as $key => $arrayValue) {
  315. $result[$key] = self::unsanitizeInputValues($arrayValue);
  316. }
  317. return $result;
  318. } else {
  319. return self::unsanitizeInputValue($value);
  320. }
  321. }
  322. /**
  323. * Undo the damage caused by magic_quotes; deprecated in php 5.3 but not removed until php 5.4
  324. *
  325. * @param string
  326. * @return string modified or not
  327. */
  328. private static function undoMagicQuotes($value)
  329. {
  330. return version_compare(PHP_VERSION, '5.4', '<')
  331. && get_magic_quotes_gpc()
  332. ? stripslashes($value)
  333. : $value;
  334. }
  335. /**
  336. *
  337. * @param string
  338. * @return string Line breaks and line carriage removed
  339. */
  340. public static function sanitizeLineBreaks($value)
  341. {
  342. $value = str_replace(array("\n", "\r", "\0"), '', $value);
  343. return $value;
  344. }
  345. /**
  346. * Gets a sanitized request parameter by name from the `$_GET` and `$_POST` superglobals.
  347. *
  348. * Use this function to get request parameter values. **_NEVER use `$_GET` and `$_POST` directly._**
  349. *
  350. * If the variable cannot be found, and a default value was not provided, an exception is raised.
  351. *
  352. * _See {@link sanitizeInputValues()} to learn more about sanitization._
  353. *
  354. * @param string $varName Name of the request parameter to get. By default, we look in `$_GET[$varName]`
  355. * and `$_POST[$varName]` for the value.
  356. * @param string|null $varDefault The value to return if the request parameter cannot be found or has an empty value.
  357. * @param string|null $varType Expected type of the request variable. This parameters value must be one of the following:
  358. * `'array'`, `'int'`, `'integer'`, `'string'`, `'json'`.
  359. *
  360. * If `'json'`, the string value will be `json_decode`-d and then sanitized.
  361. * @param array|null $requestArrayToUse The array to use instead of `$_GET` and `$_POST`.
  362. * @throws Exception If the request parameter doesn't exist and there is no default value, or if the request parameter
  363. * exists but has an incorrect type.
  364. * @return mixed The sanitized request parameter.
  365. * @api
  366. */
  367. public static function getRequestVar($varName, $varDefault = null, $varType = null, $requestArrayToUse = null)
  368. {
  369. if (is_null($requestArrayToUse)) {
  370. $requestArrayToUse = $_GET + $_POST;
  371. }
  372. $varDefault = self::sanitizeInputValues($varDefault);
  373. if ($varType === 'int') {
  374. // settype accepts only integer
  375. // 'int' is simply a shortcut for 'integer'
  376. $varType = 'integer';
  377. }
  378. // there is no value $varName in the REQUEST so we try to use the default value
  379. if (empty($varName)
  380. || !isset($requestArrayToUse[$varName])
  381. || (!is_array($requestArrayToUse[$varName])
  382. && strlen($requestArrayToUse[$varName]) === 0
  383. )
  384. ) {
  385. if (is_null($varDefault)) {
  386. throw new Exception("The parameter '$varName' isn't set in the Request, and a default value wasn't provided.");
  387. } else {
  388. if (!is_null($varType)
  389. && in_array($varType, array('string', 'integer', 'array'))
  390. ) {
  391. settype($varDefault, $varType);
  392. }
  393. return $varDefault;
  394. }
  395. }
  396. // Normal case, there is a value available in REQUEST for the requested varName:
  397. // we deal w/ json differently
  398. if ($varType == 'json') {
  399. $value = self::undoMagicQuotes($requestArrayToUse[$varName]);
  400. $value = self::json_decode($value, $assoc = true);
  401. return self::sanitizeInputValues($value, $alreadyStripslashed = true);
  402. }
  403. $value = self::sanitizeInputValues($requestArrayToUse[$varName]);
  404. if (!is_null($varType)) {
  405. $ok = false;
  406. if ($varType === 'string') {
  407. if (is_string($value)) $ok = true;
  408. } elseif ($varType === 'integer') {
  409. if ($value == (string)(int)$value) $ok = true;
  410. } elseif ($varType === 'float') {
  411. if ($value == (string)(float)$value) $ok = true;
  412. } elseif ($varType === 'array') {
  413. if (is_array($value)) $ok = true;
  414. } else {
  415. throw new Exception("\$varType specified is not known. It should be one of the following: array, int, integer, float, string");
  416. }
  417. // The type is not correct
  418. if ($ok === false) {
  419. if ($varDefault === null) {
  420. throw new Exception("The parameter '$varName' doesn't have a correct type, and a default value wasn't provided.");
  421. } // we return the default value with the good type set
  422. else {
  423. settype($varDefault, $varType);
  424. return $varDefault;
  425. }
  426. }
  427. settype($value, $varType);
  428. }
  429. return $value;
  430. }
  431. /*
  432. * Generating unique strings
  433. */
  434. /**
  435. * Returns a 32 characters long uniq ID
  436. *
  437. * @return string 32 chars
  438. */
  439. public static function generateUniqId()
  440. {
  441. return md5(uniqid(rand(), true));
  442. }
  443. /**
  444. * Configureable hash() algorithm (defaults to md5)
  445. *
  446. * @param string $str String to be hashed
  447. * @param bool $raw_output
  448. * @return string Hash string
  449. */
  450. public static function hash($str, $raw_output = false)
  451. {
  452. static $hashAlgorithm = null;
  453. if (is_null($hashAlgorithm)) {
  454. $hashAlgorithm = @Config::getInstance()->General['hash_algorithm'];
  455. }
  456. if ($hashAlgorithm) {
  457. $hash = @hash($hashAlgorithm, $str, $raw_output);
  458. if ($hash !== false)
  459. return $hash;
  460. }
  461. return md5($str, $raw_output);
  462. }
  463. /**
  464. * Generate random string.
  465. * Do not use for security related purposes (the string is not truly random).
  466. *
  467. * @param int $length string length
  468. * @param string $alphabet characters allowed in random string
  469. * @return string random string with given length
  470. */
  471. public static function getRandomString($length = 16, $alphabet = "abcdefghijklmnoprstuvwxyz0123456789")
  472. {
  473. $chars = $alphabet;
  474. $str = '';
  475. list($usec, $sec) = explode(" ", microtime());
  476. $seed = ((float)$sec + (float)$usec) * 100000;
  477. mt_srand($seed);
  478. for ($i = 0; $i < $length; $i++) {
  479. $rand_key = mt_rand(0, strlen($chars) - 1);
  480. $str .= substr($chars, $rand_key, 1);
  481. }
  482. return str_shuffle($str);
  483. }
  484. /*
  485. * Conversions
  486. */
  487. /**
  488. * Convert hexadecimal representation into binary data.
  489. * !! Will emit warning if input string is not hex!!
  490. *
  491. * @see http://php.net/bin2hex
  492. *
  493. * @param string $str Hexadecimal representation
  494. * @return string
  495. */
  496. public static function hex2bin($str)
  497. {
  498. return pack("H*", $str);
  499. }
  500. /**
  501. * This function will convert the input string to the binary representation of the ID
  502. * but it will throw an Exception if the specified input ID is not correct
  503. *
  504. * This is used when building segments containing visitorId which could be an invalid string
  505. * therefore throwing Unexpected PHP error [pack(): Type H: illegal hex digit i] severity [E_WARNING]
  506. *
  507. * It would be simply to silent fail the pack() call above but in all other cases, we don't expect an error,
  508. * so better be safe and get the php error when something unexpected is happening
  509. * @param string $id
  510. * @throws Exception
  511. * @return string binary string
  512. */
  513. public static function convertVisitorIdToBin($id)
  514. {
  515. if (strlen($id) !== Tracker::LENGTH_HEX_ID_STRING
  516. || @bin2hex(self::hex2bin($id)) != $id
  517. ) {
  518. throw new Exception("visitorId is expected to be a " . Tracker::LENGTH_HEX_ID_STRING . " hex char string");
  519. }
  520. return self::hex2bin($id);
  521. }
  522. /**
  523. * Converts a User ID string to the Visitor ID Binary representation.
  524. *
  525. * @param $userId
  526. * @return string
  527. */
  528. public static function convertUserIdToVisitorIdBin($userId)
  529. {
  530. require_once PIWIK_INCLUDE_PATH . '/libs/PiwikTracker/PiwikTracker.php';
  531. $userIdHashed = \PiwikTracker::getUserIdHashed($userId);
  532. return self::convertVisitorIdToBin($userIdHashed);
  533. }
  534. /**
  535. * Convert IP address (in network address format) to presentation format.
  536. * This is a backward compatibility function for code that only expects
  537. * IPv4 addresses (i.e., doesn't support IPv6).
  538. *
  539. * @see IP::N2P()
  540. *
  541. * This function does not support the long (or its string representation)
  542. * returned by the built-in ip2long() function, from Piwik 1.3 and earlier.
  543. *
  544. * @deprecated 1.4
  545. *
  546. * @param string $ip IP address in network address format
  547. * @return string
  548. */
  549. public static function long2ip($ip)
  550. {
  551. return IP::long2ip($ip);
  552. }
  553. /**
  554. * JSON encode wrapper
  555. * - missing or broken in some php 5.x versions
  556. *
  557. * @param mixed $value
  558. * @return string
  559. * @deprecated
  560. */
  561. public static function json_encode($value)
  562. {
  563. return @json_encode($value);
  564. }
  565. /**
  566. * JSON decode wrapper
  567. * - missing or broken in some php 5.x versions
  568. *
  569. * @param string $json
  570. * @param bool $assoc
  571. * @return mixed
  572. * @deprecated
  573. */
  574. public static function json_decode($json, $assoc = false)
  575. {
  576. return json_decode($json, $assoc);
  577. }
  578. /**
  579. * Detects whether an error occurred during the last json encode/decode.
  580. * @return bool
  581. */
  582. public static function hasJsonErrorOccurred()
  583. {
  584. return json_last_error() != JSON_ERROR_NONE;
  585. }
  586. /**
  587. * Returns a human readable error message in case an error occcurred during the last json encode/decode.
  588. * Returns an empty string in case there was no error.
  589. *
  590. * @return string
  591. */
  592. public static function getLastJsonError()
  593. {
  594. switch (json_last_error()) {
  595. case JSON_ERROR_NONE:
  596. return '';
  597. case JSON_ERROR_DEPTH:
  598. return 'Maximum stack depth exceeded';
  599. case JSON_ERROR_STATE_MISMATCH:
  600. return 'Underflow or the modes mismatch';
  601. case JSON_ERROR_CTRL_CHAR:
  602. return 'Unexpected control character found';
  603. case JSON_ERROR_SYNTAX:
  604. return 'Syntax error, malformed JSON';
  605. case JSON_ERROR_UTF8:
  606. return 'Malformed UTF-8 characters, possibly incorrectly encoded';
  607. }
  608. return 'Unknown error';
  609. }
  610. public static function stringEndsWith($haystack, $needle)
  611. {
  612. if ('' === $needle) {
  613. return true;
  614. }
  615. $lastCharacters = substr($haystack, -strlen($needle));
  616. return $lastCharacters === $needle;
  617. }
  618. /**
  619. * Returns the list of parent classes for the given class.
  620. *
  621. * @param string $klass A class name.
  622. * @return string[] The list of parent classes in order from highest ancestor to the descended class.
  623. */
  624. public static function getClassLineage($klass)
  625. {
  626. $klasses = array_merge(array($klass), array_values(class_parents($klass, $autoload = false)));
  627. return array_reverse($klasses);
  628. }
  629. /*
  630. * DataFiles
  631. */
  632. /**
  633. * Returns list of continent codes
  634. *
  635. * @see core/DataFiles/Countries.php
  636. *
  637. * @return array Array of 3 letter continent codes
  638. */
  639. public static function getContinentsList()
  640. {
  641. require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/Countries.php';
  642. $continentsList = $GLOBALS['Piwik_ContinentList'];
  643. return $continentsList;
  644. }
  645. /**
  646. * Returns list of valid country codes
  647. *
  648. * @see core/DataFiles/Countries.php
  649. *
  650. * @param bool $includeInternalCodes
  651. * @return array Array of (2 letter ISO codes => 3 letter continent code)
  652. */
  653. public static function getCountriesList($includeInternalCodes = false)
  654. {
  655. require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/Countries.php';
  656. $countriesList = $GLOBALS['Piwik_CountryList'];
  657. $extras = $GLOBALS['Piwik_CountryList_Extras'];
  658. if ($includeInternalCodes) {
  659. return array_merge($countriesList, $extras);
  660. }
  661. return $countriesList;
  662. }
  663. /**
  664. * Returns the list of valid language codes.
  665. *
  666. * See [core/DataFiles/Languages.php](https://github.com/piwik/piwik/blob/master/core/DataFiles/Languages.php).
  667. *
  668. * @return array Array of two letter ISO codes mapped with their associated language names (in English). E.g.
  669. * `array('en' => 'English', 'ja' => 'Japanese')`.
  670. * @api
  671. */
  672. public static function getLanguagesList()
  673. {
  674. require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/Languages.php';
  675. $languagesList = $GLOBALS['Piwik_LanguageList'];
  676. return $languagesList;
  677. }
  678. /**
  679. * Returns a list of language to country mappings.
  680. *
  681. * See [core/DataFiles/LanguageToCountry.php](https://github.com/piwik/piwik/blob/master/core/DataFiles/LanguageToCountry.php).
  682. *
  683. * @return array Array of two letter ISO language codes mapped with two letter ISO country codes:
  684. * `array('fr' => 'fr') // French => France`
  685. * @api
  686. */
  687. public static function getLanguageToCountryList()
  688. {
  689. require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/LanguageToCountry.php';
  690. $languagesList = $GLOBALS['Piwik_LanguageToCountry'];
  691. return $languagesList;
  692. }
  693. /**
  694. * Returns list of search engines by URL
  695. *
  696. * @see core/DataFiles/SearchEngines.php
  697. *
  698. * @return array Array of ( URL => array( searchEngineName, keywordParameter, path, charset ) )
  699. */
  700. public static function getSearchEngineUrls()
  701. {
  702. require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/SearchEngines.php';
  703. $searchEngines = $GLOBALS['Piwik_SearchEngines'];
  704. Piwik::postEvent('Referrer.addSearchEngineUrls', array(&$searchEngines));
  705. return $searchEngines;
  706. }
  707. /**
  708. * Returns list of search engines by name
  709. *
  710. * @see core/DataFiles/SearchEngines.php
  711. *
  712. * @return array Array of ( searchEngineName => URL )
  713. */
  714. public static function getSearchEngineNames()
  715. {
  716. $searchEngines = self::getSearchEngineUrls();
  717. $nameToUrl = array();
  718. foreach ($searchEngines as $url => $info) {
  719. if (!isset($nameToUrl[$info[0]])) {
  720. $nameToUrl[$info[0]] = $url;
  721. }
  722. }
  723. return $nameToUrl;
  724. }
  725. /**
  726. * Returns list of social networks by URL
  727. *
  728. * @see core/DataFiles/Socials.php
  729. *
  730. * @return array Array of ( URL => Social Network Name )
  731. */
  732. public static function getSocialUrls()
  733. {
  734. require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/Socials.php';
  735. $socialUrls = $GLOBALS['Piwik_socialUrl'];
  736. Piwik::postEvent('Referrer.addSocialUrls', array(&$socialUrls));
  737. return $socialUrls;
  738. }
  739. /**
  740. * Returns list of provider names
  741. *
  742. * @see core/DataFiles/Providers.php
  743. *
  744. * @return array Array of ( dnsName => providerName )
  745. */
  746. public static function getProviderNames()
  747. {
  748. require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/Providers.php';
  749. $providers = $GLOBALS['Piwik_ProviderNames'];
  750. return $providers;
  751. }
  752. /*
  753. * Language, country, continent
  754. */
  755. /**
  756. * Returns the browser language code, eg. "en-gb,en;q=0.5"
  757. *
  758. * @param string|null $browserLang Optional browser language, otherwise taken from the request header
  759. * @return string
  760. */
  761. public static function getBrowserLanguage($browserLang = null)
  762. {
  763. static $replacementPatterns = array(
  764. // extraneous bits of RFC 3282 that we ignore
  765. '/(\\\\.)/', // quoted-pairs
  766. '/(\s+)/', // CFWcS white space
  767. '/(\([^)]*\))/', // CFWS comments
  768. '/(;q=[0-9.]+)/', // quality
  769. // found in the LANG environment variable
  770. '/\.(.*)/', // charset (e.g., en_CA.UTF-8)
  771. '/^C$/', // POSIX 'C' locale
  772. );
  773. if (is_null($browserLang)) {
  774. $browserLang = self::sanitizeInputValues(@$_SERVER['HTTP_ACCEPT_LANGUAGE']);
  775. if (empty($browserLang) && self::isPhpCliMode()) {
  776. $browserLang = @getenv('LANG');
  777. }
  778. }
  779. if (empty($browserLang)) {
  780. // a fallback might be to infer the language in HTTP_USER_AGENT (i.e., localized build)
  781. $browserLang = "";
  782. } else {
  783. // language tags are case-insensitive per HTTP/1.1 s3.10 but the region may be capitalized per ISO3166-1;
  784. // underscores are not permitted per RFC 4646 or 4647 (which obsolete RFC 1766 and 3066),
  785. // but we guard against a bad user agent which naively uses its locale
  786. $browserLang = strtolower(str_replace('_', '-', $browserLang));
  787. // filters
  788. $browserLang = preg_replace($replacementPatterns, '', $browserLang);
  789. $browserLang = preg_replace('/((^|,)chrome:.*)/', '', $browserLang, 1); // Firefox bug
  790. $browserLang = preg_replace('/(,)(?:en-securid,)|(?:(^|,)en-securid(,|$))/', '$1', $browserLang, 1); // unregistered language tag
  791. $browserLang = str_replace('sr-sp', 'sr-rs', $browserLang); // unofficial (proposed) code in the wild
  792. }
  793. return $browserLang;
  794. }
  795. /**
  796. * Returns the visitor country based on the Browser 'accepted language'
  797. * information, but provides a hook for geolocation via IP address.
  798. *
  799. * @param string $lang browser lang
  800. * @param bool $enableLanguageToCountryGuess If set to true, some assumption will be made and detection guessed more often, but accuracy could be affected
  801. * @param string $ip
  802. * @return string 2 letter ISO code
  803. */
  804. public static function getCountry($lang, $enableLanguageToCountryGuess, $ip)
  805. {
  806. if (empty($lang) || strlen($lang) < 2 || $lang == 'xx') {
  807. return 'xx';
  808. }
  809. $validCountries = self::getCountriesList();
  810. return self::extractCountryCodeFromBrowserLanguage($lang, $validCountries, $enableLanguageToCountryGuess);
  811. }
  812. /**
  813. * Returns list of valid country codes
  814. *
  815. * @param string $browserLanguage
  816. * @param array $validCountries Array of valid countries
  817. * @param bool $enableLanguageToCountryGuess (if true, will guess country based on language that lacks region information)
  818. * @return array Array of 2 letter ISO codes
  819. */
  820. public static function extractCountryCodeFromBrowserLanguage($browserLanguage, $validCountries, $enableLanguageToCountryGuess)
  821. {
  822. $langToCountry = self::getLanguageToCountryList();
  823. if ($enableLanguageToCountryGuess) {
  824. if (preg_match('/^([a-z]{2,3})(?:,|;|$)/', $browserLanguage, $matches)) {
  825. // match language (without region) to infer the country of origin
  826. if (array_key_exists($matches[1], $langToCountry)) {
  827. return $langToCountry[$matches[1]];
  828. }
  829. }
  830. }
  831. if (!empty($validCountries) && preg_match_all('/[-]([a-z]{2})/', $browserLanguage, $matches, PREG_SET_ORDER)) {
  832. foreach ($matches as $parts) {
  833. // match location; we don't make any inferences from the language
  834. if (array_key_exists($parts[1], $validCountries)) {
  835. return $parts[1];
  836. }
  837. }
  838. }
  839. return 'xx';
  840. }
  841. /**
  842. * Returns the visitor language based only on the Browser 'accepted language' information
  843. *
  844. * @param string $browserLanguage Browser's accepted langauge header
  845. * @param array $validLanguages array of valid language codes
  846. * @return string 2 letter ISO 639 code
  847. */
  848. public static function extractLanguageCodeFromBrowserLanguage($browserLanguage, $validLanguages)
  849. {
  850. // assumes language preference is sorted;
  851. // does not handle language-script-region tags or language range (*)
  852. if (!empty($validLanguages) && preg_match_all('/(?:^|,)([a-z]{2,3})([-][a-z]{2})?/', $browserLanguage, $matches, PREG_SET_ORDER)) {
  853. foreach ($matches as $parts) {
  854. if (count($parts) == 3) {
  855. // match locale (language and location)
  856. if (in_array($parts[1] . $parts[2], $validLanguages)) {
  857. return $parts[1] . $parts[2];
  858. }
  859. }
  860. // match language only (where no region provided)
  861. if (in_array($parts[1], $validLanguages)) {
  862. return $parts[1];
  863. }
  864. }
  865. }
  866. return 'xx';
  867. }
  868. /**
  869. * Returns the continent of a given country
  870. *
  871. * @param string $country 2 letters isocode
  872. *
  873. * @return string Continent (3 letters code : afr, asi, eur, amn, ams, oce)
  874. */
  875. public static function getContinent($country)
  876. {
  877. $countryList = self::getCountriesList();
  878. if (isset($countryList[$country])) {
  879. return $countryList[$country];
  880. }
  881. return 'unk';
  882. }
  883. /*
  884. * Campaign
  885. */
  886. /**
  887. * Returns the list of Campaign parameter names that will be read to classify
  888. * a visit as coming from a Campaign
  889. *
  890. * @return array array(
  891. * 0 => array( ... ) // campaign names parameters
  892. * 1 => array( ... ) // campaign keyword parameters
  893. * );
  894. */
  895. public static function getCampaignParameters()
  896. {
  897. $return = array(
  898. Config::getInstance()->Tracker['campaign_var_name'],
  899. Config::getInstance()->Tracker['campaign_keyword_var_name'],
  900. );
  901. foreach ($return as &$list) {
  902. if (strpos($list, ',') !== false) {
  903. $list = explode(',', $list);
  904. } else {
  905. $list = array($list);
  906. }
  907. $list = array_map('trim', $list);
  908. }
  909. return $return;
  910. }
  911. /*
  912. * Referrer
  913. */
  914. /**
  915. * Returns a string with a comma separated list of placeholders for use in an SQL query. Used mainly
  916. * to fill the `IN (...)` part of a query.
  917. *
  918. * @param array|string $fields The names of the mysql table fields to bind, e.g.
  919. * `array(fieldName1, fieldName2, fieldName3)`.
  920. *
  921. * _Note: The content of the array isn't important, just its length._
  922. * @return string The placeholder string, e.g. `"?, ?, ?"`.
  923. * @api
  924. */
  925. public static function getSqlStringFieldsArray($fields)
  926. {
  927. if (is_string($fields)) {
  928. $fields = array($fields);
  929. }
  930. $count = count($fields);
  931. if ($count == 0) {
  932. return "''";
  933. }
  934. return '?' . str_repeat(',?', $count - 1);
  935. }
  936. /**
  937. * Sets outgoing header.
  938. *
  939. * @param string $header The header.
  940. * @param bool $replace Whether to replace existing or not.
  941. */
  942. public static function sendHeader($header, $replace = true)
  943. {
  944. // don't send header in CLI mode
  945. if(!Common::isPhpCliMode() and !headers_sent()) {
  946. header($header, $replace);
  947. }
  948. }
  949. /**
  950. * Returns the ID of the current LocationProvider (see UserCountry plugin code) from
  951. * the Tracker cache.
  952. */
  953. public static function getCurrentLocationProviderId()
  954. {
  955. $cache = Cache::getCacheGeneral();
  956. return empty($cache['currentLocationProviderId'])
  957. ? DefaultProvider::ID
  958. : $cache['currentLocationProviderId'];
  959. }
  960. /**
  961. * Marks an orphaned object for garbage collection.
  962. *
  963. * For more information: {@link https://github.com/piwik/piwik/issues/374}
  964. * @param $var The object to destroy.
  965. * @api
  966. */
  967. public static function destroy(&$var)
  968. {
  969. if (is_object($var) && method_exists($var, '__destruct')) {
  970. $var->__destruct();
  971. }
  972. unset($var);
  973. $var = null;
  974. }
  975. public static function printDebug($info = '')
  976. {
  977. if (isset($GLOBALS['PIWIK_TRACKER_DEBUG']) && $GLOBALS['PIWIK_TRACKER_DEBUG']) {
  978. if (is_object($info)) {
  979. $info = var_export($info, true);
  980. }
  981. Log::getInstance()->setLogLevel(Log::DEBUG);
  982. if (is_array($info) || is_object($info)) {
  983. $info = Common::sanitizeInputValues($info);
  984. $out = var_export($info, true);
  985. foreach (explode("\n", $out) as $line) {
  986. Log::debug($line);
  987. }
  988. } else {
  989. foreach (explode("\n", $info) as $line) {
  990. Log::debug(htmlspecialchars($line, ENT_QUOTES));
  991. }
  992. }
  993. }
  994. }
  995. }