OC_Helper.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641
  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
  5. * SPDX-License-Identifier: AGPL-3.0-only
  6. */
  7. use bantu\IniGetWrapper\IniGetWrapper;
  8. use OC\Files\FilenameValidator;
  9. use OC\Files\Filesystem;
  10. use OCP\Files\Mount\IMountPoint;
  11. use OCP\IBinaryFinder;
  12. use OCP\ICacheFactory;
  13. use OCP\IUser;
  14. use OCP\Util;
  15. use Psr\Log\LoggerInterface;
  16. /**
  17. * Collection of useful functions
  18. *
  19. * @psalm-type StorageInfo = array{
  20. * free: float|int,
  21. * mountPoint: string,
  22. * mountType: string,
  23. * owner: string,
  24. * ownerDisplayName: string,
  25. * quota: float|int,
  26. * relative: float|int,
  27. * total: float|int,
  28. * used: float|int,
  29. * }
  30. */
  31. class OC_Helper {
  32. private static $templateManager;
  33. private static ?ICacheFactory $cacheFactory = null;
  34. private static ?bool $quotaIncludeExternalStorage = null;
  35. /**
  36. * Make a human file size
  37. * @param int|float $bytes file size in bytes
  38. * @return string a human readable file size
  39. *
  40. * Makes 2048 to 2 kB.
  41. */
  42. public static function humanFileSize(int|float $bytes): string {
  43. if ($bytes < 0) {
  44. return '?';
  45. }
  46. if ($bytes < 1024) {
  47. return "$bytes B";
  48. }
  49. $bytes = round($bytes / 1024, 0);
  50. if ($bytes < 1024) {
  51. return "$bytes KB";
  52. }
  53. $bytes = round($bytes / 1024, 1);
  54. if ($bytes < 1024) {
  55. return "$bytes MB";
  56. }
  57. $bytes = round($bytes / 1024, 1);
  58. if ($bytes < 1024) {
  59. return "$bytes GB";
  60. }
  61. $bytes = round($bytes / 1024, 1);
  62. if ($bytes < 1024) {
  63. return "$bytes TB";
  64. }
  65. $bytes = round($bytes / 1024, 1);
  66. return "$bytes PB";
  67. }
  68. /**
  69. * Make a computer file size
  70. * @param string $str file size in human readable format
  71. * @return false|int|float a file size in bytes
  72. *
  73. * Makes 2kB to 2048.
  74. *
  75. * Inspired by: https://www.php.net/manual/en/function.filesize.php#92418
  76. */
  77. public static function computerFileSize(string $str): false|int|float {
  78. $str = strtolower($str);
  79. if (is_numeric($str)) {
  80. return Util::numericToNumber($str);
  81. }
  82. $bytes_array = [
  83. 'b' => 1,
  84. 'k' => 1024,
  85. 'kb' => 1024,
  86. 'mb' => 1024 * 1024,
  87. 'm' => 1024 * 1024,
  88. 'gb' => 1024 * 1024 * 1024,
  89. 'g' => 1024 * 1024 * 1024,
  90. 'tb' => 1024 * 1024 * 1024 * 1024,
  91. 't' => 1024 * 1024 * 1024 * 1024,
  92. 'pb' => 1024 * 1024 * 1024 * 1024 * 1024,
  93. 'p' => 1024 * 1024 * 1024 * 1024 * 1024,
  94. ];
  95. $bytes = (float)$str;
  96. if (preg_match('#([kmgtp]?b?)$#si', $str, $matches) && isset($bytes_array[$matches[1]])) {
  97. $bytes *= $bytes_array[$matches[1]];
  98. } else {
  99. return false;
  100. }
  101. return Util::numericToNumber(round($bytes));
  102. }
  103. /**
  104. * Recursive copying of folders
  105. * @param string $src source folder
  106. * @param string $dest target folder
  107. * @return void
  108. */
  109. public static function copyr($src, $dest) {
  110. if (!file_exists($src)) {
  111. return;
  112. }
  113. if (is_dir($src)) {
  114. if (!is_dir($dest)) {
  115. mkdir($dest);
  116. }
  117. $files = scandir($src);
  118. foreach ($files as $file) {
  119. if ($file != '.' && $file != '..') {
  120. self::copyr("$src/$file", "$dest/$file");
  121. }
  122. }
  123. } else {
  124. $validator = \OCP\Server::get(FilenameValidator::class);
  125. if (!$validator->isForbidden($src)) {
  126. copy($src, $dest);
  127. }
  128. }
  129. }
  130. /**
  131. * Recursive deletion of folders
  132. * @param string $dir path to the folder
  133. * @param bool $deleteSelf if set to false only the content of the folder will be deleted
  134. * @return bool
  135. */
  136. public static function rmdirr($dir, $deleteSelf = true) {
  137. if (is_dir($dir)) {
  138. $files = new RecursiveIteratorIterator(
  139. new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
  140. RecursiveIteratorIterator::CHILD_FIRST
  141. );
  142. foreach ($files as $fileInfo) {
  143. /** @var SplFileInfo $fileInfo */
  144. if ($fileInfo->isLink()) {
  145. unlink($fileInfo->getPathname());
  146. } elseif ($fileInfo->isDir()) {
  147. rmdir($fileInfo->getRealPath());
  148. } else {
  149. unlink($fileInfo->getRealPath());
  150. }
  151. }
  152. if ($deleteSelf) {
  153. rmdir($dir);
  154. }
  155. } elseif (file_exists($dir)) {
  156. if ($deleteSelf) {
  157. unlink($dir);
  158. }
  159. }
  160. if (!$deleteSelf) {
  161. return true;
  162. }
  163. return !file_exists($dir);
  164. }
  165. /**
  166. * @deprecated 18.0.0
  167. * @return \OC\Files\Type\TemplateManager
  168. */
  169. public static function getFileTemplateManager() {
  170. if (!self::$templateManager) {
  171. self::$templateManager = new \OC\Files\Type\TemplateManager();
  172. }
  173. return self::$templateManager;
  174. }
  175. /**
  176. * detect if a given program is found in the search PATH
  177. *
  178. * @param string $name
  179. * @param bool $path
  180. * @internal param string $program name
  181. * @internal param string $optional search path, defaults to $PATH
  182. * @return bool true if executable program found in path
  183. */
  184. public static function canExecute($name, $path = false) {
  185. // path defaults to PATH from environment if not set
  186. if ($path === false) {
  187. $path = getenv('PATH');
  188. }
  189. // we look for an executable file of that name
  190. $exts = [''];
  191. $check_fn = 'is_executable';
  192. // Default check will be done with $path directories :
  193. $dirs = explode(PATH_SEPARATOR, $path);
  194. // WARNING : We have to check if open_basedir is enabled :
  195. $obd = OC::$server->get(IniGetWrapper::class)->getString('open_basedir');
  196. if ($obd != 'none') {
  197. $obd_values = explode(PATH_SEPARATOR, $obd);
  198. if (count($obd_values) > 0 and $obd_values[0]) {
  199. // open_basedir is in effect !
  200. // We need to check if the program is in one of these dirs :
  201. $dirs = $obd_values;
  202. }
  203. }
  204. foreach ($dirs as $dir) {
  205. foreach ($exts as $ext) {
  206. if ($check_fn("$dir/$name" . $ext)) {
  207. return true;
  208. }
  209. }
  210. }
  211. return false;
  212. }
  213. /**
  214. * copy the contents of one stream to another
  215. *
  216. * @param resource $source
  217. * @param resource $target
  218. * @return array the number of bytes copied and result
  219. */
  220. public static function streamCopy($source, $target) {
  221. if (!$source or !$target) {
  222. return [0, false];
  223. }
  224. $bufSize = 8192;
  225. $result = true;
  226. $count = 0;
  227. while (!feof($source)) {
  228. $buf = fread($source, $bufSize);
  229. $bytesWritten = fwrite($target, $buf);
  230. if ($bytesWritten !== false) {
  231. $count += $bytesWritten;
  232. }
  233. // note: strlen is expensive so only use it when necessary,
  234. // on the last block
  235. if ($bytesWritten === false
  236. || ($bytesWritten < $bufSize && $bytesWritten < strlen($buf))
  237. ) {
  238. // write error, could be disk full ?
  239. $result = false;
  240. break;
  241. }
  242. }
  243. return [$count, $result];
  244. }
  245. /**
  246. * Adds a suffix to the name in case the file exists
  247. *
  248. * @param string $path
  249. * @param string $filename
  250. * @return string
  251. */
  252. public static function buildNotExistingFileName($path, $filename) {
  253. $view = \OC\Files\Filesystem::getView();
  254. return self::buildNotExistingFileNameForView($path, $filename, $view);
  255. }
  256. /**
  257. * Adds a suffix to the name in case the file exists
  258. *
  259. * @param string $path
  260. * @param string $filename
  261. * @return string
  262. */
  263. public static function buildNotExistingFileNameForView($path, $filename, \OC\Files\View $view) {
  264. if ($path === '/') {
  265. $path = '';
  266. }
  267. if ($pos = strrpos($filename, '.')) {
  268. $name = substr($filename, 0, $pos);
  269. $ext = substr($filename, $pos);
  270. } else {
  271. $name = $filename;
  272. $ext = '';
  273. }
  274. $newpath = $path . '/' . $filename;
  275. if ($view->file_exists($newpath)) {
  276. if (preg_match_all('/\((\d+)\)/', $name, $matches, PREG_OFFSET_CAPTURE)) {
  277. //Replace the last "(number)" with "(number+1)"
  278. $last_match = count($matches[0]) - 1;
  279. $counter = $matches[1][$last_match][0] + 1;
  280. $offset = $matches[0][$last_match][1];
  281. $match_length = strlen($matches[0][$last_match][0]);
  282. } else {
  283. $counter = 2;
  284. $match_length = 0;
  285. $offset = false;
  286. }
  287. do {
  288. if ($offset) {
  289. //Replace the last "(number)" with "(number+1)"
  290. $newname = substr_replace($name, '(' . $counter . ')', $offset, $match_length);
  291. } else {
  292. $newname = $name . ' (' . $counter . ')';
  293. }
  294. $newpath = $path . '/' . $newname . $ext;
  295. $counter++;
  296. } while ($view->file_exists($newpath));
  297. }
  298. return $newpath;
  299. }
  300. /**
  301. * Returns an array with all keys from input lowercased or uppercased. Numbered indices are left as is.
  302. *
  303. * @param array $input The array to work on
  304. * @param int $case Either MB_CASE_UPPER or MB_CASE_LOWER (default)
  305. * @param string $encoding The encoding parameter is the character encoding. Defaults to UTF-8
  306. * @return array
  307. *
  308. * Returns an array with all keys from input lowercased or uppercased. Numbered indices are left as is.
  309. * based on https://www.php.net/manual/en/function.array-change-key-case.php#107715
  310. *
  311. */
  312. public static function mb_array_change_key_case($input, $case = MB_CASE_LOWER, $encoding = 'UTF-8') {
  313. $case = ($case != MB_CASE_UPPER) ? MB_CASE_LOWER : MB_CASE_UPPER;
  314. $ret = [];
  315. foreach ($input as $k => $v) {
  316. $ret[mb_convert_case($k, $case, $encoding)] = $v;
  317. }
  318. return $ret;
  319. }
  320. /**
  321. * performs a search in a nested array
  322. * @param array $haystack the array to be searched
  323. * @param string $needle the search string
  324. * @param mixed $index optional, only search this key name
  325. * @return mixed the key of the matching field, otherwise false
  326. *
  327. * performs a search in a nested array
  328. *
  329. * taken from https://www.php.net/manual/en/function.array-search.php#97645
  330. */
  331. public static function recursiveArraySearch($haystack, $needle, $index = null) {
  332. $aIt = new RecursiveArrayIterator($haystack);
  333. $it = new RecursiveIteratorIterator($aIt);
  334. while ($it->valid()) {
  335. if (((isset($index) and ($it->key() == $index)) or !isset($index)) and ($it->current() == $needle)) {
  336. return $aIt->key();
  337. }
  338. $it->next();
  339. }
  340. return false;
  341. }
  342. /**
  343. * calculates the maximum upload size respecting system settings, free space and user quota
  344. *
  345. * @param string $dir the current folder where the user currently operates
  346. * @param int|float $freeSpace the number of bytes free on the storage holding $dir, if not set this will be received from the storage directly
  347. * @return int|float number of bytes representing
  348. */
  349. public static function maxUploadFilesize($dir, $freeSpace = null) {
  350. if (is_null($freeSpace) || $freeSpace < 0) {
  351. $freeSpace = self::freeSpace($dir);
  352. }
  353. return min($freeSpace, self::uploadLimit());
  354. }
  355. /**
  356. * Calculate free space left within user quota
  357. *
  358. * @param string $dir the current folder where the user currently operates
  359. * @return int|float number of bytes representing
  360. */
  361. public static function freeSpace($dir) {
  362. $freeSpace = \OC\Files\Filesystem::free_space($dir);
  363. if ($freeSpace < \OCP\Files\FileInfo::SPACE_UNLIMITED) {
  364. $freeSpace = max($freeSpace, 0);
  365. return $freeSpace;
  366. } else {
  367. return (INF > 0)? INF: PHP_INT_MAX; // work around https://bugs.php.net/bug.php?id=69188
  368. }
  369. }
  370. /**
  371. * Calculate PHP upload limit
  372. *
  373. * @return int|float PHP upload file size limit
  374. */
  375. public static function uploadLimit() {
  376. $ini = \OC::$server->get(IniGetWrapper::class);
  377. $upload_max_filesize = Util::computerFileSize($ini->get('upload_max_filesize')) ?: 0;
  378. $post_max_size = Util::computerFileSize($ini->get('post_max_size')) ?: 0;
  379. if ($upload_max_filesize === 0 && $post_max_size === 0) {
  380. return INF;
  381. } elseif ($upload_max_filesize === 0 || $post_max_size === 0) {
  382. return max($upload_max_filesize, $post_max_size); //only the non 0 value counts
  383. } else {
  384. return min($upload_max_filesize, $post_max_size);
  385. }
  386. }
  387. /**
  388. * Checks if a function is available
  389. *
  390. * @deprecated 25.0.0 use \OCP\Util::isFunctionEnabled instead
  391. */
  392. public static function is_function_enabled(string $function_name): bool {
  393. return \OCP\Util::isFunctionEnabled($function_name);
  394. }
  395. /**
  396. * Try to find a program
  397. * @deprecated 25.0.0 Use \OC\BinaryFinder directly
  398. */
  399. public static function findBinaryPath(string $program): ?string {
  400. $result = \OCP\Server::get(IBinaryFinder::class)->findBinaryPath($program);
  401. return $result !== false ? $result : null;
  402. }
  403. /**
  404. * Calculate the disc space for the given path
  405. *
  406. * BEWARE: this requires that Util::setupFS() was called
  407. * already !
  408. *
  409. * @param string $path
  410. * @param \OCP\Files\FileInfo $rootInfo (optional)
  411. * @param bool $includeMountPoints whether to include mount points in the size calculation
  412. * @param bool $useCache whether to use the cached quota values
  413. * @psalm-suppress LessSpecificReturnStatement Legacy code outputs weird types - manually validated that they are correct
  414. * @return StorageInfo
  415. * @throws \OCP\Files\NotFoundException
  416. */
  417. public static function getStorageInfo($path, $rootInfo = null, $includeMountPoints = true, $useCache = true) {
  418. if (!self::$cacheFactory) {
  419. self::$cacheFactory = \OC::$server->get(ICacheFactory::class);
  420. }
  421. $memcache = self::$cacheFactory->createLocal('storage_info');
  422. // return storage info without adding mount points
  423. if (self::$quotaIncludeExternalStorage === null) {
  424. self::$quotaIncludeExternalStorage = \OC::$server->getSystemConfig()->getValue('quota_include_external_storage', false);
  425. }
  426. $view = Filesystem::getView();
  427. if (!$view) {
  428. throw new \OCP\Files\NotFoundException();
  429. }
  430. $fullPath = Filesystem::normalizePath($view->getAbsolutePath($path));
  431. $cacheKey = $fullPath . '::' . ($includeMountPoints ? 'include' : 'exclude');
  432. if ($useCache) {
  433. $cached = $memcache->get($cacheKey);
  434. if ($cached) {
  435. return $cached;
  436. }
  437. }
  438. if (!$rootInfo) {
  439. $rootInfo = \OC\Files\Filesystem::getFileInfo($path, self::$quotaIncludeExternalStorage ? 'ext' : false);
  440. }
  441. if (!$rootInfo instanceof \OCP\Files\FileInfo) {
  442. throw new \OCP\Files\NotFoundException('The root directory of the user\'s files is missing');
  443. }
  444. $used = $rootInfo->getSize($includeMountPoints);
  445. if ($used < 0) {
  446. $used = 0.0;
  447. }
  448. /** @var int|float $quota */
  449. $quota = \OCP\Files\FileInfo::SPACE_UNLIMITED;
  450. $mount = $rootInfo->getMountPoint();
  451. $storage = $mount->getStorage();
  452. $sourceStorage = $storage;
  453. if ($storage->instanceOfStorage('\OCA\Files_Sharing\SharedStorage')) {
  454. self::$quotaIncludeExternalStorage = false;
  455. }
  456. if (self::$quotaIncludeExternalStorage) {
  457. if ($storage->instanceOfStorage('\OC\Files\Storage\Home')
  458. || $storage->instanceOfStorage('\OC\Files\ObjectStore\HomeObjectStoreStorage')
  459. ) {
  460. /** @var \OC\Files\Storage\Home $storage */
  461. $user = $storage->getUser();
  462. } else {
  463. $user = \OC::$server->getUserSession()->getUser();
  464. }
  465. $quota = OC_Util::getUserQuota($user);
  466. if ($quota !== \OCP\Files\FileInfo::SPACE_UNLIMITED) {
  467. // always get free space / total space from root + mount points
  468. return self::getGlobalStorageInfo($quota, $user, $mount);
  469. }
  470. }
  471. // TODO: need a better way to get total space from storage
  472. if ($sourceStorage->instanceOfStorage('\OC\Files\Storage\Wrapper\Quota')) {
  473. /** @var \OC\Files\Storage\Wrapper\Quota $storage */
  474. $quota = $sourceStorage->getQuota();
  475. }
  476. try {
  477. $free = $sourceStorage->free_space($rootInfo->getInternalPath());
  478. if (is_bool($free)) {
  479. $free = 0.0;
  480. }
  481. } catch (\Exception $e) {
  482. if ($path === '') {
  483. throw $e;
  484. }
  485. /** @var LoggerInterface $logger */
  486. $logger = \OC::$server->get(LoggerInterface::class);
  487. $logger->warning('Error while getting quota info, using root quota', ['exception' => $e]);
  488. $rootInfo = self::getStorageInfo('');
  489. $memcache->set($cacheKey, $rootInfo, 5 * 60);
  490. return $rootInfo;
  491. }
  492. if ($free >= 0) {
  493. $total = $free + $used;
  494. } else {
  495. $total = $free; //either unknown or unlimited
  496. }
  497. if ($total > 0) {
  498. if ($quota > 0 && $total > $quota) {
  499. $total = $quota;
  500. }
  501. // prevent division by zero or error codes (negative values)
  502. $relative = round(($used / $total) * 10000) / 100;
  503. } else {
  504. $relative = 0;
  505. }
  506. $ownerId = $storage->getOwner($path);
  507. $ownerDisplayName = '';
  508. if ($ownerId !== false) {
  509. $ownerDisplayName = \OC::$server->getUserManager()->getDisplayName($ownerId) ?? '';
  510. }
  511. if (substr_count($mount->getMountPoint(), '/') < 3) {
  512. $mountPoint = '';
  513. } else {
  514. [,,,$mountPoint] = explode('/', $mount->getMountPoint(), 4);
  515. }
  516. $info = [
  517. 'free' => $free,
  518. 'used' => $used,
  519. 'quota' => $quota,
  520. 'total' => $total,
  521. 'relative' => $relative,
  522. 'owner' => $ownerId,
  523. 'ownerDisplayName' => $ownerDisplayName,
  524. 'mountType' => $mount->getMountType(),
  525. 'mountPoint' => trim($mountPoint, '/'),
  526. ];
  527. if ($ownerId && $path === '/') {
  528. // If path is root, store this as last known quota usage for this user
  529. \OCP\Server::get(\OCP\IConfig::class)->setUserValue($ownerId, 'files', 'lastSeenQuotaUsage', (string)$relative);
  530. }
  531. $memcache->set($cacheKey, $info, 5 * 60);
  532. return $info;
  533. }
  534. /**
  535. * Get storage info including all mount points and quota
  536. *
  537. * @psalm-suppress LessSpecificReturnStatement Legacy code outputs weird types - manually validated that they are correct
  538. * @return StorageInfo
  539. */
  540. private static function getGlobalStorageInfo(int|float $quota, IUser $user, IMountPoint $mount): array {
  541. $rootInfo = \OC\Files\Filesystem::getFileInfo('', 'ext');
  542. /** @var int|float $used */
  543. $used = $rootInfo['size'];
  544. if ($used < 0) {
  545. $used = 0.0;
  546. }
  547. $total = $quota;
  548. /** @var int|float $free */
  549. $free = $quota - $used;
  550. if ($total > 0) {
  551. if ($quota > 0 && $total > $quota) {
  552. $total = $quota;
  553. }
  554. // prevent division by zero or error codes (negative values)
  555. $relative = round(($used / $total) * 10000) / 100;
  556. } else {
  557. $relative = 0.0;
  558. }
  559. if (substr_count($mount->getMountPoint(), '/') < 3) {
  560. $mountPoint = '';
  561. } else {
  562. [,,,$mountPoint] = explode('/', $mount->getMountPoint(), 4);
  563. }
  564. return [
  565. 'free' => $free,
  566. 'used' => $used,
  567. 'total' => $total,
  568. 'relative' => $relative,
  569. 'quota' => $quota,
  570. 'owner' => $user->getUID(),
  571. 'ownerDisplayName' => $user->getDisplayName(),
  572. 'mountType' => $mount->getMountType(),
  573. 'mountPoint' => trim($mountPoint, '/'),
  574. ];
  575. }
  576. public static function clearStorageInfo(string $absolutePath): void {
  577. /** @var ICacheFactory $cacheFactory */
  578. $cacheFactory = \OC::$server->get(ICacheFactory::class);
  579. $memcache = $cacheFactory->createLocal('storage_info');
  580. $cacheKeyPrefix = Filesystem::normalizePath($absolutePath) . '::';
  581. $memcache->remove($cacheKeyPrefix . 'include');
  582. $memcache->remove($cacheKeyPrefix . 'exclude');
  583. }
  584. /**
  585. * Returns whether the config file is set manually to read-only
  586. * @return bool
  587. */
  588. public static function isReadOnlyConfigEnabled() {
  589. return \OC::$server->getConfig()->getSystemValueBool('config_is_read_only', false);
  590. }
  591. }