PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 4.1.2
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v4.1.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 / Cookie.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
Cookie.php
469 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 Piwik\Container\StaticContainer;
12
13 /**
14 * Simple class to handle the cookies:
15 * - read a cookie values
16 * - edit an existing cookie and save it
17 * - create a new cookie, set values, expiration date, etc. and save it
18 *
19 */
20 class Cookie
21 {
22 /**
23 * Don't create a cookie bigger than 1k
24 */
25 const MAX_COOKIE_SIZE = 1024;
26
27 /**
28 * The name of the cookie
29 * @var string
30 */
31 protected $name = null;
32
33 /**
34 * The expire time for the cookie (expressed in UNIX Timestamp)
35 * @var int
36 */
37 protected $expire = null;
38
39 /**
40 * Restrict cookie path
41 * @var string
42 */
43 protected $path = '';
44
45 /**
46 * Restrict cookie to a domain (or subdomains)
47 * @var string
48 */
49 protected $domain = '';
50
51 /**
52 * If true, cookie should only be transmitted over secure HTTPS
53 * @var bool
54 */
55 protected $secure = false;
56
57 /**
58 * If true, cookie will only be made available via the HTTP protocol.
59 * Note: not well supported by browsers.
60 * @var bool
61 */
62 protected $httponly = false;
63
64 /**
65 * The content of the cookie
66 * @var array
67 */
68 protected $value = array();
69
70 /**
71 * The character used to separate the tuple name=value in the cookie
72 */
73 const VALUE_SEPARATOR = ':';
74
75 /**
76 * Instantiate a new Cookie object and tries to load the cookie content if the cookie
77 * exists already.
78 *
79 * @param string $cookieName cookie Name
80 * @param int $expire The timestamp after which the cookie will expire, eg time() + 86400;
81 * use 0 (int zero) to expire cookie at end of browser session
82 * @param string $path The path on the server in which the cookie will be available on.
83 * @param bool|string $keyStore Will be used to store several bits of data (eg. one array per website)
84 */
85 public function __construct($cookieName, $expire = null, $path = null, $keyStore = false)
86 {
87 $this->name = $cookieName;
88 $this->path = $path;
89 $this->expire = $expire;
90 if (is_null($expire)
91 || !is_numeric($expire)
92 || $expire < 0
93 ) {
94 $this->expire = $this->getDefaultExpire();
95 }
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 $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 $header = 'Set-Cookie: ' . rawurlencode($Name) . '=' . rawurlencode($Value)
155 . (empty($Expires) ? '' : '; expires=' . gmdate('D, d-M-Y H:i:s', $Expires) . ' GMT')
156 . (empty($Path) ? '' : '; path=' . $Path)
157 . (empty($Domain) ? '' : '; domain=' . rawurlencode($Domain))
158 . (!$Secure ? '' : '; secure')
159 . (!$HTTPOnly ? '' : '; HttpOnly')
160 . (!$sameSite ? '' : '; SameSite=' . rawurlencode($sameSite));
161
162 Common::sendHeader($header, false);
163 }
164
165 /**
166 * We set the privacy policy header
167 */
168 protected function setP3PHeader()
169 {
170 Common::sendHeader("P3P: CP='OTI DSP COR NID STP UNI OTPa OUR'");
171 }
172
173 /**
174 * Delete the cookie
175 */
176 public function delete()
177 {
178 $this->setP3PHeader();
179 $this->setCookie($this->name, 'deleted', time() - 31536001, $this->path, $this->domain);
180 $this->clear();
181 }
182
183 /**
184 * Saves the cookie (set the Cookie header).
185 * You have to call this method before sending any text to the browser or you would get the
186 * "Header already sent" error.
187 * @param string $sameSite Value for SameSite cookie property
188 */
189 public function save($sameSite = null)
190 {
191 if ($sameSite) {
192 $sameSite = self::getSameSiteValueForBrowser($sameSite);
193 }
194 $cookieString = $this->generateContentString();
195 if (strlen($cookieString) > self::MAX_COOKIE_SIZE) {
196 // If the cookie was going to be too large, instead, delete existing cookie and start afresh
197 $this->delete();
198 return;
199 }
200
201 $this->setP3PHeader();
202 $this->setCookie($this->name, $cookieString, $this->expire, $this->path, $this->domain, $this->secure, $this->httponly, $sameSite);
203 }
204
205 /**
206 * Extract signed content from string: content VALUE_SEPARATOR '_=' signature
207 * Only needed for BC.
208 *
209 * @param string $content
210 * @return string|bool Content or false if unsigned
211 */
212 private function extractSignedContent($content)
213 {
214 $signature = substr($content, -40);
215
216 if (substr($content, -43, 3) === self::VALUE_SEPARATOR . '_=' &&
217 ($signature === sha1(substr($content, 0, -40) . SettingsPiwik::getSalt()))
218 ) {
219 // strip trailing: VALUE_SEPARATOR '_=' signature"
220 return substr($content, 0, -43);
221 }
222
223 return false;
224 }
225
226 /**
227 * Load the cookie content into a php array.
228 * Parses the cookie string to extract the different variables.
229 * Unserialize the array when necessary.
230 * Decode the non numeric values that were base64 encoded.
231 */
232 protected function loadContentFromCookie()
233 {
234 // we keep trying to read signed content for BC ... if it detects a correctly signed cookie then we read
235 // this value
236 $cookieStr = $this->extractSignedContent($_COOKIE[$this->name]);
237 $isSigned = !empty($cookieStr);
238
239 if ($cookieStr === false
240 && !empty($_COOKIE[$this->name])
241 && strpos($_COOKIE[$this->name], '=') !== false) {
242 // cookie was set since Matomo 4
243 $cookieStr = $_COOKIE[$this->name];
244 }
245
246 if ($cookieStr === false) {
247 return;
248 }
249
250 $values = explode(self::VALUE_SEPARATOR, $cookieStr);
251 foreach ($values as $nameValue) {
252 $equalPos = strpos($nameValue, '=');
253 $varName = substr($nameValue, 0, $equalPos);
254 $varValue = substr($nameValue, $equalPos + 1);
255
256 if (!is_numeric($varValue)) {
257 $tmpValue = base64_decode($varValue);
258 if ($isSigned) {
259 // only unserialise content if it was signed meaning the cookie was generated pre Matomo 4
260 $varValue = safe_unserialize($tmpValue);
261 } else {
262 $varValue = $tmpValue;
263 }
264
265 // discard entire cookie
266 // note: this assumes we never serialize a boolean
267 // can only happen when it was signed pre Matomo 4
268 if ($varValue === false && $tmpValue !== 'b:0;') {
269 $this->value = array();
270 unset($_COOKIE[$this->name]);
271 break;
272 }
273 }
274
275 $this->value[$varName] = $varValue;
276 }
277 }
278
279 /**
280 * Returns the string to save in the cookie from the $this->value array of values.
281 * It goes through the array and generates the cookie content string.
282 *
283 * @return string Cookie content
284 */
285 public function generateContentString()
286 {
287 $cookieStrArr = [];
288
289 foreach ($this->value as $name => $value) {
290 if (!is_numeric($value) && !is_string($value)) {
291 throw new \Exception('Only strings and numbers can be used in cookies. Value is of type ' . gettype($value));
292 } elseif (!is_numeric($value)) {
293 $value = base64_encode($value);
294 }
295 $cookieStrArr[] = "$name=$value";
296 }
297
298 return implode(self::VALUE_SEPARATOR, $cookieStrArr);
299 }
300
301 /**
302 * Set cookie domain
303 *
304 * @param string $domain
305 */
306 public function setDomain($domain)
307 {
308 $this->domain = $domain;
309 }
310
311 /**
312 * Set secure flag
313 *
314 * @param bool $secure
315 */
316 public function setSecure($secure)
317 {
318 $this->secure = $secure;
319 }
320
321 /**
322 * Set HTTP only
323 *
324 * @param bool $httponly
325 */
326 public function setHttpOnly($httponly)
327 {
328 $this->httponly = $httponly;
329 }
330
331 /**
332 * Registers a new name => value association in the cookie.
333 *
334 * Registering new values is optimal if the value is a numeric value.
335 * Only numbers and strings can be saved in the cookie.
336 * A cookie has to stay small and its size shouldn't increase over time!
337 *
338 * @param string $name Name of the value to save; the name will be used to retrieve this value
339 * @param string|number $value Value to save. If null, entry will be deleted from cookie.
340 */
341 public function set($name, $value)
342 {
343 $name = self::escapeValue($name);
344
345 // Delete value if $value === null
346 if (is_null($value)) {
347 if ($this->keyStore === false) {
348 unset($this->value[$name]);
349 return;
350 }
351 unset($this->value[$this->keyStore][$name]);
352 return;
353 }
354
355 if ($this->keyStore === false) {
356 $this->value[$name] = $value;
357 return;
358 }
359
360 $this->value[$this->keyStore][$name] = $value;
361 }
362
363 /**
364 * Returns the value defined by $name from the cookie.
365 *
366 * @param string|integer Index name of the value to return
367 * @return mixed The value if found, false if the value is not found
368 */
369 public function get($name)
370 {
371 $name = self::escapeValue($name);
372 if (false === $this->keyStore) {
373 if (isset($this->value[$name])) {
374 return self::escapeValue($this->value[$name]);
375 }
376
377 return false;
378 }
379
380 if (isset($this->value[$this->keyStore][$name])) {
381 return self::escapeValue($this->value[$this->keyStore][$name]);
382 }
383
384 return false;
385 }
386
387 /**
388 * Removes all values from the cookie.
389 */
390 public function clear()
391 {
392 $this->value = [];
393 }
394
395 /**
396 * Returns an easy to read cookie dump
397 *
398 * @return string The cookie dump
399 */
400 public function __toString()
401 {
402 $str = 'COOKIE ' . $this->name . ', rows count: ' . count($this->value) . ', cookie size = ' . strlen($this->generateContentString()) . " bytes, ";
403 $str .= 'path: ' . $this->path. ', expire: ' . $this->expire . "\n";
404 $str .= var_export($this->value, $return = true);
405
406 return $str;
407 }
408
409 /**
410 * Escape values from the cookie before sending them back to the client
411 * (when using the get() method).
412 *
413 * @param string $value Value to be escaped
414 * @return mixed The value once cleaned.
415 */
416 protected static function escapeValue($value)
417 {
418 return Common::sanitizeInputValues($value);
419 }
420
421 /**
422 * Returns true if a cookie named '$name' is in the current HTTP request,
423 * false if otherwise.
424 *
425 * @param string $name the name of the cookie
426 * @return boolean
427 */
428 public static function isCookieInRequest($name)
429 {
430 return isset($_COOKIE[$name]);
431 }
432
433 /**
434 * Find the most suitable value for a cookie SameSite attribute, given environmental restrictions which
435 * may make the most "correct" value impractical:
436 * - On Chrome, the "None" value means that the cookie will not be present on third-party sites (e.g. the site
437 * that is being tracked) when the site is loaded over HTTP. This means that important cookies which should always
438 * be present (e.g. the opt-out cookie) won't be there at all. Using "Lax" means that at least they will be there
439 * for some requests which are deemed CSRF-safe, although other requests may have broken functionality.
440 * - On Safari, the "None" value is interpreted as "Strict". In order to set a cookie which will be available
441 * in all third-party contexts, we have to omit the SameSite attribute altogether.
442 * @param string $default The desired SameSite value that we should use if it won't cause any problems.
443 * @return string SameSite attribute value that should be set on the cookie. Empty string indicates that no value
444 * should be set.
445 */
446 private static function getSameSiteValueForBrowser($default)
447 {
448 $sameSite = ucfirst(strtolower($default));
449
450 if ($sameSite === 'None') {
451 if ((!ProxyHttp::isHttps())) {
452 $sameSite = 'Lax'; // None can be only used when secure flag will be set
453 } else {
454 $userAgent = Http::getUserAgent();
455 $ddFactory = StaticContainer::get(\Piwik\DeviceDetector\DeviceDetectorFactory::class);
456 $deviceDetector = $ddFactory->makeInstance($userAgent);
457 $deviceDetector->parse();
458
459 $browserFamily = \DeviceDetector\Parser\Client\Browser::getBrowserFamily($deviceDetector->getClient('short_name'));
460 if ($browserFamily === 'Safari') {
461 $sameSite = '';
462 }
463 }
464 }
465
466 return $sameSite;
467 }
468 }
469