FileTest.php 32 KB

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