1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241
|
<?php
/**
* Matomo - free/libre analytics platform
*
* @link https://matomo.org
* @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
namespace Piwik\Plugins\PrivacyManager;
use Piwik\Container\StaticContainer;
use Piwik\Exception\DI\DependencyException;
use Piwik\Exception\DI\NotFoundException;
use Piwik\Option;
use Piwik\Plugins\FeatureFlags\FeatureFlagManager;
use Piwik\Plugins\PrivacyManager\FeatureFlags\PrivacyCompliance;
use Piwik\Plugins\PrivacyManager\Settings\ReferrerAnonymisation as ReferrerAnonymizationSettings;
use Piwik\Tracker\Cache;
use Piwik\Plugins\PrivacyManager\Settings\IpAddressMaskLength as IpAddressMaskLengthSetting;
use Piwik\Plugins\PrivacyManager\Settings\IPAnonymisation as IPAnonymisationSetting;
use Piwik\Plugins\Ecommerce\Settings\OrderIdAnonymization as OrderIdAnonymizationSetting;
/**
* @property bool $doNotTrackEnabled Enable / Disable Do Not Track {@see DoNotTrackHeaderChecker}
* @property bool $ipAnonymizerEnabled Enable / Disable IP Anonymizer {@see IPAnonymizer}
* @property bool $useAnonymizedIpForVisitEnrichment Set this setting to 0 to let plugins use the full
* non-anonymized IP address when enriching visitor information.
* When set to 1, by default, Geo Location via geoip and Provider reverse name lookups
* will use the anonymized IP address when anonymization is enabled.
* @property int $ipAddressMaskLength Anonymize a visitor's IP address after testing for "Ip exclude"
* This value is the level of anonymization Piwik will use; if the IP
* anonymization is deactivated, this value is ignored. For IPv4/IPv6 addresses,
* valid values are the number of octets in IP address to mask (from 0 to 4).
* For IPv6 addresses 0..4 means that 0, 64, 80, 104 or all bits are masked.
* @property bool $forceCookielessTracking If enabled, Matomo will try to force tracking without cookies
* @property bool $anonymizeUserId If enabled, it will pseudo anonymize the User ID
* @property bool $anonymizeOrderId If enabled, it will anonymize the Order ID
* @property string $anonymizeReferrer Whether the referrer should be anonymized and how it much it should be anonymized
* @property bool $randomizeConfigId If enabled, Matomo will generate a new random Config ID (fingerprint) for each tracking request
*/
class Config
{
/**
* If provided, tells the config to only apply to a specific site ID.
*
* @var int|null
*/
private $idSite;
public function __construct(?int $idSite = null)
{
$this->setIdSite($idSite);
}
private $properties = [
'useAnonymizedIpForVisitEnrichment' => ['type' => 'boolean', 'default' => false],
'ipAddressMaskLength' => ['type' => 'integer', 'default' => 2],
'doNotTrackEnabled' => ['type' => 'boolean', 'default' => false],
'ipAnonymizerEnabled' => ['type' => 'boolean', 'default' => true],
'forceCookielessTracking' => ['type' => 'boolean', 'default' => false],
'anonymizeUserId' => ['type' => 'boolean', 'default' => false],
'anonymizeOrderId' => ['type' => 'boolean', 'default' => false],
'anonymizeReferrer' => ['type' => 'string', 'default' => ''],
'randomizeConfigId' => ['type' => 'boolean', 'default' => false],
];
public function __set($name, $value)
{
if (!array_key_exists($name, $this->properties)) {
throw new \Exception(sprintf('Property %s does not exist', $name));
}
$this->set($name, $value, $this->properties[$name]);
}
public function __get($name)
{
if (!array_key_exists($name, $this->properties)) {
throw new \Exception(sprintf('Property %s does not exist', $name));
}
return $this->getFromTrackerCache($name, $this->properties[$name]);
}
public function prefix(string $optionName, bool $addIdSite = true): string
{
// if requested, adding the site ID in the middle to have all the site-specific settings together
return 'PrivacyManager.' . (($addIdSite && $this->idSite) ? "idSite($this->idSite)." : '') . $optionName;
}
private function getFromSpecificTrackerCache(string $name, array $cache, array $config, bool $useFallback = true)
{
if (array_key_exists($name, $cache)) {
$value = $cache[$name];
settype($value, $config['type']);
return $value;
}
return $useFallback ? $config['default'] : null;
}
private function getFromTrackerCache(string $name, array $config)
{
$generalCache = Cache::getCacheGeneral();
$name = $this->prefix($name, false); // when getting from tracker cache, we always want the generic name
if ($this->idSite) {
$cache = Cache::getCacheWebsiteAttributes($this->idSite);
} else {
$cache = $generalCache; // so that we always have some cache to check below
}
// check specific cache first, if no value found there return from general cache or use default
$valueSite = $this->getFromSpecificTrackerCache($name, $cache, $config, $useFallback = false);
$valueGeneralWithFallback = $this->getFromSpecificTrackerCache($name, $generalCache, $config);
return $valueSite ?? $valueGeneralWithFallback;
}
/**
* If PrivacyCompliance is enabled and specific settings are requested, return their value, otherwise
* return a provided option value
*
* @param false|string $optionValue
* @return int|mixed|null
* @throws DependencyException
* @throws NotFoundException
*/
private function getOptionValueWithPrivacyComplianceOverride(string $name, ?int $idSite, $optionValue)
{
$featureFlagManager = StaticContainer::get(FeatureFlagManager::class);
if ($featureFlagManager->isFeatureActive(PrivacyCompliance::class)) {
if ($name === 'ipAddressMaskLength') {
return IpAddressMaskLengthSetting::getInstance($idSite)->getValue();
} elseif ($name === 'ipAnonymizerEnabled') {
return IPAnonymisationSetting::getInstance($idSite)->getValue();
} elseif ($name === 'anonymizeReferrer') {
return ReferrerAnonymizationSettings::getInstance($idSite)->getValue();
} elseif ($name === 'anonymizeOrderId') {
return OrderIdAnonymizationSetting::getInstance($idSite)->getValue();
}
}
return $optionValue;
}
/**
* Get a value from the option table, with a potential compliance policy override and a fallback value
* if there's no option stored for the given name yet
*
* @return mixed
* @throws DependencyException
* @throws NotFoundException
*/
public function getFromOption(string $name, bool $allowPolicyComplianceOverride = true)
{
$optionValue = Option::get($this->prefix($name));
$value = $allowPolicyComplianceOverride
? $this->getOptionValueWithPrivacyComplianceOverride($name, $this->idSite, $optionValue)
: $optionValue;
// fallback to global settings if we don't have specific site settings saved
if (false === $value && !$this->hasSiteSpecificSettings($name)) {
$optionValue = Option::get($this->prefix($name, false));
$value = $allowPolicyComplianceOverride
? $this->getOptionValueWithPrivacyComplianceOverride($name, null, $optionValue)
: $optionValue;
}
$config = $this->getPropertyConfig($name);
if (isset($value) && false !== $value) {
settype($value, $config['type']);
} else {
$value = $config['default'];
}
return $value;
}
private function set($name, $value, $config): void
{
if ('boolean' == $config['type']) {
$value = $value ? '1' : '0';
} else {
settype($value, $config['type']);
}
Option::set($this->prefix($name), $value);
Cache::deleteTrackerCache();
}
public function setIdSite(?int $idSite): void
{
if (null === $idSite || $idSite > 0) {
$this->idSite = $idSite;
}
}
public function setTrackerCache(array &$cacheContent): array
{
foreach ($this->getConfigPropertyNames() as $name) {
// when setting tracker cache, we always want generic name
$cacheContent[$this->prefix($name, false)] = $this->getFromOption($name);
}
return $cacheContent;
}
public function getConfigPropertyNames(): array
{
return array_keys($this->properties);
}
private function getPropertyConfig(string $name): array
{
return $this->properties[$name] ?? [];
}
public function removeForSite(): void
{
if ($this->idSite) {
Option::deleteLike($this->prefix('%'));
}
}
private function hasSiteSpecificSettings(string $name = '%'): bool
{
return $this->idSite && count(Option::getLike($this->prefix($name))) > 0;
}
public function useSiteSpecificSettings(): bool
{
if (!$this->idSite) {
return false;
}
return $this->hasSiteSpecificSettings();
}
}
|