Implement locking

Signed-off-by: Marcel Klehr <mklehr@gmx.net>
This commit is contained in:
Marcel Klehr 2021-09-18 16:24:09 +02:00
parent 291615ad26
commit c9a2f0141f
6 changed files with 239 additions and 4 deletions

View File

@ -49,6 +49,8 @@ return [
['name' => 'internal_bookmark#get_bookmark_image', 'url' => '/bookmark/{id}/image', 'verb' => 'GET'],
['name' => 'internal_bookmark#get_bookmark_favicon', 'url' => '/bookmark/{id}/favicon', 'verb' => 'GET'],
['name' => 'internal_bookmark#count_bookmarks', 'url' => '/folder/{folder}/count', 'verb' => 'GET'],
['name' => 'internal_bookmark#acquire_lock', 'url' => '/lock', 'verb' => 'POST'],
['name' => 'internal_bookmark#release_lock', 'url' => '/lock', 'verb' => 'DELETE'],
['name' => 'internal_tags#full_tags', 'url' => '/tag', 'verb' => 'GET'],
['name' => 'internal_tags#rename_tag', 'url' => '/tag', 'verb' => 'POST'],
['name' => 'internal_tags#delete_tag', 'url' => '/tag', 'verb' => 'DELETE'],
@ -89,6 +91,8 @@ return [
['name' => 'bookmark#get_bookmark_image', 'url' => '/public/rest/v2/bookmark/{id}/image', 'verb' => 'GET'],
['name' => 'bookmark#get_bookmark_favicon', 'url' => '/public/rest/v2/bookmark/{id}/favicon', 'verb' => 'GET'],
['name' => 'bookmark#count_bookmarks', 'url' => '/public/rest/v2/folder/{folder}/count', 'verb' => 'GET'],
['name' => 'bookmark#acquire_lock', 'url' => '/public/rest/v2/lock', 'verb' => 'POST'],
['name' => 'bookmark#release_lock', 'url' => '/public/rest/v2/lock', 'verb' => 'DELETE'],
['name' => 'tags#full_tags', 'url' => '/public/rest/v2/tag', 'verb' => 'GET'],
['name' => 'tags#rename_tag', 'url' => '/public/rest/v2/tag', 'verb' => 'POST'],
['name' => 'tags#delete_tag', 'url' => '/public/rest/v2/tag', 'verb' => 'DELETE'],

View File

@ -119,9 +119,13 @@ class BookmarkController extends ApiController {
* @var IRootFolder
*/
private $rootFolder;
/**
* @var \OCA\Bookmarks\Service\LockManager
*/
private $lockManager;
public function __construct(
$appName, $request, IL10N $l10n, BookmarkMapper $bookmarkMapper, TagMapper $tagMapper, FolderMapper $folderMapper, TreeMapper $treeMapper, PublicFolderMapper $publicFolderMapper, ITimeFactory $timeFactory, LoggerInterface $logger, IURLGenerator $url, HtmlExporter $htmlExporter, Authorizer $authorizer, BookmarkService $bookmarks, FolderService $folders, IRootFolder $rootFolder
$appName, $request, IL10N $l10n, BookmarkMapper $bookmarkMapper, TagMapper $tagMapper, FolderMapper $folderMapper, TreeMapper $treeMapper, PublicFolderMapper $publicFolderMapper, ITimeFactory $timeFactory, LoggerInterface $logger, IURLGenerator $url, HtmlExporter $htmlExporter, Authorizer $authorizer, BookmarkService $bookmarks, FolderService $folders, IRootFolder $rootFolder, \OCA\Bookmarks\Service\LockManager $lockManager
) {
parent::__construct($appName, $request);
$this->request = $request;
@ -139,6 +143,7 @@ class BookmarkController extends ApiController {
$this->bookmarks = $bookmarks;
$this->folders = $folders;
$this->rootFolder = $rootFolder;
$this->lockManager = $lockManager;
}
/**
@ -767,4 +772,54 @@ class BookmarkController extends ApiController {
$count = $this->bookmarkMapper->countArchived($this->authorizer->getUserId());
return new JSONResponse(['status' => 'success', 'item' => $count]);
}
/**
* @return JSONResponse
* @NoAdminRequired
* @NoCSRFRequired
* @CORS
* @PublicPage
*/
public function acquireLock(): JSONResponse {
if (!Authorizer::hasPermission(Authorizer::PERM_EDIT, $this->authorizer->getPermissionsForFolder(-1, $this->request))) {
return new JSONResponse(['status' => 'error', 'data' => ['Insufficient permissions']], Http::STATUS_FORBIDDEN);
}
try {
if ($this->lockManager->getLock($this->authorizer->getUserId()) === true) {
return new JSONResponse(['status' => 'error', 'data' => 'Resource is already locked'], Http::STATUS_LOCKED);
}
$this->lockManager->setLock($this->authorizer->getUserId(), true);
} catch (\Exception $e) {
$this->logger->error($e->getMessage());
return new JSONResponse(['status' => 'error'], Http::STATUS_INTERNAL_SERVER_ERROR);
}
return new JSONResponse(['status' => 'success']);
}
/**
* @return JSONResponse
* @NoAdminRequired
* @NoCSRFRequired
* @CORS
* @PublicPage
*/
public function releaseLock(): JSONResponse {
if (!Authorizer::hasPermission(Authorizer::PERM_EDIT, $this->authorizer->getPermissionsForFolder(-1, $this->request))) {
return new JSONResponse(['status' => 'error', 'data' => ['Insufficient permissions']], Http::STATUS_FORBIDDEN);
}
try {
if ($this->lockManager->getLock($this->authorizer->getUserId()) === false) {
return new JSONResponse(['status' => 'success']);
}
$this->lockManager->setLock($this->authorizer->getUserId(), false);
} catch (\Exception $e) {
$this->logger->error($e->getMessage());
return new JSONResponse(['status' => 'error'], Http::STATUS_INTERNAL_SERVER_ERROR);
}
return new JSONResponse(['status' => 'success']);
}
}

View File

@ -231,4 +231,22 @@ class InternalBookmarkController extends ApiController {
public function countArchived(): JSONResponse {
return $this->publicController->countArchived();
}
/**
* @return JSONResponse
* @NoAdminRequired
* @NoCSRFRequired
*/
public function acquireLock(): JSONResponse {
return $this->publicController->acquireLock();
}
/**
* @return JSONResponse
* @NoAdminRequired
* @NoCSRFRequired
*/
public function releaseLock(): JSONResponse {
return $this->publicController->releaseLock();
}
}

View File

@ -0,0 +1,60 @@
<?php
/*
* Copyright (c) 2020. The Nextcloud Bookmarks contributors.
*
* This file is licensed under the Affero General Public License version 3 or later. See the COPYING file.
*/
namespace OCA\Bookmarks\Migration;
use Closure;
use Doctrine\DBAL\Schema\SchemaException;
use OCA\Bookmarks\Db\Types;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
/**
* Auto-generated migration step: Please modify to your needs!
*/
class Version004005000Date20210918124721 extends SimpleMigrationStep {
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
*
* @return void
*/
public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options) {
}
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
*
* @return ISchemaWrapper
*
* @throws SchemaException
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
if ($schema->hasTable('bookmarks_root_folders')) {
$table = $schema->getTable('bookmarks_root_folders');
$table->addColumn('locked', Types::BOOLEAN, ['notnull' => false]);
}
return $schema;
}
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
*
* @return void
*/
public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options) {
}
}

