Process main page (linklist) through Slim controller

Including a bunch of improvements on the container,
and helper used across new controllers.
This commit is contained in:
ArthurHoaro 2020-07-06 08:04:35 +02:00
parent 6132d64748
commit 1a8ac737e5
44 changed files with 1459 additions and 747 deletions

View file

@ -25,6 +25,8 @@ class ShaarliMiddleware
/**
* Middleware execution:
* - run updates
* - if not logged in open shaarli, redirect to login
* - execute the controller
* - return the response
*
@ -36,27 +38,82 @@ class ShaarliMiddleware
*
* @return Response response.
*/
public function __invoke(Request $request, Response $response, callable $next)
public function __invoke(Request $request, Response $response, callable $next): Response
{
$this->container->basePath = rtrim($request->getUri()->getBasePath(), '/');
try {
$response = $next($request, $response);
$this->runUpdates();
$this->checkOpenShaarli($request, $response, $next);
return $next($request, $response);
} catch (ShaarliFrontException $e) {
// Possible functional error
$this->container->pageBuilder->reset();
$this->container->pageBuilder->assign('message', $e->getMessage());
if ($this->container->conf->get('dev.debug', false)) {
$this->container->pageBuilder->assign(
'stacktrace',
nl2br(get_class($this) .': '. $e->getTraceAsString())
);
}
$response = $response->withStatus($e->getCode());
$response = $response->write($this->container->pageBuilder->render('error'));
return $response->write($this->container->pageBuilder->render('error'));
} catch (UnauthorizedException $e) {
return $response->withRedirect($this->container->basePath . '/login');
} catch (\Throwable $e) {
// Unknown error encountered
$this->container->pageBuilder->reset();
if ($this->container->conf->get('dev.debug', false)) {
$this->container->pageBuilder->assign('message', $e->getMessage());
$this->container->pageBuilder->assign(
'stacktrace',
nl2br(get_class($e) .': '. PHP_EOL . $e->getTraceAsString())
);
} else {
$this->container->pageBuilder->assign('message', t('An unexpected error occurred.'));
}
$response = $response->withStatus(500);
return $response->write($this->container->pageBuilder->render('error'));
}
}
/**
* Run the updater for every requests processed while logged in.
*/
protected function runUpdates(): void
{
if ($this->container->loginManager->isLoggedIn() !== true) {
return;
}
return $response;
$newUpdates = $this->container->updater->update();
if (!empty($newUpdates)) {
$this->container->updater->writeUpdates(
$this->container->conf->get('resource.updates'),
$this->container->updater->getDoneUpdates()
);
$this->container->pageCacheManager->invalidateCaches();
}
}
/**
* Access is denied to most pages with `hide_public_links` + `force_login` settings.
*/
protected function checkOpenShaarli(Request $request, Response $response, callable $next): bool
{
if (// if the user isn't logged in
!$this->container->loginManager->isLoggedIn()
// and Shaarli doesn't have public content...
&& $this->container->conf->get('privacy.hide_public_links')
// and is configured to enforce the login
&& $this->container->conf->get('privacy.force_login')
// and the current page isn't already the login page
// and the user is not requesting a feed (which would lead to a different content-type as expected)
&& !in_array($next->getName(), ['login', 'atom', 'rss'], true)
) {
throw new UnauthorizedException();
}
return true;
}
}

