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.

FileTest.php 33KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016, ownCloud, Inc.
  4. *
  5. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  6. * @author Daniel Calviño Sánchez <danxuliu@gmail.com>
  7. * @author Daniel Kesselberg <mail@danielkesselberg.de>
  8. * @author Joas Schilling <coding@schilljs.com>
  9. * @author Morris Jobke <hey@morrisjobke.de>
  10. * @author Robin Appelman <robin@icewind.nl>
  11. * @author Roeland Jago Douma <roeland@famdouma.nl>
  12. * @author Thomas Müller <thomas.mueller@tmit.eu>
  13. * @author Vincent Petry <vincent@nextcloud.com>
  14. *
  15. * @license AGPL-3.0
  16. *
  17. * This code is free software: you can redistribute it and/or modify
  18. * it under the terms of the GNU Affero General Public License, version 3,
  19. * as published by the Free Software Foundation.
  20. *
  21. * This program is distributed in the hope that it will be useful,
  22. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  23. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  24. * GNU Affero General Public License for more details.
  25. *
  26. * You should have received a copy of the GNU Affero General Public License, version 3,
  27. * along with this program. If not, see <http://www.gnu.org/licenses/>
  28. *
  29. */
  30. namespace OCA\DAV\Tests\unit\Connector\Sabre;
  31. use OC\AppFramework\Http\Request;
  32. use OC\Files\Filesystem;
  33. use OC\Files\Storage\Local;
  34. use OC\Files\Storage\Temporary;
  35. use OC\Files\Storage\Wrapper\PermissionsMask;
  36. use OC\Files\View;
  37. use OC\Security\SecureRandom;
  38. use OCA\DAV\Connector\Sabre\File;
  39. use OCP\Constants;
  40. use OCP\Files\ForbiddenException;
  41. use OCP\Files\Storage;
  42. use OCP\IConfig;
  43. use OCP\Lock\ILockingProvider;
  44. use OCP\Security\ISecureRandom;
  45. use Test\HookHelper;
  46. use Test\TestCase;
  47. use Test\Traits\MountProviderTrait;
  48. use Test\Traits\UserTrait;
  49. /**
  50. * Class File
  51. *
  52. * @group DB
  53. *
  54. * @package OCA\DAV\Tests\unit\Connector\Sabre
  55. */
  56. class FileTest extends TestCase {
  57. use MountProviderTrait;
  58. use UserTrait;
  59. /**
  60. * @var string
  61. */
  62. private $user;
  63. /** @var IConfig | \PHPUnit\Framework\MockObject\MockObject */
  64. protected $config;
  65. /** @var ISecureRandom */
  66. protected $secureRandom;
  67. protected function setUp(): void {
  68. parent::setUp();
  69. unset($_SERVER['HTTP_OC_CHUNKED']);
  70. unset($_SERVER['CONTENT_LENGTH']);
  71. unset($_SERVER['REQUEST_METHOD']);
  72. \OC_Hook::clear();
  73. $this->user = 'test_user';
  74. $this->createUser($this->user, 'pass');
  75. $this->loginAsUser($this->user);
  76. $this->config = $this->getMockBuilder('\OCP\IConfig')->getMock();
  77. $this->secureRandom = new SecureRandom();
  78. }
  79. protected function tearDown(): void {
  80. $userManager = \OC::$server->getUserManager();
  81. $userManager->get($this->user)->delete();
  82. unset($_SERVER['HTTP_OC_CHUNKED']);
  83. parent::tearDown();
  84. }
  85. /**
  86. * @return \PHPUnit\Framework\MockObject\MockObject|Storage
  87. */
  88. private function getMockStorage() {
  89. $storage = $this->getMockBuilder(Storage::class)
  90. ->disableOriginalConstructor()
  91. ->getMock();
  92. $storage->method('getId')
  93. ->willReturn('home::someuser');
  94. return $storage;
  95. }
  96. /**
  97. * @param string $string
  98. */
  99. private function getStream($string) {
  100. $stream = fopen('php://temp', 'r+');
  101. fwrite($stream, $string);
  102. fseek($stream, 0);
  103. return $stream;
  104. }
  105. public function fopenFailuresProvider() {
  106. return [
  107. [
  108. // return false
  109. null,
  110. '\Sabre\Dav\Exception',
  111. false
  112. ],
  113. [
  114. new \OCP\Files\NotPermittedException(),
  115. 'Sabre\DAV\Exception\Forbidden'
  116. ],
  117. [
  118. new \OCP\Files\EntityTooLargeException(),
  119. 'OCA\DAV\Connector\Sabre\Exception\EntityTooLarge'
  120. ],
  121. [
  122. new \OCP\Files\InvalidContentException(),
  123. 'OCA\DAV\Connector\Sabre\Exception\UnsupportedMediaType'
  124. ],
  125. [
  126. new \OCP\Files\InvalidPathException(),
  127. 'Sabre\DAV\Exception\Forbidden'
  128. ],
  129. [
  130. new \OCP\Files\ForbiddenException('', true),
  131. 'OCA\DAV\Connector\Sabre\Exception\Forbidden'
  132. ],
  133. [
  134. new \OCP\Files\LockNotAcquiredException('/test.txt', 1),
  135. 'OCA\DAV\Connector\Sabre\Exception\FileLocked'
  136. ],
  137. [
  138. new \OCP\Lock\LockedException('/test.txt'),
  139. 'OCA\DAV\Connector\Sabre\Exception\FileLocked'
  140. ],
  141. [
  142. new \OCP\Encryption\Exceptions\GenericEncryptionException(),
  143. 'Sabre\DAV\Exception\ServiceUnavailable'
  144. ],
  145. [
  146. new \OCP\Files\StorageNotAvailableException(),
  147. 'Sabre\DAV\Exception\ServiceUnavailable'
  148. ],
  149. [
  150. new \Sabre\DAV\Exception('Generic sabre exception'),
  151. 'Sabre\DAV\Exception',
  152. false
  153. ],
  154. [
  155. new \Exception('Generic exception'),
  156. 'Sabre\DAV\Exception'
  157. ],
  158. ];
  159. }
  160. /**
  161. * @dataProvider fopenFailuresProvider
  162. */
  163. public function testSimplePutFails($thrownException, $expectedException, $checkPreviousClass = true) {
  164. // setup
  165. $storage = $this->getMockBuilder(Local::class)
  166. ->setMethods(['writeStream'])
  167. ->setConstructorArgs([['datadir' => \OC::$server->getTempManager()->getTemporaryFolder()]])
  168. ->getMock();
  169. \OC\Files\Filesystem::mount($storage, [], $this->user . '/');
  170. /** @var View | \PHPUnit\Framework\MockObject\MockObject $view */
  171. $view = $this->getMockBuilder(View::class)
  172. ->setMethods(['getRelativePath', 'resolvePath'])
  173. ->getMock();
  174. $view->expects($this->atLeastOnce())
  175. ->method('resolvePath')
  176. ->willReturnCallback(
  177. function ($path) use ($storage) {
  178. return [$storage, $path];
  179. }
  180. );
  181. if ($thrownException !== null) {
  182. $storage->expects($this->once())
  183. ->method('writeStream')
  184. ->will($this->throwException($thrownException));
  185. } else {
  186. $storage->expects($this->once())
  187. ->method('writeStream')
  188. ->willReturn(0);
  189. }
  190. $view->expects($this->any())
  191. ->method('getRelativePath')
  192. ->willReturnArgument(0);
  193. $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [
  194. 'permissions' => \OCP\Constants::PERMISSION_ALL
  195. ], null);
  196. $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
  197. // action
  198. $caughtException = null;
  199. try {
  200. $file->put('test data');
  201. } catch (\Exception $e) {
  202. $caughtException = $e;
  203. }
  204. $this->assertInstanceOf($expectedException, $caughtException);
  205. if ($checkPreviousClass) {
  206. $this->assertInstanceOf(get_class($thrownException), $caughtException->getPrevious());
  207. }
  208. $this->assertEmpty($this->listPartFiles($view, ''), 'No stray part files');
  209. }
  210. /**
  211. * Test putting a file using chunking
  212. *
  213. * @dataProvider fopenFailuresProvider
  214. */
  215. public function testChunkedPutFails($thrownException, $expectedException, $checkPreviousClass = false) {
  216. // setup
  217. $storage = $this->getMockBuilder(Local::class)
  218. ->setMethods(['fopen'])
  219. ->setConstructorArgs([['datadir' => \OC::$server->getTempManager()->getTemporaryFolder()]])
  220. ->getMock();
  221. \OC\Files\Filesystem::mount($storage, [], $this->user . '/');
  222. $view = $this->getMockBuilder(View::class)
  223. ->setMethods(['getRelativePath', 'resolvePath'])
  224. ->getMock();
  225. $view->expects($this->atLeastOnce())
  226. ->method('resolvePath')
  227. ->willReturnCallback(
  228. function ($path) use ($storage) {
  229. return [$storage, $path];
  230. }
  231. );
  232. if ($thrownException !== null) {
  233. $storage->expects($this->once())
  234. ->method('fopen')
  235. ->will($this->throwException($thrownException));
  236. } else {
  237. $storage->expects($this->once())
  238. ->method('fopen')
  239. ->willReturn(false);
  240. }
  241. $view->expects($this->any())
  242. ->method('getRelativePath')
  243. ->willReturnArgument(0);
  244. $_SERVER['HTTP_OC_CHUNKED'] = true;
  245. $info = new \OC\Files\FileInfo('/test.txt-chunking-12345-2-0', $this->getMockStorage(), null, [
  246. 'permissions' => \OCP\Constants::PERMISSION_ALL
  247. ], null);
  248. $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
  249. // put first chunk
  250. $file->acquireLock(ILockingProvider::LOCK_SHARED);
  251. $this->assertNull($file->put('test data one'));
  252. $file->releaseLock(ILockingProvider::LOCK_SHARED);
  253. $info = new \OC\Files\FileInfo('/test.txt-chunking-12345-2-1', $this->getMockStorage(), null, [
  254. 'permissions' => \OCP\Constants::PERMISSION_ALL
  255. ], null);
  256. $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
  257. // action
  258. $caughtException = null;
  259. try {
  260. // last chunk
  261. $file->acquireLock(ILockingProvider::LOCK_SHARED);
  262. $file->put('test data two');
  263. $file->releaseLock(ILockingProvider::LOCK_SHARED);
  264. } catch (\Exception $e) {
  265. $caughtException = $e;
  266. }
  267. $this->assertInstanceOf($expectedException, $caughtException);
  268. if ($checkPreviousClass) {
  269. $this->assertInstanceOf(get_class($thrownException), $caughtException->getPrevious());
  270. }
  271. $this->assertEmpty($this->listPartFiles($view, ''), 'No stray part files');
  272. }
  273. /**
  274. * Simulate putting a file to the given path.
  275. *
  276. * @param string $path path to put the file into
  277. * @param string $viewRoot root to use for the view
  278. * @param null|Request $request the HTTP request
  279. *
  280. * @return null|string of the PUT operaiton which is usually the etag
  281. */
  282. private function doPut($path, $viewRoot = null, Request $request = null) {
  283. $view = \OC\Files\Filesystem::getView();
  284. if (!is_null($viewRoot)) {
  285. $view = new \OC\Files\View($viewRoot);
  286. } else {
  287. $viewRoot = '/' . $this->user . '/files';
  288. }
  289. $info = new \OC\Files\FileInfo(
  290. $viewRoot . '/' . ltrim($path, '/'),
  291. $this->getMockStorage(),
  292. null,
  293. ['permissions' => \OCP\Constants::PERMISSION_ALL],
  294. null
  295. );
  296. /** @var \OCA\DAV\Connector\Sabre\File | \PHPUnit\Framework\MockObject\MockObject $file */
  297. $file = $this->getMockBuilder(\OCA\DAV\Connector\Sabre\File::class)
  298. ->setConstructorArgs([$view, $info, null, $request])
  299. ->setMethods(['header'])
  300. ->getMock();
  301. // beforeMethod locks
  302. $view->lockFile($path, ILockingProvider::LOCK_SHARED);
  303. $result = $file->put($this->getStream('test data'));
  304. // afterMethod unlocks
  305. $view->unlockFile($path, ILockingProvider::LOCK_SHARED);
  306. return $result;
  307. }
  308. /**
  309. * Test putting a single file
  310. */
  311. public function testPutSingleFile() {
  312. $this->assertNotEmpty($this->doPut('/foo.txt'));
  313. }
  314. public function legalMtimeProvider() {
  315. return [
  316. "string" => [
  317. 'HTTP_X_OC_MTIME' => "string",
  318. 'expected result' => null
  319. ],
  320. "castable string (int)" => [
  321. 'HTTP_X_OC_MTIME' => "34",
  322. 'expected result' => 34
  323. ],
  324. "castable string (float)" => [
  325. 'HTTP_X_OC_MTIME' => "34.56",
  326. 'expected result' => 34
  327. ],
  328. "float" => [
  329. 'HTTP_X_OC_MTIME' => 34.56,
  330. 'expected result' => 34
  331. ],
  332. "zero" => [
  333. 'HTTP_X_OC_MTIME' => 0,
  334. 'expected result' => 0
  335. ],
  336. "zero string" => [
  337. 'HTTP_X_OC_MTIME' => "0",
  338. 'expected result' => 0
  339. ],
  340. "negative zero string" => [
  341. 'HTTP_X_OC_MTIME' => "-0",
  342. 'expected result' => 0
  343. ],
  344. "string starting with number following by char" => [
  345. 'HTTP_X_OC_MTIME' => "2345asdf",
  346. 'expected result' => null
  347. ],
  348. "string castable hex int" => [
  349. 'HTTP_X_OC_MTIME' => "0x45adf",
  350. 'expected result' => null
  351. ],
  352. "string that looks like invalid hex int" => [
  353. 'HTTP_X_OC_MTIME' => "0x123g",
  354. 'expected result' => null
  355. ],
  356. "negative int" => [
  357. 'HTTP_X_OC_MTIME' => -34,
  358. 'expected result' => -34
  359. ],
  360. "negative float" => [
  361. 'HTTP_X_OC_MTIME' => -34.43,
  362. 'expected result' => -34
  363. ],
  364. ];
  365. }
  366. /**
  367. * Test putting a file with string Mtime
  368. * @dataProvider legalMtimeProvider
  369. */
  370. public function testPutSingleFileLegalMtime($requestMtime, $resultMtime) {
  371. $request = new Request([
  372. 'server' => [
  373. 'HTTP_X_OC_MTIME' => $requestMtime,
  374. ]
  375. ], $this->secureRandom, $this->config, null);
  376. $file = 'foo.txt';
  377. if ($resultMtime === null) {
  378. $this->expectException(\InvalidArgumentException::class);
  379. $this->expectExceptionMessage("X-OC-MTime header must be an integer (unix timestamp).");
  380. }
  381. $this->doPut($file, null, $request);
  382. if ($resultMtime !== null) {
  383. $this->assertEquals($resultMtime, $this->getFileInfos($file)['mtime']);
  384. }
  385. }
  386. /**
  387. * Test putting a file with string Mtime using chunking
  388. * @dataProvider legalMtimeProvider
  389. */
  390. public function testChunkedPutLegalMtime($requestMtime, $resultMtime) {
  391. $request = new Request([
  392. 'server' => [
  393. 'HTTP_X_OC_MTIME' => $requestMtime,
  394. ]
  395. ], $this->secureRandom, $this->config, null);
  396. $_SERVER['HTTP_OC_CHUNKED'] = true;
  397. $file = 'foo.txt';
  398. if ($resultMtime === null) {
  399. $this->expectException(\Sabre\DAV\Exception::class);
  400. $this->expectExceptionMessage("X-OC-MTime header must be an integer (unix timestamp).");
  401. }
  402. $this->doPut($file.'-chunking-12345-2-0', null, $request);
  403. $this->doPut($file.'-chunking-12345-2-1', null, $request);
  404. if ($resultMtime !== null) {
  405. $this->assertEquals($resultMtime, $this->getFileInfos($file)['mtime']);
  406. }
  407. }
  408. /**
  409. * Test putting a file using chunking
  410. */
  411. public function testChunkedPut() {
  412. $_SERVER['HTTP_OC_CHUNKED'] = true;
  413. $this->assertNull($this->doPut('/test.txt-chunking-12345-2-0'));
  414. $this->assertNotEmpty($this->doPut('/test.txt-chunking-12345-2-1'));
  415. }
  416. /**
  417. * Test that putting a file triggers create hooks
  418. */
  419. public function testPutSingleFileTriggersHooks() {
  420. HookHelper::setUpHooks();
  421. $this->assertNotEmpty($this->doPut('/foo.txt'));
  422. $this->assertCount(4, HookHelper::$hookCalls);
  423. $this->assertHookCall(
  424. HookHelper::$hookCalls[0],
  425. Filesystem::signal_create,
  426. '/foo.txt'
  427. );
  428. $this->assertHookCall(
  429. HookHelper::$hookCalls[1],
  430. Filesystem::signal_write,
  431. '/foo.txt'
  432. );
  433. $this->assertHookCall(
  434. HookHelper::$hookCalls[2],
  435. Filesystem::signal_post_create,
  436. '/foo.txt'
  437. );
  438. $this->assertHookCall(
  439. HookHelper::$hookCalls[3],
  440. Filesystem::signal_post_write,
  441. '/foo.txt'
  442. );
  443. }
  444. /**
  445. * Test that putting a file triggers update hooks
  446. */
  447. public function testPutOverwriteFileTriggersHooks() {
  448. $view = \OC\Files\Filesystem::getView();
  449. $view->file_put_contents('/foo.txt', 'some content that will be replaced');
  450. HookHelper::setUpHooks();
  451. $this->assertNotEmpty($this->doPut('/foo.txt'));
  452. $this->assertCount(4, HookHelper::$hookCalls);
  453. $this->assertHookCall(
  454. HookHelper::$hookCalls[0],
  455. Filesystem::signal_update,
  456. '/foo.txt'
  457. );
  458. $this->assertHookCall(
  459. HookHelper::$hookCalls[1],
  460. Filesystem::signal_write,
  461. '/foo.txt'
  462. );
  463. $this->assertHookCall(
  464. HookHelper::$hookCalls[2],
  465. Filesystem::signal_post_update,
  466. '/foo.txt'
  467. );
  468. $this->assertHookCall(
  469. HookHelper::$hookCalls[3],
  470. Filesystem::signal_post_write,
  471. '/foo.txt'
  472. );
  473. }
  474. /**
  475. * Test that putting a file triggers hooks with the correct path
  476. * if the passed view was chrooted (can happen with public webdav
  477. * where the root is the share root)
  478. */
  479. public function testPutSingleFileTriggersHooksDifferentRoot() {
  480. $view = \OC\Files\Filesystem::getView();
  481. $view->mkdir('noderoot');
  482. HookHelper::setUpHooks();
  483. // happens with public webdav where the view root is the share root
  484. $this->assertNotEmpty($this->doPut('/foo.txt', '/' . $this->user . '/files/noderoot'));
  485. $this->assertCount(4, HookHelper::$hookCalls);
  486. $this->assertHookCall(
  487. HookHelper::$hookCalls[0],
  488. Filesystem::signal_create,
  489. '/noderoot/foo.txt'
  490. );
  491. $this->assertHookCall(
  492. HookHelper::$hookCalls[1],
  493. Filesystem::signal_write,
  494. '/noderoot/foo.txt'
  495. );
  496. $this->assertHookCall(
  497. HookHelper::$hookCalls[2],
  498. Filesystem::signal_post_create,
  499. '/noderoot/foo.txt'
  500. );
  501. $this->assertHookCall(
  502. HookHelper::$hookCalls[3],
  503. Filesystem::signal_post_write,
  504. '/noderoot/foo.txt'
  505. );
  506. }
  507. /**
  508. * Test that putting a file with chunks triggers create hooks
  509. */
  510. public function testPutChunkedFileTriggersHooks() {
  511. HookHelper::setUpHooks();
  512. $_SERVER['HTTP_OC_CHUNKED'] = true;
  513. $this->assertNull($this->doPut('/foo.txt-chunking-12345-2-0'));
  514. $this->assertNotEmpty($this->doPut('/foo.txt-chunking-12345-2-1'));
  515. $this->assertCount(4, HookHelper::$hookCalls);
  516. $this->assertHookCall(
  517. HookHelper::$hookCalls[0],
  518. Filesystem::signal_create,
  519. '/foo.txt'
  520. );
  521. $this->assertHookCall(
  522. HookHelper::$hookCalls[1],
  523. Filesystem::signal_write,
  524. '/foo.txt'
  525. );
  526. $this->assertHookCall(
  527. HookHelper::$hookCalls[2],
  528. Filesystem::signal_post_create,
  529. '/foo.txt'
  530. );
  531. $this->assertHookCall(
  532. HookHelper::$hookCalls[3],
  533. Filesystem::signal_post_write,
  534. '/foo.txt'
  535. );
  536. }
  537. /**
  538. * Test that putting a chunked file triggers update hooks
  539. */
  540. public function testPutOverwriteChunkedFileTriggersHooks() {
  541. $view = \OC\Files\Filesystem::getView();
  542. $view->file_put_contents('/foo.txt', 'some content that will be replaced');
  543. HookHelper::setUpHooks();
  544. $_SERVER['HTTP_OC_CHUNKED'] = true;
  545. $this->assertNull($this->doPut('/foo.txt-chunking-12345-2-0'));
  546. $this->assertNotEmpty($this->doPut('/foo.txt-chunking-12345-2-1'));
  547. $this->assertCount(4, HookHelper::$hookCalls);
  548. $this->assertHookCall(
  549. HookHelper::$hookCalls[0],
  550. Filesystem::signal_update,
  551. '/foo.txt'
  552. );
  553. $this->assertHookCall(
  554. HookHelper::$hookCalls[1],
  555. Filesystem::signal_write,
  556. '/foo.txt'
  557. );
  558. $this->assertHookCall(
  559. HookHelper::$hookCalls[2],
  560. Filesystem::signal_post_update,
  561. '/foo.txt'
  562. );
  563. $this->assertHookCall(
  564. HookHelper::$hookCalls[3],
  565. Filesystem::signal_post_write,
  566. '/foo.txt'
  567. );
  568. }
  569. public static function cancellingHook($params) {
  570. self::$hookCalls[] = [
  571. 'signal' => Filesystem::signal_post_create,
  572. 'params' => $params
  573. ];
  574. }
  575. /**
  576. * Test put file with cancelled hook
  577. */
  578. public function testPutSingleFileCancelPreHook() {
  579. \OCP\Util::connectHook(
  580. Filesystem::CLASSNAME,
  581. Filesystem::signal_create,
  582. '\Test\HookHelper',
  583. 'cancellingCallback'
  584. );
  585. // action
  586. $thrown = false;
  587. try {
  588. $this->doPut('/foo.txt');
  589. } catch (\Sabre\DAV\Exception $e) {
  590. $thrown = true;
  591. }
  592. $this->assertTrue($thrown);
  593. $this->assertEmpty($this->listPartFiles(), 'No stray part files');
  594. }
  595. /**
  596. * Test exception when the uploaded size did not match
  597. */
  598. public function testSimplePutFailsSizeCheck() {
  599. // setup
  600. $view = $this->getMockBuilder(View::class)
  601. ->setMethods(['rename', 'getRelativePath', 'filesize'])
  602. ->getMock();
  603. $view->expects($this->any())
  604. ->method('rename')
  605. ->withAnyParameters()
  606. ->willReturn(false);
  607. $view->expects($this->any())
  608. ->method('getRelativePath')
  609. ->willReturnArgument(0);
  610. $view->expects($this->any())
  611. ->method('filesize')
  612. ->willReturn(123456);
  613. $_SERVER['CONTENT_LENGTH'] = 123456;
  614. $_SERVER['REQUEST_METHOD'] = 'PUT';
  615. $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [
  616. 'permissions' => \OCP\Constants::PERMISSION_ALL
  617. ], null);
  618. $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
  619. // action
  620. $thrown = false;
  621. try {
  622. // beforeMethod locks
  623. $file->acquireLock(ILockingProvider::LOCK_SHARED);
  624. $file->put($this->getStream('test data'));
  625. // afterMethod unlocks
  626. $file->releaseLock(ILockingProvider::LOCK_SHARED);
  627. } catch (\Sabre\DAV\Exception\BadRequest $e) {
  628. $thrown = true;
  629. }
  630. $this->assertTrue($thrown);
  631. $this->assertEmpty($this->listPartFiles($view, ''), 'No stray part files');
  632. }
  633. /**
  634. * Test exception during final rename in simple upload mode
  635. */
  636. public function testSimplePutFailsMoveFromStorage() {
  637. $view = new \OC\Files\View('/' . $this->user . '/files');
  638. // simulate situation where the target file is locked
  639. $view->lockFile('/test.txt', ILockingProvider::LOCK_EXCLUSIVE);
  640. $info = new \OC\Files\FileInfo('/' . $this->user . '/files/test.txt', $this->getMockStorage(), null, [
  641. 'permissions' => \OCP\Constants::PERMISSION_ALL
  642. ], null);
  643. $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
  644. // action
  645. $thrown = false;
  646. try {
  647. // beforeMethod locks
  648. $view->lockFile($info->getPath(), ILockingProvider::LOCK_SHARED);
  649. $file->put($this->getStream('test data'));
  650. // afterMethod unlocks
  651. $view->unlockFile($info->getPath(), ILockingProvider::LOCK_SHARED);
  652. } catch (\OCA\DAV\Connector\Sabre\Exception\FileLocked $e) {
  653. $thrown = true;
  654. }
  655. $this->assertTrue($thrown);
  656. $this->assertEmpty($this->listPartFiles($view, ''), 'No stray part files');
  657. }
  658. /**
  659. * Test exception during final rename in chunk upload mode
  660. */
  661. public function testChunkedPutFailsFinalRename() {
  662. $view = new \OC\Files\View('/' . $this->user . '/files');
  663. // simulate situation where the target file is locked
  664. $view->lockFile('/test.txt', ILockingProvider::LOCK_EXCLUSIVE);
  665. $_SERVER['HTTP_OC_CHUNKED'] = true;
  666. $info = new \OC\Files\FileInfo('/' . $this->user . '/files/test.txt-chunking-12345-2-0', $this->getMockStorage(), null, [
  667. 'permissions' => \OCP\Constants::PERMISSION_ALL
  668. ], null);
  669. $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
  670. $file->acquireLock(ILockingProvider::LOCK_SHARED);
  671. $this->assertNull($file->put('test data one'));
  672. $file->releaseLock(ILockingProvider::LOCK_SHARED);
  673. $info = new \OC\Files\FileInfo('/' . $this->user . '/files/test.txt-chunking-12345-2-1', $this->getMockStorage(), null, [
  674. 'permissions' => \OCP\Constants::PERMISSION_ALL
  675. ], null);
  676. $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
  677. // action
  678. $thrown = false;
  679. try {
  680. $file->acquireLock(ILockingProvider::LOCK_SHARED);
  681. $file->put($this->getStream('test data'));
  682. $file->releaseLock(ILockingProvider::LOCK_SHARED);
  683. } catch (\OCA\DAV\Connector\Sabre\Exception\FileLocked $e) {
  684. $thrown = true;
  685. }
  686. $this->assertTrue($thrown);
  687. $this->assertEmpty($this->listPartFiles($view, ''), 'No stray part files');
  688. }
  689. /**
  690. * Test put file with invalid chars
  691. */
  692. public function testSimplePutInvalidChars() {
  693. // setup
  694. $view = $this->getMockBuilder(View::class)
  695. ->setMethods(['getRelativePath'])
  696. ->getMock();
  697. $view->expects($this->any())
  698. ->method('getRelativePath')
  699. ->willReturnArgument(0);
  700. $info = new \OC\Files\FileInfo('/*', $this->getMockStorage(), null, [
  701. 'permissions' => \OCP\Constants::PERMISSION_ALL
  702. ], null);
  703. $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
  704. // action
  705. $thrown = false;
  706. try {
  707. // beforeMethod locks
  708. $view->lockFile($info->getPath(), ILockingProvider::LOCK_SHARED);
  709. $file->put($this->getStream('test data'));
  710. // afterMethod unlocks
  711. $view->unlockFile($info->getPath(), ILockingProvider::LOCK_SHARED);
  712. } catch (\OCA\DAV\Connector\Sabre\Exception\InvalidPath $e) {
  713. $thrown = true;
  714. }
  715. $this->assertTrue($thrown);
  716. $this->assertEmpty($this->listPartFiles($view, ''), 'No stray part files');
  717. }
  718. /**
  719. * Test setting name with setName() with invalid chars
  720. *
  721. */
  722. public function testSetNameInvalidChars() {
  723. $this->expectException(\OCA\DAV\Connector\Sabre\Exception\InvalidPath::class);
  724. // setup
  725. $view = $this->getMockBuilder(View::class)
  726. ->setMethods(['getRelativePath'])
  727. ->getMock();
  728. $view->expects($this->any())
  729. ->method('getRelativePath')
  730. ->willReturnArgument(0);
  731. $info = new \OC\Files\FileInfo('/*', $this->getMockStorage(), null, [
  732. 'permissions' => \OCP\Constants::PERMISSION_ALL
  733. ], null);
  734. $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
  735. $file->setName('/super*star.txt');
  736. }
  737. public function testUploadAbort() {
  738. // setup
  739. $view = $this->getMockBuilder(View::class)
  740. ->setMethods(['rename', 'getRelativePath', 'filesize'])
  741. ->getMock();
  742. $view->expects($this->any())
  743. ->method('rename')
  744. ->withAnyParameters()
  745. ->willReturn(false);
  746. $view->expects($this->any())
  747. ->method('getRelativePath')
  748. ->willReturnArgument(0);
  749. $view->expects($this->any())
  750. ->method('filesize')
  751. ->willReturn(123456);
  752. $_SERVER['CONTENT_LENGTH'] = 12345;
  753. $_SERVER['REQUEST_METHOD'] = 'PUT';
  754. $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [
  755. 'permissions' => \OCP\Constants::PERMISSION_ALL
  756. ], null);
  757. $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
  758. // action
  759. $thrown = false;
  760. try {
  761. // beforeMethod locks
  762. $view->lockFile($info->getPath(), ILockingProvider::LOCK_SHARED);
  763. $file->put($this->getStream('test data'));
  764. // afterMethod unlocks
  765. $view->unlockFile($info->getPath(), ILockingProvider::LOCK_SHARED);
  766. } catch (\Sabre\DAV\Exception\BadRequest $e) {
  767. $thrown = true;
  768. }
  769. $this->assertTrue($thrown);
  770. $this->assertEmpty($this->listPartFiles($view, ''), 'No stray part files');
  771. }
  772. public function testDeleteWhenAllowed() {
  773. // setup
  774. $view = $this->getMockBuilder(View::class)
  775. ->getMock();
  776. $view->expects($this->once())
  777. ->method('unlink')
  778. ->willReturn(true);
  779. $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [
  780. 'permissions' => \OCP\Constants::PERMISSION_ALL
  781. ], null);
  782. $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
  783. // action
  784. $file->delete();
  785. }
  786. public function testDeleteThrowsWhenDeletionNotAllowed() {
  787. $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
  788. // setup
  789. $view = $this->getMockBuilder(View::class)
  790. ->getMock();
  791. $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [
  792. 'permissions' => 0
  793. ], null);
  794. $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
  795. // action
  796. $file->delete();
  797. }
  798. public function testDeleteThrowsWhenDeletionFailed() {
  799. $this->expectException(\Sabre\DAV\Exception\Forbidden::class);
  800. // setup
  801. $view = $this->getMockBuilder(View::class)
  802. ->getMock();
  803. // but fails
  804. $view->expects($this->once())
  805. ->method('unlink')
  806. ->willReturn(false);
  807. $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [
  808. 'permissions' => \OCP\Constants::PERMISSION_ALL
  809. ], null);
  810. $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
  811. // action
  812. $file->delete();
  813. }
  814. public function testDeleteThrowsWhenDeletionThrows() {
  815. $this->expectException(\OCA\DAV\Connector\Sabre\Exception\Forbidden::class);
  816. // setup
  817. $view = $this->getMockBuilder(View::class)
  818. ->getMock();
  819. // but fails
  820. $view->expects($this->once())
  821. ->method('unlink')
  822. ->willThrowException(new ForbiddenException('', true));
  823. $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [
  824. 'permissions' => \OCP\Constants::PERMISSION_ALL
  825. ], null);
  826. $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
  827. // action
  828. $file->delete();
  829. }
  830. /**
  831. * Asserts hook call
  832. *
  833. * @param array $callData hook call data to check
  834. * @param string $signal signal name
  835. * @param string $hookPath hook path
  836. */
  837. protected function assertHookCall($callData, $signal, $hookPath) {
  838. $this->assertEquals($signal, $callData['signal']);
  839. $params = $callData['params'];
  840. $this->assertEquals(
  841. $hookPath,
  842. $params[Filesystem::signal_param_path]
  843. );
  844. }
  845. /**
  846. * Test whether locks are set before and after the operation
  847. */
  848. public function testPutLocking() {
  849. $view = new \OC\Files\View('/' . $this->user . '/files/');
  850. $path = 'test-locking.txt';
  851. $info = new \OC\Files\FileInfo(
  852. '/' . $this->user . '/files/' . $path,
  853. $this->getMockStorage(),
  854. null,
  855. ['permissions' => \OCP\Constants::PERMISSION_ALL],
  856. null
  857. );
  858. $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
  859. $this->assertFalse(
  860. $this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_SHARED),
  861. 'File unlocked before put'
  862. );
  863. $this->assertFalse(
  864. $this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE),
  865. 'File unlocked before put'
  866. );
  867. $wasLockedPre = false;
  868. $wasLockedPost = false;
  869. $eventHandler = $this->getMockBuilder(\stdclass::class)
  870. ->setMethods(['writeCallback', 'postWriteCallback'])
  871. ->getMock();
  872. // both pre and post hooks might need access to the file,
  873. // so only shared lock is acceptable
  874. $eventHandler->expects($this->once())
  875. ->method('writeCallback')
  876. ->willReturnCallback(
  877. function () use ($view, $path, &$wasLockedPre) {
  878. $wasLockedPre = $this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_SHARED);
  879. $wasLockedPre = $wasLockedPre && !$this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE);
  880. }
  881. );
  882. $eventHandler->expects($this->once())
  883. ->method('postWriteCallback')
  884. ->willReturnCallback(
  885. function () use ($view, $path, &$wasLockedPost) {
  886. $wasLockedPost = $this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_SHARED);
  887. $wasLockedPost = $wasLockedPost && !$this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE);
  888. }
  889. );
  890. \OCP\Util::connectHook(
  891. Filesystem::CLASSNAME,
  892. Filesystem::signal_write,
  893. $eventHandler,
  894. 'writeCallback'
  895. );
  896. \OCP\Util::connectHook(
  897. Filesystem::CLASSNAME,
  898. Filesystem::signal_post_write,
  899. $eventHandler,
  900. 'postWriteCallback'
  901. );
  902. // beforeMethod locks
  903. $view->lockFile($path, ILockingProvider::LOCK_SHARED);
  904. $this->assertNotEmpty($file->put($this->getStream('test data')));
  905. // afterMethod unlocks
  906. $view->unlockFile($path, ILockingProvider::LOCK_SHARED);
  907. $this->assertTrue($wasLockedPre, 'File was locked during pre-hooks');
  908. $this->assertTrue($wasLockedPost, 'File was locked during post-hooks');
  909. $this->assertFalse(
  910. $this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_SHARED),
  911. 'File unlocked after put'
  912. );
  913. $this->assertFalse(
  914. $this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE),
  915. 'File unlocked after put'
  916. );
  917. }
  918. /**
  919. * Returns part files in the given path
  920. *
  921. * @param \OC\Files\View view which root is the current user's "files" folder
  922. * @param string $path path for which to list part files
  923. *
  924. * @return array list of part files
  925. */
  926. private function listPartFiles(\OC\Files\View $userView = null, $path = '') {
  927. if ($userView === null) {
  928. $userView = \OC\Files\Filesystem::getView();
  929. }
  930. $files = [];
  931. [$storage, $internalPath] = $userView->resolvePath($path);
  932. if ($storage instanceof Local) {
  933. $realPath = $storage->getSourcePath($internalPath);
  934. $dh = opendir($realPath);
  935. while (($file = readdir($dh)) !== false) {
  936. if (substr($file, strlen($file) - 5, 5) === '.part') {
  937. $files[] = $file;
  938. }
  939. }
  940. closedir($dh);
  941. }
  942. return $files;
  943. }
  944. /**
  945. * returns an array of file information filesize, mtime, filetype, mimetype
  946. *
  947. * @param string $path
  948. * @param View $userView
  949. * @return array
  950. */
  951. private function getFileInfos($path = '', View $userView = null) {
  952. if ($userView === null) {
  953. $userView = Filesystem::getView();
  954. }
  955. return [
  956. "filesize" => $userView->filesize($path),
  957. "mtime" => $userView->filemtime($path),
  958. "filetype" => $userView->filetype($path),
  959. "mimetype" => $userView->getMimeType($path)
  960. ];
  961. }
  962. public function testGetFopenFails() {
  963. $this->expectException(\Sabre\DAV\Exception\ServiceUnavailable::class);
  964. $view = $this->getMockBuilder(View::class)
  965. ->setMethods(['fopen'])
  966. ->getMock();
  967. $view->expects($this->atLeastOnce())
  968. ->method('fopen')
  969. ->willReturn(false);
  970. $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [
  971. 'permissions' => \OCP\Constants::PERMISSION_ALL
  972. ], null);
  973. $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
  974. $file->get();
  975. }
  976. public function testGetFopenThrows() {
  977. $this->expectException(\OCA\DAV\Connector\Sabre\Exception\Forbidden::class);
  978. $view = $this->getMockBuilder(View::class)
  979. ->setMethods(['fopen'])
  980. ->getMock();
  981. $view->expects($this->atLeastOnce())
  982. ->method('fopen')
  983. ->willThrowException(new ForbiddenException('', true));
  984. $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [
  985. 'permissions' => \OCP\Constants::PERMISSION_ALL
  986. ], null);
  987. $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
  988. $file->get();
  989. }
  990. public function testGetThrowsIfNoPermission() {
  991. $this->expectException(\Sabre\DAV\Exception\NotFound::class);
  992. $view = $this->getMockBuilder(View::class)
  993. ->setMethods(['fopen'])
  994. ->getMock();
  995. $view->expects($this->never())
  996. ->method('fopen');
  997. $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [
  998. 'permissions' => \OCP\Constants::PERMISSION_CREATE // no read perm
  999. ], null);
  1000. $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
  1001. $file->get();
  1002. }
  1003. public function testSimplePutNoCreatePermissions() {
  1004. $this->logout();
  1005. $storage = new Temporary([]);
  1006. $storage->file_put_contents('file.txt', 'old content');
  1007. $noCreateStorage = new PermissionsMask([
  1008. 'storage' => $storage,
  1009. 'mask' => Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE
  1010. ]);
  1011. $this->registerMount($this->user, $noCreateStorage, '/' . $this->user . '/files/root');
  1012. $this->loginAsUser($this->user);
  1013. $view = new View('/' . $this->user . '/files');
  1014. $info = $view->getFileInfo('root/file.txt');
  1015. $file = new File($view, $info);
  1016. // beforeMethod locks
  1017. $view->lockFile('root/file.txt', ILockingProvider::LOCK_SHARED);
  1018. $file->put($this->getStream('new content'));
  1019. // afterMethod unlocks
  1020. $view->unlockFile('root/file.txt', ILockingProvider::LOCK_SHARED);
  1021. $this->assertEquals('new content', $view->file_get_contents('root/file.txt'));
  1022. }
  1023. public function testPutLockExpired() {
  1024. $view = new \OC\Files\View('/' . $this->user . '/files/');
  1025. $path = 'test-locking.txt';
  1026. $info = new \OC\Files\FileInfo(
  1027. '/' . $this->user . '/files/' . $path,
  1028. $this->getMockStorage(),
  1029. null,
  1030. ['permissions' => \OCP\Constants::PERMISSION_ALL],
  1031. null
  1032. );
  1033. $file = new \OCA\DAV\Connector\Sabre\File($view, $info);
  1034. // don't lock before the PUT to simulate an expired shared lock
  1035. $this->assertNotEmpty($file->put($this->getStream('test data')));
  1036. // afterMethod unlocks
  1037. $view->unlockFile($path, ILockingProvider::LOCK_SHARED);
  1038. }
  1039. }