View File

@ -0,0 +1,57 @@
<?php
/*
* Copyright (c) 2020. The Nextcloud Bookmarks contributors.
*
* This file is licensed under the Affero General Public License version 3 or later. See the COPYING file.
*/
namespace OCA\Bookmarks\Service;
use OCA\Bookmarks\Db\FolderMapper;
use OCA\Bookmarks\Db\Types;
use OCP\IDBConnection;
class LockManager {
/**
* @var IDBConnection
*/
private $db;
/**
* @var FolderMapper
*/
private $folderMapper;
public function __construct(IDBConnection $db, FolderMapper $folderMapper) {
$this->db = $db;
$this->folderMapper = $folderMapper;
}
/**
* @param string $userId
* @param bool $locked
*/
public function setLock(string $userId, bool $locked): void {
$this->folderMapper->findRootFolder($userId);
$qb = $this->db->getQueryBuilder();
$qb->update('bookmarks_root_folders')
->set('locked', $qb->createNamedParameter($locked, Types::BOOLEAN))
->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId)))
->execute();
}
/**
* @param string $userId
* @return bool
*/
public function getLock(string $userId): bool {
$this->folderMapper->findRootFolder($userId);
$qb = $this->db->getQueryBuilder();
$locked = $qb->select('locked')
->from('bookmarks_root_folders')
->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId)))
->execute()
->fetch(\PDO::FETCH_COLUMN);
return (bool) $locked;
}
}

View File

