diff options
Diffstat (limited to 'lib/Service')
-rw-r--r-- | lib/Service/FeedService.php | 454 | ||||
-rw-r--r-- | lib/Service/FolderService.php | 178 | ||||
-rw-r--r-- | lib/Service/ItemService.php | 259 | ||||
-rw-r--r-- | lib/Service/Service.php | 62 | ||||
-rw-r--r-- | lib/Service/ServiceConflictException.php | 27 | ||||
-rw-r--r-- | lib/Service/ServiceException.php | 27 | ||||
-rw-r--r-- | lib/Service/ServiceNotFoundException.php | 27 | ||||
-rw-r--r-- | lib/Service/ServiceValidationException.php | 27 | ||||
-rw-r--r-- | lib/Service/StatusService.php | 57 |
9 files changed, 1118 insertions, 0 deletions
diff --git a/lib/Service/FeedService.php b/lib/Service/FeedService.php new file mode 100644 index 000000000..2d9bc6728 --- /dev/null +++ b/lib/Service/FeedService.php @@ -0,0 +1,454 @@ +<?php +/** + * ownCloud - News + * + * This file is licensed under the Affero General Public License version 3 or + * later. See the COPYING file. + * + * @author Alessandro Cosentino <cosenal@gmail.com> + * @author Bernhard Posselt <dev@bernhard-posselt.com> + * @copyright Alessandro Cosentino 2012 + * @copyright Bernhard Posselt 2012, 2014 + */ + +namespace OCA\News\Service; + +use HTMLPurifier; + +use OCP\ILogger; +use OCP\IL10N; +use OCP\AppFramework\Db\DoesNotExistException; + +use OCA\News\Db\Feed; +use OCA\News\Db\Item; +use OCA\News\Db\FeedMapper; +use OCA\News\Db\ItemMapper; +use OCA\News\Fetcher\Fetcher; +use OCA\News\Fetcher\FetcherException; +use OCA\News\Config\Config; +use OCA\News\Utility\Time; + + +class FeedService extends Service { + + private $feedFetcher; + private $itemMapper; + private $feedMapper; + private $logger; + private $l10n; + private $timeFactory; + private $autoPurgeMinimumInterval; + private $purifier; + private $loggerParams; + + public function __construct(FeedMapper $feedMapper, + Fetcher $feedFetcher, + ItemMapper $itemMapper, + ILogger $logger, + IL10N $l10n, + Time $timeFactory, + Config $config, + HTMLPurifier $purifier, + $LoggerParameters){ + parent::__construct($feedMapper); + $this->feedFetcher = $feedFetcher; + $this->itemMapper = $itemMapper; + $this->logger = $logger; + $this->l10n = $l10n; + $this->timeFactory = $timeFactory; + $this->autoPurgeMinimumInterval = + $config->getAutoPurgeMinimumInterval(); + $this->purifier = $purifier; + $this->feedMapper = $feedMapper; + $this->loggerParams = $LoggerParameters; + } + + /** + * Finds all feeds of a user + * @param string $userId the name of the user + * @return Feed[] + */ + public function findAll($userId){ + return $this->feedMapper->findAllFromUser($userId); + } + + + /** + * Finds all feeds from all users + * @return array of feeds + */ + public function findAllFromAllUsers() { + return $this->feedMapper->findAll(); + } + + + /** + * Creates a new feed + * @param string $feedUrl the url to the feed + * @param int $folderId the folder where it should be put into, 0 for root + * folder + * @param string $userId for which user the feed should be created + * @param string $title if given, this is used for the opml feed title + * @param string $basicAuthUser if given, basic auth is set for this feed + * @param string $basicAuthPassword if given, basic auth is set for this + * feed. Ignored if user is null or an empty string + * @throws ServiceConflictException if the feed exists already + * @throws ServiceNotFoundException if the url points to an invalid feed + * @return Feed the newly created feed + */ + public function create($feedUrl, $folderId, $userId, $title=null, + $basicAuthUser=null, $basicAuthPassword=null){ + // first try if the feed exists already + try { + list($feed, $items) = $this->feedFetcher->fetch($feedUrl, true, + null, null, false, $basicAuthUser, + $basicAuthPassword); + + // try again if feed exists depending on the reported link + try { + $this->feedMapper->findByUrlHash($feed->getUrlHash(), $userId); + throw new ServiceConflictException( + $this->l10n->t('Can not add feed: Exists already')); + + // If no matching feed was found everything was ok + } catch(DoesNotExistException $ex){} + + // insert feed + $itemCount = count($items); + $feed->setBasicAuthUser($basicAuthUser); + $feed->setBasicAuthPassword($basicAuthPassword); + $feed->setFolderId($folderId); + $feed->setUserId($userId); + $feed->setArticlesPerUpdate($itemCount); + + if ($title !== null && $title !== '') { + $feed->setTitle($title); + } + + $feed = $this->feedMapper->insert($feed); + + // insert items in reverse order because the first one is usually + // the newest item + $unreadCount = 0; + for($i=$itemCount-1; $i>=0; $i--){ + $item = $items[$i]; + $item->setFeedId($feed->getId()); + + // check if item exists (guidhash is the same) + // and ignore it if it does + try { + $this->itemMapper->findByGuidHash( + $item->getGuidHash(), $item->getFeedId(), $userId); + continue; + } catch(DoesNotExistException $ex){ + $unreadCount += 1; + $item->setBody($this->purifier->purify($item->getBody())); + $this->itemMapper->insert($item); + } + } + + // set unread count + $feed->setUnreadCount($unreadCount); + + return $feed; + } catch(FetcherException $ex){ + $this->logger->debug($ex->getMessage(), $this->loggerParams); + throw new ServiceNotFoundException($ex->getMessage()); + } + } + + + /** + * Runs all the feed updates + */ + public function updateAll(){ + // TODO: this method is not covered by any tests + $feeds = $this->feedMapper->findAll(); + foreach($feeds as $feed){ + try { + $this->update($feed->getId(), $feed->getUserId()); + } catch(\Exception $ex){ + // something is really wrong here, log it + $this->logger->error( + 'Unexpected error when updating feed ' . $ex->getMessage(), + $this->loggerParams + ); + } + } + } + + + /** + * Updates a single feed + * @param int $feedId the id of the feed that should be updated + * @param string $userId the id of the user + * @param bool $forceUpdate update even if the article exists already + * @throws ServiceNotFoundException if the feed does not exist + * @return Feed the updated feed entity + */ + public function update($feedId, $userId, $forceUpdate=false){ + $existingFeed = $this->find($feedId, $userId); + + if($existingFeed->getPreventUpdate() === true) { + return $existingFeed; + } + + // for backwards compability it can be that the location is not set + // yet, if so use the url + $location = $existingFeed->getLocation(); + if (!$location) { + $location = $existingFeed->getUrl(); + } + + try { + list($fetchedFeed, $items) = $this->feedFetcher->fetch( + $location, + false, + $existingFeed->getHttpLastModified(), + $existingFeed->getHttpEtag(), + $existingFeed->getFullTextEnabled(), + $existingFeed->getBasicAuthUser(), + $existingFeed->getBasicAuthPassword() + ); + + // if there is no feed it means that no update took place + if (!$fetchedFeed) { + return $existingFeed; + } + + // update number of articles on every feed update + $itemCount = count($items); + + // this is needed to adjust to updates that add more items + // than when the feed was created. You can't update the count + // if it's lower because it may be due to the caching headers + // that were sent as the request and it might cause unwanted + // deletion and reappearing of feeds + if ($itemCount > $existingFeed->getArticlesPerUpdate()) { + $existingFeed->setArticlesPerUpdate($itemCount); + } + + $existingFeed->setHttpLastModified( + $fetchedFeed->getHttpLastModified()); + $existingFeed->setHttpEtag($fetchedFeed->getHttpEtag()); + $existingFeed->setLocation($fetchedFeed->getLocation()); + + // insert items in reverse order because the first one is + // usually the newest item + for($i=$itemCount-1; $i>=0; $i--){ + $item = $items[$i]; + $item->setFeedId($existingFeed->getId()); + + try { + $dbItem = $this->itemMapper->findByGuidHash( + $item->getGuidHash(), $feedId, $userId + ); + + // in case of update + if ($forceUpdate || + $item->getPubDate() > $dbItem->getPubDate()) { + + $dbItem->setTitle($item->getTitle()); + $dbItem->setUrl($item->getUrl()); + $dbItem->setAuthor($item->getAuthor()); + $dbItem->setSearchIndex($item->getSearchIndex()); + $dbItem->setRtl($item->getRtl()); + $dbItem->setLastModified($item->getLastModified()); + $dbItem->setPubDate($item->getPubDate()); + $dbItem->setEnclosureMime($item->getEnclosureMime()); + $dbItem->setEnclosureLink($item->getEnclosureLink()); + $dbItem->setBody( + $this->purifier->purify($item->getBody()) + ); + + // update modes: 0 nothing, 1 set unread + if ($existingFeed->getUpdateMode() === 1) { + $dbItem->setUnread(); + } + + $this->itemMapper->update($dbItem); + } + } catch(DoesNotExistException $ex){ + $item->setBody( + $this->purifier->purify($item->getBody()) + ); + $this->itemMapper->insert($item); + } + } + + // mark feed as successfully updated + $existingFeed->setUpdateErrorCount(0); + $existingFeed->setLastUpdateError(''); + + } catch(FetcherException $ex){ + $existingFeed->setUpdateErrorCount( + $existingFeed->getUpdateErrorCount()+1 + ); + $existingFeed->setLastUpdateError($ex->getMessage()); + } + + $this->feedMapper->update($existingFeed); + + return $this->find($feedId, $userId); + } + + /** + * Import articles + * @param array $json the array with json + * @param string $userId the username + * @return Feed if one had to be created for nonexistent feeds + */ + public function importArticles($json, $userId) { + $url = 'http://owncloud/nofeed'; + $urlHash = md5($url); + + // build assoc array for fast access + $feeds = $this->findAll($userId); + $feedsDict = []; + foreach($feeds as $feed) { + $feedsDict[$feed->getLink()] = $feed; + } + + $createdFeed = false; + + // loop over all items and get the corresponding feed + // if the feed does not exist, create a separate feed for them + foreach ($json as $entry) { + $item = Item::fromImport($entry); + $feedLink = $entry['feedLink']; // this is not set on the item yet + + if(array_key_exists($feedLink, $feedsDict)) { + $feed = $feedsDict[$feedLink]; + $item->setFeedId($feed->getId()); + } elseif(array_key_exists($url, $feedsDict)) { + $feed = $feedsDict[$url]; + $item->setFeedId($feed->getId()); + } else { + $createdFeed = true; + $feed = new Feed(); + $feed->setUserId($userId); + $feed->setLink($url); + $feed->setUrl($url); + $feed->setTitle($this->l10n->t('Articles without feed')); + $feed->setAdded($this->timeFactory->getTime()); + $feed->setFolderId(0); + $feed->setPreventUpdate(true); + $feed = $this->feedMapper->insert($feed); + + $item->setFeedId($feed->getId()); + $feedsDict[$feed->getLink()] = $feed; + } + + try { + // if item exists, copy the status + $existingItem = $this->itemMapper->findByGuidHash( + $item->getGuidHash(), $feed->getId(), $userId); + $existingItem->setStatus($item->getStatus()); + $this->itemMapper->update($existingItem); + } catch(DoesNotExistException $ex){ + $item->setBody($this->purifier->purify($item->getBody())); + $item->generateSearchIndex(); + $this->itemMapper->insert($item); + } + } + + if($createdFeed) { + return $this->feedMapper->findByUrlHash($urlHash, $userId); + } + + return null; + } + + + /** + * Use this to mark a feed as deleted. That way it can be un-deleted + * @param int $feedId the id of the feed that should be deleted + * @param string $userId the name of the user for security reasons + * @throws ServiceNotFoundException when feed does not exist + */ + public function markDeleted($feedId, $userId) { + $feed = $this->find($feedId, $userId); + $feed->setDeletedAt($this->timeFactory->getTime()); + $this->feedMapper->update($feed); + } + + + /** + * Use this to undo a feed deletion + * @param int $feedId the id of the feed that should be restored + * @param string $userId the name of the user for security reasons + * @throws ServiceNotFoundException when feed does not exist + */ + public function unmarkDeleted($feedId, $userId) { + $feed = $this->find($feedId, $userId); + $feed->setDeletedAt(0); + $this->feedMapper->update($feed); + } + + + /** + * Deletes all deleted feeds + * @param string $userId if given it purges only feeds of that user + * @param boolean $useInterval defaults to true, if true it only purges + * entries in a given interval to give the user a chance to undo the + * deletion + */ + public function purgeDeleted($userId=null, $useInterval=true) { + $deleteOlderThan = null; + + if ($useInterval) { + $now = $this->timeFactory->getTime(); + $deleteOlderThan = $now - $this->autoPurgeMinimumInterval; + } + + $toDelete = $this->feedMapper->getToDelete($deleteOlderThan, $userId); + + foreach ($toDelete as $feed) { + $this->feedMapper->delete($feed); + } + } + + + /** + * Deletes all feeds of a user, delete items first since the user_id + * is not defined in there + * @param string $userId the name of the user + */ + public function deleteUser($userId) { + $this->feedMapper->deleteUser($userId); + } + + /** + * @param $feedId + * @param $userId + * @param $diff an array containing the fields to update, e.g.: + * [ + * 'ordering' => 1, + * 'fullTextEnabled' => true, + * 'pinned' => true, + * 'updateMode' => 0, + * 'title' => 'title' + * ] + * @throws ServiceNotFoundException if feed does not exist + */ + public function patch($feedId, $userId, $diff=[]) { + $feed = $this->find($feedId, $userId); + + foreach ($diff as $attribute => $value) { + $method = 'set' . ucfirst($attribute); + $feed->$method($value); + } + + // special feed updates + if (array_key_exists('fullTextEnabled', $diff)) { + // disable caching for the next update + $feed->setHttpEtag(''); + $feed->setHttpLastModified(0); + $this->feedMapper->update($feed); + return $this->update($feedId, $userId, true); + } + + return $this->feedMapper->update($feed); + } + +} diff --git a/lib/Service/FolderService.php b/lib/Service/FolderService.php new file mode 100644 index 000000000..e7daf836e --- /dev/null +++ b/lib/Service/FolderService.php @@ -0,0 +1,178 @@ +<?php +/** + * ownCloud - News + * + * This file is licensed under the Affero General Public License version 3 or + * later. See the COPYING file. + * + * @author Alessandro Cosentino <cosenal@gmail.com> + * @author Bernhard Posselt <dev@bernhard-posselt.com> + * @copyright Alessandro Cosentino 2012 + * @copyright Bernhard Posselt 2012, 2014 + */ + +namespace OCA\News\Service; + +use OCP\IL10N; +use OCA\News\Db\Folder; +use OCA\News\Db\FolderMapper; +use OCA\News\Config\Config; +use OCA\News\Utility\Time; + + +class FolderService extends Service { + + private $l10n; + private $timeFactory; + private $autoPurgeMinimumInterval; + private $folderMapper; + + public function __construct(FolderMapper $folderMapper, + IL10N $l10n, + Time $timeFactory, + Config $config){ + parent::__construct($folderMapper); + $this->l10n = $l10n; + $this->timeFactory = $timeFactory; + $this->autoPurgeMinimumInterval = + $config->getAutoPurgeMinimumInterval(); + $this->folderMapper = $folderMapper; + } + + /** + * Returns all folders of a user + * @param string $userId the name of the user + * @return array of folders + */ + public function findAll($userId) { + return $this->folderMapper->findAllFromUser($userId); + } + + + private function validateFolder($folderName, $userId){ + $existingFolders = + $this->folderMapper->findByName($folderName, $userId); + if(count($existingFolders) > 0){ + + throw new ServiceConflictException( + $this->l10n->t('Can not add folder: Exists already')); + } + + if(mb_strlen($folderName) === 0) { + throw new ServiceValidationException( + 'Folder name can not be empty' + ); + } + } + + + /** + * Creates a new folder + * @param string $folderName the name of the folder + * @param string $userId the name of the user for whom it should be created + * @param int $parentId the parent folder id, deprecated we don't nest + * folders + * @throws ServiceConflictException if name exists already + * @throws ServiceValidationException if the folder has invalid parameters + * @return Folder the newly created folder + */ + public function create($folderName, $userId, $parentId=0) { + $this->validateFolder($folderName, $userId); + + $folder = new Folder(); + $folder->setName($folderName); + $folder->setUserId($userId); + $folder->setParentId($parentId); + $folder->setOpened(true); + return $this->folderMapper->insert($folder); + } + + + /** + * @throws ServiceException if the folder does not exist + */ + public function open($folderId, $opened, $userId){ + $folder = $this->find($folderId, $userId); + $folder->setOpened($opened); + $this->folderMapper->update($folder); + } + + + /** + * Renames a folder + * @param int $folderId the id of the folder that should be deleted + * @param string $folderName the new name of the folder + * @param string $userId the name of the user for security reasons + * @throws ServiceConflictException if name exists already + * @throws ServiceValidationException if the folder has invalid parameters + * @throws ServiceNotFoundException if the folder does not exist + * @return Folder the updated folder + */ + public function rename($folderId, $folderName, $userId){ + $this->validateFolder($folderName, $userId); + + $folder = $this->find($folderId, $userId); + $folder->setName($folderName); + return $this->folderMapper->update($folder); + } + + + /** + * Use this to mark a folder as deleted. That way it can be un-deleted + * @param int $folderId the id of the folder that should be deleted + * @param string $userId the name of the user for security reasons + * @throws ServiceNotFoundException when folder does not exist + */ + public function markDeleted($folderId, $userId) { + $folder = $this->find($folderId, $userId); + $folder->setDeletedAt($this->timeFactory->getTime()); + $this->folderMapper->update($folder); + } + + + /** + * Use this to restore a folder + * @param int $folderId the id of the folder that should be restored + * @param string $userId the name of the user for security reasons + * @throws ServiceNotFoundException when folder does not exist + */ + public function unmarkDeleted($folderId, $userId) { + $folder = $this->find($folderId, $userId); + $folder->setDeletedAt(0); + $this->folderMapper->update($folder); + } + + + /** + * Deletes all deleted folders + * @param string $userId if given it purges only folders of that user + * @param boolean $useInterval defaults to true, if true it only purges + * entries in a given interval to give the user a chance to undo the + * deletion + */ + public function purgeDeleted($userId=null, $useInterval=true) { + $deleteOlderThan = null; + + if ($useInterval) { + $now = $this->timeFactory->getTime(); + $deleteOlderThan = $now - $this->autoPurgeMinimumInterval; + } + + $toDelete = $this->folderMapper->getToDelete($deleteOlderThan, $userId); + + foreach ($toDelete as $folder) { + $this->folderMapper->delete($folder); + } + } + + + /** + * Deletes all folders of a user + * @param string $userId the name of the user + */ + public function deleteUser($userId) { + $this->folderMapper->deleteUser($userId); + } + + +} diff --git a/lib/Service/ItemService.php b/lib/Service/ItemService.php new file mode 100644 index 000000000..8bbb54d49 --- /dev/null +++ b/lib/Service/ItemService.php @@ -0,0 +1,259 @@ +<?php +/** + * ownCloud - News + * + * This file is licensed under the Affero General Public License version 3 or + * later. See the COPYING file. + * + * @author Alessandro Cosentino <cosenal@gmail.com> + * @author Bernhard Posselt <dev@bernhard-posselt.com> + * @copyright Alessandro Cosentino 2012 + * @copyright Bernhard Posselt 2012, 2014 + */ + +namespace OCA\News\Service; + +use OCP\IConfig; +use OCP\AppFramework\Db\DoesNotExistException; + +use OCA\News\Db\ItemMapper; +use OCA\News\Db\StatusFlag; +use OCA\News\Db\FeedType; +use OCA\News\Config\Config; +use OCA\News\Utility\Time; + + +class ItemService extends Service { + + private $statusFlag; + private $config; + private $timeFactory; + private $itemMapper; + private $systemConfig; + + public function __construct(ItemMapper $itemMapper, + StatusFlag $statusFlag, + Time $timeFactory, + Config $config, + IConfig $systemConfig){ + parent::__construct($itemMapper); + $this->statusFlag = $statusFlag; + $this->config = $config; + $this->timeFactory = $timeFactory; + $this->itemMapper = $itemMapper; + $this->systemConfig = $systemConfig; + } + + + /** + * Returns all new items + * @param int $id the id of the feed, 0 for starred or all items + * @param int $type the type of the feed + * @param int $updatedSince a timestamp with the last modification date + * returns only items with a >= modified timestamp + * @param boolean $showAll if unread items should also be returned + * @param string $userId the name of the user + * @return array of items + */ + public function findAllNew($id, $type, $updatedSince, $showAll, $userId){ + $status = $this->statusFlag->typeToStatus($type, $showAll); + + switch($type){ + case FeedType::FEED: + return $this->itemMapper->findAllNewFeed( + $id, $updatedSince, $status, $userId + ); + case FeedType::FOLDER: + return $this->itemMapper->findAllNewFolder( + $id, $updatedSince, $status, $userId + ); + default: + return $this->itemMapper->findAllNew( + $updatedSince, $status, $userId + ); + } + } + + + /** + * Returns all items + * @param int $id the id of the feed, 0 for starred or all items + * @param int $type the type of the feed + * @param int $limit how many items should be returned + * @param int $offset the offset + * @param boolean $showAll if unread items should also be returned + * @param boolean $oldestFirst if it should be ordered by oldest first + * @param string $userId the name of the user + * @param string[] $search an array of keywords that the result should + * contain in either the author, title, link or body + * @return array of items + */ + public function findAll($id, $type, $limit, $offset, $showAll, $oldestFirst, + $userId, $search=[]){ + $status = $this->statusFlag->typeToStatus($type, $showAll); + + switch($type){ + case FeedType::FEED: + return $this->itemMapper->findAllFeed( + $id, $limit, $offset, $status, $oldestFirst, $userId, + $search + ); + case FeedType::FOLDER: + return $this->itemMapper->findAllFolder( + $id, $limit, $offset, $status, $oldestFirst, $userId, + $search + ); + default: + return $this->itemMapper->findAll( + $limit, $offset, $status, $oldestFirst, $userId, $search + ); + } + } + + + /** + * Star or unstar an item + * @param int $feedId the id of the item's feed that should be starred + * @param string $guidHash the guidHash of the item that should be starred + * @param boolean $isStarred if true the item will be marked as starred, + * if false unstar + * @param string $userId the name of the user for security reasons + * @throws ServiceNotFoundException if the item does not exist + */ + public function star($feedId, $guidHash, $isStarred, $userId){ + try { + $item = $this->itemMapper->findByGuidHash( + $guidHash, $feedId, $userId + ); + + if($isStarred){ + $item->setStarred(); + } else { + $item->setUnstarred(); + } + $this->itemMapper->update($item); + } catch(DoesNotExistException $ex) { + throw new ServiceNotFoundException($ex->getMessage()); + } + } + + + /** + * Read or unread an item + * @param int $itemId the id of the item that should be read + * @param boolean $isRead if true the item will be marked as read, + * if false unread + * @param string $userId the name of the user for security reasons + * @throws ServiceNotFoundException if the item does not exist + */ + public function read($itemId, $isRead, $userId){ + $lastModified = $this->timeFactory->getMicroTime(); + $this->itemMapper->readItem($itemId, $isRead, $lastModified, $userId); + } + + + /** + * Set all items read + * @param int $highestItemId all items below that are marked read. This is + * used to prevent marking items as read that the users hasn't seen yet + * @param string $userId the name of the user + */ + public function readAll($highestItemId, $userId){ + $time = $this->timeFactory->getMicroTime(); + $this->itemMapper->readAll($highestItemId, $time, $userId); + } + + + /** + * Set a folder read + * @param int $folderId the id of the folder that should be marked read + * @param int $highestItemId all items below that are marked read. This is + * used to prevent marking items as read that the users hasn't seen yet + * @param string $userId the name of the user + */ + public function readFolder($folderId, $highestItemId, $userId){ + $time = $this->timeFactory->getMicroTime(); + $this->itemMapper->readFolder( + $folderId, $highestItemId, $time, $userId + ); + } + + + /** + * Set a feed read + * @param int $feedId the id of the feed that should be marked read + * @param int $highestItemId all items below that are marked read. This is + * used to prevent marking items as read that the users hasn't seen yet + * @param string $userId the name of the user + */ + public function readFeed($feedId, $highestItemId, $userId){ + $time = $this->timeFactory->getMicroTime(); + $this->itemMapper->readFeed($feedId, $highestItemId, $time, |