Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement persistent locking #382

Merged
merged 9 commits into from
Mar 3, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,10 @@
'name' => 'FileHandling#save',
'url' => '/ajax/savefile',
'verb' => 'PUT'
],
[
'name' => 'FileHandling#close',
'url' => '/ajax/closefile',
'verb' => 'PUT'
]
]];
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
}
},
"require": {
"firebase/php-jwt": "^5.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.4"
Expand Down
62 changes: 60 additions & 2 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

214 changes: 212 additions & 2 deletions controller/filehandlingcontroller.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,20 @@
use OCP\Files\File;
use OCP\Files\ForbiddenException;
use OCP\Files\IRootFolder;
use OCP\Files\Storage\IPersistentLockingStorage;
use OCP\Lock\Persistent\ILock;
use OCP\IL10N;
use OCP\ILogger;
use OCP\IRequest;
use OCP\IUserSession;
use OCP\Lock\LockedException;
use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share\IManager;

use Sabre\DAV\Exception\NotFound;

use Firebase\JWT\JWT;

class FileHandlingController extends Controller {

/** @var IL10N */
Expand Down Expand Up @@ -125,7 +130,23 @@ public function load($dir, $filename) {

if ($fileContents !== false) {
$permissions = $this->getPermissions($node);
$writable = ($permissions & Constants::PERMISSION_UPDATE) === Constants::PERMISSION_UPDATE;

// handle locks
$activePersistentLock = $this->getPersistentLock($node);
if ($activePersistentLock && !$this->verifyPersistentLock($node, $activePersistentLock)) {
// there is lock existing on this file
// and thus this user cannot write to this file
$writable = false;
} else {
// check if permissions allow writing
$writable = ($permissions & Constants::PERMISSION_UPDATE) === Constants::PERMISSION_UPDATE;
}

if ($writable) {
// get new/refresh write lock for the user
$activePersistentLock = $this->acquirePersistentLock($node);
}

$mime = $node->getMimeType();
$mTime = $node->getMTime();
$encoding = \mb_detect_encoding($fileContents . "a", "UTF-8, GB2312, GBK ,BIG5, WINDOWS-1252, SJIS-win, EUC-JP, ISO-8859-15, ISO-8859-1, ASCII", true);
Expand All @@ -138,6 +159,7 @@ public function load($dir, $filename) {
[
'filecontents' => $fileContents,
'writeable' => $writable,
'locked' => $activePersistentLock ? $activePersistentLock->getOwner() : null,
'mime' => $mime,
'mtime' => $mTime
],
Expand Down Expand Up @@ -198,6 +220,16 @@ public function save($path, $filecontents, $mtime) {
// Get file mtime
$filemtime = $node->getMTime();

// Check lock (if there is any)
$activePersistentLock = $this->getPersistentLock($node);
mrow4a marked this conversation as resolved.
Show resolved Hide resolved
if ($activePersistentLock && !$this->verifyPersistentLock($node, $activePersistentLock)) {
// Then the file has persistent lock acquired
return new DataResponse(
['message' => $this->l->t('Cannot save file as it is locked by %s.', [$activePersistentLock->getOwner()])],
Http::STATUS_BAD_REQUEST
);
}

if ($mtime !== $filemtime) {
// Then the file has changed since opening
$this->logger->error(
Expand All @@ -211,6 +243,10 @@ public function save($path, $filecontents, $mtime) {
} else {
// File same as when opened, save file
if (($permissions & Constants::PERMISSION_UPDATE) === Constants::PERMISSION_UPDATE) {
// Refresh (or aquire for expired) lock for the user for further writing
$activePersistentLock = $this->acquirePersistentLock($node);

// Write file
$filecontents = \iconv(\mb_detect_encoding($filecontents), "UTF-8", $filecontents);
try {
$node->putContent($filecontents);
Expand All @@ -220,8 +256,10 @@ public function save($path, $filecontents, $mtime) {
} catch (ForbiddenException $e) {
return new DataResponse(['message' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
}

// Clear statcache
\clearstatcache();

// Get new mtime
$newmtime = $node->getMTime();
$newsize = $node->getSize();
Expand Down Expand Up @@ -254,6 +292,56 @@ public function save($path, $filecontents, $mtime) {
}
}

/**
* close text file
*
* @NoAdminRequired
* @NoSubadminRequired
* @PublicPage
* @NoCSRFRequired
*
* @param string $path
* @return DataResponse
*/
public function close($path) {
try {
if ($path !== '') {
try {
$node = $this->getNode($path);
} catch (ShareNotFound $e) {
return new DataResponse(
['message' => $this->l->t('Invalid share token')],
Http::STATUS_BAD_REQUEST
);
} catch (NoUserException $e) {
return new DataResponse(
['message' => $this->l->t('No user found')],
Http::STATUS_BAD_REQUEST
);
}

// Check lock (if there is any)
$activePersistentLock = $this->getPersistentLock($node);
if ($activePersistentLock && $this->verifyPersistentLock($node, $activePersistentLock)) {
// Clear lock on close
$this->releasePersistentLock($node, $activePersistentLock);
}

// Done
return new DataResponse([], Http::STATUS_OK);
} else {
$this->logger->error('No file path supplied');
return new DataResponse(['message' => $this->l->t('File path not supplied')], Http::STATUS_BAD_REQUEST);
}
} catch (HintException $e) {
$message = (string)$e->getHint();
return new DataResponse(['message' => $message], Http::STATUS_BAD_REQUEST);
} catch (\Exception $e) {
$message = (string)$this->l->t('An internal server error occurred.');
return new DataResponse(['message' => $message], Http::STATUS_BAD_REQUEST);
}
}

private function getNode(string $path): File {
$sharingToken = $this->request->getParam('sharingToken');

Expand All @@ -279,7 +367,7 @@ private function getNode(string $path): File {
return $node;
}

private function getPermissions($node): int {
private function getPermissions(File $node): int {
$sharingToken = $this->request->getParam('sharingToken');

if ($sharingToken) {
Expand All @@ -289,4 +377,126 @@ private function getPermissions($node): int {

return $node->getPermissions();
}
private function acquirePersistentLock(File $file): ?ILock {
$storage = $file->getStorage();
if ($storage->instanceOfStorage(IPersistentLockingStorage::class)) {
$sharingToken = $this->request->getParam('sharingToken');

if ($sharingToken) {
$accessToken = $this->getTokenForPublicLinkAccess(
$file->getId(),
$file->getParent()->getPath(),
$sharingToken
);
$owner = $this->l->t('Public Link User via Text Editor');
} else {
$user = $this->userSession->getUser();
if (!$user) {
return null;
}
$accessToken = $this->getTokenForUserAccess(
$file->getId(),
$file->getParent()->getPath(),
$user->getUID()
);
$owner = $this->l->t('%s via Text Editor', [$user->getDisplayName()]);
}

/**
* @var IPersistentLockingStorage $storage
* @phpstan-ignore-next-line
*/
'@phan-var IPersistentLockingStorage $storage';
return $storage->lockNodePersistent($file->getInternalPath(), [
'token' => $accessToken,
'owner' => $owner
]);
}

return null;
}

private function getPersistentLock(File $file): ?ILock {
$storage = $file->getStorage();
if ($storage->instanceOfStorage(IPersistentLockingStorage::class)) {
/**
* @var IPersistentLockingStorage $storage
* @phpstan-ignore-next-line
*/
'@phan-var IPersistentLockingStorage $storage';
$locks = $storage->getLocks($file->getInternalPath(), false);
if (\count($locks) > 0) {
// use active lock (first returned)
return $locks[0];
}
}

return null;
}

private function verifyPersistentLock(File $file, ILock $lock): bool {
$storage = $file->getStorage();
if ($storage->instanceOfStorage(IPersistentLockingStorage::class)) {
$sharingToken = $this->request->getParam('sharingToken');

if ($sharingToken) {
$accessToken = $this->getTokenForPublicLinkAccess(
$file->getId(),
$file->getParent()->getPath(),
$sharingToken
);
} else {
$user = $this->userSession->getUser();
if (!$user) {
return false;
}
$accessToken = $this->getTokenForUserAccess(
$file->getId(),
$file->getParent()->getPath(),
$user->getUID()
);
}

// token in the lock should match access token for this user/share
return $lock->getToken() === $accessToken;
}

return false;
}

private function releasePersistentLock(File $file, ILock $lock): bool {
$storage = $file->getStorage();
if ($storage->instanceOfStorage(IPersistentLockingStorage::class)) {
/**
* @var IPersistentLockingStorage $storage
* @phpstan-ignore-next-line
*/
'@phan-var IPersistentLockingStorage $storage';
return $storage->unlockNodePersistent($file->getInternalPath(), [
'token' => $lock->getToken()
]);
}

return false;
}

private function getTokenForUserAccess(int $fileId, string $fileParentPath, string $userId): string {
// as this app is not collaborative, the token is static
return JWT::encode([
'uid' => $userId,
'st' => '',
'fid' => $fileId,
'fpp' => $fileParentPath,
], 'files_texteditor', 'HS256');
}

private function getTokenForPublicLinkAccess(int $fileId, string $fileParentPath, string $sharingToken): string {
// as this app is not collaborative, the token is static
return JWT::encode([
'uid' => '',
'st' => $sharingToken,
'fid' => $fileId,
'fpp' => $fileParentPath,
], 'files_texteditor', 'HS256');
}
}
Loading