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