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