PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 1.0.3
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v1.0.3
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 6 years ago Access 6 years ago Application 6 years ago Archive 6 years ago ArchiveProcessor 6 years ago Archiver 6 years ago AssetManager 6 years ago Auth 6 years ago Category 6 years ago CliMulti 6 years ago Columns 6 years ago Composer 6 years ago Concurrency 6 years ago Config 6 years ago Container 6 years ago CronArchive 6 years ago DataAccess 6 years ago DataFiles 6 years ago DataTable 6 years ago Db 6 years ago DeviceDetector 6 years ago Email 6 years ago Exception 6 years ago Http 6 years ago Intl 6 years ago Mail 6 years ago Measurable 6 years ago Menu 6 years ago Metrics 6 years ago Notification 6 years ago Period 6 years ago Plugin 6 years ago ProfessionalServices 6 years ago Report 6 years ago ReportRenderer 6 years ago Scheduler 6 years ago Segment 6 years ago Session 6 years ago Settings 6 years ago Tracker 6 years ago Translation 6 years ago UpdateCheck 6 years ago Updater 6 years ago Updates 6 years ago Validators 6 years ago View 6 years ago ViewDataTable 6 years ago Visualization 6 years ago Widget 6 years ago .htaccess 6 years ago Access.php 6 years ago Archive.php 6 years ago ArchiveProcessor.php 6 years ago AssetManager.php 6 years ago Auth.php 6 years ago BaseFactory.php 6 years ago Cache.php 6 years ago CacheId.php 6 years ago CliMulti.php 6 years ago Common.php 6 years ago Config.php 6 years ago Console.php 6 years ago Context.php 6 years ago Cookie.php 6 years ago CronArchive.php 6 years ago DataArray.php 6 years ago DataTable.php 6 years ago Date.php 6 years ago Db.php 6 years ago DbHelper.php 6 years ago Development.php 6 years ago DeviceDetectorFactory.php 6 years ago ErrorHandler.php 6 years ago EventDispatcher.php 6 years ago ExceptionHandler.php 6 years ago FileIntegrity.php 6 years ago Filechecks.php 6 years ago Filesystem.php 6 years ago FrontController.php 6 years ago Http.php 6 years ago IP.php 6 years ago Log.php 6 years ago LogDeleter.php 6 years ago Mail.php 6 years ago Metrics.php 6 years ago MetricsFormatter.php 6 years ago Nonce.php 6 years ago Notification.php 6 years ago NumberFormatter.php 6 years ago Option.php 6 years ago Period.php 6 years ago Piwik.php 6 years ago Plugin.php 6 years ago Profiler.php 6 years ago ProxyHeaders.php 6 years ago ProxyHttp.php 6 years ago QuickForm2.php 6 years ago RankingQuery.php 6 years ago Registry.php 6 years ago ReportRenderer.php 6 years ago ScheduledTask.php 6 years ago Segment.php 6 years ago Sequence.php 6 years ago Session.php 6 years ago SettingsPiwik.php 6 years ago SettingsServer.php 6 years ago Singleton.php 6 years ago Site.php 6 years ago TCPDF.php 6 years ago TaskScheduler.php 6 years ago Theme.php 6 years ago Timer.php 6 years ago Tracker.php 6 years ago Translate.php 6 years ago Twig.php 6 years ago Unzip.php 6 years ago UpdateCheck.php 6 years ago Updater.php 6 years ago Updates.php 6 years ago Url.php 6 years ago UrlHelper.php 6 years ago Version.php 6 years ago View.php 6 years ago bootstrap.php 6 years ago dispatch.php 6 years ago testMinimumPhpVersion.php 6 years ago
Common.php
1332 lines
1 <?php
2 /**
3 * Piwik - 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 * Secure wrapper for unserialize, which by default disallows unserializing classes
287 *
288 * @param string $string String to unserialize
289 * @param array $allowedClasses Class names that should be allowed to unserialize
290 * @param bool $rethrow Whether to rethrow exceptions or not.
291 * @return mixed
292 */
293 public static function safe_unserialize($string, $allowedClasses = [], $rethrow = false)
294 {
295 if (PHP_MAJOR_VERSION >= 7) {
296 try {
297 return unserialize($string, ['allowed_classes' => empty($allowedClasses) ? false : $allowedClasses]);
298 } catch (\Throwable $e) {
299 if ($rethrow) {
300 throw $e;
301 }
302
303 $logger = StaticContainer::get('Psr\Log\LoggerInterface');
304 $logger->debug('Unable to unserialize a string: {message} (string = {string})', [
305 'message' => $e->getMessage(),
306 'backtrace' => $e->getTraceAsString(),
307 'string' => $string,
308 ]);
309 return false;
310 }
311 }
312
313 return @unserialize($string);
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 $rand = self::getRandomInt();
633
634 return md5(uniqid($rand, true));
635 }
636
637 /**
638 * Configurable hash() algorithm (defaults to md5)
639 *
640 * @param string $str String to be hashed
641 * @param bool $raw_output
642 * @return string Hash string
643 */
644 public static function hash($str, $raw_output = false)
645 {
646 static $hashAlgorithm = null;
647
648 if (is_null($hashAlgorithm)) {
649 $hashAlgorithm = @Config::getInstance()->General['hash_algorithm'];
650 }
651
652 if ($hashAlgorithm) {
653 $hash = @hash($hashAlgorithm, $str, $raw_output);
654 if ($hash !== false) {
655 return $hash;
656 }
657 }
658
659 return md5($str, $raw_output);
660 }
661
662 /**
663 * Generate random string.
664 *
665 * @param int $length string length
666 * @param string $alphabet characters allowed in random string
667 * @return string random string with given length
668 */
669 public static function getRandomString($length = 16, $alphabet = "abcdefghijklmnoprstuvwxyz0123456789")
670 {
671 $chars = $alphabet;
672 $str = '';
673
674 for ($i = 0; $i < $length; $i++) {
675 $rand_key = self::getRandomInt(0, strlen($chars) - 1);
676 $str .= substr($chars, $rand_key, 1);
677 }
678
679 return str_shuffle($str);
680 }
681
682 /*
683 * Conversions
684 */
685
686 /**
687 * Convert hexadecimal representation into binary data.
688 * !! Will emit warning if input string is not hex!!
689 *
690 * @see http://php.net/bin2hex
691 *
692 * @param string $str Hexadecimal representation
693 * @return string
694 */
695 public static function hex2bin($str)
696 {
697 return pack("H*", $str);
698 }
699
700 /**
701 * This function will convert the input string to the binary representation of the ID
702 * but it will throw an Exception if the specified input ID is not correct
703 *
704 * This is used when building segments containing visitorId which could be an invalid string
705 * therefore throwing Unexpected PHP error [pack(): Type H: illegal hex digit i] severity [E_WARNING]
706 *
707 * It would be simply to silent fail the pack() call above but in all other cases, we don't expect an error,
708 * so better be safe and get the php error when something unexpected is happening
709 * @param string $id
710 * @throws Exception
711 * @return string binary string
712 */
713 public static function convertVisitorIdToBin($id)
714 {
715 if (strlen($id) !== Tracker::LENGTH_HEX_ID_STRING
716 || @bin2hex(self::hex2bin($id)) != $id
717 ) {
718 throw new Exception("visitorId is expected to be a " . Tracker::LENGTH_HEX_ID_STRING . " hex char string");
719 }
720
721 return self::hex2bin($id);
722 }
723
724 /**
725 * Converts a User ID string to the Visitor ID Binary representation.
726 *
727 * @param $userId
728 * @return string
729 */
730 public static function convertUserIdToVisitorIdBin($userId)
731 {
732 require_once PIWIK_INCLUDE_PATH . '/libs/PiwikTracker/PiwikTracker.php';
733 $userIdHashed = \PiwikTracker::getUserIdHashed($userId);
734
735 return self::convertVisitorIdToBin($userIdHashed);
736 }
737
738 /**
739 * JSON encode wrapper
740 * - missing or broken in some php 5.x versions
741 *
742 * @param mixed $value
743 * @return string
744 * @deprecated
745 */
746 public static function json_encode($value)
747 {
748 return @json_encode($value);
749 }
750
751 /**
752 * JSON decode wrapper
753 * - missing or broken in some php 5.x versions
754 *
755 * @param string $json
756 * @param bool $assoc
757 * @return mixed
758 * @deprecated
759 */
760 public static function json_decode($json, $assoc = false)
761 {
762 return json_decode($json, $assoc);
763 }
764
765 /**
766 * Detects whether an error occurred during the last json encode/decode.
767 * @return bool
768 */
769 public static function hasJsonErrorOccurred()
770 {
771 return json_last_error() != JSON_ERROR_NONE;
772 }
773
774 /**
775 * Returns a human readable error message in case an error occcurred during the last json encode/decode.
776 * Returns an empty string in case there was no error.
777 *
778 * @return string
779 */
780 public static function getLastJsonError()
781 {
782 switch (json_last_error()) {
783 case JSON_ERROR_NONE:
784 return '';
785 case JSON_ERROR_DEPTH:
786 return 'Maximum stack depth exceeded';
787 case JSON_ERROR_STATE_MISMATCH:
788 return 'Underflow or the modes mismatch';
789 case JSON_ERROR_CTRL_CHAR:
790 return 'Unexpected control character found';
791 case JSON_ERROR_SYNTAX:
792 return 'Syntax error, malformed JSON';
793 case JSON_ERROR_UTF8:
794 return 'Malformed UTF-8 characters, possibly incorrectly encoded';
795 }
796
797 return 'Unknown error';
798 }
799
800 public static function stringEndsWith($haystack, $needle)
801 {
802 if ('' === $needle) {
803 return true;
804 }
805
806 $lastCharacters = substr($haystack, -strlen($needle));
807
808 return $lastCharacters === $needle;
809 }
810
811 /**
812 * Returns the list of parent classes for the given class.
813 *
814 * @param string $class A class name.
815 * @return string[] The list of parent classes in order from highest ancestor to the descended class.
816 */
817 public static function getClassLineage($class)
818 {
819 $classes = array_merge(array($class), array_values(class_parents($class, $autoload = false)));
820
821 return array_reverse($classes);
822 }
823
824 /*
825 * DataFiles
826 */
827
828 /**
829 * Returns list of continent codes
830 *
831 * @see core/DataFiles/Countries.php
832 *
833 * @return array Array of 3 letter continent codes
834 *
835 * @deprecated Use Piwik\Intl\Data\Provider\RegionDataProvider instead.
836 * @see \Piwik\Intl\Data\Provider\RegionDataProvider::getContinentList()
837 */
838 public static function getContinentsList()
839 {
840 /** @var RegionDataProvider $dataProvider */
841 $dataProvider = StaticContainer::get('Piwik\Intl\Data\Provider\RegionDataProvider');
842 return $dataProvider->getContinentList();
843 }
844
845 /**
846 * Returns list of valid country codes
847 *
848 * @see core/DataFiles/Countries.php
849 *
850 * @param bool $includeInternalCodes
851 * @return array Array of (2 letter ISO codes => 3 letter continent code)
852 *
853 * @deprecated Use Piwik\Intl\Data\Provider\RegionDataProvider instead.
854 * @see \Piwik\Intl\Data\Provider\RegionDataProvider::getCountryList()
855 */
856 public static function getCountriesList($includeInternalCodes = false)
857 {
858 /** @var RegionDataProvider $dataProvider */
859 $dataProvider = StaticContainer::get('Piwik\Intl\Data\Provider\RegionDataProvider');
860 return $dataProvider->getCountryList($includeInternalCodes);
861 }
862
863 /**
864 * Returns the list of valid language codes.
865 *
866 * See [core/DataFiles/Languages.php](https://github.com/piwik/piwik/blob/master/core/DataFiles/Languages.php).
867 *
868 * @return array Array of two letter ISO codes mapped with their associated language names (in English). E.g.
869 * `array('en' => 'English', 'ja' => 'Japanese')`.
870 * @api
871 *
872 * @deprecated Use Piwik\Intl\Data\Provider\LanguageDataProvider instead.
873 * @see \Piwik\Intl\Data\Provider\LanguageDataProvider::getLanguageList()
874 */
875 public static function getLanguagesList()
876 {
877 /** @var LanguageDataProvider $dataProvider */
878 $dataProvider = StaticContainer::get('Piwik\Intl\Data\Provider\LanguageDataProvider');
879 return $dataProvider->getLanguageList();
880 }
881
882 /**
883 * Returns a list of language to country mappings.
884 *
885 * See [core/DataFiles/LanguageToCountry.php](https://github.com/piwik/piwik/blob/master/core/DataFiles/LanguageToCountry.php).
886 *
887 * @return array Array of two letter ISO language codes mapped with two letter ISO country codes:
888 * `array('fr' => 'fr') // French => France`
889 * @api
890 *
891 * @deprecated Use Piwik\Intl\Data\Provider\LanguageDataProvider instead.
892 * @see \Piwik\Intl\Data\Provider\LanguageDataProvider::getLanguageToCountryList()
893 */
894 public static function getLanguageToCountryList()
895 {
896 /** @var LanguageDataProvider $dataProvider */
897 $dataProvider = StaticContainer::get('Piwik\Intl\Data\Provider\LanguageDataProvider');
898 return $dataProvider->getLanguageToCountryList();
899 }
900
901 /**
902 * Returns list of provider names
903 *
904 * @see core/DataFiles/Providers.php
905 *
906 * @return array Array of ( dnsName => providerName )
907 */
908 public static function getProviderNames()
909 {
910 require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/Providers.php';
911
912 $providers = $GLOBALS['Piwik_ProviderNames'];
913 return $providers;
914 }
915
916 /*
917 * Language, country, continent
918 */
919
920 /**
921 * Returns the browser language code, eg. "en-gb,en;q=0.5"
922 *
923 * @param string|null $browserLang Optional browser language, otherwise taken from the request header
924 * @return string
925 */
926 public static function getBrowserLanguage($browserLang = null)
927 {
928 static $replacementPatterns = array(
929 // extraneous bits of RFC 3282 that we ignore
930 '/(\\\\.)/', // quoted-pairs
931 '/(\s+)/', // CFWcS white space
932 '/(\([^)]*\))/', // CFWS comments
933 '/(;q=[0-9.]+)/', // quality
934
935 // found in the LANG environment variable
936 '/\.(.*)/', // charset (e.g., en_CA.UTF-8)
937 '/^C$/', // POSIX 'C' locale
938 );
939
940 if (is_null($browserLang)) {
941 $browserLang = self::sanitizeInputValues(@$_SERVER['HTTP_ACCEPT_LANGUAGE']);
942 if (empty($browserLang) && self::isPhpCliMode()) {
943 $browserLang = @getenv('LANG');
944 }
945 }
946
947 if (empty($browserLang)) {
948 // a fallback might be to infer the language in HTTP_USER_AGENT (i.e., localized build)
949 $browserLang = "";
950 } else {
951 // language tags are case-insensitive per HTTP/1.1 s3.10 but the region may be capitalized per ISO3166-1;
952 // underscores are not permitted per RFC 4646 or 4647 (which obsolete RFC 1766 and 3066),
953 // but we guard against a bad user agent which naively uses its locale
954 $browserLang = strtolower(str_replace('_', '-', $browserLang));
955
956 // filters
957 $browserLang = preg_replace($replacementPatterns, '', $browserLang);
958
959 $browserLang = preg_replace('/((^|,)chrome:.*)/', '', $browserLang, 1); // Firefox bug
960 $browserLang = preg_replace('/(,)(?:en-securid,)|(?:(^|,)en-securid(,|$))/', '$1', $browserLang, 1); // unregistered language tag
961
962 $browserLang = str_replace('sr-sp', 'sr-rs', $browserLang); // unofficial (proposed) code in the wild
963 }
964
965 return $browserLang;
966 }
967
968 /**
969 * Returns the visitor country based on the Browser 'accepted language'
970 * information, but provides a hook for geolocation via IP address.
971 *
972 * @param string $lang browser lang
973 * @param bool $enableLanguageToCountryGuess If set to true, some assumption will be made and detection guessed more often, but accuracy could be affected
974 * @param string $ip
975 * @return string 2 letter ISO code
976 */
977 public static function getCountry($lang, $enableLanguageToCountryGuess, $ip)
978 {
979 if (empty($lang) || strlen($lang) < 2 || $lang == self::LANGUAGE_CODE_INVALID) {
980 return self::LANGUAGE_CODE_INVALID;
981 }
982
983 /** @var RegionDataProvider $dataProvider */
984 $dataProvider = StaticContainer::get('Piwik\Intl\Data\Provider\RegionDataProvider');
985
986 $validCountries = $dataProvider->getCountryList();
987
988 return self::extractCountryCodeFromBrowserLanguage($lang, $validCountries, $enableLanguageToCountryGuess);
989 }
990
991 /**
992 * Returns list of valid country codes
993 *
994 * @param string $browserLanguage
995 * @param array $validCountries Array of valid countries
996 * @param bool $enableLanguageToCountryGuess (if true, will guess country based on language that lacks region information)
997 * @return array Array of 2 letter ISO codes
998 */
999 public static function extractCountryCodeFromBrowserLanguage($browserLanguage, $validCountries, $enableLanguageToCountryGuess)
1000 {
1001 /** @var LanguageDataProvider $dataProvider */
1002 $dataProvider = StaticContainer::get('Piwik\Intl\Data\Provider\LanguageDataProvider');
1003
1004 $langToCountry = $dataProvider->getLanguageToCountryList();
1005
1006 if ($enableLanguageToCountryGuess) {
1007 if (preg_match('/^([a-z]{2,3})(?:,|;|$)/', $browserLanguage, $matches)) {
1008 // match language (without region) to infer the country of origin
1009 if (array_key_exists($matches[1], $langToCountry)) {
1010 return $langToCountry[$matches[1]];
1011 }
1012 }
1013 }
1014
1015 if (!empty($validCountries) && preg_match_all('/[-]([a-z]{2})/', $browserLanguage, $matches, PREG_SET_ORDER)) {
1016 foreach ($matches as $parts) {
1017 // match location; we don't make any inferences from the language
1018 if (array_key_exists($parts[1], $validCountries)) {
1019 return $parts[1];
1020 }
1021 }
1022 }
1023 return self::LANGUAGE_CODE_INVALID;
1024 }
1025
1026 /**
1027 * Returns the language and region string, based only on the Browser 'accepted language' information.
1028 * * The language tag is defined by ISO 639-1
1029 *
1030 * @param string $browserLanguage Browser's accepted langauge header
1031 * @param array $validLanguages array of valid language codes
1032 * @return string 2 letter ISO 639 code 'es' (Spanish)
1033 */
1034 public static function extractLanguageCodeFromBrowserLanguage($browserLanguage, $validLanguages = array())
1035 {
1036 $validLanguages = self::checkValidLanguagesIsSet($validLanguages);
1037 $languageRegionCode = self::extractLanguageAndRegionCodeFromBrowserLanguage($browserLanguage, $validLanguages);
1038
1039 if (strlen($languageRegionCode) == 2) {
1040 $languageCode = $languageRegionCode;
1041 } else {
1042 $languageCode = substr($languageRegionCode, 0, 2);
1043 }
1044 if (in_array($languageCode, $validLanguages)) {
1045 return $languageCode;
1046 }
1047 return self::LANGUAGE_CODE_INVALID;
1048 }
1049
1050 /**
1051 * Returns the language and region string, based only on the Browser 'accepted language' information.
1052 * * The language tag is defined by ISO 639-1
1053 * * The region tag is defined by ISO 3166-1
1054 *
1055 * @param string $browserLanguage Browser's accepted langauge header
1056 * @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.
1057 * @return string 2 letter ISO 639 code 'es' (Spanish) or if found, includes the region as well: 'es-ar'
1058 */
1059 public static function extractLanguageAndRegionCodeFromBrowserLanguage($browserLanguage, $validLanguages = array())
1060 {
1061 $validLanguages = self::checkValidLanguagesIsSet($validLanguages);
1062
1063 if (!preg_match_all('/(?:^|,)([a-z]{2,3})([-][a-z]{2})?/', $browserLanguage, $matches, PREG_SET_ORDER)) {
1064 return self::LANGUAGE_CODE_INVALID;
1065 }
1066 foreach ($matches as $parts) {
1067 $langIso639 = $parts[1];
1068 if (empty($langIso639)) {
1069 continue;
1070 }
1071
1072 // If a region tag is found eg. "fr-ca"
1073 if (count($parts) == 3) {
1074 $regionIso3166 = $parts[2]; // eg. "-ca"
1075
1076 if (in_array($langIso639 . $regionIso3166, $validLanguages)) {
1077 return $langIso639 . $regionIso3166;
1078 }
1079
1080 if (in_array($langIso639, $validLanguages)) {
1081 return $langIso639 . $regionIso3166;
1082 }
1083 }
1084 // eg. "fr" or "es"
1085 if (in_array($langIso639, $validLanguages)) {
1086 return $langIso639;
1087 }
1088 }
1089 return self::LANGUAGE_CODE_INVALID;
1090 }
1091
1092 /**
1093 * Returns the continent of a given country
1094 *
1095 * @param string $country 2 letters iso code
1096 *
1097 * @return string Continent (3 letters code : afr, asi, eur, amn, ams, oce)
1098 */
1099 public static function getContinent($country)
1100 {
1101 /** @var RegionDataProvider $dataProvider */
1102 $dataProvider = StaticContainer::get('Piwik\Intl\Data\Provider\RegionDataProvider');
1103
1104 $countryList = $dataProvider->getCountryList();
1105
1106 if ($country == 'ti') {
1107 $country = 'cn';
1108 }
1109
1110 return isset($countryList[$country]) ? $countryList[$country] : 'unk';
1111 }
1112
1113 /*
1114 * Campaign
1115 */
1116
1117 /**
1118 * Returns the list of Campaign parameter names that will be read to classify
1119 * a visit as coming from a Campaign
1120 *
1121 * @return array array(
1122 * 0 => array( ... ) // campaign names parameters
1123 * 1 => array( ... ) // campaign keyword parameters
1124 * );
1125 */
1126 public static function getCampaignParameters()
1127 {
1128 $return = array(
1129 Config::getInstance()->Tracker['campaign_var_name'],
1130 Config::getInstance()->Tracker['campaign_keyword_var_name'],
1131 );
1132
1133 foreach ($return as &$list) {
1134 if (strpos($list, ',') !== false) {
1135 $list = explode(',', $list);
1136 } else {
1137 $list = array($list);
1138 }
1139 $list = array_map('trim', $list);
1140 }
1141
1142 return $return;
1143 }
1144
1145 /*
1146 * Referrer
1147 */
1148
1149 /**
1150 * Returns a string with a comma separated list of placeholders for use in an SQL query. Used mainly
1151 * to fill the `IN (...)` part of a query.
1152 *
1153 * @param array|string $fields The names of the mysql table fields to bind, e.g.
1154 * `array(fieldName1, fieldName2, fieldName3)`.
1155 *
1156 * _Note: The content of the array isn't important, just its length._
1157 * @return string The placeholder string, e.g. `"?, ?, ?"`.
1158 * @api
1159 */
1160 public static function getSqlStringFieldsArray($fields)
1161 {
1162 if (is_string($fields)) {
1163 $fields = array($fields);
1164 }
1165 $count = count($fields);
1166 if ($count == 0) {
1167 return "''";
1168 }
1169 return '?' . str_repeat(',?', $count - 1);
1170 }
1171
1172 /**
1173 * Force the separator for decimal point to be a dot. See https://github.com/piwik/piwik/issues/6435
1174 * If for instance a German locale is used it would be a comma otherwise.
1175 *
1176 * @param float|string $value
1177 * @return string
1178 */
1179 public static function forceDotAsSeparatorForDecimalPoint($value)
1180 {
1181 if (null === $value || false === $value) {
1182 return $value;
1183 }
1184
1185 return str_replace(',', '.', $value);
1186 }
1187
1188 /**
1189 * Sets outgoing header.
1190 *
1191 * @param string $header The header.
1192 * @param bool $replace Whether to replace existing or not.
1193 */
1194 public static function sendHeader($header, $replace = true)
1195 {
1196 // don't send header in CLI mode
1197 if (!Common::isPhpCliMode() and !headers_sent()) {
1198 header($header, $replace);
1199 }
1200 }
1201
1202 /**
1203 * Strips outgoing header.
1204 *
1205 * @param string $name The header name.
1206 */
1207 public static function stripHeader($name)
1208 {
1209 // don't strip header in CLI mode
1210 if (!Common::isPhpCliMode() and !headers_sent()) {
1211 header_remove($name);
1212 }
1213 }
1214
1215 /**
1216 * Sends the given response code if supported.
1217 *
1218 * @param int $code Eg 204
1219 *
1220 * @throws Exception
1221 */
1222 public static function sendResponseCode($code)
1223 {
1224 $messages = array(
1225 200 => 'Ok',
1226 204 => 'No Response',
1227 301 => 'Moved Permanently',
1228 302 => 'Found',
1229 304 => 'Not Modified',
1230 400 => 'Bad Request',
1231 401 => 'Unauthorized',
1232 403 => 'Forbidden',
1233 404 => 'Not Found',
1234 500 => 'Internal Server Error',
1235 503 => 'Service Unavailable',
1236 );
1237
1238 if (!array_key_exists($code, $messages)) {
1239 throw new Exception('Response code not supported: ' . $code);
1240 }
1241
1242 if (strpos(PHP_SAPI, '-fcgi') === false) {
1243 $key = 'HTTP/1.1';
1244
1245 if (array_key_exists('SERVER_PROTOCOL', $_SERVER)
1246 && strlen($_SERVER['SERVER_PROTOCOL']) < 15
1247 && strlen($_SERVER['SERVER_PROTOCOL']) > 1) {
1248 $key = $_SERVER['SERVER_PROTOCOL'];
1249 }
1250 } else {
1251 // FastCGI
1252 $key = 'Status:';
1253 }
1254
1255 $message = $messages[$code];
1256 Common::sendHeader($key . ' ' . $code . ' ' . $message);
1257 }
1258
1259 /**
1260 * Returns the ID of the current LocationProvider (see UserCountry plugin code) from
1261 * the Tracker cache.
1262 */
1263 public static function getCurrentLocationProviderId()
1264 {
1265 $cache = TrackerCache::getCacheGeneral();
1266 return empty($cache['currentLocationProviderId'])
1267 ? DefaultProvider::ID
1268 : $cache['currentLocationProviderId'];
1269 }
1270
1271 /**
1272 * Marks an orphaned object for garbage collection.
1273 *
1274 * For more information: {@link https://github.com/piwik/piwik/issues/374}
1275 * @param mixed $var The object to destroy.
1276 * @api
1277 */
1278 public static function destroy(&$var)
1279 {
1280 if (is_object($var) && method_exists($var, '__destruct')) {
1281 $var->__destruct();
1282 }
1283 unset($var);
1284 $var = null;
1285 }
1286
1287 /**
1288 * @deprecated Use the logger directly instead.
1289 */
1290 public static function printDebug($info = '')
1291 {
1292 if (is_object($info)) {
1293 $info = var_export($info, true);
1294 }
1295
1296 $logger = StaticContainer::get('Psr\Log\LoggerInterface');
1297 if (is_array($info) || is_object($info)) {
1298 $out = var_export($info, true);
1299 $logger->debug($out);
1300 } else {
1301 $logger->debug($info);
1302 }
1303 }
1304
1305 /**
1306 * Returns true if the request is an AJAX request.
1307 *
1308 * @return bool
1309 */
1310 public static function isXmlHttpRequest()
1311 {
1312 return isset($_SERVER['HTTP_X_REQUESTED_WITH'])
1313 && (strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest');
1314 }
1315
1316 /**
1317 * @param $validLanguages
1318 * @return array
1319 */
1320 protected static function checkValidLanguagesIsSet($validLanguages)
1321 {
1322 /** @var LanguageDataProvider $dataProvider */
1323 $dataProvider = StaticContainer::get('Piwik\Intl\Data\Provider\LanguageDataProvider');
1324
1325 if (empty($validLanguages)) {
1326 $validLanguages = array_keys($dataProvider->getLanguageList());
1327 return $validLanguages;
1328 }
1329 return $validLanguages;
1330 }
1331 }
1332