forked from vergnet/site-accueil-insa
464 lines
16 KiB
PHP
464 lines
16 KiB
PHP
<?php
|
|
/**
|
|
* Matomo - free/libre analytics platform
|
|
*
|
|
* @link https://matomo.org
|
|
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
|
*
|
|
*/
|
|
|
|
namespace Piwik;
|
|
|
|
use Piwik\Container\StaticContainer;
|
|
use Piwik\Plugins\CustomJsTracker\Exception\AccessDeniedException;
|
|
use Piwik\Plugins\CustomJsTracker\TrackerUpdater;
|
|
|
|
class FileIntegrity
|
|
{
|
|
|
|
/**
|
|
* Get file integrity information
|
|
*
|
|
* @return array(bool $success, array $messages)
|
|
*/
|
|
public static function getFileIntegrityInformation()
|
|
{
|
|
$messages = array();
|
|
|
|
$manifest = PIWIK_INCLUDE_PATH . '/config/manifest.inc.php';
|
|
|
|
if (file_exists($manifest)) {
|
|
require_once $manifest;
|
|
}
|
|
|
|
if (!class_exists('Piwik\\Manifest')) {
|
|
$messages[] = Piwik::translate('General_WarningFileIntegrityNoManifest')
|
|
. '<br/>'
|
|
. Piwik::translate('General_WarningFileIntegrityNoManifestDeployingFromGit');
|
|
|
|
return array(
|
|
$success = false,
|
|
$messages
|
|
);
|
|
}
|
|
|
|
|
|
$messages = self::getMessagesDirectoriesFoundButNotExpected($messages);
|
|
|
|
$messages = self::getMessagesFilesFoundButNotExpected($messages);
|
|
|
|
$messages = self::getMessagesFilesMismatch($messages);
|
|
|
|
return array(
|
|
$success = empty($messages),
|
|
$messages
|
|
);
|
|
}
|
|
|
|
protected static function getFilesNotInManifestButExpectedAnyway()
|
|
{
|
|
return StaticContainer::get('fileintegrity.ignore');
|
|
}
|
|
|
|
protected static function getMessagesDirectoriesFoundButNotExpected($messages)
|
|
{
|
|
$directoriesFoundButNotExpected = self::getDirectoriesFoundButNotExpected();
|
|
if (count($directoriesFoundButNotExpected) > 0) {
|
|
|
|
$messageDirectoriesToDelete = '';
|
|
foreach ($directoriesFoundButNotExpected as $directoryFoundNotExpected) {
|
|
$messageDirectoriesToDelete .= Piwik::translate('General_ExceptionDirectoryToDelete', htmlspecialchars($directoryFoundNotExpected)) . '<br/>';
|
|
}
|
|
|
|
$directories = array();
|
|
foreach ($directoriesFoundButNotExpected as $directoryFoundNotExpected) {
|
|
$directories[] = htmlspecialchars(realpath($directoryFoundNotExpected));
|
|
}
|
|
|
|
$deleteAllAtOnce = array();
|
|
$chunks = array_chunk($directories, 50);
|
|
|
|
$command = 'rm -Rf';
|
|
|
|
if (SettingsServer::isWindows()) {
|
|
$command = 'rmdir /s /q';
|
|
}
|
|
|
|
foreach ($chunks as $directories) {
|
|
$deleteAllAtOnce[] = sprintf('%s %s', $command, implode(' ', $directories));
|
|
}
|
|
|
|
$messages[] = Piwik::translate('General_ExceptionUnexpectedDirectory')
|
|
. '<br/>'
|
|
. '--> ' . Piwik::translate('General_ExceptionUnexpectedDirectoryPleaseDelete') . ' <--'
|
|
. '<br/><br/>'
|
|
. $messageDirectoriesToDelete
|
|
. '<br/><br/>'
|
|
. Piwik::translate('General_ToDeleteAllDirectoriesRunThisCommand')
|
|
. '<br/>'
|
|
. implode('<br />', $deleteAllAtOnce)
|
|
. '<br/><br/>';
|
|
|
|
}
|
|
|
|
return $messages;
|
|
}
|
|
|
|
/**
|
|
* @param $messages
|
|
* @return array
|
|
*/
|
|
protected static function getMessagesFilesFoundButNotExpected($messages)
|
|
{
|
|
$filesFoundButNotExpected = self::getFilesFoundButNotExpected();
|
|
if (count($filesFoundButNotExpected) > 0) {
|
|
|
|
$messageFilesToDelete = '';
|
|
foreach ($filesFoundButNotExpected as $fileFoundNotExpected) {
|
|
$messageFilesToDelete .= Piwik::translate('General_ExceptionFileToDelete', htmlspecialchars($fileFoundNotExpected)) . '<br/>';
|
|
}
|
|
|
|
$files = array();
|
|
foreach ($filesFoundButNotExpected as $fileFoundNotExpected) {
|
|
$files[] = '"' . htmlspecialchars(realpath($fileFoundNotExpected)) . '"';
|
|
}
|
|
|
|
$deleteAllAtOnce = array();
|
|
$chunks = array_chunk($files, 50);
|
|
|
|
$command = 'rm';
|
|
|
|
if (SettingsServer::isWindows()) {
|
|
$command = 'del';
|
|
}
|
|
|
|
foreach ($chunks as $files) {
|
|
$deleteAllAtOnce[] = sprintf('%s %s', $command, implode(' ', $files));
|
|
}
|
|
|
|
$messages[] = Piwik::translate('General_ExceptionUnexpectedFile')
|
|
. '<br/>'
|
|
. '--> ' . Piwik::translate('General_ExceptionUnexpectedFilePleaseDelete') . ' <--'
|
|
. '<br/><br/>'
|
|
. $messageFilesToDelete
|
|
. '<br/><br/>'
|
|
. Piwik::translate('General_ToDeleteAllFilesRunThisCommand')
|
|
. '<br/>'
|
|
. implode('<br />', $deleteAllAtOnce)
|
|
. '<br/><br/>';
|
|
|
|
return $messages;
|
|
|
|
}
|
|
return $messages;
|
|
}
|
|
|
|
/**
|
|
* Look for whole directories which are in the filesystem, but should not be
|
|
*
|
|
* @return array
|
|
*/
|
|
protected static function getDirectoriesFoundButNotExpected()
|
|
{
|
|
static $cache = null;
|
|
if(!is_null($cache)) {
|
|
return $cache;
|
|
}
|
|
|
|
$pluginsInManifest = self::getPluginsFoundInManifest();
|
|
$directoriesInManifest = self::getDirectoriesFoundInManifest();
|
|
$directoriesFoundButNotExpected = array();
|
|
|
|
foreach (self::getPathsToInvestigate() as $file) {
|
|
$file = substr($file, strlen(PIWIK_DOCUMENT_ROOT)); // remove piwik path to match format in manifest.inc.php
|
|
$file = ltrim($file, "\\/");
|
|
$directory = dirname($file);
|
|
|
|
if(in_array($directory, $directoriesInManifest)) {
|
|
continue;
|
|
}
|
|
|
|
if (self::isFileNotInManifestButExpectedAnyway($file)) {
|
|
continue;
|
|
}
|
|
if (self::isFileFromPluginNotInManifest($file, $pluginsInManifest)) {
|
|
continue;
|
|
}
|
|
|
|
if (!in_array($directory, $directoriesFoundButNotExpected)) {
|
|
$directoriesFoundButNotExpected[] = $directory;
|
|
}
|
|
}
|
|
|
|
$cache = self::getParentDirectoriesFromListOfDirectories($directoriesFoundButNotExpected);
|
|
return $cache;
|
|
}
|
|
/**
|
|
* Look for files which are in the filesystem, but should not be
|
|
*
|
|
* @return array
|
|
*/
|
|
protected static function getFilesFoundButNotExpected()
|
|
{
|
|
$files = \Piwik\Manifest::$files;
|
|
$pluginsInManifest = self::getPluginsFoundInManifest();
|
|
|
|
$filesFoundButNotExpected = array();
|
|
|
|
foreach (self::getPathsToInvestigate() as $file) {
|
|
if (is_dir($file)) {
|
|
continue;
|
|
}
|
|
$file = substr($file, strlen(PIWIK_DOCUMENT_ROOT)); // remove piwik path to match format in manifest.inc.php
|
|
$file = ltrim($file, "\\/");
|
|
|
|
if (self::isFileFromPluginNotInManifest($file, $pluginsInManifest)) {
|
|
continue;
|
|
}
|
|
if (self::isFileNotInManifestButExpectedAnyway($file)) {
|
|
continue;
|
|
}
|
|
if (self::isFileFromDirectoryThatShouldBeDeleted($file)) {
|
|
// we already report the directory as "Directory to delete" so no need to repeat the instruction for each file
|
|
continue;
|
|
}
|
|
|
|
if (!isset($files[$file])) {
|
|
$filesFoundButNotExpected[] = $file;
|
|
}
|
|
}
|
|
|
|
return $filesFoundButNotExpected;
|
|
}
|
|
|
|
|
|
protected static function isFileFromDirectoryThatShouldBeDeleted($file)
|
|
{
|
|
$directoriesWillBeDeleted = self::getDirectoriesFoundButNotExpected();
|
|
foreach($directoriesWillBeDeleted as $directoryWillBeDeleted) {
|
|
if(strpos($file, $directoryWillBeDeleted) === 0) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
protected static function getDirectoriesFoundInManifest()
|
|
{
|
|
$files = \Piwik\Manifest::$files;
|
|
|
|
$directories = array();
|
|
foreach($files as $file => $manifestIntegrityInfo) {
|
|
$directory = $file;
|
|
|
|
// add this directory and each parent directory
|
|
while( ($directory = dirname($directory)) && $directory != '.' && $directory != '/') {
|
|
$directories[] = $directory;
|
|
}
|
|
}
|
|
$directories = array_unique($directories);
|
|
return $directories;
|
|
}
|
|
|
|
protected static function getPluginsFoundInManifest()
|
|
{
|
|
$files = \Piwik\Manifest::$files;
|
|
|
|
$pluginsInManifest = array();
|
|
foreach($files as $file => $manifestIntegrityInfo) {
|
|
if(strpos($file, 'plugins/') === 0) {
|
|
$pluginName = self::getPluginNameFromFilepath($file);
|
|
$pluginsInManifest[] = $pluginName;
|
|
}
|
|
}
|
|
return $pluginsInManifest;
|
|
}
|
|
|
|
/**
|
|
* If a plugin folder is not tracked in the manifest then we don't try to report any files in this folder
|
|
* Could be a third party plugin or any plugin from the Marketplace
|
|
*
|
|
* @param $file
|
|
* @param $pluginsInManifest
|
|
* @return bool
|
|
*/
|
|
protected static function isFileFromPluginNotInManifest($file, $pluginsInManifest)
|
|
{
|
|
if (strpos($file, 'plugins/') !== 0) {
|
|
return false;
|
|
}
|
|
|
|
if (substr_count($file, '/') < 2) {
|
|
// must be a file plugins/abc.xyz and not a plugin directory
|
|
return false;
|
|
}
|
|
|
|
$pluginName = self::getPluginNameFromFilepath($file);
|
|
if(in_array($pluginName, $pluginsInManifest)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
protected static function isFileNotInManifestButExpectedAnyway($file)
|
|
{
|
|
$expected = self::getFilesNotInManifestButExpectedAnyway();
|
|
foreach ($expected as $expectedPattern) {
|
|
if (fnmatch($expectedPattern, $file, defined('FNM_CASEFOLD') ? FNM_CASEFOLD : 0)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
protected static function getMessagesFilesMismatch($messages)
|
|
{
|
|
$messagesMismatch = array();
|
|
$hasMd5file = function_exists('md5_file');
|
|
$files = \Piwik\Manifest::$files;
|
|
$hasMd5 = function_exists('md5');
|
|
foreach ($files as $path => $props) {
|
|
$file = PIWIK_INCLUDE_PATH . '/' . $path;
|
|
|
|
if (!file_exists($file) || !is_readable($file)) {
|
|
$messagesMismatch[] = Piwik::translate('General_ExceptionMissingFile', $file);
|
|
} elseif (filesize($file) != $props[0]) {
|
|
|
|
if (self::isModifiedPathValid($path)) {
|
|
continue;
|
|
}
|
|
|
|
if (!$hasMd5 || in_array(substr($path, -4), array('.gif', '.ico', '.jpg', '.png', '.swf'))) {
|
|
// files that contain binary data (e.g., images) must match the file size
|
|
$messagesMismatch[] = Piwik::translate('General_ExceptionFilesizeMismatch', array($file, $props[0], filesize($file)));
|
|
} else {
|
|
// convert end-of-line characters and re-test text files
|
|
$content = @file_get_contents($file);
|
|
$content = str_replace("\r\n", "\n", $content);
|
|
if ((strlen($content) != $props[0])
|
|
|| (@md5($content) !== $props[1])
|
|
) {
|
|
$messagesMismatch[] = Piwik::translate('General_ExceptionFilesizeMismatch', array($file, $props[0], filesize($file)));
|
|
}
|
|
}
|
|
} elseif ($hasMd5file && (@md5_file($file) !== $props[1])) {
|
|
if (self::isModifiedPathValid($path)) {
|
|
continue;
|
|
}
|
|
|
|
$messagesMismatch[] = Piwik::translate('General_ExceptionFileIntegrity', $file);
|
|
}
|
|
}
|
|
|
|
if (!$hasMd5file) {
|
|
$messages[] = Piwik::translate('General_WarningFileIntegrityNoMd5file');
|
|
}
|
|
|
|
if (!empty($messagesMismatch)) {
|
|
$messages[] = Piwik::translate('General_FileIntegrityWarningReupload');
|
|
$messages[] = '--> ' . Piwik::translate('General_FileIntegrityWarningReuploadBis') . ' <--<br/>';
|
|
$messages = array_merge($messages, $messagesMismatch);
|
|
}
|
|
|
|
return $messages;
|
|
}
|
|
|
|
protected static function isModifiedPathValid($path)
|
|
{
|
|
if ($path === 'piwik.js' || $path === 'matomo.js') {
|
|
// we could have used a postEvent hook to enrich "\Piwik\Manifest::$files;" which would also benefit plugins
|
|
// that want to check for file integrity but we do not want to risk to break anything right now. It is not
|
|
// as trivial because piwik.js might be already updated, or updated on the next request. We cannot define
|
|
// 2 or 3 different filesizes and md5 hashes for one file so we check it here.
|
|
|
|
if (Plugin\Manager::getInstance()->isPluginActivated('CustomJsTracker')) {
|
|
$trackerUpdater = new TrackerUpdater();
|
|
|
|
if ($trackerUpdater->getCurrentTrackerFileContent() === $trackerUpdater->getUpdatedTrackerFileContent()) {
|
|
// file was already updated, eg manually or via custom piwik.js, this is a valid piwik.js file as
|
|
// it was enriched by tracker plugins
|
|
return true;
|
|
}
|
|
|
|
try {
|
|
// the piwik.js tracker file was not updated yet, but may be updated just after the update by
|
|
// one of the events CustomJsTracker is listening to or by a scheduled task.
|
|
// In this case, we check whether such an update will succeed later and if it will, the file is
|
|
// valid as well as it will be updated on the next request
|
|
$trackerUpdater->checkWillSucceed();
|
|
return true;
|
|
} catch (AccessDeniedException $e) {
|
|
return false;
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
protected static function getPluginNameFromFilepath($file)
|
|
{
|
|
$pathRelativeToPlugins = substr($file, strlen('plugins/'));
|
|
$pluginName = substr($pathRelativeToPlugins, 0, strpos($pathRelativeToPlugins, '/'));
|
|
return $pluginName;
|
|
}
|
|
|
|
/**
|
|
* @return array
|
|
*/
|
|
protected static function getPathsToInvestigate()
|
|
{
|
|
$filesToInvestigate = array_merge(
|
|
// all normal files
|
|
Filesystem::globr(PIWIK_DOCUMENT_ROOT, '*'),
|
|
// all hidden files
|
|
Filesystem::globr(PIWIK_DOCUMENT_ROOT, '.*')
|
|
);
|
|
return $filesToInvestigate;
|
|
}
|
|
|
|
/**
|
|
* @param $directoriesFoundButNotExpected
|
|
* @return array
|
|
*/
|
|
protected static function getParentDirectoriesFromListOfDirectories($directoriesFoundButNotExpected)
|
|
{
|
|
sort($directoriesFoundButNotExpected);
|
|
|
|
$parentDirectoriesOnly = array();
|
|
foreach ($directoriesFoundButNotExpected as $directory) {
|
|
$directoryParent = self::getDirectoryParentFromList($directory, $directoriesFoundButNotExpected);
|
|
if($directoryParent) {
|
|
$parentDirectoriesOnly[] = $directoryParent;
|
|
}
|
|
}
|
|
$parentDirectoriesOnly = array_unique($parentDirectoriesOnly);
|
|
|
|
return $parentDirectoriesOnly;
|
|
}
|
|
|
|
/**
|
|
* When the parent directory of $directory is found within $directories, return it.
|
|
*
|
|
* @param $directory
|
|
* @param $directories
|
|
* @return string
|
|
*/
|
|
protected static function getDirectoryParentFromList($directory, $directories)
|
|
{
|
|
foreach($directories as $directoryMaybeParent) {
|
|
if ($directory == $directoryMaybeParent) {
|
|
continue;
|
|
}
|
|
|
|
$isParentDirectory = strpos($directory, $directoryMaybeParent) === 0;
|
|
if ($isParentDirectory) {
|
|
return $directoryMaybeParent;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
}
|