PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.0.7
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.0.7
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
381 lines
1 <?php
2
3 /**
4 * Matomo - free/libre analytics platform
5 *
6 * @link https://matomo.org
7 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
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 class FileIntegrity
16 {
17 /**
18 * Get file integrity information
19 *
20 * @return array(bool $success, array $messages)
21 */
22 public static function getFileIntegrityInformation()
23 {
24 $messages = array();
25 self::loadManifest();
26 if (!class_exists('Piwik\\Manifest')) {
27 $messages[] = \Piwik\Piwik::translate('General_WarningFileIntegrityNoManifest') . '<br/>' . \Piwik\Piwik::translate('General_WarningFileIntegrityNoManifestDeployingFromGit');
28 return array($success = false, $messages);
29 }
30 $messages = self::getMessagesDirectoriesFoundButNotExpected($messages);
31 $messages = self::getMessagesFilesFoundButNotExpected($messages);
32 $messages = self::getMessagesFilesMismatch($messages);
33 return array($success = empty($messages), $messages);
34 }
35 /**
36 * Return just a list of the unexpected files
37 *
38 * @return array
39 */
40 public static function getUnexpectedFilesList() : array
41 {
42 self::loadManifest();
43 $files = self::getFilesFoundButNotExpected();
44 return $files;
45 }
46 /**
47 * Include the manifest
48 *
49 * @return void
50 */
51 private static function loadManifest() : void
52 {
53 $manifest = PIWIK_INCLUDE_PATH . '/config/manifest.inc.php';
54 if (file_exists($manifest)) {
55 require_once $manifest;
56 }
57 }
58 protected static function getFilesNotInManifestButExpectedAnyway()
59 {
60 return StaticContainer::get('fileintegrity.ignore');
61 }
62 protected static function getMessagesDirectoriesFoundButNotExpected($messages)
63 {
64 $directoriesFoundButNotExpected = self::getDirectoriesFoundButNotExpected();
65 if (count($directoriesFoundButNotExpected) > 0) {
66 $messageDirectoriesToDelete = '';
67 foreach ($directoriesFoundButNotExpected as $directoryFoundNotExpected) {
68 $messageDirectoriesToDelete .= \Piwik\Piwik::translate('General_ExceptionDirectoryToDelete', htmlspecialchars($directoryFoundNotExpected)) . '<br/>';
69 }
70 $directories = array();
71 foreach ($directoriesFoundButNotExpected as $directoryFoundNotExpected) {
72 $directories[] = htmlspecialchars(realpath($directoryFoundNotExpected));
73 }
74 $deleteAllAtOnce = array();
75 $chunks = array_chunk($directories, 50);
76 $command = 'rm -Rf';
77 if (\Piwik\SettingsServer::isWindows()) {
78 $command = 'rmdir /s /q';
79 }
80 foreach ($chunks as $directories) {
81 $deleteAllAtOnce[] = sprintf('%s %s', $command, implode(' ', $directories));
82 }
83 $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/>';
84 }
85 return $messages;
86 }
87 /**
88 * @param $messages
89 * @return array
90 */
91 protected static function getMessagesFilesFoundButNotExpected($messages)
92 {
93 $filesFoundButNotExpected = self::getFilesFoundButNotExpected();
94 if (count($filesFoundButNotExpected) > 0) {
95 $messageFilesToDelete = '';
96 foreach ($filesFoundButNotExpected as $fileFoundNotExpected) {
97 $messageFilesToDelete .= \Piwik\Piwik::translate('General_ExceptionFileToDelete', htmlspecialchars($fileFoundNotExpected)) . '<br/>';
98 }
99 $files = array();
100 foreach ($filesFoundButNotExpected as $fileFoundNotExpected) {
101 $files[] = '"' . htmlspecialchars(realpath($fileFoundNotExpected)) . '"';
102 }
103 $deleteAllAtOnce = array();
104 $chunks = array_chunk($files, 50);
105 $command = 'rm';
106 if (\Piwik\SettingsServer::isWindows()) {
107 $command = 'del';
108 }
109 foreach ($chunks as $files) {
110 $deleteAllAtOnce[] = sprintf('%s %s', $command, implode(' ', $files));
111 }
112 $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/>';
113 return $messages;
114 }
115 return $messages;
116 }
117 /**
118 * Look for whole directories which are in the filesystem, but should not be
119 *
120 * @return array
121 */
122 protected static function getDirectoriesFoundButNotExpected()
123 {
124 static $cache = null;
125 if (!is_null($cache)) {
126 return $cache;
127 }
128 $pluginsInManifest = self::getPluginsFoundInManifest();
129 $directoriesInManifest = self::getDirectoriesFoundInManifest();
130 $directoriesFoundButNotExpected = array();
131 foreach (self::getPathsToInvestigate() as $file) {
132 $file = substr($file, strlen(PIWIK_DOCUMENT_ROOT));
133 // remove piwik path to match format in manifest.inc.php
134 $file = ltrim($file, "\\/");
135 $directory = dirname($file);
136 if (in_array($directory, $directoriesInManifest)) {
137 continue;
138 }
139 if (self::isFileNotInManifestButExpectedAnyway($file)) {
140 continue;
141 }
142 if (self::isFileFromPluginNotInManifest($file, $pluginsInManifest)) {
143 continue;
144 }
145 if (!in_array($directory, $directoriesFoundButNotExpected)) {
146 $directoriesFoundButNotExpected[] = $directory;
147 }
148 }
149 $cache = self::getParentDirectoriesFromListOfDirectories($directoriesFoundButNotExpected);
150 return $cache;
151 }
152 /**
153 * Look for files which are in the filesystem, but should not be
154 *
155 * @return array
156 */
157 protected static function getFilesFoundButNotExpected()
158 {
159 $files = \Piwik\Manifest::$files;
160 $pluginsInManifest = self::getPluginsFoundInManifest();
161 $filesFoundButNotExpected = array();
162 foreach (self::getPathsToInvestigate() as $file) {
163 if (is_dir($file)) {
164 continue;
165 }
166 $file = substr($file, strlen(PIWIK_DOCUMENT_ROOT));
167 // remove piwik path to match format in manifest.inc.php
168 $file = ltrim($file, "\\/");
169 if (self::isFileFromPluginNotInManifest($file, $pluginsInManifest)) {
170 continue;
171 }
172 if (self::isFileNotInManifestButExpectedAnyway($file)) {
173 continue;
174 }
175 if (self::isFileFromDirectoryThatShouldBeDeleted($file)) {
176 // we already report the directory as "Directory to delete" so no need to repeat the instruction for each file
177 continue;
178 }
179 if (!isset($files[$file])) {
180 $filesFoundButNotExpected[] = $file;
181 }
182 }
183 return $filesFoundButNotExpected;
184 }
185 protected static function isFileFromDirectoryThatShouldBeDeleted($file)
186 {
187 $directoriesWillBeDeleted = self::getDirectoriesFoundButNotExpected();
188 foreach ($directoriesWillBeDeleted as $directoryWillBeDeleted) {
189 if (strpos($file, $directoryWillBeDeleted) === 0) {
190 return true;
191 }
192 }
193 return false;
194 }
195 protected static function getDirectoriesFoundInManifest()
196 {
197 $files = \Piwik\Manifest::$files;
198 $directories = array();
199 foreach ($files as $file => $manifestIntegrityInfo) {
200 $directory = $file;
201 // add this directory and each parent directory
202 while (($directory = dirname($directory)) && $directory != '.' && $directory != '/') {
203 $directories[] = $directory;
204 }
205 }
206 $directories = array_unique($directories);
207 return $directories;
208 }
209 protected static function getPluginsFoundInManifest()
210 {
211 $files = \Piwik\Manifest::$files;
212 $pluginsInManifest = array();
213 foreach ($files as $file => $manifestIntegrityInfo) {
214 if (strpos($file, 'plugins/') === 0) {
215 $pluginName = self::getPluginNameFromFilepath($file);
216 $pluginsInManifest[] = $pluginName;
217 }
218 }
219 return $pluginsInManifest;
220 }
221 /**
222 * If a plugin folder is not tracked in the manifest then we don't try to report any files in this folder
223 * Could be a third party plugin or any plugin from the Marketplace
224 *
225 * @param $file
226 * @param $pluginsInManifest
227 * @return bool
228 */
229 protected static function isFileFromPluginNotInManifest($file, $pluginsInManifest)
230 {
231 if (strpos($file, 'plugins/') !== 0) {
232 return false;
233 }
234 if (substr_count($file, '/') < 2) {
235 // must be a file plugins/abc.xyz and not a plugin directory
236 return false;
237 }
238 $pluginName = self::getPluginNameFromFilepath($file);
239 if (in_array($pluginName, $pluginsInManifest)) {
240 return false;
241 }
242 return true;
243 }
244 protected static function isFileNotInManifestButExpectedAnyway($file)
245 {
246 $expected = self::getFilesNotInManifestButExpectedAnyway();
247 foreach ($expected as $expectedPattern) {
248 if (fnmatch($expectedPattern, $file, defined('FNM_CASEFOLD') ? FNM_CASEFOLD : 0)) {
249 return true;
250 }
251 }
252 return false;
253 }
254 protected static function getMessagesFilesMismatch($messages)
255 {
256 $messagesMismatch = array();
257 $hasMd5file = function_exists('md5_file');
258 $files = \Piwik\Manifest::$files;
259 $hasMd5 = function_exists('md5');
260 foreach ($files as $path => $props) {
261 $file = PIWIK_INCLUDE_PATH . '/' . $path;
262 if (!file_exists($file) || !is_readable($file)) {
263 $messagesMismatch[] = \Piwik\Piwik::translate('General_ExceptionMissingFile', $file);
264 } elseif (filesize($file) != $props[0]) {
265 if (self::isModifiedPathValid($path)) {
266 continue;
267 }
268 if (!$hasMd5 || in_array(substr($path, -4), array('.gif', '.ico', '.jpg', '.png', '.swf'))) {
269 // files that contain binary data (e.g., images) must match the file size
270 $messagesMismatch[] = \Piwik\Piwik::translate('General_ExceptionFilesizeMismatch', array($file, $props[0], filesize($file)));
271 } else {
272 // convert end-of-line characters and re-test text files
273 $content = @file_get_contents($file);
274 $content = str_replace("\r\n", "\n", $content);
275 if (strlen($content) != $props[0] || @md5($content) !== $props[1]) {
276 $messagesMismatch[] = \Piwik\Piwik::translate('General_ExceptionFilesizeMismatch', array($file, $props[0], filesize($file)));
277 }
278 }
279 } elseif ($hasMd5file && @md5_file($file) !== $props[1]) {
280 if (self::isModifiedPathValid($path)) {
281 continue;
282 }
283 $messagesMismatch[] = \Piwik\Piwik::translate('General_ExceptionFileIntegrity', $file);
284 }
285 }
286 if (!$hasMd5file) {
287 $messages[] = \Piwik\Piwik::translate('General_WarningFileIntegrityNoMd5file');
288 }
289 if (!empty($messagesMismatch)) {
290 $messages[] = \Piwik\Piwik::translate('General_FileIntegrityWarningReupload');
291 $messages[] = '--> ' . \Piwik\Piwik::translate('General_FileIntegrityWarningReuploadBis') . ' <--<br/>';
292 $messages = array_merge($messages, $messagesMismatch);
293 }
294 return $messages;
295 }
296 protected static function isModifiedPathValid($path)
297 {
298 if ($path === 'piwik.js' || $path === 'matomo.js') {
299 // we could have used a postEvent hook to enrich "\Piwik\Manifest::$files;" which would also benefit plugins
300 // that want to check for file integrity but we do not want to risk to break anything right now. It is not
301 // as trivial because piwik.js might be already updated, or updated on the next request. We cannot define
302 // 2 or 3 different filesizes and md5 hashes for one file so we check it here.
303 if (\Piwik\Plugin\Manager::getInstance()->isPluginActivated('CustomJsTracker')) {
304 $trackerUpdater = new TrackerUpdater();
305 if ($trackerUpdater->getCurrentTrackerFileContent() === $trackerUpdater->getUpdatedTrackerFileContent()) {
306 // file was already updated, eg manually or via custom piwik.js, this is a valid piwik.js file as
307 // it was enriched by tracker plugins
308 return true;
309 }
310 try {
311 // the piwik.js tracker file was not updated yet, but may be updated just after the update by
312 // one of the events CustomJsTracker is listening to or by a scheduled task.
313 // In this case, we check whether such an update will succeed later and if it will, the file is
314 // valid as well as it will be updated on the next request
315 $trackerUpdater->checkWillSucceed();
316 return true;
317 } catch (AccessDeniedException $e) {
318 return false;
319 }
320 }
321 }
322 return false;
323 }
324 protected static function getPluginNameFromFilepath($file)
325 {
326 $pathRelativeToPlugins = substr($file, strlen('plugins/'));
327 $pluginName = substr($pathRelativeToPlugins, 0, strpos($pathRelativeToPlugins, '/'));
328 return $pluginName;
329 }
330 /**
331 * @return array
332 */
333 protected static function getPathsToInvestigate()
334 {
335 $filesToInvestigate = array_merge(
336 // all normal files
337 \Piwik\Filesystem::globr(PIWIK_DOCUMENT_ROOT, '*'),
338 // all hidden files
339 \Piwik\Filesystem::globr(PIWIK_DOCUMENT_ROOT, '.*')
340 );
341 return $filesToInvestigate;
342 }
343 /**
344 * @param $directoriesFoundButNotExpected
345 * @return array
346 */
347 protected static function getParentDirectoriesFromListOfDirectories($directoriesFoundButNotExpected)
348 {
349 sort($directoriesFoundButNotExpected);
350 $parentDirectoriesOnly = array();
351 foreach ($directoriesFoundButNotExpected as $directory) {
352 $directoryParent = self::getDirectoryParentFromList($directory, $directoriesFoundButNotExpected);
353 if ($directoryParent) {
354 $parentDirectoriesOnly[] = $directoryParent;
355 }
356 }
357 $parentDirectoriesOnly = array_unique($parentDirectoriesOnly);
358 return $parentDirectoriesOnly;
359 }
360 /**
361 * When the parent directory of $directory is found within $directories, return it.
362 *
363 * @param $directory
364 * @param $directories
365 * @return string
366 */
367 protected static function getDirectoryParentFromList($directory, $directories)
368 {
369 foreach ($directories as $directoryMaybeParent) {
370 if ($directory == $directoryMaybeParent) {
371 continue;
372 }
373 $isParentDirectory = strpos($directory, $directoryMaybeParent) === 0;
374 if ($isParentDirectory) {
375 return $directoryMaybeParent;
376 }
377 }
378 return null;
379 }
380 }
381