PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.1.0
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.1.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 2 years ago Application 2 years ago Archive 2 years ago ArchiveProcessor 2 years ago Archiver 2 years ago AssetManager 2 years ago Auth 2 years ago Category 2 years ago Changes 2 years ago CliMulti 2 years ago Columns 2 years ago Concurrency 2 years ago Config 2 years ago Container 2 years ago CronArchive 2 years ago DataAccess 2 years ago DataFiles 2 years ago DataTable 2 years ago Db 2 years ago DeviceDetector 2 years ago Email 2 years ago Exception 2 years ago Http 2 years ago Intl 2 years ago Log 2 years ago Mail 2 years ago Measurable 2 years ago Menu 2 years ago Metrics 2 years ago Notification 2 years ago Period 2 years ago Plugin 2 years ago ProfessionalServices 2 years ago Report 2 years ago ReportRenderer 2 years ago Scheduler 2 years ago Segment 2 years ago Session 2 years ago Settings 2 years ago Tracker 2 years ago Translation 2 years ago Twig 2 years ago UpdateCheck 2 years ago Updater 2 years ago Updates 2 years ago Validators 2 years ago View 2 years ago ViewDataTable 2 years ago Visualization 2 years ago Widget 2 years ago .htaccess 2 years ago Access.php 2 years ago Archive.php 2 years ago ArchiveProcessor.php 2 years ago AssetManager.php 2 years ago Auth.php 2 years ago AuthResult.php 2 years ago BaseFactory.php 2 years ago Cache.php 2 years ago CacheId.php 2 years ago CliMulti.php 2 years ago Common.php 2 years ago Config.php 2 years ago Console.php 2 years ago Context.php 2 years ago Cookie.php 2 years ago CronArchive.php 2 years ago DI.php 2 years ago DataArray.php 2 years ago DataTable.php 2 years ago Date.php 2 years ago Db.php 2 years ago DbHelper.php 2 years ago Development.php 2 years ago ErrorHandler.php 2 years ago EventDispatcher.php 2 years ago ExceptionHandler.php 2 years ago FileIntegrity.php 2 years ago Filechecks.php 2 years ago Filesystem.php 2 years ago FrontController.php 2 years ago Http.php 2 years ago IP.php 2 years ago Log.php 2 years ago LogDeleter.php 2 years ago Mail.php 2 years ago Metrics.php 2 years ago NoAccessException.php 2 years ago Nonce.php 2 years ago Notification.php 2 years ago NumberFormatter.php 2 years ago Option.php 2 years ago Period.php 2 years ago Piwik.php 2 years ago Plugin.php 2 years ago Profiler.php 2 years ago ProxyHeaders.php 2 years ago ProxyHttp.php 2 years ago QuickForm2.php 2 years ago RankingQuery.php 2 years ago ReportRenderer.php 2 years ago Request.php 2 years ago Segment.php 2 years ago Sequence.php 2 years ago Session.php 2 years ago SettingsPiwik.php 2 years ago SettingsServer.php 2 years ago Singleton.php 2 years ago Site.php 2 years ago SiteContentDetector.php 2 years ago SupportedBrowser.php 2 years ago TCPDF.php 2 years ago Theme.php 2 years ago Timer.php 2 years ago Tracker.php 2 years ago Twig.php 2 years ago Unzip.php 2 years ago UpdateCheck.php 2 years ago Updater.php 2 years ago UpdaterErrorException.php 2 years ago Updates.php 2 years ago Url.php 2 years ago UrlHelper.php 2 years ago Version.php 2 years ago View.php 2 years ago bootstrap.php 2 years ago dispatch.php 2 years ago testMinimumPhpVersion.php 2 years ago
FileIntegrity.php
380 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($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($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 $hasMd5file = function_exists('md5_file');
257 $files = \Piwik\Manifest::$files;
258 $hasMd5 = function_exists('md5');
259 foreach ($files as $path => $props) {
260 $file = PIWIK_INCLUDE_PATH . '/' . $path;
261 if (!file_exists($file) || !is_readable($file)) {
262 $messagesMismatch[] = \Piwik\Piwik::translate('General_ExceptionMissingFile', $file);
263 } elseif (filesize($file) != $props[0]) {
264 if (self::isModifiedPathValid($path)) {
265 continue;
266 }
267 if (!$hasMd5 || in_array(substr($path, -4), array('.gif', '.ico', '.jpg', '.png', '.swf'))) {
268 // files that contain binary data (e.g., images) must match the file size
269 $messagesMismatch[] = \Piwik\Piwik::translate('General_ExceptionFilesizeMismatch', array($file, $props[0], filesize($file)));
270 } else {
271 // convert end-of-line characters and re-test text files
272 $content = @file_get_contents($file);
273 $content = str_replace("\r\n", "\n", $content);
274 if (strlen($content) != $props[0] || @md5($content) !== $props[1]) {
275 $messagesMismatch[] = \Piwik\Piwik::translate('General_ExceptionFilesizeMismatch', array($file, $props[0], filesize($file)));
276 }
277 }
278 } elseif ($hasMd5file && @md5_file($file) !== $props[1]) {
279 if (self::isModifiedPathValid($path)) {
280 continue;
281 }
282 $messagesMismatch[] = \Piwik\Piwik::translate('General_ExceptionFileIntegrity', $file);
283 }
284 }
285 if (!$hasMd5file) {
286 $messages[] = \Piwik\Piwik::translate('General_WarningFileIntegrityNoMd5file');
287 }
288 if (!empty($messagesMismatch)) {
289 $messages[] = \Piwik\Piwik::translate('General_FileIntegrityWarningReupload');
290 $messages[] = '--> ' . \Piwik\Piwik::translate('General_FileIntegrityWarningReuploadBis') . ' <--<br/>';
291 $messages = array_merge($messages, $messagesMismatch);
292 }
293 return $messages;
294 }
295 protected static function isModifiedPathValid($path)
296 {
297 if ($path === 'piwik.js' || $path === 'matomo.js') {
298 // we could have used a postEvent hook to enrich "\Piwik\Manifest::$files;" which would also benefit plugins
299 // that want to check for file integrity but we do not want to risk to break anything right now. It is not
300 // as trivial because piwik.js might be already updated, or updated on the next request. We cannot define
301 // 2 or 3 different filesizes and md5 hashes for one file so we check it here.
302 if (\Piwik\Plugin\Manager::getInstance()->isPluginActivated('CustomJsTracker')) {
303 $trackerUpdater = new TrackerUpdater();
304 if ($trackerUpdater->getCurrentTrackerFileContent() === $trackerUpdater->getUpdatedTrackerFileContent()) {
305 // file was already updated, eg manually or via custom piwik.js, this is a valid piwik.js file as
306 // it was enriched by tracker plugins
307 return true;
308 }
309 try {
310 // the piwik.js tracker file was not updated yet, but may be updated just after the update by
311 // one of the events CustomJsTracker is listening to or by a scheduled task.
312 // In this case, we check whether such an update will succeed later and if it will, the file is
313 // valid as well as it will be updated on the next request
314 $trackerUpdater->checkWillSucceed();
315 return true;
316 } catch (AccessDeniedException $e) {
317 return false;
318 }
319 }
320 }
321 return false;
322 }
323 protected static function getPluginNameFromFilepath($file)
324 {
325 $pathRelativeToPlugins = substr($file, strlen('plugins/'));
326 $pluginName = substr($pathRelativeToPlugins, 0, strpos($pathRelativeToPlugins, '/'));
327 return $pluginName;
328 }
329 /**
330 * @return array
331 */
332 protected static function getPathsToInvestigate()
333 {
334 $filesToInvestigate = array_merge(
335 // all normal files
336 \Piwik\Filesystem::globr(PIWIK_DOCUMENT_ROOT, '*'),
337 // all hidden files
338 \Piwik\Filesystem::globr(PIWIK_DOCUMENT_ROOT, '.*')
339 );
340 return $filesToInvestigate;
341 }
342 /**
343 * @param $directoriesFoundButNotExpected
344 * @return array
345 */
346 protected static function getParentDirectoriesFromListOfDirectories($directoriesFoundButNotExpected)
347 {
348 sort($directoriesFoundButNotExpected);
349 $parentDirectoriesOnly = array();
350 foreach ($directoriesFoundButNotExpected as $directory) {
351 $directoryParent = self::getDirectoryParentFromList($directory, $directoriesFoundButNotExpected);
352 if ($directoryParent) {
353 $parentDirectoriesOnly[] = $directoryParent;
354 }
355 }
356 $parentDirectoriesOnly = array_unique($parentDirectoriesOnly);
357 return $parentDirectoriesOnly;
358 }
359 /**
360 * When the parent directory of $directory is found within $directories, return it.
361 *
362 * @param $directory
363 * @param $directories
364 * @return string
365 */
366 protected static function getDirectoryParentFromList($directory, $directories)
367 {
368 foreach ($directories as $directoryMaybeParent) {
369 if ($directory == $directoryMaybeParent) {
370 continue;
371 }
372 $isParentDirectory = strpos($directory, $directoryMaybeParent) === 0;
373 if ($isParentDirectory) {
374 return $directoryMaybeParent;
375 }
376 }
377 return null;
378 }
379 }
380