Teknik is a suite of services with attractive and functional interfaces. https://www.teknik.io/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Filesystem.php 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. <?php
  2. /**
  3. * Piwik - free/libre analytics platform
  4. *
  5. * @link http://piwik.org
  6. * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  7. *
  8. */
  9. namespace Piwik;
  10. use Exception;
  11. use Piwik\Tracker\Cache;
  12. /**
  13. * Contains helper functions that deal with the filesystem.
  14. *
  15. */
  16. class Filesystem
  17. {
  18. /**
  19. * Called on Core install, update, plugin enable/disable
  20. * Will clear all cache that could be affected by the change in configuration being made
  21. */
  22. public static function deleteAllCacheOnUpdate($pluginName = false)
  23. {
  24. AssetManager::getInstance()->removeMergedAssets($pluginName);
  25. View::clearCompiledTemplates();
  26. Cache::deleteTrackerCache();
  27. self::clearPhpCaches();
  28. }
  29. /**
  30. * ending WITHOUT slash
  31. *
  32. * @return string
  33. */
  34. public static function getPathToPiwikRoot()
  35. {
  36. return realpath(dirname(__FILE__) . "/..");
  37. }
  38. /**
  39. * Returns true if the string is a valid filename
  40. * File names that start with a-Z or 0-9 and contain a-Z, 0-9, underscore(_), dash(-), and dot(.) will be accepted.
  41. * File names beginning with anything but a-Z or 0-9 will be rejected (including .htaccess for example).
  42. * File names containing anything other than above mentioned will also be rejected (file names with spaces won't be accepted).
  43. *
  44. * @param string $filename
  45. * @return bool
  46. */
  47. public static function isValidFilename($filename)
  48. {
  49. return (0 !== preg_match('/(^[a-zA-Z0-9]+([a-zA-Z_0-9.-]*))$/D', $filename));
  50. }
  51. /**
  52. * Get canonicalized absolute path
  53. * See http://php.net/realpath
  54. *
  55. * @param string $path
  56. * @return string canonicalized absolute path
  57. */
  58. public static function realpath($path)
  59. {
  60. if (file_exists($path)) {
  61. return realpath($path);
  62. }
  63. return $path;
  64. }
  65. /**
  66. * Attempts to create a new directory. All errors are silenced.
  67. *
  68. * _Note: This function does **not** create directories recursively._
  69. *
  70. * @param string $path The path of the directory to create.
  71. * @api
  72. */
  73. public static function mkdir($path)
  74. {
  75. if (!is_dir($path)) {
  76. // the mode in mkdir is modified by the current umask
  77. @mkdir($path, self::getChmodForPath($path), $recursive = true);
  78. }
  79. // try to overcome restrictive umask (mis-)configuration
  80. if (!is_writable($path)) {
  81. @chmod($path, 0755);
  82. if (!is_writable($path)) {
  83. @chmod($path, 0775);
  84. // enough! we're not going to make the directory world-writeable
  85. }
  86. }
  87. }
  88. /**
  89. * Checks if the filesystem Piwik stores sessions in is NFS or not. This
  90. * check is done in order to avoid using file based sessions on NFS system,
  91. * since on such a filesystem file locking can make file based sessions
  92. * incredibly slow.
  93. *
  94. * Note: In order to figure this out, we try to run the 'df' program. If
  95. * the 'exec' or 'shell_exec' functions are not available, we can't do
  96. * the check.
  97. *
  98. * @return bool True if on an NFS filesystem, false if otherwise or if we
  99. * can't use shell_exec or exec.
  100. */
  101. public static function checkIfFileSystemIsNFS()
  102. {
  103. $sessionsPath = Session::getSessionsDirectory();
  104. // this command will display details for the filesystem that holds the $sessionsPath
  105. // path, but only if its type is NFS. if not NFS, df will return one or less lines
  106. // and the return code 1. if NFS, it will return 0 and at least 2 lines of text.
  107. $command = "df -T -t nfs \"$sessionsPath\" 2>&1";
  108. if (function_exists('exec')) // use exec
  109. {
  110. $output = $returnCode = null;
  111. @exec($command, $output, $returnCode);
  112. // check if filesystem is NFS
  113. if ($returnCode == 0
  114. && count($output) > 1
  115. ) {
  116. return true;
  117. }
  118. } else if (function_exists('shell_exec')) // use shell_exec
  119. {
  120. $output = @shell_exec($command);
  121. if ($output) {
  122. $output = explode("\n", $output);
  123. if (count($output) > 1) // check if filesystem is NFS
  124. {
  125. return true;
  126. }
  127. }
  128. }
  129. return false; // not NFS, or we can't run a program to find out
  130. }
  131. /**
  132. * Recursively find pathnames that match a pattern.
  133. *
  134. * See {@link http://php.net/manual/en/function.glob.php glob} for more info.
  135. *
  136. * @param string $sDir directory The directory to glob in.
  137. * @param string $sPattern pattern The pattern to match paths against.
  138. * @param int $nFlags `glob()` . See {@link http://php.net/manual/en/function.glob.php glob()}.
  139. * @return array The list of paths that match the pattern.
  140. * @api
  141. */
  142. public static function globr($sDir, $sPattern, $nFlags = null)
  143. {
  144. if (($aFiles = \_glob("$sDir/$sPattern", $nFlags)) == false) {
  145. $aFiles = array();
  146. }
  147. if (($aDirs = \_glob("$sDir/*", GLOB_ONLYDIR)) != false) {
  148. foreach ($aDirs as $sSubDir) {
  149. if (is_link($sSubDir)) {
  150. continue;
  151. }
  152. $aSubFiles = self::globr($sSubDir, $sPattern, $nFlags);
  153. $aFiles = array_merge($aFiles, $aSubFiles);
  154. }
  155. }
  156. return $aFiles;
  157. }
  158. /**
  159. * Recursively deletes a directory.
  160. *
  161. * @param string $dir Path of the directory to delete.
  162. * @param boolean $deleteRootToo If true, `$dir` is deleted, otherwise just its contents.
  163. * @param \Closure|false $beforeUnlink An optional closure to execute on a file path before unlinking.
  164. * @api
  165. */
  166. public static function unlinkRecursive($dir, $deleteRootToo, \Closure $beforeUnlink = null)
  167. {
  168. if (!$dh = @opendir($dir)) {
  169. return;
  170. }
  171. while (false !== ($obj = readdir($dh))) {
  172. if ($obj == '.' || $obj == '..') {
  173. continue;
  174. }
  175. $path = $dir . '/' . $obj;
  176. if ($beforeUnlink) {
  177. $beforeUnlink($path);
  178. }
  179. if (!@unlink($path)) {
  180. self::unlinkRecursive($path, true);
  181. }
  182. }
  183. closedir($dh);
  184. if ($deleteRootToo) {
  185. @rmdir($dir);
  186. }
  187. return;
  188. }
  189. /**
  190. * Removes all files and directories that are present in the target directory but are not in the source directory.
  191. *
  192. * @param string $source Path to the source directory
  193. * @param string $target Path to the target
  194. */
  195. public static function unlinkTargetFilesNotPresentInSource($source, $target)
  196. {
  197. $diff = self::directoryDiff($source, $target);
  198. $diff = self::sortFilesDescByPathLength($diff);
  199. foreach ($diff as $file) {
  200. $remove = $target . $file;
  201. if (is_dir($remove)) {
  202. @rmdir($remove);
  203. } else {
  204. self::deleteFileIfExists($remove);
  205. }
  206. }
  207. }
  208. /**
  209. * Sort all given paths/filenames by its path length. Long path names will be listed first. This method can be
  210. * useful if you have for instance a bunch of files/directories to delete. By sorting them by lengh you can make
  211. * sure to delete all files within the folders before deleting the actual folder.
  212. *
  213. * @param string[] $files
  214. * @return string[]
  215. */
  216. public static function sortFilesDescByPathLength($files)
  217. {
  218. usort($files, function ($a, $b) {
  219. // sort by filename length so we kinda make sure to remove files before its directories
  220. if ($a == $b) {
  221. return 0;
  222. }
  223. return (strlen($a) > strlen($b) ? -1 : 1);
  224. });
  225. return $files;
  226. }
  227. /**
  228. * Computes the difference of directories. Compares $target against $source and returns a relative path to all files
  229. * and directories in $target that are not present in $source.
  230. *
  231. * @param $source
  232. * @param $target
  233. *
  234. * @return string[]
  235. */
  236. public static function directoryDiff($source, $target)
  237. {
  238. $sourceFiles = self::globr($source, '*');
  239. $targetFiles = self::globr($target, '*');
  240. $sourceFiles = array_map(function ($file) use ($source) {
  241. return str_replace($source, '', $file);
  242. }, $sourceFiles);
  243. $targetFiles = array_map(function ($file) use ($target) {
  244. return str_replace($target, '', $file);
  245. }, $targetFiles);
  246. $diff = array_diff($targetFiles, $sourceFiles);
  247. return array_values($diff);
  248. }
  249. /**
  250. * Copies a file from `$source` to `$dest`.
  251. *
  252. * @param string $source A path to a file, eg. './tmp/latest/index.php'. The file must exist.
  253. * @param string $dest A path to a file, eg. './index.php'. The file does not have to exist.
  254. * @param bool $excludePhp Whether to avoid copying files if the file is related to PHP
  255. * (includes .php, .tpl, .twig files).
  256. * @throws Exception If the file cannot be copied.
  257. * @return true
  258. * @api
  259. */
  260. public static function copy($source, $dest, $excludePhp = false)
  261. {
  262. static $phpExtensions = array('php', 'tpl', 'twig');
  263. if ($excludePhp) {
  264. $path_parts = pathinfo($source);
  265. if (in_array($path_parts['extension'], $phpExtensions)) {
  266. return true;
  267. }
  268. }
  269. if (!@copy($source, $dest)) {
  270. @chmod($dest, 0755);
  271. if (!@copy($source, $dest)) {
  272. $message = "Error while creating/copying file to <code>$dest</code>. <br />"
  273. . Filechecks::getErrorMessageMissingPermissions(self::getPathToPiwikRoot());
  274. throw new Exception($message);
  275. }
  276. }
  277. return true;
  278. }
  279. /**
  280. * Copies the contents of a directory recursively from `$source` to `$target`.
  281. *
  282. * @param string $source A directory or file to copy, eg. './tmp/latest'.
  283. * @param string $target A directory to copy to, eg. '.'.
  284. * @param bool $excludePhp Whether to avoid copying files if the file is related to PHP
  285. * (includes .php, .tpl, .twig files).
  286. * @throws Exception If a file cannot be copied.
  287. * @api
  288. */
  289. public static function copyRecursive($source, $target, $excludePhp = false)
  290. {
  291. if (is_dir($source)) {
  292. self::mkdir($target);
  293. $d = dir($source);
  294. while (false !== ($entry = $d->read())) {
  295. if ($entry == '.' || $entry == '..') {
  296. continue;
  297. }
  298. $sourcePath = $source . '/' . $entry;
  299. if (is_dir($sourcePath)) {
  300. self::copyRecursive($sourcePath, $target . '/' . $entry, $excludePhp);
  301. continue;
  302. }
  303. $destPath = $target . '/' . $entry;
  304. self::copy($sourcePath, $destPath, $excludePhp);
  305. }
  306. $d->close();
  307. } else {
  308. self::copy($source, $target, $excludePhp);
  309. }
  310. }
  311. /**
  312. * Deletes the given file if it exists.
  313. *
  314. * @param string $pathToFile
  315. * @return bool true in case of success or if file does not exist, false otherwise. It might fail in case the
  316. * file is not writeable.
  317. * @api
  318. */
  319. public static function deleteFileIfExists($pathToFile)
  320. {
  321. if (!file_exists($pathToFile)) {
  322. return true;
  323. }
  324. return @unlink($pathToFile);
  325. }
  326. /**
  327. * @param $path
  328. * @return int
  329. */
  330. private static function getChmodForPath($path)
  331. {
  332. $pathIsTmp = self::getPathToPiwikRoot() . '/tmp';
  333. if (strpos($path, $pathIsTmp) === 0) {
  334. // tmp/* folder
  335. return 0750;
  336. }
  337. // plugins/* and all others
  338. return 0755;
  339. }
  340. public static function clearPhpCaches()
  341. {
  342. if (function_exists('apc_clear_cache')) {
  343. apc_clear_cache(); // clear the system (aka 'opcode') cache
  344. }
  345. if (function_exists('opcache_reset')) {
  346. @opcache_reset(); // reset the opcode cache (php 5.5.0+)
  347. }
  348. if (function_exists('wincache_refresh_if_changed')) {
  349. @wincache_refresh_if_changed(); // reset the wincache
  350. }
  351. if (function_exists('xcache_clear_cache') && defined('XC_TYPE_VAR')) {
  352. if (ini_get('xcache.admin.enable_auth')) {
  353. // XCache will not be cleared because "xcache.admin.enable_auth" is enabled in php.ini.
  354. } else {
  355. @xcache_clear_cache(XC_TYPE_VAR);
  356. }
  357. }
  358. }
  359. }