From b4fa772bc5f23f84fc292f5d6bf884543d2bfe51 Mon Sep 17 00:00:00 2001 From: Sean Molenaar Date: Sat, 2 Jan 2021 17:57:17 +0100 Subject: Remove V1 item API Signed-off-by: Sean Molenaar --- CHANGELOG.md | 2 + lib/AppInfo/Application.php | 8 - lib/Command/Config/FolderDelete.php | 4 +- lib/Controller/ApiController.php | 2 +- lib/Controller/FeedApiController.php | 27 +- lib/Controller/FeedController.php | 18 +- lib/Controller/FolderApiController.php | 25 +- lib/Controller/FolderController.php | 35 +- lib/Controller/ItemApiController.php | 157 +- lib/Controller/ItemController.php | 164 +- lib/Controller/PageController.php | 2 +- lib/Db/FeedMapperV2.php | 24 + lib/Db/FeedType.php | 5 + lib/Db/FolderMapperV2.php | 26 + lib/Db/ItemMapper.php | 586 ----- lib/Db/ItemMapperV2.php | 366 ++- lib/Db/MapperFactory.php | 54 - lib/Db/Mysql/ItemMapper.php | 98 - lib/DependencyInjection/IFactory.php | 25 - .../RecommendedSiteNotFoundException.php | 21 + lib/Explore/RecommendedSiteNotFoundException.php | 21 - lib/Explore/RecommendedSites.php | 2 + lib/Fetcher/FetcherException.php | 28 - .../Exceptions/ServiceConflictException.php | 16 +- lib/Service/Exceptions/ServiceException.php | 25 +- .../Exceptions/ServiceNotFoundException.php | 17 +- .../Exceptions/ServiceValidationException.php | 16 +- lib/Service/FeedServiceV2.php | 20 +- lib/Service/FolderServiceV2.php | 17 + lib/Service/ItemService.php | 352 --- lib/Service/ItemServiceV2.php | 267 ++- lib/Service/Service.php | 12 +- phpunit.xml | 6 +- tests/Unit/Command/FolderDeleteTest.php | 2 +- tests/Unit/Controller/FeedApiControllerTest.php | 46 +- tests/Unit/Controller/FeedControllerTest.php | 73 +- tests/Unit/Controller/FolderApiControllerTest.php | 50 +- tests/Unit/Controller/FolderControllerTest.php | 74 +- tests/Unit/Controller/ItemApiControllerTest.php | 288 ++- tests/Unit/Controller/ItemControllerTest.php | 246 +- tests/Unit/Controller/PageControllerTest.php | 22 +- tests/Unit/Db/FeedMapperTest.php | 92 +- tests/Unit/Db/FolderMapperTest.php | 90 + tests/Unit/Db/ItemMapperTest.php | 2360 ++++++++++++++++++++ tests/Unit/Db/MapperFactoryTest.php | 59 - tests/Unit/Db/MapperTestUtility.php | 2 + tests/Unit/Service/FeedServiceTest.php | 21 +- tests/Unit/Service/FolderServiceTest.php | 17 + tests/Unit/Service/ItemServiceTest.php | 640 +++--- tests/Unit/Service/ServiceTest.php | 4 +- tests/psalm-baseline.xml | 42 +- 51 files changed, 4585 insertions(+), 1991 deletions(-) delete mode 100644 lib/Db/ItemMapper.php delete mode 100644 lib/Db/MapperFactory.php delete mode 100644 lib/Db/Mysql/ItemMapper.php delete mode 100644 lib/DependencyInjection/IFactory.php create mode 100644 lib/Explore/Exceptions/RecommendedSiteNotFoundException.php delete mode 100644 lib/Explore/RecommendedSiteNotFoundException.php delete mode 100644 lib/Fetcher/FetcherException.php delete mode 100644 lib/Service/ItemService.php create mode 100644 tests/Unit/Db/ItemMapperTest.php delete mode 100644 tests/Unit/Db/MapperFactoryTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ba171dd2..05d8cd52d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is almost based on [Keep a Changelog](https://keepachangelog.com/en/1 ## [Unreleased] ### Changed +- Remove outdated item DB code. +- Stop returning all feeds after marking folder as read. ### Fixed diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index a262e2a03..079b32fe8 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -31,9 +31,6 @@ use OCP\AppFramework\App; use OCP\Files\IRootFolder; use OCP\Files\Node; - -use OCA\News\Db\MapperFactory; -use OCA\News\Db\ItemMapper; use OCA\News\Fetcher\FeedFetcher; use OCA\News\Fetcher\Fetcher; use OCP\User\Events\BeforeUserDeletedEvent; @@ -90,11 +87,6 @@ class Application extends App implements IBootstrap $context->registerParameter('exploreDir', __DIR__ . '/../Explore/feeds'); $context->registerParameter('configFile', 'config.ini'); - // factories - $context->registerService(ItemMapper::class, function (ContainerInterface $c): ItemMapper { - return $c->get(MapperFactory::class)->build(); - }); - $context->registerService(HTMLPurifier::class, function (ContainerInterface $c): HTMLPurifier { $directory = $c->get(ITempManager::class)->getTempBaseDir() . '/news/cache/purifier'; diff --git a/lib/Command/Config/FolderDelete.php b/lib/Command/Config/FolderDelete.php index a80875682..8d7722b3b 100644 --- a/lib/Command/Config/FolderDelete.php +++ b/lib/Command/Config/FolderDelete.php @@ -2,7 +2,7 @@ namespace OCA\News\Command\Config; -use OCA\News\Service\Exceptions\ServiceException; +use OCA\News\Service\Exceptions\ServiceValidationException; use OCA\News\Service\FolderServiceV2; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -50,7 +50,7 @@ class FolderDelete extends Command $id = $input->getArgument('folder-id'); if ($id === null) { - throw new ServiceException('Can not remove root folder!'); + throw new ServiceValidationException('Can not remove root folder!'); } $this->folderService->delete($user, intval($id)); diff --git a/lib/Controller/ApiController.php b/lib/Controller/ApiController.php index a434f8de7..e6a83b21a 100644 --- a/lib/Controller/ApiController.php +++ b/lib/Controller/ApiController.php @@ -77,7 +77,7 @@ class ApiController extends BaseApiController * * @return array */ - public function index() + public function index(): array { return [ 'apiLevels' => ['v1-2'] diff --git a/lib/Controller/FeedApiController.php b/lib/Controller/FeedApiController.php index f247a4e66..43d92b7ca 100644 --- a/lib/Controller/FeedApiController.php +++ b/lib/Controller/FeedApiController.php @@ -19,12 +19,12 @@ use Exception; use OCA\News\Service\Exceptions\ServiceConflictException; use OCA\News\Service\Exceptions\ServiceNotFoundException; use OCA\News\Service\FeedServiceV2; +use OCA\News\Service\ItemServiceV2; use OCP\AppFramework\Http\JSONResponse; use \OCP\IRequest; use \OCP\IUserSession; use \OCP\AppFramework\Http; -use \OCA\News\Service\ItemService; use Psr\Log\LoggerInterface; class FeedApiController extends ApiController @@ -32,10 +32,9 @@ class FeedApiController extends ApiController use JSONHttpErrorTrait, ApiPayloadTrait; /** - * TODO: Remove - * @var ItemService + * @var ItemServiceV2 */ - private $oldItemService; + private $itemService; /** * @var FeedServiceV2 @@ -51,12 +50,12 @@ class FeedApiController extends ApiController IRequest $request, ?IUserSession $userSession, FeedServiceV2 $feedService, - ItemService $oldItemService, + ItemServiceV2 $itemService, LoggerInterface $logger ) { parent::__construct($request, $userSession); $this->feedService = $feedService; - $this->oldItemService = $oldItemService; + $this->itemService = $itemService; $this->logger = $logger; } @@ -70,12 +69,12 @@ class FeedApiController extends ApiController { $result = [ - 'starredCount' => $this->oldItemService->starredCount($this->getUserId()), + 'starredCount' => count($this->itemService->starred($this->getUserId())), 'feeds' => $this->serialize($this->feedService->findAllForUser($this->getUserId())) ]; try { - $result['newestItemId'] = $this->oldItemService->getNewestItemId($this->getUserId()); + $result['newestItemId'] = $this->itemService->newest($this->getUserId())->getId(); } catch (ServiceNotFoundException $ex) { // in case there are no items, ignore } @@ -96,9 +95,7 @@ class FeedApiController extends ApiController */ public function create(string $url, ?int $folderId = null) { - if ($folderId === 0) { - $folderId = null; - } + $folderId = $folderId === 0 ? null : $folderId; try { $this->feedService->purgeDeleted($this->getUserId(), time() - 600); @@ -109,7 +106,7 @@ class FeedApiController extends ApiController $this->feedService->fetch($feed); try { - $result['newestItemId'] = $this->oldItemService->getNewestItemId($this->getUserId()); + $result['newestItemId'] = $this->itemService->newest($this->getUserId())->getId(); } catch (ServiceNotFoundException $ex) { // in case there are no items, ignore } @@ -154,7 +151,7 @@ class FeedApiController extends ApiController */ public function read(int $feedId, int $newestItemId): void { - $this->oldItemService->readFeed($feedId, $newestItemId, $this->getUserId()); + $this->itemService->read($this->getUserId(), $feedId, $newestItemId); } @@ -170,9 +167,7 @@ class FeedApiController extends ApiController */ public function move(int $feedId, ?int $folderId) { - if ($folderId === 0) { - $folderId = null; - } + $folderId = $folderId === 0 ? null : $folderId; try { $feed = $this->feedService->find($this->getUserId(), $feedId); diff --git a/lib/Controller/FeedController.php b/lib/Controller/FeedController.php index 9f7c9b0d6..681dda4bc 100644 --- a/lib/Controller/FeedController.php +++ b/lib/Controller/FeedController.php @@ -18,12 +18,12 @@ use OCA\News\Service\Exceptions\ServiceNotFoundException; use OCA\News\Service\FeedServiceV2; use OCA\News\Service\FolderServiceV2; use OCA\News\Service\ImportService; +use OCA\News\Service\ItemServiceV2; use OCP\AppFramework\Http\JSONResponse; use OCP\IRequest; use OCP\IConfig; use OCP\AppFramework\Http; -use OCA\News\Service\ItemService; use OCA\News\Db\FeedType; use OCP\IUserSession; @@ -35,7 +35,9 @@ class FeedController extends Controller * @var FeedServiceV2 */ private $feedService; - //TODO: Remove + /** + * @var ItemServiceV2 + */ private $itemService; /** * @var FolderServiceV2 @@ -54,7 +56,7 @@ class FeedController extends Controller IRequest $request, FolderServiceV2 $folderService, FeedServiceV2 $feedService, - ItemService $itemService, + ItemServiceV2 $itemService, ImportService $importService, IConfig $settings, ?IUserSession $userSession @@ -79,11 +81,11 @@ class FeedController extends Controller // item id which will be used for marking feeds read $params = [ 'feeds' => $this->feedService->findAllForUser($this->getUserId()), - 'starred' => $this->itemService->starredCount($this->getUserId()) + 'starred' => count($this->itemService->starred($this->getUserId())) ]; try { - $id = $this->itemService->getNewestItemId($this->getUserId()); + $id = $this->itemService->newest($this->getUserId())->getId(); // An exception occurs if there is a newest item. If there is none, // simply ignore it and do not add the newestItemId @@ -183,7 +185,7 @@ class FeedController extends Controller $this->feedService->fetch($feed); try { - $id = $this->itemService->getNewestItemId($this->getUserId()); + $id = $this->itemService->newest($this->getUserId())->getId(); // An exception occurs if there is a newest item. If there is none, // simply ignore it and do not add the newestItemId $params['newestItemId'] = $id; @@ -261,7 +263,7 @@ class FeedController extends Controller $feed = $this->importService->importArticles($this->getUserId(), $json); $params = [ - 'starred' => $this->itemService->starredCount($this->getUserId()) + 'starred' => count($this->itemService->starred($this->getUserId())) ]; if ($feed) { @@ -281,7 +283,7 @@ class FeedController extends Controller */ public function read(int $feedId, int $highestItemId): array { - $this->itemService->readFeed($feedId, $highestItemId, $this->getUserId()); + $this->feedService->read($this->getUserId(), $feedId, $highestItemId); return [ 'feeds' => [ diff --git a/lib/Controller/FolderApiController.php b/lib/Controller/FolderApiController.php index 8de4b9e69..71fc503e2 100644 --- a/lib/Controller/FolderApiController.php +++ b/lib/Controller/FolderApiController.php @@ -20,7 +20,6 @@ use \OCP\IRequest; use \OCP\IUserSession; use \OCP\AppFramework\Http; -use \OCA\News\Service\ItemService; use \OCA\News\Service\FolderServiceV2; use \OCA\News\Service\Exceptions\ServiceNotFoundException; use \OCA\News\Service\Exceptions\ServiceConflictException; @@ -30,20 +29,19 @@ class FolderApiController extends ApiController { use JSONHttpErrorTrait, ApiPayloadTrait; + /** + * @var FolderServiceV2 + */ private $folderService; - //TODO: Remove - private $itemService; public function __construct( IRequest $request, ?IUserSession $userSession, - FolderServiceV2 $folderService, - ItemService $itemService + FolderServiceV2 $folderService ) { parent::__construct($request, $userSession); $this->folderService = $folderService; - $this->itemService = $itemService; } @@ -52,7 +50,7 @@ class FolderApiController extends ApiController * @NoCSRFRequired * @CORS */ - public function index() + public function index(): array { $folders = $this->folderService->findAllForUser($this->getUserId()); return ['folders' => $this->serialize($folders)]; @@ -142,14 +140,13 @@ class FolderApiController extends ApiController * @NoCSRFRequired * @CORS * - * @param int|null $folderId - * @param int $newestItemId + * @param int|null $folderId ID of the folder + * @param int $maxItemId The newest read item */ - public function read(?int $folderId, int $newestItemId): void + public function read(?int $folderId, int $maxItemId): void { - if ($folderId === 0) { - $folderId = null; - } - $this->itemService->readFolder($folderId, $newestItemId, $this->getUserId()); + $folderId = $folderId === 0 ? null : $folderId; + + $this->folderService->read($this->getUserId(), $folderId, $maxItemId); } } diff --git a/lib/Controller/FolderController.php b/lib/Controller/FolderController.php index 9dc13b309..da03f9863 100644 --- a/lib/Controller/FolderController.php +++ b/lib/Controller/FolderController.php @@ -14,13 +14,11 @@ namespace OCA\News\Controller; use OCA\News\Service\Exceptions\ServiceException; -use OCA\News\Service\FeedServiceV2; use OCP\AppFramework\Http\JSONResponse; use \OCP\IRequest; use \OCP\AppFramework\Http; use \OCA\News\Service\FolderServiceV2; -use \OCA\News\Service\ItemService; use \OCA\News\Service\Exceptions\ServiceNotFoundException; use \OCA\News\Service\Exceptions\ServiceConflictException; use OCP\IUserSession; @@ -33,24 +31,14 @@ class FolderController extends Controller * @var FolderServiceV2 */ private $folderService; - /** - * @var FeedServiceV2 - */ - private $feedService; - //TODO: Remove - private $itemService; public function __construct( IRequest $request, FolderServiceV2 $folderService, - FeedServiceV2 $feedService, - ItemService $itemService, ?IUserSession $userSession ) { parent::__construct($request, $userSession); $this->folderService = $folderService; - $this->feedService = $feedService; - $this->itemService = $itemService; } @@ -134,12 +122,12 @@ class FolderController extends Controller /** * @NoAdminRequired * - * @param string $folderName - * @param int|null $folderId + * @param int|null $folderId The ID of the folder + * @param string $folderName The new name of the folder * * @return array|JSONResponse */ - public function rename(string $folderName, ?int $folderId) + public function rename(?int $folderId, string $folderName) { if (empty($folderId)) { return new JSONResponse([], Http::STATUS_BAD_REQUEST); @@ -159,21 +147,18 @@ class FolderController extends Controller * @NoAdminRequired * * @param int|null $folderId - * @param int $highestItemId + * @param int $maxItemId + * + * @return void * - * @return array + * @throws ServiceConflictException + * @throws ServiceNotFoundException */ - public function read(?int $folderId, int $highestItemId): array + public function read(?int $folderId, int $maxItemId): void { $folderId = $folderId === 0 ? null : $folderId; - $this->itemService->readFolder( - $folderId, - $highestItemId, - $this->getUserId() - ); - $feeds = $this->feedService->findAllForUser($this->getUserId()); - return ['feeds' => $this->serialize($feeds)]; + $this->folderService->read($this->getUserId(), $folderId, $maxItemId); } diff --git a/lib/Controller/ItemApiController.php b/lib/Controller/ItemApiController.php index 7ec43bc5c..003c61fa2 100644 --- a/lib/Controller/ItemApiController.php +++ b/lib/Controller/ItemApiController.php @@ -15,7 +15,9 @@ namespace OCA\News\Controller; -use OCA\News\Service\ItemService; +use OCA\News\Db\FeedType; +use OCA\News\Service\Exceptions\ServiceConflictException; +use OCA\News\Service\Exceptions\ServiceValidationException; use OCA\News\Service\ItemServiceV2; use OCP\AppFramework\Http\JSONResponse; use \OCP\IRequest; @@ -24,22 +26,27 @@ use \OCP\AppFramework\Http; use \OCA\News\Service\Exceptions\ServiceNotFoundException; +/** + * Class ItemApiController + * + * @package OCA\News\Controller + */ class ItemApiController extends ApiController { use JSONHttpErrorTrait, ApiPayloadTrait; - private $oldItemService; + /** + * @var ItemServiceV2 + */ private $itemService; public function __construct( IRequest $request, ?IUserSession $userSession, - ItemService $oldItemService, ItemServiceV2 $itemService ) { parent::__construct($request, $userSession); - $this->oldItemService = $oldItemService; $this->itemService = $itemService; } @@ -64,16 +71,38 @@ class ItemApiController extends ApiController int $batchSize = -1, int $offset = 0, bool $oldestFirst = false - ) { - $items = $this->oldItemService->findAllItems( - $id, - $type, - $batchSize, - $offset, - $getRead, - $oldestFirst, - $this->getUserId() - ); + ): array { + switch ($type) { + case FeedType::FEED: + $items = $this->itemService->findAllInFeedWithFilters( + $this->getUserId(), + $id, + $batchSize, + $offset, + !$getRead, + $oldestFirst + ); + break; + case FeedType::FOLDER: + $items = $this->itemService->findAllInFolderWithFilters( + $this->getUserId(), + $id, + $batchSize, + $offset, + !$getRead, + $oldestFirst + ); + break; + default: + $items = $this->itemService->findAllWithFilters( + $this->getUserId(), + $type, + $batchSize, + $offset, + $oldestFirst + ); + break; + } return ['items' => $this->serialize($items)]; } @@ -88,8 +117,10 @@ class ItemApiController extends ApiController * @param int $id * @param int $lastModified * @return array|JSONResponse + * + * @throws ServiceValidationException */ - public function updated(int $type = 3, int $id = 0, int $lastModified = 0) + public function updated(int $type = 3, int $id = 0, int $lastModified = 0): array { // needs to be turned into a millisecond timestamp to work properly if (strlen((string) $lastModified) <= 10) { @@ -97,27 +128,33 @@ class ItemApiController extends ApiController } else { $paddedLastModified = $lastModified; } - $items = $this->oldItemService->findAllNew( - $id, - $type, - (int) $paddedLastModified, - true, - $this->getUserId() - ); + + switch ($type) { + case FeedType::FEED: + $items = $this->itemService->findAllInFeedAfter($this->getUserId(), $id, $paddedLastModified, false); + break; + case FeedType::FOLDER: + $items = $this->itemService->findAllInFolderAfter($this->getUserId(), $id, $paddedLastModified, false); + break; + default: + $items = $this->itemService->findAllAfter($this->getUserId(), $type, $paddedLastModified); + break; + } return ['items' => $this->serialize($items)]; } - /** - * @return JSONResponse|array + * @param int $itemId + * @param bool $isRead * - * @psalm-return JSONResponse|array + * @return array|JSONResponse + * @throws ServiceConflictException */ - private function setRead(bool $isRead, int $itemId) + private function setRead(int $itemId, bool $isRead) { try { - $this->oldItemService->read($itemId, $isRead, $this->getUserId()); + $this->itemService->read($this->getUserId(), $itemId, $isRead); } catch (ServiceNotFoundException $ex) { return $this->error($ex, Http::STATUS_NOT_FOUND); } @@ -134,10 +171,11 @@ class ItemApiController extends ApiController * @param int $itemId * * @return array|JSONResponse + * @throws ServiceConflictException */ public function read(int $itemId) { - return $this->setRead(true, $itemId); + return $this->setRead($itemId, true); } @@ -149,27 +187,25 @@ class ItemApiController extends ApiController * @param int $itemId * * @return array|JSONResponse + * @throws ServiceConflictException */ public function unread(int $itemId) { - return $this->setRead(false, $itemId); + return $this->setRead($itemId, false); } - /** - * @return JSONResponse|array + * @param int $feedId + * @param string $guidHash + * @param bool $isStarred * - * @psalm-return JSONResponse|array + * @return array|JSONResponse + * @throws ServiceConflictException */ - private function setStarred(bool $isStarred, int $feedId, string $guidHash) + private function setStarred(int $feedId, string $guidHash, bool $isStarred) { try { - $this->oldItemService->star( - $feedId, - $guidHash, - $isStarred, - $this->getUserId() - ); + $this->itemService->starByGuid($this->getUserId(), $feedId, $guidHash, $isStarred); } catch (ServiceNotFoundException $ex) { return $this->error($ex, Http::STATUS_NOT_FOUND); } @@ -187,10 +223,11 @@ class ItemApiController extends ApiController * @param string $guidHash * * @return array|JSONResponse + * @throws ServiceConflictException */ public function star(int $feedId, string $guidHash) { - return $this->setStarred(true, $feedId, $guidHash); + return $this->setStarred($feedId, $guidHash, true); } @@ -203,10 +240,11 @@ class ItemApiController extends ApiController * @param string $guidHash * * @return array|JSONResponse + * @throws ServiceConflictException */ public function unstar(int $feedId, string $guidHash) { - return $this->setStarred(false, $feedId, $guidHash); + return $this->setStarred($feedId, $guidHash, false); } @@ -223,15 +261,20 @@ class ItemApiController extends ApiController */ public function readAll(int $newestItemId): void { - $this->oldItemService->readAll($newestItemId, $this->getUserId()); + $this->itemService->readAll($this->getUserId(), $newestItemId); } - - private function setMultipleRead(bool $isRead, array $items): void + /** + * @param array $items + * @param bool $isRead + * + * @throws ServiceConflictException + */ + private function setMultipleRead(array $items, bool $isRead): void { foreach ($items as $id) { try { - $this->oldItemService->read($id, $isRead, $this->getUserId()); + $this->itemService->read($this->getUserId(), $id, $isRead); } catch (ServiceNotFoundException $ex) { continue; } @@ -249,10 +292,12 @@ class ItemApiController extends ApiController * @param int[] $items item ids * * @return void + * + * @throws ServiceConflictException */ public function readMultiple(array $items): void { - $this->setMultipleRead(true, $items); + $this->setMultipleRead($items, true); } @@ -266,30 +311,32 @@ class ItemApiController extends ApiController * @param int[] $items item ids * * @return void + * + * @throws ServiceConflictException */ public function unreadMultiple(array $items): void { - $this->setMultipleRead(false, $items); + $this->setMultipleRead($items, false); } /** - * @param bool $isStarred * @param array $items + * @param bool $isStarred * * @return void */ - private function setMultipleStarred(bool $isStarred, array $items): void + private function setMultipleStarred(array $items, bool $isStarred): void { foreach ($items as $item) { try { - $this->oldItemService->star( + $this->itemService->starByGuid( + $this->getUserId(), $item['feedId'], $item['guidHash'], - $isStarred, - $this->getUserId() + $isStarred ); - } catch (ServiceNotFoundException $ex) { + } catch (ServiceNotFoundException | ServiceConflictException $ex) { continue; } } @@ -309,7 +356,7 @@ class ItemApiController extends ApiController */ public function starMultiple(array $items): void { - $this->setMultipleStarred(true, $items); + $this->setMultipleStarred($items, true); } @@ -326,6 +373,6 @@ class ItemApiController extends ApiController */ public function unstarMultiple(array $items): void { - $this->setMultipleStarred(false, $items); + $this->setMultipleStarred($items, false); } } diff --git a/lib/Controller/ItemController.php b/lib/Controller/ItemController.php index 96ebcbaec..02a308d87 100644 --- a/lib/Controller/ItemController.php +++ b/lib/Controller/ItemController.php @@ -13,20 +13,31 @@ namespace OCA\News\Controller; +use OCA\News\Db\FeedType; +use OCA\News\Service\Exceptions\ServiceConflictException; use OCA\News\Service\FeedServiceV2; +use OCP\AppFramework\Http\JSONResponse; use \OCP\IRequest; use \OCP\IConfig; use \OCP\AppFramework\Http; use \OCA\News\Service\Exceptions\ServiceException; use \OCA\News\Service\Exceptions\ServiceNotFoundException; -use \OCA\News\Service\ItemService; +use \OCA\News\Service\ItemServiceV2; use OCP\IUserSession; +/** + * Class ItemController + * + * @package OCA\News\Controller + */ class ItemController extends Controller { use JSONHttpErrorTrait; + /** + * @var ItemServiceV2 + */ private $itemService; /** * @var FeedServiceV2 @@ -40,7 +51,7 @@ class ItemController extends Controller public function __construct( IRequest $request, FeedServiceV2 $feedService, - ItemService $itemService, + ItemServiceV2 $itemService, IConfig $settings, ?IUserSession $userSession ) { @@ -71,7 +82,7 @@ class ItemController extends Controller ?bool $showAll = null, ?bool $oldestFirst = null, string $search = '' - ) { + ): array { // in case this is called directly and not from the website use the // internal state @@ -104,15 +115,14 @@ class ItemController extends Controller $type ); - $params = []; + $return = []; // split search parameter on url space - $search = trim(urldecode($search)); - $search = preg_replace('/\s+/', ' ', $search); // remove multiple ws - if ($search === '') { - $search = []; - } else { - $search = explode(' ', $search); + $search_string = trim(urldecode($search)); + $search_string = preg_replace('/\s+/', ' ', $search_string); // remove multiple ws + $search_items = []; + if ($search !== '') { + $search_items = explode(' ', $search_string); } try { @@ -120,30 +130,54 @@ class ItemController extends Controller // we need to pass the newest feeds to not let the unread count get // out of sync if ($offset === 0) { - $params['newestItemId'] = - $this->itemService->getNewestItemId($this->getUserId()); - $params['feeds'] = $this->feedService->findAllForUser($this->getUserId()); - $params['starred'] = - $this->itemService->starredCount($this->getUserId()); + $return['newestItemId'] = $this->itemService->newest($this->getUserId())->getId(); + $return['feeds'] = $this->feedService->findAllForUser($this->getUserId()); + $return['starred'] = count($this->itemService->starred($this->getUserId())); } - $params['items'] = $this->itemService->findAllItems( - $id, - $type, - $limit, - $offset, - $showAll, - $oldestFirst, - $this->getUserId(), - $search - ); + switch ($type) { + case FeedType::FEED: + $items = $this->itemService->findAllInFeedWithFilters( + $this->getUserId(), + $id, + $limit, + $offset, + !$showAll, + $oldestFirst, + $search_items + ); + break; + case FeedType::FOLDER: + $items = $this->itemService->findAllInFolderWithFilters( + $this->getUserId(), + $id, + $limit, + $offset, + !$showAll, + $oldestFirst, + $search_items + ); + break; + default: + $items = $this->itemService->findAllWithFilters( + $this->getUserId(), + $type, + $limit, + $offset, + $oldestFirst, + $search_items + ); + break; + } + $return['items'] = $items; // this gets thrown if there are no items // in that case just return an empty array } catch (ServiceException $ex) { + //NO-OP } - return $params; + return $return; } @@ -155,7 +189,7 @@ class ItemController extends Controller * @param int $lastModified * @return array */ - public function newItems($type, $id, $lastModified = 0) + public function newItems(int $type, int $id, $lastModified = 0): array { $showAll = $this->settings->getUserValue( $this->getUserId(), @@ -163,28 +197,47 @@ class ItemController extends Controller 'showAll' ) === '1'; - $params = []; + $return = []; try { - $params['newestItemId'] = - $this->itemService->getNewestItemId($this->getUserId()); - $params['feeds'] = $this->feedService->findAllForUser($this->getUserId()); - $params['starred'] = - $this->itemService->starredCount($this->getUserId()); - $params['items'] = $this->itemService->findAllNew( - $id, - $type, - $lastModified, - $showAll, - $this->getUserId() - ); + switch ($type) { + case FeedType::FEED: + $items = $this->itemService->findAllInFeedAfter( + $this->getUserId(), + $id, + $lastModified, + !$showAll + ); + break; + case FeedType::FOLDER: + $items = $this->itemService->findAllInFolderAfter( + $this->getUserId(), + $id, + $lastModified, + !$showAll + ); + break; + default: + $items = $this->itemService->findAllAfter( + $this->getUserId(), + $type, + $lastModified + ); + break; + } + + $return['newestItemId'] = $this->itemService->newest($this->getUserId())->getId(); + $return['feeds'] = $this->feedService->findAllForUser($this->getUserId()); + $return['starred'] = count($this->itemService->starred($this->getUserId())); + $return['items'] = $items; // this gets thrown if there are no items // in that case just return an empty array } catch (ServiceException $ex) { + //NO-OP } - return $params; + return $return; } @@ -194,16 +247,17 @@ class ItemController extends Controller * @param int $feedId * @param string $guidHash * @param bool $isStarred - * @return array|\OCP\AppFramework\Http\JSONResponse + * + * @return array|JSONResponse */ - public function star($feedId, $guidHash, $isStarred) + public function star(int $feedId, string $guidHash, bool $isStarred) { try { - $this->itemService->star( + $this->itemService->starByGuid( + $this->getUserId(), $feedId, $guidHash, - $isStarred, - $this->getUserId() + $isStarred ); } catch (ServiceException $ex) { return $this->error($ex, Http::STATUS_NOT_FOUND); @@ -218,12 +272,13 @@ class ItemController extends Controller * * @param int $itemId * @param bool $isRead - * @return array|\OCP\AppFramework\Http\JSONResponse + * + * @return array|JSONResponse */ - public function read($itemId, $isRead = true) + public function read(int $itemId, $isRead = true) { try { - $this->itemService->read($itemId, $isRead, $this->getUserId()); + $this->itemService->read($this->getUserId(), $itemId, $isRead); } catch (ServiceException $ex) { return $this->error($ex, Http::STATUS_NOT_FOUND); } @@ -236,11 +291,12 @@ class ItemController extends Controller * @NoAdminRequired * * @param int $highestItemId + * * @return array */ - public function readAll($highestItemId) + public function readAll(int $highestItemId): array { - $this->itemService->readAll($highestItemId, $this->getUserId()); + $this->itemService->readAll($this->getUserId(), $highestItemId); return ['feeds' => $this->feedService->findAllForUser($this->getUserId())]; } @@ -252,12 +308,12 @@ class ItemController extends Controller * * @return void */ - public function readMultiple($itemIds): void + public function readMultiple(array $itemIds): void { foreach ($itemIds as $id) { try { - $this->itemService->read($id, true, $this->getUserId()); - } catch (ServiceNotFoundException $ex) { + $this->itemService->read($this->getUserId(), $id, true); + } catch (ServiceNotFoundException | ServiceConflictException $ex) { continue; } } diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php index 35924d10c..87ed91c73 100644 --- a/lib/Controller/PageController.php +++ b/lib/Controller/PageController.php @@ -14,6 +14,7 @@ namespace OCA\News\Controller; use OCA\News\AppInfo\Application; +use OCA\News\Explore\Exceptions\RecommendedSiteNotFoundException; use OCP\IRequest; use OCP\IConfig; use OCP\IL10N; @@ -24,7 +25,6 @@ use OCP\AppFramework\Http\ContentSecurityPolicy; use OCA\News\Service\StatusService; use OCA\News\Explore\RecommendedSites; -use OCA\News\Explore\RecommendedSiteNotFoundException; use OCA\News\Db\FeedType; use OCP\IUserSession; diff --git a/lib/Db/FeedMapperV2.php b/lib/Db/FeedMapperV2.php index b3d8879f3..5e346732c 100644 --- a/lib/Db/FeedMapperV2.php +++ b/lib/Db/FeedMapperV2.php @@ -153,4 +153,28 @@ class FeedMapperV2 extends NewsMapperV2 return $this->findEntities($builder); } + + /** + * @param string $userId + * @param int $id + * @param int|null $maxItemID + */ + public function read(string $userId, int $id, ?int $maxItemID = null): void + { + $builder = $this->db->getQueryBuilder(); + $builder->update(ItemMapperV2::TABLE_NAME, 'items') + ->innerJoin('items', FeedMapperV2::TABLE_NAME, 'feeds', 'items.feed_id = feeds.id') + ->setValue('unread', 0) + ->andWhere('feeds.user_id = :userId') + ->andWhere('feeds.id = :feedId') + ->setParameter('userId', $userId) + ->setParameter('feedId', $id); + + if ($maxItemID !== null) { + $builder->andWhere('items.id =< :maxItemId') + ->setParameter('maxItemId', $maxItemID); + } + + $this->db->executeUpdate($builder->getSQL()); + } } diff --git a/lib/Db/FeedType.php b/lib/Db/FeedType.php index bf487992c..1ccd592a8 100644 --- a/lib/Db/FeedType.php +++ b/lib/Db/FeedType.php @@ -13,6 +13,11 @@ namespace OCA\News\Db; +/** + * Enum FeedType + * + * @package OCA\News\Db + */ class FeedType { const FEED = 0; diff --git a/lib/Db/FolderMapperV2.php b/lib/Db/FolderMapperV2.php index 85e07c07f..12fa26887 100644 --- a/lib/Db/FolderMapperV2.php +++ b/lib/Db/FolderMapperV2.php @@ -95,4 +95,30 @@ class FolderMapperV2 extends NewsMapperV2 return $this->findEntity($builder); } + + /** + * @param string $userId + * @param int $id + * @param int|null $maxItemID + * + * @return void + */ + public function read(string $userId, int $id, ?int $maxItemID = null): void + { + $builder = $this->db->getQueryBuilder(); + $builder->update(ItemMapperV2::TABLE_NAME, 'items') + ->innerJoin('items', FeedMapperV2::TABLE_NAME, 'feeds', 'items.feed_id = feeds.id') + ->setValue('unread', 0) + ->andWhere('feeds.user_id = :userId') + ->andWhere('feeds.folder_id = :folderId') + ->setParameter('userId', $userId) + ->setParameter('folderId', $id); + + if ($maxItemID !== null) { + $builder->andWhere('items.id =< :maxItemId') + ->setParameter('maxItemId', $maxItemID); + } + + $this->db->executeUpdate($builder->getSQL()); + } } diff --git a/lib/Db/ItemMapper.php b/lib/Db/ItemMapper.php deleted file mode 100644 index 17f6638de..000000000 --- a/lib/Db/ItemMapper.php +++ /dev/null @@ -1,586 +0,0 @@ - - * @author Bernhard Posselt - * @copyright 2012 Alessandro Cosentino - * @copyright 2012-2014 Bernhard Posselt - */ - -namespace OCA\News\Db; - -use OCA\News\Utility\Time; -use OCP\AppFramework\Db\DoesNotExistException; -use OCP\AppFramework\Db\Entity; -use OCP\AppFramework\Db\Mapper; -use OCP\AppFramework\Db\MultipleObjectsReturnedException; -use OCP\DB\QueryBuilder\IQueryBuilder; -use OCP\IDBConnection; - -/** - * Class LegacyItemMapper - * - * @package OCA\News\Db - * @deprecated use ItemMapper - */ -class ItemMapper extends Mapper -{ - - const TABLE_NAME = 'news_items'; - /** - * @var Time - */ - private $time; - - /** - * NewsMapper constructor. - * - * @param IDBConnection $db Database connection - * @param Time $time Time class - */ - public function __construct(IDBConnection $db, Time $time) - { - parent::__construct($db, static::TABLE_NAME, Item::class); - $this->time = $time; - } - - private function makeSelectQuery( - string $prependTo = '', - bool $oldestFirst = false, - bool $distinctFingerprint = false - ): string { - if ($oldestFirst) { - $ordering = 'ASC'; - } else { - $ordering = 'DESC'; - } - - return 'SELECT `items`.* FROM `*PREFIX*news_items` `items` ' . - 'JOIN `*PREFIX*news_feeds` `feeds` ' . - 'ON `feeds`.`id` = `items`.`feed_id` ' . - 'AND `feeds`.`deleted_at` = 0 ' . - 'AND `feeds`.`user_id` = ? ' . - $prependTo . - 'LEFT OUTER JOIN `*PREFIX*news_folders` `folders` ' . - 'ON `folders`.`id` = `feeds`.`folder_id` ' . - 'WHERE `feeds`.`folder_id` IS NULL ' . - 'OR `folders`.`deleted_at` = 0 ' . - 'ORDER BY `items`.`id` ' . $ordering; - } - - /** - * check if type is feed or all items should be shown - * - * @param bool $showAll - * @param int|null $type - * @return string - */ - private function buildStatusQueryPart($showAll, $type = null) - { - $sql = ''; - - if (isset($type) && $type === FeedType::STARRED) { - $sql = 'AND `items`.`starred` = '; - $sql .= $this->db->quote(true, IQueryBuilder::PARAM_BOOL) . ' '; - } elseif (!$showAll || $type === FeedType::UNREAD) { - $sql .= 'AND `items`.`unread` = '; - $sql .= $this->db->quote(true, IQueryBuilder::PARAM_BOOL) . ' '; - } - - return $sql; - } - - private function buildSearchQueryPart(array $search = []): string - { - return str_repeat('AND `items`.`search_index` LIKE ? ', count($search)); - } - - /** - * wrap and escape search parameters in a like statement - * - * @param string[] $search an array of strings that should be searched - * @return array with like parameters - */ - private function buildLikeParameters($search = []) - { - return array_map( - function ($param) { - $param = addcslashes($param, '\\_%'); - return '%' . mb_strtolower($param, 'UTF-8') . '%'; - }, - $search - ); - } - - /** - * @param int $id - * @param string $userId - * @return \OCA\News\Db\Item|Entity - */ - public function find(string $userId, int $id) - { - $sql = $this->makeSelectQuery('AND `items`.`id` = ? '); - return $this->findEntity($sql, [$userId, $id]); - } - - public function starredCount(string $userId): int - { - $sql = 'SELECT COUNT(*) AS size FROM `*PREFIX*news_items` `items` ' . - 'JOIN `*PREFIX*news_feeds` `feeds` ' . - 'ON `feeds`.`id` = `items`.`feed_id` ' . - 'AND `feeds`.`deleted_at` = 0 ' . - 'AND `feeds`.`user_id` = ? ' . - 'AND `items`.`starred` = ? ' . - 'LEFT OUTER JOIN `*PREFIX*news_folders` `folders` ' . - 'ON `folders`.`id` = `feeds`.`folder_id` ' . - 'WHERE `feeds`.`folder_id` IS NULL ' . - 'OR `folders`.`deleted_at` = 0'; - - $params = [$userId, true]; - - $result = $this->execute($sql, $params)->fetch(); - - return (int)$result['size']; - } - - - public function readAll(int $highestItemId, string $time, string $userId): void - { - $sql = 'UPDATE `*PREFIX*news_items` ' . - 'SET unread = ? ' . - ', `last_modified` = ? ' . - 'WHERE `feed_id` IN (' . - 'SELECT `id` FROM `*PREFIX*news_feeds` ' . - 'WHERE `user_id` = ? ' . - ') ' . - 'AND `id` <= ?'; - $params = [false, $time, $userId, $highestItemId]; - $this->execute($sql, $params); - } - - - public function readFolder(?int $folderId, int $highestItemId, string $time, string $userId): void - { - $folderWhere = is_null($folderId) ? 'IS' : '='; - $sql = 'UPDATE `*PREFIX*news_items` ' . - 'SET unread = ? ' . - ', `last_modified` = ? ' . - 'WHERE `feed_id` IN (' . - 'SELECT `id` FROM `*PREFIX*news_feeds` ' . - "WHERE `folder_id` ${folderWhere} ? " . - 'AND `user_id` = ? ' . - ') ' . - 'AND `id` <= ?'; - $params = [false, $time, $folderId, $userId, - $highestItemId]; - $this->execute($sql, $params); - } - - - public function readFeed(int $feedId, int $highestItemId, string $time, string $userId): void - { - $sql = 'UPDATE `*PREFIX*news_items` ' . - 'SET unread = ? ' . - ', `last_modified` = ? ' . - 'WHERE `feed_id` = ? ' . - 'AND `id` <= ? ' . - 'AND EXISTS (' . - 'SELECT * FROM `*PREFIX*news_feeds` ' . - 'WHERE `user_id` = ? ' . - 'AND `id` = ? ) '; - $params = [false, $time, $feedId, $highestItemId, - $userId, $feedId]; - - $this->execute($sql, $params); - } - - - private function getOperator(bool $oldestFirst): string - { - if ($oldestFirst) { - return '>'; - } else { - return '<'; - } - } - - - public function findAllNew(int $updatedSince, int $type, bool $showAll, string $userId): array - { - $sql = $this->buildStatusQueryPart($showAll, $type); - - $sql .= 'AND `items`.`last_modified` >= ? '; - $sql = $this->makeSelectQuery($sql); - $params = [$userId, $updatedSince]; - return $this->findEntities($sql, $params); - } - - - public function findAllNewFolder(?int $id, int $updatedSince, bool $showAll, string $userId): array - { - $sql = $this->buildStatusQueryPart($showAll); - - $folderWhere = is_null($id) ? 'IS' : '='; - $sql .= "AND `feeds`.`folder_id` ${folderWhere} ? " . - 'AND `items`.`last_modified` >= ? '; - $sql = $this->makeSelectQuery($sql); - $params = [$userId, $id, $updatedSince]; - return $this->findEntities($sql, $params); - } - - - public function findAllNewFeed(?int $id, int $updatedSince, bool $showAll, string $userId): array - { - $sql = $this->buildStatusQueryPart($showAll); - - $sql .= 'AND `items`.`feed_id` = ? ' . - 'AND `items`.`last_modified` >= ? '; - $sql = $this->makeSelectQuery($sql); - $params = [$userId, $id, $updatedSince]; - return $this->findEntities($sql, $params); - } - - - /** - * @param (int|mixed|null)[] $params - */ - private function findEntitiesIgnoringNegativeLimit(string $sql, array $params, int $limit): array - { - // ignore limit if negative to offer a way to return all feeds - if ($limit >= 0) { - return $this->findEntities($sql, $params, $limit); - } else { - return $this->findEntities($sql, $params); - } - } - - - public function findAllFeed( - ?int $id, - int $limit, - int $offset, - bool $showAll, - bool $oldestFirst, - string $userId, - array $search = [] - ): array { - $params = [$userId]; - $params = array_merge($params, $this->buildLikeParameters($search)); - $params[] = $id; - - $sql = $this->buildStatusQueryPart($showAll); - $sql .= $this->buildSearchQueryPart($search); - - $sql .= 'AND `items`.`feed_id` = ? '; - if ($offset !== 0) { - $sql .= 'AND `items`.`id` ' . - $this->getOperator($oldestFirst) . ' ? '; - $params[] = $offset; - } - $sql = $this->makeSelectQuery($sql, $oldestFirst); - return $this->findEntitiesIgnoringNegativeLimit($sql, $params, $limit); - } - - - public function findAllFolder( - ?int $id, - int $limit, - int $offset, - bool $showAll, - bool $oldestFirst, - string $userId, - array $search = [] - ): array { - $params = [$userId]; - $params = array_merge($params, $this->buildLikeParameters($search)); - $params[] = $id; - - $sql = $this->buildStatusQueryPart($showAll); - $sql .= $this->buildSearchQueryPart($search); - - $folderWhere = is_null($id) ? 'IS' : '='; - $sql .= "AND `feeds`.`folder_id` ${folderWhere} ? "; - if ($offset !== 0) { - $sql .= 'AND `items`.`id` ' . $this->getOperator($oldestFirst) . ' ? '; - $params[] = $offset; - } - $sql = $this->makeSelectQuery($sql, $oldestFirst); - return $this->findEntitiesIgnoringNegativeLimit($sql, $params, $limit); - } - - - /** - * @param string[] $search - */ - public function findAllItems( - int $limit, - int $offset, - int $type, - bool $showAll, - bool $oldestFirst, - string $userId, - array $search = [] - ): array { - $params = [$userId]; - $params = array_merge($params, $this->buildLikeParameters($search)); - $sql = $this->buildStatusQueryPart($showAll, $type); - $sql .= $this->buildSearchQueryPart($search); - - if ($offset !== 0) { - $sql .= 'AND `items`.`id` ' . - $this->getOperator($oldestFirst) . ' ? '; - $params[] = $offset; - } - - $sql = $this->makeSelectQuery($sql, $oldestFirst); - - return $this->findEntitiesIgnoringNegativeLimit($sql, $params, $limit); - } - - - public function findAllUnreadOrStarred(string $userId): array - { - $params = [$userId, true, true]; - $sql = 'AND (`items`.`unread` = ? OR `items`.`starred` = ?) '; - $sql = $this->makeSelectQuery($sql); - return $this->findEntities($sql, $params); - } - - /** - * @param $guidHash - * @param $feedId - * @param $userId - * - * @return Entity|Item - * @throws DoesNotExistException - * @throws MultipleObjectsReturnedException - */ - public function findByGuidHash($guidHash, $feedId, $userId) - { - $sql = $this->makeSelectQuery( - 'AND `items`.`guid_hash` = ? ' . - 'AND `feeds`.`id` = ? ' - ); - - return $this->findEntity($sql, [$userId, $guidHash, $feedId]); - } - - - /** - * Delete all items for feeds that have over $threshold unread and not - * starred items - * - * @param int $threshold the number of items that should be deleted - * - * @return void - */ - public function deleteReadOlderThanThreshold($threshold) - { - $params = [false, false, $threshold]; - - $sql = 'SELECT (COUNT(*) - `feeds`.`articles_per_update`) AS `size`, ' . - '`feeds`.`id` AS `feed_id`, `feeds`.`articles_per_update` ' . - 'FROM `*PREFIX*news_items` `items` ' . - 'JOIN `*PREFIX*news_feeds` `feeds` ' . - 'ON `feeds`.`id` = `items`.`feed_id` ' . - 'AND `items`.`unread` = ? ' . - 'AND `items`.`starred` = ? ' . - 'GROUP BY `feeds`.`id`, `feeds`.`articles_per_update` ' . - 'HAVING COUNT(*) > ?'; - - $result = $this->execute($sql, $params); - - while ($row = $result->fetch()) { - $size = (int)$row['size']; - $limit = $size - $threshold; - $feed_id = $row['feed_id']; - - if ($limit > 0) { - $params = [false, false, $feed_id, $limit]; - $sql = 'SELECT `id` FROM `*PREFIX*news_items` ' . - 'WHERE `unread` = ? ' . - 'AND `starred` = ? ' . - 'AND `feed_id` = ? ' . - 'ORDER BY `id` ASC ' . - 'LIMIT 1 ' . - 'OFFSET ? '; - } - $limit_result = $this->execute($sql, $params); - if ($limit_row = $limit_result->fetch()) { - $limit_id = (int)$limit_row['id']; - $params = [false, false, $feed_id, $limit_id]; - $sql = 'DELETE FROM `*PREFIX*news_items` ' . - 'WHERE `unread` = ? ' . - 'AND `starred` = ? ' . - 'AND `feed_id` = ? ' . - 'AND `id` < ? '; - $this->execute($sql, $params); - } - } - } - - - public function getNewestItemId(string $userId): int - { - $sql = 'SELECT MAX(`items`.`id`) AS `max_id` ' . - 'FROM `*PREFIX*news_items` `items` ' . - 'JOIN `*PREFIX*news_feeds` `feeds` ' . - 'ON `feeds`.`id` = `items`.`feed_id` ' . - 'AND `feeds`.`user_id` = ?'; - $params = [$userId]; - - $result = $this->findOneQuery($sql, $params); - - return (int)$result['max_id']; - } - - - /** - * Returns a list of ids and userid of all items - * - * @param int|null $limit - * @param int|null $offset - * - * @return array|false - */ - public function findAllIds(?int $limit = null, ?int $offset = null) - { - $sql = 'SELECT `id` FROM `*PREFIX*news_items`'; - return $this->execute($sql, [], $limit, $offset)->fetchAll(); - } - - /** - * Update search indices of all items - * - * @return void - */ - public function updateSearchIndices(): void - { - // update indices in steps to prevent memory issues on larger systems - $step = 1000; // update 1000 items at a time - $itemCount = 1; - $offset = 0; - - // stop condition if there are no previously fetched items - while ($itemCount > 0) { - $items = $this->findAllIds($step, $offset); - $itemCount = count($items); - $this->updateSearchIndex($items); - $offset += $step; - } - } - - private function updateSearchIndex(array $items = []): void - { - foreach ($items as $row) { - $sql = 'SELECT * FROM `*PREFIX*news_items` WHERE `id` = ?'; - $params = [$row['id']]; - $item = $this->findEntity($sql, $params); - $item->generateSearchIndex(); - $this->update($item); - } - } - - /** - * @return void - */ - public function readItem(int $itemId, bool $isRead, string $lastModified, string $userId) - { - $item = $this->find($userId, $itemId); - - // reading an item should set all of the same items as read, whereas - // marking an item as unread should only mark the selected instance - // as unread - if ($isRead) { - $sql = 'UPDATE `*PREFIX*news_items` - SET `unread` = ?, - `last_modified` = ? - WHERE `fingerprint` = ? - AND `feed_id` IN ( - SELECT `f`.`id` FROM `*PREFIX*news_feeds` AS `f` - WHERE `f`.`user_id` = ? - )'; - $params = [false, $lastModified, $item->getFingerprint(), $userId]; - $this->execute($sql, $params); - } else { - $item->setLastModified($lastModified); - $item->setUnread(true); - $this->update($item); - } - } - - public function update(Entity $entity): Entity - { - $entity->setLastModified($this->time->getMicroTime()); - return parent::update($entity); - } - - public function insert(Entity $entity): Entity - { - $entity->setLastModified($this->time->getMicroTime()); - return parent::insert($entity); - } - - /** - * Remove deleted items. - * - * @return void - */ - public function purgeDeleted(): void - { - $builder = $this->db->getQueryBuilder(); - $builder->delete($this->tableName) - ->where('deleted_at != 0') - ->execute(); - } - /** - * Performs a SELECT query with all arguments appended to the WHERE clause - * The SELECT will be performed on the current table and takes the entity - * that is related for transforming the properties into column names - * - * Important: This method does not filter marked as deleted rows! - * - * @param array $search an assoc array from property to filter value - * @param int|null $limit Output limit - * @param int|null $offset Output offset - * - * @depreacted Legacy function - * - * @return Entity[] - */ - public function where(array $search = [], ?int $limit = null, ?int $offset = null) - { - $entity = new $this->entityClass(); - - // turn keys into sql query filter, e.g. feedId -> feed_id = :feedId - $filter = array_map( - function ($property) use ($entity) { - // check if the property actually exists on the entity to prevent - // accidental Sql injection - if (!property_exists($entity, $property)) { - $msg = 'Property ' . $property . ' does not exist on ' - . $this->entityClass; - throw new \BadFunctionCallException($msg); - } - - $column = $entity->propertyToColumn($property); - return $column . ' = :' . $property; - }, - array_keys($search) - ); - - $andStatement = implode(' AND ', $filter); - - $sql = 'SELECT * FROM `' . $this->getTableName() . '`'; - - if (count($search) > 0) { - $sql .= 'WHERE ' . $andStatement; - } - - return $this->findEntities($sql, $search, $limit, $offset); - } -} diff --git a/lib/Db/ItemMapperV2.php b/lib/Db/ItemMapperV2.php index f51b7af4d..8cdf9c430 100644 --- a/lib/Db/ItemMapperV2.php +++ b/lib/Db/ItemMapperV2.php @@ -12,6 +12,7 @@ namespace OCA\News\Db; +use OCA\News\Service\Exceptions\ServiceValidationException; use Doctrine\DBAL\FetchMode; use OCA\News\Utility\Time; use OCP\AppFramework\Db\DoesNotExistException; @@ -55,12 +56,11 @@ class ItemMapperV2 extends NewsMapperV2 ->from($this->tableName, 'items') ->innerJoin('items', FeedMapperV2::TABLE_NAME, 'feeds', 'items.feed_id = feeds.id') ->where('feeds.user_id = :user_id') - ->andWhere('deleted_at = 0') + ->andWhere('feeds.deleted_at = 0') ->setParameter('user_id', $userId, IQueryBuilder::PARAM_STR); foreach ($params as $key => $value) { - $builder->andWhere("${key} = :${key}") - ->setParameter($key, $value); + $builder->andWhere("${key} = " . $builder->createNamedParameter($value)); } return $this->findEntities($builder); @@ -74,13 +74,17 @@ class ItemMapperV2 extends NewsMapperV2 public function findAll(): array { $builder = $this->db->getQueryBuilder(); - $builder->addSelect('*') + $builder->select('*') ->from($this->tableName) - ->andWhere('deleted_at = 0'); + ->innerJoin('items', FeedMapperV2::TABLE_NAME, 'feeds', 'items.feed_id = feeds.id') + ->andWhere('feeds.deleted_at = 0'); return $this->findEntities($builder); } + /** + * @inheritDoc + */ public function findFromUser(string $userId, int $id): Entity { $builder = $this->db->getQueryBuilder(); @@ -89,9 +93,9 @@ class ItemMapperV2 extends NewsMapperV2 ->innerJoin('items', FeedMapperV2::TABLE_NAME, 'feeds', 'items.feed_id = feeds.id') ->where('feeds.user_id = :user_id') ->andWhere('items.id = :item_id') - ->andWhere('deleted_at = 0') + ->andWhere('feeds.deleted_at = 0') ->setParameter('user_id', $userId, IQueryBuilder::PARAM_STR) - ->setParameter('item_id', $id, IQueryBuilder::PARAM_STR); + ->setParameter('item_id', $id, IQueryBuilder::PARAM_INT); return $this->findEntity($builder); } @@ -102,15 +106,15 @@ class ItemMapperV2 extends NewsMapperV2 * @param int $feedId ID of the feed * @param string $guidHash hash to find with * - * @return Item + * @return Item|Entity * * @throws DoesNotExistException * @throws MultipleObjectsReturnedException */ - public function findByGuidHash(int $feedId, string $guidHash): Item + public function findByGuidHash(int $feedId, string $guidHash): Entity { $builder = $this->db->getQueryBuilder(); - $builder->addSelect('*') + $builder->select('*') ->from($this->tableName) ->andWhere('feed_id = :feed_id') ->andWhere('guid_hash = :guid_hash') @@ -120,6 +124,34 @@ class ItemMapperV2 extends NewsMapperV2 return $this->findEntity($builder); } + /** + * Find a user item by a GUID hash. + * + * @param string $userId + * @param int $feedId ID of the feed + * @param string $guidHash hash to find with + * + * @return Item|Entity + * + * @throws DoesNotExistException + * @throws MultipleObjectsReturnedException + */ + public function findForUserByGuidHash(string $userId, int $feedId, string $guidHash): Item + { + $builder = $this->db->getQueryBuilder(); + $builder->select('items.*') + ->from($this->tableName, 'items') + ->innerJoin('items', FeedMapperV2::TABLE_NAME, 'feeds', 'items.feed_id = feeds.id') + ->andWhere('feeds.user_id = :user_id') + ->andWhere('feeds.id = :feed_id') + ->andWhere