RSS/ATOM feeds: process through Slim controller

This commit is contained in:
ArthurHoaro 2020-05-18 17:17:36 +02:00
parent f4929b1188
commit 7b2ba6ef82
16 changed files with 349 additions and 60 deletions

View file

@ -7,6 +7,7 @@
use Shaarli\Bookmark\BookmarkFileService;
use Shaarli\Bookmark\BookmarkServiceInterface;
use Shaarli\Config\ConfigManager;
use Shaarli\Feed\FeedBuilder;
use Shaarli\Formatter\FormatterFactory;
use Shaarli\History;
use Shaarli\Plugin\PluginManager;
@ -100,6 +101,15 @@ public function build(): ShaarliContainer
);
};
$container['feedBuilder'] = function (ShaarliContainer $container): FeedBuilder {
return new FeedBuilder(
$container->bookmarkService,
$container->formatterFactory->getFormatter(),
$container->environment,
$container->loginManager->isLoggedIn()
);
};
return $container;
}
}

View file

@ -6,6 +6,7 @@
use Shaarli\Bookmark\BookmarkServiceInterface;
use Shaarli\Config\ConfigManager;
use Shaarli\Feed\FeedBuilder;
use Shaarli\Formatter\FormatterFactory;
use Shaarli\History;
use Shaarli\Plugin\PluginManager;
@ -29,6 +30,7 @@
* @property PluginManager $pluginManager
* @property FormatterFactory $formatterFactory
* @property PageCacheManager $pageCacheManager
* @property FeedBuilder $feedBuilder
*/
class ShaarliContainer extends Container
{

View file

@ -78,7 +78,7 @@ class FeedBuilder
* @param array $serverInfo $_SERVER.
* @param boolean $isLoggedIn True if the user is currently logged in, false otherwise.
*/
public function __construct($linkDB, $formatter, array $serverInfo, $isLoggedIn)
public function __construct($linkDB, $formatter, $serverInfo, $isLoggedIn)
{
$this->linkDB = $linkDB;
$this->formatter = $formatter;

View file

@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace Shaarli\Front\Controller;
use Shaarli\Feed\FeedBuilder;
use Slim\Http\Request;
use Slim\Http\Response;
/**
* Class FeedController
*
* Slim controller handling ATOM and RSS feed.
*
* @package Front\Controller
*/
class FeedController extends ShaarliController
{
public function atom(Request $request, Response $response): Response
{
return $this->processRequest(FeedBuilder::$FEED_ATOM, $request, $response);
}
public function rss(Request $request, Response $response): Response
{
return $this->processRequest(FeedBuilder::$FEED_RSS, $request, $response);
}
protected function processRequest(string $feedType, Request $request, Response $response): Response
{
$response = $response->withHeader('Content-Type', 'application/'. $feedType .'+xml; charset=utf-8');
$pageUrl = page_url($this->container->environment);
$cache = $this->container->pageCacheManager->getCachePage($pageUrl);
$cached = $cache->cachedVersion();
if (!empty($cached)) {
return $response->write($cached);
}
// Generate data.
$this->container->feedBuilder->setLocale(strtolower(setlocale(LC_COLLATE, 0)));
$this->container->feedBuilder->setHideDates($this->container->conf->get('privacy.hide_timestamps', false));
$this->container->feedBuilder->setUsePermalinks(
null !== $request->getParam('permalinks') || !$this->container->conf->get('feed.rss_permalinks')
);
$data = $this->container->feedBuilder->buildData($feedType, $request->getParams());
$this->executeHooks($data, $feedType);
$this->assignAllView($data);
$content = $this->render('feed.'. $feedType);
$cache->cache($content);
return $response->write($content);
}
/**
* @param mixed[] $data Template data
*
* @return mixed[] Template data after active plugins hook execution.
*/
protected function executeHooks(array $data, string $feedType): array
{
$this->container->pluginManager->executeHooks(
'render_feed',
$data,
[
'loggedin' => $this->container->loginManager->isLoggedIn(),
'target' => $feedType,
]
);
return $data;
}
}

View file

@ -30,6 +30,20 @@ protected function assignView(string $name, $value): self
return $this;
}
/**
* Assign variables to RainTPL template through the PageBuilder.
*
* @param mixed $data Values to assign to the template and their keys
*/
protected function assignAllView(array $data): self
{
foreach ($data as $key => $value) {
$this->assignView($key, $value);
}
return $this;
}
protected function render(string $template): string
{
$this->assignView('linkcount', $this->container->bookmarkService->count(BookmarkFilter::$ALL));

View file

@ -1,14 +1,14 @@
### Feeds options
Feeds are available in ATOM with `?do=atom` and RSS with `do=RSS`.
Feeds are available in ATOM with `/feed-atom` and RSS with `/feed-rss`.
Options:
- You can use `permalinks` in the feed URL to get permalink to Shaares instead of direct link to shaared URL.
- E.G. `https://my.shaarli.domain/?do=atom&permalinks`.
- E.G. `https://my.shaarli.domain/feed-atom?permalinks`.
- You can use `nb` parameter in the feed URL to specify the number of Shaares you want in a feed (default if not specified: `50`). The keyword `all` is available if you want everything.
- `https://my.shaarli.domain/?do=atom&permalinks&nb=42`
- `https://my.shaarli.domain/?do=atom&permalinks&nb=all`
- `https://my.shaarli.domain/feed-atom?permalinks&nb=42`
- `https://my.shaarli.domain/feed-atom?permalinks&nb=all`
### RSS Feeds or Picture Wall for a specific search/tag

View file

@ -432,45 +432,8 @@ function renderPage($conf, $pluginManager, $bookmarkService, $history, $sessionM
// ATOM and RSS feed.
if ($targetPage == Router::$PAGE_FEED_ATOM || $targetPage == Router::$PAGE_FEED_RSS) {
$feedType = $targetPage == Router::$PAGE_FEED_RSS ? FeedBuilder::$FEED_RSS : FeedBuilder::$FEED_ATOM;
header('Content-Type: application/'. $feedType .'+xml; charset=utf-8');
// Cache system
$query = $_SERVER['QUERY_STRING'];
$cache = new CachedPage(
$conf->get('resource.page_cache'),
page_url($_SERVER),
startsWith($query, 'do='. $targetPage) && !$loginManager->isLoggedIn()
);
$cached = $cache->cachedVersion();
if (!empty($cached)) {
echo $cached;
exit;
}
$factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
// Generate data.
$feedGenerator = new FeedBuilder(
$bookmarkService,
$factory->getFormatter(),
$_SERVER,
$loginManager->isLoggedIn()
);
$feedGenerator->setLocale(strtolower(setlocale(LC_COLLATE, 0)));
$feedGenerator->setHideDates($conf->get('privacy.hide_timestamps') && !$loginManager->isLoggedIn());
$feedGenerator->setUsePermalinks(isset($_GET['permalinks']) || !$conf->get('feed.rss_permalinks'));
$data = $feedGenerator->buildData($feedType, $_GET);
// Process plugin hook.
$pluginManager->executeHooks('render_feed', $data, array(
'loggedin' => $loginManager->isLoggedIn(),
'target' => $targetPage,
));
// Render the template.
$PAGE->assignAll($data);
$PAGE->renderPage('feed.'. $feedType);
$cache->cache(ob_get_contents());
ob_end_flush();
header('Location: ./feed-'. $feedType .'?'. http_build_query($_GET));
exit;
}
@ -1610,6 +1573,8 @@ function install($conf, $sessionManager, $loginManager)
$this->get('/tag-list', '\Shaarli\Front\Controller\TagCloudController:list')->setName('taglist');
$this->get('/daily', '\Shaarli\Front\Controller\DailyController:index')->setName('daily');
$this->get('/daily-rss', '\Shaarli\Front\Controller\DailyController:rss')->setName('dailyrss');
$this->get('/feed-atom', '\Shaarli\Front\Controller\FeedController:atom')->setName('feedatom');
$this->get('/feed-rss', '\Shaarli\Front\Controller\FeedController:rss')->setName('feedrss');
$this->get('/add-tag/{newTag}', '\Shaarli\Front\Controller\TagController:addTag')->setName('add-tag');
})->add('\Shaarli\Front\ShaarliMiddleware');

View file

@ -60,8 +60,8 @@ function hook_pubsubhubbub_render_feed($data, $conf)
function hook_pubsubhubbub_save_link($data, $conf)
{
$feeds = array(
index_url($_SERVER) .'?do=atom',
index_url($_SERVER) .'?do=rss',
index_url($_SERVER) .'feed-atom',
index_url($_SERVER) .'feed-rss',
);
$httpPost = function_exists('curl_version') ? false : 'nocurl_http_post';

View file

@ -11,7 +11,7 @@ class CachedPageTest extends \PHPUnit\Framework\TestCase
{
// test cache directory
protected static $testCacheDir = 'sandbox/pagecache';
protected static $url = 'http://shaar.li/?do=atom';
protected static $url = 'http://shaar.li/feed-atom';
protected static $filename;
/**
@ -42,8 +42,8 @@ public function testConstruct()
{
new CachedPage(self::$testCacheDir, '', true);
new CachedPage(self::$testCacheDir, '', false);
new CachedPage(self::$testCacheDir, 'http://shaar.li/?do=rss', true);
new CachedPage(self::$testCacheDir, 'http://shaar.li/?do=atom', false);
new CachedPage(self::$testCacheDir, 'http://shaar.li/feed-rss', true);
new CachedPage(self::$testCacheDir, 'http://shaar.li/feed-atom', false);
$this->addToAssertionCount(1);
}

View file

@ -0,0 +1,219 @@
<?php
declare(strict_types=1);
namespace Shaarli\Front\Controller;
use PHPUnit\Framework\TestCase;
use Shaarli\Bookmark\BookmarkServiceInterface;
use Shaarli\Config\ConfigManager;
use Shaarli\Container\ShaarliContainer;
use Shaarli\Feed\FeedBuilder;
use Shaarli\Formatter\FormatterFactory;
use Shaarli\Plugin\PluginManager;
use Shaarli\Render\PageBuilder;
use Shaarli\Render\PageCacheManager;
use Shaarli\Security\LoginManager;
use Slim\Http\Request;
use Slim\Http\Response;
class FeedControllerTest extends TestCase
{
/** @var ShaarliContainer */
protected $container;
/** @var FeedController */
protected $controller;
public function setUp(): void
{
$this->container = $this->createMock(ShaarliContainer::class);
$this->controller = new FeedController($this->container);
}
/**
* Feed Controller - RSS default behaviour
*/
public function testDefaultRssController(): void
{
$this->createValidContainerMockSet();
$request = $this->createMock(Request::class);
$response = new Response();
$this->container->feedBuilder->expects(static::once())->method('setLocale');
$this->container->feedBuilder->expects(static::once())->method('setHideDates')->with(false);
$this->container->feedBuilder->expects(static::once())->method('setUsePermalinks')->with(true);
// Save RainTPL assigned variables
$assignedVariables = [];
$this->assignTemplateVars($assignedVariables);
$this->container->feedBuilder->method('buildData')->willReturn(['content' => 'data']);
// Make sure that PluginManager hook is triggered
$this->container->pluginManager
->expects(static::at(0))
->method('executeHooks')
->willReturnCallback(function (string $hook, array $data, array $param): void {
static::assertSame('render_feed', $hook);
static::assertSame('data', $data['content']);
static::assertArrayHasKey('loggedin', $param);
static::assertSame('rss', $param['target']);
})
;
$result = $this->controller->rss($request, $response);
static::assertSame(200, $result->getStatusCode());
static::assertStringContainsString('application/rss', $result->getHeader('Content-Type')[0]);
static::assertSame('feed.rss', (string) $result->getBody());
static::assertSame('data', $assignedVariables['content']);
}
/**
* Feed Controller - ATOM default behaviour
*/
public function testDefaultAtomController(): void
{
$this->createValidContainerMockSet();
$request = $this->createMock(Request::class);
$response = new Response();
$this->container->feedBuilder->expects(static::once())->method('setLocale');
$this->container->feedBuilder->expects(static::once())->method('setHideDates')->with(false);
$this->container->feedBuilder->expects(static::once())->method('setUsePermalinks')->with(true);
// Save RainTPL assigned variables
$assignedVariables = [];
$this->assignTemplateVars($assignedVariables);
$this->container->feedBuilder->method('buildData')->willReturn(['content' => 'data']);
// Make sure that PluginManager hook is triggered
$this->container->pluginManager
->expects(static::at(0))
->method('executeHooks')
->willReturnCallback(function (string $hook, array $data, array $param): void {
static::assertSame('render_feed', $hook);
static::assertSame('data', $data['content']);
static::assertArrayHasKey('loggedin', $param);
static::assertSame('atom', $param['target']);
})
;
$result = $this->controller->atom($request, $response);
static::assertSame(200, $result->getStatusCode());
static::assertStringContainsString('application/atom', $result->getHeader('Content-Type')[0]);
static::assertSame('feed.atom', (string) $result->getBody());
static::assertSame('data', $assignedVariables['content']);
}
/**
* Feed Controller - ATOM with parameters
*/
public function testAtomControllerWithParameters(): void
{
$this->createValidContainerMockSet();
$request = $this->createMock(Request::class);
$request->method('getParams')->willReturn(['parameter' => 'value']);
$response = new Response();
// Save RainTPL assigned variables
$assignedVariables = [];
$this->assignTemplateVars($assignedVariables);
$this->container->feedBuilder
->method('buildData')
->with('atom', ['parameter' => 'value'])
->willReturn(['content' => 'data'])
;
// Make sure that PluginManager hook is triggered
$this->container->pluginManager
->expects(static::at(0))
->method('executeHooks')
->willReturnCallback(function (string $hook, array $data, array $param): void {
static::assertSame('render_feed', $hook);
static::assertSame('data', $data['content']);
static::assertArrayHasKey('loggedin', $param);
static::assertSame('atom', $param['target']);
})
;
$result = $this->controller->atom($request, $response);
static::assertSame(200, $result->getStatusCode());
static::assertStringContainsString('application/atom', $result->getHeader('Content-Type')[0]);
static::assertSame('feed.atom', (string) $result->getBody());
static::assertSame('data', $assignedVariables['content']);
}
protected function createValidContainerMockSet(): void
{
$loginManager = $this->createMock(LoginManager::class);
$this->container->loginManager = $loginManager;
// Config
$conf = $this->createMock(ConfigManager::class);
$this->container->conf = $conf;
$this->container->conf->method('get')->willReturnCallback(function (string $parameter, $default) {
return $default;
});
// PageBuilder
$pageBuilder = $this->createMock(PageBuilder::class);
$pageBuilder
->method('render')
->willReturnCallback(function (string $template): string {
return $template;
})
;
$this->container->pageBuilder = $pageBuilder;
$bookmarkService = $this->createMock(BookmarkServiceInterface::class);
$this->container->bookmarkService = $bookmarkService;
// Plugin Manager
$pluginManager = $this->createMock(PluginManager::class);
$this->container->pluginManager = $pluginManager;
// Formatter
$formatterFactory = $this->createMock(FormatterFactory::class);
$this->container->formatterFactory = $formatterFactory;
// CacheManager
$pageCacheManager = $this->createMock(PageCacheManager::class);
$this->container->pageCacheManager = $pageCacheManager;
// FeedBuilder
$feedBuilder = $this->createMock(FeedBuilder::class);
$this->container->feedBuilder = $feedBuilder;
// $_SERVER
$this->container->environment = [
'SERVER_NAME' => 'shaarli',
'SERVER_PORT' => '80',
'REQUEST_URI' => '/daily-rss',
];
}
protected function assignTemplateVars(array &$variables): void
{
$this->container->pageBuilder
->expects(static::atLeastOnce())
->method('assign')
->willReturnCallback(function ($key, $value) use (&$variables) {
$variables[$key] = $value;
return $this;
})
;
}
}

View file

@ -3,8 +3,8 @@
<meta name="format-detection" content="telephone=no" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="referrer" content="same-origin">
<link rel="alternate" type="application/atom+xml" href="{$feedurl}?do=atom{$searchcrits}#" title="ATOM Feed" />
<link rel="alternate" type="application/rss+xml" href="{$feedurl}?do=rss{$searchcrits}#" title="RSS Feed" />
<link rel="alternate" type="application/atom+xml" href="{$feedurl}feed-atom?{$searchcrits}#" title="ATOM Feed" />
<link rel="alternate" type="application/rss+xml" href="{$feedurl}feed-rss?{$searchcrits}#" title="RSS Feed" />
<link href="img/favicon.png" rel="shortcut icon" type="image/png" />
<link href="img/apple-touch-icon.png" rel="apple-touch-icon" sizes="180x180" />
<link type="text/css" rel="stylesheet" href="css/shaarli.min.css?v={$version_hash}" />

View file

@ -3,8 +3,8 @@
<ShortName>Shaarli search - {$pagetitle}</ShortName>
<Description>Shaarli search - {$pagetitle}</Description>
<Url type="text/html" template="{$serverurl}?searchterm={searchTerms}" />
<Url type="application/atom+xml" template="{$serverurl}?do=atom&amp;searchterm={searchTerms}"/>
<Url type="application/rss+xml" template="{$serverurl}?do=rss&amp;searchterm={searchTerms}"/>
<Url type="application/atom+xml" template="{$serverurl}feed-atom?searchterm={searchTerms}"/>
<Url type="application/rss+xml" template="{$serverurl}feed-rss?searchterm={searchTerms}"/>
<InputEncoding>UTF-8</InputEncoding>
<Developer>Shaarli Community - https://github.com/shaarli/Shaarli/</Developer>
<Image width="16" height="16">data:image/x-icon;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAAHRklE

View file

@ -52,7 +52,7 @@
</li>
{/loop}
<li class="pure-menu-item pure-u-lg-0 shaarli-menu-mobile" id="shaarli-menu-mobile-rss">
<a href="./?do={$feed_type}{$searchcrits}" class="pure-menu-link">{'RSS Feed'|t}</a>
<a href="./feed-{$feed_type}?{$searchcrits}" class="pure-menu-link">{'RSS Feed'|t}</a>
</li>
{if="$is_logged_in"}
<li class="pure-menu-item pure-u-lg-0 shaarli-menu-mobile" id="shaarli-menu-mobile-logout">
@ -74,7 +74,7 @@
</a>
</li>
<li class="pure-menu-item" id="shaarli-menu-desktop-rss">
<a href="./?do={$feed_type}{$searchcrits}" class="pure-menu-link" title="{'RSS Feed'|t}" aria-label="{'RSS Feed'|t}">
<a href="./feed-{$feed_type}?{$searchcrits}" class="pure-menu-link" title="{'RSS Feed'|t}" aria-label="{'RSS Feed'|t}">
<i class="fa fa-rss" aria-hidden="true"></i>
</a>
</li>

View file

@ -3,8 +3,8 @@
<meta name="format-detection" content="telephone=no" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<meta name="referrer" content="same-origin">
<link rel="alternate" type="application/rss+xml" href="{$feedurl}?do=rss{$searchcrits}#" title="RSS Feed" />
<link rel="alternate" type="application/atom+xml" href="{$feedurl}?do=atom{$searchcrits}#" title="ATOM Feed" />
<link rel="alternate" type="application/rss+xml" href="{$feedurl}feed-rss?{$searchcrits}#" title="RSS Feed" />
<link rel="alternate" type="application/atom+xml" href="{$feedurl}feed-atom?{$searchcrits}#" title="ATOM Feed" />
<link href="img/favicon.ico" rel="shortcut icon" type="image/x-icon" />
<link type="text/css" rel="stylesheet" href="css/shaarli.min.css" />
{if="$formatter==='markdown'"}

View file

@ -3,8 +3,8 @@
<ShortName>Shaarli search - {$pagetitle}</ShortName>
<Description>Shaarli search - {$pagetitle}</Description>
<Url type="text/html" template="{$serverurl}?searchterm={searchTerms}" />
<Url type="application/atom+xml" template="{$serverurl}?do=atom&amp;searchterm={searchTerms}"/>
<Url type="application/rss+xml" template="{$serverurl}?do=rss&amp;searchterm={searchTerms}"/>
<Url type="application/atom+xml" template="{$serverurl}feed-atom?searchterm={searchTerms}"/>
<Url type="application/rss+xml" template="{$serverurl}feed-rss?searchterm={searchTerms}"/>
<InputEncoding>UTF-8</InputEncoding>
<Developer>Shaarli Community - https://github.com/shaarli/Shaarli/</Developer>
<Image width="16" height="16">data:image/x-icon;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAAHRklE

View file

@ -27,9 +27,9 @@
{else}
<li><a href="./login">Login</a></li>
{/if}
<li><a href="{$feedurl}?do=rss{$searchcrits}" class="nomobile">RSS Feed</a></li>
<li><a href="{$feedurl}/feed-rss?{$searchcrits}" class="nomobile">RSS Feed</a></li>
{if="$showatom"}
<li><a href="{$feedurl}?do=atom{$searchcrits}" class="nomobile">ATOM Feed</a></li>
<li><a href="{$feedurl}/feed-atom?{$searchcrits}" class="nomobile">ATOM Feed</a></li>
{/if}
<li><a href="./tag-cloud">Tag cloud</a></li>
<li><a href="./picture-wall{function="ltrim($searchcrits, '&')"}">Picture wall</a></li>