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\Template;
9
10use JShrink\Minifier;
11use Piwik\Common;
12use Piwik\Container\StaticContainer;
13use Piwik\Development;
14use Piwik\Piwik;
15use Piwik\Plugins\CorePluginsAdmin\SettingsMetadata;
16use Piwik\Plugins\TagManager\Context\WebContext;
17use Piwik\Plugins\TagManager\Settings\Storage\Backend\TransientBackend;
18use Piwik\Settings\Setting;
19use Piwik\Settings\Storage\Storage;
20
21/**
22 * @api
23 */
24abstract class BaseTemplate
25{
26    private $pluginName = null;
27
28    protected $templateType = '';
29
30    const FIELD_TEMPLATE_VARIABLE = 'plugins/TagManager/angularjs/form-field/field-variable-template.html';
31    const FIELD_TEMPLATE_TEXTAREA_VARIABLE = 'plugins/TagManager/angularjs/form-field/field-textarea-variable-template.html';
32    const FIELD_TEMPLATE_VARIABLE_TYPE = 'plugins/TagManager/angularjs/form-field/field-variabletype-template.html';
33    public static $RESERVED_SETTING_NAMES = [
34        'container', 'tag', 'variable', 'trigger', 'length', 'window', 'document', 'get', 'fire', 'setUp', 'set', 'reset', 'type', 'part',
35        'default_value', 'lookup_table', 'conditions', 'condition', 'fire_limit', 'fire_delay', 'priority', 'parameters',
36        'start_date', 'end_date', 'type', 'name', 'status'
37    ];
38
39    private $settingsStorage;
40
41    /**
42     * Get the ID of this template.
43     * The ID is by default automatically generated from the class name, but can be customized by returning a string.
44     *
45     * @return string
46     */
47    public function getId()
48    {
49        return $this->makeIdFromClassname($this->templateType);
50    }
51
52    /**
53     * Get the list of parameters that can be configured for this template.
54     * @return Setting[]
55     */
56    abstract public function getParameters();
57
58    /**
59     * Get the category this template belongs to.
60     * @return string
61     */
62    abstract public function getCategory();
63
64    /**
65     * Defines in which contexts this tag should be available, for example "web".
66     * @return string[]
67     */
68    abstract public function getSupportedContexts();
69
70    private function getTranslationKey($part)
71    {
72        if (empty($this->templateType)) {
73            return '';
74        }
75
76        if (!isset($this->pluginName)) {
77            $classname = get_class($this);
78            $parts = explode('\\', $classname);
79
80            if (count($parts) >= 4 && $parts[1] === 'Plugins') {
81                $this->pluginName = $parts[2];
82            }
83        }
84        if (isset($this->pluginName)) {
85            return $this->pluginName . '_' . $this->getId() . $this->templateType . $part;
86        }
87
88        return '';
89    }
90
91    /**
92     * Get the translated name of this template.
93     * @return string
94     */
95    public function getName()
96    {
97        $key = $this->getTranslationKey('Name');
98        if ($key) {
99            $translated = Piwik::translate($key);
100            if ($translated === $key) {
101                return $this->getId();
102            }
103            return $translated;
104        }
105        return $this->getId();
106    }
107
108    /**
109     * Get the translated description of this template.
110     * @return string
111     */
112    public function getDescription()
113    {
114        $key = $this->getTranslationKey('Description');
115        if ($key) {
116            $translated = Piwik::translate($key);
117            if ($translated === $key) {
118                return '';
119            }
120            return $translated;
121        }
122    }
123
124    /**
125     * Get the translated help text for this template.
126     * @return string
127     */
128    public function getHelp()
129    {
130        $key = $this->getTranslationKey('Help');
131        if ($key) {
132            $translated = Piwik::translate($key);
133            if ($translated === $key) {
134                return '';
135            }
136            return $translated;
137        }
138    }
139
140    /**
141     * Get the order for this template. The lower the order is, the higher in the list the template will be shown.
142     * @return int
143     */
144    public function getOrder()
145    {
146        return 9999;
147    }
148
149    /**
150     * Get the image icon url. We could also use data:uris to return the amount of requests to load a page like this:
151     * return 'data:image/svg+xml;base64,' . base64_encode('<svg...</svg>');
152     * However, we prefer the files since we can better define them in the legal notice.
153     *
154     * @return string
155     */
156    public function getIcon()
157    {
158        return 'plugins/TagManager/images/defaultIcon.svg';
159    }
160
161    /**
162     * Creates a new setting / parameter.
163     *
164     * Settings will be displayed in the UI depending on the order of `makeSetting` calls. This means you can define
165     * the order of the displayed settings by calling makeSetting first for more important settings.
166     *
167     * @param string $name         The name of the setting that shall be created
168     * @param mixed  $defaultValue The default value for this setting. Note the value will not be converted to the
169     *                             specified type.
170     * @param string $type         The PHP internal type the value of this setting should have.
171     *                             Use one of FieldConfig::TYPE_* constancts
172     * @param \Closure $fieldConfigCallback   A callback method to configure the field that shall be displayed in the
173     *                             UI to define the value for this setting
174     * @return Setting   Returns an instance of the created measurable setting.
175     */
176    protected function makeSetting($name, $defaultValue, $type, $fieldConfigCallback)
177    {
178        if (in_array(strtolower($name), self::$RESERVED_SETTING_NAMES, true)) {
179            throw new \Exception(sprintf('The setting name "%s" is reserved and cannot be used', $name));
180        }
181
182        // we need to make sure to create new instance of storage all the time to prevent "leaking" using values across
183        // multiple tags, or triggers, or variables
184        $this->settingsStorage = new Storage(new TransientBackend($this->getId()));
185
186        $setting = new Setting($name, $defaultValue, $type, 'TagManager');
187        $setting->setStorage($this->settingsStorage);
188        $setting->setConfigureCallback($fieldConfigCallback);
189        $setting->setIsWritableByCurrentUser(true); // we validate access on API level.
190
191        return $setting;
192    }
193
194    /**
195     * @ignore
196     */
197    public function loadTemplate($context, $entity)
198    {
199        switch ($context) {
200            case WebContext::ID:
201                $className = get_class($this);
202                $autoloader_reflector = new \ReflectionClass($className);
203                $fileName = $autoloader_reflector->getFileName();
204
205                $lenPhpExtension = 3;
206                $base = substr($fileName, 0 , -1 * $lenPhpExtension);
207                $file = $base . 'web.js';
208                $minFile = $base . 'web.min.js';
209
210                if (!StaticContainer::get('TagManagerJSMinificationEnabled')) {
211                    return $this->loadTemplateFile($file); // avoid minification in test mode
212                } elseif (Development::isEnabled() && $this->hasTemplateFile($file)) {
213                    // during dev mode we prefer the non-minified version for debugging purposes, but we still use
214                    // the internal minifier to make sure we debug the same as a user would receive
215                    $template = $this->loadTemplateFile($file);
216                    $minified = Minifier::minify($template);
217                    return $minified;
218                } elseif ($this->hasTemplateFile($minFile)) {
219                    // recommended when there is a lot of content in the template. For example if the tag contains the
220                    // content of a Matomo JS tracker then it will be useful or also in general.
221                    return $this->loadTemplateFile($minFile);
222                } elseif ($this->hasTemplateFile($file)) {
223                    // it does not minify so well as it doesn't rename variables, however, it does make it a bit smaller
224                    // gzip should help with filesize re variables like `tagmanager` etc.
225                    // the big advantage is really that JS Min files cannot be out of date or forgotton to be updated
226                    $template = $this->loadTemplateFile($file);
227                    $minified = Minifier::minify($template);
228                    return $minified;
229                }
230        }
231    }
232
233    /**
234     * @ignore
235     */
236    protected function makeIdFromClassname($rightTrimWord)
237    {
238        $className = get_class($this);
239        $parts = explode('\\', $className);
240        $id = end($parts);
241
242        if ($rightTrimWord && Common::stringEndsWith($id, $rightTrimWord)) {
243            $id = substr($id, 0, -strlen($rightTrimWord));
244        }
245
246        return $id;
247    }
248
249    /**
250     * @ignore tests only
251     * @param $file
252     * @return bool
253     */
254    protected function hasTemplateFile($file)
255    {
256        return is_readable($file);
257    }
258
259    /**
260     * @ignore tests only
261     * @param $file
262     * @return string|null
263     */
264    protected function loadTemplateFile($file)
265    {
266        if ($this->hasTemplateFile($file)) {
267            return trim(file_get_contents($file));
268        }
269    }
270
271    /**
272     * Lets you hide the advanced settings tab in the UI.
273     * @return bool
274     */
275    public function hasAdvancedSettings()
276    {
277        return true;
278    }
279
280    /**
281     * If your template allows a user to add js/html code to the site for example, you should be overwriting this
282     * method and return `true`.
283     * @return bool
284     */
285    public function isCustomTemplate()
286    {
287        return false;
288    }
289
290    /**
291     * @ignore
292     * @return array
293     */
294    public function toArray()
295    {
296        $settingsMetadata = new SettingsMetadata();
297        $params = array();
298        $tagParameters = $this->getParameters();
299
300        if (!empty($tagParameters)) {
301            foreach ($tagParameters as $parameter) {
302                $param = $settingsMetadata->formatSetting($parameter);
303                if (!empty($param)) {
304                    // we need to manually set the value as otherwise a value from an actual tag, trigger, variable,...
305                    // might be set because the instance of the template is shared and therefore the storage...
306                    $param['value'] = $parameter->getDefaultValue();
307                    $params[] = $param;
308                }
309            }
310        }
311
312        return array(
313            'id' => $this->getId(),
314            'name' => $this->getName(),
315            'description' => $this->getDescription(),
316            'category' => Piwik::translate($this->getCategory()),
317            'icon' => $this->getIcon(),
318            'help' => $this->getHelp(),
319            'order' => $this->getOrder(),
320            'contexts' => $this->getSupportedContexts(),
321            'hasAdvancedSettings' => $this->hasAdvancedSettings(),
322            'isCustomTemplate' => $this->isCustomTemplate(),
323            'parameters' => $params,
324        );
325    }
326
327
328}
329