PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 4.13.3
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v4.13.3
5.12.0 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 / Cookie.php
matomo / app / core Last commit date
API 3 years ago Access 3 years ago Application 4 years ago Archive 3 years ago ArchiveProcessor 3 years ago Archiver 5 years ago AssetManager 3 years ago Auth 3 years ago Category 5 years ago Changes 4 years ago CliMulti 4 years ago Columns 3 years ago Concurrency 3 years ago Config 4 years ago Container 4 years ago CronArchive 3 years ago DataAccess 3 years ago DataFiles 5 years ago DataTable 3 years ago Db 3 years ago DeviceDetector 3 years ago Email 5 years ago Exception 3 years ago Http 4 years ago Intl 4 years ago Mail 3 years ago Measurable 5 years ago Menu 3 years ago Metrics 4 years ago Notification 4 years ago Period 4 years ago Plugin 3 years ago ProfessionalServices 4 years ago Report 5 years ago ReportRenderer 3 years ago Scheduler 4 years ago Segment 3 years ago Session 4 years ago Settings 3 years ago Tracker 3 years ago Translation 4 years ago UpdateCheck 5 years ago Updater 3 years ago Updates 3 years ago Validators 4 years ago View 4 years ago ViewDataTable 3 years ago Visualization 4 years ago Widget 5 years ago .htaccess 6 years ago Access.php 4 years ago Archive.php 4 years ago ArchiveProcessor.php 4 years ago AssetManager.php 4 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 4 years ago Common.php 3 years ago Config.php 3 years ago Console.php 3 years ago Context.php 5 years ago Cookie.php 3 years ago CronArchive.php 3 years ago DataArray.php 4 years ago DataTable.php 3 years ago Date.php 4 years ago Db.php 4 years ago DbHelper.php 3 years ago Development.php 5 years ago ErrorHandler.php 5 years ago EventDispatcher.php 3 years ago ExceptionHandler.php 3 years ago FileIntegrity.php 5 years ago Filechecks.php 4 years ago Filesystem.php 3 years ago FrontController.php 3 years ago Http.php 3 years ago IP.php 4 years ago Log.php 4 years ago LogDeleter.php 4 years ago Mail.php 4 years ago Metrics.php 3 years ago NoAccessException.php 5 years ago Nonce.php 3 years ago Notification.php 5 years ago NumberFormatter.php 4 years ago Option.php 4 years ago Period.php 5 years ago Piwik.php 3 years ago Plugin.php 4 years ago Profiler.php 4 years ago ProxyHeaders.php 5 years ago ProxyHttp.php 3 years ago QuickForm2.php 5 years ago RankingQuery.php 3 years ago ReportRenderer.php 4 years ago Segment.php 3 years ago Sequence.php 5 years ago Session.php 3 years ago SettingsPiwik.php 3 years ago SettingsServer.php 5 years ago Singleton.php 5 years ago Site.php 3 years ago SiteContentDetector.php 3 years ago SupportedBrowser.php 3 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 4 years ago UpdaterErrorException.php 5 years ago Updates.php 5 years ago Url.php 3 years ago UrlHelper.php 3 years ago Version.php 3 years ago View.php 3 years ago bootstrap.php 3 years ago dispatch.php 5 years ago testMinimumPhpVersion.php 3 years ago
Cookie.php
496 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 DateTime;
12 use Piwik\Container\StaticContainer;
13
14 /**
15 * Simple class to handle the cookies:
16 * - read a cookie values
17 * - edit an existing cookie and save it
18 * - create a new cookie, set values, expiration date, etc. and save it
19 *
20 */
21 class Cookie
22 {
23 /**
24 * Don't create a cookie bigger than 1k
25 */
26 const MAX_COOKIE_SIZE = 1024;
27
28 /**
29 * The name of the cookie
30 * @var string
31 */
32 protected $name = null;
33
34 /**
35 * The expire time for the cookie (expressed in UNIX Timestamp)
36 * @var int
37 */
38 protected $expire = null;
39
40 /**
41 * Restrict cookie path
42 * @var string
43 */
44 protected $path = '';
45
46 /**
47 * @var string
48 */
49 protected $keyStore = false;
50
51 /**
52 * Restrict cookie to a domain (or subdomains)
53 * @var string
54 */
55 protected $domain = '';
56
57 /**
58 * If true, cookie should only be transmitted over secure HTTPS
59 * @var bool
60 */
61 protected $secure = false;
62
63 /**
64 * If true, cookie will only be made available via the HTTP protocol.
65 * Note: not well supported by browsers.
66 * @var bool
67 */
68 protected $httponly = false;
69
70 /**
71 * The content of the cookie
72 * @var array
73 */
74 protected $value = array();
75
76 /**
77 * The character used to separate the tuple name=value in the cookie
78 */
79 const VALUE_SEPARATOR = ':';
80
81 /**
82 * Instantiate a new Cookie object and tries to load the cookie content if the cookie
83 * exists already.
84 *
85 * @param string $cookieName cookie Name
86 * @param int|string $expire The timestamp after which the cookie will expire, eg time() + 86400;
87 * use 0 (int zero) to expire cookie at end of browser session
88 * @param string $path The path on the server in which the cookie will be available on.
89 * @param bool|string $keyStore Will be used to store several bits of data (eg. one array per website)
90 */
91 public function __construct($cookieName, $expire = null, $path = null, $keyStore = false)
92 {
93 $this->name = $cookieName;
94 $this->path = $path;
95 $this->expire = $expire;
96
97 $this->keyStore = $keyStore;
98 if ($this->isCookieFound()) {
99 $this->loadContentFromCookie();
100 }
101 }
102
103 /**
104 * Returns true if the visitor already has the cookie.
105 *
106 * @return bool
107 */
108 public function isCookieFound()
109 {
110 return self::isCookieInRequest($this->name);
111 }
112
113 /**
114 * Returns the default expiry time, 2 years
115 *
116 * @return int Timestamp in 2 years
117 */
118 protected function getDefaultExpire()
119 {
120 return time() + 86400 * 365 * 2;
121 }
122
123 /**
124 * setcookie() replacement -- we don't use the built-in function because
125 * it is buggy for some PHP versions.
126 *
127 * @link http://php.net/setcookie
128 *
129 * @param string $Name Name of cookie
130 * @param string $Value Value of cookie
131 * @param int|string $Expires Time the cookie expires
132 * @param string $Path
133 * @param string $Domain
134 * @param bool $Secure
135 * @param bool $HTTPOnly
136 * @param string $sameSite
137 */
138 protected function setCookie($Name, $Value, $Expires, $Path = '', $Domain = '', $Secure = false, $HTTPOnly = false, $sameSite = false)
139 {
140 if (!empty($Domain)) {
141 // Fix the domain to accept domains with and without 'www.'.
142 if (!strncasecmp($Domain, 'www.', 4)) {
143 $Domain = substr($Domain, 4);
144 }
145 $Domain = '.' . $Domain;
146
147 // Remove port information.
148 $Port = strpos($Domain, ':');
149 if ($Port !== false) {
150 $Domain = substr($Domain, 0, $Port);
151 }
152 }
153
154 // Format expire time only for non session cookies
155 if (0 !== $Expires) {
156 $Expires = $this->formatExpireTime($Expires);
157 }
158
159 $header = 'Set-Cookie: ' . rawurlencode($Name) . '=' . rawurlencode($Value)
160 . (empty($Expires) ? '' : '; expires=' . $Expires)
161 . (empty($Path) ? '' : '; path=' . $Path)
162 . (empty($Domain) ? '' : '; domain=' . rawurlencode($Domain))
163 . (!$Secure ? '' : '; secure')
164 . (!$HTTPOnly ? '' : '; HttpOnly')
165 . (!$sameSite ? '' : '; SameSite=' . rawurlencode($sameSite));
166
167 Common::sendHeader($header, false);
168 }
169
170 /**
171 * We set the privacy policy header
172 */
173 protected function setP3PHeader()
174 {
175 Common::sendHeader("P3P: CP='OTI DSP COR NID STP UNI OTPa OUR'");
176 }
177
178 /**
179 * Delete the cookie
180 */
181 public function delete()
182 {
183 $this->setP3PHeader();
184
185 $this->setCookie($this->name, 'deleted', time() - 31536001, $this->path, $this->domain);
186 $this->setCookie($this->name, 'deleted', time() - 31536001, $this->path, $this->domain, TRUE, FALSE, 'None');
187
188 $this->clear();
189 }
190
191 /**
192 * Saves the cookie (set the Cookie header).
193 * You have to call this method before sending any text to the browser or you would get the
194 * "Header already sent" error.
195 * @param string $sameSite Value for SameSite cookie property
196 */
197 public function save($sameSite = null)
198 {
199 if ($sameSite) {
200 $sameSite = self::getSameSiteValueForBrowser($sameSite);
201 }
202 $cookieString = $this->generateContentString();
203 if (strlen($cookieString) > self::MAX_COOKIE_SIZE) {
204 // If the cookie was going to be too large, instead, delete existing cookie and start afresh
205 $this->delete();
206 return;
207 }
208
209 $this->setP3PHeader();
210 $this->setCookie($this->name, $cookieString, $this->expire, $this->path, $this->domain, $this->secure, $this->httponly, $sameSite);
211 }
212
213 /**
214 * Extract signed content from string: content VALUE_SEPARATOR '_=' signature
215 * Only needed for BC.
216 *
217 * @param string $content
218 * @return string|bool Content or false if unsigned
219 */
220 private function extractSignedContent($content)
221 {
222 $signature = substr($content, -40);
223
224 if (substr($content, -43, 3) === self::VALUE_SEPARATOR . '_=' &&
225 ($signature === sha1(substr($content, 0, -40) . SettingsPiwik::getSalt()))
226 ) {
227 // strip trailing: VALUE_SEPARATOR '_=' signature"
228 return substr($content, 0, -43);
229 }
230
231 return false;
232 }
233
234 /**
235 * Load the cookie content into a php array.
236 * Parses the cookie string to extract the different variables.
237 * Unserialize the array when necessary.
238 * Decode the non numeric values that were base64 encoded.
239 */
240 protected function loadContentFromCookie()
241 {
242 // we keep trying to read signed content for BC ... if it detects a correctly signed cookie then we read
243 // this value
244 $cookieStr = $this->extractSignedContent($_COOKIE[$this->name]);
245 $isSigned = !empty($cookieStr);
246
247 if ($cookieStr === false
248 && !empty($_COOKIE[$this->name])
249 && strpos($_COOKIE[$this->name], '=') !== false) {
250 // cookie was set since Matomo 4
251 $cookieStr = $_COOKIE[$this->name];
252 }
253
254 if ($cookieStr === false) {
255 return;
256 }
257
258 $values = explode(self::VALUE_SEPARATOR, $cookieStr);
259 foreach ($values as $nameValue) {
260 $equalPos = strpos($nameValue, '=');
261 $varName = substr($nameValue, 0, $equalPos);
262 $varValue = substr($nameValue, $equalPos + 1);
263
264 if (!is_numeric($varValue)) {
265 $tmpValue = base64_decode($varValue);
266 if ($isSigned) {
267 // only unserialise content if it was signed meaning the cookie was generated pre Matomo 4
268 $varValue = safe_unserialize($tmpValue);
269 } else {
270 $varValue = $tmpValue;
271 }
272
273 // discard entire cookie
274 // note: this assumes we never serialize a boolean
275 // can only happen when it was signed pre Matomo 4
276 if ($varValue === false && $tmpValue !== 'b:0;') {
277 $this->value = array();
278 unset($_COOKIE[$this->name]);
279 break;
280 }
281 }
282
283 $this->value[$varName] = $varValue;
284 }
285 }
286
287 /**
288 * Returns the string to save in the cookie from the $this->value array of values.
289 * It goes through the array and generates the cookie content string.
290 *
291 * @return string Cookie content
292 */
293 public function generateContentString()
294 {
295 $cookieStrArr = [];
296
297 foreach ($this->value as $name => $value) {
298 if (!is_numeric($value) && !is_string($value)) {
299 throw new \Exception('Only strings and numbers can be used in cookies. Value is of type ' . gettype($value));
300 } elseif (!is_numeric($value)) {
301 $value = base64_encode($value);
302 }
303 $cookieStrArr[] = "$name=$value";
304 }
305
306 return implode(self::VALUE_SEPARATOR, $cookieStrArr);
307 }
308
309 /**
310 * Set cookie domain
311 *
312 * @param string $domain
313 */
314 public function setDomain($domain)
315 {
316 $this->domain = $domain;
317 }
318
319 /**
320 * Set secure flag
321 *
322 * @param bool $secure
323 */
324 public function setSecure($secure)
325 {
326 $this->secure = $secure;
327 }
328
329 /**
330 * Set HTTP only
331 *
332 * @param bool $httponly
333 */
334 public function setHttpOnly($httponly)
335 {
336 $this->httponly = $httponly;
337 }
338
339 /**
340 * Registers a new name => value association in the cookie.
341 *
342 * Registering new values is optimal if the value is a numeric value.
343 * Only numbers and strings can be saved in the cookie.
344 * A cookie has to stay small and its size shouldn't increase over time!
345 *
346 * @param string $name Name of the value to save; the name will be used to retrieve this value
347 * @param string|number $value Value to save. If null, entry will be deleted from cookie.
348 */
349 public function set($name, $value)
350 {
351 $name = self::escapeValue($name);
352
353 // Delete value if $value === null
354 if (is_null($value)) {
355 if ($this->keyStore === false) {
356 unset($this->value[$name]);
357 return;
358 }
359 unset($this->value[$this->keyStore][$name]);
360 return;
361 }
362
363 if ($this->keyStore === false) {
364 $this->value[$name] = $value;
365 return;
366 }
367
368 $this->value[$this->keyStore][$name] = $value;
369 }
370
371 /**
372 * Returns the value defined by $name from the cookie.
373 *
374 * @param string|integer Index name of the value to return
375 * @return mixed The value if found, false if the value is not found
376 */
377 public function get($name)
378 {
379 $name = self::escapeValue($name);
380 if (false === $this->keyStore) {
381 if (isset($this->value[$name])) {
382 return self::escapeValue($this->value[$name]);
383 }
384
385 return false;
386 }
387
388 if (isset($this->value[$this->keyStore][$name])) {
389 return self::escapeValue($this->value[$this->keyStore][$name]);
390 }
391
392 return false;
393 }
394
395 /**
396 * Removes all values from the cookie.
397 */
398 public function clear()
399 {
400 $this->value = [];
401 }
402
403 /**
404 * Returns an easy to read cookie dump
405 *
406 * @return string The cookie dump
407 */
408 public function __toString()
409 {
410 $str = 'COOKIE ' . $this->name . ', rows count: ' . count($this->value) . ', cookie size = ' . strlen($this->generateContentString()) . " bytes, ";
411 $str .= 'path: ' . $this->path. ', expire: ' . $this->expire . "\n";
412 $str .= var_export($this->value, $return = true);
413
414 return $str;
415 }
416
417 /**
418 * Escape values from the cookie before sending them back to the client
419 * (when using the get() method).
420 *
421 * @param string $value Value to be escaped
422 * @return mixed The value once cleaned.
423 */
424 protected static function escapeValue($value)
425 {
426 return Common::sanitizeInputValues($value);
427 }
428
429 /**
430 * Returns true if a cookie named '$name' is in the current HTTP request,
431 * false if otherwise.
432 *
433 * @param string $name the name of the cookie
434 * @return boolean
435 */
436 public static function isCookieInRequest($name)
437 {
438 return isset($_COOKIE[$name]);
439 }
440
441 /**
442 * Find the most suitable value for a cookie SameSite attribute, given environmental restrictions which
443 * may make the most "correct" value impractical:
444 * - On Chrome, the "None" value means that the cookie will not be present on third-party sites (e.g. the site
445 * that is being tracked) when the site is loaded over HTTP. This means that important cookies which should always
446 * be present (e.g. the opt-out cookie) won't be there at all. Using "Lax" means that at least they will be there
447 * for some requests which are deemed CSRF-safe, although other requests may have broken functionality.
448 * - On Safari, the "None" value is interpreted as "Strict". In order to set a cookie which will be available
449 * in all third-party contexts, we have to omit the SameSite attribute altogether.
450 * @param string $default The desired SameSite value that we should use if it won't cause any problems.
451 * @return string SameSite attribute value that should be set on the cookie. Empty string indicates that no value
452 * should be set.
453 */
454 private static function getSameSiteValueForBrowser($default)
455 {
456 $sameSite = ucfirst(strtolower($default));
457
458 if ($sameSite === 'None') {
459 if ((!ProxyHttp::isHttps())) {
460 $sameSite = 'Lax'; // None can be only used when secure flag will be set
461 } else {
462 $userAgent = Http::getUserAgent();
463 $ddFactory = StaticContainer::get(\Piwik\DeviceDetector\DeviceDetectorFactory::class);
464 $deviceDetector = $ddFactory->makeInstance($userAgent, Http::getClientHintsFromServerVariables());
465 $deviceDetector->parse();
466
467 $browserFamily = \DeviceDetector\Parser\Client\Browser::getBrowserFamily($deviceDetector->getClient('short_name'));
468 if ($browserFamily === 'Safari') {
469 $sameSite = '';
470 }
471 }
472 }
473
474 return $sameSite;
475 }
476
477 /**
478 * extend Cookie by timestamp or sting like + 30 years, + 10 months, default 2 years
479 * @param $time
480 * @return string
481 */
482 public function formatExpireTime($time = null)
483 {
484 $expireTime = new DateTime();
485 if (is_null($time) || (is_int($time) && $time < 0)) {
486 $expireTime->modify("+2 years");
487 } else if (is_int($time)) {
488 $expireTime->setTimestamp($time);
489 } else if (!$expireTime->modify($time)) {
490 $expireTime->modify("+2 years");
491 }
492 return $expireTime->format(DateTime::COOKIE);
493
494 }
495 }
496