FileTest.php 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041
  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 OCA\DAV\Tests\unit\Connector\Sabre;
  8. use OC\AppFramework\Http\Request;
  9. use OC\Files\Filesystem;
  10. use OC\Files\Storage\Local;
  11. use OC\Files\Storage\Temporary;
  12. use OC\Files\Storage\Wrapper\PermissionsMask;
  13. use OC\Files\View;
  14. use OCA\DAV\Connector\Sabre\Exception\FileLocked;
  15. use OCA\DAV\Connector\Sabre\Exception\Forbidden;
  16. use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
  17. use OCA\DAV\Connector\Sabre\File;
  18. use OCP\Constants;
  19. use OCP\Encryption\Exceptions\GenericEncryptionException;
  20. use OCP\Files\EntityTooLargeException;
  21. use OCP\Files\FileInfo;
  22. use OCP\Files\ForbiddenException;
  23. use OCP\Files\InvalidContentException;
  24. use OCP\Files\InvalidPathException;
  25. use OCP\Files\LockNotAcquiredException;
  26. use OCP\Files\NotPermittedException;
  27. use OCP\Files\Storage\IStorage;
  28. use OCP\Files\StorageNotAvailableException;
  29. use OCP\IConfig;
  30. use OCP\IRequestId;
  31. use OCP\ITempManager;
  32. use OCP\IUserManager;
  33. use OCP\Lock\ILockingProvider;
  34. use OCP\Lock\LockedException;
  35. use OCP\Server;
  36. use OCP\Util;
  37. use PHPUnit\Framework\MockObject\MockObject;
  38. use Test\HookHelper;
  39. use Test\TestCase;
  40. use Test\Traits\MountProviderTrait;
  41. use Test\Traits\UserTrait;
  42. /**
  43. * Class File
  44. *
  45. * @group DB
  46. *
  47. * @package OCA\DAV\Tests\unit\Connector\Sabre
  48. */
  49. class FileTest extends TestCase {
  50. use MountProviderTrait;
  51. use UserTrait;
  52. /**
  53. * @var string
  54. */
  55. private $user;
  56. /** @var IConfig|MockObject */
  57. protected $config;
  58. /** @var IRequestId|MockObject */
  59. protected $requestId;
  60. protected function setUp(): void {
  61. parent::setUp();
  62. \OC_Hook::clear();
  63. $this->user = 'test_user';
  64. $this->createUser($this->user, 'pass');
  65. $this->loginAsUser($this->user);
  66. $this->config = $this->createMock(IConfig::class);
  67. $this->requestId = $this->createMock(IRequestId::class);
  68. }
  69. protected function tearDown(): void {
  70. $userManager = Server::get(IUserManager::class);
  71. $userManager->get($this->user)->delete();
  72. parent::tearDown();
  73. }
  74. private function getMockStorage(): MockObject&IStorage {
  75. $storage = $this->getMockBuilder(IStorage::class)
  76. ->disableOriginalConstructor()
  77. ->getMock();
  78. $storage->method('getId')
  79. ->willReturn('home::someuser');
  80. return $storage;
  81. }
  82. private function getStream(string $string) {
  83. $stream = fopen('php://temp', 'r+');
  84. fwrite($stream, $string);
  85. fseek($stream, 0);
  86. return $stream;
  87. }
  88. public function fopenFailuresProvider() {
  89. return [
  90. [
  91. // return false
  92. null,
  93. '\Sabre\Dav\Exception',
  94. false
  95. ],
  96. [
  97. new NotPermittedException(),
  98. 'Sabre\DAV\Exception\Forbidden'
  99. ],
  100. [
  101. new EntityTooLargeException(),
  102. 'OCA\DAV\Connector\Sabre\Exception\EntityTooLarge'
  103. ],
  104. [
  105. new InvalidContentException(),
  106. 'OCA\DAV\Connector\Sabre\Exception\UnsupportedMediaType'
  107. ],
  108. [
  109. new InvalidPathException(),
  110. 'Sabre\DAV\Exception\Forbidden'
  111. ],
  112. [
  113. new ForbiddenException('', true),
  114. 'OCA\DAV\Connector\Sabre\Exception\Forbidden'
  115. ],
  116. [
  117. new LockNotAcquiredException('/test.txt', 1),
  118. 'OCA\DAV\Connector\Sabre\Exception\FileLocked'
  119. ],
  120. [
  121. new LockedException('/test.txt'),
  122. 'OCA\DAV\Connector\Sabre\Exception\FileLocked'
  123. ],
  124. [
  125. new GenericEncryptionException(),
  126. 'Sabre\DAV\Exception\ServiceUnavailable'
  127. ],
  128. [
  129. new StorageNotAvailableException(),
  130. 'Sabre\DAV\Exception\ServiceUnavailable'
  131. ],
  132. [
  133. new \Sabre\DAV\Exception('Generic sabre exception'),
  134. 'Sabre\DAV\Exception',
  135. false
  136. ],
  137. [
  138. new \Exception('Generic exception'),
  139. 'Sabre\DAV\Exception'
  140. ],
  141. ];
  142. }
  143. /**
  144. * @dataProvider fopenFailuresProvider
  145. */
  146. public function testSimplePutFails($thrownException, $expectedException, $checkPreviousClass = true): void {
  147. // setup
  148. $storage = $this->getMockBuilder(Local::class)
  149. ->onlyMethods(['writeStream'])
  150. ->setConstructorArgs([['datadir' => Server::get(ITempManager::class)->getTemporaryFolder()]])
  151. ->getMock();
  152. Filesystem::mount($storage, [], $this->user . '/');
  153. /** @var View | MockObject $view */
  154. $view = $this->getMockBuilder(View::class)
  155. ->onlyMethods(['getRelativePath', 'resolvePath'])
  156. ->getMock();
  157. $view->expects($this->atLeastOnce())
  158. ->method('resolvePath')
  159. ->willReturnCallback(
  160. function ($path) use ($storage) {
  161. return [$storage, $path];
  162. }
  163. );
  164. if ($thrownException !== null) {
  165. $storage->expects($this->once())
  166. ->method('writeStream')
  167. ->will($this->throwException($thrownException));
  168. } else {
  169. $storage->expects($this->once())
  170. ->method('writeStream')
  171. ->willReturn(0);
  172. }
  173. $view->expects($this->any())
  174. ->method('getRelativePath')
  175. ->willReturnArgument(0);
  176. $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [
  177. 'permissions' => Constants::PERMISSION_ALL,
  178. 'type' => FileInfo::TYPE_FOLDER,
  179. ], null);
  180. $file = new File($view, $info);
  181. // action
  182. $caughtException = null;
  183. try {
  184. $file->put('test data');
  185. } catch (\Exception $e) {
  186. $caughtException = $e;
  187. }
  188. $this->assertInstanceOf($expectedException, $caughtException);
  189. if ($checkPreviousClass) {
  190. $this->assertInstanceOf(get_class($thrownException), $caughtException->getPrevious());
  191. }
  192. $this->assertEmpty($this->listPartFiles($view, ''), 'No stray part files');
  193. }
  194. /**
  195. * Simulate putting a file to the given path.
  196. *
  197. * @param string $path path to put the file into
  198. * @param string $viewRoot root to use for the view
  199. * @param null|Request $request the HTTP request
  200. *
  201. * @return null|string of the PUT operation which is usually the etag
  202. */
  203. private function doPut($path, $viewRoot = null, ?Request $request = null) {
  204. $view = Filesystem::getView();
  205. if (!is_null($viewRoot)) {
  206. $view = new View($viewRoot);
  207. } else {
  208. $viewRoot = '/' . $this->user . '/files';
  209. }
  210. $info = new \OC\Files\FileInfo(
  211. $viewRoot . '/' . ltrim($path, '/'),
  212. $this->getMockStorage(),
  213. null,
  214. [
  215. 'permissions' => Constants::PERMISSION_ALL,
  216. 'type' => FileInfo::TYPE_FOLDER,
  217. ],
  218. null
  219. );
  220. /** @var File|MockObject $file */
  221. $file = $this->getMockBuilder(File::class)
  222. ->setConstructorArgs([$view, $info, null, $request])
  223. ->onlyMethods(['header'])
  224. ->getMock();
  225. // beforeMethod locks
  226. $view->lockFile($path, ILockingProvider::LOCK_SHARED);
  227. $result = $file->put($this->getStream('test data'));
  228. // afterMethod unlocks
  229. $view->unlockFile($path, ILockingProvider::LOCK_SHARED);
  230. return $result;
  231. }
  232. /**
  233. * Test putting a single file
  234. */
  235. public function testPutSingleFile(): void {
  236. $this->assertNotEmpty($this->doPut('/foo.txt'));
  237. }
  238. public function legalMtimeProvider() {
  239. return [
  240. 'string' => [
  241. 'HTTP_X_OC_MTIME' => 'string',
  242. 'expected result' => null
  243. ],
  244. 'castable string (int)' => [
  245. 'HTTP_X_OC_MTIME' => '987654321',
  246. 'expected result' => 987654321
  247. ],
  248. 'castable string (float)' => [
  249. 'HTTP_X_OC_MTIME' => '123456789.56',
  250. 'expected result' => 123456789
  251. ],
  252. 'float' => [
  253. 'HTTP_X_OC_MTIME' => 123456789.56,
  254. 'expected result' => 123456789
  255. ],
  256. 'zero' => [
  257. 'HTTP_X_OC_MTIME' => 0,
  258. 'expected result' => null
  259. ],
  260. 'zero string' => [
  261. 'HTTP_X_OC_MTIME' => '0',
  262. 'expected result' => null
  263. ],
  264. 'negative zero string' => [
  265. 'HTTP_X_OC_MTIME' => '-0',
  266. 'expected result' => null
  267. ],
  268. 'string starting with number following by char' => [
  269. 'HTTP_X_OC_MTIME' => '2345asdf',
  270. 'expected result' => null
  271. ],
  272. 'string castable hex int' => [
  273. 'HTTP_X_OC_MTIME' => '0x45adf',
  274. 'expected result' => null
  275. ],
  276. 'string that looks like invalid hex int' => [
  277. 'HTTP_X_OC_MTIME' => '0x123g',
  278. 'expected result' => null
  279. ],
  280. 'negative int' => [
  281. 'HTTP_X_OC_MTIME' => -34,
  282. 'expected result' => null
  283. ],
  284. 'negative float' => [
  285. 'HTTP_X_OC_MTIME' => -34.43,
  286. 'expected result' => null
  287. ],
  288. ];
  289. }
  290. /**
  291. * Test putting a file with string Mtime
  292. * @dataProvider legalMtimeProvider
  293. */
  294. public function testPutSingleFileLegalMtime($requestMtime, $resultMtime): void {
  295. $request = new Request([
  296. 'server' => [
  297. 'HTTP_X_OC_MTIME' => (string)$requestMtime,
  298. ]
  299. ], $this->requestId, $this->config, null);
  300. $file = 'foo.txt';
  301. if ($resultMtime === null) {
  302. $this->expectException(\InvalidArgumentException::class);
  303. }
  304. $this->doPut($file, null, $request);
  305. if ($resultMtime !== null) {
  306. $this->assertEquals($resultMtime, $this->getFileInfos($file)['mtime']);
  307. }
  308. }
  309. /**
  310. * Test that putting a file triggers create hooks
  311. */
  312. public function testPutSingleFileTriggersHooks(): void {
  313. HookHelper::setUpHooks();
  314. $this->assertNotEmpty($this->doPut('/foo.txt'));
  315. $this->assertCount(4, HookHelper::$hookCalls);
  316. $this->assertHookCall(
  317. HookHelper::$hookCalls[0],
  318. Filesystem::signal_create,
  319. '/foo.txt'
  320. );
  321. $this->assertHookCall(
  322. HookHelper::$hookCalls[1],
  323. Filesystem::signal_write,
  324. '/foo.txt'
  325. );
  326. $this->assertHookCall(
  327. HookHelper::$hookCalls[2],
  328. Filesystem::signal_post_create,
  329. '/foo.txt'
  330. );
  331. $this->assertHookCall(
  332. HookHelper::$hookCalls[3],
  333. Filesystem::signal_post_write,
  334. '/foo.txt'
  335. );
  336. }
  337. /**
  338. * Test that putting a file triggers update hooks
  339. */
  340. public function testPutOverwriteFileTriggersHooks(): void {
  341. $view = Filesystem::getView();
  342. $view->file_put_contents('/foo.txt', 'some content that will be replaced');
  343. HookHelper::setUpHooks();
  344. $this->assertNotEmpty($this->doPut('/foo.txt'));
  345. $this->assertCount(4, HookHelper::$hookCalls);
  346. $this->assertHookCall(
  347. HookHelper::$hookCalls[0],
  348. Filesystem::signal_update,
  349. '/foo.txt'
  350. );
  351. $this->assertHookCall(
  352. HookHelper::$hookCalls[1],
  353. Filesystem::signal_write,
  354. '/foo.txt'
  355. );
  356. $this->assertHookCall(
  357. HookHelper::$hookCalls[2],
  358. Filesystem::signal_post_update,
  359. '/foo.txt'
  360. );
  361. $this->assertHookCall(
  362. HookHelper::$hookCalls[3],
  363. Filesystem::signal_post_write,
  364. '/foo.txt'
  365. );
  366. }
  367. /**
  368. * Test that putting a file triggers hooks with the correct path
  369. * if the passed view was chrooted (can happen with public webdav
  370. * where the root is the share root)
  371. */
  372. public function testPutSingleFileTriggersHooksDifferentRoot(): void {
  373. $view = Filesystem::getView();
  374. $view->mkdir('noderoot');
  375. HookHelper::setUpHooks();
  376. // happens with public webdav where the view root is the share root
  377. $this->assertNotEmpty($this->doPut('/foo.txt', '/' . $this->user . '/files/noderoot'));
  378. $this->assertCount(4, HookHelper::$hookCalls);
  379. $this->assertHookCall(
  380. HookHelper::$hookCalls[0],
  381. Filesystem::signal_create,
  382. '/noderoot/foo.txt'
  383. );
  384. $this->assertHookCall(
  385. HookHelper::$hookCalls[1],
  386. Filesystem::signal_write,
  387. '/noderoot/foo.txt'
  388. );
  389. $this->assertHookCall(
  390. HookHelper::$hookCalls[2],
  391. Filesystem::signal_post_create,
  392. '/noderoot/foo.txt'
  393. );
  394. $this->assertHookCall(
  395. HookHelper::$hookCalls[3],
  396. Filesystem::signal_post_write,
  397. '/noderoot/foo.txt'
  398. );
  399. }
  400. public static function cancellingHook($params): void {
  401. self::$hookCalls[] = [
  402. 'signal' => Filesystem::signal_post_create,
  403. 'params' => $params
  404. ];
  405. }
  406. /**
  407. * Test put file with cancelled hook
  408. */
  409. public function testPutSingleFileCancelPreHook(): void {
  410. Util::connectHook(
  411. Filesystem::CLASSNAME,
  412. Filesystem::signal_create,
  413. '\Test\HookHelper',
  414. 'cancellingCallback'
  415. );
  416. // action
  417. $thrown = false;
  418. try {
  419. $this->doPut('/foo.txt');
  420. } catch (\Sabre\DAV\Exception $e) {
  421. $thrown = true;
  422. }
  423. $this->assertTrue($thrown);
  424. $this->assertEmpty($this->listPartFiles(), 'No stray part files');
  425. }
  426. /**
  427. * Test exception when the uploaded size did not match
  428. */
  429. public function testSimplePutFailsSizeCheck(): void {
  430. // setup
  431. /** @var View|MockObject */
  432. $view = $this->getMockBuilder(View::class)
  433. ->onlyMethods(['rename', 'getRelativePath', 'filesize'])
  434. ->getMock();
  435. $view->expects($this->any())
  436. ->method('rename')
  437. ->withAnyParameters()
  438. ->willReturn(false);
  439. $view->expects($this->any())
  440. ->method('getRelativePath')
  441. ->willReturnArgument(0);
  442. $view->expects($this->any())
  443. ->method('filesize')
  444. ->willReturn(123456);
  445. $request = new Request([
  446. 'server' => [
  447. 'CONTENT_LENGTH' => '123456',
  448. ],
  449. 'method' => 'PUT',
  450. ], $this->requestId, $this->config, null);
  451. $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [
  452. 'permissions' => Constants::PERMISSION_ALL,
  453. 'type' => FileInfo::TYPE_FOLDER,
  454. ], null);
  455. $file = new File($view, $info, null, $request);
  456. // action
  457. $thrown = false;
  458. try {
  459. // beforeMethod locks
  460. $file->acquireLock(ILockingProvider::LOCK_SHARED);
  461. $file->put($this->getStream('test data'));
  462. // afterMethod unlocks
  463. $file->releaseLock(ILockingProvider::LOCK_SHARED);
  464. } catch (\Sabre\DAV\Exception\BadRequest $e) {
  465. $thrown = true;
  466. }
  467. $this->assertTrue($thrown);
  468. $this->assertEmpty($this->listPartFiles($view, ''), 'No stray part files');
  469. }
  470. /**
  471. * Test exception during final rename in simple upload mode
  472. */
  473. public function testSimplePutFailsMoveFromStorage(): void {
  474. $view = new View('/' . $this->user . '/files');
  475. // simulate situation where the target file is locked
  476. $view->lockFile('/test.txt', ILockingProvider::LOCK_EXCLUSIVE);
  477. $info = new \OC\Files\FileInfo('/' . $this->user . '/files/test.txt', $this->getMockStorage(), null, [
  478. 'permissions' => Constants::PERMISSION_ALL,
  479. 'type' => FileInfo::TYPE_FOLDER,
  480. ], null);
  481. $file = new File($view, $info);
  482. // action
  483. $thrown = false;
  484. try {
  485. // beforeMethod locks
  486. $view->lockFile($info->getPath(), ILockingProvider::LOCK_SHARED);
  487. $file->put($this->getStream('test data'));
  488. // afterMethod unlocks
  489. $view->unlockFile($info->getPath(), ILockingProvider::LOCK_SHARED);
  490. } catch (FileLocked $e) {
  491. $thrown = true;
  492. }
  493. $this->assertTrue($thrown);
  494. $this->assertEmpty($this->listPartFiles($view, ''), 'No stray part files');
  495. }
  496. /**
  497. * Test put file with invalid chars
  498. */
  499. public function testSimplePutInvalidChars(): void {
  500. // setup
  501. /** @var View|MockObject */
  502. $view = $this->getMockBuilder(View::class)
  503. ->onlyMethods(['getRelativePath'])
  504. ->getMock();
  505. $view->expects($this->any())
  506. ->method('getRelativePath')
  507. ->willReturnArgument(0);
  508. $info = new \OC\Files\FileInfo("/i\nvalid", $this->getMockStorage(), null, [
  509. 'permissions' => Constants::PERMISSION_ALL,
  510. 'type' => FileInfo::TYPE_FOLDER,
  511. ], null);
  512. $file = new File($view, $info);
  513. // action
  514. $thrown = false;
  515. try {
  516. // beforeMethod locks
  517. $view->lockFile($info->getPath(), ILockingProvider::LOCK_SHARED);
  518. $file->put($this->getStream('test data'));
  519. // afterMethod unlocks
  520. $view->unlockFile($info->getPath(), ILockingProvider::LOCK_SHARED);
  521. } catch (InvalidPath $e) {
  522. $thrown = true;
  523. }
  524. $this->assertTrue($thrown);
  525. $this->assertEmpty($this->listPartFiles($view, ''), 'No stray part files');
  526. }
  527. /**
  528. * Test setting name with setName() with invalid chars
  529. *
  530. */
  531. public function testSetNameInvalidChars(): void {
  532. $this->expectException(InvalidPath::class);
  533. // setup
  534. /** @var View|MockObject */
  535. $view = $this->getMockBuilder(View::class)
  536. ->onlyMethods(['getRelativePath'])
  537. ->getMock();
  538. $view->expects($this->any())
  539. ->method('getRelativePath')
  540. ->willReturnArgument(0);
  541. $info = new \OC\Files\FileInfo('/valid', $this->getMockStorage(), null, [
  542. 'permissions' => Constants::PERMISSION_ALL,
  543. 'type' => FileInfo::TYPE_FOLDER,
  544. ], null);
  545. $file = new File($view, $info);
  546. $file->setName("/i\nvalid");
  547. }
  548. public function testUploadAbort(): void {
  549. // setup
  550. /** @var View|MockObject */
  551. $view = $this->getMockBuilder(View::class)
  552. ->onlyMethods(['rename', 'getRelativePath', 'filesize'])
  553. ->getMock();
  554. $view->expects($this->any())
  555. ->method('rename')
  556. ->withAnyParameters()
  557. ->willReturn(false);
  558. $view->expects($this->any())
  559. ->method('getRelativePath')
  560. ->willReturnArgument(0);
  561. $view->expects($this->any())
  562. ->method('filesize')
  563. ->willReturn(123456);
  564. $request = new Request([
  565. 'server' => [
  566. 'CONTENT_LENGTH' => '123456',
  567. ],
  568. 'method' => 'PUT',
  569. ], $this->requestId, $this->config, null);
  570. $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [
  571. 'permissions' => Constants::PERMISSION_ALL,
  572. 'type' => FileInfo::TYPE_FOLDER,
  573. ], null);
  574. $file = new File($view, $info, null, $request);
  575. // action
  576. $thrown = false;
  577. try {
  578. // beforeMethod locks
  579. $view->lockFile($info->getPath(), ILockingProvider::LOCK_SHARED);
  580. $file->put($this->getStream('test data'));
  581. // afterMethod unlocks
  582. $view->unlockFile($info->getPath(), ILockingProvider::LOCK_SHARED);
  583. } catch (\Sabre\DAV\Exception\BadRequest $e) {
  584. $thrown = true;
  585. }
  586. $this->assertTrue($thrown);
  587. $this->assertEmpty($this->listPartFiles($view, ''), 'No stray part files');
  588. }
  589. public function testDeleteWhenAllowed(): void {
  590. // setup
  591. /** @var View|MockObject */
  592. $view = $this->getMockBuilder(View::class)
  593. ->getMock();
  594. $view->expects($this->once())
  595. ->method('unlink')
  596. ->willReturn(true);
  597. $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [
  598. 'permissions' => Constants::PERMISSION_ALL,
  599. 'type' => FileInfo::TYPE_FOLDER,
  600. ], null);
  601. $file = new File($view, $info);
  602. // action
  603. $file->delete();
  604. }
  605. public function testDeleteThrowsWhenDeletionNotAllowed(): void {
  606. $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
  607. // setup
  608. /** @var View|MockObject */
  609. $view = $this->getMockBuilder(View::class)
  610. ->getMock();
  611. $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [
  612. 'permissions' => 0,
  613. 'type' => FileInfo::TYPE_FOLDER,
  614. ], null);
  615. $file = new File($view, $info);
  616. // action
  617. $file->delete();
  618. }
  619. public function testDeleteThrowsWhenDeletionFailed(): void {
  620. $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
  621. // setup
  622. /** @var View|MockObject */
  623. $view = $this->getMockBuilder(View::class)
  624. ->getMock();
  625. // but fails
  626. $view->expects($this->once())
  627. ->method('unlink')
  628. ->willReturn(false);
  629. $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [
  630. 'permissions' => Constants::PERMISSION_ALL,
  631. 'type' => FileInfo::TYPE_FOLDER,
  632. ], null);
  633. $file = new File($view, $info);
  634. // action
  635. $file->delete();
  636. }
  637. public function testDeleteThrowsWhenDeletionThrows(): void {
  638. $this->expectException(Forbidden::class);
  639. // setup
  640. /** @var View|MockObject */
  641. $view = $this->getMockBuilder(View::class)
  642. ->getMock();
  643. // but fails
  644. $view->expects($this->once())
  645. ->method('unlink')
  646. ->willThrowException(new ForbiddenException('', true));
  647. $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [
  648. 'permissions' => Constants::PERMISSION_ALL,
  649. 'type' => FileInfo::TYPE_FOLDER,
  650. ], null);
  651. $file = new File($view, $info);
  652. // action
  653. $file->delete();
  654. }
  655. /**
  656. * Asserts hook call
  657. *
  658. * @param array $callData hook call data to check
  659. * @param string $signal signal name
  660. * @param string $hookPath hook path
  661. */
  662. protected function assertHookCall($callData, $signal, $hookPath) {
  663. $this->assertEquals($signal, $callData['signal']);
  664. $params = $callData['params'];
  665. $this->assertEquals(
  666. $hookPath,
  667. $params[Filesystem::signal_param_path]
  668. );
  669. }
  670. /**
  671. * Test whether locks are set before and after the operation
  672. */
  673. public function testPutLocking(): void {
  674. $view = new View('/' . $this->user . '/files/');
  675. $path = 'test-locking.txt';
  676. $info = new \OC\Files\FileInfo(
  677. '/' . $this->user . '/files/' . $path,
  678. $this->getMockStorage(),
  679. null,
  680. [
  681. 'permissions' => Constants::PERMISSION_ALL,
  682. 'type' => FileInfo::TYPE_FOLDER,
  683. ],
  684. null
  685. );
  686. $file = new File($view, $info);
  687. $this->assertFalse(
  688. $this->isFileLocked($view, $path, ILockingProvider::LOCK_SHARED),
  689. 'File unlocked before put'
  690. );
  691. $this->assertFalse(
  692. $this->isFileLocked($view, $path, ILockingProvider::LOCK_EXCLUSIVE),
  693. 'File unlocked before put'
  694. );
  695. $wasLockedPre = false;
  696. $wasLockedPost = false;
  697. $eventHandler = $this->getMockBuilder(\stdclass::class)
  698. ->addMethods(['writeCallback', 'postWriteCallback'])
  699. ->getMock();
  700. // both pre and post hooks might need access to the file,
  701. // so only shared lock is acceptable
  702. $eventHandler->expects($this->once())
  703. ->method('writeCallback')
  704. ->willReturnCallback(
  705. function () use ($view, $path, &$wasLockedPre): void {
  706. $wasLockedPre = $this->isFileLocked($view, $path, ILockingProvider::LOCK_SHARED);
  707. $wasLockedPre = $wasLockedPre && !$this->isFileLocked($view, $path, ILockingProvider::LOCK_EXCLUSIVE);
  708. }
  709. );
  710. $eventHandler->expects($this->once())
  711. ->method('postWriteCallback')
  712. ->willReturnCallback(
  713. function () use ($view, $path, &$wasLockedPost): void {
  714. $wasLockedPost = $this->isFileLocked($view, $path, ILockingProvider::LOCK_SHARED);
  715. $wasLockedPost = $wasLockedPost && !$this->isFileLocked($view, $path, ILockingProvider::LOCK_EXCLUSIVE);
  716. }
  717. );
  718. Util::connectHook(
  719. Filesystem::CLASSNAME,
  720. Filesystem::signal_write,
  721. $eventHandler,
  722. 'writeCallback'
  723. );
  724. Util::connectHook(
  725. Filesystem::CLASSNAME,
  726. Filesystem::signal_post_write,
  727. $eventHandler,
  728. 'postWriteCallback'
  729. );
  730. // beforeMethod locks
  731. $view->lockFile($path, ILockingProvider::LOCK_SHARED);
  732. $this->assertNotEmpty($file->put($this->getStream('test data')));
  733. // afterMethod unlocks
  734. $view->unlockFile($path, ILockingProvider::LOCK_SHARED);
  735. $this->assertTrue($wasLockedPre, 'File was locked during pre-hooks');
  736. $this->assertTrue($wasLockedPost, 'File was locked during post-hooks');
  737. $this->assertFalse(
  738. $this->isFileLocked($view, $path, ILockingProvider::LOCK_SHARED),
  739. 'File unlocked after put'
  740. );
  741. $this->assertFalse(
  742. $this->isFileLocked($view, $path, ILockingProvider::LOCK_EXCLUSIVE),
  743. 'File unlocked after put'
  744. );
  745. }
  746. /**
  747. * Returns part files in the given path
  748. *
  749. * @param \OC\Files\View view which root is the current user's "files" folder
  750. * @param string $path path for which to list part files
  751. *
  752. * @return array list of part files
  753. */
  754. private function listPartFiles(?View $userView = null, $path = '') {
  755. if ($userView === null) {
  756. $userView = Filesystem::getView();
  757. }
  758. $files = [];
  759. [$storage, $internalPath] = $userView->resolvePath($path);
  760. if ($storage instanceof Local) {
  761. $realPath = $storage->getSourcePath($internalPath);
  762. $dh = opendir($realPath);
  763. while (($file = readdir($dh)) !== false) {
  764. if (str_ends_with($file, '.part')) {
  765. $files[] = $file;
  766. }
  767. }
  768. closedir($dh);
  769. }
  770. return $files;
  771. }
  772. /**
  773. * returns an array of file information filesize, mtime, filetype, mimetype
  774. *
  775. * @param string $path
  776. * @param View $userView
  777. * @return array
  778. */
  779. private function getFileInfos($path = '', ?View $userView = null) {
  780. if ($userView === null) {
  781. $userView = Filesystem::getView();
  782. }
  783. return [
  784. 'filesize' => $userView->filesize($path),
  785. 'mtime' => $userView->filemtime($path),
  786. 'filetype' => $userView->filetype($path),
  787. 'mimetype' => $userView->getMimeType($path)
  788. ];
  789. }
  790. public function testGetFopenFails(): void {
  791. $this->expectException(\Sabre\DAV\Exception\ServiceUnavailable::class);
  792. /** @var View|MockObject */
  793. $view = $this->getMockBuilder(View::class)
  794. ->onlyMethods(['fopen'])
  795. ->getMock();
  796. $view->expects($this->atLeastOnce())
  797. ->method('fopen')
  798. ->willReturn(false);
  799. $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [
  800. 'permissions' => Constants::PERMISSION_ALL,
  801. 'type' => FileInfo::TYPE_FILE,
  802. ], null);
  803. $file = new File($view, $info);
  804. $file->get();
  805. }
  806. public function testGetFopenThrows(): void {
  807. $this->expectException(Forbidden::class);
  808. /** @var View|MockObject */
  809. $view = $this->getMockBuilder(View::class)
  810. ->onlyMethods(['fopen'])
  811. ->getMock();
  812. $view->expects($this->atLeastOnce())
  813. ->method('fopen')
  814. ->willThrowException(new ForbiddenException('', true));
  815. $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [
  816. 'permissions' => Constants::PERMISSION_ALL,
  817. 'type' => FileInfo::TYPE_FILE,
  818. ], null);
  819. $file = new File($view, $info);
  820. $file->get();
  821. }
  822. public function testGetThrowsIfNoPermission(): void {
  823. $this->expectException(\Sabre\DAV\Exception\NotFound::class);
  824. /** @var View|MockObject */
  825. $view = $this->getMockBuilder(View::class)
  826. ->onlyMethods(['fopen'])
  827. ->getMock();
  828. $view->expects($this->never())
  829. ->method('fopen');
  830. $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [
  831. 'permissions' => Constants::PERMISSION_CREATE, // no read perm
  832. 'type' => FileInfo::TYPE_FOLDER,
  833. ], null);
  834. $file = new File($view, $info);
  835. $file->get();
  836. }
  837. public function testSimplePutNoCreatePermissions(): void {
  838. $this->logout();
  839. $storage = new Temporary([]);
  840. $storage->file_put_contents('file.txt', 'old content');
  841. $noCreateStorage = new PermissionsMask([
  842. 'storage' => $storage,
  843. 'mask' => Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE
  844. ]);
  845. $this->registerMount($this->user, $noCreateStorage, '/' . $this->user . '/files/root');
  846. $this->loginAsUser($this->user);
  847. $view = new View('/' . $this->user . '/files');
  848. $info = $view->getFileInfo('root/file.txt');
  849. $file = new File($view, $info);
  850. // beforeMethod locks
  851. $view->lockFile('root/file.txt', ILockingProvider::LOCK_SHARED);
  852. $file->put($this->getStream('new content'));
  853. // afterMethod unlocks
  854. $view->unlockFile('root/file.txt', ILockingProvider::LOCK_SHARED);
  855. $this->assertEquals('new content', $view->file_get_contents('root/file.txt'));
  856. }
  857. public function testPutLockExpired(): void {
  858. $view = new View('/' . $this->user . '/files/');
  859. $path = 'test-locking.txt';
  860. $info = new \OC\Files\FileInfo(
  861. '/' . $this->user . '/files/' . $path,
  862. $this->getMockStorage(),
  863. null,
  864. [
  865. 'permissions' => Constants::PERMISSION_ALL,
  866. 'type' => FileInfo::TYPE_FOLDER,
  867. ],
  868. null
  869. );
  870. $file = new File($view, $info);
  871. // don't lock before the PUT to simulate an expired shared lock
  872. $this->assertNotEmpty($file->put($this->getStream('test data')));
  873. // afterMethod unlocks
  874. $view->unlockFile($path, ILockingProvider::LOCK_SHARED);
  875. }
  876. }