Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/news.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorBernhard Posselt <dev@bernhard-posselt.com>2016-07-23 22:24:54 +0300
committerBernhard Posselt <dev@bernhard-posselt.com>2016-07-23 22:24:54 +0300
commit004fcbbcc7609ca83807f2e38967ef54f469bf72 (patch)
tree49eb99b4ea92b2045793fc567f719b31ec7f9042 /lib
parent60abc0ed4438c9b6fda245b0dc33cb483bc2aeaf (diff)
Move to new directory structure
Diffstat (limited to 'lib')
-rw-r--r--lib/AppInfo/Application.php246
-rw-r--r--lib/Command/Updater/AfterUpdate.php40
-rw-r--r--lib/Command/Updater/AllFeeds.php53
-rw-r--r--lib/Command/Updater/BeforeUpdate.php41
-rw-r--r--lib/Command/Updater/UpdateFeed.php59
-rw-r--r--lib/Config/AppConfig.php137
-rw-r--r--lib/Config/Config.php181
-rw-r--r--lib/Config/DependencyException.php28
-rw-r--r--lib/Controller/AdminController.php88
-rw-r--r--lib/Controller/ApiController.php37
-rw-r--r--lib/Controller/EntityApiSerializer.php68
-rw-r--r--lib/Controller/ExportController.php91
-rw-r--r--lib/Controller/FeedApiController.php224
-rw-r--r--lib/Controller/FeedController.php301
-rw-r--r--lib/Controller/FolderApiController.php139
-rw-r--r--lib/Controller/FolderController.php176
-rw-r--r--lib/Controller/ItemApiController.php245
-rw-r--r--lib/Controller/ItemController.php218
-rw-r--r--lib/Controller/JSONHttpError.php31
-rw-r--r--lib/Controller/PageController.php224
-rw-r--r--lib/Controller/UserApiController.php72
-rw-r--r--lib/Controller/UtilityApiController.php83
-rw-r--r--lib/Cron/Updater.php39
-rw-r--r--lib/Db/EntityJSONSerializer.php28
-rw-r--r--lib/Db/Feed.php189
-rw-r--r--lib/Db/FeedMapper.php166
-rw-r--r--lib/Db/FeedType.php24
-rw-r--r--lib/Db/Folder.php73
-rw-r--r--lib/Db/FolderMapper.php110
-rw-r--r--lib/Db/IAPI.php18
-rw-r--r--lib/Db/Item.php256
-rw-r--r--lib/Db/ItemMapper.php403
-rw-r--r--lib/Db/MapperFactory.php47
-rw-r--r--lib/Db/Mysql/ItemMapper.php88
-rw-r--r--lib/Db/NewsMapper.php93
-rw-r--r--lib/Db/StatusFlag.php47
-rw-r--r--lib/DependencyInjection/IFactory.php24
-rw-r--r--lib/Explore/RecommendedSiteNotFoundException.php20
-rw-r--r--lib/Explore/RecommendedSites.php42
-rw-r--r--lib/Explore/feeds/feeds.de.json65
-rw-r--r--lib/Explore/feeds/feeds.en.json166
-rw-r--r--lib/Fetcher/FeedFetcher.php302
-rw-r--r--lib/Fetcher/Fetcher.php69
-rw-r--r--lib/Fetcher/FetcherException.php26
-rw-r--r--lib/Fetcher/IFeedFetcher.php48
-rw-r--r--lib/Fetcher/YoutubeFetcher.php82
-rw-r--r--lib/Hooks/User.php35
-rw-r--r--lib/Http/TextDownloadResponse.php49
-rw-r--r--lib/Http/TextResponse.php46
-rw-r--r--lib/Plugin/Client/Plugin.php44
-rw-r--r--lib/Service/FeedService.php454
-rw-r--r--lib/Service/FolderService.php178
-rw-r--r--lib/Service/ItemService.php259
-rw-r--r--lib/Service/Service.php62
-rw-r--r--lib/Service/ServiceConflictException.php27
-rw-r--r--lib/Service/ServiceException.php27
-rw-r--r--lib/Service/ServiceNotFoundException.php27
-rw-r--r--lib/Service/ServiceValidationException.php27
-rw-r--r--lib/Service/StatusService.php57
-rw-r--r--lib/Upgrade/Upgrade.php71
-rw-r--r--lib/Utility/OPMLExporter.php91
-rw-r--r--lib/Utility/PicoFeedClientFactory.php40
-rw-r--r--lib/Utility/PicoFeedFaviconFactory.php38
-rw-r--r--lib/Utility/ProxyConfigParser.php65
-rw-r--r--lib/Utility/Time.php30
-rw-r--r--lib/Utility/Updater.php54
66 files changed, 6888 insertions, 0 deletions
diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php
new file mode 100644
index 000000000..5b403efac
--- /dev/null
+++ b/lib/AppInfo/Application.php
@@ -0,0 +1,246 @@
+<?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\AppInfo;
+
+use HTMLPurifier;
+use HTMLPurifier_Config;
+
+use PicoFeed\Config\Config as PicoFeedConfig;
+use PicoFeed\Reader\Reader as PicoFeedReader;
+
+use OCP\ILogger;
+use OCP\INavigationManager;
+use OCP\IURLGenerator;
+use OCP\IConfig;
+use OCP\IDBConnection;
+use OCP\AppFramework\App;
+use OCP\Files\IRootFolder;
+
+use OCA\News\Config\AppConfig;
+use OCA\News\Config\Config;
+use OCA\News\Service\FeedService;
+use OCA\News\Db\MapperFactory;
+use OCA\News\Db\ItemMapper;
+use OCA\News\Fetcher\Fetcher;
+use OCA\News\Fetcher\FeedFetcher;
+use OCA\News\Fetcher\YoutubeFetcher;
+use OCA\News\Explore\RecommendedSites;
+use OCA\News\Utility\ProxyConfigParser;
+
+
+class Application extends App {
+
+ public function __construct(array $urlParams=[]) {
+ parent::__construct('news', $urlParams);
+
+ // files
+ $this->registerFileContents('checksums', 'checksum.json');
+ $this->registerFileContents('info', '../../appinfo/info.xml');
+
+ // parameters
+ $this->registerParameter('exploreDir', '../Explore/feeds');
+ $this->registerParameter('configFile', 'config.ini');
+
+ // factories
+ $this->registerFactory(ItemMapper::class, MapperFactory::class);
+
+
+ /**
+ * App config parser
+ */
+ /** @noinspection PhpParamsInspection */
+ $this->registerService(AppConfig::class, function($c) {
+ $config = new AppConfig(
+ $c->query(INavigationManager::class),
+ $c->query(IURLGenerator::class)
+ );
+
+ $config->loadConfig($c->query('info'));
+
+ return $config;
+ });
+
+ /**
+ * Core
+ */
+ /** @noinspection PhpParamsInspection */
+ $this->registerService('LoggerParameters', function($c) {
+ return ['app' => $c->query('AppName')];
+ });
+
+ /** @noinspection PhpParamsInspection */
+ $this->registerService('databaseType', function($c) {
+ return $c->query(IConfig::class)->getSystemValue('dbtype');
+ });
+
+ /** @noinspection PhpParamsInspection */
+ $this->registerService('ConfigView', function($c) {
+ $fs = $c->query(IRootFolder::class);
+ $path = 'news/config';
+ if ($fs->nodeExists($path)) {
+ return $fs->get($path);
+ } else {
+ return $fs->newFolder($path);
+ }
+ });
+
+
+ /** @noinspection PhpParamsInspection */
+ $this->registerService(Config::class, function($c) {
+ $config = new Config(
+ $c->query('ConfigView'),
+ $c->query(ILogger::class),
+ $c->query('LoggerParameters')
+ );
+ $config->read($c->query('configFile'), true);
+ return $config;
+ });
+
+ /** @noinspection PhpParamsInspection */
+ $this->registerService(HTMLPurifier::class, function($c) {
+ $directory = $c->query(IConfig::class)
+ ->getSystemValue('datadirectory') . '/news/cache/purifier';
+
+ if(!is_dir($directory)) {
+ mkdir($directory, 0770, true);
+ }
+
+ $config = HTMLPurifier_Config::createDefault();
+ $config->set('HTML.ForbiddenAttributes', 'class');
+ $config->set('Cache.SerializerPath', $directory);
+ $config->set('HTML.SafeIframe', true);
+ $config->set('URI.SafeIframeRegexp',
+ '%^https://(?:www\.)?(' .
+ 'youtube(?:-nocookie)?.com/embed/|' .
+ 'player.vimeo.com/video/|' .
+ 'vk.com/video_ext.php)%'); //allow YouTube and Vimeo
+ $def = $config->getHTMLDefinition(true);
+ $def->addAttribute('iframe', 'allowfullscreen', 'Bool');
+ return new HTMLPurifier($config);
+ });
+
+ /**
+ * Fetchers
+ */
+ /** @noinspection PhpParamsInspection */
+ $this->registerService(PicoFeedConfig::class, function($c) {
+ // FIXME: move this into a separate class for testing?
+ $config = $c->query(Config::class);
+ $proxy = $c->query(ProxyConfigParser::class);
+
+ // use chrome's user agent string since mod_security rules
+ // assume that only browsers can send user agent strings. This
+ // can lead to blocked feed updates like joomla.org
+ // For more information see
+ // https://www.atomicorp.com/wiki/index.php/WAF_309925
+ $userAgent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36' .
+ '(KHTML, like Gecko) Chrome/50.0.2661.75 Safari/537.36';
+
+ $pico = new PicoFeedConfig();
+ $pico->setClientUserAgent($userAgent)
+ ->setClientTimeout($config->getFeedFetcherTimeout())
+ ->setMaxRedirections($config->getMaxRedirects())
+ ->setMaxBodySize($config->getMaxSize())
+ ->setParserHashAlgo('md5');
+
+ // proxy settings
+ $proxySettings = $proxy->parse();
+ $host = $proxySettings['host'];
+ $port = $proxySettings['port'];
+ $user = $proxySettings['user'];
+ $password = $proxySettings['password'];
+
+ if ($host) {
+ $pico->setProxyHostname($host);
+
+ if ($port) {
+ $pico->setProxyPort($port);
+ }
+ }
+
+ if ($user) {
+ $pico->setProxyUsername($user)
+ ->setProxyPassword($password);
+ }
+
+ return $pico;
+ });
+
+ /** @noinspection PhpParamsInspection */
+ $this->registerService(Fetcher::class, function($c) {
+ $fetcher = new Fetcher();
+
+ // register fetchers in order, the most generic fetcher should be
+ // the last one
+ $fetcher->registerFetcher($c->query(YoutubeFetcher::class));
+ $fetcher->registerFetcher($c->query(FeedFetcher::class));
+
+ return $fetcher;
+ });
+
+
+ }
+
+ /**
+ * Registers the content of a file under a key
+ * @param string $key
+ * @param string $file path relative to this file, __DIR__ will be prepended
+ */
+ private function registerFileContents($key, $file) {
+ /** @noinspection PhpParamsInspection */
+ $this->registerService($key, function () use ($file) {
+ return file_get_contents(__DIR__ . '/' . $file);
+ });
+ }
+
+ /**
+ * Shortcut for registering a service
+ * @param string $key
+ * @param closure $factory
+ * @param boolean $shared
+ */
+ private function registerService($key, $factory, $shared=true) {
+ $this->getContainer()->registerService($key, $factory, $shared);
+ }
+
+ /**
+ * Shortcut for registering a parameter
+ * @param string $key
+ * @param mixed $value
+ */
+ private function registerParameter($key, $value) {
+ $this->getContainer()->registerParameter($key, $value);
+ }
+
+ /**
+ * Register a class containing the app construction logic instead of the
+ * inlining everything in this class to enhance testability
+ * @param string $key fully qualified class name
+ * @param string $factory fully qualified factory class name
+ */
+ private function registerFactory($key, $factory) {
+ /** @noinspection PhpParamsInspection */
+ $this->registerService($key, function ($c) use ($factory) {
+ return $c->query($factory)->build();
+ });
+ }
+
+ /**
+ * Register the additional config parameters found in the info.xml
+ */
+ public function registerConfig() {
+ $this->getContainer()->query(AppConfig::class)->registerAll();
+ }
+
+}
diff --git a/lib/Command/Updater/AfterUpdate.php b/lib/Command/Updater/AfterUpdate.php
new file mode 100644
index 000000000..36e23b477
--- /dev/null
+++ b/lib/Command/Updater/AfterUpdate.php
@@ -0,0 +1,40 @@
+<?php
+/**
+ * ownCloud - News
+ *
+ * This file is licensed under the Affero General Public License version 3 or
+ * later. See the COPYING file.
+ *
+ * @author Bernhard Posselt <dev@bernhard-posselt.com>
+ * @copyright Bernhard Posselt 2016
+ */
+
+namespace OCA\News\Command\Updater;
+
+use Exception;
+
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+use \OCA\News\Utility\Updater;
+
+class AfterUpdate extends Command {
+ private $updater;
+
+ public function __construct(Updater $updater) {
+ parent::__construct();
+ $this->updater = $updater;
+ }
+
+ protected function configure() {
+ $this->setName('news:updater:after-update')
+ ->setDescription('This is used to clean up the database. It ' .
+ 'removes old read articles which are not starred');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output) {
+ $this->updater->afterUpdate();
+ }
+
+}
diff --git a/lib/Command/Updater/AllFeeds.php b/lib/Command/Updater/AllFeeds.php
new file mode 100644
index 000000000..05330ac01
--- /dev/null
+++ b/lib/Command/Updater/AllFeeds.php
@@ -0,0 +1,53 @@
+<?php
+/**
+ * ownCloud - News
+ *
+ * This file is licensed under the Affero General Public License version 3 or
+ * later. See the COPYING file.
+ *
+ * @author Bernhard Posselt <dev@bernhard-posselt.com>
+ * @copyright Bernhard Posselt 2016
+ */
+
+namespace OCA\News\Command\Updater;
+
+use Exception;
+
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+use OCA\News\Service\FeedService;
+
+
+class AllFeeds extends Command {
+ private $feedService;
+
+ public function __construct(FeedService $feedService) {
+ parent::__construct();
+ $this->feedService = $feedService;
+ }
+
+ protected function configure() {
+ $json = '{"feeds": [{"id": 39, "userId": "john"}, // etc ]}';
+
+ $this->setName('news:updater:all-feeds')
+ ->setDescription('Prints a JSON string which contains all feed ' .
+ 'ids and user ids, e.g.: ' . $json);
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output) {
+ $feeds = $this->feedService->findAllFromAllUsers();
+ $result = ['feeds' => []];
+
+ foreach ($feeds as $feed) {
+ $result['feeds'][] = [
+ 'id' => $feed->getId(),
+ 'userId' => $feed->getUserId()
+ ];
+ }
+
+ print(json_encode($result));
+ }
+
+}
diff --git a/lib/Command/Updater/BeforeUpdate.php b/lib/Command/Updater/BeforeUpdate.php
new file mode 100644
index 000000000..6af0a5c3a
--- /dev/null
+++ b/lib/Command/Updater/BeforeUpdate.php
@@ -0,0 +1,41 @@
+<?php
+/**
+ * ownCloud - News
+ *
+ * This file is licensed under the Affero General Public License version 3 or
+ * later. See the COPYING file.
+ *
+ * @author Bernhard Posselt <dev@bernhard-posselt.com>
+ * @copyright Bernhard Posselt 2016
+ */
+
+namespace OCA\News\Command\Updater;
+
+use Exception;
+
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+use \OCA\News\Utility\Updater;
+
+class BeforeUpdate extends Command {
+ private $updater;
+
+ public function __construct(Updater $updater) {
+ parent::__construct();
+ $this->updater = $updater;
+ }
+
+ protected function configure() {
+ $this->setName('news:updater:before-update')
+ ->setDescription('This is used to clean up the database. It ' .
+ 'deletes folders and feeds that are marked for ' .
+ 'deletion');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output) {
+ $this->updater->beforeUpdate();
+ }
+
+}
diff --git a/lib/Command/Updater/UpdateFeed.php b/lib/Command/Updater/UpdateFeed.php
new file mode 100644
index 000000000..13fc2e625
--- /dev/null
+++ b/lib/Command/Updater/UpdateFeed.php
@@ -0,0 +1,59 @@
+<?php
+/**
+ * ownCloud - News
+ *
+ * This file is licensed under the Affero General Public License version 3 or
+ * later. See the COPYING file.
+ *
+ * @author Bernhard Posselt <dev@bernhard-posselt.com>
+ * @copyright Bernhard Posselt 2016
+ */
+
+namespace OCA\News\Command\Updater;
+
+use Exception;
+
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+use OCA\News\Service\FeedService;
+
+
+class UpdateFeed extends Command {
+ private $feedService;
+
+ public function __construct(FeedService $feedService) {
+ parent::__construct();
+ $this->feedService = $feedService;
+ }
+
+ protected function configure() {
+ $this->setName('news:updater:update-feed')
+ ->addArgument(
+ 'feed-id',
+ InputArgument::REQUIRED,
+ 'feed id, integer'
+ )
+ ->addArgument(
+ 'user-id',
+ InputArgument::REQUIRED,
+ 'user id of a user, string'
+ )
+ ->setDescription('Console API for updating a single user\'s feed');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output) {
+ $feedId = $input->getArgument('feed-id');
+ $userId = $input->getArgument('user-id');
+ try {
+ $this->feedService->update($feedId, $userId);
+ } catch (Exception $e) {
+ $output->writeln('<error>Could not update feed with id ' . $feedId .
+ ' and user ' . $userId . ': ' . $e->getMessage() .
+ '</error> ');
+ }
+ }
+
+}
diff --git a/lib/Config/AppConfig.php b/lib/Config/AppConfig.php
new file mode 100644
index 000000000..55dcd6d1a
--- /dev/null
+++ b/lib/Config/AppConfig.php
@@ -0,0 +1,137 @@
+<?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\Config;
+
+use SimpleXMLElement;
+
+use OCP\INavigationManager;
+use OCP\IURLGenerator;
+use OCP\Util;
+use OCP\App;
+
+// Used to parse app.json file, should be in core at some point
+class AppConfig {
+
+ private $config;
+ private $navigationManager;
+ private $urlGenerator;
+
+ /**
+ * TODO: External deps that are needed:
+ * - add jobs
+ * - connect to hooks
+ */
+ public function __construct(INavigationManager $navigationManager,
+ IURLGenerator $urlGenerator) {
+ $this->navigationManager = $navigationManager;
+ $this->urlGenerator = $urlGenerator;
+ $this->config = [];
+ }
+
+
+ /**
+ * Parse an xml config
+ */
+ private function parseConfig($string) {
+ // no need to worry about XXE since local file
+ $xml = simplexml_load_string($string, 'SimpleXMLElement');
+ return json_decode(json_encode((array)$xml), true);
+ }
+
+
+ /**
+ * @param string|array $data path to the config file or an array with the
+ * config
+ */
+ public function loadConfig($data) {
+ if(is_array($data)) {
+ $this->config = $data;
+ } else {
+ $this->config = $this->parseConfig($data);
+ }
+ }
+
+
+ /**
+ * @param string $key if given returns the value of the config at index $key
+ * @return array|mixed the config
+ */
+ public function getConfig($key=null) {
+ // FIXME: is this function interface a good idea?
+ if($key !== null) {
+ return $this->config[$key];
+ } else {
+ return $this->config;
+ }
+ }
+
+
+ /**
+ * Registers all config options
+ */
+ public function registerAll() {
+ $this->registerNavigation();
+ $this->registerHooks();
+ // IJob API is fucked up, so silence the code checker
+ $class = '\OCP\BackgroundJob';
+ $class::addRegularTask($this->config['cron']['job'], 'run');
+ App::registerAdmin($this->config['id'], $this->config['admin']);
+ }
+
+
+ /**
+ * Parses the navigation and creates a navigation entry if needed
+ */
+ public function registerNavigation() {
+ if (array_key_exists('navigation', $this->config)) {
+ $this->navigationManager->add(function () {
+ $nav =& $this->config['navigation'];
+
+ $navConfig = [
+ 'id' => $this->config['id'],
+ 'order' => $nav['order'],
+ 'name' => $nav['name']
+ ];
+
+ $navConfig['href'] = $this->urlGenerator->linkToRoute(
+ $nav['route']
+ );
+ $navConfig['icon'] = $this->urlGenerator->imagePath(
+ $this->config['id'], $nav['icon']
+ );
+
+ return $navConfig;
+ });
+ }
+ }
+
+
+ /**
+ * Registers all hooks in the config
+ */
+ public function registerHooks() {
+ // FIXME: this is temporarily static because core emitters are not
+ // future proof, therefore legacy code in here
+ foreach ($this->config['hooks'] as $hook) {
+ $listener = explode('::', $hook['channel']);
+ $reaction = explode('::', $hook['subscriber']);
+
+ // config is written like HookNamespace::method => Class::method
+ Util::connectHook($listener[0], $listener[1], $reaction[0],
+ $reaction[1]);
+ }
+ }
+
+
+}
diff --git a/lib/Config/Config.php b/lib/Config/Config.php
new file mode 100644
index 000000000..a91c5053f
--- /dev/null
+++ b/lib/Config/Config.php
@@ -0,0 +1,181 @@
+<?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\Config;
+
+use OCP\ILogger;
+use OCP\Files\Folder;
+
+
+class Config {
+
+ private $fileSystem;
+ private $autoPurgeMinimumInterval; // seconds, used to define how
+ // long deleted folders and feeds
+ // should still be kept for an
+ // undo actions
+ private $autoPurgeCount; // number of allowed unread articles per feed
+ private $maxRedirects; // seconds
+ private $feedFetcherTimeout; // seconds
+ private $useCronUpdates; // turn off updates run by owncloud cronjob
+ private $logger;
+ private $loggerParams;
+ private $maxSize;
+ private $exploreUrl;
+
+ public function __construct(Folder $fileSystem,
+ ILogger $logger,
+ $LoggerParameters) {
+ $this->fileSystem = $fileSystem;
+ $this->autoPurgeMinimumInterval = 60;
+ $this->autoPurgeCount = 200;
+ $this->maxRedirects = 10;
+ $this->maxSize = 100*1024*1024; // 100Mb
+ $this->feedFetcherTimeout = 60;
+ $this->useCronUpdates = true;
+ $this->logger = $logger;
+ $this->exploreUrl = '';
+ $this->loggerParams = $LoggerParameters;
+ }
+
+ public function getAutoPurgeMinimumInterval() {
+ if ($this->autoPurgeMinimumInterval > 60) {
+ return $this->autoPurgeMinimumInterval;
+ } else {
+ return 60;
+ }
+ }
+
+ public function getAutoPurgeCount() {
+ return $this->autoPurgeCount;
+ }
+
+
+ public function getMaxRedirects() {
+ return $this->maxRedirects;
+ }
+
+
+ public function getFeedFetcherTimeout() {
+ return $this->feedFetcherTimeout;
+ }
+
+
+ public function getUseCronUpdates() {
+ return $this->useCronUpdates;
+ }
+
+
+ public function getMaxSize() {
+ return $this->maxSize;
+ }
+
+
+ public function getExploreUrl() {
+ return $this->exploreUrl;
+ }
+
+
+ public function setAutoPurgeMinimumInterval($value) {
+ $this->autoPurgeMinimumInterval = $value;
+ }
+
+
+ public function setAutoPurgeCount($value) {
+ $this->autoPurgeCount = $value;
+ }
+
+
+ public function setMaxRedirects($value) {
+ $this->maxRedirects = $value;
+ }
+
+
+ public function setFeedFetcherTimeout($value) {
+ $this->feedFetcherTimeout = $value;
+ }
+
+
+ public function setUseCronUpdates($value) {
+ $this->useCronUpdates = $value;
+ }
+
+ public function setMaxSize($value) {
+ $this->maxSize = $value;
+ }
+
+
+ public function setExploreUrl($value) {
+ $this->exploreUrl = $value;
+ }
+
+
+ public function read($configPath, $createIfNotExists=false) {
+ if($createIfNotExists && !$this->fileSystem->nodeExists($configPath)) {
+ $this->fileSystem->newFile($configPath);
+ $this->write($configPath);
+
+ } else {
+
+ $content = $this->fileSystem->get($configPath)->getContent();
+ $configValues = parse_ini_string($content);
+
+ if($configValues === false || count($configValues) === 0) {
+ $this->logger->warning(
+ 'Configuration invalid. Ignoring values.',
+ $this->loggerParams
+ );
+ } else {
+
+ foreach($configValues as $key => $value) {
+ if(property_exists($this, $key)) {
+ $type = gettype($this->$key);
+ settype($value, $type);
+ $this->$key = $value;
+ } else {
+ $this->logger->warning(
+ 'Configuration value "' . $key .
+ '" does not exist. Ignored value.' ,
+ $this->loggerParams
+ );
+ }
+ }
+
+ }
+ }
+ }
+
+
+ public function write($configPath) {
+ $ini =
+ 'autoPurgeMinimumInterval = ' .
+ $this->autoPurgeMinimumInterval . "\n" .
+ 'autoPurgeCount = ' .
+ $this->autoPurgeCount . "\n" .
+ 'maxRedirects = ' .
+ $this->maxRedirects . "\n" .
+ 'maxSize = ' .
+ $this->maxSize . "\n" .
+ 'exploreUrl = ' .
+ $this->exploreUrl . "\n" .
+ 'feedFetcherTimeout = ' .
+ $this->feedFetcherTimeout . "\n" .
+ 'useCronUpdates = ' .
+ var_export($this->useCronUpdates, true);
+ ;
+
+ $this->fileSystem->get($configPath)->putContent($ini);
+ }
+
+
+}
diff --git a/lib/Config/DependencyException.php b/lib/Config/DependencyException.php
new file mode 100644
index 000000000..690d187c3
--- /dev/null
+++ b/lib/Config/DependencyException.php
@@ -0,0 +1,28 @@
+<?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\Config;
+
+class DependencyException extends \Exception {
+
+
+ /**
+ * Constructor
+ * @param string $msg the error message
+ */
+ public function __construct($msg){
+ parent::__construct($msg);
+ }
+
+
+} \ No newline at end of file
diff --git a/lib/Controller/AdminController.php b/lib/Controller/AdminController.php
new file mode 100644
index 000000000..a673566de
--- /dev/null
+++ b/lib/Controller/AdminController.php
@@ -0,0 +1,88 @@
+<?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\Controller;
+
+use OCP\AppFramework\Http\TemplateResponse;
+use OCP\IRequest;
+use OCP\AppFramework\Controller;
+
+use OCA\News\Config\Config;
+use OCA\News\Service\itemService;
+
+class AdminController extends Controller {
+
+ private $config;
+ private $configPath;
+ private $itemService;
+
+ public function __construct($AppName, IRequest $request, Config $config,
+ ItemService $itemService, $configFile){
+ parent::__construct($AppName, $request);
+ $this->config = $config;
+ $this->configPath = $configFile;
+ $this->itemService = $itemService;
+ }
+
+ // There are no checks for the index method since the output is rendered
+ // in admin/admin.php
+ public function index() {
+ $data = [
+ 'autoPurgeMinimumInterval' =>
+ $this->config->getAutoPurgeMinimumInterval(),
+ 'autoPurgeCount' => $this->config->getAutoPurgeCount(),
+ 'maxRedirects' => $this->config->getMaxRedirects(),
+ 'feedFetcherTimeout' => $this->config->getFeedFetcherTimeout(),
+ 'useCronUpdates' => $this->config->getUseCronUpdates(),
+ 'maxSize' => $this->config->getMaxSize(),
+ 'exploreUrl' => $this->config->getExploreUrl(),
+ ];
+ return new TemplateResponse($this->appName, 'admin', $data, 'blank');
+ }
+
+
+ /**
+ * @param int $autoPurgeMinimumInterval
+ * @param int $autoPurgeCount
+ * @param int $maxRedirects
+ * @param int $feedFetcherTimeout
+ * @param int $maxSize
+ * @param bool $useCronUpdates
+ * @param string $exploreUrl
+ * @return array with the updated values
+ */
+ public function update($autoPurgeMinimumInterval, $autoPurgeCount,
+ $maxRedirects, $feedFetcherTimeout, $maxSize,
+ $useCronUpdates, $exploreUrl) {
+ $this->config->setAutoPurgeMinimumInterval($autoPurgeMinimumInterval);
+ $this->config->setAutoPurgeCount($autoPurgeCount);
+ $this->config->setMaxRedirects($maxRedirects);
+ $this->config->setMaxSize($maxSize);
+ $this->config->setFeedFetcherTimeout($feedFetcherTimeout);
+ $this->config->setUseCronUpdates($useCronUpdates);
+ $this->config->setExploreUrl($exploreUrl);
+ $this->config->write($this->configPath);
+
+ return [
+ 'autoPurgeMinimumInterval' =>
+ $this->config->getAutoPurgeMinimumInterval(),
+ 'autoPurgeCount' => $this->config->getAutoPurgeCount(),
+ 'maxRedirects' => $this->config->getMaxRedirects(),
+ 'maxSize' => $this->config->getMaxSize(),
+ 'feedFetcherTimeout' => $this->config->getFeedFetcherTimeout(),
+ 'useCronUpdates' => $this->config->getUseCronUpdates(),
+ 'exploreUrl' => $this->config->getExploreUrl(),
+ ];
+ }
+
+}
diff --git a/lib/Controller/ApiController.php b/lib/Controller/ApiController.php
new file mode 100644
index 000000000..f3b77b379
--- /dev/null
+++ b/lib/Controller/ApiController.php
@@ -0,0 +1,37 @@
+<?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\Controller;
+
+use OCP\IRequest;
+use OCP\AppFramework\ApiController as BaseApiController;
+
+class ApiController extends BaseApiController {
+
+ public function __construct($appName,
+ IRequest $request){
+ parent::__construct($appName, $request);
+ }
+
+ /**
+ * @PublicPage
+ * @NoCSRFRequired
+ * @CORS
+ */
+ public function index() {
+ return [
+ 'apiLevels' => ['v1-2']
+ ];
+ }
+
+}
diff --git a/lib/Controller/EntityApiSerializer.php b/lib/Controller/EntityApiSerializer.php
new file mode 100644
index 000000000..073ad5c39
--- /dev/null
+++ b/lib/Controller/EntityApiSerializer.php
@@ -0,0 +1,68 @@
+<?php
+/**
+ * ownCloud - News
+ *
+ * This file is licensed under the Affero General Public License version 3 or
+ * later. See the COPYING file.
+ *
+ * @author Bernhard Posselt <dev@bernhard-posselt.com>
+ * @copyright Bernhard Posselt 2014
+ */
+
+namespace OCA\News\Controller;
+
+use \OCA\News\Db\IAPI;
+
+
+class EntityApiSerializer {
+
+ private $level;
+
+ public function __construct($level) {
+ $this->level = $level;
+ }
+
+
+ /**
+ * Call toAPI() method on all entities. Works on
+ *
+ * @param mixed $data :
+ * * Entity
+ * * Entity[]
+ * * array('level' => Entity[])
+ * * Response
+ * @return array|mixed
+ */
+ public function serialize($data) {
+
+ if($data instanceof IAPI) {
+ return [$this->level => [$data->toAPI()]];
+ }
+
+ if(is_array($data) && array_key_exists($this->level, $data)) {
+ $data[$this->level] = $this->convert($data[$this->level]);
+ } elseif(is_array($data)) {
+ $data = [$this->level => $this->convert($data)];
+ }
+
+ return $data;
+ }
+
+
+ private function convert($entities) {
+ $converted = [];
+
+ foreach($entities as $entity) {
+ if($entity instanceof IAPI) {
+ $converted[] = $entity->toAPI();
+
+ // break if it contains anything else than entities
+ } else {
+ return $entities;
+ }
+ }
+
+ return $converted;
+ }
+
+} \ No newline at end of file
diff --git a/lib/Controller/ExportController.php b/lib/Controller/ExportController.php
new file mode 100644
index 000000000..bc9fc0ff1
--- /dev/null
+++ b/lib/Controller/ExportController.php
@@ -0,0 +1,91 @@
+<?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\Controller;
+
+use \OCP\IRequest;
+use \OCP\AppFramework\Controller;
+use \OCP\AppFramework\Http;
+use \OCP\AppFramework\Http\JSONResponse;
+
+use \OCA\News\Http\TextDownloadResponse;
+use \OCA\News\Service\FolderService;
+use \OCA\News\Service\FeedService;
+use \OCA\News\Service\ItemService;
+use \OCA\News\Utility\OPMLExporter;
+
+class ExportController extends Controller {
+
+ private $opmlExporter;
+ private $folderService;
+ private $feedService;
+ private $itemService;
+ private $userId;
+
+ public function __construct($AppName,
+ IRequest $request,
+ FolderService $folderService,
+ FeedService $feedService,
+ ItemService $itemService,
+ OPMLExporter $opmlExporter,
+ $UserId){
+ parent::__construct($AppName, $request);
+ $this->feedService = $feedService;
+ $this->folderService = $folderService;
+ $this->opmlExporter = $opmlExporter;
+ $this->itemService = $itemService;
+ $this->userId = $UserId;
+ }
+
+
+ /**
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ */
+ public function opml(){
+ $feeds = $this->feedService->findAll($this->userId);
+ $folders = $this->folderService->findAll($this->userId);
+ $opml = $this->opmlExporter->build($folders, $feeds)->saveXML();
+ $name = 'subscriptions.opml';
+ $mimeType = 'text/xml';
+ return new TextDownloadResponse($opml, $name, $mimeType);
+ }
+
+
+ /**
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ */
+ public function articles(){
+ $feeds = $this->feedService->findAll($this->userId);
+ $items = $this->itemService->getUnreadOrStarred($this->userId);
+
+ // build assoc array for fast access
+ $feedsDict = [];
+ foreach($feeds as $feed) {
+ $feedsDict['feed' . $feed->getId()] = $feed;
+ }
+
+ $articles = [];
+ foreach($items as $item) {
+ $articles[] = $item->toExport($feedsDict);
+ }
+
+ $response = new JSONResponse($articles);
+ $response->addHeader('Content-Disposition',
+ 'attachment; filename="articles.json"');
+ return $response;
+ }
+
+
+} \ No newline at end of file
diff --git a/lib/Controller/FeedApiController.php b/lib/Controller/FeedApiController.php
new file mode 100644
index 000000000..9713db8cf
--- /dev/null
+++ b/lib/Controller/FeedApiController.php
@@ -0,0 +1,224 @@
+<?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\Controller;
+
+use \OCP\IRequest;
+use \OCP\ILogger;
+use \OCP\AppFramework\ApiController;
+use \OCP\AppFramework\Http;
+
+use \OCA\News\Service\FeedService;
+use \OCA\News\Service\ItemService;
+use \OCA\News\Service\ServiceNotFoundException;
+use \OCA\News\Service\ServiceConflictException;
+
+
+class FeedApiController extends ApiController {
+
+ use JSONHttpError;
+
+ private $itemService;
+ private $feedService;
+ private $userId;
+ private $logger;
+ private $loggerParams;
+ private $serializer;
+
+ public function __construct($AppName,
+ IRequest $request,
+ FeedService $feedService,
+ ItemService $itemService,
+ ILogger $logger,
+ $UserId,
+ $LoggerParameters){
+ parent::__construct($AppName, $request);
+ $this->feedService = $feedService;
+ $this->itemService = $itemService;
+ $this->userId = $UserId;
+ $this->logger = $logger;
+ $this->loggerParams = $LoggerParameters;
+ $this->serializer = new EntityApiSerializer('feeds');
+ }
+
+
+ /**
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ * @CORS
+ */
+ public function index() {
+
+ $result = [
+ 'starredCount' => $this->itemService->starredCount($this->userId),
+ 'feeds' => $this->feedService->findAll($this->userId)
+ ];
+
+
+ try {
+ $result['newestItemId'] =
+ $this->itemService->getNewestItemId($this->userId);
+
+ // in case there are no items, ignore
+ } catch(ServiceNotFoundException $ex) {}
+
+ return $this->serializer->serialize($result);
+ }
+
+
+ /**
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ * @CORS
+ *
+ * @param string $url
+ * @param int $folderId
+ * @return array|mixed|\OCP\AppFramework\Http\JSONResponse
+ */
+ public function create($url, $folderId=0) {
+ try {
+ $this->feedService->purgeDeleted($this->userId, false);
+
+ $feed = $this->feedService->create($url, $folderId, $this->userId);
+ $result = ['feeds' => [$feed]];
+
+ try {
+ $result['newestItemId'] =
+ $this->itemService->getNewestItemId($this->userId);
+
+ // in case there are no items, ignore
+ } catch(ServiceNotFoundException $ex) {}
+
+ return $this->serializer->serialize($result);
+
+ } catch(ServiceConflictException $ex) {
+ return $this->error($ex, Http::STATUS_CONFLICT);
+ } catch(ServiceNotFoundException $ex) {
+ return $this->error($ex, Http::STATUS_NOT_FOUND);
+ }
+ }
+
+
+ /**
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ * @CORS
+ *
+ * @param int $feedId
+ * @return array|\OCP\AppFramework\Http\JSONResponse
+ */
+ public function delete($feedId) {
+ try {
+ $this->feedService->delete($feedId, $this->userId);
+ } catch(ServiceNotFoundException $ex) {
+ return $this->error($ex, Http::STATUS_NOT_FOUND);
+ }
+
+ return [];
+ }
+
+
+ /**
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ * @CORS
+ *
+ * @param int $feedId
+ * @param int $newestItemId
+ */
+ public function read($feedId, $newestItemId) {
+ $this->itemService->readFeed($feedId, $newestItemId, $this->userId);
+ }
+
+
+ /**
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ * @CORS
+ *
+ * @param int $feedId
+ * @param int $folderId
+ * @return array|\OCP\AppFramework\Http\JSONResponse
+ */
+ public function move($feedId, $folderId) {
+ try {
+ $this->feedService->patch(
+ $feedId, $this->userId, ['folderId' => $folderId]
+ );
+ } catch(ServiceNotFoundException $ex) {
+ return $this->error($ex, Http::STATUS_NOT_FOUND);
+ }
+
+ return [];
+ }
+
+
+ /**
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ * @CORS
+ *
+ * @param int $feedId
+ * @param string $feedTitle
+ * @return array|\OCP\AppFramework\Http\JSONResponse
+ */
+ public function rename($feedId, $feedTitle) {
+ try {
+ $this->feedService->patch(
+ $feedId, $this->userId, ['title' => $feedTitle]
+ );
+ } catch(ServiceNotFoundException $ex) {
+ return $this->error($ex, Http::STATUS_NOT_FOUND);
+ }
+
+ return [];
+ }
+
+
+ /**
+ * @NoCSRFRequired
+ * @CORS
+ */
+ public function fromAllUsers() {
+ $feeds = $this->feedService->findAllFromAllUsers();
+ $result = ['feeds' => []];
+
+ foreach ($feeds as $feed) {
+ $result['feeds'][] = [
+ 'id' => $feed->getId(),
+ 'userId' => $feed->getUserId()
+ ];
+ }
+
+ return $result;
+ }
+
+
+ /**
+ * @NoCSRFRequired
+ *
+ * @param string $userId
+ * @param int $feedId
+ */
+ public function update($userId, $feedId) {
+ try {
+ $this->feedService->update($feedId, $userId);
+ // ignore update failure
+ } catch(\Exception $ex) {
+ $this->logger->debug('Could not update feed ' . $ex->getMessage(),
+ $this->loggerParams);
+ }
+ }
+
+
+}
diff --git a/lib/Controller/FeedController.php b/lib/Controller/FeedController.php
new file mode 100644
index 000000000..2b95794a3
--- /dev/null
+++ b/lib/Controller/FeedController.php
@@ -0,0 +1,301 @@
+<?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\Controller;
+
+use OCP\IRequest;
+use OCP\IConfig;
+use OCP\AppFramework\Controller;
+use OCP\AppFramework\Http;
+
+use OCA\News\Service\ItemService;
+use OCA\News\Service\FeedService;
+use OCA\News\Service\FolderService;
+use OCA\News\Service\ServiceNotFoundException;
+use OCA\News\Service\ServiceConflictException;
+use OCA\News\Db\FeedType;
+
+
+class FeedController extends Controller {
+
+ use JSONHttpError;
+
+ private $feedService;
+ private $folderService;
+ private $itemService;
+ private $userId;
+ private $settings;
+
+ public function __construct($AppName,
+ IRequest $request,
+ FolderService $folderService,
+ FeedService $feedService,
+ ItemService $itemService,
+ IConfig $settings,
+ $UserId){
+ parent::__construct($AppName, $request);
+ $this->feedService = $feedService;
+ $this->folderService = $folderService;
+ $this->itemService = $itemService;
+ $this->userId = $UserId;
+ $this->settings = $settings;
+ }
+
+
+ /**
+ * @NoAdminRequired
+ */
+ public function index(){
+
+ // this method is also used to update the interface
+ // because of this we also pass the starred count and the newest
+ // item id which will be used for marking feeds read
+ $params = [
+ 'feeds' => $this->feedService->findAll($this->userId),
+ 'starred' => $this->itemService->starredCount($this->userId)
+ ];
+
+ try {
+ $params['newestItemId'] =
+ $this->itemService->getNewestItemId($this->userId);
+
+ // An exception occurs if there is a newest item. If there is none,
+ // simply ignore it and do not add the newestItemId
+ } catch (ServiceNotFoundException $ex) {}
+
+ return $params;
+ }
+
+
+ /**
+ * @NoAdminRequired
+ */
+ public function active(){
+ $feedId = (int) $this->settings->getUserValue($this->userId,
+ $this->appName,'lastViewedFeedId');
+ $feedType = $this->settings->getUserValue($this->userId, $this->appName,
+ 'lastViewedFeedType');
+
+ // cast from null to int is 0
+ if($feedType !== null){
+ $feedType = (int) $feedType;
+ }
+
+ // check if feed or folder exists
+ try {
+ if($feedType === FeedType::FOLDER){
+ $this->folderService->find($feedId, $this->userId);
+
+ } elseif ($feedType === FeedType::FEED){
+ $this->feedService->find($feedId, $this->userId);
+
+ // if its the first launch, those values will be null
+ } elseif($feedType === null){
+ throw new ServiceNotFoundException('');
+ }
+
+ } catch (ServiceNotFoundException $ex){
+ $feedId = 0;
+ $feedType = FeedType::SUBSCRIPTIONS;
+ }
+
+ return [
+ 'activeFeed' => [
+ 'id' => $feedId,
+ 'type' => $feedType
+ ]
+ ];
+ }
+
+
+ /**
+ * @NoAdminRequired
+ *
+ * @param string $url
+ * @param int $parentFolderId
+ * @param string $title
+ * @param string $user
+ * @param string $password
+ * @return array|\OCP\AppFramework\Http\JSONResponse
+ */
+ public function create($url, $parentFolderId, $title=null,
+ $user=null, $password=null){
+ try {
+ // we need to purge deleted feeds if a feed is created to
+ // prevent already exists exceptions
+ $this->feedService->purgeDeleted($this->userId, false);
+
+ $feed = $this->feedService->create($url, $parentFolderId,
+ $this->userId, $title,
+ $user, $password);
+ $params = ['feeds' => [$feed]];
+
+ try {
+ $params['newestItemId'] =
+ $this->itemService->getNewestItemId($this->userId);
+
+ // An exception occurs if there is a newest item. If there is none,
+ // simply ignore it and do not add the newestItemId
+ } catch (ServiceNotFoundException $ex) {}
+
+ return $params;
+
+ } catch(ServiceConflictException $ex) {
+ return $this->error($ex, Http::STATUS_CONFLICT);
+ } catch(ServiceNotFoundException $ex) {
+ return $this->error($ex, Http::STATUS_UNPROCESSABLE_ENTITY);
+ }
+
+ }
+
+
+ /**
+ * @NoAdminRequired
+ *
+ * @param int $feedId
+ * @return array|\OCP\AppFramework\Http\JSONResponse
+ */
+ public function delete($feedId){
+ try {
+ $this->feedService->markDeleted($feedId, $this->userId);
+ } catch(ServiceNotFoundException $ex) {
+ return $this->error($ex, Http::STATUS_NOT_FOUND);
+ }
+
+ return [];
+ }
+
+
+ /**
+ * @NoAdminRequired
+ *
+ * @param int $feedId
+ * @return array|\OCP\AppFramework\Http\JSONResponse
+ */
+ public function update($feedId){
+ try {
+ $feed = $this->feedService->update($feedId, $this->userId);
+
+ return [
+ 'feeds' => [
+ // only pass unread count to not accidentally readd
+ // the feed again
+ [
+ 'id' => $feed->getId(),
+ 'unreadCount' => $feed->getUnreadCount()
+ ]
+ ]
+ ];
+
+ } catch(ServiceNotFoundException $ex) {
+ return $this->error($ex, Http::STATUS_NOT_FOUND);
+ }
+
+ }
+
+
+ /**
+ * @NoAdminRequired
+ *
+ * @param array $json
+ * @return array
+ */
+ public function import($json) {
+ $feed = $this->feedService->importArticles($json, $this->userId);
+
+ $params = [
+ 'starred' => $this->itemService->starredCount($this->userId)
+ ];
+
+ if($feed) {
+ $params['feeds'] = [$feed];
+ }
+
+ return $params;
+ }
+
+
+ /**
+ * @NoAdminRequired
+ *
+ * @param int $feedId
+ * @param int $highestItemId
+ * @return array
+ */
+ public function read($feedId, $highestItemId){
+ $this->itemService->readFeed($feedId, $highestItemId, $this->userId);
+
+ return [
+ 'feeds' => [
+ [
+ 'id' => $feedId,
+ 'unreadCount' => 0
+ ]
+ ]
+ ];
+ }
+
+
+ /**
+ * @NoAdminRequired
+ *
+ * @param int $feedId
+ * @return array|\OCP\AppFramework\Http\JSONResponse
+ */
+ public function restore($feedId){
+ try {
+ $this->feedService->unmarkDeleted($feedId, $this->userId);
+ } catch(ServiceNotFoundException $ex) {
+ return $this->error($ex, Http::STATUS_NOT_FOUND);
+ }
+
+ return [];
+ }
+
+ /**
+ * @NoAdminRequired
+ *
+ * @param int $feedId
+ * @param bool $pinned
+ * @param bool $fullTextEnabled
+ * @param int $updateMode
+ * @param int $ordering
+ * @param int $folderId
+ * @param string $title
+ */
+ public function patch($feedId, $pinned=null, $fullTextEnabled=null,
+ $updateMode=null, $ordering=null, $title=null,
+ $folderId=null) {
+ $attributes = [
+ 'pinned' => $pinned,
+ 'fullTextEnabled' => $fullTextEnabled,
+ 'updateMode' => $updateMode,
+ 'ordering' => $ordering,
+ 'title' => $title,
+ 'folderId' => $folderId
+ ];
+
+ $diff = array_filter($attributes, function ($value) {
+ return $value !== null;
+ });
+
+ try {
+ $this->feedService->patch($feedId, $this->userId, $diff);
+ } catch(ServiceNotFoundException $ex) {
+ return $this->error($ex, Http::STATUS_NOT_FOUND);
+ }
+
+ return [];
+ }
+
+}
diff --git a/lib/Controller/FolderApiController.php b/lib/Controller/FolderApiController.php
new file mode 100644
index 000000000..53693e84f
--- /dev/null
+++ b/lib/Controller/FolderApiController.php
@@ -0,0 +1,139 @@
+<?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\Controller;
+
+use \OCP\IRequest;
+use \OCP\AppFramework\ApiController;
+use \OCP\AppFramework\Http;
+
+use \OCA\News\Service\FolderService;
+use \OCA\News\Service\ItemService;
+use \OCA\News\Service\ServiceNotFoundException;
+use \OCA\News\Service\ServiceConflictException;
+use \OCA\News\Service\ServiceValidationException;
+
+
+class FolderApiController extends ApiController {
+
+ use JSONHttpError;
+
+ private $folderService;
+ private $itemService;
+ private $userId;
+ private $serializer;
+
+ public function __construct($AppName,
+ IRequest $request,
+ FolderService $folderService,
+ ItemService $itemService,
+ $UserId){
+ parent::__construct($AppName, $request);
+ $this->folderService = $folderService;
+ $this->itemService = $itemService;
+ $this->userId = $UserId;
+ $this->serializer = new EntityApiSerializer('folders');
+ }
+
+
+ /**
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ * @CORS
+ */
+ public function index() {
+ return $this->serializer->serialize(
+ $this->folderService->findAll($this->userId)
+ );
+ }
+
+
+ /**
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ * @CORS
+ *
+ * @param string $name
+ * @return array|mixed|\OCP\AppFramework\Http\JSONResponse
+ */
+ public function create($name) {
+ try {
+ $this->folderService->purgeDeleted($this->userId, false);
+ return $this->serializer->serialize(
+ $this->folderService->create($name, $this->userId)
+ );
+ } catch(ServiceValidationException $ex) {
+ return $this->error($ex, Http::STATUS_UNPROCESSABLE_ENTITY);
+ } catch(ServiceConflictException $ex) {
+ return $this->error($ex, Http::STATUS_CONFLICT);
+ }
+ }
+
+
+ /**
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ * @CORS
+ *
+ * @param int $folderId
+ * @return array|\OCP\AppFramework\Http\JSONResponse
+ */
+ public function delete($folderId) {
+ try {
+ $this->folderService->delete($folderId, $this->userId);
+ } catch(ServiceNotFoundException $ex) {
+ return $this->error($ex, Http::STATUS_NOT_FOUND);
+ }
+
+ return [];
+ }
+
+
+ /**
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ * @CORS
+ * @param int $folderId
+ * @param string $name
+ * @return array|\OCP\AppFramework\Http\JSONResponse
+ */
+ public function update($folderId, $name) {
+ try {
+ $this->folderService->rename($folderId, $name, $this->userId);
+
+ } catch(ServiceValidationException $ex) {
+ return $this->error($ex, Http::STATUS_UNPROCESSABLE_ENTITY);
+ } catch(ServiceConflictException $ex) {
+ return $this->error($ex, Http::STATUS_CONFLICT);
+ } catch(ServiceNotFoundException $ex) {
+ return $this->error($ex, Http::STATUS_NOT_FOUND);
+ }
+
+ return [];
+ }
+
+
+ /**
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ * @CORS
+ *
+ * @param int $folderId
+ * @param int $newestItemId
+ */
+ public function read($folderId, $newestItemId) {
+ $this->itemService->readFolder($folderId, $newestItemId, $this->userId);
+ }
+
+
+}
diff --git a/lib/Controller/FolderController.php b/lib/Controller/FolderController.php
new file mode 100644
index 000000000..e8c0b0e6c
--- /dev/null
+++ b/lib/Controller/FolderController.php
@@ -0,0 +1,176 @@
+<?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\Controller;
+
+use \OCP\IRequest;
+use \OCP\AppFramework\Controller;
+use \OCP\AppFramework\Http;
+
+use \OCA\News\Service\FolderService;
+use \OCA\News\Service\FeedService;
+use \OCA\News\Service\ItemService;
+use \OCA\News\Service\ServiceNotFoundException;
+use \OCA\News\Service\ServiceConflictException;
+use \OCA\News\Service\ServiceValidationException;
+
+
+class FolderController extends Controller {
+
+ use JSONHttpError;
+
+ private $folderService;
+ private $feedService;
+ private $itemService;
+ private $userId;
+
+ public function __construct($AppName,
+ IRequest $request,
+ FolderService $folderService,
+ FeedService $feedService,
+ ItemService $itemService,
+ $UserId) {
+ parent::__construct($AppName, $request);
+ $this->folderService = $folderService;
+ $this->feedService = $feedService;
+ $this->itemService = $itemService;
+ $this->userId = $UserId;
+ }
+
+
+ /**
+ * @NoAdminRequired
+ */
+ public function index() {
+ $folders = $this->folderService->findAll($this->userId);
+ return ['folders' => $folders];
+ }
+
+
+ /**
+ * @NoAdminRequired
+ *
+ * @param int $folderId
+ * @param bool $open
+ * @return array|\OCP\AppFramework\Http\JSONResponse
+ */
+ public function open($folderId, $open) {
+ try {
+ $this->folderService->open($folderId, $open, $this->userId);
+ } catch(ServiceNotFoundException $ex) {
+ return $this->error($ex, Http::STATUS_NOT_FOUND);
+ }
+
+ return [];
+ }
+
+
+ /**
+ * @NoAdminRequired
+ *
+ * @param string $folderName
+ * @return array|\OCP\AppFramework\Http\JSONResponse
+ */
+ public function create($folderName) {
+ try {
+ // we need to purge deleted folders if a folder is created to
+ // prevent already exists exceptions
+ $this->folderService->purgeDeleted($this->userId, false);
+ $folder = $this->folderService->create($folderName, $this->userId);
+
+ return ['folders' => [$folder]];
+
+ } catch(ServiceConflictException $ex) {
+ return $this->error($ex, Http::STATUS_CONFLICT);
+ } catch(ServiceValidationException $ex) {
+ return $this->error($ex, Http::STATUS_UNPROCESSABLE_ENTITY);
+ }
+
+ }
+
+
+ /**
+ * @NoAdminRequired
+ *
+ * @param int $folderId
+ * @return array|\OCP\AppFramework\Http\JSONResponse
+ */
+ public function delete($folderId) {
+ try {
+ $this->folderService->markDeleted($folderId, $this->userId);
+ } catch (ServiceNotFoundException $ex){
+ return $this->error($ex, Http::STATUS_NOT_FOUND);
+ }
+
+ return [];
+ }
+
+
+ /**
+ * @NoAdminRequired
+ *
+ * @param string $folderName
+ * @param int $folderId
+ * @return array|\OCP\AppFramework\Http\JSONResponse
+ */
+ public function rename($folderName, $folderId) {
+ try {
+ $folder = $this->folderService->rename($folderId, $folderName,
+ $this->userId);
+
+ return ['folders' => [$folder]];
+
+ } catch(ServiceConflictException $ex) {
+ return $this->error($ex, Http::STATUS_CONFLICT);
+ } catch(ServiceValidationException $ex) {
+ return $this->error($ex, Http::STATUS_UNPROCESSABLE_ENTITY);
+ } catch (ServiceNotFoundException $ex){
+ return $this->error($ex, Http::STATUS_NOT_FOUND);
+ }
+
+ }
+
+ /**
+ * @NoAdminRequired
+ *
+ * @param int $folderId
+ * @param int $highestItemId
+ * @return array
+ */
+ public function read($folderId, $highestItemId) {
+ $this->itemService->readFolder(
+ $folderId, $highestItemId, $this->userId
+ );
+
+ return ['feeds' => $this->feedService->findAll($this->userId)];
+ }
+
+
+ /**
+ * @NoAdminRequired
+ *
+ * @param int $folderId
+ * @return array|\OCP\AppFramework\Http\JSONResponse
+ */
+ public function restore($folderId) {
+ try {
+ $this->folderService->unmarkDeleted($folderId, $this->userId);
+ } catch (ServiceNotFoundException $ex){
+ return $this->error($ex, Http::STATUS_NOT_FOUND);
+ }
+
+ return [];
+ }
+
+
+} \ No newline at end of file
diff --git a/lib/Controller/ItemApiController.php b/lib/Controller/ItemApiController.php
new file mode 100644
index 000000000..ec0baabfe
--- /dev/null
+++ b/lib/Controller/ItemApiController.php
@@ -0,0 +1,245 @@
+<?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\Controller;
+
+use \OCP\IRequest;
+use \OCP\AppFramework\ApiController;
+use \OCP\AppFramework\Http;
+
+use \OCA\News\Service\ItemService;
+use \OCA\News\Service\ServiceNotFoundException;
+
+class ItemApiController extends ApiController {
+
+ use JSONHttpError;
+
+ private $itemService;
+ private $userId;
+ private $serializer;
+
+ public function __construct($AppName,
+ IRequest $request,
+ ItemService $itemService,
+ $UserId){
+ parent::__construct($AppName, $request);
+ $this->itemService = $itemService;
+ $this->userId = $UserId;
+ $this->serializer = new EntityApiSerializer('items');
+ }
+
+
+ /**
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ * @CORS
+ *
+ * @param int $type
+ * @param int $id
+ * @param bool $getRead
+ * @param int $batchSize
+ * @param int $offset
+ * @param bool $oldestFirst
+ * @return array|mixed
+ */
+ public function index($type=3, $id=0, $getRead=true, $batchSize=-1,
+ $offset=0, $oldestFirst=false) {
+ return $this->serializer->serialize(
+ $this->itemService->findAll(
+ $id, $type, $batchSize, $offset, $getRead, $oldestFirst,
+ $this->userId
+ )
+ );
+ }
+
+
+ /**
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ * @CORS
+ *
+ * @param int $type
+ * @param int $id
+ * @param int $lastModified
+ * @return array|mixed
+ */
+ public function updated($type=3, $id=0, $lastModified=0) {
+ return $this->serializer->serialize(
+ $this->itemService->findAllNew($id, $type, $lastModified,
+ true, $this->userId)
+ );
+ }
+
+
+ private function setRead($isRead, $itemId) {
+ try {
+ $this->itemService->read($itemId, $isRead, $this->userId);
+ } catch(ServiceNotFoundException $ex){
+ return $this->error($ex, Http::STATUS_NOT_FOUND);
+ }
+
+ return [];
+ }
+
+
+ /**
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ * @CORS
+ *
+ * @param int $itemId
+ * @return array|\OCP\AppFramework\Http\JSONResponse
+ */
+ public function read($itemId) {
+ return $this->setRead(true, $itemId);
+ }
+
+
+ /**
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ * @CORS
+ *
+ * @param int $itemId
+ * @return array|\OCP\AppFramework\Http\JSONResponse
+ */
+ public function unread($itemId) {
+ return $this->setRead(false, $itemId);
+ }
+
+
+ private function setStarred($isStarred, $feedId, $guidHash) {
+ try {
+ $this->itemService->star(
+ $feedId, $guidHash, $isStarred, $this->userId
+ );
+ } catch(ServiceNotFoundException $ex){
+ return $this->error($ex, Http::STATUS_NOT_FOUND);
+ }
+
+ return [];
+ }
+
+
+ /**
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ * @CORS
+ *
+ * @param int $feedId
+ * @param string $guidHash
+ * @return array|\OCP\AppFramework\Http\JSONResponse
+ */
+ public function star($feedId, $guidHash) {
+ return $this->setStarred(true, $feedId, $guidHash);
+ }
+
+
+ /**
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ * @CORS
+ *
+ * @param int $feedId
+ * @param string $guidHash
+ * @return array|\OCP\AppFramework\Http\JSONResponse
+ */
+ public function unstar($feedId, $guidHash) {
+ return $this->setStarred(false, $feedId, $guidHash);
+ }
+
+
+ /**
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ * @CORS
+ *
+ * @param int $newestItemId
+ */
+ public function readAll($newestItemId) {
+ $this->itemService->readAll($newestItemId, $this->userId);
+ }
+
+
+ private function setMultipleRead($isRead, $items) {
+ foreach($items as $id) {
+ try {
+ $this->itemService->read($id, $isRead, $this->userId);
+ } catch(ServiceNotFoundException $ex) {
+ continue;
+ }
+ }
+ }
+
+
+ /**
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ * @CORS
+ *
+ * @param int[] item ids
+ */
+ public function readMultiple($items) {
+ $this->setMultipleRead(true, $items);
+ }
+
+
+ /**
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ * @CORS
+ *
+ * @param int[] item ids
+ */
+ public function unreadMultiple($items) {
+ $this->setMultipleRead(false, $items);
+ }
+
+
+ private function setMultipleStarred($isStarred, $items) {
+ foreach($items as $item) {
+ try {
+ $this->itemService->star($item['feedId'], $item['guidHash'],
+ $isStarred, $this->userId);
+ } catch(ServiceNotFoundException $ex) {
+ continue;
+ }
+ }
+ }
+
+
+ /**
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ * @CORS
+ *
+ * @param int[] item ids
+ */
+ public function starMultiple($items) {
+ $this->setMultipleStarred(true, $items);
+ }
+
+
+ /**
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ * @CORS
+ *
+ * @param int[] item ids
+ */
+ public function unstarMultiple($items) {
+ $this->setMultipleStarred(false, $items);
+ }
+
+
+}
diff --git a/lib/Controller/ItemController.php b/lib/Controller/ItemController.php
new file mode 100644
index 000000000..ac838f1bf
--- /dev/null
+++ b/lib/Controller/ItemController.php
@@ -0,0 +1,218 @@
+<?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\Controller;
+
+use \OCP\IRequest;
+use \OCP\IConfig;
+use \OCP\AppFramework\Controller;
+use \OCP\AppFramework\Http;
+
+use \OCA\News\Service\ServiceException;
+use \OCA\News\Service\ServiceNotFoundException;
+use \OCA\News\Service\ItemService;
+use \OCA\News\Service\FeedService;
+
+
+class ItemController extends Controller {
+
+ use JSONHttpError;
+
+ private $itemService;
+ private $feedService;
+ private $userId;
+ private $settings;
+
+ public function __construct($AppName,
+ IRequest $request,
+ FeedService $feedService,
+ ItemService $itemService,
+ IConfig $settings,
+ $UserId){
+ parent::__construct($AppName, $request);
+ $this->itemService = $itemService;
+ $this->feedService = $feedService;
+ $this->userId = $UserId;
+ $this->settings = $settings;
+ }
+
+
+ /**
+ * @NoAdminRequired
+ *
+ * @param int $type
+ * @param int $id
+ * @param int $limit
+ * @param int $offset
+ * @param bool $showAll
+ * @param bool $oldestFirst
+ * @param string $search
+ * @return array
+ */
+ public function index($type=3, $id=0, $limit=50, $offset=0, $showAll=null,
+ $oldestFirst=null, $search='') {
+
+ // in case this is called directly and not from the website use the
+ // internal state
+ if ($showAll === null) {
+ $showAll = $this->settings->getUserValue(
+ $this->userId, $this->appName,'showAll'
+ ) === '1';
+ }
+
+ if ($oldestFirst === null) {
+ $oldestFirst = $this->settings->getUserValue(
+ $this->userId, $this->appName, 'oldestFirst'
+ ) === '1';
+ }
+
+ $this->settings->setUserValue($this->userId, $this->appName,
+ 'lastViewedFeedId', $id);
+ $this->settings->setUserValue($this->userId, $this->appName,
+ 'lastViewedFeedType', $type);
+
+ $params = [];
+
+ // 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);
+ }
+
+ try {
+
+ // the offset is 0 if the user clicks on a new feed
+ // 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->userId);
+ $params['feeds'] = $this->feedService->findAll($this->userId);
+ $params['starred'] =
+ $this->itemService->starredCount($this->userId);
+ }
+
+ $params['items'] = $this->itemService->findAll(
+ $id, $type, $limit, $offset, $showAll, $oldestFirst,
+ $this->userId, $search
+ );
+
+ // this gets thrown if there are no items
+ // in that case just return an empty array
+ } catch(ServiceException $ex) {}
+
+ return $params;
+ }
+
+
+ /**
+ * @NoAdminRequired
+ *
+ * @param int $type
+ * @param int $id
+ * @param int $lastModified
+ * @return array
+ */
+ public function newItems($type, $id, $lastModified=0) {
+ $showAll = $this->settings->getUserValue($this->userId, $this->appName,
+ 'showAll') === '1';
+
+ $params = [];
+
+ try {
+ $params['newestItemId'] =
+ $this->itemService->getNewestItemId($this->userId);
+ $params['feeds'] = $this->feedService->findAll($this->userId);
+ $params['starred'] =
+ $this->itemService->starredCount($this->userId);
+ $params['items'] = $this->itemService->findAllNew($id, $type,
+ $lastModified, $showAll, $this->userId);
+
+ // this gets thrown if there are no items
+ // in that case just return an empty array
+ } catch(ServiceException $ex) {}
+
+ return $params;
+ }
+
+
+ /**
+ * @NoAdminRequired
+ *
+ * @param int $feedId
+ * @param string $guidHash
+ * @param bool $isStarred
+ * @return array|\OCP\AppFramework\Http\JSONResponse
+ */
+ public function star($feedId, $guidHash, $isStarred){
+ try {
+ $this->itemService->star($feedId, $guidHash, $isStarred,
+ $this->userId);
+ } catch(ServiceException $ex) {
+ return $this->error($ex, Http::STATUS_NOT_FOUND);
+ }
+
+ return [];
+ }
+
+
+ /**
+ * @NoAdminRequired
+ *
+ * @param int $itemId
+ * @param bool $isRead
+ * @return array|\OCP\AppFramework\Http\JSONResponse
+ */
+ public function read($itemId, $isRead=true){
+ try {
+ $this->itemService->read($itemId, $isRead, $this->userId);
+ } catch(ServiceException $ex) {
+ return $this->error($ex, Http::STATUS_NOT_FOUND);
+ }
+
+ return [];
+ }
+
+
+ /**
+ * @NoAdminRequired
+ *
+ * @param int $highestItemId
+ * @return array
+ */
+ public function readAll($highestItemId){
+ $this->itemService->readAll($highestItemId, $this->userId);
+ return ['feeds' => $this->feedService->findAll($this->userId)];
+ }
+
+
+ /**
+ * @NoAdminRequired
+ *
+ * @param int[] item ids
+ */
+ public function readMultiple($itemIds) {
+ foreach($itemIds as $id) {
+ try {
+ $this->itemService->read($id, true, $this->userId);
+ } catch(ServiceNotFoundException $ex) {
+ continue;
+ }
+ }
+ }
+
+
+}
diff --git a/lib/Controller/JSONHttpError.php b/lib/Controller/JSONHttpError.php
new file mode 100644
index 000000000..722019ae1
--- /dev/null
+++ b/lib/Controller/JSONHttpError.php
@@ -0,0 +1,31 @@
+<?php
+/**
+ * ownCloud - News
+ *
+ * This file is licensed under the Affero General Public License version 3 or
+ * later. See the COPYING file.
+ *
+ * @author Bernhard Posselt <dev@bernhard-posselt.com>
+ * @copyright Bernhard Posselt 2014
+ */
+
+namespace OCA\News\Controller;
+
+use \OCP\AppFramework\Http\JSONResponse;
+
+
+trait JSONHttpError {
+
+
+ /**
+ * @param \Exception $exception the message that is returned taken from the
+ * exception
+ * @param int $code the http error code
+ * @return \OCP\AppFramework\Http\JSONResponse
+ */
+ public function error(\Exception $exception, $code) {
+ return new JSONResponse(['message' => $exception->getMessage()], $code);
+ }
+
+
+} \ No newline at end of file
diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php
new file mode 100644
index 000000000..558a85e70
--- /dev/null
+++ b/lib/Controller/PageController.php
@@ -0,0 +1,224 @@
+<?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\Controller;
+
+use OCP\IRequest;
+use OCP\IConfig;
+use OCP\IL10N;
+use OCP\IURLGenerator;
+use OCP\AppFramework\Controller;
+use OCP\AppFramework\Http\TemplateResponse;
+use OCP\AppFramework\Http\JSONResponse;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\ContentSecurityPolicy;
+
+use OCA\News\Service\StatusService;
+use OCA\News\Config\AppConfig;
+use OCA\News\Config\Config;
+use OCA\News\Explore\RecommendedSites;
+use OCA\News\Explore\RecommendedSiteNotFoundException;
+use OCA\News\Db\FeedType;
+
+class PageController extends Controller {
+
+ private $settings;
+ private $l10n;
+ private $userId;
+ private $appConfig;
+ private $urlGenerator;
+ private $config;
+ private $recommendedSites;
+ private $statusService;
+
+ use JSONHttpError;
+
+ public function __construct($AppName,
+ IRequest $request,
+ IConfig $settings,
+ IURLGenerator $urlGenerator,
+ AppConfig $appConfig,
+ Config $config,
+ IL10N $l10n,
+ RecommendedSites $recommendedSites,
+ StatusService $statusService,
+ $UserId){
+ parent::__construct($AppName, $request);
+ $this->settings = $settings;
+ $this->urlGenerator = $urlGenerator;
+ $this->appConfig = $appConfig;
+ $this->l10n = $l10n;
+ $this->userId = $UserId;
+ $this->config = $config;
+ $this->recommendedSites = $recommendedSites;
+ $this->statusService = $statusService;
+ }
+
+
+ /**
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ */
+ public function index() {
+ $status = $this->statusService->getStatus();
+ $response = new TemplateResponse($this->appName, 'index', [
+ 'cronWarning' => $status['warnings']['improperlyConfiguredCron'],
+ 'url_generator' => $this->urlGenerator
+ ]);
+
+ $csp = new ContentSecurityPolicy();
+ $csp->addAllowedImageDomain('*')
+ ->addAllowedMediaDomain('*')
+ ->addAllowedConnectDomain('*') // chrome breaks on audio elements
+ ->addAllowedFrameDomain('https://youtube.com')
+ ->addAllowedFrameDomain('https://www.youtube.com')
+ ->addAllowedFrameDomain('https://player.vimeo.com')
+ ->addAllowedFrameDomain('https://www.player.vimeo.com')
+ ->addAllowedFrameDomain('https://vk.com')
+ ->addAllowedFrameDomain('https://www.vk.com');
+ $response->setContentSecurityPolicy($csp);
+
+ return $response;
+ }
+
+
+ /**
+ * @NoAdminRequired
+ */
+ public function settings() {
+ $settings = [
+ 'showAll',
+ 'compact',
+ 'preventReadOnScroll',
+ 'oldestFirst',
+ 'compactExpand'
+ ];
+
+ $exploreUrl = $this->config->getExploreUrl();
+ if (trim($exploreUrl) === '') {
+ // default url should not feature the sites.en.json
+ $exploreUrl = $this->urlGenerator->linkToRoute(
+ 'news.page.explore', ['lang' => 'en']
+ );
+ $exploreUrl = preg_replace('/feeds\.en\.json$/', '', $exploreUrl);
+ }
+
+ $result = [
+ 'language' => $this->l10n->getLanguageCode(),
+ 'exploreUrl' => $exploreUrl
+ ];
+
+ foreach ($settings as $setting) {
+ $result[$setting] = $this->settings->getUserValue(
+ $this->userId, $this->appName, $setting
+ ) === '1';
+ }
+ return ['settings' => $result];
+ }
+
+
+ /**
+ * @NoAdminRequired
+ *
+ * @param bool $showAll
+ * @param bool $compact
+ * @param bool $preventReadOnScroll
+ * @param bool $oldestFirst
+ */
+ public function updateSettings($showAll, $compact, $preventReadOnScroll,
+ $oldestFirst, $compactExpand) {
+ $settings = ['showAll',
+ 'compact',
+ 'preventReadOnScroll',
+ 'oldestFirst',
+ 'compactExpand'
+ ];
+
+ foreach ($settings as $setting) {
+ if (${$setting}) {
+ $value = '1';
+ } else {
+ $value = '0';
+ }
+ $this->settings->setUserValue($this->userId, $this->appName,
+ $setting, $value);
+ }
+ }
+
+
+ /**
+ * @NoCSRFRequired
+ * @PublicPage
+ *
+ * Generates a web app manifest, according to specs in:
+ * https://developer.mozilla.org/en-US/Apps/Build/Manifest
+ */
+ public function manifest() {
+ $config = $this->appConfig->getConfig();
+
+ // size of the icons: 128x128 is required by FxOS for all app manifests
+ $iconSizes = ['128', '512'];
+ $icons = [];
+
+ $locale = str_replace('_', '-', $this->l10n->getLanguageCode());
+
+ foreach ($iconSizes as $size) {
+ $filename = 'app-' . $size . '.png';
+ if (file_exists(__DIR__ . '/../../img/' . $filename)) {
+ $icons[$size] = $this->urlGenerator->imagePath($config['id'],
+ $filename);
+ }
+ }
+
+
+ $data = [
+ "name" => $config['name'],
+ "type" => 'web',
+ "default_locale" => $locale,
+ "description" => $config['description'],
+ "launch_path" => $this->urlGenerator->linkToRoute(
+ $config['navigation']['route']),
+ "icons" => $icons,
+ "developer" => [
+ "name" => $config['author'],
+ "url" => $config['homepage']
+ ]
+ ];
+
+ $response = new JSONResponse($data);
+ $response->addHeader('Content-Type',
+ 'application/x-web-app-manifest+json');
+
+ return $response;
+ }
+
+ /**
+ * @NoAdminRequired
+ *
+ * @param string $lang
+ */
+ public function explore($lang) {
+ $this->settings->setUserValue($this->userId, $this->appName,
+ 'lastViewedFeedId', 0);
+ $this->settings->setUserValue($this->userId, $this->appName,
+ 'lastViewedFeedType', FeedType::EXPLORE);
+
+ try {
+ return $this->recommendedSites->forLanguage($lang);
+ } catch (RecommendedSiteNotFoundException $ex) {
+ return $this->error($ex, Http::STATUS_NOT_FOUND);
+ }
+ }
+
+
+}
diff --git a/lib/Controller/UserApiController.php b/lib/Controller/UserApiController.php
new file mode 100644
index 000000000..8db2b6937
--- /dev/null
+++ b/lib/Controller/UserApiController.php
@@ -0,0 +1,72 @@
+<?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\Controller;
+
+use \OCP\IRequest;
+use \OCP\IUserSession;
+use \OCP\IURLGenerator;
+use \OCP\Files\IRootFolder;
+use \OCP\AppFramework\ApiController;
+use \OCP\AppFramework\Http;
+
+class UserApiController extends ApiController {
+
+ private $userSession;
+ private $rootFolder;
+
+ public function __construct($AppName,
+ IRequest $request,
+ IUserSession $userSession,
+ IRootFolder $rootFolder){
+ parent::__construct($AppName, $request);
+ $this->userSession = $userSession;
+ $this->rootFolder = $rootFolder;
+ }
+
+ /**
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ * @CORS
+ */
+ public function index() {
+ $user = $this->userSession->getUser();
+
+ // find the avatar
+ $jpgAvatar = '/' . $user->getUID() . '/avatar.jpg';
+ $pngAvatar = '/' . $user->getUID() . '/avatar.png';
+ $avatar = null;
+
+ if ($this->rootFolder->nodeExists($jpgAvatar)) {
+ $file = $this->rootFolder->get($jpgAvatar);
+ $avatar = [
+ 'data' => base64_encode($file->getContent()),
+ 'mime' => 'image/jpeg'
+ ];
+ } elseif ($this->rootFolder->nodeExists($pngAvatar)) {
+ $file = $this->rootFolder->get($pngAvatar);
+ $avatar = [
+ 'data' => base64_encode($file->getContent()),
+ 'mime' => 'image/png'
+ ];
+ }
+
+ return [
+ 'userId' => $user->getUID(),
+ 'displayName' => $user->getDisplayName(),
+ 'lastLoginTimestamp' => $user->getLastLogin(),
+ 'avatar' => $avatar
+ ];
+ }
+
+}
diff --git a/lib/Controller/UtilityApiController.php b/lib/Controller/UtilityApiController.php
new file mode 100644
index 000000000..e613e70a5
--- /dev/null
+++ b/lib/Controller/UtilityApiController.php
@@ -0,0 +1,83 @@
+<?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\Controller;
+
+use \OCP\IRequest;
+use \OCP\IConfig;
+use \OCP\AppFramework\ApiController;
+use \OCP\AppFramework\Http;
+
+use \OCA\News\Utility\Updater;
+use \OCA\News\Service\StatusService;
+
+
+class UtilityApiController extends ApiController {
+
+ private $updater;
+ private $settings;
+ private $statusService;
+
+ public function __construct($AppName,
+ IRequest $request,
+ Updater $updater,
+ IConfig $settings,
+ StatusService $statusService){
+ parent::__construct($AppName, $request);
+ $this->updater = $updater;
+ $this->settings = $settings;
+ $this->statusService = $statusService;
+ }
+
+
+ /**
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ * @CORS
+ */
+ public function version() {
+ $version = $this->settings->getAppValue($this->appName,
+ 'installed_version');
+ return ['version' => $version];
+ }
+
+
+ /**
+ * @NoCSRFRequired
+ * @CORS
+ */
+ public function beforeUpdate() {
+ $this->updater->beforeUpdate();
+ }
+
+
+ /**
+ * @NoCSRFRequired
+ * @CORS
+ */
+ public function afterUpdate() {
+ $this->updater->afterUpdate();
+ }
+
+
+ /**
+ * @CORS
+ * @NoCSRFRequired
+ * @NoAdminRequired
+ */
+ public function status() {
+ return $this->statusService->getStatus();
+ }
+
+
+}
diff --git a/lib/Cron/Updater.php b/lib/Cron/Updater.php
new file mode 100644
index 000000000..46790d793
--- /dev/null
+++ b/lib/Cron/Updater.php
@@ -0,0 +1,39 @@
+<?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\Cron;
+
+use OCA\News\AppInfo\Application;
+use OCA\News\Config\Config;
+use OCA\News\Service\StatusService;
+use OCA\News\Utility\Updater as UpdaterService;
+
+class Updater {
+
+ public static function run() {
+ $app = new Application();
+
+ $container = $app->getContainer();
+
+ // make it possible to turn off cron updates if you use an external
+ // script to execute updates in parallel
+ $useCronUpdates = $container->query(Config::class)->getUseCronUpdates();
+ $isProperlyConfigured = $container->query(StatusService::class)->isProperlyConfigured();
+ if ($useCronUpdates && $isProperlyConfigured) {
+ $container->query(UpdaterService::class)->update();
+ $container->query(UpdaterService::class)->beforeUpdate();
+ $container->query(UpdaterService::class)->afterUpdate();
+ }
+ }
+
+}
diff --git a/lib/Db/EntityJSONSerializer.php b/lib/Db/EntityJSONSerializer.php
new file mode 100644
index 000000000..c0d946452
--- /dev/null
+++ b/lib/Db/EntityJSONSerializer.php
@@ -0,0 +1,28 @@
+<?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\Db;
+
+trait EntityJSONSerializer {
+
+
+ public function serializeFields($properties) {
+ $result = [];
+ foreach($properties as $property) {
+ $result[$property] = $this->$property;
+ }
+ return $result;
+ }
+
+
+} \ No newline at end of file
diff --git a/lib/Db/Feed.php b/lib/Db/Feed.php
new file mode 100644
index 000000000..62d49c01b
--- /dev/null
+++ b/lib/Db/Feed.php
@@ -0,0 +1,189 @@
+<?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\Db;
+
+use \OCP\AppFramework\Db\Entity;
+
+/**
+ * @method integer getId()
+ * @method void setId(integer $value)
+ * @method string getUserId()
+ * @method void setUserId(string $value)
+ * @method int getOrdering()
+ * @method void setOrdering(integer $value)
+ * @method string getUrlHash()
+ * @method void setUrlHash(string $value)
+ * @method string getLocation()
+ * @method void setLocation(string $value)
+ * @method string getUrl()
+ * @method string getTitle()
+ * @method void setTitle(string $value)
+ * @method string getLastModified()
+ * @method void setLastModified(integer $value)
+ * @method string getHttpLastModified()
+ * @method void setHttpLastModified(string $value)
+ * @method string getHttpEtag()
+ * @method void setHttpEtag(string $value)
+ * @method string getFaviconLink()
+ * @method void setFaviconLink(string $value)
+ * @method integer getAdded()
+ * @method void setAdded(integer $value)
+ * @method boolean getPinned()
+ * @method void setPinned(boolean $value)
+ * @method integer getFolderId()
+ * @method void setFolderId(integer $value)
+ * @method integer getFullTextEnabled()
+ * @method void setFullTextEnabled(bool $value)
+ * @method integer getUnreadCount()
+ * @method void setUnreadCount(integer $value)
+ * @method string getLink()
+ * @method boolean getPreventUpdate()
+ * @method void setPreventUpdate(boolean $value)
+ * @method integer getDeletedAt()
+ * @method void setDeletedAt(integer $value)
+ * @method integer getArticlesPerUpdate()
+ * @method void setArticlesPerUpdate(integer $value)
+ * @method integer getUpdateErrorCount()
+ * @method void setUpdateErrorCount(integer $value)
+ * @method string getLastUpdateError()
+ * @method void setLastUpdateError(string $value)
+ * @method string getBasicAuthUser()
+ * @method void setBasicAuthUser(string $value)
+ * @method string getBasicAuthPassword()
+ * @method void setBasicAuthPassword(string $value)
+ */
+class Feed extends Entity implements IAPI, \JsonSerializable {
+
+ use EntityJSONSerializer;
+
+ protected $userId;
+ protected $urlHash;
+ protected $url;
+ protected $title;
+ protected $faviconLink;
+ protected $added;
+ protected $folderId;
+ protected $unreadCount;
+ protected $link;
+ protected $preventUpdate;
+ protected $deletedAt;
+ protected $articlesPerUpdate;
+ protected $httpLastModified;
+ protected $lastModified;
+ protected $httpEtag;
+ protected $location;
+ protected $ordering;
+ protected $fullTextEnabled;
+ protected $pinned;
+ protected $updateMode;
+ protected $updateErrorCount;
+ protected $lastUpdateError;
+ protected $basicAuthUser;
+ protected $basicAuthPassword;
+
+ public function __construct(){
+ $this->addType('parentId', 'integer');
+ $this->addType('added', 'integer');
+ $this->addType('folderId', 'integer');
+ $this->addType('unreadCount', 'integer');
+ $this->addType('preventUpdate', 'boolean');
+ $this->addType('pinned', 'boolean');
+ $this->addType('deletedAt', 'integer');
+ $this->addType('articlesPerUpdate', 'integer');
+ $this->addType('ordering', 'integer');
+ $this->addType('fullTextEnabled', 'boolean');
+ $this->addType('updateMode', 'integer');
+ $this->addType('updateErrorCount', 'integer');
+ $this->addType('lastModified', 'integer');
+ }
+
+
+ /**
+ * Turns entitie attributes into an array
+ */
+ public function jsonSerialize() {
+ $serialized = $this->serializeFields([
+ 'id',
+ 'userId',
+ 'urlHash',
+ 'url',
+ 'title',
+ 'faviconLink',
+ 'added',
+ 'folderId',
+ 'unreadCount',
+ 'link',
+ 'preventUpdate',
+ 'deletedAt',
+ 'articlesPerUpdate',
+ 'location',
+ 'ordering',
+ 'fullTextEnabled',
+ 'pinned',
+ 'updateMode',
+ 'updateErrorCount',
+ 'lastUpdateError',
+ 'basicAuthUser',
+ 'basicAuthPassword'
+ ]);
+
+ $url = parse_url($this->link)['host'];
+
+ // strip leading www. to avoid css class confusion
+ if (strpos($url, 'www.') === 0) {
+ $url = substr($url, 4);
+ }
+
+ $serialized['cssClass'] = 'custom-' . str_replace('.', '-', $url);
+
+ return $serialized;
+ }
+
+
+ public function toAPI() {
+ return $this->serializeFields([
+ 'id',
+ 'url',
+ 'title',
+ 'faviconLink',
+ 'added',
+ 'folderId',
+ 'unreadCount',
+ 'ordering',
+ 'link',
+ 'pinned',
+ 'updateErrorCount',
+ 'lastUpdateError'
+ ]);
+ }
+
+
+ public function setUrl($url) {
+ $url = trim($url);
+ if(strpos($url, 'http') === 0) {
+ parent::setUrl($url);
+ $this->setUrlHash(md5($url));
+ }
+ }
+
+
+ public function setLink($url) {
+ $url = trim($url);
+ if(strpos($url, 'http') === 0) {
+ parent::setLink($url);
+ }
+ }
+
+
+}
diff --git a/lib/Db/FeedMapper.php b/lib/Db/FeedMapper.php
new file mode 100644
index 000000000..80d75e723
--- /dev/null
+++ b/lib/Db/FeedMapper.php
@@ -0,0 +1,166 @@
+<?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\Db;
+
+use OCA\News\Utility\Time;
+use OCP\IDBConnection;
+use OCP\AppFramework\Db\Entity;
+
+
+class FeedMapper extends NewsMapper {
+
+
+ public function __construct(IDBConnection $db, Time $time) {
+ parent::__construct($db, 'news_feeds', Feed::class, $time);
+ }
+
+
+ public function find($id, $userId){
+ $sql = 'SELECT `feeds`.*, COUNT(`items`.`id`) AS `unread_count` ' .
+ 'FROM `*PREFIX*news_feeds` `feeds` ' .
+ 'LEFT JOIN `*PREFIX*news_items` `items` ' .
+ 'ON `feeds`.`id` = `items`.`feed_id` ' .
+ // WARNING: this is a desperate attempt at making this query
+ // work because prepared statements dont work. This is a
+ // POSSIBLE SQL INJECTION RISK WHEN MODIFIED WITHOUT THOUGHT.
+ // think twice when changing this
+ 'AND (`items`.`status` & ' . StatusFlag::UNREAD . ') = ' .
+ StatusFlag::UNREAD . ' ' .
+ 'WHERE `feeds`.`id` = ? ' .
+ 'AND `feeds`.`user_id` = ? ' .
+ 'GROUP BY `feeds`.`id`';
+ $params = [$id, $userId];
+
+ return $this->findEntity($sql, $params);
+ }
+
+
+ public function findAllFromUser($userId){
+ $sql = 'SELECT `feeds`.*, COUNT(`items`.`id`) AS `unread_count` ' .
+ 'FROM `*PREFIX*news_feeds` `feeds` ' .
+ 'LEFT OUTER JOIN `*PREFIX*news_folders` `folders` '.
+ 'ON `feeds`.`folder_id` = `folders`.`id` ' .
+ 'LEFT JOIN `*PREFIX*news_items` `items` ' .
+ 'ON `feeds`.`id` = `items`.`feed_id` ' .
+ // WARNING: this is a desperate attempt at making this query
+ // work because prepared statements dont work. This is a
+ // POSSIBLE SQL INJECTION RISK WHEN MODIFIED WITHOUT THOUGHT.
+ // think twice when changing this
+ 'AND (`items`.`status` & ' . StatusFlag::UNREAD . ') = ' .
+ StatusFlag::UNREAD . ' ' .
+ 'WHERE `feeds`.`user_id` = ? ' .
+ 'AND (`feeds`.`folder_id` = 0 ' .
+ 'OR `folders`.`deleted_at` = 0' .
+ ')' .
+ 'AND `feeds`.`deleted_at` = 0 ' .
+ 'GROUP BY `feeds`.`id`';
+ $params = [$userId];
+
+ return $this->findEntities($sql, $params);
+ }
+
+
+ public function findAll(){
+ $sql = 'SELECT `feeds`.*, COUNT(`items`.`id`) AS `unread_count` ' .
+ 'FROM `*PREFIX*news_feeds` `feeds` ' .
+ 'LEFT OUTER JOIN `*PREFIX*news_folders` `folders` '.
+ 'ON `feeds`.`folder_id` = `folders`.`id` ' .
+ 'LEFT JOIN `*PREFIX*news_items` `items` ' .
+ 'ON `feeds`.`id` = `items`.`feed_id` ' .
+ // WARNING: this is a desperate attempt at making this query
+ // work because prepared statements dont work. This is a
+ // POSSIBLE SQL INJECTION RISK WHEN MODIFIED WITHOUT THOUGHT.
+ // think twice when changing this
+ 'AND (`items`.`status` & ' . StatusFlag::UNREAD . ') = ' .
+ StatusFlag::UNREAD . ' ' .
+ 'WHERE (`feeds`.`folder_id` = 0 ' .
+ 'OR `folders`.`deleted_at` = 0' .
+ ')' .
+ 'AND `feeds`.`deleted_at` = 0 ' .
+ 'GROUP BY `feeds`.`id`';
+
+ return $this->findEntities($sql);
+ }
+
+
+ public function findByUrlHash($hash, $userId){
+ $sql = 'SELECT `feeds`.*, COUNT(`items`.`id`) AS `unread_count` ' .
+ 'FROM `*PREFIX*news_feeds` `feeds` ' .
+ 'LEFT JOIN `*PREFIX*news_items` `items` ' .
+ 'ON `feeds`.`id` = `items`.`feed_id` ' .
+ // WARNING: this is a desperate attempt at making this query
+ // work because prepared statements dont work. This is a
+ // POSSIBLE SQL INJECTION RISK WHEN MODIFIED WITHOUT THOUGHT.
+ // think twice when changing this
+ 'AND (`items`.`status` & ' . StatusFlag::UNREAD . ') = ' .
+ StatusFlag::UNREAD . ' ' .
+ 'WHERE `feeds`.`url_hash` = ? ' .
+ 'AND `feeds`.`user_id` = ? ' .
+ 'GROUP BY `feeds`.`id`';
+ $params = [$hash, $userId];
+
+ return $this->findEntity($sql, $params);
+ }
+
+
+ public function delete(Entity $entity){
+ parent::delete($entity);
+
+ // someone please slap me for doing this manually :P
+ // we needz CASCADE + FKs please
+ $sql = 'DELETE FROM `*PREFIX*news_items` WHERE `feed_id` = ?';
+ $params = [$entity->getId()];
+ $this->execute($sql, $params);
+ }
+
+
+ /**
+ * @param int $deleteOlderThan if given gets all entries with a delete date
+ * older than that timestamp
+ * @param string $userId if given returns only entries from the given user
+ * @return array with the database rows
+ */
+ public function getToDelete($deleteOlderThan=null, $userId=null) {
+ $sql = 'SELECT * FROM `*PREFIX*news_feeds` ' .
+ 'WHERE `deleted_at` > 0 ';
+ $params = [];
+
+ // sometimes we want to delete all entries
+ if ($deleteOlderThan !== null) {
+ $sql .= 'AND `deleted_at` < ? ';
+ $params[] = $deleteOlderThan;
+ }
+
+ // we need to sometimes only delete feeds of a user
+ if($userId !== null) {
+ $sql .= 'AND `user_id` = ?';
+ $params[] = $userId;
+ }
+
+ return $this->findEntities($sql, $params);
+ }
+
+
+ /**
+ * 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) {
+ $sql = 'DELETE FROM `*PREFIX*news_feeds` WHERE `user_id` = ?';
+ $this->execute($sql, [$userId]);
+ }
+
+
+}
diff --git a/lib/Db/FeedType.php b/lib/Db/FeedType.php
new file mode 100644
index 000000000..fcb42bb8a
--- /dev/null
+++ b/lib/Db/FeedType.php
@@ -0,0 +1,24 @@
+<?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\Db;
+
+
+class FeedType {
+ const FEED = 0;
+ const FOLDER = 1;
+ const STARRED = 2;
+ const SUBSCRIPTIONS = 3;
+ const SHARED = 4;
+ const EXPLORE = 5;
+} \ No newline at end of file
diff --git a/lib/Db/Folder.php b/lib/Db/Folder.php
new file mode 100644
index 000000000..8d1432a73
--- /dev/null
+++ b/lib/Db/Folder.php
@@ -0,0 +1,73 @@
+<?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\Db;
+
+use \OCP\AppFramework\Db\Entity;
+
+/**
+ * @method integer getId()
+ * @method void setId(integer $value)
+ * @method string getUserId()
+ * @method void setUserId(string $value)
+ * @method string getName()
+ * @method void setName(string $value)
+ * @method integer getParentId()
+ * @method void setParentId(integer $value)
+ * @method boolean getOpened()
+ * @method void setOpened(boolean $value)
+ * @method integer getDeletedAt()
+ * @method void setDeletedAt(integer $value)
+ * @method integer getLastModified()
+ * @method void setLastModified(integer $value)
+
+ */
+class Folder extends Entity implements IAPI, \JsonSerializable {
+
+ use EntityJSONSerializer;
+
+ protected $parentId;
+ protected $name;
+ protected $userId;
+ protected $opened;
+ protected $deletedAt;
+ protected $lastModified;
+
+ public function __construct(){
+ $this->addType('parentId', 'integer');
+ $this->addType('opened', 'boolean');
+ $this->addType('deletedAt', 'integer');
+ $this->addType('lastModified', 'integer');
+ }
+
+ /**
+ * Turns entitie attributes into an array
+ */
+ public function jsonSerialize() {
+ return $this->serializeFields([
+ 'id',
+ 'parentId',
+ 'name',
+ 'userId',
+ 'opened',
+ 'deletedAt',
+ ]);
+ }
+
+ public function toAPI() {
+ return $this->serializeFields([
+ 'id',
+ 'name'
+ ]);
+ }
+} \ No newline at end of file
diff --git a/lib/Db/FolderMapper.php b/lib/Db/FolderMapper.php
new file mode 100644
index 000000000..30acb455c
--- /dev/null
+++ b/lib/Db/FolderMapper.php
@@ -0,0 +1,110 @@
+<?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\Db;
+
+use OCA\News\Utility\Time;
+use OCP\IDBConnection;
+use OCP\AppFramework\Db\Entity;
+
+class FolderMapper extends NewsMapper {
+
+ public function __construct(IDBConnection $db, Time $time) {
+ parent::__construct($db, 'news_folders', Folder::class, $time);
+ }
+
+ public function find($id, $userId){
+ $sql = 'SELECT * FROM `*PREFIX*news_folders` ' .
+ 'WHERE `id` = ? ' .
+ 'AND `user_id` = ?';
+
+ return $this->findEntity($sql, [$id, $userId]);
+ }
+
+
+ public function findAllFromUser($userId){
+ $sql = 'SELECT * FROM `*PREFIX*news_folders` ' .
+ 'WHERE `user_id` = ? ' .
+ 'AND `deleted_at` = 0';
+ $params = [$userId];
+
+ return $this->findEntities($sql, $params);
+ }
+
+
+ public function findByName($folderName, $userId){
+ $sql = 'SELECT * FROM `*PREFIX*news_folders` ' .
+ 'WHERE `name` = ? ' .
+ 'AND `user_id` = ?';
+ $params = [$folderName, $userId];
+
+ return $this->findEntities($sql, $params);
+ }
+
+
+ public function delete(Entity $entity){
+ parent::delete($entity);
+
+ // someone please slap me for doing this manually :P
+ // we needz CASCADE + FKs please
+ $sql = 'DELETE FROM `*PREFIX*news_feeds` WHERE `folder_id` = ?';
+ $params = [$entity->getId()];
+ $stmt = $this->execute($sql, $params);
+ $stmt->closeCursor();
+
+ $sql = 'DELETE FROM `*PREFIX*news_items` WHERE `feed_id` NOT IN '.
+ '(SELECT `feeds`.`id` FROM `*PREFIX*news_feeds` `feeds`)';
+
+ $stmt = $this->execute($sql);
+ $stmt->closeCursor();
+ }
+
+
+ /**
+ * @param int $deleteOlderThan if given gets all entries with a delete date
+ * older than that timestamp
+ * @param string $userId if given returns only entries from the given user
+ * @return array with the database rows
+ */
+ public function getToDelete($deleteOlderThan=null, $userId=null) {
+ $sql = 'SELECT * FROM `*PREFIX*news_folders` ' .
+ 'WHERE `deleted_at` > 0 ';
+ $params = [];
+
+ // sometimes we want to delete all entries
+ if ($deleteOlderThan !== null) {
+ $sql .= 'AND `deleted_at` < ? ';
+ $params[] = $deleteOlderThan;
+ }
+
+ // we need to sometimes only delete feeds of a user
+ if($userId !== null) {
+ $sql .= 'AND `user_id` = ?';
+ $params[] = $userId;
+ }
+
+ return $this->findEntities($sql, $params);
+ }
+
+
+ /**
+ * Deletes all folders of a user
+ * @param string $userId the name of the user
+ */
+ public function deleteUser($userId) {
+ $sql = 'DELETE FROM `*PREFIX*news_folders` WHERE `user_id` = ?';
+ $this->execute($sql, [$userId]);
+ }
+
+
+}
diff --git a/lib/Db/IAPI.php b/lib/Db/IAPI.php
new file mode 100644
index 000000000..ff9791753
--- /dev/null
+++ b/lib/Db/IAPI.php
@@ -0,0 +1,18 @@
+<?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\Db;
+
+interface IAPI {
+ public function toAPI();
+}
diff --git a/lib/Db/Item.php b/lib/Db/Item.php
new file mode 100644
index 000000000..e0d8b069b
--- /dev/null
+++ b/lib/Db/Item.php
@@ -0,0 +1,256 @@
+<?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\Db;
+
+use \OCP\AppFramework\Db\Entity;
+
+/**
+ * @method integer getId()
+ * @method void setId(integer $value)
+ * @method string getGuid()
+ * @method void setGuid(string $value)
+ * @method string getGuidHash()
+ * @method void setGuidHash(string $value)
+ * @method string getUrl()
+ * @method string getTitle()
+ * @method string getAuthor()
+ * @method string getRtl()
+ * @method string getFingerprint()
+ * @method string getContentHash()
+ * @method integer getPubDate()
+ * @method void setPubDate(integer $value)
+ * @method string getBody()
+ * @method string getEnclosureMime()
+ * @method void setEnclosureMime(string $value)
+ * @method string getEnclosureLink()
+ * @method void setEnclosureLink(string $value)
+ * @method integer getFeedId()
+ * @method void setFeedId(integer $value)
+ * @method integer getStatus()
+ * @method void setStatus(integer $value)
+ * @method void setRtl(boolean $value)
+ * @method integer getLastModified()
+ * @method void setLastModified(integer $value)
+ * @method void setFingerprint(string $value)
+ * @method void setContentHash(string $value)
+ * @method void setSearchIndex(string $value)
+ */
+class Item extends Entity implements IAPI, \JsonSerializable {
+
+ use EntityJSONSerializer;
+
+ protected $contentHash;
+ protected $guidHash;
+ protected $guid;
+ protected $url;
+ protected $title;
+ protected $author;
+ protected $pubDate;
+ protected $body;
+ protected $enclosureMime;
+ protected $enclosureLink;
+ protected $feedId;
+ protected $status = 0;
+ protected $lastModified;
+ protected $searchIndex;
+ protected $rtl;
+ protected $fingerprint;
+
+ public function __construct() {
+ $this->addType('pubDate', 'integer');
+ $this->addType('feedId', 'integer');
+ $this->addType('status', 'integer');
+ $this->addType('lastModified', 'integer');
+ $this->addType('rtl', 'boolean');
+ }
+
+ public function setRead() {
+ $this->markFieldUpdated('status');
+ $this->status &= ~StatusFlag::UNREAD;
+ }
+
+ public function isRead() {
+ return !(($this->status & StatusFlag::UNREAD) === StatusFlag::UNREAD);
+ }
+
+ public function setUnread() {
+ $this->markFieldUpdated('status');
+ $this->status |= StatusFlag::UNREAD;
+ }
+
+ public function isUnread() {
+ return !$this->isRead();
+ }
+
+ public function setStarred() {
+ $this->markFieldUpdated('status');
+ $this->status |= StatusFlag::STARRED;
+ }
+
+ public function isStarred() {
+ return ($this->status & StatusFlag::STARRED) === StatusFlag::STARRED;
+ }
+
+ public function setUnstarred() {
+ $this->markFieldUpdated('status');
+ $this->status &= ~StatusFlag::STARRED;
+ }
+
+ public function isUnstarred() {
+ return !$this->isStarred();
+ }
+
+ /**
+ * Turns entitie attributes into an array
+ */
+ public function jsonSerialize() {
+ return [
+ 'id' => $this->getId(),
+ 'guid' => $this->getGuid(),
+ 'guidHash' => $this->getGuidHash(),
+ 'url' => $this->getUrl(),
+ 'title' => $this->getTitle(),
+ 'author' => $this->getAuthor(),
+ 'pubDate' => $this->getPubDate(),
+ 'body' => $this->getBody(),
+ 'enclosureMime' => $this->getEnclosureMime(),
+ 'enclosureLink' => $this->getEnclosureLink(),
+ 'feedId' => $this->getFeedId(),
+ 'unread' => $this->isUnread(),
+ 'starred' => $this->isStarred(),
+ 'lastModified' => $this->getLastModified(),
+ 'rtl' => $this->getRtl(),
+ 'intro' => $this->getIntro(),
+ 'fingerprint' => $this->getFingerprint(),
+ ];
+ }
+
+ public function toAPI() {
+ return [
+ 'id' => $this->getId(),
+ 'guid' => $this->getGuid(),
+ 'guidHash' => $this->getGuidHash(),
+ 'url' => $this->getUrl(),
+ 'title' => $this->getTitle(),
+ 'author' => $this->getAuthor(),
+ 'pubDate' => $this->getPubDate(),
+ 'body' => $this->getBody(),
+ 'enclosureMime' => $this->getEnclosureMime(),
+ 'enclosureLink' => $this->getEnclosureLink(),
+ 'feedId' => $this->getFeedId(),
+ 'unread' => $this->isUnread(),
+ 'starred' => $this->isStarred(),
+ 'lastModified' => $this->getLastModified(),
+ 'rtl' => $this->getRtl(),
+ 'fingerprint' => $this->getFingerprint(),
+ 'contentHash' => $this->getContentHash()
+ ];
+ }
+
+ public function toExport($feeds) {
+ return [
+ 'guid' => $this->getGuid(),
+ 'url' => $this->getUrl(),
+ 'title' => $this->getTitle(),
+ 'author' => $this->getAuthor(),
+ 'pubDate' => $this->getPubDate(),
+ 'body' => $this->getBody(),
+ 'enclosureMime' => $this->getEnclosureMime(),
+ 'enclosureLink' => $this->getEnclosureLink(),
+ 'unread' => $this->isUnread(),
+ 'starred' => $this->isStarred(),
+ 'feedLink' => $feeds['feed' . $this->getFeedId()]->getLink(),
+ 'rtl' => $this->getRtl(),
+ ];
+ }
+
+ public function getIntro() {
+ return strip_tags($this->getBody());
+ }
+
+ public static function fromImport($import) {
+ $item = new static();
+ $item->setGuid($import['guid']);
+ $item->setGuidHash($import['guid']);
+ $item->setUrl($import['url']);
+ $item->setTitle($import['title']);
+ $item->setAuthor($import['author']);
+ $item->setPubDate($import['pubDate']);
+ $item->setBody($import['body']);
+ $item->setEnclosureMime($import['enclosureMime']);
+ $item->setEnclosureLink($import['enclosureLink']);
+ $item->setRtl($import['rtl']);
+ if ($import['unread']) {
+ $item->setUnread();
+ } else {
+ $item->setRead();
+ }
+ if ($import['starred']) {
+ $item->setStarred();
+ } else {
+ $item->setUnstarred();
+ }
+
+ return $item;
+ }
+
+ public function setAuthor($name) {
+ parent::setAuthor(strip_tags($name));
+ }
+
+ public function setTitle($title) {
+ parent::setTitle(strip_tags($title));
+ }
+
+ public function generateSearchIndex() {
+ $this->setSearchIndex(
+ mb_strtolower(
+ html_entity_decode(strip_tags($this->getBody())) .
+ html_entity_decode($this->getAuthor()) .
+ html_entity_decode($this->getTitle()) .
+ $this->getUrl(),
+ 'UTF-8'
+ )
+ );
+ $this->setFingerprint($this->computeFingerprint());
+ $this->setContentHash($this->computeContentHash());
+ }
+
+ private function computeContentHash() {
+ return md5($this->getTitle() . $this->getUrl() . $this->getBody() .
+ $this->getEnclosureLink() . $this->getEnclosureMime() .
+ $this->getAuthor());
+ }
+
+ private function computeFingerprint() {
+ return md5($this->getTitle() . $this->getUrl() . $this->getBody() .
+ $this->getEnclosureLink());
+ }
+
+ public function setUrl($url) {
+ $url = trim($url);
+ if (strpos($url, 'http') === 0 || strpos($url, 'magnet') === 0) {
+ parent::setUrl($url);
+ }
+ }
+
+ public function setBody($body) {
+ // FIXME: this should not happen if the target="_blank" is already
+ // on the link
+ parent::setBody(str_replace(
+ '<a', '<a target="_blank" rel="noreferrer"', $body
+ ));
+ }
+
+}
diff --git a/lib/Db/ItemMapper.php b/lib/Db/ItemMapper.php
new file mode 100644
index 000000000..091022c6c
--- /dev/null
+++ b/lib/Db/ItemMapper.php
@@ -0,0 +1,403 @@
+<?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\Db;
+
+use Exception;
+use OCA\News\Utility\Time;
+use OCP\IDBConnection;
+
+
+class ItemMapper extends NewsMapper {
+
+ public function __construct(IDBConnection $db, Time $time) {
+ parent::__construct($db, 'news_items', Item::class, $time);
+ }
+
+ private function makeSelectQuery($prependTo = '', $oldestFirst = false,
+ $distinctFingerprint = false) {
+ 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` = 0 ' .
+ 'OR `folders`.`deleted_at` = 0 ' .
+ 'ORDER BY `items`.`id` ' . $ordering;
+ }
+
+ private function makeSelectQueryStatus($prependTo, $status,
+ $oldestFirst = false, $search = [],
+ $distinctFingerprint = false) {
+ $status = (int)$status;
+ $count = count($search);
+
+ // WARNING: Potential SQL injection if you change this carelessly
+ $sql = 'AND ((`items`.`status` & ' . $status . ') = ' . $status . ') ';
+ $sql .= str_repeat('AND `items`.`search_index` LIKE ? ', $count);
+ $sql .= $prependTo;
+
+ return $this->makeSelectQuery($sql, $oldestFirst, $distinctFingerprint);
+ }
+
+ /**
+ * 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
+ */
+ public function find($id, $userId) {
+ $sql = $this->makeSelectQuery('AND `items`.`id` = ? ');
+ return $this->findEntity($sql, [$userId, $id]);
+ }
+
+ public function starredCount($userId) {
+ $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`.`status` & ' . StatusFlag::STARRED . ') = ' .
+ StatusFlag::STARRED . ')' .
+ 'LEFT OUTER JOIN `*PREFIX*news_folders` `folders` ' .
+ 'ON `folders`.`id` = `feeds`.`folder_id` ' .
+ 'WHERE `feeds`.`folder_id` = 0 ' .
+ 'OR `folders`.`deleted_at` = 0';
+
+ $params = [$userId];
+
+ $result = $this->execute($sql, $params)->fetch();
+
+ return (int)$result['size'];
+ }
+
+
+ public function readAll($highestItemId, $time, $userId) {
+ $sql = 'UPDATE `*PREFIX*news_items` ' .
+ 'SET `status` = `status` & ? ' .
+ ', `last_modified` = ? ' .
+ 'WHERE `feed_id` IN (' .
+ 'SELECT `id` FROM `*PREFIX*news_feeds` ' .
+ 'WHERE `user_id` = ? ' .
+ ') ' .
+ 'AND `id` <= ?';
+ $params = [~StatusFlag::UNREAD, $time, $userId, $highestItemId];
+ $this->execute($sql, $params);
+ }
+
+
+ public function readFolder($folderId, $highestItemId, $time, $userId) {
+ $sql = 'UPDATE `*PREFIX*news_items` ' .
+ 'SET `status` = `status` & ? ' .
+ ', `last_modified` = ? ' .
+ 'WHERE `feed_id` IN (' .
+ 'SELECT `id` FROM `*PREFIX*news_feeds` ' .
+ 'WHERE `folder_id` = ? ' .
+ 'AND `user_id` = ? ' .
+ ') ' .
+ 'AND `id` <= ?';
+ $params = [~StatusFlag::UNREAD, $time, $folderId, $userId,
+ $highestItemId];
+ $this->execute($sql, $params);
+ }
+
+
+ public function readFeed($feedId, $highestItemId, $time, $userId) {
+ $sql = 'UPDATE `*PREFIX*news_items` ' .
+ 'SET `status` = `status` & ? ' .
+ ', `last_modified` = ? ' .
+ 'WHERE `feed_id` = ? ' .
+ 'AND `id` <= ? ' .
+ 'AND EXISTS (' .
+ 'SELECT * FROM `*PREFIX*news_feeds` ' .
+ 'WHERE `user_id` = ? ' .
+ 'AND `id` = ? ) ';
+ $params = [~StatusFlag::UNREAD, $time, $feedId, $highestItemId,
+ $userId, $feedId];
+
+ $this->execute($sql, $params);
+ }
+
+
+ private function getOperator($oldestFirst) {
+ if ($oldestFirst) {
+ return '>';
+ } else {
+ return '<';
+ }
+ }
+
+
+ public function findAllNew($updatedSince, $status, $userId) {
+ $sql = $this->makeSelectQueryStatus(
+ 'AND `items`.`last_modified` >= ? ', $status);
+ $params = [$userId, $updatedSince];
+ return $this->findEntities($sql, $params);
+ }
+
+
+ public function findAllNewFolder($id, $updatedSince, $status, $userId) {
+ $sql = 'AND `feeds`.`folder_id` = ? ' .
+ 'AND `items`.`last_modified` >= ? ';
+ $sql = $this->makeSelectQueryStatus($sql, $status);
+ $params = [$userId, $id, $updatedSince];
+ return $this->findEntities($sql, $params);
+ }
+
+
+ public function findAllNewFeed($id, $updatedSince, $status, $userId) {
+ $sql = 'AND `items`.`feed_id` = ? ' .
+ 'AND `items`.`last_modified` >= ? ';
+ $sql = $this->makeSelectQueryStatus($sql, $status);
+ $params = [$userId, $id, $updatedSince];
+ return $this->findEntities($sql, $params);
+ }
+
+
+ private function findEntitiesIgnoringNegativeLimit($sql, $params, $limit) {
+ // 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($id, $limit, $offset, $status, $oldestFirst,
+ $userId, $search = []) {
+ $params = [$userId];
+ $params = array_merge($params, $this->buildLikeParameters($search));
+ $params[] = $id;
+
+ $sql = 'AND `items`.`feed_id` = ? ';
+ if ($offset !== 0) {
+ $sql .= 'AND `items`.`id` ' .
+ $this->getOperator($oldestFirst) . ' ? ';
+ $params[] = $offset;
+ }
+ $sql = $this->makeSelectQueryStatus($sql, $status, $oldestFirst,
+ $search);
+ return $this->findEntitiesIgnoringNegativeLimit($sql, $params, $limit);
+ }
+
+
+ public function findAllFolder($id, $limit, $offset, $status, $oldestFirst,
+ $userId, $search = []) {
+ $params = [$userId];
+ $params = array_merge($params, $this->buildLikeParameters($search));
+ $params[] = $id;
+
+ $sql = 'AND `feeds`.`folder_id` = ? ';
+ if ($offset !== 0) {
+ $sql .= 'AND `items`.`id` ' .
+ $this->getOperator($oldestFirst) . ' ? ';
+ $params[] = $offset;
+ }
+ $sql = $this->makeSelectQueryStatus($sql, $status, $oldestFirst,
+ $search);
+ return $this->findEntitiesIgnoringNegativeLimit($sql, $params, $limit);
+ }
+
+
+ public function findAll($limit, $offset, $status, $oldestFirst, $userId,
+ $search = []) {
+ $params = [$userId];
+ $params = array_merge($params, $this->buildLikeParameters($search));
+ $sql = '';
+ if ($offset !== 0) {
+ $sql .= 'AND `items`.`id` ' .
+ $this->getOperator($oldestFirst) . ' ? ';
+ $params[] = $offset;
+ }
+ $sql = $this->makeSelectQueryStatus($sql, $status, $oldestFirst,
+ $search);
+
+ return $this->findEntitiesIgnoringNegativeLimit($sql, $params, $limit);
+ }
+
+
+ public function findAllUnreadOrStarred($userId) {
+ $params = [$userId];
+ $status = StatusFlag::UNREAD | StatusFlag::STARRED;
+ $sql = 'AND ((`items`.`status` & ' . $status . ') > 0) ';
+ $sql = $this->makeSelectQuery($sql);
+ return $this->findEntities($sql, $params);
+ }
+
+
+ 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
+ */
+ public function deleteReadOlderThanThreshold($threshold) {
+ $status = StatusFlag::STARRED | StatusFlag::UNREAD;
+ $params = [$status, $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 NOT ((`items`.`status` & ?) > 0) ' .
+ '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;
+
+ if ($limit > 0) {
+ $params = [$status, $row['feed_id'], $limit];
+
+ $sql = 'DELETE FROM `*PREFIX*news_items` ' .
+ 'WHERE `id` IN (' .
+ 'SELECT `id` FROM `*PREFIX*news_items` ' .
+ 'WHERE NOT ((`status` & ?) > 0) ' .
+ 'AND `feed_id` = ? ' .
+ 'ORDER BY `id` ASC ' .
+ 'LIMIT ?' .
+ ')';
+
+ $this->execute($sql, $params);
+ }
+ }
+
+ }
+
+
+ public function getNewestItemId($userId) {
+ $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'];
+ }
+
+
+ /**
+ * Deletes all items of a user
+ * @param string $userId the name of the user
+ */
+ public function deleteUser($userId) {
+ $sql = 'DELETE FROM `*PREFIX*news_items` ' .
+ 'WHERE `feed_id` IN (' .
+ 'SELECT `feeds`.`id` FROM `*PREFIX*news_feeds` `feeds` ' .
+ 'WHERE `feeds`.`user_id` = ?' .
+ ')';
+
+ $this->execute($sql, [$userId]);
+ }
+
+
+ /**
+ * Returns a list of ids and userid of all items
+ */
+ public function findAllIds($limit = null, $offset = null) {
+ $sql = 'SELECT `id` FROM `*PREFIX*news_items`';
+ return $this->execute($sql, [], $limit, $offset)->fetchAll();
+ }
+
+ /**
+ * Update search indices of all items
+ */
+ public function updateSearchIndices() {
+ // 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 = []) {
+ 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);
+ }
+ }
+
+ public function readItem($itemId, $isRead, $lastModified, $userId) {
+ $item = $this->find($itemId, $userId);
+
+ // 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 `status` = `status` & ?,
+ `last_modified` = ?
+ WHERE `fingerprint` = ?
+ AND `feed_id` IN (
+ SELECT `f`.`id` FROM `*PREFIX*news_feeds` AS `f`
+ WHERE `f`.`user_id` = ?
+ )';
+ $params = [~StatusFlag::UNREAD, $lastModified,
+ $item->getFingerprint(), $userId];
+ $this->execute($sql, $params);
+ } else {
+ $item->setLastModified($lastModified);
+ $item->setUnread();
+ $this->update($item);
+ }
+ }
+
+}
diff --git a/lib/Db/MapperFactory.php b/lib/Db/MapperFactory.php
new file mode 100644
index 000000000..9e99e44bc
--- /dev/null
+++ b/lib/Db/MapperFactory.php
@@ -0,0 +1,47 @@
+<?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\Db;
+
+use OCA\News\Utility\Time;
+use OCP\IDBConnection;
+
+use OCA\News\Db\Mysql\ItemMapper as MysqlItemMapper;
+use OCA\News\DependencyInjection\IFactory;
+
+
+class MapperFactory implements IFactory {
+
+ private $dbType;
+ private $db;
+ /**
+ * @var Time
+ */
+ private $time;
+
+ public function __construct(IDBConnection $db, $databaseType, Time $time) {
+ $this->dbType = $databaseType;
+ $this->db = $db;
+ $this->time = $time;
+ }
+
+ public function build() {
+ switch($this->dbType) {
+ case 'mysql':
+ return new MysqlItemMapper($this->db, $this->time);
+ default:
+ return new ItemMapper($this->db, $this->time);
+ }
+ }
+
+}
diff --git a/lib/Db/Mysql/ItemMapper.php b/lib/Db/Mysql/ItemMapper.php
new file mode 100644
index 000000000..70eefe457
--- /dev/null
+++ b/lib/Db/Mysql/ItemMapper.php
@@ -0,0 +1,88 @@
+<?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\Db\Mysql;
+
+use OCA\News\Utility\Time;
+use OCP\IDBConnection;
+
+use OCA\News\Db\StatusFlag;
+
+
+class ItemMapper extends \OCA\News\Db\ItemMapper {
+
+ public function __construct(IDBConnection $db, Time $time){
+ parent::__construct($db, $time);
+ }
+
+
+ /**
+ * 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
+ */
+ public function deleteReadOlderThanThreshold($threshold){
+ $status = StatusFlag::STARRED | StatusFlag::UNREAD;
+ $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 NOT ((`items`.`status` & ?) > 0) ' .
+ 'GROUP BY `feeds`.`id`, `feeds`.`articles_per_update` ' .
+ 'HAVING COUNT(*) > ?';
+ $params = [$status, $threshold];
+ $result = $this->execute($sql, $params);
+
+ while($row = $result->fetch()) {
+
+ $size = (int) $row['size'];
+ $limit = $size - $threshold;
+
+ if($limit > 0) {
+ $params = [$status, $row['feed_id'], $limit];
+
+ $sql = 'DELETE FROM `*PREFIX*news_items` ' .
+ 'WHERE NOT ((`status` & ?) > 0) ' .
+ 'AND `feed_id` = ? ' .
+ 'ORDER BY `id` ASC ' .
+ 'LIMIT ?';
+
+ $this->execute($sql, $params);
+ }
+ }
+
+ }
+
+ public function readItem($itemId, $isRead, $lastModified, $userId) {
+ $item = $this->find($itemId, $userId);
+
+ if ($isRead) {
+ $sql = 'UPDATE `*PREFIX*news_items` `items`
+ JOIN `*PREFIX*news_feeds` `feeds`
+ ON `feeds`.`id` = `items`.`feed_id`
+ SET `items`.`status` = `items`.`status` & ?,
+ `items`.`last_modified` = ?
+ WHERE `items`.`fingerprint` = ?
+ AND `feeds`.`user_id` = ?';
+ $params = [~StatusFlag::UNREAD, $lastModified,
+ $item->getFingerprint(), $userId];
+ $this->execute($sql, $params);
+ } else {
+ $item->setLastModified($lastModified);
+ $item->setUnread();
+ $this->update($item);
+ }
+ }
+
+}
diff --git a/lib/Db/NewsMapper.php b/lib/Db/NewsMapper.php
new file mode 100644
index 000000000..0acc252b9
--- /dev/null
+++ b/lib/Db/NewsMapper.php
@@ -0,0 +1,93 @@
+<?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\Db;
+
+use OCA\News\Utility\Time;
+use OCP\IDBConnection;
+use OCP\AppFramework\Db\Mapper;
+use OCP\AppFramework\Db\Entity;
+
+abstract class NewsMapper extends Mapper {
+
+ /**
+ * @var Time
+ */
+ private $time;
+
+ public function __construct(IDBConnection $db, $table, $entity,
+ Time $time) {
+ parent::__construct($db, $table, $entity);
+ $this->time = $time;
+ }
+
+ public function update(Entity $entity) {
+ $entity->setLastModified($this->time->getMicroTime());
+ return parent::update($entity);
+ }
+
+ public function insert(Entity $entity) {
+ $entity->setLastModified($this->time->getMicroTime());
+ return parent::insert($entity);
+ }
+
+ /**
+ * @param int $id the id of the feed
+ * @param string $userId the id of the user
+ *
+ * @return \OCP\AppFramework\Db\Entity
+ */
+ abstract public function find($id, $userId);
+
+ /**
+ * Performs a SELECT query with all arguments appened to the WHERE clause
+ * The SELECT will be performed on the current table and take 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 $limit
+ *
+ * @paran int $offset
+ * @return array
+ */
+ public function where(array $search = [], $limit = null, $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/StatusFlag.php b/lib/Db/StatusFlag.php
new file mode 100644
index 000000000..d91fd0280
--- /dev/null
+++ b/lib/Db/StatusFlag.php
@@ -0,0 +1,47 @@
+<?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\Db;
+
+class StatusFlag {
+ const UNREAD = 0x02;
+ const STARRED = 0x04;
+ const DELETED = 0x08;
+ const UPDATED = 0x16;
+
+
+ /**
+ * Get status for query
+ *
+ * @param int $type the type that should be turned into the status
+ * @param bool $showAll true if it should return all read items
+ * @return int the status for the database
+ */
+ public function typeToStatus($type, $showAll){
+ if($type === FeedType::STARRED){
+ return self::STARRED;
+ } else {
+ $status = 0;
+ }
+
+ if($showAll){
+ $status &= ~self::UNREAD;
+ } else {
+ $status |= self::UNREAD;
+ }
+
+ return $status;
+ }
+
+
+} \ No newline at end of file
diff --git a/lib/DependencyInjection/IFactory.php b/lib/DependencyInjection/IFactory.php
new file mode 100644
index 000000000..cb0e740d1
--- /dev/null
+++ b/lib/DependencyInjection/IFactory.php
@@ -0,0 +1,24 @@
+<?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\DependencyInjection;
+
+interface IFactory {
+
+ /**
+ * Method that constructs the object
+ * @return mixed the constructed object
+ */
+ public function build();
+
+}
diff --git a/lib/Explore/RecommendedSiteNotFoundException.php b/lib/Explore/RecommendedSiteNotFoundException.php
new file mode 100644
index 000000000..15442273b
--- /dev/null
+++ b/lib/Explore/RecommendedSiteNotFoundException.php
@@ -0,0 +1,20 @@
+<?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\Explore;
+
+use Exception;
+
+class RecommendedSiteNotFoundException extends Exception {
+
+}
diff --git a/lib/Explore/RecommendedSites.php b/lib/Explore/RecommendedSites.php
new file mode 100644
index 000000000..ecde817cd
--- /dev/null
+++ b/lib/Explore/RecommendedSites.php
@@ -0,0 +1,42 @@
+<?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\Explore;
+
+class RecommendedSites {
+
+ private $directory;
+
+ /**
+ * @param string $exploreDir the absolute path to where the recommendation
+ * config files lie without a trailing slash
+ */
+ public function __construct($exploreDir) {
+ $this->directory = $exploreDir;
+ }
+
+
+ public function forLanguage($languageCode) {
+ $file = $this->directory . '/feeds.' . $languageCode . '.json';
+
+ if (file_exists($file)) {
+ return json_decode(file_get_contents($file), true);
+ } else {
+ $msg = 'No recommended sites found for language code ' .
+ $languageCode;
+ throw new RecommendedSiteNotFoundException($msg);
+ }
+ }
+
+
+}
diff --git a/lib/Explore/feeds/feeds.de.json b/lib/Explore/feeds/feeds.de.json
new file mode 100644
index 000000000..4a3759a17
--- /dev/null
+++ b/lib/Explore/feeds/feeds.de.json
@@ -0,0 +1,65 @@
+{
+ "Technologie": [{
+ "title": "ComputerBase",
+ "favicon": "https:\/\/computerbase.de\/favicon.ico",
+ "url": "http:\/\/www.computerbase.de\/",
+ "feed": "http:\/\/www.computerbase.de\/rss\/news.xml",
+ "description": "Unabh\u00e4ngiges Tech-Magazin",
+ "votes": 100
+ }, {
+ "title": "Golem.de",
+ "favicon": "http:\/\/golem.de\/favicon.ico",
+ "url": "http:\/\/www.golem.de\/",
+ "feed": "http:\/\/golem.de.dynamic.feedsportal.com\/pf\/578068\/http:\/\/rss.golem.de\/rss.php?feed=RSS1.0",
+ "description": "IT-News fuer Profis",
+ "votes": 100,
+ "image": "http:\/\/www.golem.de\/staticrl\/images\/golem-rss.png"
+ }, {
+ "title": "derStandard.at \u203a Web",
+ "favicon": "https:\/\/derstandard.at\/img\/derstandard.at.ico",
+ "url": "http:\/\/derstandard.at\/Web",
+ "feed": "http:\/\/derstandard.at\/?page=rss&ressort=web",
+ "description": "Nachrichten in Echtzeit. derStandard.at: Permanent aktualisierte Nachrichten aus aller Welt vom Online-Newsroom der f\u00fchrenden Qualit\u00e4tszeitung \u00d6sterreichs. Plus die ganze Tageszeitung gratis im WWW. News in German, brought to you by the leading quality newspaper from Austria.",
+ "votes": 100
+ }, {
+ "title": "Pro-Linux News",
+ "favicon": "http:\/\/www.pro-linux.de\/favicon.ico",
+ "url": "http:\/\/www.pro-linux.de\/",
+ "feed": "http:\/\/www.pro-linux.de\/rss\/index1.xml",
+ "description": "Wir geben Ihrem Computer das Leben zur\u00fcck",
+ "votes": 100,
+ "image": "http:\/\/www.pro-linux.de\/images\/NB3\/base\/global\/pl-logo_i70.png"
+ }, {
+ "title": "heise online News",
+ "favicon": "https:\/\/www.heise.de\/favicon.ico",
+ "url": "http:\/\/www.heise.de\/newsticker\/",
+ "feed": "http:\/\/heise.de.feedsportal.com\/c\/35207\/f\/653901\/index.rss",
+ "description": "Nachrichten nicht nur aus der Welt der Computer",
+ "votes": 100
+ }],
+ "FOSS": [{
+ "title": "heise open news",
+ "favicon": "https:\/\/www.heise.de\/favicon.ico",
+ "url": "http:\/\/www.heise.de\/open\/news\/",
+ "feed": "http:\/\/heise.de.open.feedsportal.com\/c\/32509\/f\/480599\/index.rss",
+ "description": "Nachrichten zu Open Source",
+ "votes": 100
+ }],
+ "Blogs": [{
+ "title": "Fefes Blog",
+ "favicon": "http:\/\/blog.fefe.de\/favicon.ico",
+ "url": "http:\/\/blog.fefe.de\/",
+ "feed": "http:\/\/blog.fefe.de\/rss.xml?html",
+ "description": "Verschw\u00f6rungen und Obskures aus aller Welt",
+ "votes": 100
+ }
+ ],
+ "Nachrichten": [{
+ "title": "derStandard.at",
+ "favicon": "https:\/\/derstandard.at\/img\/derstandard.at.ico",
+ "url": "http:\/\/derstandard.at\/",
+ "feed": "http:\/\/derstandard.at\/?page=rss&ressort=seite1",
+ "description": "Nachrichten in Echtzeit. derStandard.at: Permanent aktualisierte Nachrichten aus aller Welt vom Online-Newsroom der f\u00fchrenden Qualit\u00e4tszeitung \u00d6sterreichs. Plus die ganze Tageszeitung gratis im WWW. News in German, brought to you by the leading quality newspaper from Austria.",
+ "votes": 100
+ }]
+}
diff --git a/lib/Explore/feeds/feeds.en.json b/lib/Explore/feeds/feeds.en.json
new file mode 100644
index 000000000..162ef59d3
--- /dev/null
+++ b/lib/Explore/feeds/feeds.en.json
@@ -0,0 +1,166 @@
+{
+ "Nextcloud": [{
+ "title": "Nextcloud Planet",
+ "favicon": "https://nextcloud.com/contribook/main/images/nextcloud/100.png",
+ "url": "https://nextcloud.com/news/",
+ "feed": "https://nextcloud.com/feed/",
+ "description": "Nextcloud Planet is a blog feed aggregator",
+ "votes": 1000
+ }, {
+ "title": "News app releases",
+ "favicon": "https://nextcloud.com/contribook/main/images/nextcloud/100.png",
+ "url": "https://github.com/nextcloud/news/releases",
+ "feed": "https://github.com/nextcloud/news/releases.atom",
+ "description": "Nextcloud News app releases page. Get notified when a new release comes out",
+ "votes": 900
+ }],
+ "FOSS": [{
+ "title": "Planet GNOME",
+ "favicon": "http:\/\/planet.gnome.org\/img\/gnome-16.png",
+ "url": "http:\/\/planet.gnome.org\/",
+ "feed": "http:\/\/planet.gnome.org\/rss20.xml",
+ "description": "Planet GNOME - http:\/\/planet.gnome.org\/",
+ "votes": 100
+ }, {
+ "title": "Planet KDE",
+ "favicon": "https:\/\/planetkde.org\/favicon.ico",
+ "url": "http:\/\/planetKDE.org\/",
+ "feed": "http:\/\/planetKDE.org\/rss20.xml",
+ "description": "Planet KDE - http:\/\/planetKDE.org\/",
+ "votes": 100
+ }, {
+ "title": "Planet Mozilla",
+ "favicon": "https:\/\/planet.mozilla.org\/img\/mozilla-16.png",
+ "url": "http:\/\/planet.mozilla.org\/",
+ "feed": "http:\/\/planet.mozilla.org\/atom.xml",
+ "description": "Planet Mozilla - Mozilla Blogs",
+ "votes": 100
+ }
+
+ ],
+ "Tech": [{
+ "title": "Slashdot",
+ "favicon": "https://slashdot.org/favicon.ico",
+ "url": "https://slashdot.org",
+ "feed": "https://rss.slashdot.org/Slashdot/slashdot",
+ "description": "Slashdot: News for nerds, stuff that matters",
+ "votes": 500
+ }],
+ "Gaming": [{
+ "title": "TotalBiscuit, The Cynical Brit",
+ "favicon": "https:\/\/s.ytimg.com\/yts\/img\/favicon-vflz7uhzw.ico",
+ "url": "http:\/\/www.youtube.com\/channel\/UCy1Ms_5qBTawC-k7PVjHXKQ",
+ "feed": "http:\/\/www.youtube.com\/feeds\/videos.xml?channel_id=UCy1Ms_5qBTawC-k7PVjHXKQ",
+ "description": "General gaming news and reviews",
+ "votes": 100
+ }, {
+ "title": "Force Gaming",
+ "favicon": "https:\/\/s.ytimg.com\/yts\/img\/favicon-vflz7uhzw.ico",
+ "url": "http:\/\/www.youtube.com\/channel\/UCGhs9S33RAeT5DEuKTO4Oew",
+ "feed": "http:\/\/www.youtube.com\/feeds\/videos.xml?channel_id=UCGhs9S33RAeT5DEuKTO4Oew",
+ "description": "General gaming news and reviews",
+ "votes": 100
+ }, {
+ "title": "Trump",
+ "favicon": "https:\/\/s.ytimg.com\/yts\/img\/favicon-vflz7uhzw.ico",
+ "url": "http:\/\/www.youtube.com\/channel\/UCsQnAt5I56M-qx4OgCoVmeA",
+ "feed": "http:\/\/www.youtube.com\/feeds\/videos.xml?channel_id=UCsQnAt5I56M-qx4OgCoVmeA",
+ "description": "Hearthstone focused let's plays",
+ "votes": 100
+ }],
+ "UX": [{
+ "title": "Mozilla UX blog",
+ "favicon": "https://blog.mozilla.org/ux/wp-content/themes/OneMozilla/img/favicon.ico",
+ "url": "https://blog.mozilla.org/ux/",
+ "feed": "https://blog.mozilla.org/ux/feed/",
+ "description": "Designing great products that make sense & bring joy",
+ "votes": 500
+ }, {
+ "title": "Little Big Details",
+ "url": "http://littlebigdetails.com/",
+ "feed": "http://littlebigdetails.com/rss",
+ "favicon": "http://33.media.tumblr.com/avatar_21babec3285d_128.png",
+ "description": "UX eastereggs and goodies",
+ "votes": 1000
+ }, {
+ "title": "Ignore the code",
+ "favicon": "http://ignorethecode.net/blog_images/iphone.png",
+ "url": "http://ignorethecode.net/blog/",
+ "feed": "http://feeds.feedburner.com/IgnoreTheCode",
+ "description": "A blog about usability",
+ "votes": 500
+ }, {
+ "title": "Elezea",
+ "url": "http://www.elezea.com",
+ "feed": "http://feedpress.me/elezea",
+ "description": "Links and articles about technology, design, and sociology. Written by Rian van der Merwe.",
+ "votes": 500
+ }],
+ "Music": [{
+ "title": "Pebber Brown",
+ "favicon": "https:\/\/s.ytimg.com\/yts\/img\/favicon-vflz7uhzw.ico",
+ "url": "http:\/\/www.youtube.com\/channel\/UCbA-Tt8ELg8v0d7RdieeZyA",
+ "feed": "http:\/\/www.youtube.com\/feeds\/videos.xml?channel_id=UCbA-Tt8ELg8v0d7RdieeZyA",
+ "description": "Videos about music theory and scales",
+ "votes": 100
+ }],
+ "Science": [{
+ "title": "Numberphile",
+ "favicon": "https:\/\/s.ytimg.com\/yts\/img\/favicon-vflz7uhzw.ico",
+ "url": "http:\/\/www.youtube.com\/channel\/UCoxcjq-8xIDTYp3uz647V5A",
+ "feed": "http:\/\/www.youtube.com\/feeds\/videos.xml?channel_id=UCoxcjq-8xIDTYp3uz647V5A",
+ "description": "Awesome mathematics videos",
+ "votes": 500
+ }, {
+ "title": "Periodic Videos",
+ "favicon": "https:\/\/s.ytimg.com\/yts\/img\/favicon-vflz7uhzw.ico",
+ "url": "http:\/\/www.youtube.com\/channel\/UCtESv1e7ntJaLJYKIO1FoYw",
+ "feed": "http:\/\/www.youtube.com\/feeds\/videos.xml?channel_id=UCtESv1e7ntJaLJYKIO1FoYw",
+ "description": "Videos about elements and chemistry",
+ "votes": 1000
+ }, {
+ "title": "Sixty Symbols",
+ "favicon": "https:\/\/s.ytimg.com\/yts\/img\/favicon-vflz7uhzw.ico",
+ "url": "http:\/\/www.youtube.com\/channel\/UCvBqzzvUBLCs8Y7Axb-jZew",
+ "feed": "http:\/\/www.youtube.com\/feeds\/videos.xml?channel_id=UCvBqzzvUBLCs8Y7Axb-jZew",
+ "description": "Well explained Physics videos",
+ "votes": 100
+ }, {
+ "title": "Computerphile",
+ "favicon": "https:\/\/s.ytimg.com\/yts\/img\/favicon-vflz7uhzw.ico",
+ "url": "http:\/\/www.youtube.com\/channel\/UC9-y-6csu5WGm29I7JiwpnA",
+ "feed": "http:\/\/www.youtube.com\/feeds\/videos.xml?channel_id=UC9-y-6csu5WGm29I7JiwpnA",
+ "description": "Great source of computer science videos",
+ "votes": 100
+ }],
+ "Food": [{
+ "title": "Smitten Kitchen",
+ "favicon": "http://smittenkitchen.com/uploads/favicon.ico",
+ "url": "http://smittenkitchen.com/",
+ "feed": "http://feeds.feedburner.com/smittenkitchen",
+ "description": "A home cooking weblog from a tiny kitchen in New York City. The place to find all of your new favorite things to cook",
+ "votes": 500
+ }, {
+ "title": "Food in Jars",
+ "favicon": "http://foodinjars.com.s164546.gridserver.com/arugulapesto/wp-content/uploads/2013/05/jar-favicon.png",
+ "url": "http://foodinjars.com/",
+ "feed": "http://feedpress.me/FIJ",
+ "description": "A blog dedicated to canning, preserving, and pantry staples made from scratch.",
+ "votes": 500
+ }],
+ "Comics": [{
+ "title": "Explosm.net",
+ "favicon": "http://explosm.net/favicons/favicon.ico",
+ "url": "http:\/\/www.explosm.net\/rss.php",
+ "feed": "http:\/\/feeds.feedburner.com\/Explosm",
+ "description": "Flash Animations, Daily Comics and more!",
+ "votes": 100
+ }, {
+ "title": "The Oatmeal - Comics, Quizzes, & Stories",
+ "favicon": "http:\/\/theoatmeal.com\/favicon.ico",
+ "url": "http:\/\/theoatmeal.com\/",
+ "feed": "http:\/\/feeds.feedburner.com\/oatmealfeed",
+ "description": "The oatmeal tastes better than stale skittles found under the couch cushions",
+ "votes": 100
+ }]
+}
diff --git a/lib/Fetcher/FeedFetcher.php b/lib/Fetcher/FeedFetcher.php
new file mode 100644
index 000000000..beffe9051
--- /dev/null
+++ b/lib/Fetcher/FeedFetcher.php
@@ -0,0 +1,302 @@
+<?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\Fetcher;
+
+use Exception;
+
+use PicoFeed\Parser\MalFormedXmlException;
+use PicoFeed\Reader\Reader;
+use PicoFeed\Parser\Parser;
+use PicoFeed\Reader\SubscriptionNotFoundException;
+use PicoFeed\Reader\UnsupportedFeedFormatException;
+use PicoFeed\Client\InvalidCertificateException;
+use PicoFeed\Client\InvalidUrlException;
+use PicoFeed\Client\MaxRedirectException;
+use PicoFeed\Client\MaxSizeException;
+use PicoFeed\Client\TimeoutException;
+use PicoFeed\Client\ForbiddenException;
+use PicoFeed\Client\UnauthorizedException;
+
+use OCP\IL10N;
+
+use OCA\News\Db\Item;
+use OCA\News\Db\Feed;
+use OCA\News\Utility\PicoFeedFaviconFactory;
+use OCA\News\Utility\PicoFeedReaderFactory;
+use OCA\News\Utility\Time;
+
+class FeedFetcher implements IFeedFetcher {
+
+ private $faviconFactory;
+ private $reader;
+ private $l10n;
+ private $time;
+
+ public function __construct(Reader $reader,
+ PicoFeedFaviconFactory $faviconFactory,
+ IL10N $l10n,
+ Time $time) {
+ $this->faviconFactory = $faviconFactory;
+ $this->reader = $reader;
+ $this->time = $time;
+ $this->l10n = $l10n;
+ }
+
+
+ /**
+ * This fetcher handles all the remaining urls therefore always returns true
+ */
+ public function canHandle($url) {
+ return true;
+ }
+
+
+ /**
+ * Fetch a feed from remote
+ * @param string $url remote url of the feed
+ * @param boolean $getFavicon if the favicon should also be fetched,
+ * defaults to true
+ * @param string $lastModified a last modified value from an http header
+ * defaults to false. If lastModified matches the http header from the feed
+ * no results are fetched
+ * @param string $etag an etag from an http header.
+ * If lastModified matches the http header from the feed
+ * no results are fetched
+ * @param bool fullTextEnabled if true tells the fetcher to enhance the
+ * articles by fetching custom enhanced content
+ * @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 FetcherException if it fails
+ * @return array an array containing the new feed and its items, first
+ * element being the Feed and second element being an array of Items
+ */
+ public function fetch($url, $getFavicon = true, $lastModified = null,
+ $etag = null, $fullTextEnabled = false,
+ $basicAuthUser = null, $basicAuthPassword = null) {
+ try {
+ if ($basicAuthUser !== null && trim($basicAuthUser) !== '') {
+ $resource = $this->reader->discover($url, $lastModified, $etag,
+ $basicAuthUser,
+ $basicAuthPassword);
+ } else {
+ $resource = $this->reader->discover($url, $lastModified, $etag);
+ }
+
+ if (!$resource->isModified()) {
+ return [null, null];
+ }
+
+ $location = $resource->getUrl();
+ $etag = $resource->getEtag();
+ $content = $resource->getContent();
+ $encoding = $resource->getEncoding();
+ $lastModified = $resource->getLastModified();
+
+ $parser = $this->reader->getParser($location, $content, $encoding);
+
+ if ($fullTextEnabled) {
+ $parser->enableContentGrabber();
+ }
+
+ $parsedFeed = $parser->execute();
+
+ $feed = $this->buildFeed(
+ $parsedFeed, $url, $getFavicon, $lastModified, $etag, $location
+ );
+
+ $items = [];
+ foreach ($parsedFeed->getItems() as $item) {
+ $items[] = $this->buildItem($item, $parsedFeed);
+ }
+
+ return [$feed, $items];
+
+ } catch (Exception $ex) {
+ $this->handleError($ex, $url);
+ }
+
+ }
+
+
+ private function handleError(Exception $ex, $url) {
+ $msg = $ex->getMessage();
+
+ if ($ex instanceof MalFormedXmlException) {
+ $msg = $this->l10n->t('Feed contains invalid XML');
+ } else if ($ex instanceof SubscriptionNotFoundException) {
+ $msg = $this->l10n->t('Feed not found: either the website ' .
+ 'does not provide a feed or blocks access. To rule out ' .
+ 'blocking, try to download the feed on your server\'s ' .
+ 'command line using curl: curl ' . $url);
+ } else if ($ex instanceof UnsupportedFeedFormatException) {
+ $msg = $this->l10n->t('Detected feed format is not supported');
+ } else if ($ex instanceof InvalidCertificateException) {
+ $msg = $this->buildCurlSslErrorMessage($ex->getCode());
+ } else if ($ex instanceof InvalidUrlException) {
+ $msg = $this->l10n->t('Website not found');
+ } else if ($ex instanceof MaxRedirectException) {
+ $msg = $this->l10n->t('More redirects than allowed, aborting');
+ } else if ($ex instanceof MaxSizeException) {
+ $msg = $this->l10n->t('Bigger than maximum allowed size');
+ } else if ($ex instanceof TimeoutException) {
+ $msg = $this->l10n->t('Request timed out');
+ } else if ($ex instanceof UnauthorizedException) {
+ $msg = $this->l10n->t('Required credentials for feed were ' .
+ 'either missing or incorrect');
+ } else if ($ex instanceof ForbiddenException) {
+ $msg = $this->l10n->t('Forbidden to access feed');
+ }
+
+ throw new FetcherException($msg);
+ }
+
+ private function buildCurlSslErrorMessage($errorCode) {
+ switch ($errorCode) {
+ case 35: // CURLE_SSL_CONNECT_ERROR
+ return $this->l10n->t(
+ 'Certificate error: A problem occurred ' .
+ 'somewhere in the SSL/TLS handshake. Could be ' .
+ 'certificates (file formats, paths, permissions), ' .
+ 'passwords, and others.'
+ );
+ case 51: // CURLE_PEER_FAILED_VERIFICATION
+ return $this->l10n->t(
+ 'Certificate error: The remote server\'s SSL ' .
+ 'certificate or SSH md5 fingerprint was deemed not OK.'
+ );
+ case 58: // CURLE_SSL_CERTPROBLEM
+ return $this->l10n->t(
+ 'Certificate error: Problem with the local client ' .
+ 'certificate.'
+ );
+ case 59: // CURLE_SSL_CIPHER
+ return $this->l10n->t(
+ 'Certificate error: Couldn\'t use specified cipher.'
+ );
+ case 60: // CURLE_SSL_CACERT
+ return $this->l10n->t(
+ 'Certificate error: Peer certificate cannot be ' .
+ 'authenticated with known CA certificates.'
+ );
+ case 64: // CURLE_USE_SSL_FAILED
+ return $this->l10n->t(
+ 'Certificate error: Requested FTP SSL level failed.'
+ );
+ case 66: // CURLE_SSL_ENGINE_INITFAILED
+ return $this->l10n->t(
+ 'Certificate error: Initiating the SSL Engine failed.'
+ );
+ case 77: // CURLE_SSL_CACERT_BADFILE
+ return $this->l10n->t(
+ 'Certificate error: Problem with reading the SSL CA ' .
+ 'cert (path? access rights?)'
+ );
+ case 83: // CURLE_SSL_ISSUER_ERROR
+ return $this->l10n->t(
+ 'Certificate error: Issuer check failed'
+ );
+ default:
+ return $this->l10n->t('Unknown SSL certificate error!');
+ }
+ }
+
+ private function decodeTwice($string) {
+ return html_entity_decode(
+ html_entity_decode(
+ $string, ENT_QUOTES | ENT_HTML5, 'UTF-8'
+ ),
+ ENT_QUOTES | ENT_HTML5, 'UTF-8'
+ );
+ }
+
+
+ protected function determineRtl($parsedItem, $parsedFeed) {
+ $itemLang = $parsedItem->getLanguage();
+ $feedLang = $parsedFeed->getLanguage();
+
+ if ($itemLang) {
+ return Parser::isLanguageRTL($itemLang);
+ } else {
+ return Parser::isLanguageRTL($feedLang);
+ }
+ }
+
+
+ protected function buildItem($parsedItem, $parsedFeed) {
+ $item = new Item();
+ $item->setUnread();
+ $item->setUrl($parsedItem->getUrl());
+ $item->setGuid($parsedItem->getId());
+ $item->setGuidHash($item->getGuid());
+ $item->setPubDate($parsedItem->getDate()->getTimestamp());
+ $item->setRtl($this->determineRtl($parsedItem, $parsedFeed));
+
+ // unescape content because angularjs helps against XSS
+ $item->setTitle($this->decodeTwice($parsedItem->getTitle()));
+ $item->setAuthor($this->decodeTwice($parsedItem->getAuthor()));
+
+ // purification is done in the service layer
+ $body = $parsedItem->getContent();
+ $body = mb_convert_encoding($body, 'HTML-ENTITIES',
+ mb_detect_encoding($body));
+ $item->setBody($body);
+
+ $enclosureUrl = $parsedItem->getEnclosureUrl();
+ if ($enclosureUrl) {
+ $enclosureType = $parsedItem->getEnclosureType();
+ if (stripos($enclosureType, 'audio/') !== false ||
+ stripos($enclosureType, 'video/') !== false
+ ) {
+ $item->setEnclosureMime($enclosureType);
+ $item->setEnclosureLink($enclosureUrl);
+ }
+ }
+
+ $item->generateSearchIndex();
+
+ return $item;
+ }
+
+
+ protected function buildFeed($parsedFeed, $url, $getFavicon, $modified,
+ $etag, $location) {
+ $feed = new Feed();
+
+ $link = $parsedFeed->getSiteUrl();
+
+ if (!$link) {
+ $link = $location;
+ }
+
+ // unescape content because angularjs helps against XSS
+ $title = strip_tags($this->decodeTwice($parsedFeed->getTitle()));
+ $feed->setTitle($title);
+ $feed->setUrl($url); // the url used to add the feed
+ $feed->setLocation($location); // the url where the feed was found
+ $feed->setLink($link); // <link> attribute in the feed
+ $feed->setHttpLastModified($modified);
+ $feed->setHttpEtag($etag);
+ $feed->setAdded($this->time->getTime());
+
+ if ($getFavicon) {
+ $faviconFetcher = $this->faviconFactory->build();
+ $favicon = $faviconFetcher->find($feed->getLink());
+ $feed->setFaviconLink($favicon);
+ }
+
+ return $feed;
+ }
+
+}
diff --git a/lib/Fetcher/Fetcher.php b/lib/Fetcher/Fetcher.php
new file mode 100644
index 000000000..43c9e7a3f
--- /dev/null
+++ b/lib/Fetcher/Fetcher.php
@@ -0,0 +1,69 @@
+<?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\Fetcher;
+
+
+class Fetcher {
+
+ private $fetchers;
+
+ public function __construct(){
+ $this->fetchers = [];
+ }
+
+
+ /**
+ * Add an additional fetcher
+ * @param IFeedFetcher $fetcher the fetcher
+ */
+ public function registerFetcher(IFeedFetcher $fetcher){
+ $this->fetchers[] = $fetcher;
+ }
+
+ /**
+ * Fetch a feed from remote
+ * @param string $url remote url of the feed
+ * @param boolean $getFavicon if the favicon should also be fetched,
+ * defaults to true
+ * @param string $lastModified a last modified value from an http header
+ * defaults to false. If lastModified matches the http header from the feed
+ * no results are fetched
+ * @param string $etag an etag from an http header.
+ * If lastModified matches the http header from the feed
+ * no results are fetched
+ * @param bool fullTextEnabled if true tells the fetcher to enhance the
+ * articles by fetching custom enhanced content
+ * @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 FetcherException if simple pie fails
+ * @return array an array containing the new feed and its items, first
+ * element being the Feed and second element being an array of Items
+ */
+ public function fetch($url, $getFavicon=true, $lastModified=null,
+ $etag=null, $fullTextEnabled=false,
+ $basicAuthUser=null, $basicAuthPassword=null) {
+ foreach($this->fetchers as $fetcher){
+ if($fetcher->canHandle($url)){
+ return $fetcher->fetch($url, $getFavicon, $lastModified, $etag,
+ $fullTextEnabled, $basicAuthUser,
+ $basicAuthPassword);
+ }
+ }
+
+ return [null, []];
+ }
+
+
+}
diff --git a/lib/Fetcher/FetcherException.php b/lib/Fetcher/FetcherException.php
new file mode 100644
index 000000000..27dd42f39
--- /dev/null
+++ b/lib/Fetcher/FetcherException.php
@@ -0,0 +1,26 @@
+<?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\Fetcher;
+
+class FetcherException extends \Exception {
+
+ /**
+ * Constructor
+ * @param string $msg the error message
+ */
+ public function __construct($msg){
+ parent::__construct($msg);
+ }
+
+} \ No newline at end of file
diff --git a/lib/Fetcher/IFeedFetcher.php b/lib/Fetcher/IFeedFetcher.php
new file mode 100644
index 000000000..e854d83fe
--- /dev/null
+++ b/lib/Fetcher/IFeedFetcher.php
@@ -0,0 +1,48 @@
+<?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\Fetcher;
+
+interface IFeedFetcher {
+
+ /**
+ * @param string $url remote url of the feed
+ * @param boolean $getFavicon if the favicon should also be fetched,
+ * defaults to true
+ * @param string $lastModified a last modified value from an http header
+ * defaults to false. If lastModified matches the http header from the feed
+ * no results are fetched
+ * @param string $etag an etag from an http header.
+ * If lastModified matches the http header from the feed
+ * no results are fetched
+ * @param bool fullTextEnabled if true tells the fetcher to enhance the
+ * articles by fetching custom enhanced content
+ * @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 FetcherException if the fetcher encounters a problem
+ * @return array an array containing the new feed and its items, first
+ * element being the Feed and second element being an array of Items
+ */
+ function fetch($url, $getFavicon=true, $lastModified=null, $etag=null,
+ $fullTextEnabled=false, $basicAuthUser=null,
+ $basicAuthPassword=null);
+
+ /**
+ * @param string $url the url that should be fetched
+ * @return boolean if the fetcher can handle the url. This fetcher will be
+ * used exclusively to fetch the feed and the items of the page
+ */
+ function canHandle($url);
+
+}
diff --git a/lib/Fetcher/YoutubeFetcher.php b/lib/Fetcher/YoutubeFetcher.php
new file mode 100644
index 000000000..3752ba197
--- /dev/null
+++ b/lib/Fetcher/YoutubeFetcher.php
@@ -0,0 +1,82 @@
+<?php
+/**
+ * ownCloud - News
+ *
+ * This file is licensed under the Affero General Public License version 3 or
+ * later. See the COPYING file.
+ *
+ * @author Bernhard Posselt <dev@bernhard-posselt.com>
+ * @copyright Bernhard Posselt 2012, 2014
+ */
+
+namespace OCA\News\Fetcher;
+
+
+class YoutubeFetcher implements IFeedFetcher {
+
+ private $feedFetcher;
+
+ public function __construct(FeedFetcher $feedFetcher){
+ $this->feedFetcher = $feedFetcher;
+ }
+
+
+ private function buildUrl($url) {
+ $baseRegex = '%(?:https?://|//)?(?:www.)?youtube.com';
+ $playRegex = $baseRegex . '.*?list=([^&]*)%';
+
+ if (preg_match($playRegex, $url, $matches)) {
+ $id = $matches[1];
+ return 'http://gdata.youtube.com/feeds/api/playlists/' . $id;
+ } else {
+ return $url;
+ }
+ }
+
+
+ /**
+ * This fetcher handles all the remaining urls therefore always returns true
+ */
+ public function canHandle($url){
+ return $this->buildUrl($url) !== $url;
+ }
+
+
+ /**
+ * Fetch a feed from remote
+ * @param string $url remote url of the feed
+ * @param boolean $getFavicon if the favicon should also be fetched,
+ * defaults to true
+ * @param string $lastModified a last modified value from an http header
+ * defaults to false. If lastModified matches the http header from the feed
+ * no results are fetched
+ * @param string $etag an etag from an http header.
+ * If lastModified matches the http header from the feed
+ * no results are fetched
+ * @param bool fullTextEnabled if true tells the fetcher to enhance the
+ * articles by fetching custom enhanced content
+ * @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 FetcherException if it fails
+ * @return array an array containing the new feed and its items, first
+ * element being the Feed and second element being an array of Items
+ */
+ public function fetch($url, $getFavicon=true, $lastModified=null,
+ $etag=null, $fullTextEnabled=false,
+ $basicAuthUser=null, $basicAuthPassword=null) {
+ $transformedUrl = $this->buildUrl($url);
+
+ $result = $this->feedFetcher->fetch(
+ $transformedUrl, $getFavicon, $lastModified, $etag,
+ $fullTextEnabled, $basicAuthUser, $basicAuthPassword
+ );
+
+ // reset feed url so we know the correct added url for the feed
+ $result[0]->setUrl($url);
+
+ return $result;
+ }
+
+
+}
diff --git a/lib/Hooks/User.php b/lib/Hooks/User.php
new file mode 100644
index 000000000..158396219
--- /dev/null
+++ b/lib/Hooks/User.php
@@ -0,0 +1,35 @@
+<?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\Hooks;
+
+use OCA\News\AppInfo\Application;
+use OCA\News\Service\ItemService;
+use OCA\News\Service\FeedService;
+use OCA\News\Service\FolderService;
+
+class User {
+
+ public static function deleteUser($params) {
+ $userId = $params['uid'];
+
+ $app = new Application();
+ $container = $app->getContainer();
+
+ // order is important!
+ $container->query(ItemService::class)->deleteUser($userId);
+ $container->query(FeedService::class)->deleteUser($userId);
+ $container->query(FolderService::class)->deleteUser($userId);
+ }
+
+}
diff --git a/lib/Http/TextDownloadResponse.php b/lib/Http/TextDownloadResponse.php
new file mode 100644
index 000000000..ccf959b5b
--- /dev/null
+++ b/lib/Http/TextDownloadResponse.php
@@ -0,0 +1,49 @@
+<?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\Http;
+
+use \OCP\AppFramework\Http\DownloadResponse;
+
+
+/**
+ * Prompts the user to download the a text file
+ */
+class TextDownloadResponse extends DownloadResponse {
+
+ private $content;
+
+ /**
+ * Creates a response that prompts the user to download a file which
+ * contains the passed string
+ * @param string $content the content that should be written into the file
+ * @param string $filename the name that the downloaded file should have
+ * @param string $contentType the mimetype that the downloaded file should
+ * have
+ */
+ public function __construct($content, $filename, $contentType){
+ parent::__construct($filename, $contentType);
+ $this->content = $content;
+ }
+
+
+ /**
+ * Simply sets the headers and returns the file contents
+ * @return string the file contents
+ */
+ public function render(){
+ return $this->content;
+ }
+
+
+}
diff --git a/lib/Http/TextResponse.php b/lib/Http/TextResponse.php
new file mode 100644
index 000000000..7f1866ec4
--- /dev/null
+++ b/lib/Http/TextResponse.php
@@ -0,0 +1,46 @@
+<?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\Http;
+
+use \OCP\AppFramework\Http\Response;
+
+/**
+ * Just outputs text to the browser
+ */
+class TextResponse extends Response {
+
+ private $content;
+
+ /**
+ * Creates a response that just outputs text
+ * @param string $content the content that should be written into the file
+ * @param string $contentType the mimetype. text/ is added automatically so
+ * only plain or html can be added to get text/plain or text/html
+ */
+ public function __construct($content, $contentType='plain'){
+ $this->content = $content;
+ $this->addHeader('Content-type', 'text/' . $contentType);
+ }
+
+
+ /**
+ * Simply sets the headers and returns the file contents
+ * @return string the file contents
+ */
+ public function render(){
+ return $this->content;
+ }
+
+
+}
diff --git a/lib/Plugin/Client/Plugin.php b/lib/Plugin/Client/Plugin.php
new file mode 100644
index 000000000..2be110e5a
--- /dev/null
+++ b/lib/Plugin/Client/Plugin.php
@@ -0,0 +1,44 @@
+<?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\Plugin\Client;
+
+/**
+ * We actually really want to avoid this global list of plugins. A way would be
+ * for News plugin apps to register themselves in a special database table
+ * and the News app would just pull out the scripts that should be attached
+ * but atm there is no really good way since there is no uninstall hook which
+ * would remove the plugin from the apps so fk it :)
+ */
+class Plugin {
+
+ private static $scripts = [];
+ private static $styles = [];
+
+ public static function registerStyle($appName, $styleName) {
+ self::$styles[$appName] = $styleName;
+ }
+
+ public static function registerScript($appName, $scriptName) {
+ self::$scripts[$appName] = $scriptName;
+ }
+
+ public static function getStyles() {
+ return self::$styles;
+ }
+
+ public static function getScripts() {
+ return self::$scripts;
+ }
+
+} \ No newline at end of file
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, $userId);
+ }
+
+
+ /**
+ * This method deletes all unread feeds that are not starred and over the
+ * count of $this->autoPurgeCount starting by the oldest. This is to clean
+ * up the database so that old entries don't spam your db. As criteria for
+ * old, the id is taken
+ */
+ public function autoPurgeOld(){
+ $count = $this->config->getAutoPurgeCount();
+ if ($count >= 0) {
+ $this->itemMapper->deleteReadOlderThanThreshold($count);
+ }
+ }
+
+
+ /**
+ * Returns the newest item id, use this for marking feeds read
+ * @param string $userId the name of the user
+ * @throws ServiceNotFoundException if there is no newest item
+ * @return int
+ */
+ public function getNewestItemId($userId) {
+ try {
+ return $this->itemMapper->getNewestItemId($userId);
+ } catch(DoesNotExistException $ex) {
+ throw new ServiceNotFoundException($ex->getMessage());
+ }
+ }
+
+
+ /**
+ * Returns the starred count
+ * @param string $userId the name of the user
+ * @return int the count
+ */
+ public function starredCount($userId){
+ return $this->itemMapper->starredCount($userId);
+ }
+
+
+ /**
+ * @param string $userId from which user the items should be taken
+ * @return array of items which are starred or unread
+ */
+ public function getUnreadOrStarred($userId) {
+ return $this->itemMapper->findAllUnreadOrStarred($userId);
+ }
+
+
+ /**
+ * Deletes all items of a user
+ * @param string $userId the name of the user
+ */
+ public function deleteUser($userId) {
+ $this->itemMapper->deleteUser($userId);
+ }
+
+
+ /**
+ * Regenerates the search index for all items
+ */
+ public function generateSearchIndices() {
+ $this->itemMapper->updateSearchIndices();
+ }
+
+}
diff --git a/lib/Service/Service.php b/lib/Service/Service.php
new file mode 100644
index 000000000..e53468828
--- /dev/null
+++ b/lib/Service/Service.php
@@ -0,0 +1,62 @@
+<?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\AppFramework\Db\DoesNotExistException;
+use \OCP\AppFramework\Db\MultipleObjectsReturnedException;
+
+use \OCA\News\Db\NewsMapper;
+
+
+abstract class Service {
+
+ protected $mapper;
+
+ public function __construct(NewsMapper $mapper){
+ $this->mapper = $mapper;
+ }
+
+
+ /**
+ * Delete an entity
+ * @param int $id the id of the entity
+ * @param string $userId the name of the user for security reasons
+ * @throws ServiceNotFoundException if the entity does not exist, or there
+ * are more than one of it
+ */
+ public function delete($id, $userId){
+ $entity = $this->find($id, $userId);
+ $this->mapper->delete($entity);
+ }
+
+
+ /**
+ * Finds an entity by id
+ * @param int $id the id of the entity
+ * @param string $userId the name of the user for security reasons
+ * @throws ServiceNotFoundException if the entity does not exist, or there
+ * are more than one of it
+ * @return \OCP\AppFramework\Db\Entity the entity
+ */
+ public function find($id, $userId){
+ try {
+ return $this->mapper->find($id, $userId);
+ } catch(DoesNotExistException $ex){
+ throw new ServiceNotFoundException($ex->getMessage());
+ } catch(MultipleObjectsReturnedException $ex){
+ throw new ServiceNotFoundException($ex->getMessage());
+ }
+ }
+
+}
diff --git a/lib/Service/ServiceConflictException.php b/lib/Service/ServiceConflictException.php
new file mode 100644
index 000000000..d27fb98c1
--- /dev/null
+++ b/lib/Service/ServiceConflictException.php
@@ -0,0 +1,27 @@
+<?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;
+
+
+class ServiceConflictException extends ServiceException {
+
+ /**
+ * Constructor
+ * @param string $msg the error message
+ */
+ public function __construct($msg){
+ parent::__construct($msg);
+ }
+
+} \ No newline at end of file
diff --git a/lib/Service/ServiceException.php b/lib/Service/ServiceException.php
new file mode 100644
index 000000000..1a789e0b0
--- /dev/null
+++ b/lib/Service/ServiceException.php
@@ -0,0 +1,27 @@
+<?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;
+
+
+class ServiceException extends \Exception {
+
+ /**
+ * Constructor
+ * @param string $msg the error message
+ */
+ public function __construct($msg){
+ parent::__construct($msg);
+ }
+
+} \ No newline at end of file
diff --git a/lib/Service/ServiceNotFoundException.php b/lib/Service/ServiceNotFoundException.php
new file mode 100644
index 000000000..e4ee61fbe
--- /dev/null
+++ b/lib/Service/ServiceNotFoundException.php
@@ -0,0 +1,27 @@
+<?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;
+
+
+class ServiceNotFoundException extends ServiceException {
+
+ /**
+ * Constructor
+ * @param string $msg the error message
+ */
+ public function __construct($msg){
+ parent::__construct($msg);
+ }
+
+} \ No newline at end of file
diff --git a/lib/Service/ServiceValidationException.php b/lib/Service/ServiceValidationException.php
new file mode 100644
index 000000000..510df0a22
--- /dev/null
+++ b/lib/Service/ServiceValidationException.php
@@ -0,0 +1,27 @@
+<?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;
+
+
+class ServiceValidationException extends ServiceException {
+
+ /**
+ * Constructor
+ * @param string $msg the error message
+ */
+ public function __construct($msg){
+ parent::__construct($msg);
+ }
+
+} \ No newline at end of file
diff --git a/lib/Service/StatusService.php b/lib/Service/StatusService.php
new file mode 100644
index 000000000..b36e64c1b
--- /dev/null
+++ b/lib/Service/StatusService.php
@@ -0,0 +1,57 @@
+<?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 OCA\News\Config\Config;
+
+
+class StatusService {
+
+ private $settings;
+ private $config;
+ private $appName;
+
+ public function __construct(IConfig $settings, Config $config, $AppName) {
+ $this->settings = $settings;
+ $this->config = $config;
+ $this->appName = $AppName;
+ }
+
+ public function isProperlyConfigured() {
+ $cronMode = $this->settings->getAppValue(
+ 'core', 'backgroundjobs_mode'
+ );
+ $cronOff = !$this->config->getUseCronUpdates();
+
+ // check for cron modes which may lead to problems
+ return $cronMode === 'cron' || $cronOff;
+ }
+
+
+ public function getStatus() {
+ $version = $this->settings->getAppValue(
+ $this->appName, 'installed_version'
+ );
+
+ return [
+ 'version' => $version,
+ 'warnings' => [
+ 'improperlyConfiguredCron' => !$this->isProperlyConfigured()
+ ]
+ ];
+ }
+
+} \ No newline at end of file
diff --git a/lib/Upgrade/Upgrade.php b/lib/Upgrade/Upgrade.php
new file mode 100644
index 000000000..2ddcddb2c
--- /dev/null
+++ b/lib/Upgrade/Upgrade.php
@@ -0,0 +1,71 @@
+<?php
+/**
+ * ownCloud - News
+ *
+ * This file is licensed under the Affero General Public License version 3 or
+ * later. See the COPYING file.
+ *
+ * @author Bernhard Posselt <dev@bernhard-posselt.com>
+ * @copyright Bernhard Posselt 2015
+ */
+
+namespace OCA\News\Upgrade;
+
+use OCP\IConfig;
+use OCA\News\Service\ItemService;
+use OCP\IDBConnection;
+
+class Upgrade {
+
+ /** @var IConfig */
+ private $config;
+
+ /** @var ItemService */
+ private $itemService;
+
+ private $appName;
+ /**
+ * @var IDBConnection
+ */
+ private $db;
+
+ /**
+ * Upgrade constructor.
+ * @param IConfig $config
+ * @param $appName
+ */
+ public function __construct(IConfig $config, ItemService $itemService,
+ IDBConnection $db, $appName) {
+ $this->config = $config;
+ $this->appName = $appName;
+ $this->itemService = $itemService;
+ $this->db = $db;
+ }
+
+ public function upgrade() {
+ $previousVersion = $this->config->getAppValue(
+ $this->appName, 'installed_version'
+ );
+
+ if (version_compare($previousVersion, '8.9.0', '<=')) {
+ $this->itemService->generateSearchIndices();
+ }
+ }
+
+ public function preUpgrade() {
+ $previousVersion = $this->config->getAppValue(
+ $this->appName, 'installed_version'
+ );
+
+ $dbType = $this->config->getSystemValue('dbtype');
+ if (version_compare($previousVersion, '8.2.2', '<') &&
+ $dbType !== 'sqlite3'
+ ) {
+ $sql = 'ALTER TABLE `*PREFIX*news_feeds` DROP COLUMN
+ `last_modified`';
+ $query = $this->db->prepare($sql);
+ $query->execute();
+ }
+ }
+
+}
diff --git a/lib/Utility/OPMLExporter.php b/lib/Utility/OPMLExporter.php
new file mode 100644
index 000000000..d786c0c6f
--- /dev/null
+++ b/lib/Utility/OPMLExporter.php
@@ -0,0 +1,91 @@
+<?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\Utility;
+
+/**
+* Exports the OPML
+*/
+class OPMLExporter {
+
+ /**
+ * Generates the OPML for the active user
+ *
+ * @param \OCA\News\Db\Folder[] $folders
+ * @param \OCA\News\Db\Feed[] $feeds
+ * @return \DomDocument the document
+ */
+ public function build($folders, $feeds){
+ $document = new \DomDocument('1.0', 'UTF-8');
+ $document->formatOutput = true;
+
+ $root = $document->createElement('opml');
+ $root->setAttribute('version', '2.0');
+
+ // head
+ $head = $document->createElement('head');
+
+ $title = $document->createElement('title', 'Subscriptions');
+ $head->appendChild($title);
+
+ $root->appendChild($head);
+
+ // body
+ $body = $document->createElement('body');
+
+ // feeds with folders
+ foreach($folders as $folder) {
+ $folderOutline = $document->createElement('outline');
+ $folderOutline->setAttribute('title', $folder->getName());
+ $folderOutline->setAttribute('text', $folder->getName());
+
+ // feeds in folders
+ foreach ($feeds as $feed) {
+ if ($feed->getFolderId() === $folder->getId()) {
+ $feedOutline = $this->createFeedOutline($feed, $document);
+ $folderOutline->appendChild($feedOutline);
+ }
+ }
+
+ $body->appendChild($folderOutline);
+ }
+
+ // feeds without folders
+ foreach ($feeds as $feed) {
+ if ($feed->getFolderId() === 0) {
+ $feedOutline = $this->createFeedOutline($feed, $document);
+ $body->appendChild($feedOutline);
+ }
+ }
+
+ $root->appendChild($body);
+
+ $document->appendChild($root);
+
+ return $document;
+ }
+
+
+ protected function createFeedOutline($feed, $document) {
+ $feedOutline = $document->createElement('outline');
+ $feedOutline->setAttribute('title', $feed->getTitle());
+ $feedOutline->setAttribute('text', $feed->getTitle());
+ $feedOutline->setAttribute('type', 'rss');
+ $feedOutline->setAttribute('xmlUrl', $feed->getUrl());
+ $feedOutline->setAttribute('htmlUrl', $feed->getLink());
+ return $feedOutline;
+ }
+
+
+}
+
diff --git a/lib/Utility/PicoFeedClientFactory.php b/lib/Utility/PicoFeedClientFactory.php
new file mode 100644
index 000000000..a57f7f022
--- /dev/null
+++ b/lib/Utility/PicoFeedClientFactory.php
@@ -0,0 +1,40 @@
+<?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\Utility;
+
+use \PicoFeed\Config\Config;
+use \PicoFeed\Client\Client;
+
+class PicoFeedClientFactory {
+
+ private $config;
+
+ public function __construct(Config $config) {
+ $this->config = $config;
+ }
+
+
+ /**
+ * Returns a new instance of an PicoFeed Http client
+ * @return \PicoFeed\Client instance
+ */
+ public function build() {
+ $client = Client::getInstance();
+ $client->setConfig($this->config);
+ return $client;
+ }
+
+
+} \ No newline at end of file
diff --git a/lib/Utility/PicoFeedFaviconFactory.php b/lib/Utility/PicoFeedFaviconFactory.php
new file mode 100644
index 000000000..b3a48747a
--- /dev/null
+++ b/lib/Utility/PicoFeedFaviconFactory.php
@@ -0,0 +1,38 @@
+<?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\Utility;
+
+use \PicoFeed\Config\Config;
+use \PicoFeed\Reader\Favicon;
+
+class PicoFeedFaviconFactory {
+
+ private $config;
+
+ public function __construct(Config $config) {
+ $this->config = $config;
+ }
+
+
+ /**
+ * Returns a new instance of an PicoFeed Http client
+ * @return \PicoFeed\Favicon instance
+ */
+ public function build() {
+ return new Favicon($this->config);
+ }
+
+
+} \ No newline at end of file
diff --git a/lib/Utility/ProxyConfigParser.php b/lib/Utility/ProxyConfigParser.php
new file mode 100644
index 000000000..4710477e8
--- /dev/null
+++ b/lib/Utility/ProxyConfigParser.php
@@ -0,0 +1,65 @@
+<?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\Utility;
+
+use \OCP\IConfig;
+
+
+class ProxyConfigParser {
+
+ private $config;
+
+ public function __construct(IConfig $config) {
+ $this->config = $config;
+ }
+
+
+ /**
+ * Parses the config and splits up the port + url
+ * @return array
+ */
+ public function parse() {
+ $proxy = $this->config->getSystemValue('proxy');
+ $userpasswd = $this->config->getSystemValue('proxyuserpwd');
+
+ $result = [
+ 'host' => null,
+ 'port' => null,
+ 'user' => null,
+ 'password' => null
+ ];
+
+ // we need to filter out the port -.-
+ $url = new \Net_URL2($proxy);
+ $port = $url->getPort();
+
+ $url->setPort(false);
+ $host = $url->getUrl();
+
+
+ $result['host'] = $host;
+ $result['port'] = $port;
+
+ if ($userpasswd) {
+ $auth = explode(':', $userpasswd, 2);
+ $result['user'] = $auth[0];
+ $result['password'] = $auth[1];
+ }
+
+ return $result;
+ }
+
+
+} \ No newline at end of file
diff --git a/lib/Utility/Time.php b/lib/Utility/Time.php
new file mode 100644
index 000000000..e6ea2b470
--- /dev/null
+++ b/lib/Utility/Time.php
@@ -0,0 +1,30 @@
+<?php
+/**
+ * ownCloud - News
+ *
+ * This file is licensed under the Affero General Public License version 3 or
+ * later. See the COPYING file.
+ *
+ * @author Bernhard Posselt <dev@bernhard-posselt.com>
+ * @copyright Bernhard Posselt 2016
+ */
+
+namespace OCA\News\Utility;
+
+class Time {
+ public function getTime() {
+ return time();
+ }
+
+ /**
+ * @return int the current unix time in miliseconds
+ */
+ public function getMicroTime() {
+ $utimestamp = microtime(true);
+ $timestamp = floor($utimestamp);
+ $milliseconds = round(($utimestamp - $timestamp) * 1000000);
+ $result = ($timestamp * 1000000) + $milliseconds;
+ return $result;
+ }
+
+} \ No newline at end of file
diff --git a/lib/Utility/Updater.php b/lib/Utility/Updater.php
new file mode 100644
index 000000000..a6f8dc28a
--- /dev/null
+++ b/lib/Utility/Updater.php
@@ -0,0 +1,54 @@
+<?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\Utility;
+
+use \OCA\News\Service\FolderService;
+use \OCA\News\Service\FeedService;
+use \OCA\News\Service\ItemService;
+
+
+class Updater {
+
+
+ private $folderService;
+ private $feedService;
+ private $itemService;
+
+ public function __construct(FolderService $folderService,
+ FeedService $feedService,
+ ItemService $itemService) {
+ $this->folderService = $folderService;
+ $this->feedService = $feedService;
+ $this->itemService = $itemService;
+ }
+
+
+ public function beforeUpdate() {
+ $this->folderService->purgeDeleted();
+ $this->feedService->purgeDeleted();
+ }
+
+
+ public function update() {
+ $this->feedService->updateAll();
+ }
+
+
+ public function afterUpdate() {
+ $this->itemService->autoPurgeOld();
+ }
+
+
+} \ No newline at end of file