PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.7.0
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.7.0
5.13.0 5.12.1 5.12.0 5.11.1 5.11.0 5.10.2 5.10.1 trunk 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.3.2 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.1.0 4.1.1 4.1.2 4.1.3 4.10.0 4.11.0 4.12.0 4.13.0 4.13.2 4.13.3 4.13.4 4.13.5 4.14.0 4.14.1 4.14.2 4.15.0 4.15.1 4.15.2 4.15.3 4.2.0 4.3.0 4.3.1 4.4.1 4.4.2 4.5.0 4.6.0 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.10.0 5.2.0 5.2.1 5.2.2 5.3.0 5.3.1 5.3.2 5.3.3 5.6.0 5.6.1 5.7.0 5.7.1 5.8.0 5.8.1 5.8.2
matomo / app / core / FileIntegrity.php
matomo / app / core Last commit date
API 6 months ago Access 8 months ago Application 6 months ago Archive 7 months ago ArchiveProcessor 6 months ago Archiver 2 years ago AssetManager 6 months ago Auth 8 months ago Category 8 months ago Changes 8 months ago CliMulti 1 year ago Columns 8 months ago Concurrency 8 months ago Config 8 months ago Container 8 months ago CronArchive 8 months ago DataAccess 7 months ago DataFiles 2 years ago DataTable 8 months ago Db 6 months ago DeviceDetector 1 year ago Email 2 years ago Exception 6 months ago Http 6 months ago Intl 8 months ago Log 2 years ago Mail 1 year ago Measurable 8 months ago Menu 8 months ago Metrics 8 months ago Notification 8 months ago Period 8 months ago Plugin 6 months ago Policy 6 months ago ProfessionalServices 1 year ago Report 1 year ago ReportRenderer 1 year ago Request 8 months ago Scheduler 8 months ago Segment 6 months ago Session 6 months ago Settings 6 months ago Tracker 6 months ago Translation 1 year ago Twig 1 year ago UpdateCheck 8 months ago Updater 6 months ago Updates 6 months ago Validators 1 year ago View 8 months ago ViewDataTable 8 months ago Visualization 1 year ago Widget 1 year ago .htaccess 2 years ago Access.php 6 months ago Archive.php 8 months ago ArchiveProcessor.php 6 months ago AssetManager.php 1 year ago Auth.php 8 months ago AuthResult.php 8 months ago BaseFactory.php 2 years ago Cache.php 2 years ago CacheId.php 6 months ago CliMulti.php 8 months ago Common.php 8 months ago Config.php 6 months ago Console.php 1 year ago Context.php 2 years ago Cookie.php 1 year ago CronArchive.php 8 months ago DI.php 1 year ago DataArray.php 7 months ago DataTable.php 6 months ago Date.php 6 months ago Db.php 1 year ago DbHelper.php 8 months ago Development.php 1 year ago ErrorHandler.php 8 months ago EventDispatcher.php 1 year ago ExceptionHandler.php 6 months ago FileIntegrity.php 1 year ago Filechecks.php 1 year ago Filesystem.php 1 year ago FrontController.php 6 months ago Http.php 6 months ago IP.php 1 year ago Log.php 8 months ago LogDeleter.php 1 year ago Mail.php 1 year ago Metrics.php 8 months ago NoAccessException.php 2 years ago Nonce.php 8 months ago Notification.php 8 months ago NumberFormatter.php 7 months ago Option.php 7 months ago Period.php 7 months ago Piwik.php 8 months ago Plugin.php 1 year ago Process.php 1 year ago Profiler.php 8 months ago ProxyHeaders.php 6 months ago ProxyHttp.php 7 months ago QuickForm2.php 1 year ago RankingQuery.php 7 months ago ReportRenderer.php 1 year ago Request.php 1 year ago Segment.php 8 months ago Sequence.php 8 months ago Session.php 1 year ago SettingsPiwik.php 1 year ago SettingsServer.php 1 year ago Singleton.php 2 years ago Site.php 6 months ago SiteContentDetector.php 1 year ago SupportedBrowser.php 2 years ago TCPDF.php 1 year ago Theme.php 1 year ago Timer.php 2 years ago Tracker.php 6 months ago Twig.php 8 months ago Unzip.php 1 year ago UpdateCheck.php 1 year ago Updater.php 1 year ago UpdaterErrorException.php 2 years ago Updates.php 1 year ago Url.php 6 months ago UrlHelper.php 1 year ago Version.php 6 months ago View.php 8 months ago bootstrap.php 1 year ago dispatch.php 2 years ago testMinimumPhpVersion.php 8 months ago
FileIntegrity.php
379 lines
1 <?php
2
3 /**
4 * Matomo - free/libre analytics platform
5 *
6 * @link https://matomo.org
7 * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
8 */
9 namespace Piwik;
10
11 use Piwik\Container\StaticContainer;
12 use Piwik\Plugins\CustomJsTracker\Exception\AccessDeniedException;
13 use Piwik\Plugins\CustomJsTracker\TrackerUpdater;
14 class FileIntegrity
15 {
16 /**
17 * Get file integrity information
18 *
19 * @return array(bool $success, array $messages)
20 */
21 public static function getFileIntegrityInformation()
22 {
23 $messages = array();
24 self::loadManifest();
25 if (!class_exists('Piwik\\Manifest')) {
26 $messages[] = \Piwik\Piwik::translate('General_WarningFileIntegrityNoManifest') . '<br/>' . \Piwik\Piwik::translate('General_WarningFileIntegrityNoManifestDeployingFromGit');
27 return array($success = \false, $messages);
28 }
29 $messages = self::getMessagesDirectoriesFoundButNotExpected($messages);
30 $messages = self::getMessagesFilesFoundButNotExpected($messages);
31 $messages = self::getMessagesFilesMismatch($messages);
32 return array($success = empty($messages), $messages);
33 }
34 /**
35 * Return just a list of the unexpected files
36 *
37 * @return array
38 */
39 public static function getUnexpectedFilesList() : array
40 {
41 self::loadManifest();
42 $files = self::getFilesFoundButNotExpected();
43 return $files;
44 }
45 /**
46 * Include the manifest
47 *
48 * @return void
49 */
50 private static function loadManifest() : void
51 {
52 $manifest = PIWIK_INCLUDE_PATH . '/config/manifest.inc.php';
53 if (file_exists($manifest)) {
54 require_once $manifest;
55 }
56 }
57 protected static function getFilesNotInManifestButExpectedAnyway()
58 {
59 return StaticContainer::get('fileintegrity.ignore');
60 }
61 protected static function getMessagesDirectoriesFoundButNotExpected($messages)
62 {
63 $directoriesFoundButNotExpected = self::getDirectoriesFoundButNotExpected();
64 if (count($directoriesFoundButNotExpected) > 0) {
65 $messageDirectoriesToDelete = '';
66 foreach ($directoriesFoundButNotExpected as $directoryFoundNotExpected) {
67 $messageDirectoriesToDelete .= \Piwik\Piwik::translate('General_ExceptionDirectoryToDelete', htmlspecialchars($directoryFoundNotExpected)) . '<br/>';
68 }
69 $directories = array();
70 foreach ($directoriesFoundButNotExpected as $directoryFoundNotExpected) {
71 $directories[] = htmlspecialchars(realpath(dirname($directoryFoundNotExpected)) . \DIRECTORY_SEPARATOR . basename($directoryFoundNotExpected));
72 }
73 $deleteAllAtOnce = array();
74 $chunks = array_chunk($directories, 50);
75 $command = 'rm -Rf';
76 if (\Piwik\SettingsServer::isWindows()) {
77 $command = 'rmdir /s /q';
78 }
79 foreach ($chunks as $directories) {
80 $deleteAllAtOnce[] = sprintf('%s %s', $command, implode(' ', $directories));
81 }
82 $messages[] = \Piwik\Piwik::translate('General_ExceptionUnexpectedDirectory') . '<br/>' . '--> ' . \Piwik\Piwik::translate('General_ExceptionUnexpectedDirectoryPleaseDelete') . ' <--' . '<br/><br/>' . $messageDirectoriesToDelete . '<br/><br/>' . \Piwik\Piwik::translate('General_ToDeleteAllDirectoriesRunThisCommand') . '<br/>' . implode('<br />', $deleteAllAtOnce) . '<br/><br/>';
83 }
84 return $messages;
85 }
86 /**
87 * @param $messages
88 * @return array
89 */
90 protected static function getMessagesFilesFoundButNotExpected($messages)
91 {
92 $filesFoundButNotExpected = self::getFilesFoundButNotExpected();
93 if (count($filesFoundButNotExpected) > 0) {
94 $messageFilesToDelete = '';
95 foreach ($filesFoundButNotExpected as $fileFoundNotExpected) {
96 $messageFilesToDelete .= \Piwik\Piwik::translate('General_ExceptionFileToDelete', htmlspecialchars($fileFoundNotExpected)) . '<br/>';
97 }
98 $files = array();
99 foreach ($filesFoundButNotExpected as $fileFoundNotExpected) {
100 $files[] = '"' . htmlspecialchars(realpath(dirname($fileFoundNotExpected)) . \DIRECTORY_SEPARATOR . basename($fileFoundNotExpected)) . '"';
101 }
102 $deleteAllAtOnce = array();
103 $chunks = array_chunk($files, 50);
104 $command = 'rm';
105 if (\Piwik\SettingsServer::isWindows()) {
106 $command = 'del';
107 }
108 foreach ($chunks as $files) {
109 $deleteAllAtOnce[] = sprintf('%s %s', $command, implode(' ', $files));
110 }
111 $messages[] = \Piwik\Piwik::translate('General_ExceptionUnexpectedFile') . '<br/>' . '--> ' . \Piwik\Piwik::translate('General_ExceptionUnexpectedFilePleaseDelete') . ' <--' . '<br/><br/>' . $messageFilesToDelete . '<br/><br/>' . \Piwik\Piwik::translate('General_ToDeleteAllFilesRunThisCommand') . '<br/>' . implode('<br />', $deleteAllAtOnce) . '<br/><br/>';
112 return $messages;
113 }
114 return $messages;
115 }
116 /**
117 * Look for whole directories which are in the filesystem, but should not be
118 *
119 * @return array
120 */
121 protected static function getDirectoriesFoundButNotExpected()
122 {
123 static $cache = null;
124 if (!is_null($cache)) {
125 return $cache;
126 }
127 $pluginsInManifest = self::getPluginsFoundInManifest();
128 $directoriesInManifest = self::getDirectoriesFoundInManifest();
129 $directoriesFoundButNotExpected = array();
130 foreach (self::getPathsToInvestigate() as $file) {
131 $file = substr($file, strlen(PIWIK_DOCUMENT_ROOT));
132 // remove piwik path to match format in manifest.inc.php
133 $file = ltrim($file, "\\/");
134 $directory = dirname($file);
135 if (in_array($directory, $directoriesInManifest)) {
136 continue;
137 }
138 if (self::isFileNotInManifestButExpectedAnyway($file)) {
139 continue;
140 }
141 if (self::isFileFromPluginNotInManifest($file, $pluginsInManifest)) {
142 continue;
143 }
144 if (!in_array($directory, $directoriesFoundButNotExpected)) {
145 $directoriesFoundButNotExpected[] = $directory;
146 }
147 }
148 $cache = self::getParentDirectoriesFromListOfDirectories($directoriesFoundButNotExpected);
149 return $cache;
150 }
151 /**
152 * Look for files which are in the filesystem, but should not be
153 *
154 * @return array
155 */
156 protected static function getFilesFoundButNotExpected()
157 {
158 $files = \Piwik\Manifest::$files;
159 $pluginsInManifest = self::getPluginsFoundInManifest();
160 $filesFoundButNotExpected = array();
161 foreach (self::getPathsToInvestigate() as $file) {
162 if (is_dir($file)) {
163 continue;
164 }
165 $file = substr($file, strlen(PIWIK_DOCUMENT_ROOT));
166 // remove piwik path to match format in manifest.inc.php
167 $file = ltrim($file, "\\/");
168 if (self::isFileFromPluginNotInManifest($file, $pluginsInManifest)) {
169 continue;
170 }
171 if (self::isFileNotInManifestButExpectedAnyway($file)) {
172 continue;
173 }
174 if (self::isFileFromDirectoryThatShouldBeDeleted($file)) {
175 // we already report the directory as "Directory to delete" so no need to repeat the instruction for each file
176 continue;
177 }
178 if (!isset($files[$file])) {
179 $filesFoundButNotExpected[] = $file;
180 }
181 }
182 return $filesFoundButNotExpected;
183 }
184 protected static function isFileFromDirectoryThatShouldBeDeleted($file)
185 {
186 $directoriesWillBeDeleted = self::getDirectoriesFoundButNotExpected();
187 foreach ($directoriesWillBeDeleted as $directoryWillBeDeleted) {
188 if (strpos($file, $directoryWillBeDeleted) === 0) {
189 return \true;
190 }
191 }
192 return \false;
193 }
194 protected static function getDirectoriesFoundInManifest()
195 {
196 $files = \Piwik\Manifest::$files;
197 $directories = array();
198 foreach ($files as $file => $manifestIntegrityInfo) {
199 $directory = $file;
200 // add this directory and each parent directory
201 while (($directory = dirname($directory)) && $directory != '.' && $directory != '/') {
202 $directories[] = $directory;
203 }
204 }
205 $directories = array_unique($directories);
206 return $directories;
207 }
208 protected static function getPluginsFoundInManifest()
209 {
210 $files = \Piwik\Manifest::$files;
211 $pluginsInManifest = array();
212 foreach ($files as $file => $manifestIntegrityInfo) {
213 if (strpos($file, 'plugins/') === 0) {
214 $pluginName = self::getPluginNameFromFilepath($file);
215 $pluginsInManifest[] = $pluginName;
216 }
217 }
218 return $pluginsInManifest;
219 }
220 /**
221 * If a plugin folder is not tracked in the manifest then we don't try to report any files in this folder
222 * Could be a third party plugin or any plugin from the Marketplace
223 *
224 * @param $file
225 * @param $pluginsInManifest
226 * @return bool
227 */
228 protected static function isFileFromPluginNotInManifest($file, $pluginsInManifest)
229 {
230 if (strpos($file, 'plugins/') !== 0) {
231 return \false;
232 }
233 if (substr_count($file, '/') < 2) {
234 // must be a file plugins/abc.xyz and not a plugin directory
235 return \false;
236 }
237 $pluginName = self::getPluginNameFromFilepath($file);
238 if (in_array($pluginName, $pluginsInManifest)) {
239 return \false;
240 }
241 return \true;
242 }
243 protected static function isFileNotInManifestButExpectedAnyway($file)
244 {
245 $expected = self::getFilesNotInManifestButExpectedAnyway();
246 foreach ($expected as $expectedPattern) {
247 if (fnmatch($expectedPattern, $file, defined('FNM_CASEFOLD') ? \FNM_CASEFOLD : 0)) {
248 return \true;
249 }
250 }
251 return \false;
252 }
253 protected static function getMessagesFilesMismatch($messages)
254 {
255 $messagesMismatch = array();
256 $hasHashFile = function_exists('hash_file');
257 $files = \Piwik\Manifest::$files;
258 foreach ($files as $path => $props) {
259 $file = PIWIK_INCLUDE_PATH . '/' . $path;
260 if (!file_exists($file) || !is_readable($file)) {
261 $messagesMismatch[] = \Piwik\Piwik::translate('General_ExceptionMissingFile', $file);
262 } elseif (filesize($file) != $props[0]) {
263 if (self::isModifiedPathValid($path)) {
264 continue;
265 }
266 if (in_array(substr($path, -4), array('.gif', '.ico', '.jpg', '.png', '.swf'))) {
267 // files that contain binary data (e.g., images) must match the file size
268 $messagesMismatch[] = \Piwik\Piwik::translate('General_ExceptionFilesizeMismatch', array($file, $props[0], filesize($file)));
269 } else {
270 // convert end-of-line characters and re-test text files
271 $content = @file_get_contents($file);
272 $content = str_replace("\r\n", "\n", $content);
273 if (strlen($content) != $props[0] || @hash('sha256', $content) !== $props[1]) {
274 $messagesMismatch[] = \Piwik\Piwik::translate('General_ExceptionFilesizeMismatch', array($file, $props[0], filesize($file)));
275 }
276 }
277 } elseif ($hasHashFile && @hash_file('sha256', $file) !== $props[1]) {
278 if (self::isModifiedPathValid($path)) {
279 continue;
280 }
281 $messagesMismatch[] = \Piwik\Piwik::translate('General_ExceptionFileIntegrity', $file);
282 }
283 }
284 if (!$hasHashFile) {
285 $messages[] = \Piwik\Piwik::translate('General_WarningFileIntegrityNoHashFile');
286 }
287 if (!empty($messagesMismatch)) {
288 $messages[] = \Piwik\Piwik::translate('General_FileIntegrityWarningReupload');
289 $messages[] = '--> ' . \Piwik\Piwik::translate('General_FileIntegrityWarningReuploadBis') . ' <--<br/>';
290 $messages = array_merge($messages, $messagesMismatch);
291 }
292 return $messages;
293 }
294 protected static function isModifiedPathValid($path)
295 {
296 if ($path === 'piwik.js' || $path === 'matomo.js') {
297 // we could have used a postEvent hook to enrich "\Piwik\Manifest::$files;" which would also benefit plugins
298 // that want to check for file integrity but we do not want to risk to break anything right now. It is not
299 // as trivial because piwik.js might be already updated, or updated on the next request. We cannot define
300 // 2 or 3 different filesizes and md5 hashes for one file so we check it here.
301 if (\Piwik\Plugin\Manager::getInstance()->isPluginActivated('CustomJsTracker')) {
302 $trackerUpdater = new TrackerUpdater();
303 if ($trackerUpdater->getCurrentTrackerFileContent() === $trackerUpdater->getUpdatedTrackerFileContent()) {
304 // file was already updated, eg manually or via custom piwik.js, this is a valid piwik.js file as
305 // it was enriched by tracker plugins
306 return \true;
307 }
308 try {
309 // the piwik.js tracker file was not updated yet, but may be updated just after the update by
310 // one of the events CustomJsTracker is listening to or by a scheduled task.
311 // In this case, we check whether such an update will succeed later and if it will, the file is
312 // valid as well as it will be updated on the next request
313 $trackerUpdater->checkWillSucceed();
314 return \true;
315 } catch (AccessDeniedException $e) {
316 return \false;
317 }
318 }
319 }
320 return \false;
321 }
322 protected static function getPluginNameFromFilepath($file)
323 {
324 $pathRelativeToPlugins = substr($file, strlen('plugins/'));
325 $pluginName = substr($pathRelativeToPlugins, 0, strpos($pathRelativeToPlugins, '/'));
326 return $pluginName;
327 }
328 /**
329 * @return array
330 */
331 protected static function getPathsToInvestigate()
332 {
333 $filesToInvestigate = array_merge(
334 // all normal files
335 \Piwik\Filesystem::globr(PIWIK_DOCUMENT_ROOT, '*'),
336 // all hidden files
337 \Piwik\Filesystem::globr(PIWIK_DOCUMENT_ROOT, '.*')
338 );
339 return $filesToInvestigate;
340 }
341 /**
342 * @param $directoriesFoundButNotExpected
343 * @return array
344 */
345 protected static function getParentDirectoriesFromListOfDirectories($directoriesFoundButNotExpected)
346 {
347 sort($directoriesFoundButNotExpected);
348 $parentDirectoriesOnly = array();
349 foreach ($directoriesFoundButNotExpected as $directory) {
350 $directoryParent = self::getDirectoryParentFromList($directory, $directoriesFoundButNotExpected);
351 if ($directoryParent) {
352 $parentDirectoriesOnly[] = $directoryParent;
353 }
354 }
355 $parentDirectoriesOnly = array_unique($parentDirectoriesOnly);
356 return $parentDirectoriesOnly;
357 }
358 /**
359 * When the parent directory of $directory is found within $directories, return it.
360 *
361 * @param $directory
362 * @param $directories
363 * @return string
364 */
365 protected static function getDirectoryParentFromList($directory, $directories)
366 {
367 foreach ($directories as $directoryMaybeParent) {
368 if ($directory == $directoryMaybeParent) {
369 continue;
370 }
371 $isParentDirectory = strpos($directory, $directoryMaybeParent) === 0;
372 if ($isParentDirectory) {
373 return $directoryMaybeParent;
374 }
375 }
376 return null;
377 }
378 }
379