View file

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Shaarli\Front\Controller\Admin;
use Shaarli\Languages;
use Shaarli\Render\TemplatePage;
use Shaarli\Render\ThemeUtils;
use Shaarli\Thumbnailer;
use Slim\Http\Request;
@ -52,7 +53,7 @@ class ConfigureController extends ShaarliAdminController
$this->assignView('thumbnails_mode', $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE));
$this->assignView('pagetitle', t('Configure') .' - '. $this->container->conf->get('general.title', 'Shaarli'));
return $response->write($this->render('configure'));
return $response->write($this->render(TemplatePage::CONFIGURE));
}
/**

View file

@ -6,6 +6,7 @@ namespace Shaarli\Front\Controller\Admin;
use DateTime;
use Shaarli\Bookmark\Bookmark;
use Shaarli\Render\TemplatePage;
use Slim\Http\Request;
use Slim\Http\Response;
@ -24,7 +25,7 @@ class ExportController extends ShaarliAdminController
{
$this->assignView('pagetitle', t('Export') .' - '. $this->container->conf->get('general.title', 'Shaarli'));
return $response->write($this->render('export'));
return $response->write($this->render(TemplatePage::EXPORT));
}
/**
@ -74,6 +75,6 @@ class ExportController extends ShaarliAdminController
$this->assignView('eol', PHP_EOL);
$this->assignView('selection', $selection);
return $response->write($this->render('export.bookmarks'));
return $response->write($this->render(TemplatePage::NETSCAPE_EXPORT_BOOKMARKS));
}
}

View file

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Shaarli\Front\Controller\Admin;
use Psr\Http\Message\UploadedFileInterface;
use Shaarli\Render\TemplatePage;
use Slim\Http\Request;
use Slim\Http\Response;
@ -39,7 +40,7 @@ class ImportController extends ShaarliAdminController
);
$this->assignView('pagetitle', t('Import') .' - '. $this->container->conf->get('general.title', 'Shaarli'));
return $response->write($this->render('import'));
return $response->write($this->render(TemplatePage::IMPORT));
}
/**

View file

@ -7,6 +7,7 @@ namespace Shaarli\Front\Controller\Admin;
use Shaarli\Bookmark\Bookmark;
use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
use Shaarli\Formatter\BookmarkMarkdownFormatter;
use Shaarli\Render\TemplatePage;
use Shaarli\Thumbnailer;
use Slim\Http\Request;
use Slim\Http\Response;
@ -28,7 +29,7 @@ class ManageShaareController extends ShaarliAdminController
t('Shaare a new link') .' - '. $this->container->conf->get('general.title', 'Shaarli')
);
return $response->write($this->render('addlink'));
return $response->write($this->render(TemplatePage::ADDLINK));
}
/**
@ -365,7 +366,7 @@ class ManageShaareController extends ShaarliAdminController
$editLabel . t('Shaare') .' - '. $this->container->conf->get('general.title', 'Shaarli')
);
return $response->write($this->render('editlink'));
return $response->write($this->render(TemplatePage::EDIT_LINK));
}
/**

View file

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Shaarli\Front\Controller\Admin;
use Shaarli\Bookmark\BookmarkFilter;
use Shaarli\Render\TemplatePage;
use Slim\Http\Request;
use Slim\Http\Response;
@ -28,7 +29,7 @@ class ManageTagController extends ShaarliAdminController
t('Manage tags') .' - '. $this->container->conf->get('general.title', 'Shaarli')
);
return $response->write($this->render('changetag'));
return $response->write($this->render(TemplatePage::CHANGE_TAG));
}
/**

View file

@ -7,6 +7,7 @@ namespace Shaarli\Front\Controller\Admin;
use Shaarli\Container\ShaarliContainer;
use Shaarli\Front\Exception\OpenShaarliPasswordException;
use Shaarli\Front\Exception\ShaarliFrontException;
use Shaarli\Render\TemplatePage;
use Slim\Http\Request;
use Slim\Http\Response;
use Throwable;
@ -33,7 +34,7 @@ class PasswordController extends ShaarliAdminController
*/
public function index(Request $request, Response $response): Response
{
return $response->write($this->render('changepassword'));
return $response->write($this->render(TemplatePage::CHANGE_PASSWORD));
}
/**
@ -55,7 +56,7 @@ class PasswordController extends ShaarliAdminController
return $response
->withStatus(400)
->write($this->render('changepassword'))
->write($this->render(TemplatePage::CHANGE_PASSWORD))
;
}
@ -71,7 +72,7 @@ class PasswordController extends ShaarliAdminController
return $response
->withStatus(400)
->write($this->render('changepassword'))
->write($this->render(TemplatePage::CHANGE_PASSWORD))
;
}
@ -95,6 +96,6 @@ class PasswordController extends ShaarliAdminController
$this->saveSuccessMessage(t('Your password has been changed'));
return $response->write($this->render('changepassword'));
return $response->write($this->render(TemplatePage::CHANGE_PASSWORD));
}
}

View file

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Shaarli\Front\Controller\Admin;
use Exception;
use Shaarli\Render\TemplatePage;
use Slim\Http\Request;
use Slim\Http\Response;
@ -44,7 +45,7 @@ class PluginsController extends ShaarliAdminController
t('Plugin Administration') .' - '. $this->container->conf->get('general.title', 'Shaarli')
);
return $response->write($this->render('pluginsadmin'));
return $response->write($this->render(TemplatePage::PLUGINS_ADMIN));
}
/**

View file

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Shaarli\Front\Controller\Admin;
use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
use Shaarli\Render\TemplatePage;
use Slim\Http\Request;
use Slim\Http\Response;
@ -36,7 +37,7 @@ class ThumbnailsController extends ShaarliAdminController
t('Thumbnails update') .' - '. $this->container->conf->get('general.title', 'Shaarli')
);
return $response->write($this->render('thumbnails'));
return $response->write($this->render(TemplatePage::THUMBNAILS));
}
/**
@ -61,19 +62,4 @@ class ThumbnailsController extends ShaarliAdminController
return $response->withJson($this->container->formatterFactory->getFormatter('raw')->format($bookmark));
}
/**
* @param mixed[] $data Variables passed to the template engine
*
* @return mixed[] Template data after active plugins render_picwall hook execution.
*/
protected function executeHooks(array $data): array
{
$this->container->pluginManager->executeHooks(
'render_tools',
$data
);
return $data;
}
}

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Shaarli\Front\Controller\Admin;
use Shaarli\Render\TemplatePage;
use Slim\Http\Request;
use Slim\Http\Response;
@ -29,7 +30,7 @@ class ToolsController extends ShaarliAdminController
$this->assignView('pagetitle', t('Tools') .' - '. $this->container->conf->get('general.title', 'Shaarli'));
return $response->write($this->render('tools'));
return $response->write($this->render(TemplatePage::TOOLS));
}
/**

View file

@ -0,0 +1,248 @@
<?php
declare(strict_types=1);
namespace Shaarli\Front\Controller\Visitor;
use Shaarli\Bookmark\Bookmark;
use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
use Shaarli\Legacy\LegacyController;
use Shaarli\Legacy\UnknowLegacyRouteException;
use Shaarli\Render\TemplatePage;
use Shaarli\Thumbnailer;
use Slim\Http\Request;
use Slim\Http\Response;
/**
* Class BookmarkListController
*
* Slim controller used to render the bookmark list, the home page of Shaarli.
* It also displays permalinks, and process legacy routes based on GET parameters.
*/
class BookmarkListController extends ShaarliVisitorController
{
/**
* GET / - Displays the bookmark list, with optional filter parameters.
*/
public function index(Request $request, Response $response): Response
{
$legacyResponse = $this->processLegacyController($request, $response);
if (null !== $legacyResponse) {
return $legacyResponse;
}
$formatter = $this->container->formatterFactory->getFormatter();
$searchTags = escape(normalize_spaces($request->getParam('searchtags') ?? ''));
$searchTerm = escape(normalize_spaces($request->getParam('searchterm') ?? ''));;
// Filter bookmarks according search parameters.
$visibility = $this->container->sessionManager->getSessionParameter('visibility');
$search = [
'searchtags' => $searchTags,
'searchterm' => $searchTerm,
];
$linksToDisplay = $this->container->bookmarkService->search(
$search,
$visibility,
false,
!!$this->container->sessionManager->getSessionParameter('untaggedonly')
) ?? [];
// ---- Handle paging.
$keys = [];
foreach ($linksToDisplay as $key => $value) {
$keys[] = $key;
}
$linksPerPage = $this->container->sessionManager->getSessionParameter('LINKS_PER_PAGE', 20) ?: 20;
// Select articles according to paging.
$pageCount = (int) ceil(count($keys) / $linksPerPage) ?: 1;
$page = (int) $request->getParam('page') ?? 1;
$page = $page < 1 ? 1 : $page;
$page = $page > $pageCount ? $pageCount : $page;
// Start index.
$i = ($page - 1) * $linksPerPage;
$end = $i + $linksPerPage;
$linkDisp = [];
$save = false;
while ($i < $end && $i < count($keys)) {
$save = $this->updateThumbnail($linksToDisplay[$keys[$i]], false) || $save;
$link = $formatter->format($linksToDisplay[$keys[$i]]);
$linkDisp[$keys[$i]] = $link;
$i++;
}
if ($save) {
$this->container->bookmarkService->save();
}
// Compute paging navigation
$searchtagsUrl = $searchTags === '' ? '' : '&searchtags=' . urlencode($searchTags);
$searchtermUrl = $searchTerm === '' ? '' : '&searchterm=' . urlencode($searchTerm);
$previous_page_url = '';
if ($i !== count($keys)) {
$previous_page_url = '?page=' . ($page + 1) . $searchtermUrl . $searchtagsUrl;
}
$next_page_url = '';
if ($page > 1) {
$next_page_url = '?page=' . ($page - 1) . $searchtermUrl . $searchtagsUrl;
}
// Fill all template fields.
$data = array_merge(
$this->initializeTemplateVars(),
[
'previous_page_url' => $previous_page_url,
'next_page_url' => $next_page_url,
'page_current' => $page,
'page_max' => $pageCount,
'result_count' => count($linksToDisplay),
'search_term' => $searchTerm,
'search_tags' => $searchTags,
'visibility' => $visibility,
'links' => $linkDisp,
]
);
if (!empty($searchTerm) || !empty($searchTags)) {
$data['pagetitle'] = t('Search: ');
$data['pagetitle'] .= ! empty($searchTerm) ? $searchTerm . ' ' : '';
$bracketWrap = function ($tag) {
return '[' . $tag . ']';
};
$data['pagetitle'] .= ! empty($searchTags)
? implode(' ', array_map($bracketWrap, preg_split('/\s+/', $searchTags))) . ' '
: '';
$data['pagetitle'] .= '- ';
}
$data['pagetitle'] = ($data['pagetitle'] ?? '') . $this->container->conf->get('general.title', 'Shaarli');
$this->executeHooks($data);
$this->assignAllView($data);
return $response->write($this->render(TemplatePage::LINKLIST));
}
/**
* GET /shaare/{hash} - Display a single shaare
*/
public function permalink(Request $request, Response $response, array $args): Response
{
try {
$bookmark = $this->container->bookmarkService->findByHash($args['hash']);
} catch (BookmarkNotFoundException $e) {
$this->assignView('error_message', $e->getMessage());
return $response->write($this->render(TemplatePage::ERROR_404));
}
$this->updateThumbnail($bookmark);
$data = array_merge(
$this->initializeTemplateVars(),
[
'pagetitle' => $bookmark->getTitle() .' - '. $this->container->conf->get('general.title', 'Shaarli'),
'links' => [$this->container->formatterFactory->getFormatter()->format($bookmark)],
]
);
$this->executeHooks($data);
$this->assignAllView($data);
return $response->write($this->render(TemplatePage::LINKLIST));
}
/**
* Update the thumbnail of a single bookmark if necessary.
*/
protected function updateThumbnail(Bookmark $bookmark, bool $writeDatastore = true): bool
{
// Logged in, thumbnails enabled, not a note, is HTTP
// and (never retrieved yet or no valid cache file)
if ($this->container->loginManager->isLoggedIn()
&& $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE
&& false !== $bookmark->getThumbnail()
&& !$bookmark->isNote()
&& (null === $bookmark->getThumbnail() || !is_file($bookmark->getThumbnail()))
&& startsWith(strtolower($bookmark->getUrl()), 'http')
) {
$bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl()));
$this->container->bookmarkService->set($bookmark, $writeDatastore);
return true;
}
return false;
}
/**
* @param mixed[] $data Template vars to process in plugins, passed as reference.
*/
protected function executeHooks(array &$data): void
{
$this->container->pluginManager->executeHooks(
'render_linklist',
$data,
['loggedin' => $this->container->loginManager->isLoggedIn()]
);
}
/**
* @return string[] Default template variables without values.
*/
protected function initializeTemplateVars(): array
{
return [
'previous_page_url' => '',
'next_page_url' => '',
'page_max' => '',
'search_tags' => '',
'result_count' => '',
];
}
/**
* Process legacy routes if necessary. They used query parameters.
* If no legacy routes is passed, return null.
*/
protected function processLegacyController(Request $request, Response $response): ?Response
{
// Legacy smallhash filter
$queryString = $this->container->environment['QUERY_STRING'] ?? null;
if (null !== $queryString && 1 === preg_match('/^([a-zA-Z0-9-_@]{6})($|&|#)/', $queryString, $match)) {
return $this->redirect($response, '/shaare/' . $match[1]);
}
// Legacy controllers (mostly used for redirections)
if (null !== $request->getQueryParam('do')) {
$legacyController = new LegacyController($this->container);
try {
return $legacyController->process($request, $response, $request->getQueryParam('do'));
} catch (UnknowLegacyRouteException $e) {
// We ignore legacy 404
return null;
}
}
// Legacy GET admin routes
$legacyGetRoutes = array_intersect(
LegacyController::LEGACY_GET_ROUTES,
array_keys($request->getQueryParams() ?? [])
);
if (1 === count($legacyGetRoutes)) {
$legacyController = new LegacyController($this->container);
return $legacyController->process($request, $response, $legacyGetRoutes[0]);
}
return null;
}
}

