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 */ 8namespace Piwik\Plugins\TagManager\Model; 9 10use Piwik\Piwik; 11use Piwik\Plugins\TagManager\API\TagReference; 12use Piwik\Plugins\TagManager\API\TriggerReference; 13use Piwik\Plugins\TagManager\API\VariableReference; 14use Piwik\Plugins\TagManager\Dao\VariablesDao; 15use Piwik\Plugins\TagManager\Input\IdSite; 16use Piwik\Plugins\TagManager\Validators\LookupTable; 17use Piwik\Plugins\TagManager\Input\Name; 18use Piwik\Plugins\TagManager\Template\BaseTemplate; 19use Piwik\Plugins\TagManager\Template\Variable\VariablesProvider; 20use Piwik\Settings\FieldConfig; 21use Piwik\Validators\BaseValidator; 22use Piwik\Validators\CharacterLength; 23 24class Variable extends BaseModel 25{ 26 /** 27 * @var VariablesDao 28 */ 29 private $dao; 30 31 /** 32 * @var VariablesProvider 33 */ 34 private $variablesProvider; 35 36 /** 37 * @var Tag 38 */ 39 private $tag; 40 41 /** 42 * @var Trigger 43 */ 44 private $trigger; 45 46 public function __construct(VariablesDao $variablesDao, VariablesProvider $variablesProvider, Tag $tag, Trigger $trigger) 47 { 48 $this->dao = $variablesDao; 49 $this->variablesProvider = $variablesProvider; 50 $this->tag = $tag; 51 $this->trigger = $trigger; 52 } 53 54 private function validateValues($idSite, $name, $defaultValue, $lookupTable) 55 { 56 $site = new IdSite($idSite); 57 $site->check(); 58 59 $theName = new Name($name); 60 $theName->check(); 61 62 if ($this->variablesProvider->getPreConfiguredVariable($name)) { 63 throw new \Exception(Piwik::translate('TagManager_ErrorVariableNameInUseByPreconfiguredVariable')); 64 } 65 66 if (isset($defaultValue) && !is_string($defaultValue) && !is_int($defaultValue) && !is_float($defaultValue) && !is_bool($defaultValue)) { 67 throw new \Exception(Piwik::translate('TagManager_ErrorVariableInvalidDefaultValue')); 68 } 69 70 BaseValidator::check(Piwik::translate('TagManager_DefaultValue'), $lookupTable, [new CharacterLength(0, 300)]); 71 BaseValidator::check(Piwik::translate('TagManager_LookupTable'), $lookupTable, [new LookupTable()]); 72 } 73 74 public function addContainerVariable($idSite, $idContainerVersion, $type, $name, $parameters, $defaultValue, $lookupTable) 75 { 76 $this->validateValues($idSite, $name, $defaultValue, $lookupTable); 77 $this->variablesProvider->checkIsValidVariable($type); 78 $createdDate = $this->getCurrentDateTime(); 79 $parameters = $this->formatParameters($type, $parameters); 80 return $this->dao->createVariable($idSite, $idContainerVersion, $type, $name, $parameters, $defaultValue, $lookupTable, $createdDate); 81 } 82 83 public function updateContainerVariable($idSite, $idContainerVersion, $idVariable, $name, $parameters, $defaultValue, $lookupTable) 84 { 85 $this->validateValues($idSite, $name, $defaultValue, $lookupTable); 86 $variable = $this->dao->getContainerVariable($idSite, $idContainerVersion, $idVariable); 87 if (!empty($variable)) { 88 $parameters = $this->formatParameters($variable['type'], $parameters); 89 $columns = array( 90 'name' => $name, 91 'default_value' => $defaultValue, 92 'lookup_table' => $lookupTable, 93 'parameters' => $parameters 94 ); 95 $this->updateVariableColumns($idSite, $idContainerVersion, $idVariable, $columns); 96 if ($variable['name'] !== $name) { 97 $this->updateContainerVariableReferences($idSite, $idContainerVersion, $variable['name'], $name); 98 } 99 } 100 } 101 102 private function formatParameters($variableType, $parameters) 103 { 104 $variableTemplate = $this->variablesProvider->getVariable($variableType); 105 if (empty($variableTemplate)) { 106 throw new \Exception('Invalid variable type'); 107 } 108 109 $params = $variableTemplate->getParameters(); 110 111 // we make sure to only save parameters that are defined in the tag template 112 $newParameters = []; 113 foreach ($params as $param) { 114 if (isset($parameters[$param->getName()])) { 115 $param->setValue($parameters[$param->getName()]); 116 $newParameters[$param->getName()] = $param->getValue(); 117 } else { 118 // we need to set a value to make sure that if for example a value is required, we trigger an error 119 $param->setValue($param->getDefaultValue()); 120 } 121 } 122 123 return $newParameters; 124 } 125 126 public function getContainerVariableReferences($idSite, $idContainerVersion, $idVariable) 127 { 128 $variable = $this->dao->getContainerVariable($idSite, $idContainerVersion, $idVariable); 129 130 if (empty($variable)) { 131 return []; 132 } 133 134 $varName = $variable['name']; 135 136 $references = []; 137 $tags = $this->tag->getContainerTags($idSite, $idContainerVersion); 138 $triggers = $this->trigger->getContainerTriggers($idSite, $idContainerVersion); 139 $variables = $this->getContainerVariables($idSite, $idContainerVersion); 140 141 foreach ($tags as $tag) { 142 foreach ($tag['typeMetadata']['parameters'] as $parameter) { 143 if ($this->isUsingParameterTheVariable($parameter, $varName)) { 144 $tagRef = new TagReference($tag['idtag'], $tag['name']); 145 $references[] = $tagRef->toArray(); 146 } 147 } 148 } 149 foreach ($triggers as $trigger) { 150 foreach ($trigger['typeMetadata']['parameters'] as $parameter) { 151 if ($this->isUsingParameterTheVariable($parameter, $varName)) { 152 $triggerRef = new TriggerReference($trigger['idtrigger'], $trigger['name']); 153 $references[] = $triggerRef->toArray(); 154 continue 2; // not needed to check for condition reference 155 } 156 } 157 158 foreach ($trigger['conditions'] as $condition) { 159 if ($condition['actual'] === $varName) { 160 $triggerRef = new TriggerReference($trigger['idtrigger'], $trigger['name']); 161 $references[] = $triggerRef->toArray(); 162 } 163 } 164 } 165 166 foreach ($variables as $var) { 167 foreach ($var['typeMetadata']['parameters'] as $parameter) { 168 if ($this->isUsingParameterTheVariable($parameter, $varName)) { 169 $variableRef = new VariableReference($var['idvariable'], $var['name']); 170 $references[] = $variableRef->toArray(); 171 } 172 } 173 } 174 175 return $references; 176 } 177 178 public static function hasFieldConfigVariableParameter($parameter) 179 { 180 if (!empty($parameter['templateFile']) && 181 ($parameter['templateFile'] === BaseTemplate::FIELD_TEMPLATE_VARIABLE 182 || $parameter['templateFile'] === BaseTemplate::FIELD_TEMPLATE_TEXTAREA_VARIABLE 183 || $parameter['templateFile'] === BaseTemplate::FIELD_TEMPLATE_VARIABLE_TYPE)) { 184 return true; 185 } 186 187 if (!empty($parameter['uiControl']) && $parameter['uiControl'] === FieldConfig::UI_CONTROL_MULTI_TUPLE) { 188 if (!empty($parameter['uiControlAttributes']['field1']) && self::hasFieldConfigVariableParameter($parameter['uiControlAttributes']['field1'])) { 189 return true; 190 } 191 if (!empty($parameter['uiControlAttributes']['field2']) && self::hasFieldConfigVariableParameter($parameter['uiControlAttributes']['field2'])) { 192 return true; 193 } 194 } 195 if (!empty($parameter['uiControlAttributes']['parseVariables'])) { 196 // workaround for some variables that don't use above templates but still need to be parsed 197 return true; 198 } 199 return false; 200 } 201 202 private function isUsingParameterTheVariable($parameter, $varName) 203 { 204 $varNameTemplate = $this->convertVariableNameToTemplateVar($varName); 205 206 if (!self::hasFieldConfigVariableParameter($parameter)) { 207 return false; 208 } 209 210 if (is_string($parameter['value'])) { 211 $value = $parameter['value']; 212 } elseif (is_array($parameter['value'])) { 213 // todo: in theory, when using a MultiTuple field where 2 fields can be configured, we would need to check 214 // whether both or only one of the fields are using variables and then iterate over all values to only 215 // check the values for that specific object key/field. Eg array(array('index' => '{{foo}}', 'bar' => '{{baz}}')) 216 // in theory it is possible that "index" key is a variable, but "bar" key is not and actually the user entered that text 217 218 // simplify when the value has an array instead of iterating over everything... 219 $value = json_encode($parameter['value']); 220 } else { 221 // we do not support objects or resources... and an integer or boolean etc cannot contain a variable 222 return false; 223 } 224 225 return strpos($value, $varNameTemplate) !== false; 226 } 227 228 private function updateContainerVariableReferences($idSite, $idContainerVersion, $oldVarName, $newVarName) 229 { 230 $tags = $this->tag->getContainerTags($idSite, $idContainerVersion); 231 $triggers = $this->trigger->getContainerTriggers($idSite, $idContainerVersion); 232 $variables = $this->getContainerVariables($idSite, $idContainerVersion); 233 234 foreach ($tags as $tag) { 235 $parameters = $this->replaceVariableNameInParameters($tag, $oldVarName, $newVarName); 236 if ($parameters) { 237 $this->tag->updateParameters($idSite, $idContainerVersion, $tag['idtag'], $parameters); 238 } 239 } 240 241 foreach ($triggers as $trigger) { 242 $parameters = $this->replaceVariableNameInParameters($trigger, $oldVarName, $newVarName); 243 244 $found = false; 245 foreach ($trigger['conditions'] as $index => $condition) { 246 if (isset($condition['actual']) && $condition['actual'] === $oldVarName) { 247 $found = true; 248 $condition['actual'] = $newVarName; 249 $trigger['conditions'][$index] = $condition; 250 } 251 } 252 if ($parameters || $found) { 253 $this->trigger->updateContainerTrigger($idSite, $idContainerVersion, $trigger['idtrigger'], $trigger['name'], $parameters, $trigger['conditions']); 254 } 255 } 256 257 foreach ($variables as $variable) { 258 $parameters = $this->replaceVariableNameInParameters($variable, $oldVarName, $newVarName); 259 if ($parameters) { 260 $this->updateVariableColumns($idSite, $idContainerVersion, $variable['idvariable'], array( 261 'parameters' => $parameters 262 )); 263 } 264 } 265 } 266 267 private function replaceVariableNameInParameters($entity, $oldVarName, $newVarName) 268 { 269 $oldVarNameTemplate = $this->convertVariableNameToTemplateVar($oldVarName); 270 $newVarNameTemplate = $this->convertVariableNameToTemplateVar($newVarName); 271 272 $found = false; 273 274 $parameters = $entity['parameters']; 275 foreach ($entity['typeMetadata']['parameters'] as $parameter) { 276 $paramName = $parameter['name']; 277 if ($parameter['templateFile'] === BaseTemplate::FIELD_TEMPLATE_VARIABLE 278 && isset($parameters[$paramName]) 279 && is_string($parameters[$paramName]) 280 && strpos($parameters[$paramName], $oldVarNameTemplate) !== false) { 281 $found = true; 282 $parameters[$paramName] = str_replace($oldVarNameTemplate, $newVarNameTemplate, $parameters[$paramName]); 283 } 284 } 285 if ($found) { 286 return $parameters; 287 } 288 } 289 290 public function convertVariableNameToTemplateVar($variableName) 291 { 292 return '{{' . $variableName . '}}'; 293 } 294 295 public function getContainerVariables($idSite, $idContainerVersion) 296 { 297 $variables = $this->dao->getContainerVariables($idSite, $idContainerVersion); 298 return $this->enrichVariables($variables); 299 } 300 301 public function deleteContainerVariable($idSite, $idContainerVersion, $idVariable) 302 { 303 if ($this->getContainerVariableReferences($idSite, $idContainerVersion, $idVariable)) { 304 throw new \Exception('This variable cannot be deleted as it is used in other places. To remove this variable, first remove all places where this variable is used'); 305 } 306 $this->dao->deleteContainerVariable($idSite, $idContainerVersion, $idVariable, $this->getCurrentDateTime()); 307 } 308 309 public function getContainerVariable($idSite, $idContainerVersion, $idVariable) 310 { 311 $variable = $this->dao->getContainerVariable($idSite, $idContainerVersion, $idVariable); 312 return $this->enrichVariable($variable); 313 } 314 315 public function findVariableByName($idSite, $idContainerVersion, $variableName) 316 { 317 $variable = $this->dao->findVariableByName($idSite, $idContainerVersion, $variableName); 318 return $this->enrichVariable($variable); 319 } 320 321 private function updateVariableColumns($idSite, $idContainerVersion, $idVariable, $columns) 322 { 323 if (!isset($columns['updated_date'])) { 324 $columns['updated_date'] = $this->getCurrentDateTime(); 325 } 326 $this->dao->updateVariableColumns($idSite, $idContainerVersion, $idVariable, $columns); 327 } 328 329 private function enrichVariables($variables) 330 { 331 if (empty($variables)) { 332 return array(); 333 } 334 335 foreach ($variables as $index => $variable) { 336 $variables[$index] = $this->enrichVariable($variable); 337 } 338 339 return $variables; 340 } 341 342 private function enrichVariable($variable) 343 { 344 if (empty($variable)) { 345 return $variable; 346 } 347 348 $variable['created_date_pretty'] = $this->formatDate($variable['created_date'], $variable['idsite']); 349 $variable['updated_date_pretty'] = $this->formatDate($variable['updated_date'], $variable['idsite']); 350 351 unset($variable['deleted_date']); 352 $variable['typeMetadata'] = null; 353 if (empty($variable['parameters'])) { 354 $variable['parameters'] = array(); 355 } 356 357 $variableTemplate = $this->variablesProvider->getVariable($variable['type']); 358 359 if (!empty($variableTemplate)) { 360 $variable['typeMetadata'] = $variableTemplate->toArray(); 361 foreach ($variable['typeMetadata']['parameters'] as &$parameter) { 362 $paramName = $parameter['name']; 363 if (isset($variable['parameters'][$paramName])) { 364 $parameter['value'] = $variable['parameters'][$paramName]; 365 } else { 366 $variable['parameters'][$paramName] = $parameter['defaultValue']; 367 } 368 } 369 } 370 371 return $variable; 372 } 373 374} 375 376