@ -25,6 +25,7 @@ use OCA\Bookmarks\Service\Authorizer;
use OCA\Bookmarks\Service\BookmarkService;
use OCA\Bookmarks\Service\FolderService;
use OCA\Bookmarks\Service\HtmlExporter;
use OCA\Bookmarks\Service\LockManager;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\AppFramework\Http;
@ -146,6 +147,10 @@ class BookmarkControllerTest extends TestCase {
* @var FolderService
*/
private $folders;
/**
* @var LockManager
*/
private $lockManager;
/**
* @throws \OCP\AppFramework\QueryException
@ -188,14 +193,15 @@ class BookmarkControllerTest extends TestCase {
$htmlExporter = OC::$server->get(HtmlExporter::class);
$this->authorizer = OC::$server->get(Authorizer::class);
$this->folders = OC::$server->get(FolderService::class);
$this->lockManager = OC::$server->get(LockManager::class);
/** @var IRootFolder $rootFolder */
$rootFolder = OC::$server->get(IRootFolder::class);
$this->controller = new BookmarkController('bookmarks', $this->request, $l, $this->bookmarkMapper, $this->tagMapper, $this->folderMapper, $this->treeMapper, $this->publicFolderMapper, $timeFactory, $logger, $urlGenerator, $htmlExporter, $this->authorizer, $this->bookmarks, $this->folders, $rootFolder);
$this->otherController = new BookmarkController('bookmarks', $this->request, $l, $this->bookmarkMapper, $this->tagMapper, $this->folderMapper, $this->treeMapper, $this->publicFolderMapper, $timeFactory, $logger, $urlGenerator, $htmlExporter, $this->authorizer, $this->bookmarks, $this->folders, $rootFolder);
$this->controller = new BookmarkController('bookmarks', $this->request, $l, $this->bookmarkMapper, $this->tagMapper, $this->folderMapper, $this->treeMapper, $this->publicFolderMapper, $timeFactory, $logger, $urlGenerator, $htmlExporter, $this->authorizer, $this->bookmarks, $this->folders, $rootFolder, $this->lockManager);
$this->otherController = new BookmarkController('bookmarks', $this->request, $l, $this->bookmarkMapper, $this->tagMapper, $this->folderMapper, $this->treeMapper, $this->publicFolderMapper, $timeFactory, $logger, $urlGenerator, $htmlExporter, $this->authorizer, $this->bookmarks, $this->folders, $rootFolder, $this->lockManager);
$this->publicController = new BookmarkController('bookmarks', $this->publicRequest, $l, $this->bookmarkMapper, $this->tagMapper, $this->folderMapper, $this->treeMapper, $this->publicFolderMapper, $timeFactory, $logger, $urlGenerator, $htmlExporter, $this->authorizer, $this->bookmarks, $this->folders, $rootFolder);
$this->publicController = new BookmarkController('bookmarks', $this->publicRequest, $l, $this->bookmarkMapper, $this->tagMapper, $this->folderMapper, $this->treeMapper, $this->publicFolderMapper, $timeFactory, $logger, $urlGenerator, $htmlExporter, $this->authorizer, $this->bookmarks, $this->folders, $rootFolder, $this->lockManager);
}
/**
@ -717,4 +723,39 @@ class BookmarkControllerTest extends TestCase {
$r = $this->otherController->clickBookmark('https://www.golem.de');
$this->assertNotSame(Http::STATUS_OK, $r->getStatus());
}
/**
* @throws AlreadyExistsError
* @throws DoesNotExistException
* @throws MultipleObjectsReturnedException
* @throws UrlParseError
* @throws UserLimitExceededError
* @throws UnsupportedOperation
*/
public function testLock(): void {
$this->cleanUp();
$this->setupBookmarksWithSharedFolder();
$this->authorizer->setUserId($this->userId);
// First come, gets the lock
$output = $this->controller->acquireLock();
$this->assertEquals(Http::STATUS_OK, $output->getStatus());
// Second gets an error
$output = $this->controller->acquireLock();
$this->assertEquals(Http::STATUS_LOCKED, $output->getStatus());
// Releasing...
$output = $this->controller->releaseLock();
$this->assertEquals(Http::STATUS_OK, $output->getStatus());
// Then we get the lock again
$output = $this->controller->acquireLock();
$this->assertEquals(Http::STATUS_OK, $output->getStatus());
// other users can lock independently
$this->authorizer->setUserId($this->otherUserId);
$output = $this->otherController->acquireLock();
$this->assertEquals(Http::STATUS_OK, $output->getStatus());
}
}