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