Merge pull request #282 from SMillerDev/switch_feedIO

Switch to feed-io for parsing
This commit is contained in:
John Molakvoæ 2019-02-28 07:35:41 +01:00 коммит произвёл GitHub
Родитель 89d9b77994 7c17b2c24b
Коммит 249352c9d3
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
26 изменённых файлов: 1689 добавлений и 1930 удалений

Просмотреть файл

@ -41,10 +41,10 @@
app_name:=$(notdir $(CURDIR))
build_tools_directory:=$(CURDIR)/build/tools
source_build_directory:=$(CURDIR)/build/source/news
source_build_directory:=$(CURDIR)/build/source/$(app_name)
source_artifact_directory:=$(CURDIR)/build/artifacts/source
source_package_name:=$(source_artifact_directory)/$(app_name)
appstore_build_directory:=$(CURDIR)/build/appstore/news
appstore_build_directory:=$(CURDIR)/build/appstore/$(app_name)
appstore_artifact_directory:=$(CURDIR)/build/artifacts/appstore
appstore_package_name:=$(appstore_artifact_directory)/$(app_name)
npm:=$(shell which npm 2> /dev/null)

Просмотреть файл

@ -10,6 +10,12 @@
"homepage": "https://bernhard-posselt.com",
"role": "Developer"
},
{
"name": "Sean Molenaar",
"email": "sean@seanmolenaar.eu",
"homepage": "https://seanmolenaar.eu",
"role": "Developer"
},
{
"name": "Alessandro Cosentino",
"homepage": "http://algorithmsforthekitchen.com/",
@ -33,11 +39,16 @@
"ezyang/htmlpurifier": "4.10.0",
"pear/net_url2": "2.2.2",
"riimu/kit-pathjoin": "1.2.0",
"nicolus/picofeed": "0.1.35"
"debril/feed-io": "^3.0",
"arthurhoaro/favicon": "^1.2"
},
"require-dev": {
"phpunit/phpunit": "^6.5",
"squizlabs/php_codesniffer": "^3.3"
"squizlabs/php_codesniffer": "^3.3",
"guzzlehttp/guzzle": "~6.3"
},
"replace": {
"guzzlehttp/guzzle": "*"
},
"autoload": {
"psr-4": {

350
composer.lock сгенерированный
Просмотреть файл

@ -4,8 +4,118 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "f4a0d96b7e83ec4d9d232412b9e61566",
"content-hash": "1630b553e70e8245b11922394d4d9f59",
"packages": [
{
"name": "arthurhoaro/favicon",
"version": "v1.2.2",
"source": {
"type": "git",
"url": "https://github.com/ArthurHoaro/favicon.git",
"reference": "50fd2a0f984db13948a69ab120451e03e41979fa"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ArthurHoaro/favicon/zipball/50fd2a0f984db13948a69ab120451e03e41979fa",
"reference": "50fd2a0f984db13948a69ab120451e03e41979fa",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"require-dev": {
"phpunit/phpunit": "~4.8",
"weew/helpers-filesystem": "~1.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Favicon\\": "src/Favicon/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "Chris Shiflett",
"homepage": "http://shiflett.org/"
},
{
"name": "Arthur Hoaro",
"homepage": "http://hoa.ro"
}
],
"description": "PHP Library used to discover favicon from given URL",
"homepage": "https://github.com/ArthurHoaro/favicon",
"keywords": [
"favicon",
"finder",
"icon"
],
"time": "2018-09-08T09:37:54+00:00"
},
{
"name": "debril/feed-io",
"version": "v3.1.1",
"source": {
"type": "git",
"url": "https://github.com/alexdebril/feed-io.git",
"reference": "a79a09a915540b5475b12c82effb3dd43c2b2a0b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/alexdebril/feed-io/zipball/a79a09a915540b5475b12c82effb3dd43c2b2a0b",
"reference": "a79a09a915540b5475b12c82effb3dd43c2b2a0b",
"shasum": ""
},
"require": {
"guzzlehttp/guzzle": "~6.2",
"php": ">=5.6.0",
"psr/log": "~1.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.4",
"monolog/monolog": "1.*",
"phpunit/phpunit": "~5.6.0"
},
"suggest": {
"monolog/monolog": "Allows to handle logs",
"symfony/console": "Allows to use the command line interface"
},
"bin": [
"bin/feedio"
],
"type": "library",
"autoload": {
"psr-4": {
"FeedIo\\": "src/FeedIo"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Alexandre Debril",
"email": "alex.debril@gmail.com"
}
],
"description": "PHP library built to consume and serve JSONFeed / RSS / Atom feeds",
"homepage": "https://feed-io.net",
"keywords": [
"atom",
"cli",
"client",
"feed",
"jsonfeed",
"news",
"rss"
],
"time": "2018-06-18T12:31:47+00:00"
},
{
"name": "ezyang/htmlpurifier",
"version": "v4.10.0",
@ -53,59 +163,6 @@
],
"time": "2018-02-23T01:58:20+00:00"
},
{
"name": "nicolus/picofeed",
"version": "v0.1.35",
"source": {
"type": "git",
"url": "https://github.com/nicolus/picoFeed.git",
"reference": "3a27b47de31eedec075c719f961783c5db7a7b08"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nicolus/picoFeed/zipball/3a27b47de31eedec075c719f961783c5db7a7b08",
"reference": "3a27b47de31eedec075c719f961783c5db7a7b08",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-iconv": "*",
"ext-libxml": "*",
"ext-simplexml": "*",
"ext-xml": "*",
"php": ">=5.3.0",
"zendframework/zendxml": "^1.0"
},
"require-dev": {
"phpdocumentor/reflection-docblock": "2.0.4",
"phpunit/phpunit": "4.8.26",
"symfony/yaml": "2.8.7"
},
"suggest": {
"ext-curl": "PicoFeed will use cURL if present"
},
"bin": [
"picofeed"
],
"type": "library",
"autoload": {
"psr-0": {
"PicoFeed": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Frédéric Guillot"
}
],
"description": "Modern library to handle RSS/Atom feeds",
"homepage": "https://github.com/miniflux/picoFeed",
"time": "2017-06-20T22:54:47+00:00"
},
{
"name": "pear/net_url2",
"version": "v2.2.2",
@ -170,6 +227,53 @@
],
"time": "2017-08-25T06:16:11+00:00"
},
{
"name": "psr/log",
"version": "1.1.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/log.git",
"reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/log/zipball/6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd",
"reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Log\\": "Psr/Log/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "Common interface for logging libraries",
"homepage": "https://github.com/php-fig/log",
"keywords": [
"log",
"psr",
"psr-3"
],
"time": "2018-11-20T15:27:04+00:00"
},
{
"name": "riimu/kit-pathjoin",
"version": "v1.2.0",
@ -219,52 +323,6 @@
"system"
],
"time": "2017-07-09T14:41:04+00:00"
},
{
"name": "zendframework/zendxml",
"version": "1.1.0",
"source": {
"type": "git",
"url": "https://github.com/zendframework/ZendXml.git",
"reference": "267db6a2c431a08a8f8ff0f1f4c302a5ba6f5b99"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/zendframework/ZendXml/zipball/267db6a2c431a08a8f8ff0f1f4c302a5ba6f5b99",
"reference": "267db6a2c431a08a8f8ff0f1f4c302a5ba6f5b99",
"shasum": ""
},
"require": {
"php": "^5.6 || ^7.0"
},
"require-dev": {
"phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.4",
"zendframework/zend-coding-standard": "~1.0.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.1.x-dev",
"dev-develop": "1.2.x-dev"
}
},
"autoload": {
"psr-4": {
"ZendXml\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"description": "Utility library for XML usage, best practices, and security in PHP",
"keywords": [
"ZendFramework",
"security",
"xml",
"zf"
],
"time": "2018-04-30T15:11:04+00:00"
}
],
"packages-dev": [
@ -938,16 +996,16 @@
},
{
"name": "phpunit/phpunit",
"version": "6.5.13",
"version": "6.5.14",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "0973426fb012359b2f18d3bd1e90ef1172839693"
"reference": "bac23fe7ff13dbdb461481f706f0e9fe746334b7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0973426fb012359b2f18d3bd1e90ef1172839693",
"reference": "0973426fb012359b2f18d3bd1e90ef1172839693",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/bac23fe7ff13dbdb461481f706f0e9fe746334b7",
"reference": "bac23fe7ff13dbdb461481f706f0e9fe746334b7",
"shasum": ""
},
"require": {
@ -1018,7 +1076,7 @@
"testing",
"xunit"
],
"time": "2018-09-08T15:10:43+00:00"
"time": "2019-02-01T05:22:47+00:00"
},
{
"name": "phpunit/phpunit-mock-objects",
@ -1077,6 +1135,7 @@
"mock",
"xunit"
],
"abandoned": true,
"time": "2018-08-09T05:50:03+00:00"
},
{
@ -1640,16 +1699,16 @@
},
{
"name": "squizlabs/php_codesniffer",
"version": "3.3.2",
"version": "3.4.0",
"source": {
"type": "git",
"url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
"reference": "6ad28354c04b364c3c71a34e4a18b629cc3b231e"
"reference": "379deb987e26c7cd103a7b387aea178baec96e48"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/6ad28354c04b364c3c71a34e4a18b629cc3b231e",
"reference": "6ad28354c04b364c3c71a34e4a18b629cc3b231e",
"url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/379deb987e26c7cd103a7b387aea178baec96e48",
"reference": "379deb987e26c7cd103a7b387aea178baec96e48",
"shasum": ""
},
"require": {
@ -1687,7 +1746,65 @@
"phpcs",
"standards"
],
"time": "2018-09-23T23:08:17+00:00"
"time": "2018-12-19T23:57:18+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.10.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "e3d826245268269cd66f8326bd8bc066687b4a19"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e3d826245268269cd66f8326bd8bc066687b4a19",
"reference": "e3d826245268269cd66f8326bd8bc066687b4a19",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"suggest": {
"ext-ctype": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.9-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Ctype\\": ""
},
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
},
{
"name": "Gert de Pagter",
"email": "BackEndTea@gmail.com"
}
],
"description": "Symfony polyfill for ctype functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"ctype",
"polyfill",
"portable"
],
"time": "2018-08-06T14:22:27+00:00"
},
{
"name": "theseer/tokenizer",
@ -1731,20 +1848,21 @@
},
{
"name": "webmozart/assert",
"version": "1.3.0",
"version": "1.4.0",
"source": {
"type": "git",
"url": "https://github.com/webmozart/assert.git",
"reference": "0df1908962e7a3071564e857d86874dad1ef204a"
"reference": "83e253c8e0be5b0257b881e1827274667c5c17a9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/webmozart/assert/zipball/0df1908962e7a3071564e857d86874dad1ef204a",
"reference": "0df1908962e7a3071564e857d86874dad1ef204a",
"url": "https://api.github.com/repos/webmozart/assert/zipball/83e253c8e0be5b0257b881e1827274667c5c17a9",
"reference": "83e253c8e0be5b0257b881e1827274667c5c17a9",
"shasum": ""
},
"require": {
"php": "^5.3.3 || ^7.0"
"php": "^5.3.3 || ^7.0",
"symfony/polyfill-ctype": "^1.8"
},
"require-dev": {
"phpunit/phpunit": "^4.6",
@ -1777,7 +1895,7 @@
"check",
"validate"
],
"time": "2018-01-29T19:49:41+00:00"
"time": "2018-12-25T11:19:39+00:00"
}
],
"aliases": [],

