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