diff options
author | Bernhard Posselt <dev@bernhard-posselt.com> | 2016-07-23 22:24:54 +0300 |
---|---|---|
committer | Bernhard Posselt <dev@bernhard-posselt.com> | 2016-07-23 22:24:54 +0300 |
commit | 004fcbbcc7609ca83807f2e38967ef54f469bf72 (patch) | |
tree | 49eb99b4ea92b2045793fc567f719b31ec7f9042 /lib | |
parent | 60abc0ed4438c9b6fda245b0dc33cb483bc2aeaf (diff) |
Move to new directory structure
Diffstat (limited to 'lib')
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 |