View file

@ -7,6 +7,7 @@ namespace Shaarli\Front\Controller\Visitor;
use DateTime;
use DateTimeImmutable;
use Shaarli\Bookmark\Bookmark;
use Shaarli\Render\TemplatePage;
use Slim\Http\Request;
use Slim\Http\Response;
@ -85,7 +86,7 @@ class DailyController extends ShaarliVisitorController
t('Daily') .' - '. format_date($dayDate, false) . ' - ' . $mainTitle
);
return $response->write($this->render('daily'));
return $response->write($this->render(TemplatePage::DAILY));
}
/**
@ -152,7 +153,7 @@ class DailyController extends ShaarliVisitorController
$this->assignView('hide_timestamps', $this->container->conf->get('privacy.hide_timestamps', false));
$this->assignView('days', $dataPerDay);
$rssContent = $this->render('dailyrss');
$rssContent = $this->render(TemplatePage::DAILY_RSS);
$cache->cache($rssContent);

View file

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Shaarli\Front\Controller\Visitor;
use Shaarli\Front\Exception\LoginBannedException;
use Shaarli\Render\TemplatePage;
use Slim\Http\Request;
use Slim\Http\Response;
@ -41,6 +42,6 @@ class LoginController extends ShaarliVisitorController
->assignView('pagetitle', t('Login') .' - '. $this->container->conf->get('general.title', 'Shaarli'))
;
return $response->write($this->render('loginform'));
return $response->write($this->render(TemplatePage::LOGIN));
}
}

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Shaarli\Front\Controller\Visitor;
use Shaarli\Render\TemplatePage;
use Slim\Http\Request;
use Slim\Http\Response;
@ -21,6 +22,6 @@ class OpenSearchController extends ShaarliVisitorController
$this->assignView('serverurl', index_url($this->container->environment));
return $response->write($this->render('opensearch'));
return $response->write($this->render(TemplatePage::OPEN_SEARCH));
}
}

View file

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Shaarli\Front\Controller\Visitor;
use Shaarli\Front\Exception\ThumbnailsDisabledException;
use Shaarli\Render\TemplatePage;
use Shaarli\Thumbnailer;
use Slim\Http\Request;
use Slim\Http\Response;
@ -46,7 +47,7 @@ class PictureWallController extends ShaarliVisitorController
$this->assignView($key, $value);
}
return $response->write($this->render('picwall'));
return $response->write($this->render(TemplatePage::PICTURE_WALL));
}
/**