PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 4.15.0
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v4.15.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 2 years ago Access 3 years ago Application 4 years ago Archive 3 years ago ArchiveProcessor 2 years ago Archiver 5 years ago AssetManager 3 years ago Auth 3 years ago Category 5 years ago Changes 3 years ago CliMulti 4 years ago Columns 3 years ago Concurrency 3 years ago Config 4 years ago Container 4 years ago CronArchive 3 years ago DataAccess 2 years ago DataFiles 5 years ago DataTable 2 years ago Db 3 years ago DeviceDetector 3 years ago Email 5 years ago Exception 3 years ago Http 4 years ago Intl 4 years ago Mail 3 years ago Measurable 5 years ago Menu 3 years ago Metrics 4 years ago Notification 4 years ago Period 4 years ago Plugin 2 years ago ProfessionalServices 4 years ago Report 5 years ago ReportRenderer 2 years ago Scheduler 4 years ago Segment 3 years ago Session 4 years ago Settings 3 years ago Tracker 3 years ago Translation 4 years ago UpdateCheck 5 years ago Updater 3 years ago Updates 3 years ago Validators 4 years ago View 4 years ago ViewDataTable 3 years ago Visualization 4 years ago Widget 5 years ago Access.php 4 years ago Archive.php 3 years ago ArchiveProcessor.php 2 years ago AssetManager.php 4 years ago Auth.php 5 years ago AuthResult.php 5 years ago BaseFactory.php 5 years ago Cache.php 5 years ago CacheId.php 5 years ago CliMulti.php 4 years ago Common.php 3 years ago Config.php 3 years ago Console.php 3 years ago Context.php 5 years ago Cookie.php 3 years ago CronArchive.php 3 years ago DataArray.php 4 years ago DataTable.php 2 years ago Date.php 3 years ago Db.php 4 years ago DbHelper.php 3 years ago Development.php 5 years ago ErrorHandler.php 5 years ago EventDispatcher.php 3 years ago ExceptionHandler.php 3 years ago FileIntegrity.php 3 years ago Filechecks.php 4 years ago Filesystem.php 3 years ago FrontController.php 3 years ago Http.php 3 years ago IP.php 4 years ago Log.php 4 years ago LogDeleter.php 4 years ago Mail.php 3 years ago Metrics.php 3 years ago NoAccessException.php 5 years ago Nonce.php 3 years ago Notification.php 5 years ago NumberFormatter.php 4 years ago Option.php 4 years ago Period.php 5 years ago Piwik.php 3 years ago Plugin.php 4 years ago Profiler.php 4 years ago ProxyHeaders.php 5 years ago ProxyHttp.php 3 years ago QuickForm2.php 5 years ago RankingQuery.php 3 years ago ReportRenderer.php 4 years ago Segment.php 3 years ago Sequence.php 5 years ago Session.php 3 years ago SettingsPiwik.php 3 years ago SettingsServer.php 5 years ago Singleton.php 5 years ago Site.php 3 years ago SiteContentDetector.php 2 years ago SupportedBrowser.php 3 years ago TCPDF.php 5 years ago Theme.php 5 years ago Timer.php 5 years ago Tracker.php 4 years ago Twig.php 4 years ago Unzip.php 5 years ago UpdateCheck.php 5 years ago Updater.php 4 years ago UpdaterErrorException.php 5 years ago Updates.php 5 years ago Url.php 2 years ago UrlHelper.php 3 years ago Version.php 2 years ago View.php 3 years ago bootstrap.php 3 years ago dispatch.php 5 years ago testMinimumPhpVersion.php 3 years ago
FileIntegrity.php
487 lines
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 */
9
10 namespace Piwik;
11
12 use Piwik\Container\StaticContainer;
13 use Piwik\Plugins\CustomJsTracker\Exception\AccessDeniedException;
14 use Piwik\Plugins\CustomJsTracker\TrackerUpdater;
15
16 class FileIntegrity
17 {
18
19 /**
20 * Get file integrity information
21 *
22 * @return array(bool $success, array $messages)
23 */
24 public static function getFileIntegrityInformation()
25 {
26 $messages = array();
27
28 self::loadManifest();
29
30 if (!class_exists('Piwik\\Manifest')) {
31 $messages[] = Piwik::translate('General_WarningFileIntegrityNoManifest')
32 . '<br/>'
33 . Piwik::translate('General_WarningFileIntegrityNoManifestDeployingFromGit');
34
35 return array(
36 $success = false,
37 $messages
38 );
39 }
40
41
42 $messages = self::getMessagesDirectoriesFoundButNotExpected($messages);
43
44 $messages = self::getMessagesFilesFoundButNotExpected($messages);
45
46 $messages = self::getMessagesFilesMismatch($messages);
47
48 return array(
49 $success = empty($messages),
50 $messages
51 );
52 }
53
54 /**
55 * Return just a list of the unexpected files
56 *
57 * @return array
58 */
59 public static function getUnexpectedFilesList(): array
60 {
61 self::loadManifest();
62 $files = self::getFilesFoundButNotExpected();
63 return $files;
64 }
65
66 /**
67 * Include the manifest
68 *
69 * @return void
70 */
71 private static function loadManifest(): void
72 {
73 $manifest = PIWIK_INCLUDE_PATH . '/config/manifest.inc.php';
74
75 if (file_exists($manifest)) {
76 require_once $manifest;
77 }
78 }
79
80 protected static function getFilesNotInManifestButExpectedAnyway()
81 {
82 return StaticContainer::get('fileintegrity.ignore');
83 }
84
85 protected static function getMessagesDirectoriesFoundButNotExpected($messages)
86 {
87 $directoriesFoundButNotExpected = self::getDirectoriesFoundButNotExpected();
88 if (count($directoriesFoundButNotExpected) > 0) {
89
90 $messageDirectoriesToDelete = '';
91 foreach ($directoriesFoundButNotExpected as $directoryFoundNotExpected) {
92 $messageDirectoriesToDelete .= Piwik::translate('General_ExceptionDirectoryToDelete', htmlspecialchars($directoryFoundNotExpected)) . '<br/>';
93 }
94
95 $directories = array();
96 foreach ($directoriesFoundButNotExpected as $directoryFoundNotExpected) {
97 $directories[] = htmlspecialchars(realpath($directoryFoundNotExpected));
98 }
99
100 $deleteAllAtOnce = array();
101 $chunks = array_chunk($directories, 50);
102
103 $command = 'rm -Rf';
104
105 if (SettingsServer::isWindows()) {
106 $command = 'rmdir /s /q';
107 }
108
109 foreach ($chunks as $directories) {
110 $deleteAllAtOnce[] = sprintf('%s %s', $command, implode(' ', $directories));
111 }
112
113 $messages[] = Piwik::translate('General_ExceptionUnexpectedDirectory')
114 . '<br/>'
115 . '--> ' . Piwik::translate('General_ExceptionUnexpectedDirectoryPleaseDelete') . ' <--'
116 . '<br/><br/>'
117 . $messageDirectoriesToDelete
118 . '<br/><br/>'
119 . Piwik::translate('General_ToDeleteAllDirectoriesRunThisCommand')
120 . '<br/>'
121 . implode('<br />', $deleteAllAtOnce)
122 . '<br/><br/>';
123
124 }
125
126 return $messages;
127 }
128
129 /**
130 * @param $messages
131 * @return array
132 */
133 protected static function getMessagesFilesFoundButNotExpected($messages)
134 {
135 $filesFoundButNotExpected = self::getFilesFoundButNotExpected();
136 if (count($filesFoundButNotExpected) > 0) {
137
138 $messageFilesToDelete = '';
139 foreach ($filesFoundButNotExpected as $fileFoundNotExpected) {
140 $messageFilesToDelete .= Piwik::translate('General_ExceptionFileToDelete', htmlspecialchars($fileFoundNotExpected)) . '<br/>';
141 }
142
143 $files = array();
144 foreach ($filesFoundButNotExpected as $fileFoundNotExpected) {
145 $files[] = '"' . htmlspecialchars(realpath($fileFoundNotExpected)) . '"';
146 }
147
148 $deleteAllAtOnce = array();
149 $chunks = array_chunk($files, 50);
150
151 $command = 'rm';
152
153 if (SettingsServer::isWindows()) {
154 $command = 'del';
155 }
156
157 foreach ($chunks as $files) {
158 $deleteAllAtOnce[] = sprintf('%s %s', $command, implode(' ', $files));
159 }
160
161 $messages[] = Piwik::translate('General_ExceptionUnexpectedFile')
162 . '<br/>'
163 . '--> ' . Piwik::translate('General_ExceptionUnexpectedFilePleaseDelete') . ' <--'
164 . '<br/><br/>'
165 . $messageFilesToDelete
166 . '<br/><br/>'
167 . Piwik::translate('General_ToDeleteAllFilesRunThisCommand')
168 . '<br/>'
169 . implode('<br />', $deleteAllAtOnce)
170 . '<br/><br/>';
171
172 return $messages;
173
174 }
175 return $messages;
176 }
177
178 /**
179 * Look for whole directories which are in the filesystem, but should not be
180 *
181 * @return array
182 */
183 protected static function getDirectoriesFoundButNotExpected()
184 {
185 static $cache = null;
186 if(!is_null($cache)) {
187 return $cache;
188 }
189
190 $pluginsInManifest = self::getPluginsFoundInManifest();
191 $directoriesInManifest = self::getDirectoriesFoundInManifest();
192 $directoriesFoundButNotExpected = array();
193
194 foreach (self::getPathsToInvestigate() as $file) {
195 $file = substr($file, strlen(PIWIK_DOCUMENT_ROOT)); // remove piwik path to match format in manifest.inc.php
196 $file = ltrim($file, "\\/");
197 $directory = dirname($file);
198
199 if(in_array($directory, $directoriesInManifest)) {
200 continue;
201 }
202
203 if (self::isFileNotInManifestButExpectedAnyway($file)) {
204 continue;
205 }
206 if (self::isFileFromPluginNotInManifest($file, $pluginsInManifest)) {
207 continue;
208 }
209
210 if (!in_array($directory, $directoriesFoundButNotExpected)) {
211 $directoriesFoundButNotExpected[] = $directory;
212 }
213 }
214
215 $cache = self::getParentDirectoriesFromListOfDirectories($directoriesFoundButNotExpected);
216 return $cache;
217 }
218 /**
219 * Look for files which are in the filesystem, but should not be
220 *
221 * @return array
222 */
223 protected static function getFilesFoundButNotExpected()
224 {
225 $files = \Piwik\Manifest::$files;
226 $pluginsInManifest = self::getPluginsFoundInManifest();
227
228 $filesFoundButNotExpected = array();
229
230 foreach (self::getPathsToInvestigate() as $file) {
231 if (is_dir($file)) {
232 continue;
233 }
234 $file = substr($file, strlen(PIWIK_DOCUMENT_ROOT)); // remove piwik path to match format in manifest.inc.php
235 $file = ltrim($file, "\\/");
236
237 if (self::isFileFromPluginNotInManifest($file, $pluginsInManifest)) {
238 continue;
239 }
240 if (self::isFileNotInManifestButExpectedAnyway($file)) {
241 continue;
242 }
243 if (self::isFileFromDirectoryThatShouldBeDeleted($file)) {
244 // we already report the directory as "Directory to delete" so no need to repeat the instruction for each file
245 continue;
246 }
247
248 if (!isset($files[$file])) {
249 $filesFoundButNotExpected[] = $file;
250 }
251 }
252
253 return $filesFoundButNotExpected;
254 }
255
256
257 protected static function isFileFromDirectoryThatShouldBeDeleted($file)
258 {
259 $directoriesWillBeDeleted = self::getDirectoriesFoundButNotExpected();
260 foreach($directoriesWillBeDeleted as $directoryWillBeDeleted) {
261 if(strpos($file, $directoryWillBeDeleted) === 0) {
262 return true;
263 }
264 }
265 return false;
266 }
267
268 protected static function getDirectoriesFoundInManifest()
269 {
270 $files = \Piwik\Manifest::$files;
271
272 $directories = array();
273 foreach($files as $file => $manifestIntegrityInfo) {
274 $directory = $file;
275
276 // add this directory and each parent directory
277 while( ($directory = dirname($directory)) && $directory != '.' && $directory != '/') {
278 $directories[] = $directory;
279 }
280 }
281 $directories = array_unique($directories);
282 return $directories;
283 }
284
285 protected static function getPluginsFoundInManifest()
286 {
287 $files = \Piwik\Manifest::$files;
288
289 $pluginsInManifest = array();
290 foreach($files as $file => $manifestIntegrityInfo) {
291 if(strpos($file, 'plugins/') === 0) {
292 $pluginName = self::getPluginNameFromFilepath($file);
293 $pluginsInManifest[] = $pluginName;
294 }
295 }
296 return $pluginsInManifest;
297 }
298
299 /**
300 * If a plugin folder is not tracked in the manifest then we don't try to report any files in this folder
301 * Could be a third party plugin or any plugin from the Marketplace
302 *
303 * @param $file
304 * @param $pluginsInManifest
305 * @return bool
306 */
307 protected static function isFileFromPluginNotInManifest($file, $pluginsInManifest)
308 {
309 if (strpos($file, 'plugins/') !== 0) {
310 return false;
311 }
312
313 if (substr_count($file, '/') < 2) {
314 // must be a file plugins/abc.xyz and not a plugin directory
315 return false;
316 }
317
318 $pluginName = self::getPluginNameFromFilepath($file);
319 if(in_array($pluginName, $pluginsInManifest)) {
320 return false;
321 }
322
323 return true;
324 }
325
326 protected static function isFileNotInManifestButExpectedAnyway($file)
327 {
328 $expected = self::getFilesNotInManifestButExpectedAnyway();
329 foreach ($expected as $expectedPattern) {
330 if (fnmatch($expectedPattern, $file, defined('FNM_CASEFOLD') ? FNM_CASEFOLD : 0)) {
331 return true;
332 }
333 }
334 return false;
335 }
336
337 protected static function getMessagesFilesMismatch($messages)
338 {
339 $messagesMismatch = array();
340 $hasMd5file = function_exists('md5_file');
341 $files = \Piwik\Manifest::$files;
342 $hasMd5 = function_exists('md5');
343 foreach ($files as $path => $props) {
344 $file = PIWIK_INCLUDE_PATH . '/' . $path;
345
346 if (!file_exists($file) || !is_readable($file)) {
347 $messagesMismatch[] = Piwik::translate('General_ExceptionMissingFile', $file);
348 } elseif (filesize($file) != $props[0]) {
349
350 if (self::isModifiedPathValid($path)) {
351 continue;
352 }
353
354 if (!$hasMd5 || in_array(substr($path, -4), array('.gif', '.ico', '.jpg', '.png', '.swf'))) {
355 // files that contain binary data (e.g., images) must match the file size
356 $messagesMismatch[] = Piwik::translate('General_ExceptionFilesizeMismatch', array($file, $props[0], filesize($file)));
357 } else {
358 // convert end-of-line characters and re-test text files
359 $content = @file_get_contents($file);
360 $content = str_replace("\r\n", "\n", $content);
361 if ((strlen($content) != $props[0])
362 || (@md5($content) !== $props[1])
363 ) {
364 $messagesMismatch[] = Piwik::translate('General_ExceptionFilesizeMismatch', array($file, $props[0], filesize($file)));
365 }
366 }
367 } elseif ($hasMd5file && (@md5_file($file) !== $props[1])) {
368 if (self::isModifiedPathValid($path)) {
369 continue;
370 }
371
372 $messagesMismatch[] = Piwik::translate('General_ExceptionFileIntegrity', $file);
373 }
374 }
375
376 if (!$hasMd5file) {
377 $messages[] = Piwik::translate('General_WarningFileIntegrityNoMd5file');
378 }
379
380 if (!empty($messagesMismatch)) {
381 $messages[] = Piwik::translate('General_FileIntegrityWarningReupload');
382 $messages[] = '--> ' . Piwik::translate('General_FileIntegrityWarningReuploadBis') . ' <--<br/>';
383 $messages = array_merge($messages, $messagesMismatch);
384 }
385
386 return $messages;
387 }
388
389 protected static function isModifiedPathValid($path)
390 {
391 if ($path === 'piwik.js' || $path === 'matomo.js') {
392 // we could have used a postEvent hook to enrich "\Piwik\Manifest::$files;" which would also benefit plugins
393 // that want to check for file integrity but we do not want to risk to break anything right now. It is not
394 // as trivial because piwik.js might be already updated, or updated on the next request. We cannot define
395 // 2 or 3 different filesizes and md5 hashes for one file so we check it here.
396
397 if (Plugin\Manager::getInstance()->isPluginActivated('CustomJsTracker')) {
398 $trackerUpdater = new TrackerUpdater();
399
400 if ($trackerUpdater->getCurrentTrackerFileContent() === $trackerUpdater->getUpdatedTrackerFileContent()) {
401 // file was already updated, eg manually or via custom piwik.js, this is a valid piwik.js file as
402 // it was enriched by tracker plugins
403 return true;
404 }
405
406 try {
407 // the piwik.js tracker file was not updated yet, but may be updated just after the update by
408 // one of the events CustomJsTracker is listening to or by a scheduled task.
409 // In this case, we check whether such an update will succeed later and if it will, the file is
410 // valid as well as it will be updated on the next request
411 $trackerUpdater->checkWillSucceed();
412 return true;
413 } catch (AccessDeniedException $e) {
414 return false;
415 }
416
417 }
418 }
419
420 return false;
421 }
422
423 protected static function getPluginNameFromFilepath($file)
424 {
425 $pathRelativeToPlugins = substr($file, strlen('plugins/'));
426 $pluginName = substr($pathRelativeToPlugins, 0, strpos($pathRelativeToPlugins, '/'));
427 return $pluginName;
428 }
429
430 /**
431 * @return array
432 */
433 protected static function getPathsToInvestigate()
434 {
435 $filesToInvestigate = array_merge(
436 // all normal files
437 Filesystem::globr(PIWIK_DOCUMENT_ROOT, '*'),
438 // all hidden files
439 Filesystem::globr(PIWIK_DOCUMENT_ROOT, '.*')
440 );
441 return $filesToInvestigate;
442 }
443
444 /**
445 * @param $directoriesFoundButNotExpected
446 * @return array
447 */
448 protected static function getParentDirectoriesFromListOfDirectories($directoriesFoundButNotExpected)
449 {
450 sort($directoriesFoundButNotExpected);
451
452 $parentDirectoriesOnly = array();
453 foreach ($directoriesFoundButNotExpected as $directory) {
454 $directoryParent = self::getDirectoryParentFromList($directory, $directoriesFoundButNotExpected);
455 if($directoryParent) {
456 $parentDirectoriesOnly[] = $directoryParent;
457 }
458 }
459 $parentDirectoriesOnly = array_unique($parentDirectoriesOnly);
460
461 return $parentDirectoriesOnly;
462 }
463
464 /**
465 * When the parent directory of $directory is found within $directories, return it.
466 *
467 * @param $directory
468 * @param $directories
469 * @return string
470 */
471 protected static function getDirectoryParentFromList($directory, $directories)
472 {
473 foreach($directories as $directoryMaybeParent) {
474 if ($directory == $directoryMaybeParent) {
475 continue;
476 }
477
478 $isParentDirectory = strpos($directory, $directoryMaybeParent) === 0;
479 if ($isParentDirectory) {
480 return $directoryMaybeParent;
481 }
482 }
483 return null;
484 }
485
486 }
487