1238
js/package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -43,21 +43,23 @@
"gulp-uglify": "^2.0.0",
"jasmine-core": "^2.99.1",
"jquery": "^2.2.4",
"jshint": "^2.9.6",
"jshint": "^2.10.1",
"karma": "^1.3.0",
"karma-chrome-launcher": "^2.0.0",
"karma-coverage": "^1.1.2",
"karma-firefox-launcher": "^1.0.0",
"karma-jasmine": "^1.1.2"
"karma-jasmine": "^1.1.2",
"natives": "^1.1.6",
"minimatch": "^3.0.4"
},
"dependencies": {
"angular": "^1.7.5",
"angular-animate": "^1.7.5",
"angular-mocks": "^1.7.5",
"angular-route": "^1.7.5",
"angular-sanitize": "^1.7.5",
"angular": "^1.7.7",
"angular-animate": "^1.7.7",
"angular-mocks": "^1.7.7",
"angular-route": "^1.7.7",
"angular-sanitize": "^1.7.7",
"debug": "^2.6.8",
"masonry-layout": "^4.2.2",
"moment": "^2.22.2"
"moment": "^2.24.0"
}
}

Просмотреть файл

@ -13,27 +13,46 @@
namespace OCA\News\AppInfo;
use Closure;
use FeedIo\FeedIo;
use HTMLPurifier;
use HTMLPurifier_Config;
use OCA\News\Config\FetcherConfig;
use OCA\News\Utility\PsrLogger;
use OCP\BackgroundJob\IJobList;
use OCP\IContainer;
use OCP\INavigationManager;
use OCP\IURLGenerator;
use OCP\IConfig;
use OCP\AppFramework\App;
use OCP\Files\IRootFolder;
use OCP\Files\Node;
use OCA\News\Config\AppConfig;
use OCA\News\Config\Config;
use OCA\News\Db\ItemMapper;
use OCA\News\Db\MapperFactory;
use OCA\News\Db\ItemMapper;
use OCA\News\Fetcher\FeedFetcher;
use OCA\News\Fetcher\Fetcher;
use OCA\News\Fetcher\YoutubeFetcher;
use OCA\News\Utility\ProxyConfigParser;
use OCP\AppFramework\App;
use OCP\Files\IRootFolder;
use OCP\Files\Node;
use OCP\IConfig;
use OCP\IContainer;
use OCP\ILogger;
use PicoFeed\Config\Config as PicoFeedConfig;
use PicoFeed\Reader\Reader as PicoFeedReader;
/**
* Class Application
*
* @package OCA\News\AppInfo
*/
class Application extends App
{
/**
* Application constructor.
*
* @param array $urlParams Parameters
*/
public function __construct(array $urlParams = [])
{
parent::__construct('news', $urlParams);
@ -53,10 +72,25 @@ class Application extends App
$container->registerParameter('configFile', 'config.ini');
// factories
$container->registerService(ItemMapper::class, function (IContainer $c) {
$container->registerService(ItemMapper::class, function (IContainer $c): ItemMapper {
return $c->query(MapperFactory::class)->build();
});
/**
* App config parser.
*/
$container->registerService(AppConfig::class, function (IContainer $c): AppConfig {
$config = new AppConfig(
$c->query(INavigationManager::class),
$c->query(IURLGenerator::class),
$c->query(IJobList::class)
);
$config->loadConfig($c->query('info'));
return $config;
});
/**
* Core
*/
@ -79,10 +113,21 @@ class Application extends App
}
});
/**
* Logger base
*/
$container->registerService(PsrLogger::class, function (IContainer $c): PsrLogger {
return new PsrLogger(
$c->query('ServerContainer')->getLogger(),
$c->query('AppName')
);
});
$container->registerService(Config::class, function (IContainer $c): Config {
$config = new Config(
$c->query('ConfigView'),
$c->query(ILogger::class),
$c->query(PsrLogger::class),
$c->query('LoggerParameters')
);
$config->read($c->query('configFile'), true);
@ -115,47 +160,26 @@ class Application extends App
/**
* Fetchers
*/
$container->registerService(PicoFeedConfig::class, function (IContainer $c): PicoFeedConfig {
$container->registerService(FetcherConfig::class, function (IContainer $c): FetcherConfig {
// FIXME: move this into a separate class for testing?
$config = $c->query(Config::class);
$proxy = $c->query(ProxyConfigParser::class);
$proxy = $c->query(ProxyConfigParser::class);
$userAgent = 'NextCloud-News/1.0';
$fConfig = new FetcherConfig();
$fConfig->setClientTimeout($config->getFeedFetcherTimeout());
$fConfig->setProxy($proxy);
$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;
return $fConfig;
});
$container->registerService(PicoFeedReader::class, function (IContainer $c): PicoFeedReader {
return new PicoFeedReader($c->query(PicoFeedConfig::class));
$container->registerService(FeedIo::class, function (IContainer $c): FeedIo {
$config = $c->query(FetcherConfig::class);
return new FeedIo($config->getClient(), $c->query(PsrLogger::class));
});
/**
* @noinspection PhpParamsInspection
*/
$container->registerService(Fetcher::class, function (IContainer $c): Fetcher {
$fetcher = new Fetcher();
@ -163,7 +187,6 @@ class Application extends App
// the last one
$fetcher->registerFetcher($c->query(YoutubeFetcher::class));
$fetcher->registerFetcher($c->query(FeedFetcher::class));
return $fetcher;
});
}

Просмотреть файл

@ -13,7 +13,7 @@
namespace OCA\News\Config;
use OCP\ILogger;
use OCA\News\Utility\PsrLogger;
use OCP\Files\Folder;
class Config
@ -35,7 +35,7 @@ class Config
public function __construct(
Folder $fileSystem,
ILogger $logger,
PsrLogger $logger,
$LoggerParameters
) {
$this->fileSystem = $fileSystem;

Просмотреть файл

@ -0,0 +1,118 @@
<?php
/**
* Nextcloud - 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 2012 Alessandro Cosentino
* @copyright 2012-2014 Bernhard Posselt
*/
namespace OCA\News\Config;
use FeedIo\Adapter\ClientInterface;
use \GuzzleHttp\Client;
use \FeedIo\Adapter\Guzzle\Client as FeedIoClient;
/**
* Class FetcherConfig
*
* @package OCA\News\Config
*/
class FetcherConfig
{
protected $client_timeout;
protected $proxy;
/**
* Configure a guzzle client
*
* @return ClientInterface Legacy client to guzzle.
*/
public function getClient()
{
if (!class_exists('GuzzleHttp\Collection')) {
$config = [
'timeout' => $this->getClientTimeout(),
];
if (!empty($this->proxy)) {
$config['proxy'] = $this->proxy;
}
$guzzle = new Client();
$client = new FeedIoClient($guzzle);
return $client;
}
$config = [
'request.options' => [
'timeout' => $this->getClientTimeout(),
],
];
if (!empty($this->proxy)) {
$config['request.options']['proxy'] = $this->proxy;
}
$guzzle = new Client($config);
return new LegacyGuzzleClient($guzzle);
}
/**
* Set a timeout for the client
*
* @param int $timeout The timeout
*
* @return self
*/
public function setClientTimeout($timeout)
{
$this->client_timeout = $timeout;
return $this;
}
/**
* Get the client timeout.
*
* @return mixed
*/
public function getClientTimeout()
{
return $this->client_timeout;
}
/**
* Set the proxy
*
* @param \OCA\News\Utility\ProxyConfigParser $proxy The proxy to set.
*
* @return self
*/
public function setProxy($proxy)
{
// proxy settings
$proxySettings = $proxy->parse();
$host = $proxySettings['host'];
$port = $proxySettings['port'];
$user = $proxySettings['user'];
$password = $proxySettings['password'];
$proxy_string = 'https://';
if (!empty($user)) {
$proxy_string .= $user . ':' . $password . '@';
}
$proxy_string .= $host;
if (!empty($port)) {
$proxy_string .= ':' . $port;
}
$this->proxy = $proxy_string;
return $this;
}
}

Просмотреть файл

@ -0,0 +1,65 @@
<?php
/**
* Nextcloud - News
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Sean Molenaar <smillernl@me.com>
* @copyright 2018 Sean Molenaar
*/
namespace OCA\News\Config;
use FeedIo\Adapter\ClientInterface as FeedIoClientInterface;
use FeedIo\Adapter\NotFoundException;
use FeedIo\Adapter\ServerErrorException;
use Guzzle\Service\ClientInterface;
use GuzzleHttp\Exception\BadResponseException;
/**
* Guzzle dependent HTTP client
*/
class LegacyGuzzleClient implements FeedIoClientInterface
{
/**
* @var ClientInterface
*/
protected $guzzleClient;
/**
* @param ClientInterface $guzzleClient
*/
public function __construct(ClientInterface $guzzleClient)
{
$this->guzzleClient = $guzzleClient;
}
/**
* @param string $url
* @param \DateTime $modifiedSince
* @throws \FeedIo\Adapter\NotFoundException
* @throws \FeedIo\Adapter\ServerErrorException
* @return \FeedIo\Adapter\ResponseInterface
*/
public function getResponse($url, \DateTime $modifiedSince)
{
try {
$options = [
'headers' => [
'User-Agent' => 'NextCloud-News/1.0',
'If-Modified-Since' => $modifiedSince->format(\DateTime::RFC2822)
]
];
return new LegacyGuzzleResponse($this->guzzleClient->get($url, $options));
} catch (BadResponseException $e) {
switch ((int) $e->getResponse()->getStatusCode()) {
case 404:
throw new NotFoundException($e->getMessage());
default:
throw new ServerErrorException($e->getMessage());
}
}
}
}

Просмотреть файл

@ -0,0 +1,86 @@
<?php
/**
* Nextcloud - News
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Sean Molenaar <smillernl@me.com>
* @copyright 2018 Sean Molenaar
*/
namespace OCA\News\Config;
use FeedIo\Adapter\ResponseInterface;
use GuzzleHttp\Message\ResponseInterface as GuzzleResponseInterface;
/**
* Guzzle dependent HTTP Response
*/
class LegacyGuzzleResponse implements ResponseInterface
{
const HTTP_LAST_MODIFIED = 'Last-Modified';
/**
* @var \GuzzleHttp\Message\ResponseInterface
*/
protected $response;
/**
* @param \GuzzleHttp\Message\ResponseInterface
*/
public function __construct(GuzzleResponseInterface $psrResponse)
{
$this->response = $psrResponse;
}
/**
* @return boolean
*/
public function isModified()
{
return $this->response->getStatusCode() !== 304 && $this->response->getBody()->getSize() > 0;
}
/**
* @return \Psr\Http\Message\StreamInterface
*/
public function getBody()
{
return $this->response->getBody();
}
/**
* @return \DateTime|null
*/
public function getLastModified()
{
if ($this->response->hasHeader(static::HTTP_LAST_MODIFIED)) {
$lastModified = \DateTime::createFromFormat(
\DateTime::RFC2822,
$this->getHeader(static::HTTP_LAST_MODIFIED)
);
return false === $lastModified ? null : $lastModified;
}
return;
}
/**
* @return array
*/
public function getHeaders()
{
return $this->response->getHeaders();
}
/**
* @param string $name
* @return string[]
*/
public function getHeader($name)
{
return $this->response->getHeader($name);
}
}

Просмотреть файл

@ -491,4 +491,18 @@ class Item extends Entity implements IAPI, \JsonSerializable
$this->getEnclosureLink()
);
}
/**
* Check if a given mimetype is supported
*
* @param string $mime mimetype to check
*
* @return boolean
*/
public function isSupportedMime($mime)
{
return (
stripos($mime, 'audio/') !== false ||
stripos($mime, 'video/') !== false);
}
}

414
lib/Fetcher/FeedFetcher.php Normal file → Executable file
Просмотреть файл

@ -13,29 +13,17 @@
namespace OCA\News\Fetcher;
use Exception;
use OCA\News\PostProcessor\LWNProcessor;
use OCP\Http\Client\IClientService;
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 DateTime;
use Favicon\Favicon;
use FeedIo\Feed\ItemInterface;
use FeedIo\FeedInterface;
use FeedIo\FeedIo;
use OCA\News\Utility\PsrLogger;
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
@ -45,27 +33,26 @@ class FeedFetcher implements IFeedFetcher
private $reader;
private $l10n;
private $time;
private $clientService;
private $logger;
public function __construct(
Reader $reader,
PicoFeedFaviconFactory $faviconFactory,
IL10N $l10n,
Time $time,
IClientService $clientService
) {
$this->faviconFactory = $faviconFactory;
$this->reader = $reader;
$this->time = $time;
$this->l10n = $l10n;
$this->clientService = $clientService;
public function __construct(FeedIo $fetcher, Favicon $favicon, IL10N $l10n, Time $time, PsrLogger $logger)
{
$this->reader = $fetcher;
$this->faviconFactory = $favicon;
$this->l10n = $l10n;
$this->time = $time;
$this->logger = $logger;
}
/**
* This fetcher handles all the remaining urls therefore always returns true
* This fetcher handles all the remaining urls therefore always returns true.
*
* @param string $url The URL to check
*
* @return bool
*/
public function canHandle($url)
public function canHandle($url): bool
{
return true;
}
@ -74,177 +61,56 @@ class FeedFetcher implements IFeedFetcher
/**
* 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 more 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 empty
*
* @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
* @inheritdoc
*/
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();
$parser->getItemPostProcessor()->register(
new LWNProcessor(
$basicAuthUser,
$basicAuthPassword,
$this->clientService
)
);
}
$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)
public function fetch(string $url, bool $favicon, $lastModified, $user, $password): array
{
$msg = $ex->getMessage();
if ($ex instanceof MalFormedXmlException) {
$msg = $this->l10n->t('Feed contains invalid XML');
} elseif ($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
);
} elseif ($ex instanceof UnsupportedFeedFormatException) {
$msg = $this->l10n->t('Detected feed format is not supported');
} elseif ($ex instanceof InvalidCertificateException) {
$msg = $this->buildCurlSslErrorMessage($ex->getCode());
} elseif ($ex instanceof InvalidUrlException) {
$msg = $this->l10n->t('Website not found');
} elseif ($ex instanceof MaxRedirectException) {
$msg = $this->l10n->t('More redirects than allowed, aborting');
} elseif ($ex instanceof MaxSizeException) {
$msg = $this->l10n->t('Bigger than maximum allowed size');
} elseif ($ex instanceof TimeoutException) {
$msg = $this->l10n->t('Request timed out');
} elseif ($ex instanceof UnauthorizedException) {
$msg = $this->l10n->t(
'Required credentials for feed were ' .
'either missing or incorrect'
);
} elseif ($ex instanceof ForbiddenException) {
$msg = $this->l10n->t('Forbidden to access feed');
if (!empty($user) && !empty(trim($user))) {
$url = explode('://', $url);
$url = $url[0] . '://' . $user . ':' . $password . '@' . $url[1];
}
if (is_null($lastModified) || !is_string($lastModified)) {
$resource = $this->reader->read($url);
} else {
$resource = $this->reader->readSince($url, new DateTime($lastModified));
}
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!');
$response = $resource->getResponse();
if (!$response->isModified()) {
$this->logger->debug('Feed {url} was not modified since last fetch. old: {old}, new: {new}', [
'url' => $url,
'old' => print_r($lastModified, true),
'new' => print_r($response->getLastModified(), true),
]);
return [null, []];
}
$location = $resource->getUrl();
$parsedFeed = $resource->getFeed();
$feed = $this->buildFeed(
$parsedFeed,
$url,
$favicon,
$location
);
$items = [];
$this->logger->debug('Feed ' . $url . ' was modified since last fetch. #' . count($parsedFeed) . ' items');
foreach ($parsedFeed as $item) {
$items[] = $this->buildItem($item, $parsedFeed);
}
return [$feed, $items];
}
private function decodeTwice($string)
/**
* Decode the string twice
*
* @param string $string String to decode
*
* @return string
*/
private function decodeTwice($string): string
{
return html_entity_decode(
html_entity_decode(
@ -257,37 +123,79 @@ class FeedFetcher implements IFeedFetcher
);
}
protected function determineRtl($parsedItem, $parsedFeed)
/**
* Check if a feed is RTL or not
*
* @param FeedInterface $parsedFeed The feed that was parsed
*
* @return bool
*/
protected function determineRtl(FeedInterface $parsedFeed): bool
{
$itemLang = $parsedItem->getLanguage();
$feedLang = $parsedFeed->getLanguage();
$language = $parsedFeed->getLanguage();
if ($itemLang) {
return Parser::isLanguageRTL($itemLang);
} else {
return Parser::isLanguageRTL($feedLang);
$language = strtolower($language);
$rtl_languages = array(
'ar', // Arabic (ar-**)
'fa', // Farsi (fa-**)
'ur', // Urdu (ur-**)
'ps', // Pashtu (ps-**)
'syr', // Syriac (syr-**)
'dv', // Divehi (dv-**)
'he', // Hebrew (he-**)
'yi', // Yiddish (yi-**)
);
foreach ($rtl_languages as $prefix) {
if (strpos($language, $prefix) === 0) {
return true;
}
}
return false;
}
protected function buildItem($parsedItem, $parsedFeed)
/**
* Build an item based on a feed.
*
* @param ItemInterface $parsedItem The item to use
* @param FeedInterface $parsedFeed The feed to use
*
* @return Item
*/
protected function buildItem(ItemInterface $parsedItem, FeedInterface $parsedFeed): Item
{
$item = new Item();
$item->setUnread(true);
$item->setUrl($parsedItem->getUrl());
$item->setGuid($parsedItem->getId());
$item->setGuidHash($item->getGuid());
$item->setPubDate($parsedItem->getPublishedDate()->getTimestamp());
$item->setUpdatedDate($parsedItem->getUpdatedDate()->getTimestamp());
$item->setRtl($this->determineRtl($parsedItem, $parsedFeed));
$item->setUrl($parsedItem->getLink());
$item->setGuid($parsedItem->getPublicId());
$item->setGuidHash(md5($item->getGuid()));
$lastmodified = $parsedItem->getLastModified() ?? new \DateTime();
if ($parsedItem->getValue('pubDate') !== null) {
$pubDT = new DateTime($parsedItem->getValue('pubDate'));
} elseif ($parsedItem->getValue('published') !== null) {
$pubDT = new DateTime($parsedItem->getValue('published'));
} else {
$pubDT = $lastmodified;
}
$item->setPubDate(
$pubDT->getTimestamp()
);
$item->setLastModified(
$lastmodified->getTimestamp()
);
$item->setRtl($this->determineRtl($parsedFeed));
// unescape content because angularjs helps against XSS
$item->setTitle($this->decodeTwice($parsedItem->getTitle()));
$item->setAuthor($this->decodeTwice($parsedItem->getAuthor()));
$author = $parsedItem->getAuthor();
if (!is_null($author)) {
$item->setAuthor($this->decodeTwice($author->getName()));
}
// purification is done in the service layer
$body = $parsedItem->getContent();
$body = $parsedItem->getDescription();
$body = mb_convert_encoding(
$body,
'HTML-ENTITIES',
@ -295,55 +203,57 @@ class FeedFetcher implements IFeedFetcher
);
$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);
if ($parsedItem->hasMedia()) {
// TODO: Fix multiple media support
foreach ($parsedItem->getMedias() as $media) {
if (!$item->isSupportedMime($media->getType())) {
continue;
}
$item->setEnclosureMime($media->getType());
$item->setEnclosureLink($media->getUrl());
}
}
$item->generateSearchIndex();
$this->logger->debug('Added item {title} for feed {feed} publishdate: {datetime}', [
'title' => $item->getTitle(),
'feed' => $parsedFeed->getTitle(),
'datetime' => $item->getLastModified(),
]);
return $item;
}
protected function buildFeed(
$parsedFeed,
$url,
$getFavicon,
$modified,
$etag,
$location
) {
$feed = new Feed();
$link = $parsedFeed->getSiteUrl();
if (!$link) {
$link = $location;
}
/**
* Build a feed based on provided info
*
* @param FeedInterface $feed Feed to build from
* @param string $url URL to use
* @param boolean $getFavicon To get the favicon
* @param string $location String base URL
*
* @return Feed
*/
protected function buildFeed(FeedInterface $feed, string $url, bool $getFavicon, string $location): Feed
{
$newFeed = new Feed();
// 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());
$title = strip_tags($this->decodeTwice($feed->getTitle()));
$newFeed->setTitle($title);
$newFeed->setUrl($url); // the url used to add the feed
$newFeed->setLocation($location); // the url where the feed was found
$newFeed->setLink($feed->getLink()); // <link> attribute in the feed
$lastmodified = $feed->getLastModified() ?? new DateTime();
$newFeed->setLastModified($lastmodified->getTimestamp());
$newFeed->setAdded($this->time->getTime());
if ($getFavicon) {
$faviconFetcher = $this->faviconFactory->build();
$favicon = $faviconFetcher->find($feed->getLink());
$feed->setFaviconLink($favicon);
if (!$getFavicon) {
return $newFeed;
}
$favicon = $this->faviconFactory->get($url);
$newFeed->setFaviconLink($favicon);
return $feed;
return $newFeed;
}
}

Просмотреть файл

@ -16,6 +16,10 @@ namespace OCA\News\Fetcher;
class Fetcher
{
/**
* List of fetchers.
* @var IFeedFetcher[]
*/
private $fetchers;
public function __construct()
@ -39,39 +43,28 @@ class Fetcher
*
* @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.
* @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 more 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 empty
* @param string $user if given, basic auth is set for this feed
* @param string $password if given, basic auth is set for this feed. Ignored if user is empty
*
* @throws FetcherException if simple pie fails
* @throws FetcherException if FeedIO 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
) {
public function fetch($url, $getFavicon = true, $lastModified = null, $user = null, $password = null)
{
foreach ($this->fetchers as $fetcher) {
if ($fetcher->canHandle($url)) {
return $fetcher->fetch(
$url,
$getFavicon,
$lastModified,
$etag,
$fullTextEnabled,
$basicAuthUser,
$basicAuthPassword
);
if (!$fetcher->canHandle($url)) {
continue;
}
return $fetcher->fetch(
$url,
$getFavicon,
$lastModified,
$user,
$password
);
}
return [null, []];

Просмотреть файл

@ -19,29 +19,18 @@ interface IFeedFetcher
/**
* Fetch feed content.
*
* @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.
* @param string $url remote url of the feed
* @param boolean $favicon if the favicon should also be fetched, defaults to true
* @param string|null $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 more 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 empty
* @param string|null $user if given, basic auth is set for this feed
* @param string|null $password if given, basic auth is set for this feed. Ignored if user is empty
*
* @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
* @throws FetcherException if the fetcher encounters a problem
*/
public function fetch(
$url,
$getFavicon = true,
$lastModified = null,
$etag = null,
$fullTextEnabled = false,
$basicAuthUser = null,
$basicAuthPassword = null
);
public function fetch(string $url, bool $favicon, $lastModified, $user, $password): array;
/**
* Can a fetcher handle a feed.
@ -51,5 +40,5 @@ interface IFeedFetcher
* @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
*/
public function canHandle($url);
public function canHandle($url): bool;
}

Просмотреть файл

@ -39,7 +39,7 @@ class YoutubeFetcher implements IFeedFetcher
/**
* This fetcher handles all the remaining urls therefore always returns true
*/
public function canHandle($url)
public function canHandle($url): bool
{
return $this->buildUrl($url) !== $url;
}
@ -48,39 +48,18 @@ class YoutubeFetcher implements IFeedFetcher
/**
* 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 more 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 empty
*
* @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
* @inheritdoc
*/
public function fetch(
$url,
$getFavicon = true,
$lastModified = null,
$etag = null,
$fullTextEnabled = false,
$basicAuthUser = null,
$basicAuthPassword = null
) {
public function fetch(string $url, bool $favicon, $lastModified, $user, $password): array
{
$transformedUrl = $this->buildUrl($url);
$result = $this->feedFetcher->fetch(
$transformedUrl,
$getFavicon,
$favicon,
$lastModified,
$etag,
$fullTextEnabled,
$basicAuthUser,
$basicAuthPassword
$user,
$password
);
// reset feed url so we know the correct added url for the feed

Просмотреть файл

@ -1,117 +0,0 @@
<?php
/**
* Nextcloud - News
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Robin Appelman <robin@icewind.nl>
*/
namespace OCA\News\PostProcessor;
use GuzzleHttp\Cookie\CookieJar;
use OCP\Http\Client\IClientService;
use PicoFeed\Parser\Feed;
use PicoFeed\Parser\Item;
use PicoFeed\Processor\ItemProcessorInterface;
use PicoFeed\Scraper\RuleParser;
class LWNProcessor implements ItemProcessorInterface
{
private $user;
private $password;
private $clientService;
private $cookieJar;
/**
* @param $user
* @param $password
*/
public function __construct($user, $password, IClientService $clientService)
{
$this->user = $user;
$this->password = $password;
$this->clientService = $clientService;
$this->cookieJar = new CookieJar();
}
private function login()
{
if ($this->cookieJar->count() > 0) {
return true;
}
if (!$this->user || !$this->password) {
return false;
}
$client = $this->clientService->newClient();
$response = $client->post(
'https://lwn.net/login',
[
'cookies' => $this->cookieJar,
'body' => [
'Username' => $this->user,
'Password' => $this->password,
'target' => '/'
]
]
);
return ($response->getStatusCode() === 200 && $this->cookieJar->count() > 0);
}
private function getBody($url)
{
$client = $this->clientService->newClient();
$response = $client->get(
$url,
[
'cookies' => $this->cookieJar
]
);
$parser = new RuleParser(
$response->getBody(),
[
'body' => array(
'//div[@class="ArticleText"]',
),
'strip' => array(
'//div[@class="FeatureByline"]'
)
]
);
$articleBody = $parser->execute();
// make all links absolute
return str_replace('href="/', 'href="https://lwn.net/', $articleBody);
}
private function canHandle($url)
{
$regex = '%(?:https?://|//)?(?:www.)?lwn.net%';
return (bool)preg_match($regex, $url);
}
/**
* Execute Item Processor
*
* @access public
* @param Feed $feed
* @param Item $item
* @return bool
*/
public function execute(Feed $feed, Item $item)
{
if ($this->canHandle($item->getUrl())) {
$loggedIn = $this->login();
$item->setUrl(str_replace('/rss', '', $item->getUrl()));
if ($loggedIn) {
$item->setContent($this->getBody($item->getUrl()));
}
}
}
}

Просмотреть файл

@ -58,8 +58,8 @@ class FeedService extends Service
$this->logger = $logger;
$this->l10n = $l10n;
$this->timeFactory = $timeFactory;
$this->autoPurgeMinimumInterval =
$config->getAutoPurgeMinimumInterval();
$this->autoPurgeMinimumInterval = $config->getAutoPurgeMinimumInterval(
);
$this->purifier = $purifier;
$this->feedMapper = $feedMapper;
$this->loggerParams = $LoggerParameters;
@ -69,6 +69,7 @@ class FeedService extends Service
* Finds all feeds of a user
*
* @param string $userId the name of the user
*
* @return Feed[]
*/
public function findAll($userId)
@ -96,57 +97,43 @@ class FeedService extends Service
* 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
* @param string $user if given, basic auth is set for this feed
* @param string $password 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
) {
public function create($feedUrl, $folderId, $userId, $title = null, $user = null, $password = null)
{
// first try if the feed exists already
try {
/**
* @var Feed $feed
* @var Feed $feed
* @var Item[] $items
*/
list($feed, $items) = $this->feedFetcher->fetch(
$feedUrl,
true,
null,
null,
false,
$basicAuthUser,
$basicAuthPassword
);
list($feed, $items) = $this->feedFetcher->fetch($feedUrl, true, null, $user, $password);
// try again if feed exists depending on the reported link
try {
$this->feedMapper->findByUrlHash($feed->getUrlHash(), $userId);
$hash = $feed->getUrlHash();
$this->feedMapper->findByUrlHash($hash, $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) {
// If no matching feed was found everything was ok
}
// insert feed
$itemCount = count($items);
$feed->setBasicAuthUser($basicAuthUser);
$feed->setBasicAuthPassword($basicAuthPassword);
$feed->setBasicAuthUser($user);
$feed->setBasicAuthPassword($password);
$feed->setFolderId($folderId);
$feed->setUserId($userId);
$feed->setArticlesPerUpdate($itemCount);
if ($title !== null && $title !== '') {
if (!empty($title)) {
$feed->setTitle($title);
}
@ -155,8 +142,7 @@ class FeedService extends Service
// 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];
foreach (array_reverse($items) as $item) {
$item->setFeedId($feed->getId());
// check if item exists (guidhash is the same)
@ -213,6 +199,7 @@ class FeedService extends Service
* @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
*/
@ -237,8 +224,6 @@ class FeedService extends Service
$location,
false,
$existingFeed->getHttpLastModified(),
$existingFeed->getHttpEtag(),
$existingFeed->getFullTextEnabled(),
$existingFeed->getBasicAuthUser(),
$existingFeed->getBasicAuthPassword()
);
@ -332,6 +317,7 @@ class FeedService extends Service
*
* @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)
@ -406,6 +392,7 @@ class FeedService extends Service
*
* @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)
@ -421,6 +408,7 @@ class FeedService extends Service
*
* @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)
@ -471,13 +459,14 @@ class FeedService extends Service
* @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'
* ]
* [
* 'ordering' => 1,
* 'fullTextEnabled' => true,
* 'pinned' => true,
* 'updateMode' => 0,
* 'title' => 'title'
* ]
*
* @throws ServiceNotFoundException if feed does not exist
*/
public function patch($feedId, $userId, $diff = [])

Просмотреть файл

@ -1,42 +0,0 @@
<?php
/**
* Nextcloud - 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 2012 Alessandro Cosentino
* @copyright 2012-2014 Bernhard Posselt
*/
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;
}
}

Просмотреть файл

@ -1,40 +0,0 @@
<?php
/**
* Nextcloud - 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 2012 Alessandro Cosentino
* @copyright 2012-2014 Bernhard Posselt
*/
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);
}
}

97
lib/Utility/PsrLogger.php Normal file
Просмотреть файл

@ -0,0 +1,97 @@
<?php
/**
* Nextcloud - News
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Sean Molenaar <sean@seanmolenaar.eu>
* @copyright 2018 Sean Molenaar
*/
namespace OCA\News\Utility;
use \OCP\ILogger;
/**
* This is a wrapper to make OC\Log conform to Psr\Log\LoggerInterface
*
* @package OCA\News\Utility
*/
class PsrLogger implements \Psr\Log\LoggerInterface
{
private $logger;
private $appName;
/**
* PsrLogger constructor.
*
* @param ILogger $logger The logger
* @param string $appName Name of the app
*/
public function __construct(ILogger $logger, $appName)
{
$this->logger = $logger;
$this->appName = $appName;
}
public function logException($exception, array $context = [])
{
$context['app'] = $this->appName;
$this->logger->logException($exception, $context);
}
public function emergency($message, array $context = [])
{
$context['app'] = $this->appName;
$this->logger->emergency($message, $context);
}
public function alert($message, array $context = [])
{
$context['app'] = $this->appName;
$this->logger->alert($message, $context);
}
public function critical($message, array $context = [])
{
$context['app'] = $this->appName;
$this->logger->critical($message, $context);
}
public function error($message, array $context = [])
{
$context['app'] = $this->appName;
$this->logger->error($message, $context);
}
public function warning($message, array $context = [])
{
$context['app'] = $this->appName;
$this->logger->warning($message, $context);
}
public function notice($message, array $context = [])
{
$context['app'] = $this->appName;
$this->logger->notice($message, $context);
}
public function info($message, array $context = [])
{
$context['app'] = $this->appName;
$this->logger->info($message, $context);
}
public function debug($message, array $context = [])
{
$context['app'] = $this->appName;
$this->logger->debug($message, $context);
}
public function log($level, $message, array $context = [])
{
$context['app'] = $this->appName;
$this->logger->log($level, $message, $context);
}
}

3
package-lock.json сгенерированный Normal file
Просмотреть файл

@ -0,0 +1,3 @@
{
"lockfileVersion": 1
}

Просмотреть файл

@ -29,7 +29,7 @@ class ConfigTest extends TestCase
public function setUp()
{
$this->logger = $this->getMockBuilder(ILogger::class)
$this->logger = $this->getMockBuilder('OCA\News\Utility\PsrLogger')
->disableOriginalConstructor()
->getMock();
$this->fileSystem = $this->getMockBuilder(Folder::class)->getMock();

Просмотреть файл

@ -13,13 +13,20 @@
namespace OCA\News\Tests\Unit\Fetcher;
use \OCA\News\Db\Item;
use FeedIo\Feed\Item\Author;
use FeedIo\Feed\Item\MediaInterface;
use FeedIo\Feed\ItemInterface;
use FeedIo\FeedInterface;
use OC\L10N\L10N;
use OCA\AdminAudit\Actions\Auth;
use \OCA\News\Db\Feed;
use \OCA\News\Db\Item;
use OCA\News\Fetcher\FeedFetcher;
use OCA\News\Utility\PicoFeedFaviconFactory;
use OCA\News\Utility\PsrLogger;
use OCA\News\Utility\Time;
use OCP\Http\Client\IClientService;
use OCP\IL10N;
use PHPUnit\Framework\TestCase;
use PicoFeed\Client\Client;
use PicoFeed\Parser\Parser;
@ -27,121 +34,175 @@ use PicoFeed\Processor\ItemPostProcessor;
use PicoFeed\Reader\Favicon;
use PicoFeed\Reader\Reader;
/**
* Class FeedFetcherTest
*
* @package OCA\News\Tests\Unit\Fetcher
*/
class FeedFetcherTest extends TestCase
{
/**
* The class to test
*
* @var FeedFetcher
*/
private $fetcher;
private $parser;
/**
* Feed reader
*
* @var \FeedIo\FeedIo
*/
private $reader;
private $client;
private $faviconFetcher;
private $parsedFeed;
private $faviconFactory;
/**
* Feed reader result
*
* @var \FeedIo\Reader\Result
*/
private $result;
/**
* Feed reader result object
*
* @var \FeedIo\Adapter\ResponseInterface
*/
private $response;
/**
* @var \Favicon\Favicon
*/
private $favicon;
/**
* @var L10N
*/
private $l10n;
private $url;
/**
* @var ItemInterface
*/
private $item_mock;
/**
* @var FeedInterface
*/
private $feed_mock;
/**
* @var PsrLogger
*/
private $logger;
//metadata
/**
* @var integer
*/
private $time;
private $item;
private $content;
/**
* @var string
*/
private $encoding;
/**
* @var string
*/
private $url;
// items
private $permalink;
private $title;
private $guid;
private $guid_hash;
private $pub;
private $updated;
private $body;
/**
* @var Author
*/
private $author;
private $enclosureLink;
private $enclosure;
private $rtl;
private $language;
// feed
private $feedTitle;
private $feedLink;
private $feedImage;
private $webFavicon;
private $feed_title;
private $feed_link;
private $feed_image;
private $web_favicon;
private $modified;
private $etag;
private $location;
private $feedLanguage;
protected function setUp()
{
$this->l10n = $this->getMockBuilder(IL10N::class)
$this->l10n = $this->getMockBuilder(\OCP\IL10N::class)
->disableOriginalConstructor()
->getMock();
$this->reader = $this->getMockBuilder(Reader::class)
$this->reader = $this->getMockBuilder(\FeedIo\FeedIo::class)
->disableOriginalConstructor()
->getMock();
$this->parser = $this->getMockBuilder(Parser::class)
$this->favicon = $this->getMockBuilder(\Favicon\Favicon::class)
->disableOriginalConstructor()
->getMock();
$this->client = $this->getMockBuilder(Client::class)
$this->result = $this->getMockBuilder(\FeedIo\Reader\Result::class)
->disableOriginalConstructor()
->getMock();
$this->parsedFeed = $this->getMockBuilder(\PicoFeed\Parser\Feed::class)
$this->response = $this->getMockBuilder(\FeedIo\Adapter\ResponseInterface::class)
->disableOriginalConstructor()
->getMock();
$this->item = $this->getMockBuilder(\PicoFeed\Parser\Item::class)
$this->item_mock = $this->getMockBuilder(\FeedIo\Feed\ItemInterface::class)
->disableOriginalConstructor()
->getMock();
$this->faviconFetcher = $this->getMockBuilder(Favicon::class)
->disableOriginalConstructor()
->getMock();
$this->faviconFactory = $this->getMockBuilder(PicoFeedFaviconFactory::class)
$this->feed_mock = $this->getMockBuilder(\FeedIo\FeedInterface::class)
->disableOriginalConstructor()
->getMock();
$this->time = 2323;
$timeFactory = $this->getMockBuilder(Time::class)
$timeFactory = $this->getMockBuilder(\OCA\News\Utility\Time::class)
->disableOriginalConstructor()
->getMock();
$timeFactory->expects($this->any())
->method('getTime')
->will($this->returnValue($this->time));
$postProcessor = $this->getMockBuilder(ItemPostProcessor::class)
->getMock();
$this->parser->expects($this->any())
->method('getItemPostProcessor')
->will($this->returnValue($postProcessor));
$clientService = $this->getMockBuilder(IClientService::class)
$this->logger = $this->getMockBuilder(PsrLogger::class)
->disableOriginalConstructor()
->getMock();
$this->fetcher = new FeedFetcher(
$this->reader,
$this->faviconFactory,
$this->favicon,
$this->l10n,
$timeFactory,
$clientService
$this->logger
);
$this->url = 'http://tests';
$this->url = 'http://tests';
$this->permalink = 'http://permalink';
$this->title = 'my&amp;lt;&apos; title';
$this->guid = 'hey guid here';
$this->body = 'let the bodies hit the floor <a href="test">test</a>';
$this->body2 = 'let the bodies hit the floor ' .
'<a target="_blank" href="test">test</a>';
$this->pub = 23111;
$this->updated = 23444;
$this->author = '&lt;boogieman';
$this->enclosureLink = 'http://enclosure.you';
$this->title = 'my&amp;lt;&apos; title';
$this->guid = 'hey guid here';
$this->guid_hash = 'df9a5f84e44bfe38cf44f6070d5b0250';
$this->body = 'let the bodies hit the floor <a href="test">test</a>';
$this->pub = 23111;
$this->updated = 23444;
$this->author = new Author();
$this->author->setName('&lt;boogieman');
$this->enclosure = 'http://enclosure.you';
$this->feedTitle = '&lt;a&gt;&amp;its a&lt;/a&gt; title';
$this->feedLink = 'http://goatse';
$this->feedImage = '/an/image';
$this->webFavicon = 'http://anon.google.com';
$this->authorMail = 'doe@joes.com';
$this->modified = 3;
$this->etag = 'yo';
$this->content = 'some content';
$this->encoding = 'UTF-8';
$this->language = 'de-DE';
$this->feedLanguage = 'de-DE';
$this->feed_title = '&lt;a&gt;&amp;its a&lt;/a&gt; title';
$this->feed_link = 'http://tests';
$this->feed_image = '/an/image';
$this->web_favicon = 'http://anon.google.com';
$this->modified = $this->getMockBuilder('\DateTime')->getMock();
$this->modified->expects($this->any())
->method('getTimestamp')
->will($this->returnValue(3));
$this->encoding = 'UTF-8';
$this->language = 'de-DE';
$this->rtl = false;
}
public function testCanHandle()
{
$url = 'google.de';
@ -149,163 +210,26 @@ class FeedFetcherTest extends TestCase
$this->assertTrue($this->fetcher->canHandle($url));
}
private function setUpReader($url='', $modified=true, $noParser=false)
{
$this->reader->expects($this->once())
->method('discover')
->with($this->equalTo($url))
->will($this->returnValue($this->client));
$this->client->expects($this->once())
->method('isModified')
->will($this->returnValue($modified));
if (!$modified) {
$this->reader->expects($this->never())
->method('getParser');
} else {
$this->client->expects($this->once())
->method('getLastModified')
->will($this->returnValue($this->modified));
$this->client->expects($this->once())
->method('getEtag')
->will($this->returnValue($this->etag));
$this->client->expects($this->once())
->method('getUrl')
->will($this->returnValue($this->location));
$this->client->expects($this->once())
->method('getContent')
->will($this->returnValue($this->content));
$this->client->expects($this->once())
->method('getEncoding')
->will($this->returnValue($this->encoding));
if ($noParser) {
$this->reader->expects($this->once())
->method('getParser')
->will(
$this->throwException(
new \PicoFeed\Reader\SubscriptionNotFoundException()
)
);
} else {
$this->reader->expects($this->once())
->method('getParser')
->with(
$this->equalTo($this->location),
$this->equalTo($this->content),
$this->equalTo($this->encoding)
)
->will($this->returnValue($this->parser));
}
$this->parser->expects($this->once())
->method('execute')
->will($this->returnValue($this->parsedFeed));
}
}
private function expectFeed($method, $return, $count = 1)
{
$this->parsedFeed->expects($this->exactly($count))
->method($method)
->will($this->returnValue($return));
}
private function expectItem($method, $return, $count = 1)
{
$this->item->expects($this->exactly($count))
->method($method)
->will($this->returnValue($return));
}
private function createItem($enclosureType=null)
{
$this->expectItem('getUrl', $this->permalink);
$this->expectItem('getTitle', $this->title);
$this->expectItem('getId', $this->guid);
$this->expectItem('getContent', $this->body);
$item = new Item();
date_default_timezone_set('America/Los_Angeles');
$pubdate = \Datetime::createFromFormat('U', $this->pub);
$this->expectItem('getPublishedDate', $pubdate);
$item->setPubDate($this->pub);
$update = \Datetime::createFromFormat('U', $this->updated);
$this->expectItem('getUpdatedDate', $update);
$item->setUpdatedDate($this->updated);
$item->setStatus(0);
$item->setUnread(true);
$item->setUrl($this->permalink);
$item->setTitle('my<\' title');
$item->setGuid($this->guid);
$item->setGuidHash($this->guid);
$item->setBody($this->body);
$item->setRtl(false);
$this->expectItem('getAuthor', $this->author);
$item->setAuthor(html_entity_decode($this->author));
if($enclosureType === 'audio/ogg' || $enclosureType === 'video/ogg') {
$this->expectItem('getEnclosureUrl', $this->enclosureLink);
$this->expectItem('getEnclosureType', $enclosureType);
$item->setEnclosureMime($enclosureType);
$item->setEnclosureLink($this->enclosureLink);
}
$item->generateSearchIndex();
return $item;
}
private function createFeed($hasFavicon=false)
{
$this->expectFeed('getTitle', $this->feedTitle);
$this->expectFeed('getSiteUrl', $this->feedLink);
$feed = new Feed();
$feed->setTitle('&its a title');
$feed->setUrl($this->url);
$feed->setLink($this->feedLink);
$feed->setAdded($this->time);
$feed->setHttpLastModified($this->modified);
$feed->setHttpEtag($this->etag);
$feed->setLocation($this->location);
if($hasFavicon) {
$this->faviconFactory->expects($this->once())
->method('build')
->will($this->returnValue($this->faviconFetcher));
$this->faviconFetcher->expects($this->once())
->method('find')
->with($this->equalTo($this->feedLink))
->will($this->returnValue($this->webFavicon));
$feed->setFaviconLink($this->webFavicon);
}
return $feed;
}
/**
* Test if empty is logged when the feed remain the same.
*/
public function testNoFetchIfNotModified()
{
$this->setUpReader($this->url, false);;
$result = $this->fetcher->fetch($this->url, false);
$this->__setUpReader($this->url, false);
$this->logger->expects($this->once())
->method('debug')
->with('Feed {url} was not modified since last fetch. old: {old}, new: {new}');
$result = $this->fetcher->fetch($this->url, false, null, null, null);
$this->assertSame([null, []], $result);
}
public function testFetch()
{
$this->setUpReader($this->url);
$item = $this->createItem();
$feed = $this->createFeed();
$this->expectFeed('getItems', [$this->item]);
$result = $this->fetcher->fetch($this->url, false);
$this->__setUpReader($this->url);
$item = $this->_createItem();
$feed = $this->_createFeed();
$this->_mockIterator($this->feed_mock, [$this->item_mock]);
$result = $this->fetcher->fetch($this->url, false, null, null, null);
$this->assertEquals([$feed, [$item]], $result);
}
@ -313,11 +237,11 @@ class FeedFetcherTest extends TestCase
public function testAudioEnclosure()
{
$this->setUpReader($this->url);
$item = $this->createItem('audio/ogg');
$feed = $this->createFeed();
$this->expectFeed('getItems', [$this->item]);
$result = $this->fetcher->fetch($this->url, false);
$this->__setUpReader($this->url);
$item = $this->_createItem('audio/ogg');
$feed = $this->_createFeed();
$this->_mockIterator($this->feed_mock, [$this->item_mock]);
$result = $this->fetcher->fetch($this->url, false, null, null, null);
$this->assertEquals([$feed, [$item]], $result);
}
@ -325,105 +249,284 @@ class FeedFetcherTest extends TestCase
public function testVideoEnclosure()
{
$this->setUpReader($this->url);
$item = $this->createItem('video/ogg');
$feed = $this->createFeed();
$this->expectFeed('getItems', [$this->item]);
$result = $this->fetcher->fetch($this->url, false);
$this->__setUpReader($this->url);
$item = $this->_createItem('video/ogg');
$feed = $this->_createFeed();
$this->_mockIterator($this->feed_mock, [$this->item_mock]);
$result = $this->fetcher->fetch($this->url, false, null, null, null);
$this->assertEquals([$feed, [$item]], $result);
}
public function testFavicon()
{
$this->setUpReader($this->url);
$this->__setUpReader($this->url);
$feed = $this->createFeed(true);
$item = $this->createItem();
$this->expectFeed('getItems', [$this->item]);
$result = $this->fetcher->fetch($this->url);
$feed = $this->_createFeed('de-DE', true);
$item = $this->_createItem();
$this->_mockIterator($this->feed_mock, [$this->item_mock]);
$result = $this->fetcher->fetch($this->url, true, null, null, null);
$this->assertEquals([$feed, [$item]], $result);
}
public function testFullText()
{
$this->setUpReader($this->url);
$feed = $this->createFeed();
$item = $this->createItem();
$this->parser->expects($this->once())
->method('enableContentGrabber');
$this->expectFeed('getItems', [$this->item]);
$this->fetcher->fetch($this->url, false, null, null, true);
}
public function testNoFavicon()
{
$this->setUpReader($this->url);
$this->__setUpReader($this->url);
$feed = $this->createFeed(false);
$feed = $this->_createFeed(false);
$this->faviconFetcher->expects($this->never())
->method('find');
$this->favicon->expects($this->never())
->method('get');
$item = $this->createItem();
$this->expectFeed('getItems', [$this->item]);
$result = $this->fetcher->fetch($this->url, false);
$item = $this->_createItem();
$this->_mockIterator($this->feed_mock, [$this->item_mock]);
$result = $this->fetcher->fetch($this->url, false, null, null, null);
$this->assertEquals([$feed, [$item]], $result);
}
public function testRtl()
{
$this->setUpReader($this->url);
$this->expectFeed('getLanguage', 'he-IL');
$this->expectItem('getLanguage', '');
$feed = $this->createFeed();
$item = $this->createItem(null);
$this->expectFeed('getItems', [$this->item]);
list($feed, $items) = $this->fetcher->fetch(
$this->url, false, null,
null, true
);
$this->__setUpReader($this->url);
$this->_createFeed('he-IL');
$this->_createItem();
$this->_mockIterator($this->feed_mock, [$this->item_mock]);
list($feed, $items) = $this->fetcher->fetch($this->url, false, null, null, null);
$this->assertTrue($items[0]->getRtl());
}
public function testRtlItemPrecedence()
public function testRssPubDate()
{
$this->setUpReader($this->url);
$this->expectFeed('getLanguage', 'de-DE');
$this->expectItem('getLanguage', 'he-IL');
$this->__setUpReader($this->url);
$this->_createFeed('he-IL');
$this->_createItem();
$feed = $this->createFeed();
$item = $this->createItem(null);
$this->expectFeed('getItems', [$this->item]);
list($feed, $items) = $this->fetcher->fetch(
$this->url, false, null,
null, true
);
$this->assertTrue($items[0]->getRtl());
$this->item_mock->expects($this->exactly(2))
->method('getValue')
->will($this->returnValueMap([
['pubDate', '2018-03-27T19:50:29Z'],
['published', NULL],
]));
$this->_mockIterator($this->feed_mock, [$this->item_mock]);
list($feed, $items) = $this->fetcher->fetch($this->url, false, null, null, null);
$this->assertSame($items[0]->getPubDate(), 1522180229);
}
public function testNegativeRtlItemPrecedence()
public function testAtomPubDate()
{
$this->setUpReader($this->url);
$this->expectFeed('getLanguage', 'he-IL');
$this->expectItem('getLanguage', 'de-DE');
$this->__setUpReader($this->url);
$this->_createFeed('he-IL');
$this->_createItem();
$feed = $this->createFeed();
$item = $this->createItem(null);
$this->expectFeed('getItems', [$this->item]);
list($feed, $items) = $this->fetcher->fetch(
$this->url, false, null,
null, true
);
$this->assertFalse($items[0]->getRtl());
$this->item_mock->expects($this->exactly(3))
->method('getValue')
->will($this->returnValueMap([
['pubDate', NULL],
['published', '2018-02-27T19:50:29Z'],
]));
$this->_mockIterator($this->feed_mock, [$this->item_mock]);
list($feed, $items) = $this->fetcher->fetch($this->url, false, null, null, null);
$this->assertSame($items[0]->getPubDate(), 1519761029);
}
/**
* Mock an iteration option on an existing mock
*
* @param object $iteratorMock The mock to enhance
* @param array $items The items to make available
*
* @return mixed
*/
private function _mockIterator($iteratorMock, array $items)
{
$iteratorData = new \stdClass();
$iteratorData->array = $items;
$iteratorData->position = 0;
$iteratorMock->expects($this->any())
->method('rewind')
->will(
$this->returnCallback(
function () use ($iteratorData) {
$iteratorData->position = 0;
}
)
);
$iteratorMock->expects($this->any())
->method('current')
->will(
$this->returnCallback(
function () use ($iteratorData) {
return $iteratorData->array[$iteratorData->position];
}
)
);
$iteratorMock->expects($this->any())
->method('key')
->will(
$this->returnCallback(
function () use ($iteratorData) {
return $iteratorData->position;
}
)
);
$iteratorMock->expects($this->any())
->method('next')
->will(
$this->returnCallback(
function () use ($iteratorData) {
$iteratorData->position++;
}
)
);
$iteratorMock->expects($this->any())
->method('valid')
->will(
$this->returnCallback(
function () use ($iteratorData) {
return isset($iteratorData->array[$iteratorData->position]);
}
)
);
$iteratorMock->expects($this->any())
->method('count')
->will(
$this->returnCallback(
function () use ($iteratorData) {
return sizeof($iteratorData->array);
}
)
);
return $iteratorMock;
}
private function __setUpReader($url = '', $modified = true)
{
$this->reader->expects($this->once())
->method('read')
->with($this->equalTo($url))
->will($this->returnValue($this->result));
$this->result->expects($this->once())
->method('getResponse')
->will($this->returnValue($this->response));
$this->response->expects($this->once())
->method('isModified')
->will($this->returnValue($modified));
$this->location = $url;
if (!$modified) {
$this->result->expects($this->never())
->method('getUrl');
} else {
$this->result->expects($this->once())
->method('getUrl')
->will($this->returnValue($this->location));
$this->result->expects($this->once())
->method('getFeed')
->will($this->returnValue($this->feed_mock));
}
}
private function _expectFeed($method, $return, $count = 1)
{
$this->feed_mock->expects($this->exactly($count))
->method($method)
->will($this->returnValue($return));
}
private function _expectItem($method, $return, $count = 1)
{
$this->item_mock->expects($this->exactly($count))
->method($method)
->will($this->returnValue($return));
}
private function _createItem($enclosureType=null)
{
$this->_expectItem('getLink', $this->permalink);
$this->_expectItem('getTitle', $this->title);
$this->_expectItem('getPublicId', $this->guid);
$this->_expectItem('getDescription', $this->body);
$this->_expectItem('getLastModified', $this->modified);
$this->_expectItem('getAuthor', $this->author);
$item = new Item();
$item->setStatus(0);
$item->setUnread(true);
$item->setUrl($this->permalink);
$item->setTitle('my<\' title');
$item->setGuid($this->guid);
$item->setGuidHash($this->guid_hash);
$item->setBody($this->body);
$item->setRtl(false);
$item->setLastModified(3);
$item->setPubDate(3);
$item->setAuthor(html_entity_decode($this->author->getName()));
if ($enclosureType === 'audio/ogg' || $enclosureType === 'video/ogg') {
$media = $this->getMockbuilder(MediaInterface::class)->getMock();
$media->expects($this->once())
->method('getType')
->will($this->returnValue('sounds'));
$media2 = $this->getMockbuilder(MediaInterface::class)->getMock();
$media2->expects($this->exactly(2))
->method('getType')
->will($this->returnValue($enclosureType));
$media2->expects($this->once())
->method('getUrl')
->will($this->returnValue($this->enclosure));
$this->_expectItem('hasMedia', true);
$this->_expectItem('getMedias', [$media, $media2]);
$item->setEnclosureMime($enclosureType);
$item->setEnclosureLink($this->enclosure);
}
$item->generateSearchIndex();
return $item;
}
private function _createFeed($lang='de-DE', $favicon=false)
{
$this->_expectFeed('getTitle', $this->feed_title, 2);
$this->_expectFeed('getLink', $this->feed_link);
$this->_expectFeed('getLastModified', $this->modified);
$this->_expectFeed('getLanguage', $lang);
$feed = new Feed();
$feed->setTitle('&its a title');
$feed->setLink($this->feed_link);
$feed->setLocation($this->location);
$feed->setUrl($this->url);
$feed->setLastModified(3);
$feed->setAdded($this->time);
if ($favicon) {
$feed->setFaviconLink('http://anon.google.com');
$this->favicon->expects($this->exactly(1))
->method('get')
->with($this->equalTo($this->feed_link))
->will($this->returnValue($this->web_favicon));
} else {
$this->favicon->expects($this->never())
->method('get');
}
return $feed;
}
}

Просмотреть файл

@ -118,7 +118,7 @@ class FetcherTest extends TestCase
public function testMultipleFetchersOnlyOneShouldHandle()
{
$url = 'hi';
$return = 'zeas';
$return = [];
$mockFetcher = $this->getMockBuilder(IFeedFetcher::class)
->disableOriginalConstructor()
->getMock();

Просмотреть файл

@ -13,6 +13,7 @@ namespace OCA\News\Tests\Unit\Fetcher;
use \OCA\News\Db\Feed;
use OCA\News\Fetcher\FeedFetcher;
use OCA\News\Fetcher\Fetcher;
use OCA\News\Fetcher\YoutubeFetcher;
use PHPUnit\Framework\TestCase;
@ -20,7 +21,18 @@ use PHPUnit\Framework\TestCase;
class YoutubeFetcherTest extends TestCase
{
/**
* Mocked fetcher.
*
* @var Fetcher
*/
private $fetcher;
/**
* Mocked Feed Fetcher.
*
* @var FeedFetcher
*/
private $feedFetcher;
public function setUp()
@ -52,7 +64,8 @@ class YoutubeFetcherTest extends TestCase
$transformedUrl = 'http://gdata.youtube.com/feeds/api/playlists/sobo3';
$favicon = true;
$modified = 3;
$etag = 5;
$user = 5;
$password = 5;
$feed = new Feed();
$feed->setUrl('http://google.de');
$result = [$feed, []];
@ -63,10 +76,10 @@ class YoutubeFetcherTest extends TestCase
$this->equalTo($transformedUrl),
$this->equalTo($favicon),
$this->equalTo($modified),
$this->equalTo($etag)
$this->equalTo($user)
)
->will($this->returnValue($result));
$feed = $this->fetcher->fetch($url, $favicon, $modified, $etag);
$feed = $this->fetcher->fetch($url, $favicon, $modified, $user, $password);
$this->assertEquals($url, $result[0]->getUrl());
}

Просмотреть файл

@ -311,7 +311,8 @@ class FeedServiceTest extends TestCase
$this->equalTo('http://test'),
$this->equalTo(false),
$this->equalTo(3),
$this->equalTo(4)
$this->equalTo(''),
$this->equalTo('')
)
->will($this->returnValue($fetchReturn));
$this->feedMapper->expects($this->at(1))
@ -377,7 +378,8 @@ class FeedServiceTest extends TestCase
$this->equalTo('http://test'),
$this->equalTo(false),
$this->equalTo(3),
$this->equalTo(4)
$this->equalTo(''),
$this->equalTo('')
)
->will($this->returnValue($fetchReturn));
$this->feedMapper->expects($this->at(1))
@ -635,7 +637,6 @@ class FeedServiceTest extends TestCase
$feed = new Feed();
$feed->setId(3);
$feed->setUrl('https://goo.com');
$feed->setHttpEtag('abc');
$feed->setHttpLastModified(123);
$feed->setFullTextEnabled(true);
@ -654,9 +655,7 @@ class FeedServiceTest extends TestCase
->with(
$this->equalTo($feed->getUrl()),
$this->equalTo(false),
$this->equalTo($feed->getHttpLastModified()),
$this->equalTo($feed->getHttpEtag()),
$this->equalTo($feed->getFullTextEnabled())
$this->equalTo($feed->getHttpLastModified())
)
->will($this->throwException($ex));