site-accueil-insa/matomo/core/Filesystem.php

601 lines
19 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\Exception\FailedCopyException;
use Piwik\Tracker\Cache as TrackerCache;
use Piwik\Cache as PiwikCache;
use Piwik\Exception\Exception;
/**
* Contains helper functions that deal with the filesystem.
*
*/
class Filesystem
{
/**
* @var bool
* @internal
*/
public static $skipCacheClearOnUpdate = false;
/**
* Called on Core install, update, plugin enable/disable
* Will clear all cache that could be affected by the change in configuration being made
*/
public static function deleteAllCacheOnUpdate($pluginName = false)
{
if (self::$skipCacheClearOnUpdate) {
return;
}
AssetManager::getInstance()->removeMergedAssets($pluginName);
View::clearCompiledTemplates();
TrackerCache::deleteTrackerCache();
PiwikCache::flushAll();
self::clearPhpCaches();
$pluginManager = Plugin\Manager::getInstance();
$plugins = $pluginManager->getLoadedPlugins();
foreach ($plugins as $plugin) {
$plugin->reloadPluginInformation();
}
/**
* Triggered after all non-memory caches are cleared (eg, via the cache:clear
* command).
*/
Piwik::postEvent('Filesystem.allCachesCleared');
}
/**
* ending WITHOUT slash
*
* @return string
*/
public static function getPathToPiwikRoot()
{
return realpath(dirname(__FILE__) . "/..");
}
/**
* Returns true if the string is a valid filename
* File names that start with a-Z or 0-9 and contain a-Z, 0-9, underscore(_), dash(-), and dot(.) will be accepted.
* File names beginning with anything but a-Z or 0-9 will be rejected (including .htaccess for example).
* File names containing anything other than above mentioned will also be rejected (file names with spaces won't be accepted).
*
* @param string $filename
* @return bool
*/
public static function isValidFilename($filename)
{
return (0 !== preg_match('/(^[a-zA-Z0-9]+([a-zA-Z_0-9.-]*))$/D', $filename));
}
/**
* Get canonicalized absolute path
* See http://php.net/realpath
*
* @param string $path
* @return string canonicalized absolute path
*/
public static function realpath($path)
{
if (file_exists($path)) {
return realpath($path);
}
return $path;
}
/**
* Attempts to create a new directory. All errors are silenced.
*
* _Note: This function does **not** create directories recursively._
*
* @param string $path The path of the directory to create.
* @api
*/
public static function mkdir($path)
{
if (!is_dir($path)) {
// the mode in mkdir is modified by the current umask
@mkdir($path, self::getChmodForPath($path), $recursive = true);
}
// try to overcome restrictive umask (mis-)configuration
if (!is_writable($path)) {
@chmod($path, 0755);
if (!is_writable($path)) {
@chmod($path, 0775);
// enough! we're not going to make the directory world-writeable
}
}
self::createIndexFilesToPreventDirectoryListing($path);
}
/**
* Checks if the filesystem Piwik stores sessions in is NFS or not. This
* check is done in order to avoid using file based sessions on NFS system,
* since on such a filesystem file locking can make file based sessions
* incredibly slow.
*
* Note: In order to figure this out, we try to run the 'df' program. If
* the 'exec' or 'shell_exec' functions are not available, we can't do
* the check.
*
* @return bool True if on an NFS filesystem, false if otherwise or if we
* can't use shell_exec or exec.
*/
public static function checkIfFileSystemIsNFS()
{
$sessionsPath = Session::getSessionsDirectory();
// this command will display details for the filesystem that holds the $sessionsPath
// path, but only if its type is NFS. if not NFS, df will return one or less lines
// and the return code 1. if NFS, it will return 0 and at least 2 lines of text.
$command = "df -T -t nfs \"$sessionsPath\" 2>&1";
if (function_exists('exec')) {
// use exec
$output = $returnCode = null;
@exec($command, $output, $returnCode);
// check if filesystem is NFS
if ($returnCode == 0
&& count($output) > 1
&& preg_match('/\bnfs\d?\b/', implode("\n", $output))
) {
return true;
}
} elseif (function_exists('shell_exec')) {
// use shell_exec
$output = @shell_exec($command);
if ($output) {
$commandFailed = (false !== strpos($output, "no file systems processed"));
$output = trim($output);
$outputArray = explode("\n", $output);
if (!$commandFailed
&& count($outputArray) > 1
&& preg_match('/\bnfs\d?\b/', $output)) {
// check if filesystem is NFS
return true;
}
}
}
return false; // not NFS, or we can't run a program to find out
}
/**
* Recursively find pathnames that match a pattern.
*
* See {@link http://php.net/manual/en/function.glob.php glob} for more info.
*
* @param string $sDir directory The directory to glob in.
* @param string $sPattern pattern The pattern to match paths against.
* @param int $nFlags `glob()` . See {@link http://php.net/manual/en/function.glob.php glob()}.
* @return array The list of paths that match the pattern.
* @api
*/
public static function globr($sDir, $sPattern, $nFlags = 0)
{
if (($aFiles = \_glob("$sDir/$sPattern", $nFlags)) == false) {
$aFiles = array();
}
if (($aDirs = \_glob("$sDir/*", GLOB_ONLYDIR)) != false) {
foreach ($aDirs as $sSubDir) {
if (is_link($sSubDir)) {
continue;
}
$aSubFiles = self::globr($sSubDir, $sPattern, $nFlags);
$aFiles = array_merge($aFiles, $aSubFiles);
}
}
sort($aFiles);
return $aFiles;
}
/**
* Recursively deletes a directory.
*
* @param string $dir Path of the directory to delete.
* @param boolean $deleteRootToo If true, `$dir` is deleted, otherwise just its contents.
* @param \Closure|false $beforeUnlink An optional closure to execute on a file path before unlinking.
* @api
*/
public static function unlinkRecursive($dir, $deleteRootToo, \Closure $beforeUnlink = null)
{
if (!$dh = @opendir($dir)) {
return;
}
while (false !== ($obj = readdir($dh))) {
if ($obj == '.' || $obj == '..') {
continue;
}
$path = $dir . '/' . $obj;
if ($beforeUnlink) {
$beforeUnlink($path);
}
if (!@unlink($path)) {
self::unlinkRecursive($path, true);
}
}
closedir($dh);
if ($deleteRootToo) {
@rmdir($dir);
}
}
/**
* Removes all files and directories that are present in the target directory but are not in the source directory.
*
* @param string $source Path to the source directory
* @param string $target Path to the target
*/
public static function unlinkTargetFilesNotPresentInSource($source, $target)
{
$diff = self::directoryDiff($source, $target);
$diff = self::sortFilesDescByPathLength($diff);
foreach ($diff as $file) {
$remove = $target . $file;
if (is_dir($remove)) {
@rmdir($remove);
} else {
self::deleteFileIfExists($remove);
}
}
}
/**
* Sort all given paths/filenames by its path length. Long path names will be listed first. This method can be
* useful if you have for instance a bunch of files/directories to delete. By sorting them by length you can make
* sure to delete all files within the folders before deleting the actual folder.
*
* @param string[] $files
* @return string[]
*/
public static function sortFilesDescByPathLength($files)
{
usort($files, function ($a, $b) {
// sort by filename length so we kinda make sure to remove files before its directories
if ($a == $b) {
return 0;
}
return (strlen($a) > strlen($b) ? -1 : 1);
});
return $files;
}
/**
* Computes the difference of directories. Compares $target against $source and returns a relative path to all files
* and directories in $target that are not present in $source.
*
* @param $source
* @param $target
*
* @return string[]
*/
public static function directoryDiff($source, $target)
{
$flags = 0;
$pattern = '*';
if (defined('GLOB_BRACE')) {
// The GLOB_BRACE flag is not available on some non GNU systems, like Solaris or Alpine Linux.
$flags = GLOB_BRACE;
$pattern = '{,.}*[!.]*'; // matches all files and folders, including those starting with ".", but excludes "." and ".."
}
$sourceFiles = self::globr($source, $pattern, $flags);
$targetFiles = self::globr($target, $pattern, $flags);
$sourceFiles = array_map(function ($file) use ($source) {
return str_replace($source, '', $file);
}, $sourceFiles);
$targetFiles = array_map(function ($file) use ($target) {
return str_replace($target, '', $file);
}, $targetFiles);
if (FileSystem::isFileSystemCaseInsensitive()) {
$diff = array_udiff($targetFiles, $sourceFiles, 'strcasecmp');
} else {
$diff = array_diff($targetFiles, $sourceFiles);
}
return array_values($diff);
}
/**
* Copies a file from `$source` to `$dest`.
*
* @param string $source A path to a file, eg. './tmp/latest/index.php'. The file must exist.
* @param string $dest A path to a file, eg. './index.php'. The file does not have to exist.
* @param bool $excludePhp Whether to avoid copying files if the file is related to PHP
* (includes .php, .tpl, .twig files).
* @throws Exception If the file cannot be copied.
* @return true
* @api
*/
public static function copy($source, $dest, $excludePhp = false)
{
if ($excludePhp) {
if (self::hasPHPExtension($source)) {
return true;
}
}
$success = self::tryToCopyFileAndVerifyItWasCopied($source, $dest);
if (!$success) {
$success = self::tryToCopyFileAndVerifyItWasCopied($source, $dest);
}
if (!$success) {
$ex = new FailedCopyException("Error while creating/copying file from $source to <code>" . Common::sanitizeInputValue($dest)
. "</code>. Content of copied file is different.");
$ex->setIsHtmlMessage();
throw $ex;
}
return true;
}
private static function hasPHPExtension($file)
{
static $phpExtensions = array('php', 'tpl', 'twig');
$path_parts = pathinfo($file);
if (!empty($path_parts['extension'])
&& in_array($path_parts['extension'], $phpExtensions)) {
return true;
}
return false;
}
/**
* Copies the contents of a directory recursively from `$source` to `$target`.
*
* @param string $source A directory or file to copy, eg. './tmp/latest'.
* @param string $target A directory to copy to, eg. '.'.
* @param bool $excludePhp Whether to avoid copying files if the file is related to PHP
* (includes .php, .tpl, .twig files).
* @throws Exception If a file cannot be copied.
* @api
*/
public static function copyRecursive($source, $target, $excludePhp = false)
{
if (is_dir($source)) {
self::mkdir($target);
$d = dir($source);
while (false !== ($entry = $d->read())) {
if ($entry == '.' || $entry == '..') {
continue;
}
$sourcePath = $source . '/' . $entry;
if (is_dir($sourcePath)) {
self::copyRecursive($sourcePath, $target . '/' . $entry, $excludePhp);
continue;
}
$destPath = $target . '/' . $entry;
self::copy($sourcePath, $destPath, $excludePhp);
}
$d->close();
} else {
self::copy($source, $target, $excludePhp);
}
}
/**
* Deletes the given file if it exists.
*
* @param string $pathToFile
* @return bool true in case of success or if file does not exist, false otherwise. It might fail in case the
* file is not writeable.
* @api
*/
public static function deleteFileIfExists($pathToFile)
{
if (!file_exists($pathToFile)) {
return true;
}
return @unlink($pathToFile);
}
/**
* Get the size of a file in the specified unit.
*
* @param string $pathToFile
* @param string $unit eg 'B' for Byte, 'KB', 'MB', 'GB', 'TB'.
*
* @return float|null Returns null if file does not exist or the size of the file in the specified unit
*
* @throws Exception In case the unit is invalid
*/
public static function getFileSize($pathToFile, $unit = 'B')
{
$unit = strtoupper($unit);
$units = array('TB' => pow(1024, 4),
'GB' => pow(1024, 3),
'MB' => pow(1024, 2),
'KB' => 1024,
'B' => 1);
if (!array_key_exists($unit, $units)) {
throw new \Exception('Invalid unit given');
}
if (!file_exists($pathToFile)) {
return;
}
$filesize = filesize($pathToFile);
$factor = $units[$unit];
$converted = $filesize / $factor;
return $converted;
}
/**
* Remove a file.
*
* @param string $file
* @param bool $silenceErrors If true, no exception will be thrown in case removing fails.
*/
public static function remove($file, $silenceErrors = false)
{
if (!file_exists($file)) {
return;
}
$result = @unlink($file);
// Testing if the file still exist avoids race conditions
if (!$result && file_exists($file)) {
if ($silenceErrors) {
Log::warning('Failed to delete file ' . $file);
} else {
throw new \RuntimeException('Unable to delete file ' . $file);
}
}
}
/**
* @param $path
* @return int
*/
private static function getChmodForPath($path)
{
if (self::isPathWithinTmpFolder($path)) {
// tmp/* folder
return 0750;
}
// plugins/* and all others
return 0755;
}
public static function clearPhpCaches()
{
if (function_exists('apc_clear_cache')) {
apc_clear_cache(); // clear the system (aka 'opcode') cache
}
if (function_exists('opcache_reset')) {
@opcache_reset(); // reset the opcode cache (php 5.5.0+)
}
if (function_exists('wincache_refresh_if_changed')) {
@wincache_refresh_if_changed(); // reset the wincache
}
if (function_exists('xcache_clear_cache') && defined('XC_TYPE_VAR')) {
if (ini_get('xcache.admin.enable_auth')) {
// XCache will not be cleared because "xcache.admin.enable_auth" is enabled in php.ini.
} else {
@xcache_clear_cache(XC_TYPE_VAR);
}
}
}
private static function havePhpFilesSameContent($file1, $file2)
{
if (self::hasPHPExtension($file1)) {
$sourceMd5 = md5_file($file1);
$destMd5 = md5_file($file2);
return $sourceMd5 === $destMd5;
}
return true;
}
private static function tryToCopyFileAndVerifyItWasCopied($source, $dest)
{
if (!@copy($source, $dest)) {
@chmod($dest, 0755);
if (!@copy($source, $dest)) {
$message = "Error while creating/copying file to <code>" . Common::sanitizeInputValue($dest) . "</code>. <br />"
. Filechecks::getErrorMessageMissingPermissions(self::getPathToPiwikRoot());
$ex = new FailedCopyException($message);
$ex->setIsHtmlMessage();
throw $ex;
}
}
if (file_exists($source) && file_exists($dest)) {
return self::havePhpFilesSameContent($source, $dest);
}
return true;
}
/**
* @param $path
* @return bool
*/
private static function isPathWithinTmpFolder($path)
{
$pathIsTmp = StaticContainer::get('path.tmp');
$isPathWithinTmpFolder = strpos($path, $pathIsTmp) === 0;
return $isPathWithinTmpFolder;
}
/**
* Check if the filesystem is case sensitive by writing a temporary file
*
* @return bool
*/
public static function isFileSystemCaseInsensitive() : bool
{
$testFileName = 'caseSensitivityTest.txt';
$pathTmp = StaticContainer::get('path.tmp');
@file_put_contents($pathTmp.'/'.$testFileName, 'Nothing to see here.');
if (\file_exists($pathTmp.'/'.strtolower($testFileName))) {
// Wrote caseSensitivityTest.txt but casesensitivitytest.txt exists, so case insensitive
return true;
}
return false;
}
/**
* in tmp/ (sub-)folder(s) we create empty index.htm|php files
*
* @param $path
*/
private static function createIndexFilesToPreventDirectoryListing($path)
{
if (!self::isPathWithinTmpFolder($path)) {
return;
}
$filesToCreate = array(
$path . '/index.htm',
$path . '/index.php'
);
foreach ($filesToCreate as $file) {
if (!is_file($file)) {
@file_put_contents($file, 'Nothing to see here.');
}
}
}
}