server/tests/lib/connector/sabre/file.php
Vincent Petry 4497aa4c68 Webdav PUT small file lock must be shared during hooks
Fixed code path for Webdav PUT of small files to use shared locks during
hook execution, and exclusive during the file operation

This makes it possible for versions to be copied by accessing the file
in a post_write hook.
2015-06-12 18:52:18 +02:00

487 lines
12 KiB
PHP

<?php
/**
* Copyright (c) 2013 Thomas Müller <thomas.mueller@tmit.eu>
* This file is licensed under the Affero General Public License version 3 or
* later.
* See the COPYING-README file.
*/
namespace Test\Connector\Sabre;
use Test\HookHelper;
use OC\Files\Filesystem;
class File extends \Test\TestCase {
/**
* @var string
*/
private $user;
public function setUp() {
parent::setUp();
\OC_Hook::clear();
$this->user = $this->getUniqueID('user_');
$userManager = \OC::$server->getUserManager();
$userManager->createUser($this->user, 'pass');
$this->loginAsUser($this->user);
}
public function tearDown() {
$userManager = \OC::$server->getUserManager();
$userManager->get($this->user)->delete();
parent::tearDown();
}
private function getStream($string) {
$stream = fopen('php://temp', 'r+');
fwrite($stream, $string);
fseek($stream, 0);
return $stream;
}
/**
* @expectedException \Sabre\DAV\Exception
*/
public function testSimplePutFails() {
// setup
$storage = $this->getMock('\OC\Files\Storage\Local', ['fopen'], [['datadir' => \OC::$server->getTempManager()->getTemporaryFolder()]]);
$view = $this->getMock('\OC\Files\View', array('getRelativePath', 'resolvePath'), array());
$view->expects($this->any())
->method('resolvePath')
->will($this->returnValue(array($storage, '')));
$storage->expects($this->once())
->method('fopen')
->will($this->returnValue(false));
$view->expects($this->any())
->method('getRelativePath')
->will($this->returnValue('/test.txt'));
$info = new \OC\Files\FileInfo('/test.txt', null, null, array(
'permissions' => \OCP\Constants::PERMISSION_ALL
), null);
$file = new \OC\Connector\Sabre\File($view, $info);
// action
$file->put('test data');
}
private function doPut($path, $viewRoot = null) {
$view = \OC\Files\Filesystem::getView();
if (!is_null($viewRoot)) {
$view = new \OC\Files\View($viewRoot);
} else {
$viewRoot = '/' . $this->user . '/files';
}
$info = new \OC\Files\FileInfo(
$viewRoot . '/' . ltrim($path, '/'),
null,
null,
['permissions' => \OCP\Constants::PERMISSION_ALL],
null
);
$file = new \OC\Connector\Sabre\File($view, $info);
$this->assertNotEmpty($file->put($this->getStream('test data')));
}
/**
* Test putting a single file
*/
public function testPutSingleFile() {
$this->doPut('/foo.txt');
}
/**
* Test that putting a file triggers create hooks
*/
public function testPutSingleFileTriggersHooks() {
HookHelper::setUpHooks();
$this->doPut('/foo.txt');
$this->assertCount(4, HookHelper::$hookCalls);
$this->assertHookCall(
HookHelper::$hookCalls[0],
Filesystem::signal_create,
'/foo.txt'
);
$this->assertHookCall(
HookHelper::$hookCalls[1],
Filesystem::signal_write,
'/foo.txt'
);
$this->assertHookCall(
HookHelper::$hookCalls[2],
Filesystem::signal_post_create,
'/foo.txt'
);
$this->assertHookCall(
HookHelper::$hookCalls[3],
Filesystem::signal_post_write,
'/foo.txt'
);
}
/**
* Test that putting a file triggers update hooks
*/
public function testPutOverwriteFileTriggersHooks() {
$view = \OC\Files\Filesystem::getView();
$view->file_put_contents('/foo.txt', 'some content that will be replaced');
HookHelper::setUpHooks();
$this->doPut('/foo.txt');
$this->assertCount(4, HookHelper::$hookCalls);
$this->assertHookCall(
HookHelper::$hookCalls[0],
Filesystem::signal_update,
'/foo.txt'
);
$this->assertHookCall(
HookHelper::$hookCalls[1],
Filesystem::signal_write,
'/foo.txt'
);
$this->assertHookCall(
HookHelper::$hookCalls[2],
Filesystem::signal_post_update,
'/foo.txt'
);
$this->assertHookCall(
HookHelper::$hookCalls[3],
Filesystem::signal_post_write,
'/foo.txt'
);
}
/**
* Test that putting a file triggers hooks with the correct path
* if the passed view was chrooted (can happen with public webdav
* where the root is the share root)
*/
public function testPutSingleFileTriggersHooksDifferentRoot() {
$view = \OC\Files\Filesystem::getView();
$view->mkdir('noderoot');
HookHelper::setUpHooks();
// happens with public webdav where the view root is the share root
$this->doPut('/foo.txt', '/' . $this->user . '/files/noderoot');
$this->assertCount(4, HookHelper::$hookCalls);
$this->assertHookCall(
HookHelper::$hookCalls[0],
Filesystem::signal_create,
'/noderoot/foo.txt'
);
$this->assertHookCall(
HookHelper::$hookCalls[1],
Filesystem::signal_write,
'/noderoot/foo.txt'
);
$this->assertHookCall(
HookHelper::$hookCalls[2],
Filesystem::signal_post_create,
'/noderoot/foo.txt'
);
$this->assertHookCall(
HookHelper::$hookCalls[3],
Filesystem::signal_post_write,
'/noderoot/foo.txt'
);
}
public static function cancellingHook($params) {
self::$hookCalls[] = array(
'signal' => Filesystem::signal_post_create,
'params' => $params
);
}
/**
* Test put file with cancelled hook
*
* @expectedException \Sabre\DAV\Exception
*/
public function testPutSingleFileCancelPreHook() {
\OCP\Util::connectHook(
Filesystem::CLASSNAME,
Filesystem::signal_create,
'\Test\HookHelper',
'cancellingCallback'
);
$this->doPut('/foo.txt');
}
/**
* @expectedException \Sabre\DAV\Exception
*/
public function testSimplePutFailsOnRename() {
// setup
$view = $this->getMock('\OC\Files\View',
array('rename', 'getRelativePath', 'filesize'));
$view->expects($this->any())
->method('rename')
->withAnyParameters()
->will($this->returnValue(false));
$view->expects($this->any())
->method('getRelativePath')
->will($this->returnValue('/test.txt'));
$view->expects($this->any())
->method('filesize')
->will($this->returnValue(123456));
$_SERVER['CONTENT_LENGTH'] = 123456;
$_SERVER['REQUEST_METHOD'] = 'PUT';
$info = new \OC\Files\FileInfo('/test.txt', null, null, array(
'permissions' => \OCP\Constants::PERMISSION_ALL
), null);
$file = new \OC\Connector\Sabre\File($view, $info);
// action
$file->put($this->getStream('test data'));
}
/**
* @expectedException \OC\Connector\Sabre\Exception\InvalidPath
*/
public function testSimplePutInvalidChars() {
// setup
$view = $this->getMock('\OC\Files\View', array('getRelativePath'));
$view->expects($this->any())
->method('getRelativePath')
->will($this->returnValue('/*'));
$info = new \OC\Files\FileInfo('/*', null, null, array(
'permissions' => \OCP\Constants::PERMISSION_ALL
), null);
$file = new \OC\Connector\Sabre\File($view, $info);
// action
$file->put($this->getStream('test data'));
}
/**
* Test setting name with setName() with invalid chars
*
* @expectedException \OC\Connector\Sabre\Exception\InvalidPath
*/
public function testSetNameInvalidChars() {
// setup
$view = $this->getMock('\OC\Files\View', array('getRelativePath'));
$view->expects($this->any())
->method('getRelativePath')
->will($this->returnValue('/*'));
$info = new \OC\Files\FileInfo('/*', null, null, array(
'permissions' => \OCP\Constants::PERMISSION_ALL
), null);
$file = new \OC\Connector\Sabre\File($view, $info);
$file->setName('/super*star.txt');
}
/**
* @expectedException \Sabre\DAV\Exception\BadRequest
*/
public function testUploadAbort() {
// setup
$view = $this->getMock('\OC\Files\View',
array('rename', 'getRelativePath', 'filesize'));
$view->expects($this->any())
->method('rename')
->withAnyParameters()
->will($this->returnValue(false));
$view->expects($this->any())
->method('getRelativePath')
->will($this->returnValue('/test.txt'));
$view->expects($this->any())
->method('filesize')
->will($this->returnValue(123456));
$_SERVER['CONTENT_LENGTH'] = 12345;
$_SERVER['REQUEST_METHOD'] = 'PUT';
$info = new \OC\Files\FileInfo('/test.txt', null, null, array(
'permissions' => \OCP\Constants::PERMISSION_ALL
), null);
$file = new \OC\Connector\Sabre\File($view, $info);
// action
$file->put($this->getStream('test data'));
}
/**
*
*/
public function testDeleteWhenAllowed() {
// setup
$view = $this->getMock('\OC\Files\View',
array());
$view->expects($this->once())
->method('unlink')
->will($this->returnValue(true));
$info = new \OC\Files\FileInfo('/test.txt', null, null, array(
'permissions' => \OCP\Constants::PERMISSION_ALL
), null);
$file = new \OC\Connector\Sabre\File($view, $info);
// action
$file->delete();
}
/**
* @expectedException \Sabre\DAV\Exception\Forbidden
*/
public function testDeleteThrowsWhenDeletionNotAllowed() {
// setup
$view = $this->getMock('\OC\Files\View',
array());
$info = new \OC\Files\FileInfo('/test.txt', null, null, array(
'permissions' => 0
), null);
$file = new \OC\Connector\Sabre\File($view, $info);
// action
$file->delete();
}
/**
* @expectedException \Sabre\DAV\Exception\Forbidden
*/
public function testDeleteThrowsWhenDeletionFailed() {
// setup
$view = $this->getMock('\OC\Files\View',
array());
// but fails
$view->expects($this->once())
->method('unlink')
->will($this->returnValue(false));
$info = new \OC\Files\FileInfo('/test.txt', null, null, array(
'permissions' => \OCP\Constants::PERMISSION_ALL
), null);
$file = new \OC\Connector\Sabre\File($view, $info);
// action
$file->delete();
}
/**
* Asserts hook call
*
* @param array $callData hook call data to check
* @param string $signal signal name
* @param string $hookPath hook path
*/
protected function assertHookCall($callData, $signal, $hookPath) {
$this->assertEquals($signal, $callData['signal']);
$params = $callData['params'];
$this->assertEquals(
$hookPath,
$params[Filesystem::signal_param_path]
);
}
/**
* Test whether locks are set before and after the operation
*/
public function testPutLocking() {
$view = new \OC\Files\View('/' . $this->user . '/files/');
$path = 'test-locking.txt';
$info = new \OC\Files\FileInfo(
'/' . $this->user . '/files/' . $path,
null,
null,
['permissions' => \OCP\Constants::PERMISSION_ALL],
null
);
$file = new \OC\Connector\Sabre\File($view, $info);
$this->assertFalse(
$this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_SHARED),
'File unlocked before put'
);
$this->assertFalse(
$this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE),
'File unlocked before put'
);
$wasLockedPre = false;
$wasLockedPost = false;
$eventHandler = $this->getMockBuilder('\stdclass')
->setMethods(['writeCallback', 'postWriteCallback'])
->getMock();
// both pre and post hooks might need access to the file,
// so only shared lock is acceptable
$eventHandler->expects($this->once())
->method('writeCallback')
->will($this->returnCallback(
function() use ($view, $path, &$wasLockedPre){
$wasLockedPre = $this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_SHARED);
$wasLockedPre = $wasLockedPre && !$this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE);
}
));
$eventHandler->expects($this->once())
->method('postWriteCallback')
->will($this->returnCallback(
function() use ($view, $path, &$wasLockedPost){
$wasLockedPost = $this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_SHARED);
$wasLockedPost = $wasLockedPost && !$this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE);
}
));
\OCP\Util::connectHook(
Filesystem::CLASSNAME,
Filesystem::signal_write,
$eventHandler,
'writeCallback'
);
\OCP\Util::connectHook(
Filesystem::CLASSNAME,
Filesystem::signal_post_write,
$eventHandler,
'postWriteCallback'
);
$this->assertNotEmpty($file->put($this->getStream('test data')));
$this->assertTrue($wasLockedPre, 'File was locked during pre-hooks');
$this->assertTrue($wasLockedPost, 'File was locked during post-hooks');
$this->assertFalse(
$this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_SHARED),
'File unlocked after put'
);
$this->assertFalse(
$this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE),
'File unlocked after put'
);
}
}