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
9namespace Piwik\Plugins\CustomJsTracker;
10
11use Piwik\Common;
12use Piwik\Container\StaticContainer;
13use Piwik\Plugins\CustomJsTracker\TrackingCode\PiwikJsManipulator;
14use Piwik\Plugins\CustomJsTracker\TrackingCode\PluginTrackerFiles;
15use Piwik\Piwik;
16
17/**
18 * Updates the Piwik JavaScript Tracker "piwik.js" in case plugins extend the tracker.
19 *
20 * Usage:
21 * StaticContainer::get('Piwik\Plugins\CustomJsTracker\TrackerUpdater')->update();
22 */
23class TrackerUpdater
24{
25    const DEVELOPMENT_PIWIK_JS = '/js/piwik.js';
26    const ORIGINAL_PIWIK_JS = '/js/piwik.min.js';
27    const TARGET_MATOMO_JS = '/matomo.js';
28
29    /**
30     * @var File
31     */
32    private $fromFile;
33
34    /**
35     * @var File
36     */
37    private $toFile;
38
39    private $trackerFiles = array();
40
41    /**
42     * @param string|null $fromFile If null then the minified JS tracker file in /js fill be used
43     * @param string|null $toFile If null then the minified JS tracker will be updated.
44     */
45    public function __construct($fromFile = null, $toFile = null)
46    {
47        if (!isset($fromFile)) {
48            $fromFile = PIWIK_DOCUMENT_ROOT . self::ORIGINAL_PIWIK_JS;
49        }
50
51        if (!isset($toFile)) {
52            $toFile = PIWIK_DOCUMENT_ROOT . self::TARGET_MATOMO_JS;
53        }
54
55        $this->setFromFile($fromFile);
56        $this->setToFile($toFile);
57        $this->trackerFiles = StaticContainer::get('Piwik\Plugins\CustomJsTracker\TrackingCode\PluginTrackerFiles');
58    }
59
60    public function setFromFile($fromFile)
61    {
62        if (is_string($fromFile)) {
63            $fromFile = new File($fromFile);
64        }
65        $this->fromFile = $fromFile;
66    }
67
68    public function getFromFile()
69    {
70        return $this->fromFile;
71    }
72
73    public function setToFile($toFile)
74    {
75        if (is_string($toFile)) {
76            $toFile = new File($toFile);
77        }
78        $this->toFile = $toFile;
79    }
80
81    public function getToFile()
82    {
83        return $this->toFile;
84    }
85
86    public function setTrackerFiles(PluginTrackerFiles $trackerFiles)
87    {
88        $this->trackerFiles = $trackerFiles;
89    }
90
91    /**
92     * Checks whether the Piwik JavaScript tracker file "piwik.js" is writable.
93     * @throws \Exception In case the piwik.js file is not writable.
94     *
95     * @api
96     */
97    public function checkWillSucceed()
98    {
99        $this->fromFile->checkReadable();
100        $this->toFile->checkWritable();
101    }
102
103    public function getCurrentTrackerFileContent()
104    {
105        return $this->toFile->getContent();
106    }
107
108    public function getUpdatedTrackerFileContent()
109    {
110        $trackingCode = new PiwikJsManipulator($this->fromFile->getContent(), $this->trackerFiles);
111        $newContent = $trackingCode->manipulateContent();
112
113        return $newContent;
114    }
115
116    /**
117     * Updates / re-generates the Piwik JavaScript tracker "piwik.js".
118     *
119     * It may not be possible to update the "piwik.js" tracker file if the file is not writable. It won't throw
120     * an exception in such a case and instead just to nothing. To check if the update would succeed, call
121     * {@link checkWillSucceed()}.
122     *
123     * @api
124     */
125    public function update()
126    {
127        if (!$this->toFile->hasWriteAccess() || !$this->fromFile->hasReadAccess()) {
128            return;
129        }
130
131        $newContent = $this->getUpdatedTrackerFileContent();
132
133        if (!$this->toFile->isFileContentSame($newContent)) {
134            $savedFiles = $this->toFile->save($newContent);
135            foreach ($savedFiles as $savedFile) {
136
137                /**
138                 * Triggered after the tracker JavaScript content (the content of the piwik.js file) is changed.
139                 *
140                 * @param string $absolutePath The path to the new piwik.js file.
141                 */
142                Piwik::postEvent('CustomJsTracker.trackerJsChanged', [$savedFile]);
143            }
144
145        }
146
147        // we need to make sure to sync matomo.js / piwik.js
148        $this->updateAlternative('piwik.js', 'matomo.js', $newContent);
149        $this->updateAlternative('matomo.js', 'piwik.js', $newContent);
150    }
151
152    private function updateAlternative($fromFile, $toFile, $newContent)
153    {
154        if (Common::stringEndsWith($this->toFile->getName(), $fromFile)) {
155            $alternativeFilename = dirname($this->toFile->getPath()) . DIRECTORY_SEPARATOR . $toFile;
156            $file = $this->toFile->setFile($alternativeFilename);
157            if ($file->hasWriteAccess() && !$file->isFileContentSame($newContent)) {
158                $savedFiles = $file->save($newContent);
159                foreach ($savedFiles as $savedFile) {
160                    Piwik::postEvent('CustomJsTracker.trackerJsChanged', [$savedFile]);
161                }
162            }
163        }
164    }
165}
166