PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.2.2
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.2.2
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 1 year ago Access 1 year ago Application 1 year ago Archive 1 year ago ArchiveProcessor 1 year ago Archiver 2 years ago AssetManager 1 year ago Auth 1 year ago Category 2 years ago Changes 1 year ago CliMulti 1 year ago Columns 1 year ago Concurrency 1 year ago Config 1 year ago Container 1 year ago CronArchive 1 year ago DataAccess 1 year ago DataFiles 2 years ago DataTable 1 year ago Db 1 year ago DeviceDetector 1 year ago Email 2 years ago Exception 1 year ago Http 1 year ago Intl 1 year ago Log 2 years ago Mail 1 year ago Measurable 1 year ago Menu 1 year ago Metrics 1 year ago Notification 1 year ago Period 1 year ago Plugin 1 year ago ProfessionalServices 1 year ago Report 1 year ago ReportRenderer 1 year ago Scheduler 1 year ago Segment 1 year ago Session 1 year ago Settings 1 year ago Tracker 1 year ago Translation 1 year ago Twig 1 year ago UpdateCheck 1 year ago Updater 1 year ago Updates 1 year ago Validators 1 year ago View 1 year ago ViewDataTable 1 year ago Visualization 1 year ago Widget 1 year ago .htaccess 2 years ago Access.php 1 year ago Archive.php 1 year ago ArchiveProcessor.php 1 year ago AssetManager.php 1 year ago Auth.php 2 years ago AuthResult.php 2 years ago BaseFactory.php 2 years ago Cache.php 2 years ago CacheId.php 1 year ago CliMulti.php 1 year ago Common.php 1 year ago Config.php 1 year ago Console.php 1 year ago Context.php 2 years ago Cookie.php 1 year ago CronArchive.php 1 year ago DI.php 1 year ago DataArray.php 1 year ago DataTable.php 1 year ago Date.php 1 year ago Db.php 1 year ago DbHelper.php 1 year ago Development.php 1 year ago ErrorHandler.php 1 year ago EventDispatcher.php 1 year ago ExceptionHandler.php 1 year ago FileIntegrity.php 1 year ago Filechecks.php 1 year ago Filesystem.php 1 year ago FrontController.php 1 year ago Http.php 1 year ago IP.php 1 year ago Log.php 2 years ago LogDeleter.php 1 year ago Mail.php 1 year ago Metrics.php 1 year ago NoAccessException.php 2 years ago Nonce.php 1 year ago Notification.php 1 year ago NumberFormatter.php 1 year ago Option.php 1 year ago Period.php 1 year ago Piwik.php 1 year ago Plugin.php 1 year ago Process.php 1 year ago Profiler.php 1 year ago ProxyHeaders.php 2 years ago ProxyHttp.php 1 year ago QuickForm2.php 1 year ago RankingQuery.php 1 year ago ReportRenderer.php 1 year ago Request.php 1 year ago Segment.php 1 year ago Sequence.php 2 years ago Session.php 1 year ago SettingsPiwik.php 1 year ago SettingsServer.php 1 year ago Singleton.php 2 years ago Site.php 1 year 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 1 year ago Twig.php 1 year 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 1 year ago UrlHelper.php 1 year ago Version.php 1 year ago View.php 1 year ago bootstrap.php 1 year ago dispatch.php 2 years ago testMinimumPhpVersion.php 2 years 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($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 $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