Encoding.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524
  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. namespace OC\Files\Storage\Wrapper;
  8. use OC\Files\Filesystem;
  9. use OCP\Cache\CappedMemoryCache;
  10. use OCP\Files\Storage\IStorage;
  11. use OCP\ICache;
  12. /**
  13. * Encoding wrapper that deals with file names that use unsupported encodings like NFD.
  14. *
  15. * When applied and a UTF-8 path name was given, the wrapper will first attempt to access
  16. * the actual given name and then try its NFD form.
  17. */
  18. class Encoding extends Wrapper {
  19. /**
  20. * @var ICache
  21. */
  22. private $namesCache;
  23. /**
  24. * @param array $parameters
  25. */
  26. public function __construct($parameters) {
  27. $this->storage = $parameters['storage'];
  28. $this->namesCache = new CappedMemoryCache();
  29. }
  30. /**
  31. * Returns whether the given string is only made of ASCII characters
  32. *
  33. * @param string $str string
  34. *
  35. * @return bool true if the string is all ASCII, false otherwise
  36. */
  37. private function isAscii($str) {
  38. return !preg_match('/[\\x80-\\xff]+/', $str);
  39. }
  40. /**
  41. * Checks whether the given path exists in NFC or NFD form after checking
  42. * each form for each path section and returns the correct form.
  43. * If no existing path found, returns the path as it was given.
  44. *
  45. * @param string $fullPath path to check
  46. *
  47. * @return string original or converted path
  48. */
  49. private function findPathToUse($fullPath) {
  50. $cachedPath = $this->namesCache[$fullPath];
  51. if ($cachedPath !== null) {
  52. return $cachedPath;
  53. }
  54. $sections = explode('/', $fullPath);
  55. $path = '';
  56. foreach ($sections as $section) {
  57. $convertedPath = $this->findPathToUseLastSection($path, $section);
  58. if ($convertedPath === null) {
  59. // no point in continuing if the section was not found, use original path
  60. return $fullPath;
  61. }
  62. $path = $convertedPath . '/';
  63. }
  64. $path = rtrim($path, '/');
  65. return $path;
  66. }
  67. /**
  68. * Checks whether the last path section of the given path exists in NFC or NFD form
  69. * and returns the correct form. If no existing path found, returns null.
  70. *
  71. * @param string $basePath base path to check
  72. * @param string $lastSection last section of the path to check for NFD/NFC variations
  73. *
  74. * @return string|null original or converted path, or null if none of the forms was found
  75. */
  76. private function findPathToUseLastSection($basePath, $lastSection) {
  77. $fullPath = $basePath . $lastSection;
  78. if ($lastSection === '' || $this->isAscii($lastSection) || $this->storage->file_exists($fullPath)) {
  79. $this->namesCache[$fullPath] = $fullPath;
  80. return $fullPath;
  81. }
  82. // swap encoding
  83. if (\Normalizer::isNormalized($lastSection, \Normalizer::FORM_C)) {
  84. $otherFormPath = \Normalizer::normalize($lastSection, \Normalizer::FORM_D);
  85. } else {
  86. $otherFormPath = \Normalizer::normalize($lastSection, \Normalizer::FORM_C);
  87. }
  88. $otherFullPath = $basePath . $otherFormPath;
  89. if ($this->storage->file_exists($otherFullPath)) {
  90. $this->namesCache[$fullPath] = $otherFullPath;
  91. return $otherFullPath;
  92. }
  93. // return original path, file did not exist at all
  94. $this->namesCache[$fullPath] = $fullPath;
  95. return null;
  96. }
  97. /**
  98. * see https://www.php.net/manual/en/function.mkdir.php
  99. *
  100. * @param string $path
  101. * @return bool
  102. */
  103. public function mkdir($path) {
  104. // note: no conversion here, method should not be called with non-NFC names!
  105. $result = $this->storage->mkdir($path);
  106. if ($result) {
  107. $this->namesCache[$path] = $path;
  108. }
  109. return $result;
  110. }
  111. /**
  112. * see https://www.php.net/manual/en/function.rmdir.php
  113. *
  114. * @param string $path
  115. * @return bool
  116. */
  117. public function rmdir($path) {
  118. $result = $this->storage->rmdir($this->findPathToUse($path));
  119. if ($result) {
  120. unset($this->namesCache[$path]);
  121. }
  122. return $result;
  123. }
  124. /**
  125. * see https://www.php.net/manual/en/function.opendir.php
  126. *
  127. * @param string $path
  128. * @return resource|false
  129. */
  130. public function opendir($path) {
  131. $handle = $this->storage->opendir($this->findPathToUse($path));
  132. return EncodingDirectoryWrapper::wrap($handle);
  133. }
  134. /**
  135. * see https://www.php.net/manual/en/function.is_dir.php
  136. *
  137. * @param string $path
  138. * @return bool
  139. */
  140. public function is_dir($path) {
  141. return $this->storage->is_dir($this->findPathToUse($path));
  142. }
  143. /**
  144. * see https://www.php.net/manual/en/function.is_file.php
  145. *
  146. * @param string $path
  147. * @return bool
  148. */
  149. public function is_file($path) {
  150. return $this->storage->is_file($this->findPathToUse($path));
  151. }
  152. /**
  153. * see https://www.php.net/manual/en/function.stat.php
  154. * only the following keys are required in the result: size and mtime
  155. *
  156. * @param string $path
  157. * @return array|bool
  158. */
  159. public function stat($path) {
  160. return $this->storage->stat($this->findPathToUse($path));
  161. }
  162. /**
  163. * see https://www.php.net/manual/en/function.filetype.php
  164. *
  165. * @param string $path
  166. * @return string|bool
  167. */
  168. public function filetype($path) {
  169. return $this->storage->filetype($this->findPathToUse($path));
  170. }
  171. /**
  172. * see https://www.php.net/manual/en/function.filesize.php
  173. * The result for filesize when called on a folder is required to be 0
  174. */
  175. public function filesize($path): false|int|float {
  176. return $this->storage->filesize($this->findPathToUse($path));
  177. }
  178. /**
  179. * check if a file can be created in $path
  180. *
  181. * @param string $path
  182. * @return bool
  183. */
  184. public function isCreatable($path) {
  185. return $this->storage->isCreatable($this->findPathToUse($path));
  186. }
  187. /**
  188. * check if a file can be read
  189. *
  190. * @param string $path
  191. * @return bool
  192. */
  193. public function isReadable($path) {
  194. return $this->storage->isReadable($this->findPathToUse($path));
  195. }
  196. /**
  197. * check if a file can be written to
  198. *
  199. * @param string $path
  200. * @return bool
  201. */
  202. public function isUpdatable($path) {
  203. return $this->storage->isUpdatable($this->findPathToUse($path));
  204. }
  205. /**
  206. * check if a file can be deleted
  207. *
  208. * @param string $path
  209. * @return bool
  210. */
  211. public function isDeletable($path) {
  212. return $this->storage->isDeletable($this->findPathToUse($path));
  213. }
  214. /**
  215. * check if a file can be shared
  216. *
  217. * @param string $path
  218. * @return bool
  219. */
  220. public function isSharable($path) {
  221. return $this->storage->isSharable($this->findPathToUse($path));
  222. }
  223. /**
  224. * get the full permissions of a path.
  225. * Should return a combination of the PERMISSION_ constants defined in lib/public/constants.php
  226. *
  227. * @param string $path
  228. * @return int
  229. */
  230. public function getPermissions($path) {
  231. return $this->storage->getPermissions($this->findPathToUse($path));
  232. }
  233. /**
  234. * see https://www.php.net/manual/en/function.file_exists.php
  235. *
  236. * @param string $path
  237. * @return bool
  238. */
  239. public function file_exists($path) {
  240. return $this->storage->file_exists($this->findPathToUse($path));
  241. }
  242. /**
  243. * see https://www.php.net/manual/en/function.filemtime.php
  244. *
  245. * @param string $path
  246. * @return int|bool
  247. */
  248. public function filemtime($path) {
  249. return $this->storage->filemtime($this->findPathToUse($path));
  250. }
  251. /**
  252. * see https://www.php.net/manual/en/function.file_get_contents.php
  253. *
  254. * @param string $path
  255. * @return string|false
  256. */
  257. public function file_get_contents($path) {
  258. return $this->storage->file_get_contents($this->findPathToUse($path));
  259. }
  260. /**
  261. * see https://www.php.net/manual/en/function.file_put_contents.php
  262. *
  263. * @param string $path
  264. * @param mixed $data
  265. * @return int|float|false
  266. */
  267. public function file_put_contents($path, $data) {
  268. return $this->storage->file_put_contents($this->findPathToUse($path), $data);
  269. }
  270. /**
  271. * see https://www.php.net/manual/en/function.unlink.php
  272. *
  273. * @param string $path
  274. * @return bool
  275. */
  276. public function unlink($path) {
  277. $result = $this->storage->unlink($this->findPathToUse($path));
  278. if ($result) {
  279. unset($this->namesCache[$path]);
  280. }
  281. return $result;
  282. }
  283. /**
  284. * see https://www.php.net/manual/en/function.rename.php
  285. *
  286. * @param string $source
  287. * @param string $target
  288. * @return bool
  289. */
  290. public function rename($source, $target) {
  291. // second name always NFC
  292. return $this->storage->rename($this->findPathToUse($source), $this->findPathToUse($target));
  293. }
  294. /**
  295. * see https://www.php.net/manual/en/function.copy.php
  296. *
  297. * @param string $source
  298. * @param string $target
  299. * @return bool
  300. */
  301. public function copy($source, $target) {
  302. return $this->storage->copy($this->findPathToUse($source), $this->findPathToUse($target));
  303. }
  304. /**
  305. * see https://www.php.net/manual/en/function.fopen.php
  306. *
  307. * @param string $path
  308. * @param string $mode
  309. * @return resource|bool
  310. */
  311. public function fopen($path, $mode) {
  312. $result = $this->storage->fopen($this->findPathToUse($path), $mode);
  313. if ($result && $mode !== 'r' && $mode !== 'rb') {
  314. unset($this->namesCache[$path]);
  315. }
  316. return $result;
  317. }
  318. /**
  319. * get the mimetype for a file or folder
  320. * The mimetype for a folder is required to be "httpd/unix-directory"
  321. *
  322. * @param string $path
  323. * @return string|bool
  324. */
  325. public function getMimeType($path) {
  326. return $this->storage->getMimeType($this->findPathToUse($path));
  327. }
  328. /**
  329. * see https://www.php.net/manual/en/function.hash.php
  330. *
  331. * @param string $type
  332. * @param string $path
  333. * @param bool $raw
  334. * @return string|bool
  335. */
  336. public function hash($type, $path, $raw = false) {
  337. return $this->storage->hash($type, $this->findPathToUse($path), $raw);
  338. }
  339. /**
  340. * see https://www.php.net/manual/en/function.free_space.php
  341. *
  342. * @param string $path
  343. * @return int|float|bool
  344. */
  345. public function free_space($path) {
  346. return $this->storage->free_space($this->findPathToUse($path));
  347. }
  348. /**
  349. * search for occurrences of $query in file names
  350. *
  351. * @param string $query
  352. * @return array|bool
  353. */
  354. public function search($query) {
  355. return $this->storage->search($query);
  356. }
  357. /**
  358. * see https://www.php.net/manual/en/function.touch.php
  359. * If the backend does not support the operation, false should be returned
  360. *
  361. * @param string $path
  362. * @param int $mtime
  363. * @return bool
  364. */
  365. public function touch($path, $mtime = null) {
  366. return $this->storage->touch($this->findPathToUse($path), $mtime);
  367. }
  368. /**
  369. * get the path to a local version of the file.
  370. * The local version of the file can be temporary and doesn't have to be persistent across requests
  371. *
  372. * @param string $path
  373. * @return string|false
  374. */
  375. public function getLocalFile($path) {
  376. return $this->storage->getLocalFile($this->findPathToUse($path));
  377. }
  378. /**
  379. * check if a file or folder has been updated since $time
  380. *
  381. * @param string $path
  382. * @param int $time
  383. * @return bool
  384. *
  385. * hasUpdated for folders should return at least true if a file inside the folder is add, removed or renamed.
  386. * returning true for other changes in the folder is optional
  387. */
  388. public function hasUpdated($path, $time) {
  389. return $this->storage->hasUpdated($this->findPathToUse($path), $time);
  390. }
  391. /**
  392. * get a cache instance for the storage
  393. *
  394. * @param string $path
  395. * @param \OC\Files\Storage\Storage (optional) the storage to pass to the cache
  396. * @return \OC\Files\Cache\Cache
  397. */
  398. public function getCache($path = '', $storage = null) {
  399. if (!$storage) {
  400. $storage = $this;
  401. }
  402. return $this->storage->getCache($this->findPathToUse($path), $storage);
  403. }
  404. /**
  405. * get a scanner instance for the storage
  406. *
  407. * @param string $path
  408. * @param \OC\Files\Storage\Storage (optional) the storage to pass to the scanner
  409. * @return \OC\Files\Cache\Scanner
  410. */
  411. public function getScanner($path = '', $storage = null) {
  412. if (!$storage) {
  413. $storage = $this;
  414. }
  415. return $this->storage->getScanner($this->findPathToUse($path), $storage);
  416. }
  417. /**
  418. * get the ETag for a file or folder
  419. *
  420. * @param string $path
  421. * @return string|false
  422. */
  423. public function getETag($path) {
  424. return $this->storage->getETag($this->findPathToUse($path));
  425. }
  426. /**
  427. * @param IStorage $sourceStorage
  428. * @param string $sourceInternalPath
  429. * @param string $targetInternalPath
  430. * @return bool
  431. */
  432. public function copyFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) {
  433. if ($sourceStorage === $this) {
  434. return $this->copy($sourceInternalPath, $this->findPathToUse($targetInternalPath));
  435. }
  436. $result = $this->storage->copyFromStorage($sourceStorage, $sourceInternalPath, $this->findPathToUse($targetInternalPath));
  437. if ($result) {
  438. unset($this->namesCache[$targetInternalPath]);
  439. }
  440. return $result;
  441. }
  442. /**
  443. * @param IStorage $sourceStorage
  444. * @param string $sourceInternalPath
  445. * @param string $targetInternalPath
  446. * @return bool
  447. */
  448. public function moveFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) {
  449. if ($sourceStorage === $this) {
  450. $result = $this->rename($sourceInternalPath, $this->findPathToUse($targetInternalPath));
  451. if ($result) {
  452. unset($this->namesCache[$sourceInternalPath]);
  453. unset($this->namesCache[$targetInternalPath]);
  454. }
  455. return $result;
  456. }
  457. $result = $this->storage->moveFromStorage($sourceStorage, $sourceInternalPath, $this->findPathToUse($targetInternalPath));
  458. if ($result) {
  459. unset($this->namesCache[$sourceInternalPath]);
  460. unset($this->namesCache[$targetInternalPath]);
  461. }
  462. return $result;
  463. }
  464. public function getMetaData($path) {
  465. $entry = $this->storage->getMetaData($this->findPathToUse($path));
  466. $entry['name'] = trim(Filesystem::normalizePath($entry['name']), '/');
  467. return $entry;
  468. }
  469. public function getDirectoryContent($directory): \Traversable {
  470. $entries = $this->storage->getDirectoryContent($this->findPathToUse($directory));
  471. foreach ($entries as $entry) {
  472. $entry['name'] = trim(Filesystem::normalizePath($entry['name']), '/');
  473. yield $entry;
  474. }
  475. }
  476. }