indent_size = 2
[*.php]
-max_line_length = 100
+max_line_length = 120
[Dockerfile]
max_line_length = 80
-ArthurHoaro <arthur@hoa.ro>
+ArthurHoaro <arthur@hoa.ro> <arthur.hoareau@wizacha.com>
+ArthurHoaro <arthur@hoa.ro> Arthur
Florian Eula <eula.florian@gmail.com> feula
Florian Eula <eula.florian@gmail.com> <mr.pikzen@gmail.com>
Immánuel Fodor <immanuelfactor+github@gmail.com>
kalvn <kalvnthereal@gmail.com> <kalvn@users.noreply.github.com>
+kalvn <kalvnthereal@gmail.com> <kalvn@pm.me>
+Neros <contact@neros.fr> <NerosTie@users.noreply.github.com>
Nicolas Danelon <hi@nicolasmd.com.ar> nicolasm
Nicolas Danelon <hi@nicolasmd.com.ar> <nda@3818.com.ar>
Nicolas Danelon <hi@nicolasmd.com.ar> <nicolasdanelon@gmail.com>
Nicolas Danelon <hi@nicolasmd.com.ar> <nicolasdanelon@users.noreply.github.com>
Sébastien Sauvage <sebsauvage@sebsauvage.net>
+Sébastien NOBILI <code@pipoprods.org> <s-code-github@pipoprods.org>
Timo Van Neerden <fire@lehollandaisvolant.net>
Timo Van Neerden <fire@lehollandaisvolant.net> lehollandaisvolant <levoltigeurhollandais@gmail.com>
VirtualTam <virtualtam@flibidi.net> <tamisier.aurelien@gmail.com>
-sudo: false
-dist: trusty
+dist: bionic
matrix:
include:
- 782 ArthurHoaro <arthur@hoa.ro>
- 401 VirtualTam <virtualtam@flibidi.net>
- 218 nodiscc <nodiscc@gmail.com>
+ 903 ArthurHoaro <arthur@hoa.ro>
+ 402 VirtualTam <virtualtam@flibidi.net>
+ 250 nodiscc <nodiscc@gmail.com>
56 Sébastien Sauvage <sebsauvage@sebsauvage.net>
16 Luce Carević <lcarevic@access42.net>
15 Florian Eula <eula.florian@gmail.com>
12 Nicolas Danelon <hi@nicolasmd.com.ar>
9 Willi Eggeling <thewilli@gmail.com>
8 Christophe HENRY <christophe.henry@sbgodin.fr>
+ 7 Lucas Cimon <lucas.cimon@gmail.com>
6 B. van Berkum <dev@dotmpe.com>
+ 6 kalvn <kalvnthereal@gmail.com>
6 llune <llune@users.noreply.github.com>
- 5 Lucas Cimon <lucas.cimon@gmail.com>
5 Mark Schmitz <kramred@gmail.com>
- 5 kalvn <kalvnthereal@gmail.com>
+ 5 Sébastien NOBILI <code@pipoprods.org>
4 Alexandre Alapetite <alexandre@alapetite.fr>
4 David Sferruzza <david.sferruzza@gmail.com>
4 Immánuel Fodor <immanuelfactor+github@gmail.com>
2 Alexandre G.-Raymond <alex@ndre.gr>
2 Chris Kuethe <chris.kuethe@gmail.com>
2 Felix Bartels <felix@host-consultants.de>
+ 2 Guillaume Virlet <github@virlet.org>
2 Knah Tsaeb <Knah-Tsaeb@knah-tsaeb.org>
2 Mathieu Chabanon <git@matchab.fr>
2 Miloš Jovanović <mjovanovic@gmail.com>
+ 2 Neros <contact@neros.fr>
2 Qwerty <champlywood@free.fr>
2 Stephen Muth <smuth4@gmail.com>
2 Timo Van Neerden <fire@lehollandaisvolant.net>
+ 2 dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
+ 2 flow.gunso <flow.gunso@gmail.com>
2 julienCXX <software@chmodplusx.eu>
2 philipp-r <philipp-r@users.noreply.github.com>
2 pips <pips@e5150.fr>
2 trailjeep <trailjeep@gmail.com>
+ 2 yude <yudesleepy@gmail.com>
1 Adrien Oliva <adrien.oliva@yapbreak.fr>
1 Adrien le Maire <adrien@alemaire.be>
1 Alexis J <alexis@effingo.be>
1 Angristan <angristan@users.noreply.github.com>
1 Bish Erbas <42714627+bisherbas@users.noreply.github.com>
1 BoboTiG <bobotig@gmail.com>
+ 1 Brendan M. Sleight <bms.git@barwap.com>
1 Bronco <bronco@warriordudimanche.net>
1 Buster One <37770318+buster-one@users.noreply.github.com>
1 D Low <daniellowtw@gmail.com>
1 Daniel Jakots <vigdis@chown.me>
+ 1 David Foucher <dev@tyjak.net>
1 Dennis Verspuij <dennisverspuij@users.noreply.github.com>
1 Dimtion <zizou.xena@gmail.com>
1 Fanch <fanch-github@qth.fr>
1 Florian Voigt <flvoigt@me.com>
1 Franck Kerbiriou <FranckKe@users.noreply.github.com>
1 Gary Marigliano <gmarigliano93@gmail.com>
- 1 Guillaume Virlet <github@virlet.org>
1 Jonathan Amiez <jonathan.amiez@gmail.com>
1 Jonathan Druart <jonathan.druart@gmail.com>
1 Julien Pivotto <roidelapluie@inuits.eu>
1 Kevin Canévet <kevin@streamroot.io>
+ 1 Kevin Masson <kevin.masson@methodinthemadness.eu>
1 Knah Tsaeb <knah-tsaeb@knah-tsaeb.org>
1 Lionel Martin <renarddesmers@gmail.com>
1 Mark Gerarts <mark.gerarts@gmail.com>
1 Marsup <marsup@gmail.com>
- 1 Neros <contact@neros.fr>
+ 1 Paul van den Burg <github@paulvandenburg.nl>
1 Rajat Hans <rajathans9@gmail.com>
1 Sbgodin <Sbgodin@users.noreply.github.com>
+ 1 Sebastien Wains <sebw@users.noreply.github.com>
1 TsT <tst2005@gmail.com>
1 agentcobra <agentcobra@free.fr>
+ 1 aguy <aguytech@users.noreply.github.com>
1 dimtion <zizou.xena@gmail.com>
1 durcheinandr <jochen@durcheinandr.de>
1 lapineige <lapineige@users.noreply.github.com>
+ 1 rfolo9li <50079896+rfolo9li@users.noreply.github.com>
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
+## [v0.12.0](https://github.com/shaarli/Shaarli/releases/tag/v0.12.0-beta) - UNRELEASED [beta 2020-08-27]
+
+**Save you `data/` folder before updating!**
+
+This is a beta version containing major changes, including new URLs for Shaarli and datastore format update.
+Be aware that by using a beta version you might encounter bugs, and that 3rd party themes or plugins might not be compatible.
+
+### Added
+- Thumbnailer: add soundcloud.com to list of common media domains
+- Markdown rendering is now integrated into Shaarli core
+- Add autofocus on tag cloud filter input
+- Japanese translations
+- Support for local anchor URL (startting with `#`)
+- LDAP authentication
+- Encapsulated PageCacheManager
+- Docs:
+ - add screenshots of all pages
+ - section about mkdocs
+ - Ulauncher extension
+- CI: run against PHP 7.4
+
+### Changed
+- Introduce Bookmark object and Service layer
+ - Save bookmark as objects in the datastore
+ - Handle bookmark as objects across the whole codebase (except templates and plugins)
+- Process all Shaarli page through Slim controller, with proper URL rewriting (see #1516)
+- ATOM feed: use instance name as author name instead of URL
+- Updated French translation
+- Docs:
+ - Troubleshooting page rewritten
+ - Updated unit tests page
+ - Updated Server security page
+
+### Fixed
+- Undefined index: thumbnail in daily page
+- Undefined index: thumbnail on OpenGraph headers
+- Undefined index: updated on linklist
+- Make sure that bookmark sort is consistent, even with equal timestamps
+- Code PHP version check as requirement bumped to PHP 7.1
+- Thumbnail images lazy loading
+- Markdown plugin: fix RSS feed direct link reverse
+- Fix RSS permalink included in Markdown bloc
+- Demo plugin: multiple typos
+- Makefile target for releases
+- Makefile target for html documentation
+- Session cookie setting being set while session is active
+- Deprecated use of implode
+- Division by zero in tag cloud
+- CI: deprecated linux distribution and sudo directive
+- Docker build: gcc is no longer included in python alpine image
+- Docs:
+ - Outdated Docker documentation for stable branch
+ - Outdated links
+ - Plugin description in meta files
+
+### Removed
+- Markdown plugin
+- Docs:
+ - emojione & twemoji removed
+
## [v0.11.1](https://github.com/shaarli/Shaarli/releases/tag/v0.11.1) - 2019-08-03
Release to fix broken Docker build on the latest version.
FROM python:3-alpine as docs
ADD . /usr/src/app/shaarli
RUN cd /usr/src/app/shaarli \
+ && apk add --no-cache gcc musl-dev \
&& pip install --no-cache-dir mkdocs \
&& mkdocs build --clean
use Shaarli\Config\ConfigManager;
use WebThumbnailer\Application\ConfigManager as WTConfigManager;
-use WebThumbnailer\Exception\WebThumbnailerException;
use WebThumbnailer\WebThumbnailer;
/**
try {
return $this->wt->thumbnail($url);
- } catch (WebThumbnailerException $e) {
+ } catch (\Throwable $e) {
// Exceptions are only thrown in debug mode.
error_log(get_class($e) . ': ' . $e->getMessage());
}
*
* @param mixed $input Data to escape: a single string or an array of strings.
*
- * @return string escaped.
+ * @return string|array escaped.
*/
function escape($input)
{
+ if (null === $input) {
+ return null;
+ }
+
if (is_bool($input)) {
return $input;
}
* Requires php-intl to display international datetimes,
* otherwise default format '%c' will be returned.
*
- * @param DateTime $date to format.
- * @param bool $time Displays time if true.
- * @param bool $intl Use international format if true.
+ * @param DateTimeInterface $date to format.
+ * @param bool $time Displays time if true.
+ * @param bool $intl Use international format if true.
*
* @return bool|string Formatted date, or false if the input is invalid.
*/
function format_date($date, $time = true, $intl = true)
{
- if (! $date instanceof DateTime) {
+ if (! $date instanceof DateTimeInterface) {
return false;
}
$response = $e->getApiResponse();
}
- return $response;
+ return $response
+ ->withHeader('Access-Control-Allow-Origin', '*')
+ ->withHeader(
+ 'Access-Control-Allow-Headers',
+ 'X-Requested-With, Content-Type, Accept, Origin, Authorization'
+ )
+ ->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
+ ;
}
/**
if (! $bookmark->isNote()) {
$out['url'] = $bookmark->getUrl();
} else {
- $out['url'] = $indexUrl . $bookmark->getUrl();
+ $out['url'] = rtrim($indexUrl, '/') . '/' . ltrim($bookmark->getUrl(), '/');
}
$out['shorturl'] = $bookmark->getShortUrl();
$out['title'] = $bookmark->getTitle();
namespace Shaarli\Bookmark;
use DateTime;
+use DateTimeInterface;
use Shaarli\Bookmark\Exception\InvalidBookmarkException;
/**
/** @var array List of bookmark's tags */
protected $tags;
- /** @var string Thumbnail's URL - false if no thumbnail could be found */
+ /** @var string|bool|null Thumbnail's URL - initialized at null, false if no thumbnail could be found */
protected $thumbnail;
/** @var bool Set to true if the bookmark is set as sticky */
protected $sticky;
- /** @var DateTime Creation datetime */
+ /** @var DateTimeInterface Creation datetime */
protected $created;
- /** @var DateTime Update datetime */
+ /** @var DateTimeInterface datetime */
protected $updated;
/** @var bool True if the bookmark can only be seen while logged in */
|| ! is_int($this->id)
|| empty($this->shortUrl)
|| empty($this->created)
- || ! $this->created instanceof DateTime
+ || ! $this->created instanceof DateTimeInterface
) {
throw new InvalidBookmarkException($this);
}
if (empty($this->url)) {
- $this->url = '?'. $this->shortUrl;
+ $this->url = '/shaare/'. $this->shortUrl;
}
if (empty($this->title)) {
$this->title = $this->url;
/**
* Get the Created.
*
- * @return DateTime
+ * @return DateTimeInterface
*/
public function getCreated()
{
/**
* Get the Updated.
*
- * @return DateTime
+ * @return DateTimeInterface
*/
public function getUpdated()
{
* Set the Created.
* Note: you shouldn't set this manually except for special cases (like bookmark import)
*
- * @param DateTime $created
+ * @param DateTimeInterface $created
*
* @return Bookmark
*/
/**
* Set the Updated.
*
- * @param DateTime $updated
+ * @param DateTimeInterface $updated
*
* @return Bookmark
*/
/**
* Get the Thumbnail.
*
- * @return string|bool
+ * @return string|bool|null Thumbnail's URL - initialized at null, false if no thumbnail could be found
*/
public function getThumbnail()
{
/**
* Set the Thumbnail.
*
- * @param string|bool $thumbnail
+ * @param string|bool $thumbnail Thumbnail's URL - false if no thumbnail could be found
*
* @return Bookmark
*/
public function isNote()
{
// We check empty value to get a valid result if the link has not been saved yet
- return empty($this->url) || $this->url[0] === '?';
+ return empty($this->url) || startsWith($this->url, '/shaare/') || $this->url[0] === '?';
}
/**
use Exception;
use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
+use Shaarli\Bookmark\Exception\DatastoreNotInitializedException;
use Shaarli\Bookmark\Exception\EmptyDataStoreException;
use Shaarli\Config\ConfigManager;
use Shaarli\Formatter\BookmarkMarkdownFormatter;
use Shaarli\History;
use Shaarli\Legacy\LegacyLinkDB;
use Shaarli\Legacy\LegacyUpdater;
+use Shaarli\Render\PageCacheManager;
use Shaarli\Updater\UpdaterUtils;
/**
/** @var History instance */
protected $history;
+ /** @var PageCacheManager instance */
+ protected $pageCacheManager;
+
/** @var bool true for logged in users. Default value to retrieve private bookmarks. */
protected $isLoggedIn;
{
$this->conf = $conf;
$this->history = $history;
+ $this->pageCacheManager = new PageCacheManager($this->conf->get('resource.page_cache'), $isLoggedIn);
$this->bookmarksIO = new BookmarkIO($this->conf);
$this->isLoggedIn = $isLoggedIn;
} else {
try {
$this->bookmarks = $this->bookmarksIO->read();
- } catch (EmptyDataStoreException $e) {
+ } catch (EmptyDataStoreException|DatastoreNotInitializedException $e) {
$this->bookmarks = new BookmarkArray();
- if ($isLoggedIn) {
- $this->save();
+
+ if ($this->isLoggedIn) {
+ // Datastore file does not exists, we initialize it with default bookmarks.
+ if ($e instanceof DatastoreNotInitializedException) {
+ $this->initialize();
+ } else {
+ $this->save();
+ }
}
}
throw new Exception('Not authorized');
}
- return $bookmark;
+ return $first;
}
/**
*/
public function set($bookmark, $save = true)
{
- if ($this->isLoggedIn !== true) {
+ if (true !== $this->isLoggedIn) {
throw new Exception(t('You\'re not authorized to alter the datastore'));
}
if (! $bookmark instanceof Bookmark) {
*/
public function add($bookmark, $save = true)
{
- if ($this->isLoggedIn !== true) {
+ if (true !== $this->isLoggedIn) {
throw new Exception(t('You\'re not authorized to alter the datastore'));
}
if (! $bookmark instanceof Bookmark) {
*/
public function addOrSet($bookmark, $save = true)
{
- if ($this->isLoggedIn !== true) {
+ if (true !== $this->isLoggedIn) {
throw new Exception(t('You\'re not authorized to alter the datastore'));
}
if (! $bookmark instanceof Bookmark) {
*/
public function remove($bookmark, $save = true)
{
- if ($this->isLoggedIn !== true) {
+ if (true !== $this->isLoggedIn) {
throw new Exception(t('You\'re not authorized to alter the datastore'));
}
if (! $bookmark instanceof Bookmark) {
*/
public function save()
{
- if (!$this->isLoggedIn) {
+ if (true !== $this->isLoggedIn) {
// TODO: raise an Exception instead
die('You are not authorized to change the database.');
}
+
$this->bookmarks->reorder();
$this->bookmarksIO->write($this->bookmarks);
- invalidateCaches($this->conf->get('resource.page_cache'));
+ $this->pageCacheManager->invalidateCaches();
}
/**
if (empty($tag)
|| (! $this->isLoggedIn && startsWith($tag, '.'))
|| $tag === BookmarkMarkdownFormatter::NO_MD_TAG
+ || in_array($tag, $filteringTags, true)
) {
continue;
}
{
$initializer = new BookmarkInitializer($this);
$initializer->initialize();
+
+ if (true === $this->isLoggedIn) {
+ $this->save();
+ }
}
/**
throw new Exception('Invalid date format');
}
- $filtered = array();
+ $filtered = [];
foreach ($this->bookmarks as $key => $l) {
if ($l->getCreated()->format('Ymd') == $day) {
$filtered[$key] = $l;
namespace Shaarli\Bookmark;
+use Shaarli\Bookmark\Exception\DatastoreNotInitializedException;
use Shaarli\Bookmark\Exception\EmptyDataStoreException;
use Shaarli\Bookmark\Exception\NotWritableDataStoreException;
use Shaarli\Config\ConfigManager;
*
* @return BookmarkArray instance
*
- * @throws NotWritableDataStoreException Data couldn't be loaded
- * @throws EmptyDataStoreException Datastore doesn't exist
+ * @throws NotWritableDataStoreException Data couldn't be loaded
+ * @throws EmptyDataStoreException Datastore file exists but does not contain any bookmark
+ * @throws DatastoreNotInitializedException File does not exists
*/
public function read()
{
if (! file_exists($this->datastore)) {
- throw new EmptyDataStoreException();
+ throw new DatastoreNotInitializedException();
}
if (!is_writable($this->datastore)) {
$this->datastore,
self::$phpPrefix.base64_encode(gzdeflate(serialize($links))).self::$phpSuffix
);
-
- invalidateCaches($this->conf->get('resource.page_cache'));
}
}
* Class BookmarkInitializer
*
* This class is used to initialized default bookmarks after a fresh install of Shaarli.
- * It is no longer call when the data store is empty,
- * because user might want to delete default bookmarks after the install.
+ * It should be only called if the datastore file does not exist(users might want to delete the default bookmarks).
*
* To prevent data corruption, it does not overwrite existing bookmarks,
* even though there should not be any.
{
$bookmark = new Bookmark();
$bookmark->setTitle(t('My secret stuff... - Pastebin.com'));
- $bookmark->setUrl('http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=', []);
+ $bookmark->setUrl('http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=');
$bookmark->setDescription(t('Shhhh! I\'m a private link only YOU can see. You can delete me too.'));
$bookmark->setTagsString('secretstuff');
$bookmark->setPrivate(true);
- $this->bookmarkService->add($bookmark);
+ $this->bookmarkService->add($bookmark, false);
$bookmark = new Bookmark();
$bookmark->setTitle(t('The personal, minimalist, super-fast, database free, bookmarking service'));
You use the community supported version of the original Shaarli project, by Sebastien Sauvage.'
));
$bookmark->setTagsString('opensource software');
- $this->bookmarkService->add($bookmark);
+ $this->bookmarkService->add($bookmark, false);
}
}
use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
use Shaarli\Bookmark\Exception\NotWritableDataStoreException;
use Shaarli\Config\ConfigManager;
-use Shaarli\Exceptions\IOException;
use Shaarli\History;
/**
use Shaarli\Bookmark\Bookmark;
-/**
- * Get cURL callback function for CURLOPT_WRITEFUNCTION
- *
- * @param string $charset to extract from the downloaded page (reference)
- * @param string $title to extract from the downloaded page (reference)
- * @param string $description to extract from the downloaded page (reference)
- * @param string $keywords to extract from the downloaded page (reference)
- * @param bool $retrieveDescription Automatically tries to retrieve description and keywords from HTML content
- * @param string $curlGetInfo Optionally overrides curl_getinfo function
- *
- * @return Closure
- */
-function get_curl_download_callback(
- &$charset,
- &$title,
- &$description,
- &$keywords,
- $retrieveDescription,
- $curlGetInfo = 'curl_getinfo'
-) {
- $isRedirected = false;
- $currentChunk = 0;
- $foundChunk = null;
-
- /**
- * cURL callback function for CURLOPT_WRITEFUNCTION (called during the download).
- *
- * While downloading the remote page, we check that the HTTP code is 200 and content type is 'html/text'
- * Then we extract the title and the charset and stop the download when it's done.
- *
- * @param resource $ch cURL resource
- * @param string $data chunk of data being downloaded
- *
- * @return int|bool length of $data or false if we need to stop the download
- */
- return function (&$ch, $data) use (
- $retrieveDescription,
- $curlGetInfo,
- &$charset,
- &$title,
- &$description,
- &$keywords,
- &$isRedirected,
- &$currentChunk,
- &$foundChunk
- ) {
- $currentChunk++;
- $responseCode = $curlGetInfo($ch, CURLINFO_RESPONSE_CODE);
- if (!empty($responseCode) && in_array($responseCode, [301, 302])) {
- $isRedirected = true;
- return strlen($data);
- }
- if (!empty($responseCode) && $responseCode !== 200) {
- return false;
- }
- // After a redirection, the content type will keep the previous request value
- // until it finds the next content-type header.
- if (! $isRedirected || strpos(strtolower($data), 'content-type') !== false) {
- $contentType = $curlGetInfo($ch, CURLINFO_CONTENT_TYPE);
- }
- if (!empty($contentType) && strpos($contentType, 'text/html') === false) {
- return false;
- }
- if (!empty($contentType) && empty($charset)) {
- $charset = header_extract_charset($contentType);
- }
- if (empty($charset)) {
- $charset = html_extract_charset($data);
- }
- if (empty($title)) {
- $title = html_extract_title($data);
- $foundChunk = ! empty($title) ? $currentChunk : $foundChunk;
- }
- if ($retrieveDescription && empty($description)) {
- $description = html_extract_tag('description', $data);
- $foundChunk = ! empty($description) ? $currentChunk : $foundChunk;
- }
- if ($retrieveDescription && empty($keywords)) {
- $keywords = html_extract_tag('keywords', $data);
- if (! empty($keywords)) {
- $foundChunk = $currentChunk;
- // Keywords use the format tag1, tag2 multiple words, tag
- // So we format them to match Shaarli's separator and glue multiple words with '-'
- $keywords = implode(' ', array_map(function($keyword) {
- return implode('-', preg_split('/\s+/', trim($keyword)));
- }, explode(',', $keywords)));
- }
- }
-
- // We got everything we want, stop the download.
- // If we already found either the title, description or keywords,
- // it's highly unlikely that we'll found the other metas further than
- // in the same chunk of data or the next one. So we also stop the download after that.
- if ((!empty($responseCode) && !empty($contentType) && !empty($charset)) && $foundChunk !== null
- && (! $retrieveDescription
- || $foundChunk < $currentChunk
- || (!empty($title) && !empty($description) && !empty($keywords))
- )
- ) {
- return false;
- }
-
- return strlen($data);
- };
-}
-
/**
* Extract title from an HTML document.
*
* \p{Mn} - any non marking space (accents, umlauts, etc)
*/
$regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui';
- $replacement = '$1<a href="'. $indexUrl .'?addtag=$2" title="Hashtag $2">#$2</a>';
+ $replacement = '$1<a href="'. $indexUrl .'./add-tag/$2" title="Hashtag $2">#$2</a>';
return preg_replace($regex, $replacement, $description);
}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Bookmark\Exception;
+
+class DatastoreNotInitializedException extends \Exception
+{
+
+}
use Shaarli\Config\Exception\MissingFieldConfigException;
use Shaarli\Config\Exception\UnauthorizedConfigException;
+use Shaarli\Thumbnailer;
/**
* Class ConfigManager
$this->setEmpty('security.open_shaarli', false);
$this->setEmpty('security.allowed_protocols', ['ftp', 'ftps', 'magnet']);
- $this->setEmpty('general.header_link', '?');
+ $this->setEmpty('general.header_link', '/');
$this->setEmpty('general.links_per_page', 20);
$this->setEmpty('general.enabled_plugins', self::$DEFAULT_PLUGINS);
$this->setEmpty('general.default_note_title', 'Note: ');
// default state of the 'remember me' checkbox of the login form
$this->setEmpty('privacy.remember_user_default', true);
+ $this->setEmpty('thumbnails.mode', Thumbnailer::MODE_ALL);
$this->setEmpty('thumbnails.width', '125');
$this->setEmpty('thumbnails.height', '90');
<?php
use Shaarli\Config\Exception\PluginConfigOrderException;
+use Shaarli\Plugin\PluginManager;
/**
* Plugin configuration helper functions.
*/
function save_plugin_config($formData)
{
+ // We can only save existing plugins
+ $directories = str_replace(
+ PluginManager::$PLUGINS_PATH . '/',
+ '',
+ glob(PluginManager::$PLUGINS_PATH . '/*')
+ );
+ $formData = array_filter(
+ $formData,
+ function ($value, string $key) use ($directories) {
+ return startsWith($key, 'order') || in_array($key, $directories);
+ },
+ ARRAY_FILTER_USE_BOTH
+ );
+
// Make sure there are no duplicates in orders.
if (!validate_plugin_order($formData)) {
throw new PluginConfigOrderException();
$orders = array();
foreach ($formData as $key => $value) {
// No duplicate order allowed.
- if (in_array($value, $orders)) {
+ if (in_array($value, $orders, true)) {
return false;
}
use Shaarli\Bookmark\BookmarkFileService;
use Shaarli\Bookmark\BookmarkServiceInterface;
use Shaarli\Config\ConfigManager;
+use Shaarli\Feed\FeedBuilder;
+use Shaarli\Formatter\FormatterFactory;
+use Shaarli\Front\Controller\Visitor\ErrorController;
use Shaarli\History;
+use Shaarli\Http\HttpAccess;
+use Shaarli\Netscape\NetscapeBookmarkUtils;
use Shaarli\Plugin\PluginManager;
use Shaarli\Render\PageBuilder;
+use Shaarli\Render\PageCacheManager;
+use Shaarli\Security\CookieManager;
use Shaarli\Security\LoginManager;
use Shaarli\Security\SessionManager;
+use Shaarli\Thumbnailer;
+use Shaarli\Updater\Updater;
+use Shaarli\Updater\UpdaterUtils;
/**
* Class ContainerBuilder
/** @var SessionManager */
protected $session;
+ /** @var CookieManager */
+ protected $cookieManager;
+
/** @var LoginManager */
protected $login;
- public function __construct(ConfigManager $conf, SessionManager $session, LoginManager $login)
- {
+ /** @var string|null */
+ protected $basePath = null;
+
+ public function __construct(
+ ConfigManager $conf,
+ SessionManager $session,
+ CookieManager $cookieManager,
+ LoginManager $login
+ ) {
$this->conf = $conf;
$this->session = $session;
$this->login = $login;
+ $this->cookieManager = $cookieManager;
}
public function build(): ShaarliContainer
{
$container = new ShaarliContainer();
+
$container['conf'] = $this->conf;
$container['sessionManager'] = $this->session;
+ $container['cookieManager'] = $this->cookieManager;
$container['loginManager'] = $this->login;
+ $container['basePath'] = $this->basePath;
+
$container['plugins'] = function (ShaarliContainer $container): PluginManager {
return new PluginManager($container->conf);
};
};
$container['pluginManager'] = function (ShaarliContainer $container): PluginManager {
- return new PluginManager($container->conf);
+ $pluginManager = new PluginManager($container->conf);
+
+ $pluginManager->load($container->conf->get('general.enabled_plugins'));
+
+ return $pluginManager;
+ };
+
+ $container['formatterFactory'] = function (ShaarliContainer $container): FormatterFactory {
+ return new FormatterFactory(
+ $container->conf,
+ $container->loginManager->isLoggedIn()
+ );
+ };
+
+ $container['pageCacheManager'] = function (ShaarliContainer $container): PageCacheManager {
+ return new PageCacheManager(
+ $container->conf->get('resource.page_cache'),
+ $container->loginManager->isLoggedIn()
+ );
+ };
+
+ $container['feedBuilder'] = function (ShaarliContainer $container): FeedBuilder {
+ return new FeedBuilder(
+ $container->bookmarkService,
+ $container->formatterFactory->getFormatter(),
+ $container->environment,
+ $container->loginManager->isLoggedIn()
+ );
+ };
+
+ $container['thumbnailer'] = function (ShaarliContainer $container): Thumbnailer {
+ return new Thumbnailer($container->conf);
+ };
+
+ $container['httpAccess'] = function (): HttpAccess {
+ return new HttpAccess();
+ };
+
+ $container['netscapeBookmarkUtils'] = function (ShaarliContainer $container): NetscapeBookmarkUtils {
+ return new NetscapeBookmarkUtils($container->bookmarkService, $container->conf, $container->history);
+ };
+
+ $container['updater'] = function (ShaarliContainer $container): Updater {
+ return new Updater(
+ UpdaterUtils::read_updates_file($container->conf->get('resource.updates')),
+ $container->bookmarkService,
+ $container->conf,
+ $container->loginManager->isLoggedIn()
+ );
+ };
+
+ $container['errorHandler'] = function (ShaarliContainer $container): ErrorController {
+ return new ErrorController($container);
+ };
+ $container['phpErrorHandler'] = function (ShaarliContainer $container): ErrorController {
+ return new ErrorController($container);
};
return $container;
use Shaarli\Bookmark\BookmarkServiceInterface;
use Shaarli\Config\ConfigManager;
+use Shaarli\Feed\FeedBuilder;
+use Shaarli\Formatter\FormatterFactory;
use Shaarli\History;
+use Shaarli\Http\HttpAccess;
+use Shaarli\Netscape\NetscapeBookmarkUtils;
use Shaarli\Plugin\PluginManager;
use Shaarli\Render\PageBuilder;
+use Shaarli\Render\PageCacheManager;
+use Shaarli\Security\CookieManager;
use Shaarli\Security\LoginManager;
use Shaarli\Security\SessionManager;
+use Shaarli\Thumbnailer;
+use Shaarli\Updater\Updater;
use Slim\Container;
/**
* Extension of Slim container to document the injected objects.
*
+ * @property string $basePath Shaarli's instance base path (e.g. `/shaarli/`)
+ * @property BookmarkServiceInterface $bookmarkService
+ * @property CookieManager $cookieManager
* @property ConfigManager $conf
- * @property SessionManager $sessionManager
- * @property LoginManager $loginManager
+ * @property mixed[] $environment $_SERVER automatically injected by Slim
+ * @property callable $errorHandler Overrides default Slim exception display
+ * @property FeedBuilder $feedBuilder
+ * @property FormatterFactory $formatterFactory
* @property History $history
- * @property BookmarkServiceInterface $bookmarkService
+ * @property HttpAccess $httpAccess
+ * @property LoginManager $loginManager
+ * @property NetscapeBookmarkUtils $netscapeBookmarkUtils
* @property PageBuilder $pageBuilder
+ * @property PageCacheManager $pageCacheManager
+ * @property callable $phpErrorHandler Overrides default Slim PHP error display
* @property PluginManager $pluginManager
+ * @property SessionManager $sessionManager
+ * @property Thumbnailer $thumbnailer
+ * @property Updater $updater
*/
class ShaarliContainer extends Container
{
+++ /dev/null
-<?php
-/**
- * Cache utilities
- */
-
-/**
- * Purges all cached pages
- *
- * @param string $pageCacheDir page cache directory
- *
- * @return mixed an error string if the directory is missing
- */
-function purgeCachedPages($pageCacheDir)
-{
- if (! is_dir($pageCacheDir)) {
- $error = sprintf(t('Cannot purge %s: no directory'), $pageCacheDir);
- error_log($error);
- return $error;
- }
-
- array_map('unlink', glob($pageCacheDir.'/*.cache'));
-}
-
-/**
- * Invalidates caches when the database is changed or the user logs out.
- *
- * @param string $pageCacheDir page cache directory
- */
-function invalidateCaches($pageCacheDir)
-{
- // Purge cache attached to session.
- if (isset($_SESSION['tags'])) {
- unset($_SESSION['tags']);
- }
-
- // Purge page cache shared by sessions.
- purgeCachedPages($pageCacheDir);
-}
*/
protected $formatter;
- /**
- * @var string RSS or ATOM feed.
- */
- protected $feedType;
-
- /**
- * @var array $_SERVER
- */
+ /** @var mixed[] $_SERVER */
protected $serverInfo;
- /**
- * @var array $_GET
- */
- protected $userInput;
-
/**
* @var boolean True if the user is currently logged in, false otherwise.
*/
* @var string server locale.
*/
protected $locale;
-
/**
* @var DateTime Latest item date.
*/
*
* @param BookmarkServiceInterface $linkDB LinkDB instance.
* @param BookmarkFormatter $formatter instance.
- * @param string $feedType Type of feed.
* @param array $serverInfo $_SERVER.
- * @param array $userInput $_GET.
* @param boolean $isLoggedIn True if the user is currently logged in, false otherwise.
*/
- public function __construct($linkDB, $formatter, $feedType, $serverInfo, $userInput, $isLoggedIn)
+ public function __construct($linkDB, $formatter, $serverInfo, $isLoggedIn)
{
$this->linkDB = $linkDB;
$this->formatter = $formatter;
- $this->feedType = $feedType;
$this->serverInfo = $serverInfo;
- $this->userInput = $userInput;
$this->isLoggedIn = $isLoggedIn;
}
/**
* Build data for feed templates.
*
+ * @param string $feedType Type of feed (RSS/ATOM).
+ * @param array $userInput $_GET.
+ *
* @return array Formatted data for feeds templates.
*/
- public function buildData()
+ public function buildData(string $feedType, ?array $userInput)
{
// Search for untagged bookmarks
- if (isset($this->userInput['searchtags']) && empty($this->userInput['searchtags'])) {
- $this->userInput['searchtags'] = false;
+ if (isset($this->userInput['searchtags']) && empty($userInput['searchtags'])) {
+ $userInput['searchtags'] = false;
}
// Optionally filter the results:
- $linksToDisplay = $this->linkDB->search($this->userInput);
+ $linksToDisplay = $this->linkDB->search($userInput);
- $nblinksToDisplay = $this->getNbLinks(count($linksToDisplay));
+ $nblinksToDisplay = $this->getNbLinks(count($linksToDisplay), $userInput);
// Can't use array_keys() because $link is a LinkDB instance and not a real array.
$keys = array();
$this->formatter->addContextData('index_url', $pageaddr);
$linkDisplayed = array();
for ($i = 0; $i < $nblinksToDisplay && $i < count($keys); $i++) {
- $linkDisplayed[$keys[$i]] = $this->buildItem($linksToDisplay[$keys[$i]], $pageaddr);
+ $linkDisplayed[$keys[$i]] = $this->buildItem($feedType, $linksToDisplay[$keys[$i]], $pageaddr);
}
- $data['language'] = $this->getTypeLanguage();
- $data['last_update'] = $this->getLatestDateFormatted();
+ $data['language'] = $this->getTypeLanguage($feedType);
+ $data['last_update'] = $this->getLatestDateFormatted($feedType);
$data['show_dates'] = !$this->hideDates || $this->isLoggedIn;
// Remove leading slash from REQUEST_URI.
$data['self_link'] = escape(server_url($this->serverInfo))
return $data;
}
+ /**
+ * Set this to true to use permalinks instead of direct bookmarks.
+ *
+ * @param boolean $usePermalinks true to force permalinks.
+ */
+ public function setUsePermalinks($usePermalinks)
+ {
+ $this->usePermalinks = $usePermalinks;
+ }
+
+ /**
+ * Set this to true to hide timestamps in feeds.
+ *
+ * @param boolean $hideDates true to enable.
+ */
+ public function setHideDates($hideDates)
+ {
+ $this->hideDates = $hideDates;
+ }
+
+ /**
+ * Set the locale. Used to show feed language.
+ *
+ * @param string $locale The locale (eg. 'fr_FR.UTF8').
+ */
+ public function setLocale($locale)
+ {
+ $this->locale = strtolower($locale);
+ }
+
/**
* Build a feed item (one per shaare).
*
+ * @param string $feedType Type of feed (RSS/ATOM).
* @param Bookmark $link Single link array extracted from LinkDB.
* @param string $pageaddr Index URL.
*
* @return array Link array with feed attributes.
*/
- protected function buildItem($link, $pageaddr)
+ protected function buildItem(string $feedType, $link, $pageaddr)
{
$data = $this->formatter->format($link);
- $data['guid'] = $pageaddr . '?' . $data['shorturl'];
+ $data['guid'] = rtrim($pageaddr, '/') . '/shaare/' . $data['shorturl'];
if ($this->usePermalinks === true) {
$permalink = '<a href="'. $data['url'] .'" title="'. t('Direct link') .'">'. t('Direct link') .'</a>';
} else {
}
$data['description'] .= PHP_EOL . PHP_EOL . '<br>— ' . $permalink;
- $data['pub_iso_date'] = $this->getIsoDate($data['created']);
+ $data['pub_iso_date'] = $this->getIsoDate($feedType, $data['created']);
// atom:entry elements MUST contain exactly one atom:updated element.
if (!empty($link->getUpdated())) {
- $data['up_iso_date'] = $this->getIsoDate($data['updated'], DateTime::ATOM);
+ $data['up_iso_date'] = $this->getIsoDate($feedType, $data['updated'], DateTime::ATOM);
} else {
- $data['up_iso_date'] = $this->getIsoDate($data['created'], DateTime::ATOM);
+ $data['up_iso_date'] = $this->getIsoDate($feedType, $data['created'], DateTime::ATOM);
}
// Save the more recent item.
return $data;
}
- /**
- * Set this to true to use permalinks instead of direct bookmarks.
- *
- * @param boolean $usePermalinks true to force permalinks.
- */
- public function setUsePermalinks($usePermalinks)
- {
- $this->usePermalinks = $usePermalinks;
- }
-
- /**
- * Set this to true to hide timestamps in feeds.
- *
- * @param boolean $hideDates true to enable.
- */
- public function setHideDates($hideDates)
- {
- $this->hideDates = $hideDates;
- }
-
- /**
- * Set the locale. Used to show feed language.
- *
- * @param string $locale The locale (eg. 'fr_FR.UTF8').
- */
- public function setLocale($locale)
- {
- $this->locale = strtolower($locale);
- }
-
/**
* Get the language according to the feed type, based on the locale:
*
* - RSS format: en-us (default: 'en-en').
* - ATOM format: fr (default: 'en').
*
+ * @param string $feedType Type of feed (RSS/ATOM).
+ *
* @return string The language.
*/
- public function getTypeLanguage()
+ protected function getTypeLanguage(string $feedType)
{
// Use the locale do define the language, if available.
if (!empty($this->locale) && preg_match('/^\w{2}[_\-]\w{2}/', $this->locale)) {
- $length = ($this->feedType === self::$FEED_RSS) ? 5 : 2;
+ $length = ($feedType === self::$FEED_RSS) ? 5 : 2;
return str_replace('_', '-', substr($this->locale, 0, $length));
}
- return ($this->feedType === self::$FEED_RSS) ? 'en-en' : 'en';
+ return ($feedType === self::$FEED_RSS) ? 'en-en' : 'en';
}
/**
*
* Return an empty string if invalid DateTime is passed.
*
+ * @param string $feedType Type of feed (RSS/ATOM).
+ *
* @return string Formatted date.
*/
- protected function getLatestDateFormatted()
+ protected function getLatestDateFormatted(string $feedType)
{
if (empty($this->latestDate) || !$this->latestDate instanceof DateTime) {
return '';
}
- $type = ($this->feedType == self::$FEED_RSS) ? DateTime::RSS : DateTime::ATOM;
+ $type = ($feedType == self::$FEED_RSS) ? DateTime::RSS : DateTime::ATOM;
return $this->latestDate->format($type);
}
/**
* Get ISO date from DateTime according to feed type.
*
+ * @param string $feedType Type of feed (RSS/ATOM).
* @param DateTime $date Date to format.
* @param string|bool $format Force format.
*
* @return string Formatted date.
*/
- protected function getIsoDate(DateTime $date, $format = false)
+ protected function getIsoDate(string $feedType, DateTime $date, $format = false)
{
if ($format !== false) {
return $date->format($format);
}
- if ($this->feedType == self::$FEED_RSS) {
+ if ($feedType == self::$FEED_RSS) {
return $date->format(DateTime::RSS);
}
return $date->format(DateTime::ATOM);
* If 'nb' not set or invalid, default value: $DEFAULT_NB_LINKS.
* If 'nb' is set to 'all', display all filtered bookmarks (max parameter).
*
- * @param int $max maximum number of bookmarks to display.
+ * @param int $max maximum number of bookmarks to display.
+ * @param array $userInput $_GET.
*
* @return int number of bookmarks to display.
*/
- public function getNbLinks($max)
+ protected function getNbLinks($max, ?array $userInput)
{
- if (empty($this->userInput['nb'])) {
+ if (empty($userInput['nb'])) {
return self::$DEFAULT_NB_LINKS;
}
- if ($this->userInput['nb'] == 'all') {
+ if ($userInput['nb'] == 'all') {
return $max;
}
- $intNb = intval($this->userInput['nb']);
+ $intNb = intval($userInput['nb']);
if (!is_int($intNb) || $intNb == 0) {
return self::$DEFAULT_NB_LINKS;
}
*/
public function formatUrl($bookmark)
{
- if (! empty($this->contextData['index_url']) && (
- startsWith($bookmark->getUrl(), '?') || startsWith($bookmark->getUrl(), '/')
- )) {
- return $this->contextData['index_url'] . escape($bookmark->getUrl());
+ if ($bookmark->isNote() && isset($this->contextData['index_url'])) {
+ return rtrim($this->contextData['index_url'], '/') . '/' . escape(ltrim($bookmark->getUrl(), '/'));
}
+
return escape($bookmark->getUrl());
}
*/
protected function formatRealUrl($bookmark)
{
- if (! empty($this->contextData['index_url']) && (
- startsWith($bookmark->getUrl(), '?') || startsWith($bookmark->getUrl(), '/')
- )) {
- return $this->contextData['index_url'] . escape($bookmark->getUrl());
+ if ($bookmark->isNote()) {
+ if (isset($this->contextData['index_url'])) {
+ $prefix = rtrim($this->contextData['index_url'], '/') . '/';
+ }
+
+ if (isset($this->contextData['base_path'])) {
+ $prefix = rtrim($this->contextData['base_path'], '/') . '/';
+ }
+
+ return escape($prefix ?? '') . escape(ltrim($bookmark->getUrl(), '/'));
}
+
return escape($bookmark->getUrl());
}
namespace Shaarli\Formatter;
use DateTime;
-use Shaarli\Config\ConfigManager;
use Shaarli\Bookmark\Bookmark;
+use Shaarli\Config\ConfigManager;
/**
* Class BookmarkFormatter
public function addContextData($key, $value)
{
$this->contextData[$key] = $value;
+
+ return $this;
}
/**
*/
protected function formatRealUrl($bookmark)
{
- return $bookmark->getUrl();
+ return $this->formatUrl($bookmark);
}
/**
/**
* Replace hashtag in Markdown links format
- * E.g. `#hashtag` becomes `[#hashtag](?addtag=hashtag)`
+ * E.g. `#hashtag` becomes `[#hashtag](./add-tag/hashtag)`
* It includes the index URL if specified.
*
* @param string $description
* \p{Mn} - any non marking space (accents, umlauts, etc)
*/
$regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui';
- $replacement = '$1[#$2]('. $indexUrl .'?addtag=$2)';
+ $replacement = '$1[#$2]('. $indexUrl .'./add-tag/$2)';
$descriptionLines = explode(PHP_EOL, $description);
$descriptionOut = '';
*
* @return BookmarkFormatter instance.
*/
- public function getFormatter(string $type = null)
+ public function getFormatter(string $type = null): BookmarkFormatter
{
$type = $type ? $type : $this->conf->get('formatter', 'default');
$className = '\\Shaarli\\Formatter\\Bookmark'. ucfirst($type) .'Formatter';
--- /dev/null
+<?php
+
+namespace Shaarli\Front;
+
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+/**
+ * Middleware used for controller requiring to be authenticated.
+ * It extends ShaarliMiddleware, and just make sure that the user is authenticated.
+ * Otherwise, it redirects to the login page.
+ */
+class ShaarliAdminMiddleware extends ShaarliMiddleware
+{
+ public function __invoke(Request $request, Response $response, callable $next): Response
+ {
+ $this->initBasePath($request);
+
+ if (true !== $this->container->loginManager->isLoggedIn()) {
+ $returnUrl = urlencode($this->container->environment['REQUEST_URI']);
+
+ return $response->withRedirect($this->container->basePath . '/login?returnurl=' . $returnUrl);
+ }
+
+ return parent::__invoke($request, $response, $next);
+ }
+}
namespace Shaarli\Front;
use Shaarli\Container\ShaarliContainer;
-use Shaarli\Front\Exception\ShaarliException;
+use Shaarli\Front\Exception\UnauthorizedException;
use Slim\Http\Request;
use Slim\Http\Response;
/**
* Middleware execution:
+ * - run updates
+ * - if not logged in open shaarli, redirect to login
* - execute the controller
* - return the response
*
*
* @return Response response.
*/
- public function __invoke(Request $request, Response $response, callable $next)
+ public function __invoke(Request $request, Response $response, callable $next): Response
{
+ $this->initBasePath($request);
+
try {
- $response = $next($request, $response);
- } catch (ShaarliException $e) {
- $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())
- );
+ if (!is_file($this->container->conf->getConfigFileExt())
+ && !in_array($next->getName(), ['displayInstall', 'saveInstall'], true)
+ ) {
+ return $response->withRedirect($this->container->basePath . '/install');
}
- $response = $response->withStatus($e->getCode());
- $response = $response->write($this->container->pageBuilder->render('error'));
+ $this->runUpdates();
+ $this->checkOpenShaarli($request, $response, $next);
+
+ return $next($request, $response);
+ } catch (UnauthorizedException $e) {
+ $returnUrl = urlencode($this->container->environment['REQUEST_URI']);
+
+ return $response->withRedirect($this->container->basePath . '/login?returnurl=' . $returnUrl);
+ }
+ // Other exceptions are handled by ErrorController
+ }
+
+ /**
+ * Run the updater for every requests processed while logged in.
+ */
+ protected function runUpdates(): void
+ {
+ if ($this->container->loginManager->isLoggedIn() !== true) {
+ return;
+ }
+
+ $this->container->updater->setBasePath($this->container->basePath);
+ $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 $response;
+ return true;
+ }
+
+ /**
+ * Initialize the URL base path if it hasn't been defined yet.
+ */
+ protected function initBasePath(Request $request): void
+ {
+ if (null === $this->container->basePath) {
+ $this->container->basePath = rtrim($request->getUri()->getBasePath(), '/');
+ }
}
}
--- /dev/null
+<?php
+
+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;
+use Slim\Http\Response;
+use Throwable;
+
+/**
+ * Class ConfigureController
+ *
+ * Slim controller used to handle Shaarli configuration page (display + save new config).
+ */
+class ConfigureController extends ShaarliAdminController
+{
+ /**
+ * GET /admin/configure - Displays the configuration page
+ */
+ public function index(Request $request, Response $response): Response
+ {
+ $this->assignView('title', $this->container->conf->get('general.title', 'Shaarli'));
+ $this->assignView('theme', $this->container->conf->get('resource.theme'));
+ $this->assignView(
+ 'theme_available',
+ ThemeUtils::getThemes($this->container->conf->get('resource.raintpl_tpl'))
+ );
+ $this->assignView('formatter_available', ['default', 'markdown']);
+ list($continents, $cities) = generateTimeZoneData(
+ timezone_identifiers_list(),
+ $this->container->conf->get('general.timezone')
+ );
+ $this->assignView('continents', $continents);
+ $this->assignView('cities', $cities);
+ $this->assignView('retrieve_description', $this->container->conf->get('general.retrieve_description', false));
+ $this->assignView('private_links_default', $this->container->conf->get('privacy.default_private_links', false));
+ $this->assignView(
+ 'session_protection_disabled',
+ $this->container->conf->get('security.session_protection_disabled', false)
+ );
+ $this->assignView('enable_rss_permalinks', $this->container->conf->get('feed.rss_permalinks', false));
+ $this->assignView('enable_update_check', $this->container->conf->get('updates.check_updates', true));
+ $this->assignView('hide_public_links', $this->container->conf->get('privacy.hide_public_links', false));
+ $this->assignView('api_enabled', $this->container->conf->get('api.enabled', true));
+ $this->assignView('api_secret', $this->container->conf->get('api.secret'));
+ $this->assignView('languages', Languages::getAvailableLanguages());
+ $this->assignView('gd_enabled', extension_loaded('gd'));
+ $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(TemplatePage::CONFIGURE));
+ }
+
+ /**
+ * POST /admin/configure - Update Shaarli's configuration
+ */
+ public function save(Request $request, Response $response): Response
+ {
+ $this->checkToken($request);
+
+ $continent = $request->getParam('continent');
+ $city = $request->getParam('city');
+ $tz = 'UTC';
+ if (null !== $continent && null !== $city && isTimeZoneValid($continent, $city)) {
+ $tz = $continent . '/' . $city;
+ }
+
+ $this->container->conf->set('general.timezone', $tz);
+ $this->container->conf->set('general.title', escape($request->getParam('title')));
+ $this->container->conf->set('general.header_link', escape($request->getParam('titleLink')));
+ $this->container->conf->set('general.retrieve_description', !empty($request->getParam('retrieveDescription')));
+ $this->container->conf->set('resource.theme', escape($request->getParam('theme')));
+ $this->container->conf->set(
+ 'security.session_protection_disabled',
+ !empty($request->getParam('disablesessionprotection'))
+ );
+ $this->container->conf->set(
+ 'privacy.default_private_links',
+ !empty($request->getParam('privateLinkByDefault'))
+ );
+ $this->container->conf->set('feed.rss_permalinks', !empty($request->getParam('enableRssPermalinks')));
+ $this->container->conf->set('updates.check_updates', !empty($request->getParam('updateCheck')));
+ $this->container->conf->set('privacy.hide_public_links', !empty($request->getParam('hidePublicLinks')));
+ $this->container->conf->set('api.enabled', !empty($request->getParam('enableApi')));
+ $this->container->conf->set('api.secret', escape($request->getParam('apiSecret')));
+ $this->container->conf->set('formatter', escape($request->getParam('formatter')));
+
+ if (!empty($request->getParam('language'))) {
+ $this->container->conf->set('translation.language', escape($request->getParam('language')));
+ }
+
+ $thumbnailsMode = extension_loaded('gd') ? $request->getParam('enableThumbnails') : Thumbnailer::MODE_NONE;
+ if ($thumbnailsMode !== Thumbnailer::MODE_NONE
+ && $thumbnailsMode !== $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE)
+ ) {
+ $this->saveWarningMessage(
+ t('You have enabled or changed thumbnails mode.') .
+ '<a href="'. $this->container->basePath .'/admin/thumbnails">' . t('Please synchronize them.') .'</a>'
+ );
+ }
+ $this->container->conf->set('thumbnails.mode', $thumbnailsMode);
+
+ try {
+ $this->container->conf->write($this->container->loginManager->isLoggedIn());
+ $this->container->history->updateSettings();
+ $this->container->pageCacheManager->invalidateCaches();
+ } catch (Throwable $e) {
+ $this->assignView('message', t('Error while writing config file after configuration update.'));
+
+ if ($this->container->conf->get('dev.debug', false)) {
+ $this->assignView('stacktrace', $e->getMessage() . PHP_EOL . $e->getTraceAsString());
+ }
+
+ return $response->write($this->render('error'));
+ }
+
+ $this->saveSuccessMessage(t('Configuration was saved.'));
+
+ return $this->redirect($response, '/admin/configure');
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Admin;
+
+use DateTime;
+use Shaarli\Bookmark\Bookmark;
+use Shaarli\Render\TemplatePage;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+/**
+ * Class ExportController
+ *
+ * Slim controller used to display Shaarli data export page,
+ * and process the bookmarks export as a Netscape Bookmarks file.
+ */
+class ExportController extends ShaarliAdminController
+{
+ /**
+ * GET /admin/export - Display export page
+ */
+ public function index(Request $request, Response $response): Response
+ {
+ $this->assignView('pagetitle', t('Export') .' - '. $this->container->conf->get('general.title', 'Shaarli'));
+
+ return $response->write($this->render(TemplatePage::EXPORT));
+ }
+
+ /**
+ * POST /admin/export - Process export, and serve download file named
+ * bookmarks_(all|private|public)_datetime.html
+ */
+ public function export(Request $request, Response $response): Response
+ {
+ $this->checkToken($request);
+
+ $selection = $request->getParam('selection');
+
+ if (empty($selection)) {
+ $this->saveErrorMessage(t('Please select an export mode.'));
+
+ return $this->redirect($response, '/admin/export');
+ }
+
+ $prependNoteUrl = filter_var($request->getParam('prepend_note_url') ?? false, FILTER_VALIDATE_BOOLEAN);
+
+ try {
+ $formatter = $this->container->formatterFactory->getFormatter('raw');
+
+ $this->assignView(
+ 'links',
+ $this->container->netscapeBookmarkUtils->filterAndFormat(
+ $formatter,
+ $selection,
+ $prependNoteUrl,
+ index_url($this->container->environment)
+ )
+ );
+ } catch (\Exception $exc) {
+ $this->saveErrorMessage($exc->getMessage());
+
+ return $this->redirect($response, '/admin/export');
+ }
+
+ $now = new DateTime();
+ $response = $response->withHeader('Content-Type', 'text/html; charset=utf-8');
+ $response = $response->withHeader(
+ 'Content-disposition',
+ 'attachment; filename=bookmarks_'.$selection.'_'.$now->format(Bookmark::LINK_DATE_FORMAT).'.html'
+ );
+
+ $this->assignView('date', $now->format(DateTime::RFC822));
+ $this->assignView('eol', PHP_EOL);
+ $this->assignView('selection', $selection);
+
+ return $response->write($this->render(TemplatePage::NETSCAPE_EXPORT_BOOKMARKS));
+ }
+}
--- /dev/null
+<?php
+
+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;
+
+/**
+ * Class ImportController
+ *
+ * Slim controller used to display Shaarli data import page,
+ * and import bookmarks from Netscape Bookmarks file.
+ */
+class ImportController extends ShaarliAdminController
+{
+ /**
+ * GET /admin/import - Display import page
+ */
+ public function index(Request $request, Response $response): Response
+ {
+ $this->assignView(
+ 'maxfilesize',
+ get_max_upload_size(
+ ini_get('post_max_size'),
+ ini_get('upload_max_filesize'),
+ false
+ )
+ );
+ $this->assignView(
+ 'maxfilesizeHuman',
+ get_max_upload_size(
+ ini_get('post_max_size'),
+ ini_get('upload_max_filesize'),
+ true
+ )
+ );
+ $this->assignView('pagetitle', t('Import') .' - '. $this->container->conf->get('general.title', 'Shaarli'));
+
+ return $response->write($this->render(TemplatePage::IMPORT));
+ }
+
+ /**
+ * POST /admin/import - Process import file provided and create bookmarks
+ */
+ public function import(Request $request, Response $response): Response
+ {
+ $this->checkToken($request);
+
+ $file = ($request->getUploadedFiles() ?? [])['filetoupload'] ?? null;
+ if (!$file instanceof UploadedFileInterface) {
+ $this->saveErrorMessage(t('No import file provided.'));
+
+ return $this->redirect($response, '/admin/import');
+ }
+
+
+ // Import bookmarks from an uploaded file
+ if (0 === $file->getSize()) {
+ // The file is too big or some form field may be missing.
+ $msg = sprintf(
+ t(
+ 'The file you are trying to upload is probably bigger than what this webserver can accept'
+ .' (%s). Please upload in smaller chunks.'
+ ),
+ get_max_upload_size(ini_get('post_max_size'), ini_get('upload_max_filesize'))
+ );
+ $this->saveErrorMessage($msg);
+
+ return $this->redirect($response, '/admin/import');
+ }
+
+ $status = $this->container->netscapeBookmarkUtils->import($request->getParams(), $file);
+
+ $this->saveSuccessMessage($status);
+
+ return $this->redirect($response, '/admin/import');
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Admin;
+
+use Shaarli\Security\CookieManager;
+use Shaarli\Security\LoginManager;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+/**
+ * Class LogoutController
+ *
+ * Slim controller used to logout the user.
+ * It invalidates page cache and terminate the user session. Then it redirects to the homepage.
+ */
+class LogoutController extends ShaarliAdminController
+{
+ public function index(Request $request, Response $response): Response
+ {
+ $this->container->pageCacheManager->invalidateCaches();
+ $this->container->sessionManager->logout();
+ $this->container->cookieManager->setCookieParameter(
+ CookieManager::STAY_SIGNED_IN,
+ 'false',
+ 0,
+ $this->container->basePath . '/'
+ );
+
+ return $this->redirect($response, '/');
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+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;
+
+/**
+ * Class PostBookmarkController
+ *
+ * Slim controller used to handle Shaarli create or edit bookmarks.
+ */
+class ManageShaareController extends ShaarliAdminController
+{
+ /**
+ * GET /admin/add-shaare - Displays the form used to create a new bookmark from an URL
+ */
+ public function addShaare(Request $request, Response $response): Response
+ {
+ $this->assignView(
+ 'pagetitle',
+ t('Shaare a new link') .' - '. $this->container->conf->get('general.title', 'Shaarli')
+ );
+
+ return $response->write($this->render(TemplatePage::ADDLINK));
+ }
+
+ /**
+ * GET /admin/shaare - Displays the bookmark form for creation.
+ * Note that if the URL is found in existing bookmarks, then it will be in edit mode.
+ */
+ public function displayCreateForm(Request $request, Response $response): Response
+ {
+ $url = cleanup_url($request->getParam('post'));
+
+ $linkIsNew = false;
+ // Check if URL is not already in database (in this case, we will edit the existing link)
+ $bookmark = $this->container->bookmarkService->findByUrl($url);
+ if (null === $bookmark) {
+ $linkIsNew = true;
+ // Get shaare data if it was provided in URL (e.g.: by the bookmarklet).
+ $title = $request->getParam('title');
+ $description = $request->getParam('description');
+ $tags = $request->getParam('tags');
+ $private = filter_var($request->getParam('private'), FILTER_VALIDATE_BOOLEAN);
+
+ // If this is an HTTP(S) link, we try go get the page to extract
+ // the title (otherwise we will to straight to the edit form.)
+ if (empty($title) && strpos(get_url_scheme($url) ?: '', 'http') !== false) {
+ $retrieveDescription = $this->container->conf->get('general.retrieve_description');
+ // Short timeout to keep the application responsive
+ // The callback will fill $charset and $title with data from the downloaded page.
+ $this->container->httpAccess->getHttpResponse(
+ $url,
+ $this->container->conf->get('general.download_timeout', 30),
+ $this->container->conf->get('general.download_max_size', 4194304),
+ $this->container->httpAccess->getCurlDownloadCallback(
+ $charset,
+ $title,
+ $description,
+ $tags,
+ $retrieveDescription
+ )
+ );
+ if (! empty($title) && strtolower($charset) !== 'utf-8') {
+ $title = mb_convert_encoding($title, 'utf-8', $charset);
+ }
+ }
+
+ if (empty($url) && empty($title)) {
+ $title = $this->container->conf->get('general.default_note_title', t('Note: '));
+ }
+
+ $link = escape([
+ 'title' => $title,
+ 'url' => $url ?? '',
+ 'description' => $description ?? '',
+ 'tags' => $tags ?? '',
+ 'private' => $private,
+ ]);
+ } else {
+ $formatter = $this->container->formatterFactory->getFormatter('raw');
+ $link = $formatter->format($bookmark);
+ }
+
+ return $this->displayForm($link, $linkIsNew, $request, $response);
+ }
+
+ /**
+ * GET /admin/shaare/{id} - Displays the bookmark form in edition mode.
+ */
+ public function displayEditForm(Request $request, Response $response, array $args): Response
+ {
+ $id = $args['id'] ?? '';
+ try {
+ if (false === ctype_digit($id)) {
+ throw new BookmarkNotFoundException();
+ }
+ $bookmark = $this->container->bookmarkService->get((int) $id); // Read database
+ } catch (BookmarkNotFoundException $e) {
+ $this->saveErrorMessage(sprintf(
+ t('Bookmark with identifier %s could not be found.'),
+ $id
+ ));
+
+ return $this->redirect($response, '/');
+ }
+
+ $formatter = $this->container->formatterFactory->getFormatter('raw');
+ $link = $formatter->format($bookmark);
+
+ return $this->displayForm($link, false, $request, $response);
+ }
+
+ /**
+ * POST /admin/shaare
+ */
+ public function save(Request $request, Response $response): Response
+ {
+ $this->checkToken($request);
+
+ // lf_id should only be present if the link exists.
+ $id = $request->getParam('lf_id') ? intval(escape($request->getParam('lf_id'))) : null;
+ if (null !== $id && true === $this->container->bookmarkService->exists($id)) {
+ // Edit
+ $bookmark = $this->container->bookmarkService->get($id);
+ } else {
+ // New link
+ $bookmark = new Bookmark();
+ }
+
+ $bookmark->setTitle($request->getParam('lf_title'));
+ $bookmark->setDescription($request->getParam('lf_description'));
+ $bookmark->setUrl($request->getParam('lf_url'), $this->container->conf->get('security.allowed_protocols', []));
+ $bookmark->setPrivate(filter_var($request->getParam('lf_private'), FILTER_VALIDATE_BOOLEAN));
+ $bookmark->setTagsString($request->getParam('lf_tags'));
+
+ if ($this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE
+ && false === $bookmark->isNote()
+ ) {
+ $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl()));
+ }
+ $this->container->bookmarkService->addOrSet($bookmark, false);
+
+ // To preserve backward compatibility with 3rd parties, plugins still use arrays
+ $formatter = $this->container->formatterFactory->getFormatter('raw');
+ $data = $formatter->format($bookmark);
+ $this->executePageHooks('save_link', $data);
+
+ $bookmark->fromArray($data);
+ $this->container->bookmarkService->set($bookmark);
+
+ // If we are called from the bookmarklet, we must close the popup:
+ if ($request->getParam('source') === 'bookmarklet') {
+ return $response->write('<script>self.close();</script>');
+ }
+
+ if (!empty($request->getParam('returnurl'))) {
+ $this->container->environment['HTTP_REFERER'] = escape($request->getParam('returnurl'));
+ }
+
+ return $this->redirectFromReferer(
+ $request,
+ $response,
+ ['add-shaare', 'shaare'], ['addlink', 'post', 'edit_link'],
+ $bookmark->getShortUrl()
+ );
+ }
+
+ /**
+ * GET /admin/shaare/delete - Delete one or multiple bookmarks (depending on `id` query parameter).
+ */
+ public function deleteBookmark(Request $request, Response $response): Response
+ {
+ $this->checkToken($request);
+
+ $ids = escape(trim($request->getParam('id') ?? ''));
+ if (empty($ids) || strpos($ids, ' ') !== false) {
+ // multiple, space-separated ids provided
+ $ids = array_values(array_filter(preg_split('/\s+/', $ids), 'ctype_digit'));
+ } else {
+ $ids = [$ids];
+ }
+
+ // assert at least one id is given
+ if (0 === count($ids)) {
+ $this->saveErrorMessage(t('Invalid bookmark ID provided.'));
+
+ return $this->redirectFromReferer($request, $response, [], ['delete-shaare']);
+ }
+
+ $formatter = $this->container->formatterFactory->getFormatter('raw');
+ $count = 0;
+ foreach ($ids as $id) {
+ try {
+ $bookmark = $this->container->bookmarkService->get((int) $id);
+ } catch (BookmarkNotFoundException $e) {
+ $this->saveErrorMessage(sprintf(
+ t('Bookmark with identifier %s could not be found.'),
+ $id
+ ));
+
+ continue;
+ }
+
+ $data = $formatter->format($bookmark);
+ $this->executePageHooks('delete_link', $data);
+ $this->container->bookmarkService->remove($bookmark, false);
+ ++ $count;
+ }
+
+ if ($count > 0) {
+ $this->container->bookmarkService->save();
+ }
+
+ // If we are called from the bookmarklet, we must close the popup:
+ if ($request->getParam('source') === 'bookmarklet') {
+ return $response->write('<script>self.close();</script>');
+ }
+
+ // Don't redirect to where we were previously because the datastore has changed.
+ return $this->redirect($response, '/');
+ }
+
+ /**
+ * GET /admin/shaare/visibility
+ *
+ * Change visibility (public/private) of one or multiple bookmarks (depending on `id` query parameter).
+ */
+ public function changeVisibility(Request $request, Response $response): Response
+ {
+ $this->checkToken($request);
+
+ $ids = trim(escape($request->getParam('id') ?? ''));
+ if (empty($ids) || strpos($ids, ' ') !== false) {
+ // multiple, space-separated ids provided
+ $ids = array_values(array_filter(preg_split('/\s+/', $ids), 'ctype_digit'));
+ } else {
+ // only a single id provided
+ $ids = [$ids];
+ }
+
+ // assert at least one id is given
+ if (0 === count($ids)) {
+ $this->saveErrorMessage(t('Invalid bookmark ID provided.'));
+
+ return $this->redirectFromReferer($request, $response, [], ['change_visibility']);
+ }
+
+ // assert that the visibility is valid
+ $visibility = $request->getParam('newVisibility');
+ if (null === $visibility || false === in_array($visibility, ['public', 'private'], true)) {
+ $this->saveErrorMessage(t('Invalid visibility provided.'));
+
+ return $this->redirectFromReferer($request, $response, [], ['change_visibility']);
+ } else {
+ $isPrivate = $visibility === 'private';
+ }
+
+ $formatter = $this->container->formatterFactory->getFormatter('raw');
+ $count = 0;
+
+ foreach ($ids as $id) {
+ try {
+ $bookmark = $this->container->bookmarkService->get((int) $id);
+ } catch (BookmarkNotFoundException $e) {
+ $this->saveErrorMessage(sprintf(
+ t('Bookmark with identifier %s could not be found.'),
+ $id
+ ));
+
+ continue;
+ }
+
+ $bookmark->setPrivate($isPrivate);
+
+ // To preserve backward compatibility with 3rd parties, plugins still use arrays
+ $data = $formatter->format($bookmark);
+ $this->executePageHooks('save_link', $data);
+ $bookmark->fromArray($data);
+
+ $this->container->bookmarkService->set($bookmark, false);
+ ++$count;
+ }
+
+ if ($count > 0) {
+ $this->container->bookmarkService->save();
+ }
+
+ return $this->redirectFromReferer($request, $response, ['/visibility'], ['change_visibility']);
+ }
+
+ /**
+ * GET /admin/shaare/{id}/pin - Pin or unpin a bookmark.
+ */
+ public function pinBookmark(Request $request, Response $response, array $args): Response
+ {
+ $this->checkToken($request);
+
+ $id = $args['id'] ?? '';
+ try {
+ if (false === ctype_digit($id)) {
+ throw new BookmarkNotFoundException();
+ }
+ $bookmark = $this->container->bookmarkService->get((int) $id); // Read database
+ } catch (BookmarkNotFoundException $e) {
+ $this->saveErrorMessage(sprintf(
+ t('Bookmark with identifier %s could not be found.'),
+ $id
+ ));
+
+ return $this->redirectFromReferer($request, $response, ['/pin'], ['pin']);
+ }
+
+ $formatter = $this->container->formatterFactory->getFormatter('raw');
+
+ $bookmark->setSticky(!$bookmark->isSticky());
+
+ // To preserve backward compatibility with 3rd parties, plugins still use arrays
+ $data = $formatter->format($bookmark);
+ $this->executePageHooks('save_link', $data);
+ $bookmark->fromArray($data);
+
+ $this->container->bookmarkService->set($bookmark);
+
+ return $this->redirectFromReferer($request, $response, ['/pin'], ['pin']);
+ }
+
+ /**
+ * Helper function used to display the shaare form whether it's a new or existing bookmark.
+ *
+ * @param array $link data used in template, either from parameters or from the data store
+ */
+ protected function displayForm(array $link, bool $isNew, Request $request, Response $response): Response
+ {
+ $tags = $this->container->bookmarkService->bookmarksCountPerTag();
+ if ($this->container->conf->get('formatter') === 'markdown') {
+ $tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1;
+ }
+
+ $data = [
+ 'link' => $link,
+ 'link_is_new' => $isNew,
+ 'http_referer' => escape($this->container->environment['HTTP_REFERER'] ?? ''),
+ 'source' => $request->getParam('source') ?? '',
+ 'tags' => $tags,
+ 'default_private_links' => $this->container->conf->get('privacy.default_private_links', false),
+ ];
+
+ $this->executePageHooks('render_editlink', $data, TemplatePage::EDIT_LINK);
+
+ foreach ($data as $key => $value) {
+ $this->assignView($key, $value);
+ }
+
+ $editLabel = false === $isNew ? t('Edit') .' ' : '';
+ $this->assignView(
+ 'pagetitle',
+ $editLabel . t('Shaare') .' - '. $this->container->conf->get('general.title', 'Shaarli')
+ );
+
+ return $response->write($this->render(TemplatePage::EDIT_LINK));
+ }
+}
--- /dev/null
+<?php
+
+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;
+
+/**
+ * Class ManageTagController
+ *
+ * Slim controller used to handle Shaarli manage tags page (rename and delete tags).
+ */
+class ManageTagController extends ShaarliAdminController
+{
+ /**
+ * GET /admin/tags - Displays the manage tags page
+ */
+ public function index(Request $request, Response $response): Response
+ {
+ $fromTag = $request->getParam('fromtag') ?? '';
+
+ $this->assignView('fromtag', escape($fromTag));
+ $this->assignView(
+ 'pagetitle',
+ t('Manage tags') .' - '. $this->container->conf->get('general.title', 'Shaarli')
+ );
+
+ return $response->write($this->render(TemplatePage::CHANGE_TAG));
+ }
+
+ /**
+ * POST /admin/tags - Update or delete provided tag
+ */
+ public function save(Request $request, Response $response): Response
+ {
+ $this->checkToken($request);
+
+ $isDelete = null !== $request->getParam('deletetag') && null === $request->getParam('renametag');
+
+ $fromTag = escape(trim($request->getParam('fromtag') ?? ''));
+ $toTag = escape(trim($request->getParam('totag') ?? ''));
+
+ if (0 === strlen($fromTag) || false === $isDelete && 0 === strlen($toTag)) {
+ $this->saveWarningMessage(t('Invalid tags provided.'));
+
+ return $this->redirect($response, '/admin/tags');
+ }
+
+ // TODO: move this to bookmark service
+ $count = 0;
+ $bookmarks = $this->container->bookmarkService->search(['searchtags' => $fromTag], BookmarkFilter::$ALL, true);
+ foreach ($bookmarks as $bookmark) {
+ if (false === $isDelete) {
+ $bookmark->renameTag($fromTag, $toTag);
+ } else {
+ $bookmark->deleteTag($fromTag);
+ }
+
+ $this->container->bookmarkService->set($bookmark, false);
+ $this->container->history->updateLink($bookmark);
+ $count++;
+ }
+
+ $this->container->bookmarkService->save();
+
+ if (true === $isDelete) {
+ $alert = sprintf(
+ t('The tag was removed from %d bookmark.', 'The tag was removed from %d bookmarks.', $count),
+ $count
+ );
+ } else {
+ $alert = sprintf(
+ t('The tag was renamed in %d bookmark.', 'The tag was renamed in %d bookmarks.', $count),
+ $count
+ );
+ }
+
+ $this->saveSuccessMessage($alert);
+
+ $redirect = true === $isDelete ? '/admin/tags' : '/?searchtags='. urlencode($toTag);
+
+ return $this->redirect($response, $redirect);
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+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;
+
+/**
+ * Class PasswordController
+ *
+ * Slim controller used to handle passwords update.
+ */
+class PasswordController extends ShaarliAdminController
+{
+ public function __construct(ShaarliContainer $container)
+ {
+ parent::__construct($container);
+
+ $this->assignView(
+ 'pagetitle',
+ t('Change password') .' - '. $this->container->conf->get('general.title', 'Shaarli')
+ );
+ }
+
+ /**
+ * GET /admin/password - Displays the change password template
+ */
+ public function index(Request $request, Response $response): Response
+ {
+ return $response->write($this->render(TemplatePage::CHANGE_PASSWORD));
+ }
+
+ /**
+ * POST /admin/password - Change admin password - existing and new passwords need to be provided.
+ */
+ public function change(Request $request, Response $response): Response
+ {
+ $this->checkToken($request);
+
+ if ($this->container->conf->get('security.open_shaarli', false)) {
+ throw new OpenShaarliPasswordException();
+ }
+
+ $oldPassword = $request->getParam('oldpassword');
+ $newPassword = $request->getParam('setpassword');
+
+ if (empty($newPassword) || empty($oldPassword)) {
+ $this->saveErrorMessage(t('You must provide the current and new password to change it.'));
+
+ return $response
+ ->withStatus(400)
+ ->write($this->render(TemplatePage::CHANGE_PASSWORD))
+ ;
+ }
+
+ // Make sure old password is correct.
+ $oldHash = sha1(
+ $oldPassword .
+ $this->container->conf->get('credentials.login') .
+ $this->container->conf->get('credentials.salt')
+ );
+
+ if ($oldHash !== $this->container->conf->get('credentials.hash')) {
+ $this->saveErrorMessage(t('The old password is not correct.'));
+
+ return $response
+ ->withStatus(400)
+ ->write($this->render(TemplatePage::CHANGE_PASSWORD))
+ ;
+ }
+
+ // Save new password
+ // Salt renders rainbow-tables attacks useless.
+ $this->container->conf->set('credentials.salt', sha1(uniqid('', true) .'_'. mt_rand()));
+ $this->container->conf->set(
+ 'credentials.hash',
+ sha1(
+ $newPassword
+ . $this->container->conf->get('credentials.login')
+ . $this->container->conf->get('credentials.salt')
+ )
+ );
+
+ try {
+ $this->container->conf->write($this->container->loginManager->isLoggedIn());
+ } catch (Throwable $e) {
+ throw new ShaarliFrontException($e->getMessage(), 500, $e);
+ }
+
+ $this->saveSuccessMessage(t('Your password has been changed'));
+
+ return $response->write($this->render(TemplatePage::CHANGE_PASSWORD));
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Admin;
+
+use Exception;
+use Shaarli\Render\TemplatePage;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+/**
+ * Class PluginsController
+ *
+ * Slim controller used to handle Shaarli plugins configuration page (display + save new config).
+ */
+class PluginsController extends ShaarliAdminController
+{
+ /**
+ * GET /admin/plugins - Displays the configuration page
+ */
+ public function index(Request $request, Response $response): Response
+ {
+ $pluginMeta = $this->container->pluginManager->getPluginsMeta();
+
+ // Split plugins into 2 arrays: ordered enabled plugins and disabled.
+ $enabledPlugins = array_filter($pluginMeta, function ($v) {
+ return ($v['order'] ?? false) !== false;
+ });
+ $enabledPlugins = load_plugin_parameter_values($enabledPlugins, $this->container->conf->get('plugins', []));
+ uasort(
+ $enabledPlugins,
+ function ($a, $b) {
+ return $a['order'] - $b['order'];
+ }
+ );
+ $disabledPlugins = array_filter($pluginMeta, function ($v) {
+ return ($v['order'] ?? false) === false;
+ });
+
+ $this->assignView('enabledPlugins', $enabledPlugins);
+ $this->assignView('disabledPlugins', $disabledPlugins);
+ $this->assignView(
+ 'pagetitle',
+ t('Plugin Administration') .' - '. $this->container->conf->get('general.title', 'Shaarli')
+ );
+
+ return $response->write($this->render(TemplatePage::PLUGINS_ADMIN));
+ }
+
+ /**
+ * POST /admin/plugins - Update Shaarli's configuration
+ */
+ public function save(Request $request, Response $response): Response
+ {
+ $this->checkToken($request);
+
+ try {
+ $parameters = $request->getParams() ?? [];
+
+ $this->executePageHooks('save_plugin_parameters', $parameters);
+
+ if (isset($parameters['parameters_form'])) {
+ unset($parameters['parameters_form']);
+ foreach ($parameters as $param => $value) {
+ $this->container->conf->set('plugins.'. $param, escape($value));
+ }
+ } else {
+ $this->container->conf->set('general.enabled_plugins', save_plugin_config($parameters));
+ }
+
+ $this->container->conf->write($this->container->loginManager->isLoggedIn());
+ $this->container->history->updateSettings();
+
+ $this->saveSuccessMessage(t('Setting successfully saved.'));
+ } catch (Exception $e) {
+ $this->saveErrorMessage(
+ t('Error while saving plugin configuration: ') . PHP_EOL . $e->getMessage()
+ );
+ }
+
+ return $this->redirect($response, '/admin/plugins');
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Admin;
+
+use Shaarli\Bookmark\BookmarkFilter;
+use Shaarli\Security\SessionManager;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+/**
+ * Class SessionFilterController
+ *
+ * Slim controller used to handle filters stored in the user session, such as visibility, etc.
+ */
+class SessionFilterController extends ShaarliAdminController
+{
+ /**
+ * GET /admin/visibility: allows to display only public or only private bookmarks in linklist
+ */
+ public function visibility(Request $request, Response $response, array $args): Response
+ {
+ if (false === $this->container->loginManager->isLoggedIn()) {
+ return $this->redirectFromReferer($request, $response, ['visibility']);
+ }
+
+ $newVisibility = $args['visibility'] ?? null;
+ if (false === in_array($newVisibility, [BookmarkFilter::$PRIVATE, BookmarkFilter::$PUBLIC], true)) {
+ $newVisibility = null;
+ }
+
+ $currentVisibility = $this->container->sessionManager->getSessionParameter(SessionManager::KEY_VISIBILITY);
+
+ // Visibility not set or not already expected value, set expected value, otherwise reset it
+ if ($newVisibility !== null && (null === $currentVisibility || $currentVisibility !== $newVisibility)) {
+ // See only public bookmarks
+ $this->container->sessionManager->setSessionParameter(
+ SessionManager::KEY_VISIBILITY,
+ $newVisibility
+ );
+ } else {
+ $this->container->sessionManager->deleteSessionParameter(SessionManager::KEY_VISIBILITY);
+ }
+
+ return $this->redirectFromReferer($request, $response, ['visibility']);
+ }
+
+
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Admin;
+
+use Shaarli\Container\ShaarliContainer;
+use Shaarli\Front\Controller\Visitor\ShaarliVisitorController;
+use Shaarli\Front\Exception\UnauthorizedException;
+use Shaarli\Front\Exception\WrongTokenException;
+use Shaarli\Security\SessionManager;
+use Slim\Http\Request;
+
+/**
+ * Class ShaarliAdminController
+ *
+ * All admin controllers (for logged in users) MUST extend this abstract class.
+ * It makes sure that the user is properly logged in, and otherwise throw an exception
+ * which will redirect to the login page.
+ *
+ * @package Shaarli\Front\Controller\Admin
+ */
+abstract class ShaarliAdminController extends ShaarliVisitorController
+{
+ /**
+ * Any persistent action to the config or data store must check the XSRF token validity.
+ */
+ protected function checkToken(Request $request): bool
+ {
+ if (!$this->container->sessionManager->checkToken($request->getParam('token'))) {
+ throw new WrongTokenException();
+ }
+
+ return true;
+ }
+
+ /**
+ * Save a SUCCESS message in user session, which will be displayed on any template page.
+ */
+ protected function saveSuccessMessage(string $message): void
+ {
+ $this->saveMessage(SessionManager::KEY_SUCCESS_MESSAGES, $message);
+ }
+
+ /**
+ * Save a WARNING message in user session, which will be displayed on any template page.
+ */
+ protected function saveWarningMessage(string $message): void
+ {
+ $this->saveMessage(SessionManager::KEY_WARNING_MESSAGES, $message);
+ }
+
+ /**
+ * Save an ERROR message in user session, which will be displayed on any template page.
+ */
+ protected function saveErrorMessage(string $message): void
+ {
+ $this->saveMessage(SessionManager::KEY_ERROR_MESSAGES, $message);
+ }
+
+ /**
+ * Use the sessionManager to save the provided message using the proper type.
+ *
+ * @param string $type successed/warnings/errors
+ */
+ protected function saveMessage(string $type, string $message): void
+ {
+ $messages = $this->container->sessionManager->getSessionParameter($type) ?? [];
+ $messages[] = $message;
+
+ $this->container->sessionManager->setSessionParameter($type, $messages);
+ }
+}
--- /dev/null
+<?php
+
+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;
+
+/**
+ * Class ToolsController
+ *
+ * Slim controller used to handle thumbnails update.
+ */
+class ThumbnailsController extends ShaarliAdminController
+{
+ /**
+ * GET /admin/thumbnails - Display thumbnails update page
+ */
+ public function index(Request $request, Response $response): Response
+ {
+ $ids = [];
+ foreach ($this->container->bookmarkService->search() as $bookmark) {
+ // A note or not HTTP(S)
+ if ($bookmark->isNote() || !startsWith(strtolower($bookmark->getUrl()), 'http')) {
+ continue;
+ }
+
+ $ids[] = $bookmark->getId();
+ }
+
+ $this->assignView('ids', $ids);
+ $this->assignView(
+ 'pagetitle',
+ t('Thumbnails update') .' - '. $this->container->conf->get('general.title', 'Shaarli')
+ );
+
+ return $response->write($this->render(TemplatePage::THUMBNAILS));
+ }
+
+ /**
+ * PATCH /admin/shaare/{id}/thumbnail-update - Route for AJAX calls
+ */
+ public function ajaxUpdate(Request $request, Response $response, array $args): Response
+ {
+ $id = $args['id'] ?? null;
+
+ if (false === ctype_digit($id)) {
+ return $response->withStatus(400);
+ }
+
+ try {
+ $bookmark = $this->container->bookmarkService->get($id);
+ } catch (BookmarkNotFoundException $e) {
+ return $response->withStatus(404);
+ }
+
+ $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl()));
+ $this->container->bookmarkService->set($bookmark);
+
+ return $response->withJson($this->container->formatterFactory->getFormatter('raw')->format($bookmark));
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Admin;
+
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+/**
+ * Class TokenController
+ *
+ * Endpoint used to retrieve a XSRF token. Useful for AJAX requests.
+ */
+class TokenController extends ShaarliAdminController
+{
+ /**
+ * GET /admin/token
+ */
+ public function getToken(Request $request, Response $response): Response
+ {
+ $response = $response->withHeader('Content-Type', 'text/plain');
+
+ return $response->write($this->container->sessionManager->generateToken());
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Admin;
+
+use Shaarli\Render\TemplatePage;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+/**
+ * Class ToolsController
+ *
+ * Slim controller used to display the tools page.
+ */
+class ToolsController extends ShaarliAdminController
+{
+ public function index(Request $request, Response $response): Response
+ {
+ $data = [
+ 'pageabsaddr' => index_url($this->container->environment),
+ 'sslenabled' => is_https($this->container->environment),
+ ];
+
+ $this->executePageHooks('render_tools', $data, TemplatePage::TOOLS);
+
+ foreach ($data as $key => $value) {
+ $this->assignView($key, $value);
+ }
+
+ $this->assignView('pagetitle', t('Tools') .' - '. $this->container->conf->get('general.title', 'Shaarli'));
+
+ return $response->write($this->render(TemplatePage::TOOLS));
+ }
+}
--- /dev/null
+<?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();
+ $formatter->addContextData('base_path', $this->container->basePath);
+
+ $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->executePageHooks('render_linklist', $data, TemplatePage::LINKLIST);
+ $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);
+
+ $formatter = $this->container->formatterFactory->getFormatter();
+ $formatter->addContextData('base_path', $this->container->basePath);
+
+ $data = array_merge(
+ $this->initializeTemplateVars(),
+ [
+ 'pagetitle' => $bookmark->getTitle() .' - '. $this->container->conf->get('general.title', 'Shaarli'),
+ 'links' => [$formatter->format($bookmark)],
+ ]
+ );
+
+ $this->executePageHooks('render_linklist', $data, TemplatePage::LINKLIST);
+ $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;
+ }
+
+ /**
+ * @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;
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+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;
+
+/**
+ * Class DailyController
+ *
+ * Slim controller used to render the daily page.
+ */
+class DailyController extends ShaarliVisitorController
+{
+ public static $DAILY_RSS_NB_DAYS = 8;
+
+ /**
+ * Controller displaying all bookmarks published in a single day.
+ * It take a `day` date query parameter (format YYYYMMDD).
+ */
+ public function index(Request $request, Response $response): Response
+ {
+ $day = $request->getQueryParam('day') ?? date('Ymd');
+
+ $availableDates = $this->container->bookmarkService->days();
+ $nbAvailableDates = count($availableDates);
+ $index = array_search($day, $availableDates);
+
+ if ($index === false) {
+ // no bookmarks for day, but at least one day with bookmarks
+ $day = $availableDates[$nbAvailableDates - 1] ?? $day;
+ $previousDay = $availableDates[$nbAvailableDates - 2] ?? '';
+ } else {
+ $previousDay = $availableDates[$index - 1] ?? '';
+ $nextDay = $availableDates[$index + 1] ?? '';
+ }
+
+ if ($day === date('Ymd')) {
+ $this->assignView('dayDesc', t('Today'));
+ } elseif ($day === date('Ymd', strtotime('-1 days'))) {
+ $this->assignView('dayDesc', t('Yesterday'));
+ }
+
+ try {
+ $linksToDisplay = $this->container->bookmarkService->filterDay($day);
+ } catch (\Exception $exc) {
+ $linksToDisplay = [];
+ }
+
+ $formatter = $this->container->formatterFactory->getFormatter();
+ $formatter->addContextData('base_path', $this->container->basePath);
+ // We pre-format some fields for proper output.
+ foreach ($linksToDisplay as $key => $bookmark) {
+ $linksToDisplay[$key] = $formatter->format($bookmark);
+ // This page is a bit specific, we need raw description to calculate the length
+ $linksToDisplay[$key]['formatedDescription'] = $linksToDisplay[$key]['description'];
+ $linksToDisplay[$key]['description'] = $bookmark->getDescription();
+ }
+
+ $dayDate = DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, $day.'_000000');
+ $data = [
+ 'linksToDisplay' => $linksToDisplay,
+ 'day' => $dayDate->getTimestamp(),
+ 'dayDate' => $dayDate,
+ 'previousday' => $previousDay ?? '',
+ 'nextday' => $nextDay ?? '',
+ ];
+
+ // Hooks are called before column construction so that plugins don't have to deal with columns.
+ $this->executePageHooks('render_daily', $data, TemplatePage::DAILY);
+
+ $data['cols'] = $this->calculateColumns($data['linksToDisplay']);
+
+ $this->assignAllView($data);
+
+ $mainTitle = $this->container->conf->get('general.title', 'Shaarli');
+ $this->assignView(
+ 'pagetitle',
+ t('Daily') .' - '. format_date($dayDate, false) . ' - ' . $mainTitle
+ );
+
+ return $response->write($this->render(TemplatePage::DAILY));
+ }
+
+ /**
+ * Daily RSS feed: 1 RSS entry per day giving all the bookmarks on that day.
+ * Gives the last 7 days (which have bookmarks).
+ * This RSS feed cannot be filtered and does not trigger plugins yet.
+ */
+ public function rss(Request $request, Response $response): Response
+ {
+ $response = $response->withHeader('Content-Type', 'application/rss+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);
+ }
+
+ $days = [];
+ foreach ($this->container->bookmarkService->search() as $bookmark) {
+ $day = $bookmark->getCreated()->format('Ymd');
+
+ // Stop iterating after DAILY_RSS_NB_DAYS entries
+ if (count($days) === static::$DAILY_RSS_NB_DAYS && !isset($days[$day])) {
+ break;
+ }
+
+ $days[$day][] = $bookmark;
+ }
+
+ // Build the RSS feed.
+ $indexUrl = escape(index_url($this->container->environment));
+
+ $formatter = $this->container->formatterFactory->getFormatter();
+ $formatter->addContextData('index_url', $indexUrl);
+
+ $dataPerDay = [];
+
+ /** @var Bookmark[] $bookmarks */
+ foreach ($days as $day => $bookmarks) {
+ $dayDatetime = DateTimeImmutable::createFromFormat(Bookmark::LINK_DATE_FORMAT, $day.'_000000');
+ $dataPerDay[$day] = [
+ 'date' => $dayDatetime,
+ 'date_rss' => $dayDatetime->format(DateTime::RSS),
+ 'date_human' => format_date($dayDatetime, false, true),
+ 'absolute_url' => $indexUrl . '/daily?day=' . $day,
+ 'links' => [],
+ ];
+
+ foreach ($bookmarks as $key => $bookmark) {
+ $dataPerDay[$day]['links'][$key] = $formatter->format($bookmark);
+
+ // Make permalink URL absolute
+ if ($bookmark->isNote()) {
+ $dataPerDay[$day]['links'][$key]['url'] = $indexUrl . $bookmark->getUrl();
+ }
+ }
+ }
+
+ $this->assignView('title', $this->container->conf->get('general.title', 'Shaarli'));
+ $this->assignView('index_url', $indexUrl);
+ $this->assignView('page_url', $pageUrl);
+ $this->assignView('hide_timestamps', $this->container->conf->get('privacy.hide_timestamps', false));
+ $this->assignView('days', $dataPerDay);
+
+ $rssContent = $this->render(TemplatePage::DAILY_RSS);
+
+ $cache->cache($rssContent);
+
+ return $response->write($rssContent);
+ }
+
+ /**
+ * We need to spread the articles on 3 columns.
+ * did not want to use a JavaScript lib like http://masonry.desandro.com/
+ * so I manually spread entries with a simple method: I roughly evaluate the
+ * height of a div according to title and description length.
+ */
+ protected function calculateColumns(array $links): array
+ {
+ // Entries to display, for each column.
+ $columns = [[], [], []];
+ // Rough estimate of columns fill.
+ $fill = [0, 0, 0];
+ foreach ($links as $link) {
+ // Roughly estimate length of entry (by counting characters)
+ // Title: 30 chars = 1 line. 1 line is 30 pixels height.
+ // Description: 836 characters gives roughly 342 pixel height.
+ // This is not perfect, but it's usually OK.
+ $length = strlen($link['title'] ?? '') + (342 * strlen($link['description'] ?? '')) / 836;
+ if (! empty($link['thumbnail'])) {
+ $length += 100; // 1 thumbnails roughly takes 100 pixels height.
+ }
+ // Then put in column which is the less filled:
+ $smallest = min($fill); // find smallest value in array.
+ $index = array_search($smallest, $fill); // find index of this smallest value.
+ array_push($columns[$index], $link); // Put entry in this column.
+ $fill[$index] += $length;
+ }
+
+ return $columns;
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Visitor;
+
+use Shaarli\Front\Exception\ShaarliFrontException;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+/**
+ * Controller used to render the error page, with a provided exception.
+ * It is actually used as a Slim error handler.
+ */
+class ErrorController extends ShaarliVisitorController
+{
+ public function __invoke(Request $request, Response $response, \Throwable $throwable): Response
+ {
+ // Unknown error encountered
+ $this->container->pageBuilder->reset();
+
+ if ($throwable instanceof ShaarliFrontException) {
+ // Functional error
+ $this->assignView('message', nl2br($throwable->getMessage()));
+
+ $response = $response->withStatus($throwable->getCode());
+ } else {
+ // Internal error (any other Throwable)
+ if ($this->container->conf->get('dev.debug', false)) {
+ $this->assignView('message', $throwable->getMessage());
+ $this->assignView(
+ 'stacktrace',
+ nl2br(get_class($throwable) .': '. PHP_EOL . $throwable->getTraceAsString())
+ );
+ } else {
+ $this->assignView('message', t('An unexpected error occurred.'));
+ }
+
+ $response = $response->withStatus(500);
+ }
+
+
+ return $response->write($this->render('error'));
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Visitor;
+
+use Shaarli\Feed\FeedBuilder;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+/**
+ * Class FeedController
+ *
+ * Slim controller handling ATOM and RSS feed.
+ */
+class FeedController extends ShaarliVisitorController
+{
+ 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->executePageHooks('render_feed', $data, $feedType);
+ $this->assignAllView($data);
+
+ $content = $this->render('feed.'. $feedType);
+
+ $cache->cache($content);
+
+ return $response->write($content);
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Visitor;
+
+use Shaarli\ApplicationUtils;
+use Shaarli\Container\ShaarliContainer;
+use Shaarli\Front\Exception\AlreadyInstalledException;
+use Shaarli\Front\Exception\ResourcePermissionException;
+use Shaarli\Languages;
+use Shaarli\Security\SessionManager;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+/**
+ * Slim controller used to render install page, and create initial configuration file.
+ */
+class InstallController extends ShaarliVisitorController
+{
+ public const SESSION_TEST_KEY = 'session_tested';
+ public const SESSION_TEST_VALUE = 'Working';
+
+ public function __construct(ShaarliContainer $container)
+ {
+ parent::__construct($container);
+
+ if (is_file($this->container->conf->getConfigFileExt())) {
+ throw new AlreadyInstalledException();
+ }
+ }
+
+ /**
+ * Display the install template page.
+ * Also test file permissions and sessions beforehand.
+ */
+ public function index(Request $request, Response $response): Response
+ {
+ // Before installation, we'll make sure that permissions are set properly, and sessions are working.
+ $this->checkPermissions();
+
+ if (static::SESSION_TEST_VALUE
+ !== $this->container->sessionManager->getSessionParameter(static::SESSION_TEST_KEY)
+ ) {
+ $this->container->sessionManager->setSessionParameter(static::SESSION_TEST_KEY, static::SESSION_TEST_VALUE);
+
+ return $this->redirect($response, '/install/session-test');
+ }
+
+ [$continents, $cities] = generateTimeZoneData(timezone_identifiers_list(), date_default_timezone_get());
+
+ $this->assignView('continents', $continents);
+ $this->assignView('cities', $cities);
+ $this->assignView('languages', Languages::getAvailableLanguages());
+
+ return $response->write($this->render('install'));
+ }
+
+ /**
+ * Route checking that the session parameter has been properly saved between two distinct requests.
+ * If the session parameter is preserved, redirect to install template page, otherwise displays error.
+ */
+ public function sessionTest(Request $request, Response $response): Response
+ {
+ // This part makes sure sessions works correctly.
+ // (Because on some hosts, session.save_path may not be set correctly,
+ // or we may not have write access to it.)
+ if (static::SESSION_TEST_VALUE
+ !== $this->container->sessionManager->getSessionParameter(static::SESSION_TEST_KEY)
+ ) {
+ // Step 2: Check if data in session is correct.
+ $msg = t(
+ '<pre>Sessions do not seem to work correctly on your server.<br>'.
+ 'Make sure the variable "session.save_path" is set correctly in your PHP config, '.
+ 'and that you have write access to it.<br>'.
+ 'It currently points to %s.<br>'.
+ 'On some browsers, accessing your server via a hostname like \'localhost\' '.
+ 'or any custom hostname without a dot causes cookie storage to fail. '.
+ 'We recommend accessing your server via it\'s IP address or Fully Qualified Domain Name.<br>'
+ );
+ $msg = sprintf($msg, $this->container->sessionManager->getSavePath());
+
+ $this->assignView('message', $msg);
+
+ return $response->write($this->render('error'));
+ }
+
+ return $this->redirect($response, '/install');
+ }
+
+ /**
+ * Save installation form and initialize config file and datastore if necessary.
+ */
+ public function save(Request $request, Response $response): Response
+ {
+ $timezone = 'UTC';
+ if (!empty($request->getParam('continent'))
+ && !empty($request->getParam('city'))
+ && isTimeZoneValid($request->getParam('continent'), $request->getParam('city'))
+ ) {
+ $timezone = $request->getParam('continent') . '/' . $request->getParam('city');
+ }
+ $this->container->conf->set('general.timezone', $timezone);
+
+ $login = $request->getParam('setlogin');
+ $this->container->conf->set('credentials.login', $login);
+ $salt = sha1(uniqid('', true) .'_'. mt_rand());
+ $this->container->conf->set('credentials.salt', $salt);
+ $this->container->conf->set('credentials.hash', sha1($request->getParam('setpassword') . $login . $salt));
+
+ if (!empty($request->getParam('title'))) {
+ $this->container->conf->set('general.title', escape($request->getParam('title')));
+ } else {
+ $this->container->conf->set(
+ 'general.title',
+ 'Shared bookmarks on '.escape(index_url($this->container->environment))
+ );
+ }
+
+ $this->container->conf->set('translation.language', escape($request->getParam('language')));
+ $this->container->conf->set('updates.check_updates', !empty($request->getParam('updateCheck')));
+ $this->container->conf->set('api.enabled', !empty($request->getParam('enableApi')));
+ $this->container->conf->set(
+ 'api.secret',
+ generate_api_secret(
+ $this->container->conf->get('credentials.login'),
+ $this->container->conf->get('credentials.salt')
+ )
+ );
+ $this->container->conf->set('general.header_link', $this->container->basePath . '/');
+
+ try {
+ // Everything is ok, let's create config file.
+ $this->container->conf->write($this->container->loginManager->isLoggedIn());
+ } catch (\Exception $e) {
+ $this->assignView('message', t('Error while writing config file after configuration update.'));
+ $this->assignView('stacktrace', $e->getMessage() . PHP_EOL . $e->getTraceAsString());
+
+ return $response->write($this->render('error'));
+ }
+
+ $this->container->sessionManager->setSessionParameter(
+ SessionManager::KEY_SUCCESS_MESSAGES,
+ [t('Shaarli is now configured. Please login and start shaaring your bookmarks!')]
+ );
+
+ return $this->redirect($response, '/login');
+ }
+
+ protected function checkPermissions(): bool
+ {
+ // Ensure Shaarli has proper access to its resources
+ $errors = ApplicationUtils::checkResourcePermissions($this->container->conf);
+ if (empty($errors)) {
+ return true;
+ }
+
+ $message = t('Insufficient permissions:') . PHP_EOL;
+ foreach ($errors as $error) {
+ $message .= PHP_EOL . $error;
+ }
+
+ throw new ResourcePermissionException($message);
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Visitor;
+
+use Shaarli\Front\Exception\CantLoginException;
+use Shaarli\Front\Exception\LoginBannedException;
+use Shaarli\Front\Exception\WrongTokenException;
+use Shaarli\Render\TemplatePage;
+use Shaarli\Security\CookieManager;
+use Shaarli\Security\SessionManager;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+/**
+ * Class LoginController
+ *
+ * Slim controller used to render the login page.
+ *
+ * The login page is not available if the user is banned
+ * or if open shaarli setting is enabled.
+ */
+class LoginController extends ShaarliVisitorController
+{
+ /**
+ * GET /login - Display the login page.
+ */
+ public function index(Request $request, Response $response): Response
+ {
+ try {
+ $this->checkLoginState();
+ } catch (CantLoginException $e) {
+ return $this->redirect($response, '/');
+ }
+
+ if ($request->getParam('login') !== null) {
+ $this->assignView('username', escape($request->getParam('login')));
+ }
+
+ $returnUrl = $request->getParam('returnurl') ?? $this->container->environment['HTTP_REFERER'] ?? null;
+
+ $this
+ ->assignView('returnurl', escape($returnUrl))
+ ->assignView('remember_user_default', $this->container->conf->get('privacy.remember_user_default', true))
+ ->assignView('pagetitle', t('Login') .' - '. $this->container->conf->get('general.title', 'Shaarli'))
+ ;
+
+ return $response->write($this->render(TemplatePage::LOGIN));
+ }
+
+ /**
+ * POST /login - Process login
+ */
+ public function login(Request $request, Response $response): Response
+ {
+ if (!$this->container->sessionManager->checkToken($request->getParam('token'))) {
+ throw new WrongTokenException();
+ }
+
+ try {
+ $this->checkLoginState();
+ } catch (CantLoginException $e) {
+ return $this->redirect($response, '/');
+ }
+
+ if (!$this->container->loginManager->checkCredentials(
+ $this->container->environment['REMOTE_ADDR'],
+ client_ip_id($this->container->environment),
+ $request->getParam('login'),
+ $request->getParam('password')
+ )
+ ) {
+ $this->container->loginManager->handleFailedLogin($this->container->environment);
+
+ $this->container->sessionManager->setSessionParameter(
+ SessionManager::KEY_ERROR_MESSAGES,
+ [t('Wrong login/password.')]
+ );
+
+ // Call controller directly instead of unnecessary redirection
+ return $this->index($request, $response);
+ }
+
+ $this->container->loginManager->handleSuccessfulLogin($this->container->environment);
+
+ $cookiePath = $this->container->basePath . '/';
+ $expirationTime = $this->saveLongLastingSession($request, $cookiePath);
+ $this->renewUserSession($cookiePath, $expirationTime);
+
+ // Force referer from given return URL
+ $this->container->environment['HTTP_REFERER'] = $request->getParam('returnurl');
+
+ return $this->redirectFromReferer($request, $response, ['login', 'install']);
+ }
+
+ /**
+ * Make sure that the user is allowed to login and/or displaying the login page:
+ * - not already logged in
+ * - not open shaarli
+ * - not banned
+ */
+ protected function checkLoginState(): bool
+ {
+ if ($this->container->loginManager->isLoggedIn()
+ || $this->container->conf->get('security.open_shaarli', false)
+ ) {
+ throw new CantLoginException();
+ }
+
+ if (true !== $this->container->loginManager->canLogin($this->container->environment)) {
+ throw new LoginBannedException();
+ }
+
+ return true;
+ }
+
+ /**
+ * @return int Session duration in seconds
+ */
+ protected function saveLongLastingSession(Request $request, string $cookiePath): int
+ {
+ if (empty($request->getParam('longlastingsession'))) {
+ // Standard session expiration (=when browser closes)
+ $expirationTime = 0;
+ } else {
+ // Keep the session cookie even after the browser closes
+ $this->container->sessionManager->setStaySignedIn(true);
+ $expirationTime = $this->container->sessionManager->extendSession();
+ }
+
+ $this->container->cookieManager->setCookieParameter(
+ CookieManager::STAY_SIGNED_IN,
+ $this->container->loginManager->getStaySignedInToken(),
+ $expirationTime,
+ $cookiePath
+ );
+
+ return $expirationTime;
+ }
+
+ protected function renewUserSession(string $cookiePath, int $expirationTime): void
+ {
+ // Send cookie with the new expiration date to the browser
+ $this->container->sessionManager->destroy();
+ $this->container->sessionManager->cookieParameters(
+ $expirationTime,
+ $cookiePath,
+ $this->container->environment['SERVER_NAME']
+ );
+ $this->container->sessionManager->start();
+ $this->container->sessionManager->regenerateId(true);
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Visitor;
+
+use Shaarli\Render\TemplatePage;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+/**
+ * Class OpenSearchController
+ *
+ * Slim controller used to render open search template.
+ * This allows to add Shaarli as a search engine within the browser.
+ */
+class OpenSearchController extends ShaarliVisitorController
+{
+ public function index(Request $request, Response $response): Response
+ {
+ $response = $response->withHeader('Content-Type', 'application/opensearchdescription+xml; charset=utf-8');
+
+ $this->assignView('serverurl', index_url($this->container->environment));
+
+ return $response->write($this->render(TemplatePage::OPEN_SEARCH));
+ }
+}
--- /dev/null
+<?php
+
+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;
+
+/**
+ * Class PicturesWallController
+ *
+ * Slim controller used to render the pictures wall page.
+ * If thumbnails mode is set to NONE, we just render the template without any image.
+ */
+class PictureWallController extends ShaarliVisitorController
+{
+ public function index(Request $request, Response $response): Response
+ {
+ if ($this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) === Thumbnailer::MODE_NONE) {
+ throw new ThumbnailsDisabledException();
+ }
+
+ $this->assignView(
+ 'pagetitle',
+ t('Picture wall') .' - '. $this->container->conf->get('general.title', 'Shaarli')
+ );
+
+ // Optionally filter the results:
+ $links = $this->container->bookmarkService->search($request->getQueryParams());
+ $linksToDisplay = [];
+
+ // Get only bookmarks which have a thumbnail.
+ // Note: we do not retrieve thumbnails here, the request is too heavy.
+ $formatter = $this->container->formatterFactory->getFormatter('raw');
+ foreach ($links as $key => $link) {
+ if (!empty($link->getThumbnail())) {
+ $linksToDisplay[] = $formatter->format($link);
+ }
+ }
+
+ $data = ['linksToDisplay' => $linksToDisplay];
+ $this->executePageHooks('render_picwall', $data, TemplatePage::PICTURE_WALL);
+
+ foreach ($data as $key => $value) {
+ $this->assignView($key, $value);
+ }
+
+ return $response->write($this->render(TemplatePage::PICTURE_WALL));
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Visitor;
+
+use Shaarli\Security\SessionManager;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+/**
+ * Slim controller used to handle filters stored in the visitor session, links per page, etc.
+ */
+class PublicSessionFilterController extends ShaarliVisitorController
+{
+ /**
+ * GET /links-per-page: set the number of bookmarks to display per page in homepage
+ */
+ public function linksPerPage(Request $request, Response $response): Response
+ {
+ $linksPerPage = $request->getParam('nb') ?? null;
+ if (null === $linksPerPage || false === is_numeric($linksPerPage)) {
+ $linksPerPage = $this->container->conf->get('general.links_per_page', 20);
+ }
+
+ $this->container->sessionManager->setSessionParameter(
+ SessionManager::KEY_LINKS_PER_PAGE,
+ abs(intval($linksPerPage))
+ );
+
+ return $this->redirectFromReferer($request, $response, ['linksperpage'], ['nb']);
+ }
+
+ /**
+ * GET /untagged-only: allows to display only bookmarks without any tag
+ */
+ public function untaggedOnly(Request $request, Response $response): Response
+ {
+ $this->container->sessionManager->setSessionParameter(
+ SessionManager::KEY_UNTAGGED_ONLY,
+ empty($this->container->sessionManager->getSessionParameter(SessionManager::KEY_UNTAGGED_ONLY))
+ );
+
+ return $this->redirectFromReferer($request, $response, ['untaggedonly', 'untagged-only']);
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Visitor;
+
+use Shaarli\Bookmark\BookmarkFilter;
+use Shaarli\Container\ShaarliContainer;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+/**
+ * Class ShaarliVisitorController
+ *
+ * All controllers accessible by visitors (non logged in users) should extend this abstract class.
+ * Contains a few helper function for template rendering, plugins, etc.
+ *
+ * @package Shaarli\Front\Controller\Visitor
+ */
+abstract class ShaarliVisitorController
+{
+ /** @var ShaarliContainer */
+ protected $container;
+
+ /** @param ShaarliContainer $container Slim container (extended for attribute completion). */
+ public function __construct(ShaarliContainer $container)
+ {
+ $this->container = $container;
+ }
+
+ /**
+ * Assign variables to RainTPL template through the PageBuilder.
+ *
+ * @param mixed $value Value to assign to the template
+ */
+ protected function assignView(string $name, $value): self
+ {
+ $this->container->pageBuilder->assign($name, $value);
+
+ 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));
+ $this->assignView('privateLinkcount', $this->container->bookmarkService->count(BookmarkFilter::$PRIVATE));
+
+ $this->executeDefaultHooks($template);
+
+ $this->assignView('plugin_errors', $this->container->pluginManager->getErrors());
+
+ return $this->container->pageBuilder->render($template, $this->container->basePath);
+ }
+
+ /**
+ * Call plugin hooks for header, footer and includes, specifying which page will be rendered.
+ * Then assign generated data to RainTPL.
+ */
+ protected function executeDefaultHooks(string $template): void
+ {
+ $common_hooks = [
+ 'includes',
+ 'header',
+ 'footer',
+ ];
+
+ foreach ($common_hooks as $name) {
+ $pluginData = [];
+ $this->container->pluginManager->executeHooks(
+ 'render_' . $name,
+ $pluginData,
+ [
+ 'target' => $template,
+ 'loggedin' => $this->container->loginManager->isLoggedIn(),
+ 'basePath' => $this->container->basePath,
+ ]
+ );
+ $this->assignView('plugins_' . $name, $pluginData);
+ }
+ }
+
+ protected function executePageHooks(string $hook, array &$data, string $template = null): void
+ {
+ $params = [
+ 'target' => $template,
+ 'loggedin' => $this->container->loginManager->isLoggedIn(),
+ 'basePath' => $this->container->basePath,
+ ];
+
+ $this->container->pluginManager->executeHooks(
+ $hook,
+ $data,
+ $params
+ );
+ }
+
+ /**
+ * Simple helper which prepend the base path to redirect path.
+ *
+ * @param Response $response
+ * @param string $path Absolute path, e.g.: `/`, or `/admin/shaare/123` regardless of install directory
+ *
+ * @return Response updated
+ */
+ protected function redirect(Response $response, string $path): Response
+ {
+ return $response->withRedirect($this->container->basePath . $path);
+ }
+
+ /**
+ * Generates a redirection to the previous page, based on the HTTP_REFERER.
+ * It fails back to the home page.
+ *
+ * @param array $loopTerms Terms to remove from path and query string to prevent direction loop.
+ * @param array $clearParams List of parameter to remove from the query string of the referrer.
+ */
+ protected function redirectFromReferer(
+ Request $request,
+ Response $response,
+ array $loopTerms = [],
+ array $clearParams = [],
+ string $anchor = null
+ ): Response {
+ $defaultPath = $this->container->basePath . '/';
+ $referer = $this->container->environment['HTTP_REFERER'] ?? null;
+
+ if (null !== $referer) {
+ $currentUrl = parse_url($referer);
+ parse_str($currentUrl['query'] ?? '', $params);
+ $path = $currentUrl['path'] ?? $defaultPath;
+ } else {
+ $params = [];
+ $path = $defaultPath;
+ }
+
+ // Prevent redirection loop
+ if (isset($currentUrl)) {
+ foreach ($clearParams as $value) {
+ unset($params[$value]);
+ }
+
+ $checkQuery = implode('', array_keys($params));
+ foreach ($loopTerms as $value) {
+ if (strpos($path . $checkQuery, $value) !== false) {
+ $params = [];
+ $path = $defaultPath;
+ break;
+ }
+ }
+ }
+
+ $queryString = count($params) > 0 ? '?'. http_build_query($params) : '';
+ $anchor = $anchor ? '#' . $anchor : '';
+
+ return $response->withRedirect($path . $queryString . $anchor);
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Visitor;
+
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+/**
+ * Class TagCloud
+ *
+ * Slim controller used to render the tag cloud and tag list pages.
+ */
+class TagCloudController extends ShaarliVisitorController
+{
+ protected const TYPE_CLOUD = 'cloud';
+ protected const TYPE_LIST = 'list';
+
+ /**
+ * Display the tag cloud through the template engine.
+ * This controller a few filters:
+ * - Visibility stored in the session for logged in users
+ * - `searchtags` query parameter: will return tags associated with filter in at least one bookmark
+ */
+ public function cloud(Request $request, Response $response): Response
+ {
+ return $this->processRequest(static::TYPE_CLOUD, $request, $response);
+ }
+
+ /**
+ * Display the tag list through the template engine.
+ * This controller a few filters:
+ * - Visibility stored in the session for logged in users
+ * - `searchtags` query parameter: will return tags associated with filter in at least one bookmark
+ * - `sort` query parameters:
+ * + `usage` (default): most used tags first
+ * + `alpha`: alphabetical order
+ */
+ public function list(Request $request, Response $response): Response
+ {
+ return $this->processRequest(static::TYPE_LIST, $request, $response);
+ }
+
+ /**
+ * Process the request for both tag cloud and tag list endpoints.
+ */
+ protected function processRequest(string $type, Request $request, Response $response): Response
+ {
+ if ($this->container->loginManager->isLoggedIn() === true) {
+ $visibility = $this->container->sessionManager->getSessionParameter('visibility');
+ }
+
+ $sort = $request->getQueryParam('sort');
+ $searchTags = $request->getQueryParam('searchtags');
+ $filteringTags = $searchTags !== null ? explode(' ', $searchTags) : [];
+
+ $tags = $this->container->bookmarkService->bookmarksCountPerTag($filteringTags, $visibility ?? null);
+
+ if (static::TYPE_CLOUD === $type || 'alpha' === $sort) {
+ // TODO: the sorting should be handled by bookmarkService instead of the controller
+ alphabetical_sort($tags, false, true);
+ }
+
+ if (static::TYPE_CLOUD === $type) {
+ $tags = $this->formatTagsForCloud($tags);
+ }
+
+ $searchTags = implode(' ', escape($filteringTags));
+ $data = [
+ 'search_tags' => $searchTags,
+ 'tags' => $tags,
+ ];
+ $this->executePageHooks('render_tag' . $type, $data, 'tag.' . $type);
+ $this->assignAllView($data);
+
+ $searchTags = !empty($searchTags) ? $searchTags .' - ' : '';
+ $this->assignView(
+ 'pagetitle',
+ $searchTags . t('Tag '. $type) .' - '. $this->container->conf->get('general.title', 'Shaarli')
+ );
+
+ return $response->write($this->render('tag.' . $type));
+ }
+
+ /**
+ * Format the tags array for the tag cloud template.
+ *
+ * @param array<string, int> $tags List of tags as key with count as value
+ *
+ * @return mixed[] List of tags as key, with count and expected font size in a subarray
+ */
+ protected function formatTagsForCloud(array $tags): array
+ {
+ // We sort tags alphabetically, then choose a font size according to count.
+ // First, find max value.
+ $maxCount = count($tags) > 0 ? max($tags) : 0;
+ $logMaxCount = $maxCount > 1 ? log($maxCount, 30) : 1;
+ $tagList = [];
+ foreach ($tags as $key => $value) {
+ // Tag font size scaling:
+ // default 15 and 30 logarithm bases affect scaling,
+ // 2.2 and 0.8 are arbitrary font sizes in em.
+ $size = log($value, 15) / $logMaxCount * 2.2 + 0.8;
+ $tagList[$key] = [
+ 'count' => $value,
+ 'size' => number_format($size, 2, '.', ''),
+ ];
+ }
+
+ return $tagList;
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Visitor;
+
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+/**
+ * Class TagController
+ *
+ * Slim controller handle tags.
+ */
+class TagController extends ShaarliVisitorController
+{
+ /**
+ * Add another tag in the current search through an HTTP redirection.
+ *
+ * @param array $args Should contain `newTag` key as tag to add to current search
+ */
+ public function addTag(Request $request, Response $response, array $args): Response
+ {
+ $newTag = $args['newTag'] ?? null;
+ $referer = $this->container->environment['HTTP_REFERER'] ?? null;
+
+ // In case browser does not send HTTP_REFERER, we search a single tag
+ if (null === $referer) {
+ if (null !== $newTag) {
+ return $this->redirect($response, '/?searchtags='. urlencode($newTag));
+ }
+
+ return $this->redirect($response, '/');
+ }
+
+ $currentUrl = parse_url($referer);
+ parse_str($currentUrl['query'] ?? '', $params);
+
+ if (null === $newTag) {
+ return $response->withRedirect(($currentUrl['path'] ?? './') .'?'. http_build_query($params));
+ }
+
+ // Prevent redirection loop
+ if (isset($params['addtag'])) {
+ unset($params['addtag']);
+ }
+
+ // Check if this tag is already in the search query and ignore it if it is.
+ // Each tag is always separated by a space
+ $currentTags = isset($params['searchtags']) ? explode(' ', $params['searchtags']) : [];
+
+ $addtag = true;
+ foreach ($currentTags as $value) {
+ if ($value === $newTag) {
+ $addtag = false;
+ break;
+ }
+ }
+
+ // Append the tag if necessary
+ if (true === $addtag) {
+ $currentTags[] = trim($newTag);
+ }
+
+ $params['searchtags'] = trim(implode(' ', $currentTags));
+
+ // We also remove page (keeping the same page has no sense, since the results are different)
+ unset($params['page']);
+
+ return $response->withRedirect(($currentUrl['path'] ?? './') .'?'. http_build_query($params));
+ }
+
+ /**
+ * Remove a tag from the current search through an HTTP redirection.
+ *
+ * @param array $args Should contain `tag` key as tag to remove from current search
+ */
+ public function removeTag(Request $request, Response $response, array $args): Response
+ {
+ $referer = $this->container->environment['HTTP_REFERER'] ?? null;
+
+ // If the referrer is not provided, we can update the search, so we failback on the bookmark list
+ if (empty($referer)) {
+ return $this->redirect($response, '/');
+ }
+
+ $tagToRemove = $args['tag'] ?? null;
+ $currentUrl = parse_url($referer);
+ parse_str($currentUrl['query'] ?? '', $params);
+
+ if (null === $tagToRemove) {
+ return $response->withRedirect(($currentUrl['path'] ?? './') .'?'. http_build_query($params));
+ }
+
+ // Prevent redirection loop
+ if (isset($params['removetag'])) {
+ unset($params['removetag']);
+ }
+
+ if (isset($params['searchtags'])) {
+ $tags = explode(' ', $params['searchtags']);
+ // Remove value from array $tags.
+ $tags = array_diff($tags, [$tagToRemove]);
+ $params['searchtags'] = implode(' ', $tags);
+
+ if (empty($params['searchtags'])) {
+ unset($params['searchtags']);
+ }
+
+ // We also remove page (keeping the same page has no sense, since the results are different)
+ unset($params['page']);
+ }
+
+ $queryParams = count($params) > 0 ? '?' . http_build_query($params) : '';
+
+ return $response->withRedirect(($currentUrl['path'] ?? './') . $queryParams);
+ }
+}
+++ /dev/null
-<?php
-
-declare(strict_types=1);
-
-namespace Shaarli\Front\Controller;
-
-use Shaarli\Front\Exception\LoginBannedException;
-use Slim\Http\Request;
-use Slim\Http\Response;
-
-/**
- * Class LoginController
- *
- * Slim controller used to render the login page.
- *
- * The login page is not available if the user is banned
- * or if open shaarli setting is enabled.
- *
- * @package Front\Controller
- */
-class LoginController extends ShaarliController
-{
- public function index(Request $request, Response $response): Response
- {
- if ($this->container->loginManager->isLoggedIn()
- || $this->container->conf->get('security.open_shaarli', false)
- ) {
- return $response->withRedirect('./');
- }
-
- $userCanLogin = $this->container->loginManager->canLogin($request->getServerParams());
- if ($userCanLogin !== true) {
- throw new LoginBannedException();
- }
-
- if ($request->getParam('username') !== null) {
- $this->assignView('username', escape($request->getParam('username')));
- }
-
- $this
- ->assignView('returnurl', escape($request->getServerParam('HTTP_REFERER')))
- ->assignView('remember_user_default', $this->container->conf->get('privacy.remember_user_default', true))
- ->assignView('pagetitle', t('Login') .' - '. $this->container->conf->get('general.title', 'Shaarli'))
- ;
-
- return $response->write($this->render('loginform'));
- }
-}
+++ /dev/null
-<?php
-
-declare(strict_types=1);
-
-namespace Shaarli\Front\Controller;
-
-use Shaarli\Bookmark\BookmarkFilter;
-use Shaarli\Container\ShaarliContainer;
-
-abstract class ShaarliController
-{
- /** @var ShaarliContainer */
- protected $container;
-
- /** @param ShaarliContainer $container Slim container (extended for attribute completion). */
- public function __construct(ShaarliContainer $container)
- {
- $this->container = $container;
- }
-
- /**
- * Assign variables to RainTPL template through the PageBuilder.
- *
- * @param mixed $value Value to assign to the template
- */
- protected function assignView(string $name, $value): self
- {
- $this->container->pageBuilder->assign($name, $value);
-
- return $this;
- }
-
- protected function render(string $template): string
- {
- $this->assignView('linkcount', $this->container->bookmarkService->count(BookmarkFilter::$ALL));
- $this->assignView('privateLinkcount', $this->container->bookmarkService->count(BookmarkFilter::$PRIVATE));
- $this->assignView('plugin_errors', $this->container->pluginManager->getErrors());
-
- $this->executeDefaultHooks($template);
-
- return $this->container->pageBuilder->render($template);
- }
-
- /**
- * Call plugin hooks for header, footer and includes, specifying which page will be rendered.
- * Then assign generated data to RainTPL.
- */
- protected function executeDefaultHooks(string $template): void
- {
- $common_hooks = [
- 'includes',
- 'header',
- 'footer',
- ];
-
- foreach ($common_hooks as $name) {
- $plugin_data = [];
- $this->container->pluginManager->executeHooks(
- 'render_' . $name,
- $plugin_data,
- [
- 'target' => $template,
- 'loggedin' => $this->container->loginManager->isLoggedIn()
- ]
- );
- $this->assignView('plugins_' . $name, $plugin_data);
- }
- }
-}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Exception;
+
+class AlreadyInstalledException extends ShaarliFrontException
+{
+ public function __construct()
+ {
+ $message = t('Shaarli has already been installed. Login to edit the configuration.');
+
+ parent::__construct($message, 401);
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Exception;
+
+class CantLoginException extends \Exception
+{
+
+}
namespace Shaarli\Front\Exception;
-class LoginBannedException extends ShaarliException
+class LoginBannedException extends ShaarliFrontException
{
public function __construct()
{
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Exception;
+
+/**
+ * Class OpenShaarliPasswordException
+ *
+ * Raised if the user tries to change the admin password on an open shaarli instance.
+ */
+class OpenShaarliPasswordException extends ShaarliFrontException
+{
+ public function __construct()
+ {
+ parent::__construct(t('You are not supposed to change a password on an Open Shaarli.'), 403);
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Exception;
+
+class ResourcePermissionException extends ShaarliFrontException
+{
+ public function __construct(string $message)
+ {
+ parent::__construct($message, 500);
+ }
+}
/**
* Class ShaarliException
*
- * Abstract exception class used to defined any custom exception thrown during front rendering.
+ * Exception class used to defined any custom exception thrown during front rendering.
*
* @package Front\Exception
*/
-abstract class ShaarliException extends \Exception
+class ShaarliFrontException extends \Exception
{
/** Override parent constructor to force $message and $httpCode parameters to be set. */
public function __construct(string $message, int $httpCode, Throwable $previous = null)
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Exception;
+
+class ThumbnailsDisabledException extends ShaarliFrontException
+{
+ public function __construct()
+ {
+ $message = t('Picture wall unavailable (thumbnails are disabled).');
+
+ parent::__construct($message, 400);
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Exception;
+
+/**
+ * Class UnauthorizedException
+ *
+ * Exception raised if the user tries to access a ShaarliAdminController while logged out.
+ */
+class UnauthorizedException extends \Exception
+{
+
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Exception;
+
+/**
+ * Class OpenShaarliPasswordException
+ *
+ * Raised if the user tries to perform an action with an invalid XSRF token.
+ */
+class WrongTokenException extends ShaarliFrontException
+{
+ public function __construct()
+ {
+ parent::__construct(t('Wrong token.'), 403);
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Http;
+
+/**
+ * Class HttpAccess
+ *
+ * This is mostly an OOP wrapper for HTTP functions defined in `HttpUtils`.
+ * It is used as dependency injection in Shaarli's container.
+ *
+ * @package Shaarli\Http
+ */
+class HttpAccess
+{
+ public function getHttpResponse($url, $timeout = 30, $maxBytes = 4194304, $curlWriteFunction = null)
+ {
+ return get_http_response($url, $timeout, $maxBytes, $curlWriteFunction);
+ }
+
+ public function getCurlDownloadCallback(
+ &$charset,
+ &$title,
+ &$description,
+ &$keywords,
+ $retrieveDescription,
+ $curlGetInfo = 'curl_getinfo'
+ ) {
+ return get_curl_download_callback(
+ $charset,
+ $title,
+ $description,
+ $keywords,
+ $retrieveDescription,
+ $curlGetInfo
+ );
+ }
+}
*/
function index_url($server)
{
- $scriptname = $server['SCRIPT_NAME'];
+ $scriptname = $server['SCRIPT_NAME'] ?? '';
if (endsWith($scriptname, 'index.php')) {
$scriptname = substr($scriptname, 0, -9);
}
}
/**
- * Returns the absolute URL of the current script, with the query
+ * Returns the absolute URL of the current script, with current route and query
*
* If the resource is "index.php", then it is removed (for better-looking URLs)
*
*/
function page_url($server)
{
+ $scriptname = $server['SCRIPT_NAME'] ?? '';
+ if (endsWith($scriptname, 'index.php')) {
+ $scriptname = substr($scriptname, 0, -9);
+ }
+
+ $route = ltrim($server['REQUEST_URI'] ?? '', $scriptname);
if (! empty($server['QUERY_STRING'])) {
- return index_url($server).'?'.$server['QUERY_STRING'];
+ return index_url($server) . $route . '?' . $server['QUERY_STRING'];
}
- return index_url($server);
+
+ return index_url($server) . $route;
}
/**
return ! empty($server['HTTPS']);
}
+
+/**
+ * Get cURL callback function for CURLOPT_WRITEFUNCTION
+ *
+ * @param string $charset to extract from the downloaded page (reference)
+ * @param string $title to extract from the downloaded page (reference)
+ * @param string $description to extract from the downloaded page (reference)
+ * @param string $keywords to extract from the downloaded page (reference)
+ * @param bool $retrieveDescription Automatically tries to retrieve description and keywords from HTML content
+ * @param string $curlGetInfo Optionally overrides curl_getinfo function
+ *
+ * @return Closure
+ */
+function get_curl_download_callback(
+ &$charset,
+ &$title,
+ &$description,
+ &$keywords,
+ $retrieveDescription,
+ $curlGetInfo = 'curl_getinfo'
+) {
+ $isRedirected = false;
+ $currentChunk = 0;
+ $foundChunk = null;
+
+ /**
+ * cURL callback function for CURLOPT_WRITEFUNCTION (called during the download).
+ *
+ * While downloading the remote page, we check that the HTTP code is 200 and content type is 'html/text'
+ * Then we extract the title and the charset and stop the download when it's done.
+ *
+ * @param resource $ch cURL resource
+ * @param string $data chunk of data being downloaded
+ *
+ * @return int|bool length of $data or false if we need to stop the download
+ */
+ return function (&$ch, $data) use (
+ $retrieveDescription,
+ $curlGetInfo,
+ &$charset,
+ &$title,
+ &$description,
+ &$keywords,
+ &$isRedirected,
+ &$currentChunk,
+ &$foundChunk
+ ) {
+ $currentChunk++;
+ $responseCode = $curlGetInfo($ch, CURLINFO_RESPONSE_CODE);
+ if (!empty($responseCode) && in_array($responseCode, [301, 302])) {
+ $isRedirected = true;
+ return strlen($data);
+ }
+ if (!empty($responseCode) && $responseCode !== 200) {
+ return false;
+ }
+ // After a redirection, the content type will keep the previous request value
+ // until it finds the next content-type header.
+ if (! $isRedirected || strpos(strtolower($data), 'content-type') !== false) {
+ $contentType = $curlGetInfo($ch, CURLINFO_CONTENT_TYPE);
+ }
+ if (!empty($contentType) && strpos($contentType, 'text/html') === false) {
+ return false;
+ }
+ if (!empty($contentType) && empty($charset)) {
+ $charset = header_extract_charset($contentType);
+ }
+ if (empty($charset)) {
+ $charset = html_extract_charset($data);
+ }
+ if (empty($title)) {
+ $title = html_extract_title($data);
+ $foundChunk = ! empty($title) ? $currentChunk : $foundChunk;
+ }
+ if ($retrieveDescription && empty($description)) {
+ $description = html_extract_tag('description', $data);
+ $foundChunk = ! empty($description) ? $currentChunk : $foundChunk;
+ }
+ if ($retrieveDescription && empty($keywords)) {
+ $keywords = html_extract_tag('keywords', $data);
+ if (! empty($keywords)) {
+ $foundChunk = $currentChunk;
+ // Keywords use the format tag1, tag2 multiple words, tag
+ // So we format them to match Shaarli's separator and glue multiple words with '-'
+ $keywords = implode(' ', array_map(function($keyword) {
+ return implode('-', preg_split('/\s+/', trim($keyword)));
+ }, explode(',', $keywords)));
+ }
+ }
+
+ // We got everything we want, stop the download.
+ // If we already found either the title, description or keywords,
+ // it's highly unlikely that we'll found the other metas further than
+ // in the same chunk of data or the next one. So we also stop the download after that.
+ if ((!empty($responseCode) && !empty($contentType) && !empty($charset)) && $foundChunk !== null
+ && (! $retrieveDescription
+ || $foundChunk < $currentChunk
+ || (!empty($title) && !empty($description) && !empty($keywords))
+ )
+ ) {
+ return false;
+ }
+
+ return strlen($data);
+ };
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Legacy;
+
+use Shaarli\Feed\FeedBuilder;
+use Shaarli\Front\Controller\Visitor\ShaarliVisitorController;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+/**
+ * We use this to maintain legacy routes, and redirect requests to the corresponding Slim route.
+ * Only public routes, and both `?addlink` and `?post` were kept here.
+ * Other routes will just display the linklist.
+ *
+ * @deprecated
+ */
+class LegacyController extends ShaarliVisitorController
+{
+ /** @var string[] Both `?post` and `?addlink` do not use `?do=` format. */
+ public const LEGACY_GET_ROUTES = [
+ 'post',
+ 'addlink',
+ ];
+
+ /**
+ * This method will call `$action` method, which will redirect to corresponding Slim route.
+ */
+ public function process(Request $request, Response $response, string $action): Response
+ {
+ if (!method_exists($this, $action)) {
+ throw new UnknowLegacyRouteException();
+ }
+
+ return $this->{$action}($request, $response);
+ }
+
+ /** Legacy route: ?post= */
+ public function post(Request $request, Response $response): Response
+ {
+ $parameters = count($request->getQueryParams()) > 0 ? '?' . http_build_query($request->getQueryParams()) : '';
+
+ if (!$this->container->loginManager->isLoggedIn()) {
+ return $this->redirect($response, '/login' . $parameters);
+ }
+
+ return $this->redirect($response, '/admin/shaare' . $parameters);
+ }
+
+ /** Legacy route: ?addlink= */
+ protected function addlink(Request $request, Response $response): Response
+ {
+ if (!$this->container->loginManager->isLoggedIn()) {
+ return $this->redirect($response, '/login');
+ }
+
+ return $this->redirect($response, '/admin/add-shaare');
+ }
+
+ /** Legacy route: ?do=login */
+ protected function login(Request $request, Response $response): Response
+ {
+ return $this->redirect($response, '/login');
+ }
+
+ /** Legacy route: ?do=logout */
+ protected function logout(Request $request, Response $response): Response
+ {
+ return $this->redirect($response, '/admin/logout');
+ }
+
+ /** Legacy route: ?do=picwall */
+ protected function picwall(Request $request, Response $response): Response
+ {
+ return $this->redirect($response, '/picture-wall');
+ }
+
+ /** Legacy route: ?do=tagcloud */
+ protected function tagcloud(Request $request, Response $response): Response
+ {
+ return $this->redirect($response, '/tags/cloud');
+ }
+
+ /** Legacy route: ?do=taglist */
+ protected function taglist(Request $request, Response $response): Response
+ {
+ return $this->redirect($response, '/tags/list');
+ }
+
+ /** Legacy route: ?do=daily */
+ protected function daily(Request $request, Response $response): Response
+ {
+ $dayParam = !empty($request->getParam('day')) ? '?day=' . escape($request->getParam('day')) : '';
+
+ return $this->redirect($response, '/daily' . $dayParam);
+ }
+
+ /** Legacy route: ?do=rss */
+ protected function rss(Request $request, Response $response): Response
+ {
+ return $this->feed($request, $response, FeedBuilder::$FEED_RSS);
+ }
+
+ /** Legacy route: ?do=atom */
+ protected function atom(Request $request, Response $response): Response
+ {
+ return $this->feed($request, $response, FeedBuilder::$FEED_ATOM);
+ }
+
+ /** Legacy route: ?do=opensearch */
+ protected function opensearch(Request $request, Response $response): Response
+ {
+ return $this->redirect($response, '/open-search');
+ }
+
+ /** Legacy route: ?do=dailyrss */
+ protected function dailyrss(Request $request, Response $response): Response
+ {
+ return $this->redirect($response, '/daily-rss');
+ }
+
+ /** Legacy route: ?do=feed */
+ protected function feed(Request $request, Response $response, string $feedType): Response
+ {
+ $parameters = count($request->getQueryParams()) > 0 ? '?' . http_build_query($request->getQueryParams()) : '';
+
+ return $this->redirect($response, '/feed/' . $feedType . $parameters);
+ }
+}
use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
use Shaarli\Exceptions\IOException;
use Shaarli\FileUtils;
+use Shaarli\Render\PageCacheManager;
/**
* Data storage for bookmarks.
$this->write();
- invalidateCaches($pageCacheDir);
+ $pageCacheManager = new PageCacheManager($pageCacheDir, $this->loggedIn);
+ $pageCacheManager->invalidateCaches();
}
/**
<?php
-namespace Shaarli;
+
+namespace Shaarli\Legacy;
/**
* Class Router
*
* (only displayable pages here)
+ *
+ * @deprecated
*/
-class Router
+class LegacyRouter
{
public static $AJAX_THUMB_UPDATE = 'ajax_thumb_update';
use Shaarli\ApplicationUtils;
use Shaarli\Bookmark\Bookmark;
use Shaarli\Bookmark\BookmarkArray;
-use Shaarli\Bookmark\LinkDB;
use Shaarli\Bookmark\BookmarkFilter;
use Shaarli\Bookmark\BookmarkIO;
+use Shaarli\Bookmark\LinkDB;
use Shaarli\Config\ConfigJson;
use Shaarli\Config\ConfigManager;
use Shaarli\Config\ConfigPhp;
if ($thumbnailsEnabled) {
$this->session['warnings'][] = t(
- 'You have enabled or changed thumbnails mode. <a href="?do=thumbs_update">Please synchronize them</a>.'
+ t('You have enabled or changed thumbnails mode.') .
+ '<a href="./admin/thumbnails">' . t('Please synchronize them.') . '</a>'
);
}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Legacy;
+
+class UnknowLegacyRouteException extends \Exception
+{
+}
use DateTimeZone;
use Exception;
use Katzgrau\KLogger\Logger;
+use Psr\Http\Message\UploadedFileInterface;
use Psr\Log\LogLevel;
use Shaarli\Bookmark\Bookmark;
use Shaarli\Bookmark\BookmarkServiceInterface;
/**
* Utilities to import and export bookmarks using the Netscape format
- * TODO: Not static, use a container.
*/
class NetscapeBookmarkUtils
{
+ /** @var BookmarkServiceInterface */
+ protected $bookmarkService;
+
+ /** @var ConfigManager */
+ protected $conf;
+
+ /** @var History */
+ protected $history;
+
+ public function __construct(BookmarkServiceInterface $bookmarkService, ConfigManager $conf, History $history)
+ {
+ $this->bookmarkService = $bookmarkService;
+ $this->conf = $conf;
+ $this->history = $history;
+ }
/**
* Filters bookmarks and adds Netscape-formatted fields
* - timestamp link addition date, using the Unix epoch format
* - taglist comma-separated tag list
*
- * @param BookmarkServiceInterface $bookmarkService Link datastore
* @param BookmarkFormatter $formatter instance
* @param string $selection Which bookmarks to export: (all|private|public)
* @param bool $prependNoteUrl Prepend note permalinks with the server's URL
* @param string $indexUrl Absolute URL of the Shaarli index page
*
* @return array The bookmarks to be exported, with additional fields
- *@throws Exception Invalid export selection
*
+ * @throws Exception Invalid export selection
*/
- public static function filterAndFormat(
- $bookmarkService,
+ public function filterAndFormat(
$formatter,
$selection,
$prependNoteUrl,
}
$bookmarkLinks = array();
- foreach ($bookmarkService->search([], $selection) as $bookmark) {
+ foreach ($this->bookmarkService->search([], $selection) as $bookmark) {
$link = $formatter->format($bookmark);
$link['taglist'] = implode(',', $bookmark->getTags());
if ($bookmark->isNote() && $prependNoteUrl) {
- $link['url'] = $indexUrl . $link['url'];
+ $link['url'] = rtrim($indexUrl, '/') . '/' . ltrim($link['url'], '/');
}
$bookmarkLinks[] = $link;
return $bookmarkLinks;
}
- /**
- * Generates an import status summary
- *
- * @param string $filename name of the file to import
- * @param int $filesize size of the file to import
- * @param int $importCount how many bookmarks were imported
- * @param int $overwriteCount how many bookmarks were overwritten
- * @param int $skipCount how many bookmarks were skipped
- * @param int $duration how many seconds did the import take
- *
- * @return string Summary of the bookmark import status
- */
- private static function importStatus(
- $filename,
- $filesize,
- $importCount = 0,
- $overwriteCount = 0,
- $skipCount = 0,
- $duration = 0
- ) {
- $status = sprintf(t('File %s (%d bytes) '), $filename, $filesize);
- if ($importCount == 0 && $overwriteCount == 0 && $skipCount == 0) {
- $status .= t('has an unknown file format. Nothing was imported.');
- } else {
- $status .= vsprintf(
- t(
- 'was successfully processed in %d seconds: '
- . '%d bookmarks imported, %d bookmarks overwritten, %d bookmarks skipped.'
- ),
- [$duration, $importCount, $overwriteCount, $skipCount]
- );
- }
- return $status;
- }
-
/**
* Imports Web bookmarks from an uploaded Netscape bookmark dump
*
- * @param array $post Server $_POST parameters
- * @param array $files Server $_FILES parameters
- * @param BookmarkServiceInterface $bookmarkService Loaded LinkDB instance
- * @param ConfigManager $conf instance
- * @param History $history History instance
+ * @param array $post Server $_POST parameters
+ * @param UploadedFileInterface $file File in PSR-7 object format
*
* @return string Summary of the bookmark import status
*/
- public static function import($post, $files, $bookmarkService, $conf, $history)
+ public function import($post, UploadedFileInterface $file)
{
$start = time();
- $filename = $files['filetoupload']['name'];
- $filesize = $files['filetoupload']['size'];
- $data = file_get_contents($files['filetoupload']['tmp_name']);
+ $filename = $file->getClientFilename();
+ $filesize = $file->getSize();
+ $data = (string) $file->getStream();
if (preg_match('/<!DOCTYPE NETSCAPE-Bookmark-file-1>/i', $data) === 0) {
- return self::importStatus($filename, $filesize);
+ return $this->importStatus($filename, $filesize);
}
// Overwrite existing bookmarks?
true, // nested tag support
$defaultTags, // additional user-specified tags
strval(1 - $defaultPrivacy), // defaultPub = 1 - defaultPrivacy
- $conf->get('resource.data_dir') // log path, will be overridden
+ $this->conf->get('resource.data_dir') // log path, will be overridden
);
$logger = new Logger(
- $conf->get('resource.data_dir'),
- !$conf->get('dev.debug') ? LogLevel::INFO : LogLevel::DEBUG,
+ $this->conf->get('resource.data_dir'),
+ !$this->conf->get('dev.debug') ? LogLevel::INFO : LogLevel::DEBUG,
[
'prefix' => 'import.',
'extension' => 'log',
$private = 0;
}
- $link = $bookmarkService->findByUrl($bkm['uri']);
+ $link = $this->bookmarkService->findByUrl($bkm['uri']);
$existingLink = $link !== null;
if (! $existingLink) {
$link = new Bookmark();
}
$link->setTitle($bkm['title']);
- $link->setUrl($bkm['uri'], $conf->get('security.allowed_protocols'));
+ $link->setUrl($bkm['uri'], $this->conf->get('security.allowed_protocols'));
$link->setDescription($bkm['note']);
$link->setPrivate($private);
$link->setTagsString($bkm['tags']);
- $bookmarkService->addOrSet($link, false);
+ $this->bookmarkService->addOrSet($link, false);
$importCount++;
}
- $bookmarkService->save();
- $history->importLinks();
+ $this->bookmarkService->save();
+ $this->history->importLinks();
$duration = time() - $start;
- return self::importStatus(
+
+ return $this->importStatus(
$filename,
$filesize,
$importCount,
$duration
);
}
+
+ /**
+ * Generates an import status summary
+ *
+ * @param string $filename name of the file to import
+ * @param int $filesize size of the file to import
+ * @param int $importCount how many bookmarks were imported
+ * @param int $overwriteCount how many bookmarks were overwritten
+ * @param int $skipCount how many bookmarks were skipped
+ * @param int $duration how many seconds did the import take
+ *
+ * @return string Summary of the bookmark import status
+ */
+ protected function importStatus(
+ $filename,
+ $filesize,
+ $importCount = 0,
+ $overwriteCount = 0,
+ $skipCount = 0,
+ $duration = 0
+ ) {
+ $status = sprintf(t('File %s (%d bytes) '), $filename, $filesize);
+ if ($importCount == 0 && $overwriteCount == 0 && $skipCount == 0) {
+ $status .= t('has an unknown file format. Nothing was imported.');
+ } else {
+ $status .= vsprintf(
+ t(
+ 'was successfully processed in %d seconds: '
+ . '%d bookmarks imported, %d bookmarks overwritten, %d bookmarks skipped.'
+ ),
+ [$duration, $importCount, $overwriteCount, $skipCount]
+ );
+ }
+ return $status;
+ }
}
*
* @var array $authorizedPlugins
*/
- private $authorizedPlugins;
+ private $authorizedPlugins = [];
/**
* List of loaded plugins.
$data['_LOGGEDIN_'] = $params['loggedin'];
}
+ if (isset($params['basePath'])) {
+ $data['_BASE_PATH_'] = $params['basePath'];
+ }
+
foreach ($this->loadedPlugins as $plugin) {
$hookFunction = $this->buildHookName($hook, $plugin);
if (function_exists($hookFunction)) {
- $data = call_user_func($hookFunction, $data, $this->conf);
+ try {
+ $data = call_user_func($hookFunction, $data, $this->conf);
+ } catch (\Throwable $e) {
+ $error = $plugin . t(' [plugin incompatibility]: ') . $e->getMessage();
+ $this->errors = array_unique(array_merge($this->errors, [$error]));
+ }
}
}
}
namespace Shaarli\Render;
use Exception;
+use exceptions\MissingBasePathException;
use RainTPL;
use Shaarli\ApplicationUtils;
use Shaarli\Bookmark\BookmarkServiceInterface;
use Shaarli\Config\ConfigManager;
+use Shaarli\Security\SessionManager;
use Shaarli\Thumbnailer;
/**
$this->isLoggedIn = $isLoggedIn;
}
+ /**
+ * Reset current state of template rendering.
+ * Mostly useful for error handling. We remove everything, and display the error template.
+ */
+ public function reset(): void
+ {
+ $this->tpl = false;
+ }
+
/**
* Initialize all default tpl tags.
*/
$this->tpl->assign('thumbnails_width', $this->conf->get('thumbnails.width'));
$this->tpl->assign('thumbnails_height', $this->conf->get('thumbnails.height'));
- if (!empty($_SESSION['warnings'])) {
- $this->tpl->assign('global_warnings', $_SESSION['warnings']);
- unset($_SESSION['warnings']);
- }
-
$this->tpl->assign('formatter', $this->conf->get('formatter', 'default'));
// To be removed with a proper theme configuration.
$this->tpl->assign('conf', $this->conf);
}
+ /**
+ * Affect variable after controller processing.
+ * Used for alert messages.
+ */
+ protected function finalize(string $basePath): void
+ {
+ // TODO: use the SessionManager
+ $messageKeys = [
+ SessionManager::KEY_SUCCESS_MESSAGES,
+ SessionManager::KEY_WARNING_MESSAGES,
+ SessionManager::KEY_ERROR_MESSAGES
+ ];
+ foreach ($messageKeys as $messageKey) {
+ if (!empty($_SESSION[$messageKey])) {
+ $this->tpl->assign('global_' . $messageKey, $_SESSION[$messageKey]);
+ unset($_SESSION[$messageKey]);
+ }
+ }
+
+ $this->assign('base_path', $basePath);
+ $this->assign(
+ 'asset_path',
+ $basePath . '/' .
+ rtrim($this->conf->get('resource.raintpl_tpl', 'tpl'), '/') . '/' .
+ $this->conf->get('resource.theme', 'default')
+ );
+ }
+
/**
* The following assign() method is basically the same as RainTPL (except lazy loading)
*
return true;
}
- /**
- * Render a specific page (using a template file).
- * e.g. $pb->renderPage('picwall');
- *
- * @param string $page Template filename (without extension).
- */
- public function renderPage($page)
- {
- if ($this->tpl === false) {
- $this->initialize();
- }
-
- $this->tpl->draw($page);
- }
-
/**
* Render a specific page as string (using a template file).
* e.g. $pb->render('picwall');
*
* @return string Processed template content
*/
- public function render(string $page): string
+ public function render(string $page, string $basePath): string
{
if ($this->tpl === false) {
$this->initialize();
}
- return $this->tpl->draw($page, true);
- }
+ $this->finalize($basePath);
- /**
- * Render a 404 page (uses the template : tpl/404.tpl)
- * usage: $PAGE->render404('The link was deleted')
- *
- * @param string $message A message to display what is not found
- */
- public function render404($message = '')
- {
- if (empty($message)) {
- $message = t('The page you are trying to reach does not exist or has been deleted.');
- }
- header($_SERVER['SERVER_PROTOCOL'] . ' ' . t('404 Not Found'));
- $this->tpl->assign('error_message', $message);
- $this->renderPage('404');
+ return $this->tpl->draw($page, true);
}
}
--- /dev/null
+<?php
+
+namespace Shaarli\Render;
+
+use Shaarli\Feed\CachedPage;
+
+/**
+ * Cache utilities
+ */
+class PageCacheManager
+{
+ /** @var string Cache directory */
+ protected $pageCacheDir;
+
+ /** @var bool */
+ protected $isLoggedIn;
+
+ public function __construct(string $pageCacheDir, bool $isLoggedIn)
+ {
+ $this->pageCacheDir = $pageCacheDir;
+ $this->isLoggedIn = $isLoggedIn;
+ }
+
+ /**
+ * Purges all cached pages
+ *
+ * @return string|null an error string if the directory is missing
+ */
+ public function purgeCachedPages(): ?string
+ {
+ if (!is_dir($this->pageCacheDir)) {
+ $error = sprintf(t('Cannot purge %s: no directory'), $this->pageCacheDir);
+ error_log($error);
+
+ return $error;
+ }
+
+ array_map('unlink', glob($this->pageCacheDir . '/*.cache'));
+
+ return null;
+ }
+
+ /**
+ * Invalidates caches when the database is changed or the user logs out.
+ */
+ public function invalidateCaches(): void
+ {
+ // Purge page cache shared by sessions.
+ $this->purgeCachedPages();
+ }
+
+ public function getCachePage(string $pageUrl): CachedPage
+ {
+ return new CachedPage(
+ $this->pageCacheDir,
+ $pageUrl,
+ false === $this->isLoggedIn
+ );
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Render;
+
+interface TemplatePage
+{
+ public const ERROR_404 = '404';
+ public const ADDLINK = 'addlink';
+ public const CHANGE_PASSWORD = 'changepassword';
+ public const CHANGE_TAG = 'changetag';
+ public const CONFIGURE = 'configure';
+ public const DAILY = 'daily';
+ public const DAILY_RSS = 'dailyrss';
+ public const EDIT_LINK = 'editlink';
+ public const ERROR = 'error';
+ public const EXPORT = 'export';
+ public const NETSCAPE_EXPORT_BOOKMARKS = 'export.bookmarks';
+ public const FEED_ATOM = 'feed.atom';
+ public const FEED_RSS = 'feed.rss';
+ public const IMPORT = 'import';
+ public const INSTALL = 'install';
+ public const LINKLIST = 'linklist';
+ public const LOGIN = 'loginform';
+ public const OPEN_SEARCH = 'opensearch';
+ public const PICTURE_WALL = 'picwall';
+ public const PLUGINS_ADMIN = 'pluginsadmin';
+ public const TAG_CLOUD = 'tag.cloud';
+ public const TAG_LIST = 'tag.list';
+ public const THUMBNAILS = 'thumbnails';
+ public const TOOLS = 'tools';
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Security;
+
+class CookieManager
+{
+ /** @var string Name of the cookie set after logging in **/
+ public const STAY_SIGNED_IN = 'shaarli_staySignedIn';
+
+ /** @var mixed $_COOKIE set by reference */
+ protected $cookies;
+
+ public function __construct(array &$cookies)
+ {
+ $this->cookies = $cookies;
+ }
+
+ public function setCookieParameter(string $key, string $value, int $expires, string $path): self
+ {
+ $this->cookies[$key] = $value;
+
+ setcookie($key, $value, $expires, $path);
+
+ return $this;
+ }
+
+ public function getCookieParameter(string $key, string $default = null): ?string
+ {
+ return $this->cookies[$key] ?? $default;
+ }
+}
*/
class LoginManager
{
- /** @var string Name of the cookie set after logging in **/
- public static $STAY_SIGNED_IN_COOKIE = 'shaarli_staySignedIn';
-
/** @var array A reference to the $_GLOBALS array */
protected $globals = [];
/** @var string User sign-in token depending on remote IP and credentials */
protected $staySignedInToken = '';
+ /** @var CookieManager */
+ protected $cookieManager;
/**
* Constructor
*
* @param ConfigManager $configManager Configuration Manager instance
* @param SessionManager $sessionManager SessionManager instance
+ * @param CookieManager $cookieManager CookieManager instance
*/
- public function __construct($configManager, $sessionManager)
+ public function __construct($configManager, $sessionManager, $cookieManager)
{
$this->configManager = $configManager;
$this->sessionManager = $sessionManager;
+ $this->cookieManager = $cookieManager;
$this->banManager = new BanManager(
$this->configManager->get('security.trusted_proxies', []),
$this->configManager->get('security.ban_after'),
/**
* Check user session state and validity (expiration)
*
- * @param array $cookie The $_COOKIE array
* @param string $clientIpId Client IP address identifier
*/
- public function checkLoginState($cookie, $clientIpId)
+ public function checkLoginState($clientIpId)
{
if (! $this->configManager->exists('credentials.login')) {
// Shaarli is not configured yet
return;
}
- if (isset($cookie[self::$STAY_SIGNED_IN_COOKIE])
- && $cookie[self::$STAY_SIGNED_IN_COOKIE] === $this->staySignedInToken
- ) {
+ if ($this->staySignedInToken === $this->cookieManager->getCookieParameter(CookieManager::STAY_SIGNED_IN)) {
// The user client has a valid stay-signed-in cookie
// Session information is updated with the current client information
$this->sessionManager->storeLoginInfo($clientIpId);
*/
class SessionManager
{
+ public const KEY_LINKS_PER_PAGE = 'LINKS_PER_PAGE';
+ public const KEY_VISIBILITY = 'visibility';
+ public const KEY_UNTAGGED_ONLY = 'untaggedonly';
+
+ public const KEY_SUCCESS_MESSAGES = 'successes';
+ public const KEY_WARNING_MESSAGES = 'warnings';
+ public const KEY_ERROR_MESSAGES = 'errors';
+
/** @var int Session expiration timeout, in seconds */
public static $SHORT_TIMEOUT = 3600; // 1 hour
/** @var bool Whether the user should stay signed in (LONG_TIMEOUT) */
protected $staySignedIn = false;
+ /** @var string */
+ protected $savePath;
+
/**
* Constructor
*
- * @param array $session The $_SESSION array (reference)
- * @param ConfigManager $conf ConfigManager instance
+ * @param array $session The $_SESSION array (reference)
+ * @param ConfigManager $conf ConfigManager instance
+ * @param string $savePath Session save path returned by builtin function session_save_path()
*/
- public function __construct(& $session, $conf)
+ public function __construct(&$session, $conf, string $savePath)
{
$this->session = &$session;
$this->conf = $conf;
+ $this->savePath = $savePath;
+ }
+
+ /**
+ * Initialize XSRF token and links per page session variables.
+ */
+ public function initialize(): void
+ {
+ if (!isset($this->session['tokens'])) {
+ $this->session['tokens'] = [];
+ }
+
+ if (!isset($this->session['LINKS_PER_PAGE'])) {
+ $this->session['LINKS_PER_PAGE'] = $this->conf->get('general.links_per_page', 20);
+ }
}
/**
{
return $this->session;
}
+
+ /**
+ * @param mixed $default value which will be returned if the $key is undefined
+ *
+ * @return mixed Content stored in session
+ */
+ public function getSessionParameter(string $key, $default = null)
+ {
+ return $this->session[$key] ?? $default;
+ }
+
+ /**
+ * Store a variable in user session.
+ *
+ * @param string $key Session key
+ * @param mixed $value Session value to store
+ *
+ * @return $this
+ */
+ public function setSessionParameter(string $key, $value): self
+ {
+ $this->session[$key] = $value;
+
+ return $this;
+ }
+
+ /**
+ * Store a variable in user session.
+ *
+ * @param string $key Session key
+ *
+ * @return $this
+ */
+ public function deleteSessionParameter(string $key): self
+ {
+ unset($this->session[$key]);
+
+ return $this;
+ }
+
+ public function getSavePath(): string
+ {
+ return $this->savePath;
+ }
+
+ /*
+ * Next public functions wrapping native PHP session API.
+ */
+
+ public function destroy(): bool
+ {
+ $this->session = [];
+
+ return session_destroy();
+ }
+
+ public function start(): bool
+ {
+ if (session_status() === PHP_SESSION_ACTIVE) {
+ $this->destroy();
+ }
+
+ return session_start();
+ }
+
+ public function cookieParameters(int $lifeTime, string $path, string $domain): bool
+ {
+ return session_set_cookie_params($lifeTime, $path, $domain);
+ }
+
+ public function regenerateId(bool $deleteOldSession = false): bool
+ {
+ return session_regenerate_id($deleteOldSession);
+ }
}
namespace Shaarli\Updater;
-use Shaarli\Config\ConfigManager;
use Shaarli\Bookmark\BookmarkServiceInterface;
+use Shaarli\Config\ConfigManager;
use Shaarli\Updater\Exception\UpdaterException;
/**
/**
* @var BookmarkServiceInterface instance.
*/
- protected $linkServices;
+ protected $bookmarkService;
/**
* @var ConfigManager $conf Configuration Manager instance.
*/
protected $methods;
+ /**
+ * @var string $basePath Shaarli root directory (from HTTP Request)
+ */
+ protected $basePath = null;
+
/**
* Object constructor.
*
public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn)
{
$this->doneUpdates = $doneUpdates;
- $this->linkServices = $linkDB;
+ $this->bookmarkService = $linkDB;
$this->conf = $conf;
$this->isLoggedIn = $isLoggedIn;
* Run all new updates.
* Update methods have to start with 'updateMethod' and return true (on success).
*
+ * @param string $basePath Shaarli root directory (from HTTP Request)
+ *
* @return array An array containing ran updates.
*
* @throws UpdaterException If something went wrong.
*/
- public function update()
+ public function update(string $basePath = null)
{
- $updatesRan = array();
+ $updatesRan = [];
// If the user isn't logged in, exit without updating.
if ($this->isLoggedIn !== true) {
{
return $this->doneUpdates;
}
+
+ public function readUpdates(string $updatesFilepath): array
+ {
+ return UpdaterUtils::read_updates_file($updatesFilepath);
+ }
+
+ public function writeUpdates(string $updatesFilepath, array $updates): void
+ {
+ UpdaterUtils::write_updates_file($updatesFilepath, $updates);
+ }
+
+ /**
+ * With the Slim routing system, default header link should be `/subfolder/` instead of `?`.
+ * Otherwise you can not go back to the home page.
+ * Example: `/subfolder/picture-wall` -> `/subfolder/picture-wall?` instead of `/subfolder/`.
+ */
+ public function updateMethodRelativeHomeLink(): bool
+ {
+ if ('?' === trim($this->conf->get('general.header_link'))) {
+ $this->conf->set('general.header_link', $this->basePath . '/', true, true);
+ }
+
+ return true;
+ }
+
+ /**
+ * With the Slim routing system, note bookmarks URL formatted `?abcdef`
+ * should be replaced with `/shaare/abcdef`
+ */
+ public function updateMethodMigrateExistingNotesUrl(): bool
+ {
+ $updated = false;
+
+ foreach ($this->bookmarkService->search() as $bookmark) {
+ if ($bookmark->isNote()
+ && startsWith($bookmark->getUrl(), '?')
+ && 1 === preg_match('/^\?([a-zA-Z0-9-_@]{6})($|&|#)/', $bookmark->getUrl(), $match)
+ ) {
+ $updated = true;
+ $bookmark = $bookmark->setUrl('/shaare/' . $match[1]);
+
+ $this->bookmarkService->set($bookmark, false);
+ }
+ }
+
+ if ($updated) {
+ $this->bookmarkService->save();
+ }
+
+ return true;
+ }
+
+ public function setBasePath(string $basePath): self
+ {
+ $this->basePath = $basePath;
+
+ return $this;
+ }
}
* It contains a recursive call to retrieve the thumb of the next link when it succeed.
* It also update the progress bar and other visual feedback elements.
*
+ * @param {string} basePath Shaarli subfolder for XHR requests
* @param {array} ids List of LinkID to update
* @param {int} i Current index in ids
* @param {object} elements List of DOM element to avoid retrieving them at each iteration
*/
-function updateThumb(ids, i, elements) {
+function updateThumb(basePath, ids, i, elements) {
const xhr = new XMLHttpRequest();
- xhr.open('POST', '?do=ajax_thumb_update');
+ xhr.open('PATCH', `${basePath}/admin/shaare/${ids[i]}/update-thumbnail`);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.responseType = 'json';
xhr.onload = () => {
elements.current.innerHTML = i;
elements.title.innerHTML = response.title;
if (response.thumbnail !== false) {
- elements.thumbnail.innerHTML = `<img src="${response.thumbnail}">`;
+ elements.thumbnail.innerHTML = `<img src="${basePath}/${response.thumbnail}">`;
}
if (i < ids.length) {
- updateThumb(ids, i, elements);
+ updateThumb(basePath, ids, i, elements);
}
}
};
- xhr.send(`id=${ids[i]}`);
+ xhr.send();
}
(() => {
+ const basePath = document.querySelector('input[name="js_base_path"]').value;
const ids = document.getElementsByName('ids')[0].value.split(',');
const elements = {
progressBar: document.querySelector('.progressbar > div'),
thumbnail: document.querySelector('.thumbnail-placeholder'),
title: document.querySelector('.thumbnail-link-title'),
};
- updateThumb(ids, 0, elements);
+ updateThumb(basePath, ids, 0, elements);
})();
/**
* Ajax request to refresh the CSRF token.
*/
-function refreshToken() {
+function refreshToken(basePath) {
+ console.log('refresh');
const xhr = new XMLHttpRequest();
- xhr.open('GET', '?do=token');
+ xhr.open('GET', `${basePath}/admin/token`);
xhr.onload = () => {
- const token = document.getElementById('token');
- token.setAttribute('value', xhr.responseText);
+ const elements = document.querySelectorAll('input[name="token"]');
+ [...elements].forEach((element) => {
+ console.log(element);
+ element.setAttribute('value', xhr.responseText);
+ });
};
xhr.send();
}
}
(() => {
+ const basePath = document.querySelector('input[name="js_base_path"]').value;
+
/**
* Handle responsive menu.
* Source: http://purecss.io/layouts/tucked-menu-vertical/
});
if (window.confirm(message)) {
- window.location = `?delete_link&lf_linkdate=${ids.join('+')}&token=${token.value}`;
+ window.location = `${basePath}/admin/shaare/delete?id=${ids.join('+')}&token=${token.value}`;
}
});
}
});
const ids = links.map(item => item.id);
- window.location = `?change_visibility&token=${token.value}&newVisibility=${visibility}&ids=${ids.join('+')}`;
+ window.location =
+ `${basePath}/admin/shaare/visibility?token=${token.value}&newVisibility=${visibility}&id=${ids.join('+')}`;
});
});
}
const refreshedToken = document.getElementById('token').value;
const fromtag = block.getAttribute('data-tag');
const xhr = new XMLHttpRequest();
- xhr.open('POST', '?do=changetag');
+ xhr.open('POST', `${basePath}/admin/tags`);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onload = () => {
if (xhr.status !== 200) {
input.setAttribute('value', totag);
findParent(input, 'div', { class: 'rename-tag-form' }).style.display = 'none';
block.querySelector('a.tag-link').innerHTML = htmlEntities(totag);
- block.querySelector('a.tag-link').setAttribute('href', `?searchtags=${encodeURIComponent(totag)}`);
- block.querySelector('a.rename-tag').setAttribute('href', `?do=changetag&fromtag=${encodeURIComponent(totag)}`);
+ block
+ .querySelector('a.tag-link')
+ .setAttribute('href', `${basePath}/?searchtags=${encodeURIComponent(totag)}`);
+ block
+ .querySelector('a.rename-tag')
+ .setAttribute('href', `${basePath}/admin/tags?fromtag=${encodeURIComponent(totag)}`);
// Refresh awesomplete values
existingTags = existingTags.map(tag => (tag === fromtag ? totag : tag));
}
};
xhr.send(`renametag=1&fromtag=${encodeURIComponent(fromtag)}&totag=${encodeURIComponent(totag)}&token=${refreshedToken}`);
- refreshToken();
+ refreshToken(basePath);
});
});
if (confirm(`Are you sure you want to delete the tag "${tag}"?`)) {
const xhr = new XMLHttpRequest();
- xhr.open('POST', '?do=changetag');
+ xhr.open('POST', `${basePath}/admin/tags`);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onload = () => {
block.remove();
};
xhr.send(encodeURI(`deletetag=1&fromtag=${tag}&token=${refreshedToken}`));
- refreshToken();
+ refreshToken(basePath);
existingTags = existingTags.filter(tagItem => tagItem !== tag);
awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes);
}
}
+.header-alert-message {
+ text-align: center;
+}
+
// CONTENT - GENERAL
.container {
position: relative;
"Shaarli\\Feed\\": "application/feed",
"Shaarli\\Formatter\\": "application/formatter",
"Shaarli\\Front\\": "application/front",
- "Shaarli\\Front\\Controller\\": "application/front/controllers",
+ "Shaarli\\Front\\Controller\\Admin\\": "application/front/controller/admin",
+ "Shaarli\\Front\\Controller\\Visitor\\": "application/front/controller/visitor",
"Shaarli\\Front\\Exception\\": "application/front/exceptions",
"Shaarli\\Http\\": "application/http",
"Shaarli\\Legacy\\": "application/legacy",
},
{
"name": "psr/log",
- "version": "1.1.2",
+ "version": "1.1.3",
"source": {
"type": "git",
"url": "https://github.com/php-fig/log.git",
- "reference": "446d54b4cb6bf489fc9d75f55843658e6f25d801"
+ "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/php-fig/log/zipball/446d54b4cb6bf489fc9d75f55843658e6f25d801",
- "reference": "446d54b4cb6bf489fc9d75f55843658e6f25d801",
+ "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc",
+ "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc",
"shasum": ""
},
"require": {
"psr",
"psr-3"
],
- "time": "2019-11-01T11:05:21+00:00"
+ "time": "2020-03-23T09:12:05+00:00"
},
{
"name": "pubsubhubbub/publisher",
},
{
"name": "phpdocumentor/reflection-common",
- "version": "2.0.0",
+ "version": "2.1.0",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/ReflectionCommon.git",
- "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a"
+ "reference": "6568f4687e5b41b054365f9ae03fcb1ed5f2069b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/63a995caa1ca9e5590304cd845c15ad6d482a62a",
- "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a",
+ "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/6568f4687e5b41b054365f9ae03fcb1ed5f2069b",
+ "reference": "6568f4687e5b41b054365f9ae03fcb1ed5f2069b",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
- "require-dev": {
- "phpunit/phpunit": "~6"
- },
"type": "library",
"extra": {
"branch-alias": {
"reflection",
"static analysis"
],
- "time": "2018-08-07T13:53:10+00:00"
+ "time": "2020-04-27T09:25:28+00:00"
},
{
"name": "phpdocumentor/reflection-docblock",
},
{
"name": "phpspec/prophecy",
- "version": "1.10.1",
+ "version": "v1.10.3",
"source": {
"type": "git",
"url": "https://github.com/phpspec/prophecy.git",
- "reference": "cbe1df668b3fe136bcc909126a0f529a78d4cbbc"
+ "reference": "451c3cd1418cf640de218914901e51b064abb093"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpspec/prophecy/zipball/cbe1df668b3fe136bcc909126a0f529a78d4cbbc",
- "reference": "cbe1df668b3fe136bcc909126a0f529a78d4cbbc",
+ "url": "https://api.github.com/repos/phpspec/prophecy/zipball/451c3cd1418cf640de218914901e51b064abb093",
+ "reference": "451c3cd1418cf640de218914901e51b064abb093",
"shasum": ""
},
"require": {
"doctrine/instantiator": "^1.0.2",
"php": "^5.3|^7.0",
"phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0",
- "sebastian/comparator": "^1.2.3|^2.0|^3.0",
- "sebastian/recursion-context": "^1.0|^2.0|^3.0"
+ "sebastian/comparator": "^1.2.3|^2.0|^3.0|^4.0",
+ "sebastian/recursion-context": "^1.0|^2.0|^3.0|^4.0"
},
"require-dev": {
"phpspec/phpspec": "^2.5 || ^3.2",
"spy",
"stub"
],
- "time": "2019-12-22T21:05:45+00:00"
+ "time": "2020-03-05T15:02:03+00:00"
},
{
"name": "phpunit/php-code-coverage",
"source": {
"type": "git",
"url": "https://github.com/Roave/SecurityAdvisories.git",
- "reference": "67ac6ea8f4a078c3c9b7aec5d7ae70f098c37389"
+ "reference": "5a342e2dc0408d026b97ee3176b5b406e54e3766"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/67ac6ea8f4a078c3c9b7aec5d7ae70f098c37389",
- "reference": "67ac6ea8f4a078c3c9b7aec5d7ae70f098c37389",
+ "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/5a342e2dc0408d026b97ee3176b5b406e54e3766",
+ "reference": "5a342e2dc0408d026b97ee3176b5b406e54e3766",
"shasum": ""
},
"conflict": {
"api-platform/core": ">=2.2,<2.2.10|>=2.3,<2.3.6",
"asymmetricrypt/asymmetricrypt": ">=0,<9.9.99",
"aws/aws-sdk-php": ">=3,<3.2.1",
+ "bagisto/bagisto": "<0.1.5",
+ "barrelstrength/sprout-base-email": "<3.9",
+ "bolt/bolt": "<3.6.10",
"brightlocal/phpwhois": "<=4.2.5",
+ "buddypress/buddypress": "<5.1.2",
"bugsnag/bugsnag-laravel": ">=2,<2.0.2",
"cakephp/cakephp": ">=1.3,<1.3.18|>=2,<2.4.99|>=2.5,<2.5.99|>=2.6,<2.6.12|>=2.7,<2.7.6|>=3,<3.5.18|>=3.6,<3.6.15|>=3.7,<3.7.7",
"cart2quote/module-quotation": ">=4.1.6,<=4.4.5|>=5,<5.4.4",
"cartalyst/sentry": "<=2.1.6",
+ "centreon/centreon": "<18.10.8|>=19,<19.4.5",
+ "cesnet/simplesamlphp-module-proxystatistics": "<3.1",
"codeigniter/framework": "<=3.0.6",
"composer/composer": "<=1-alpha.11",
"contao-components/mediaelement": ">=2.14.2,<2.21.1",
"doctrine/mongodb-odm": ">=1,<1.0.2",
"doctrine/mongodb-odm-bundle": ">=2,<3.0.1",
"doctrine/orm": ">=2,<2.4.8|>=2.5,<2.5.1",
+ "dolibarr/dolibarr": "<=10.0.6",
"dompdf/dompdf": ">=0.6,<0.6.2",
- "drupal/core": ">=7,<7.69|>=8,<8.7.11|>=8.8,<8.8.1",
- "drupal/drupal": ">=7,<7.69|>=8,<8.7.11|>=8.8,<8.8.1",
+ "drupal/core": ">=7,<7.69|>=8,<8.7.12|>=8.8,<8.8.4",
+ "drupal/drupal": ">=7,<7.69|>=8,<8.7.12|>=8.8,<8.8.4",
"endroid/qr-code-bundle": "<3.4.2",
+ "enshrined/svg-sanitize": "<0.13.1",
"erusev/parsedown": "<1.7.2",
- "ezsystems/ezplatform-admin-ui": ">=1.3,<1.3.5|>=1.4,<1.4.4",
- "ezsystems/ezpublish-kernel": ">=5.3,<5.3.12.1|>=5.4,<5.4.13.1|>=6,<6.7.9.1|>=6.8,<6.13.5.1|>=7,<7.2.4.1|>=7.3,<7.3.2.1",
- "ezsystems/ezpublish-legacy": ">=5.3,<5.3.12.6|>=5.4,<5.4.12.3|>=2011,<2017.12.4.3|>=2018.6,<2018.6.1.4|>=2018.9,<2018.9.1.3",
+ "ezsystems/demobundle": ">=5.4,<5.4.6.1",
+ "ezsystems/ezdemo-ls-extension": ">=5.4,<5.4.2.1",
+ "ezsystems/ezfind-ls": ">=5.3,<5.3.6.1|>=5.4,<5.4.11.1|>=2017.12,<2017.12.0.1",
+ "ezsystems/ezplatform": ">=1.7,<1.7.9.1|>=1.13,<1.13.5.1|>=2.5,<2.5.4",
+ "ezsystems/ezplatform-admin-ui": ">=1.3,<1.3.5|>=1.4,<1.4.6",
+ "ezsystems/ezplatform-admin-ui-assets": ">=4,<4.2",
+ "ezsystems/ezplatform-user": ">=1,<1.0.1",
+ "ezsystems/ezpublish-kernel": ">=5.3,<5.3.12.1|>=5.4,<5.4.14.1|>=6,<6.7.9.1|>=6.8,<6.13.6.2|>=7,<7.2.4.1|>=7.3,<7.3.2.1|>=7.5,<7.5.6.2",
+ "ezsystems/ezpublish-legacy": ">=5.3,<5.3.12.6|>=5.4,<5.4.14.1|>=2011,<2017.12.7.2|>=2018.6,<2018.6.1.4|>=2018.9,<2018.9.1.3|>=2019.3,<2019.3.4.2",
"ezsystems/repository-forms": ">=2.3,<2.3.2.1",
"ezyang/htmlpurifier": "<4.1.1",
"firebase/php-jwt": "<2",
"fooman/tcpdf": "<6.2.22",
"fossar/tcpdf-parser": "<6.2.22",
+ "friendsofsymfony/oauth2-php": "<1.3",
"friendsofsymfony/rest-bundle": ">=1.2,<1.2.2",
"friendsofsymfony/user-bundle": ">=1.2,<1.3.5",
"fuel/core": "<1.8.1",
+ "getgrav/grav": "<1.7-beta.8",
"gree/jose": "<=2.2",
"gregwar/rst": "<1.0.3",
"guzzlehttp/guzzle": ">=4-rc.2,<4.2.4|>=5,<5.3.1|>=6,<6.2.1",
"illuminate/cookie": ">=4,<=4.0.11|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.42|>=5.6,<5.6.30",
"illuminate/database": ">=4,<4.0.99|>=4.1,<4.1.29",
"illuminate/encryption": ">=4,<=4.0.11|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.40|>=5.6,<5.6.15",
+ "illuminate/view": ">=7,<7.1.2",
"ivankristianto/phpwhois": "<=4.3",
"james-heinrich/getid3": "<1.9.9",
"joomla/session": "<1.3.1",
"kazist/phpwhois": "<=4.2.6",
"kreait/firebase-php": ">=3.2,<3.8.1",
"la-haute-societe/tcpdf": "<6.2.22",
- "laravel/framework": ">=4,<4.0.99|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.42|>=5.6,<5.6.30",
+ "laravel/framework": ">=4,<4.0.99|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.42|>=5.6,<5.6.30|>=7,<7.1.2",
"laravel/socialite": ">=1,<1.0.99|>=2,<2.0.10",
"league/commonmark": "<0.18.3",
+ "librenms/librenms": "<1.53",
+ "magento/community-edition": ">=2,<2.2.10|>=2.3,<2.3.3",
"magento/magento1ce": "<1.9.4.3",
"magento/magento1ee": ">=1,<1.14.4.3",
"magento/product-community-edition": ">=2,<2.2.10|>=2.3,<2.3.2-p.2",
"monolog/monolog": ">=1.8,<1.12",
"namshi/jose": "<2.2",
+ "nzo/url-encryptor-bundle": ">=4,<4.3.2|>=5,<5.0.1",
"onelogin/php-saml": "<2.10.4",
+ "oneup/uploader-bundle": "<1.9.3|>=2,<2.1.5",
"openid/php-openid": "<2.3",
"oro/crm": ">=1.7,<1.7.4",
"oro/platform": ">=1.7,<1.7.4",
"paragonie/random_compat": "<2",
"paypal/merchant-sdk-php": "<3.12",
"pear/archive_tar": "<1.4.4",
+ "phpfastcache/phpfastcache": ">=5,<5.0.13",
"phpmailer/phpmailer": ">=5,<5.2.27|>=6,<6.0.6",
- "phpoffice/phpexcel": "<=1.8.1",
- "phpoffice/phpspreadsheet": "<=1.5",
+ "phpmyadmin/phpmyadmin": "<4.9.2",
+ "phpoffice/phpexcel": "<1.8.2",
+ "phpoffice/phpspreadsheet": "<1.8",
"phpunit/phpunit": ">=4.8.19,<4.8.28|>=5.0.10,<5.6.3",
"phpwhois/phpwhois": "<=4.2.5",
"phpxmlrpc/extras": "<0.6.1",
+ "pimcore/pimcore": "<6.3",
+ "prestashop/autoupgrade": ">=4,<4.10.1",
+ "prestashop/gamification": "<2.3.2",
+ "prestashop/ps_facetedsearch": "<3.4.1",
+ "privatebin/privatebin": "<1.2.2|>=1.3,<1.3.2",
"propel/propel": ">=2-alpha.1,<=2-alpha.7",
"propel/propel1": ">=1,<=1.7.1",
"pusher/pusher-php-server": "<2.2.1",
- "robrichards/xmlseclibs": ">=1,<3.0.4",
+ "robrichards/xmlseclibs": "<3.0.4",
"sabre/dav": ">=1.6,<1.6.99|>=1.7,<1.7.11|>=1.8,<1.8.9",
"scheb/two-factor-bundle": ">=0,<3.26|>=4,<4.11",
"sensiolabs/connect": "<4.2.3",
"serluck/phpwhois": "<=4.2.6",
"shopware/shopware": "<5.3.7",
- "silverstripe/cms": ">=3,<=3.0.11|>=3.1,<3.1.11",
+ "silverstripe/admin": ">=1.0.3,<1.0.4|>=1.1,<1.1.1",
+ "silverstripe/assets": ">=1,<1.4.7|>=1.5,<1.5.2",
+ "silverstripe/cms": "<4.3.6|>=4.4,<4.4.4",
+ "silverstripe/comments": ">=1.3,<1.9.99|>=2,<2.9.99|>=3,<3.1.1",
"silverstripe/forum": "<=0.6.1|>=0.7,<=0.7.3",
- "silverstripe/framework": ">=3,<3.6.7|>=3.7,<3.7.3|>=4,<4.4",
+ "silverstripe/framework": "<4.4.5|>=4.5,<4.5.2",
"silverstripe/graphql": ">=2,<2.0.5|>=3,<3.1.2",
"silverstripe/registry": ">=2.1,<2.1.2|>=2.2,<2.2.1",
"silverstripe/restfulserver": ">=1,<1.0.9|>=2,<2.0.4",
+ "silverstripe/subsites": ">=2,<2.1.1",
+ "silverstripe/taxonomy": ">=1.3,<1.3.1|>=2,<2.0.1",
"silverstripe/userforms": "<3",
"simple-updates/phpwhois": "<=1",
"simplesamlphp/saml2": "<1.10.6|>=2,<2.3.8|>=3,<3.1.4",
- "simplesamlphp/simplesamlphp": "<1.17.8",
+ "simplesamlphp/simplesamlphp": "<1.18.6",
"simplesamlphp/simplesamlphp-module-infocard": "<1.0.1",
+ "simplito/elliptic-php": "<1.0.6",
"slim/slim": "<2.6",
"smarty/smarty": "<3.1.33",
"socalnick/scn-social-auth": "<1.15.2",
"spoonity/tcpdf": "<6.2.22",
"squizlabs/php_codesniffer": ">=1,<2.8.1|>=3,<3.0.1",
+ "ssddanbrown/bookstack": "<0.29.2",
"stormpath/sdk": ">=0,<9.9.99",
- "studio-42/elfinder": "<2.1.48",
+ "studio-42/elfinder": "<2.1.49",
"swiftmailer/swiftmailer": ">=4,<5.4.5",
"sylius/admin-bundle": ">=1,<1.0.17|>=1.1,<1.1.9|>=1.2,<1.2.2",
"sylius/grid": ">=1,<1.1.19|>=1.2,<1.2.18|>=1.3,<1.3.13|>=1.4,<1.4.5|>=1.5,<1.5.1",
"sylius/grid-bundle": ">=1,<1.1.19|>=1.2,<1.2.18|>=1.3,<1.3.13|>=1.4,<1.4.5|>=1.5,<1.5.1",
- "sylius/sylius": ">=1,<1.1.18|>=1.2,<1.2.17|>=1.3,<1.3.12|>=1.4,<1.4.4",
+ "sylius/resource-bundle": "<1.3.13|>=1.4,<1.4.6|>=1.5,<1.5.1|>=1.6,<1.6.3",
+ "sylius/sylius": "<1.3.16|>=1.4,<1.4.12|>=1.5,<1.5.9|>=1.6,<1.6.5",
+ "symbiote/silverstripe-multivaluefield": ">=3,<3.0.99",
+ "symbiote/silverstripe-versionedfiles": "<=2.0.3",
"symfony/cache": ">=3.1,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8",
"symfony/dependency-injection": ">=2,<2.0.17|>=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7",
+ "symfony/error-handler": ">=4.4,<4.4.4|>=5,<5.0.4",
"symfony/form": ">=2.3,<2.3.35|>=2.4,<2.6.12|>=2.7,<2.7.50|>=2.8,<2.8.49|>=3,<3.4.20|>=4,<4.0.15|>=4.1,<4.1.9|>=4.2,<4.2.1",
"symfony/framework-bundle": ">=2,<2.3.18|>=2.4,<2.4.8|>=2.5,<2.5.2|>=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7",
- "symfony/http-foundation": ">=2,<2.8.52|>=3,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8",
+ "symfony/http-foundation": ">=2,<2.8.52|>=3,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8|>=4.4,<4.4.7|>=5,<5.0.7",
"symfony/http-kernel": ">=2,<2.8.52|>=3,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8",
"symfony/intl": ">=2.7,<2.7.38|>=2.8,<2.8.31|>=3,<3.2.14|>=3.3,<3.3.13",
"symfony/mime": ">=4.3,<4.3.8",
"symfony/polyfill-php55": ">=1,<1.10",
"symfony/proxy-manager-bridge": ">=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7",
"symfony/routing": ">=2,<2.0.19",
- "symfony/security": ">=2,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7",
+ "symfony/security": ">=2,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7|>=4.4,<4.4.7|>=5,<5.0.7",
"symfony/security-bundle": ">=2,<2.7.48|>=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11",
"symfony/security-core": ">=2.4,<2.6.13|>=2.7,<2.7.9|>=2.7.30,<2.7.32|>=2.8,<2.8.37|>=3,<3.3.17|>=3.4,<3.4.7|>=4,<4.0.7",
"symfony/security-csrf": ">=2.4,<2.7.48|>=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11",
"symfony/security-guard": ">=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11",
- "symfony/security-http": ">=2.3,<2.3.41|>=2.4,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.2.12|>=4.3,<4.3.8",
+ "symfony/security-http": ">=2.3,<2.3.41|>=2.4,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.2.12|>=4.3,<4.3.8|>=4.4,<4.4.7|>=5,<5.0.7",
"symfony/serializer": ">=2,<2.0.11",
- "symfony/symfony": ">=2,<2.8.52|>=3,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8",
+ "symfony/symfony": ">=2,<2.8.52|>=3,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8|>=4.4,<4.4.7|>=5,<5.0.7",
"symfony/translation": ">=2,<2.0.17",
"symfony/validator": ">=2,<2.0.24|>=2.1,<2.1.12|>=2.2,<2.2.5|>=2.3,<2.3.3",
"symfony/var-exporter": ">=4.2,<4.2.12|>=4.3,<4.3.8",
"titon/framework": ">=0,<9.9.99",
"truckersmp/phpwhois": "<=4.3.1",
"twig/twig": "<1.38|>=2,<2.7",
- "typo3/cms": ">=6.2,<6.2.30|>=7,<7.6.32|>=8,<8.7.30|>=9,<9.5.12|>=10,<10.2.1",
- "typo3/cms-core": ">=8,<8.7.30|>=9,<9.5.12|>=10,<10.2.1",
+ "typo3/cms": ">=6.2,<6.2.30|>=7,<7.6.32|>=8,<8.7.30|>=9,<9.5.17|>=10,<10.4.2",
+ "typo3/cms-core": ">=8,<8.7.30|>=9,<9.5.17|>=10,<10.4.2",
"typo3/flow": ">=1,<1.0.4|>=1.1,<1.1.1|>=2,<2.0.1|>=2.3,<2.3.16|>=3,<3.0.10|>=3.1,<3.1.7|>=3.2,<3.2.7|>=3.3,<3.3.5",
"typo3/neos": ">=1.1,<1.1.3|>=1.2,<1.2.13|>=2,<2.0.4",
"typo3/phar-stream-wrapper": ">=1,<2.1.1|>=3,<3.1.1",
"ua-parser/uap-php": "<3.8",
+ "usmanhalalit/pixie": "<1.0.3|>=2,<2.0.2",
+ "verot/class.upload.php": "<=1.0.3|>=2,<=2.0.4",
"wallabag/tcpdf": "<6.2.22",
"willdurand/js-translation-bundle": "<2.1.1",
+ "yii2mod/yii2-cms": "<1.9.2",
"yiisoft/yii": ">=1.1.14,<1.1.15",
"yiisoft/yii2": "<2.0.15",
"yiisoft/yii2-bootstrap": "<2.0.4",
"yiisoft/yii2-gii": "<2.0.4",
"yiisoft/yii2-jui": "<2.0.4",
"yiisoft/yii2-redis": "<2.0.8",
+ "yourls/yourls": "<1.7.4",
"zendframework/zend-cache": ">=2.4,<2.4.8|>=2.5,<2.5.3",
"zendframework/zend-captcha": ">=2,<2.4.9|>=2.5,<2.5.2",
"zendframework/zend-crypt": ">=2,<2.4.9|>=2.5,<2.5.2",
"name": "Marco Pivetta",
"email": "ocramius@gmail.com",
"role": "maintainer"
+ },
+ {
+ "name": "Ilya Tribusean",
+ "email": "slash3b@gmail.com",
+ "role": "maintainer"
}
],
"description": "Prevents installation of composer packages with known security vulnerabilities: no API, simply require it",
- "time": "2020-01-06T19:16:46+00:00"
+ "time": "2020-05-12T11:18:47+00:00"
},
{
"name": "sebastian/code-unit-reverse-lookup",
},
{
"name": "squizlabs/php_codesniffer",
- "version": "3.5.3",
+ "version": "3.5.5",
"source": {
"type": "git",
"url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
- "reference": "557a1fc7ac702c66b0bbfe16ab3d55839ef724cb"
+ "reference": "73e2e7f57d958e7228fce50dc0c61f58f017f9f6"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/557a1fc7ac702c66b0bbfe16ab3d55839ef724cb",
- "reference": "557a1fc7ac702c66b0bbfe16ab3d55839ef724cb",
+ "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/73e2e7f57d958e7228fce50dc0c61f58f017f9f6",
+ "reference": "73e2e7f57d958e7228fce50dc0c61f58f017f9f6",
"shasum": ""
},
"require": {
"phpcs",
"standards"
],
- "time": "2019-12-04T04:46:47+00:00"
+ "time": "2020-04-17T01:09:41+00:00"
},
{
"name": "symfony/console",
- "version": "v4.4.2",
+ "version": "v4.4.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
- "reference": "82437719dab1e6bdd28726af14cb345c2ec816d0"
+ "reference": "10bb3ee3c97308869d53b3e3d03f6ac23ff985f7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/console/zipball/82437719dab1e6bdd28726af14cb345c2ec816d0",
- "reference": "82437719dab1e6bdd28726af14cb345c2ec816d0",
+ "url": "https://api.github.com/repos/symfony/console/zipball/10bb3ee3c97308869d53b3e3d03f6ac23ff985f7",
+ "reference": "10bb3ee3c97308869d53b3e3d03f6ac23ff985f7",
"shasum": ""
},
"require": {
],
"description": "Symfony Console Component",
"homepage": "https://symfony.com",
- "time": "2019-12-17T10:32:23+00:00"
+ "time": "2020-03-30T11:41:10+00:00"
},
{
"name": "symfony/finder",
- "version": "v4.4.2",
+ "version": "v4.4.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
- "reference": "ce8743441da64c41e2a667b8eb66070444ed911e"
+ "reference": "5729f943f9854c5781984ed4907bbb817735776b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/finder/zipball/ce8743441da64c41e2a667b8eb66070444ed911e",
- "reference": "ce8743441da64c41e2a667b8eb66070444ed911e",
+ "url": "https://api.github.com/repos/symfony/finder/zipball/5729f943f9854c5781984ed4907bbb817735776b",
+ "reference": "5729f943f9854c5781984ed4907bbb817735776b",
"shasum": ""
},
"require": {
],
"description": "Symfony Finder Component",
"homepage": "https://symfony.com",
- "time": "2019-11-17T21:56:56+00:00"
+ "time": "2020-03-27T16:54:36+00:00"
},
{
"name": "symfony/polyfill-ctype",
- "version": "v1.13.1",
+ "version": "v1.17.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
- "reference": "f8f0b461be3385e56d6de3dbb5a0df24c0c275e3"
+ "reference": "e94c8b1bbe2bc77507a1056cdb06451c75b427f9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/f8f0b461be3385e56d6de3dbb5a0df24c0c275e3",
- "reference": "f8f0b461be3385e56d6de3dbb5a0df24c0c275e3",
+ "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e94c8b1bbe2bc77507a1056cdb06451c75b427f9",
+ "reference": "e94c8b1bbe2bc77507a1056cdb06451c75b427f9",
"shasum": ""
},
"require": {
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.13-dev"
+ "dev-master": "1.17-dev"
}
},
"autoload": {
"polyfill",
"portable"
],
- "time": "2019-11-27T13:56:44+00:00"
+ "time": "2020-05-12T16:14:59+00:00"
},
{
"name": "symfony/polyfill-mbstring",
- "version": "v1.13.1",
+ "version": "v1.17.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
- "reference": "7b4aab9743c30be783b73de055d24a39cf4b954f"
+ "reference": "fa79b11539418b02fc5e1897267673ba2c19419c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/7b4aab9743c30be783b73de055d24a39cf4b954f",
- "reference": "7b4aab9743c30be783b73de055d24a39cf4b954f",
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fa79b11539418b02fc5e1897267673ba2c19419c",
+ "reference": "fa79b11539418b02fc5e1897267673ba2c19419c",
"shasum": ""
},
"require": {
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.13-dev"
+ "dev-master": "1.17-dev"
}
},
"autoload": {
"portable",
"shim"
],
- "time": "2019-11-27T14:18:11+00:00"
+ "time": "2020-05-12T16:47:27+00:00"
},
{
"name": "symfony/polyfill-php73",
- "version": "v1.13.1",
+ "version": "v1.17.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php73.git",
- "reference": "4b0e2222c55a25b4541305a053013d5647d3a25f"
+ "reference": "a760d8964ff79ab9bf057613a5808284ec852ccc"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/4b0e2222c55a25b4541305a053013d5647d3a25f",
- "reference": "4b0e2222c55a25b4541305a053013d5647d3a25f",
+ "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/a760d8964ff79ab9bf057613a5808284ec852ccc",
+ "reference": "a760d8964ff79ab9bf057613a5808284ec852ccc",
"shasum": ""
},
"require": {
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.13-dev"
+ "dev-master": "1.17-dev"
}
},
"autoload": {
"portable",
"shim"
],
- "time": "2019-11-27T16:25:15+00:00"
+ "time": "2020-05-12T16:47:27+00:00"
},
{
"name": "symfony/service-contracts",
},
{
"name": "webmozart/assert",
- "version": "1.6.0",
+ "version": "1.8.0",
"source": {
"type": "git",
"url": "https://github.com/webmozart/assert.git",
- "reference": "573381c0a64f155a0d9a23f4b0c797194805b925"
+ "reference": "ab2cb0b3b559010b75981b1bdce728da3ee90ad6"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/webmozart/assert/zipball/573381c0a64f155a0d9a23f4b0c797194805b925",
- "reference": "573381c0a64f155a0d9a23f4b0c797194805b925",
+ "url": "https://api.github.com/repos/webmozart/assert/zipball/ab2cb0b3b559010b75981b1bdce728da3ee90ad6",
+ "reference": "ab2cb0b3b559010b75981b1bdce728da3ee90ad6",
"shasum": ""
},
"require": {
"symfony/polyfill-ctype": "^1.8"
},
"conflict": {
- "vimeo/psalm": "<3.6.0"
+ "vimeo/psalm": "<3.9.1"
},
"require-dev": {
"phpunit/phpunit": "^4.8.36 || ^7.5.13"
"check",
"validate"
],
- "time": "2019-11-24T13:36:37+00:00"
+ "time": "2020-04-18T12:12:48+00:00"
}
],
"aliases": [],
| ------------- |:-------------:|
| [render_header](#render_header) | Allow plugin to add content in page headers. |
| [render_includes](#render_includes) | Allow plugin to include their own CSS files. |
-| [render_footer](#render_footer) | Allow plugin to add content in page footer and include their own JS files. |
+| [render_footer](#render_footer) | Allow plugin to add content in page footer and include their own JS files. |
| [render_linklist](#render_linklist) | It allows to add content at the begining and end of the page, after every link displayed and to alter link data. |
| [render_editlink](#render_editlink) | Allow to add fields in the form, or display elements. |
| [render_tools](#render_tools) | Allow to add content at the end of the page. |
### Placeholder system
-In order to make plugins work with every custom themes, you need to add variable placeholder in your templates.
+In order to make plugins work with every custom themes, you need to add variable placeholder in your templates.
It's a RainTPL loop like this:
At the end of file, before clearing floating blocks:
- {if="!empty($plugin_errors) && isLoggedIn()"}
+ {if="!empty($plugin_errors) && $is_logged_in"}
<ul class="errors">
{loop="plugin_errors"}
<li>{$value}</li>
### 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
- Click on the `RSS Feed` button.
- You are presented with an RSS feed showing only these links. Subscribe to it to receive only updates with this tag.
- The same method **also works for a full-text search** (_Search_ box) **and for the Picture Wall** (want to only see pictures about `nature`?)
-- You can also build the URLs manually:
+- You can also build the URLs manually:
- `https://my.shaarli.domain/?do=rss&searchtags=nature`
- - `https://my.shaarli.domain/links/?do=picwall&searchterm=poney`
+ - `https://my.shaarli.domain/links/picture-wall?searchterm=poney`
![](images/rss-filter-1.png) ![](images/rss-filter-2.png)
```
http://<replace_domain>/
http://<replace_domain>/?nonope
-http://<replace_domain>/?do=addlink
-http://<replace_domain>/?do=changepasswd
-http://<replace_domain>/?do=changetag
-http://<replace_domain>/?do=configure
-http://<replace_domain>/?do=tools
-http://<replace_domain>/?do=daily
-http://<replace_domain>/?post
-http://<replace_domain>/?do=export
-http://<replace_domain>/?do=import
+http://<replace_domain>/admin/add-shaare
+http://<replace_domain>/admin/password
+http://<replace_domain>/admin/tags
+http://<replace_domain>/admin/configure
+http://<replace_domain>/admin/tools
+http://<replace_domain>/daily
+http://<replace_domain>/admin/shaare
+http://<replace_domain>/admin/export
+http://<replace_domain>/admin/import
http://<replace_domain>/login
-http://<replace_domain>/?do=picwall
-http://<replace_domain>/?do=pluginadmin
-http://<replace_domain>/?do=tagcloud
-http://<replace_domain>/?do=taglist
+http://<replace_domain>/picture-wall
+http://<replace_domain>/admin/plugins
+http://<replace_domain>/tags/cloud
+http://<replace_domain>/tags/list
```
#### Improve existing translation
msgid ""
msgstr ""
"Project-Id-Version: Shaarli\n"
-"POT-Creation-Date: 2019-07-13 10:45+0200\n"
-"PO-Revision-Date: 2019-07-13 10:49+0200\n"
+"POT-Creation-Date: 2020-08-27 12:01+0200\n"
+"PO-Revision-Date: 2020-08-27 12:02+0200\n"
"Last-Translator: \n"
"Language-Team: Shaarli\n"
"Language: fr_FR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Generator: Poedit 2.2.1\n"
+"X-Generator: Poedit 2.3\n"
"X-Poedit-Basepath: ../../../..\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
"X-Poedit-SourceCharset: UTF-8\n"
"X-Poedit-KeywordsList: t:1,2;t\n"
-"X-Poedit-SearchPath-0: .\n"
-"X-Poedit-SearchPathExcluded-0: node_modules\n"
-"X-Poedit-SearchPathExcluded-1: vendor\n"
+"X-Poedit-SearchPath-0: application\n"
+"X-Poedit-SearchPath-1: tmp\n"
+"X-Poedit-SearchPath-2: index.php\n"
+"X-Poedit-SearchPath-3: init.php\n"
+"X-Poedit-SearchPath-4: plugins\n"
-#: application/ApplicationUtils.php:159
+#: application/ApplicationUtils.php:161
#, php-format
msgid ""
"Your PHP version is obsolete! Shaarli requires at least PHP %s, and thus "
"peut donc pas fonctionner. Votre version de PHP a des failles de sécurités "
"connues et devrait être mise à jour au plus tôt."
-#: application/ApplicationUtils.php:189 application/ApplicationUtils.php:201
+#: application/ApplicationUtils.php:192 application/ApplicationUtils.php:204
msgid "directory is not readable"
msgstr "le répertoire n'est pas accessible en lecture"
-#: application/ApplicationUtils.php:204
+#: application/ApplicationUtils.php:207
msgid "directory is not writable"
msgstr "le répertoire n'est pas accessible en écriture"
-#: application/ApplicationUtils.php:222
+#: application/ApplicationUtils.php:225
msgid "file is not readable"
msgstr "le fichier n'est pas accessible en lecture"
-#: application/ApplicationUtils.php:225
+#: application/ApplicationUtils.php:228
msgid "file is not writable"
msgstr "le fichier n'est pas accessible en écriture"
-#: application/History.php:178
+#: application/History.php:179
msgid "History file isn't readable or writable"
msgstr "Le fichier d'historique n'est pas accessible en lecture ou en écriture"
-#: application/History.php:189
+#: application/History.php:190
msgid "Could not parse history file"
msgstr "Format incorrect pour le fichier d'historique"
"l'extension php-gd doit être chargée pour utiliser les miniatures. Les "
"miniatures sont désormais désactivées. Rechargez la page."
-#: application/Utils.php:379 tests/UtilsTest.php:343
+#: application/Utils.php:383
msgid "Setting not set"
msgstr "Paramètre non défini"
-#: application/Utils.php:386 tests/UtilsTest.php:341 tests/UtilsTest.php:342
+#: application/Utils.php:390
msgid "Unlimited"
msgstr "Illimité"
-#: application/Utils.php:389 tests/UtilsTest.php:338 tests/UtilsTest.php:339
-#: tests/UtilsTest.php:353
+#: application/Utils.php:393
msgid "B"
msgstr "o"
-#: application/Utils.php:389 tests/UtilsTest.php:332 tests/UtilsTest.php:333
-#: tests/UtilsTest.php:340
+#: application/Utils.php:393
msgid "kiB"
msgstr "ko"
-#: application/Utils.php:389 tests/UtilsTest.php:334 tests/UtilsTest.php:335
-#: tests/UtilsTest.php:351 tests/UtilsTest.php:352
+#: application/Utils.php:393
msgid "MiB"
msgstr "Mo"
-#: application/Utils.php:389 tests/UtilsTest.php:336 tests/UtilsTest.php:337
+#: application/Utils.php:393
msgid "GiB"
msgstr "Go"
-#: application/bookmark/LinkDB.php:128
-msgid "You are not authorized to add a link."
-msgstr "Vous n'êtes pas autorisé à ajouter un lien."
-
-#: application/bookmark/LinkDB.php:131
-msgid "Internal Error: A link should always have an id and URL."
-msgstr "Erreur interne : un lien devrait toujours avoir un ID et une URL."
-
-#: application/bookmark/LinkDB.php:134
-msgid "You must specify an integer as a key."
-msgstr "Vous devez utiliser un entier comme clé."
+#: application/bookmark/BookmarkFileService.php:165
+#: application/bookmark/BookmarkFileService.php:190
+#: application/bookmark/BookmarkFileService.php:215
+#: application/bookmark/BookmarkFileService.php:232
+msgid "You're not authorized to alter the datastore"
+msgstr "Vous n'êtes pas autorisé à modifier les données"
+
+#: application/bookmark/BookmarkFileService.php:168
+#: application/bookmark/BookmarkFileService.php:193
+#: application/bookmark/BookmarkFileService.php:235
+msgid "Provided data is invalid"
+msgstr "Les informations fournies ne sont pas valides"
+
+#: application/bookmark/BookmarkFileService.php:196
+msgid "This bookmarks already exists"
+msgstr "Ce marque-page existe déjà ."
+
+#: application/bookmark/BookmarkInitializer.php:37
+#: application/legacy/LegacyLinkDB.php:266
+msgid "My secret stuff... - Pastebin.com"
+msgstr "Mes trucs secrets... - Pastebin.com"
-#: application/bookmark/LinkDB.php:137
-msgid "Array offset and link ID must be equal."
-msgstr "La clé du tableau et l'ID du lien doivent être identiques."
+#: application/bookmark/BookmarkInitializer.php:39
+#: application/legacy/LegacyLinkDB.php:268
+msgid "Shhhh! I'm a private link only YOU can see. You can delete me too."
+msgstr ""
+"Pssst ! Je suis un lien privé que VOUS êtes le seul à voir. Vous pouvez me "
+"supprimer aussi."
-#: application/bookmark/LinkDB.php:243
+#: application/bookmark/BookmarkInitializer.php:45
+#: application/legacy/LegacyLinkDB.php:246
#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49
#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:15
"Le gestionnaire de marque-pages personnel, minimaliste, et sans base de "
"données"
-#: application/bookmark/LinkDB.php:246
+#: application/bookmark/BookmarkInitializer.php:48
+#: application/legacy/LegacyLinkDB.php:249
msgid ""
"Welcome to Shaarli! This is your first public bookmark. To edit or delete "
"me, you must first login.\n"
"Vous utilisez la version supportée par la communauté du projet original "
"Shaarli de Sébastien Sauvage."
-#: application/bookmark/LinkDB.php:263
-msgid "My secret stuff... - Pastebin.com"
-msgstr "Mes trucs secrets... - Pastebin.com"
-
-#: application/bookmark/LinkDB.php:265
-msgid "Shhhh! I'm a private link only YOU can see. You can delete me too."
-msgstr ""
-"Pssst ! Je suis un lien privé que VOUS êtes le seul à voir. Vous pouvez me "
-"supprimer aussi."
-
-#: application/bookmark/exception/LinkNotFoundException.php:13
+#: application/bookmark/exception/BookmarkNotFoundException.php:13
msgid "The link you are trying to reach does not exist or has been deleted."
msgstr "Le lien que vous essayez de consulter n'existe pas ou a été supprimé."
"Shaarli n'a pas pu créer le fichier de configuration. Merci de vérifier que "
"Shaarli a les droits d'écriture dans le dossier dans lequel il est installé."
-#: application/config/ConfigManager.php:135
-#: application/config/ConfigManager.php:162
+#: application/config/ConfigManager.php:136
+#: application/config/ConfigManager.php:163
msgid "Invalid setting key parameter. String expected, got: "
msgstr "Clé de paramétrage invalide. Chaîne de caractères obtenue, attendu : "
msgid "Error accessing"
msgstr "Une erreur s'est produite en accédant à "
-#: application/feed/Cache.php:16
-#, php-format
-msgid "Cannot purge %s: no directory"
-msgstr "Impossible de purger %s : le répertoire n'existe pas"
-
-#: application/feed/FeedBuilder.php:155
+#: application/feed/FeedBuilder.php:179
msgid "Direct link"
msgstr "Liens directs"
-#: application/feed/FeedBuilder.php:157
+#: application/feed/FeedBuilder.php:181
#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:96
-#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:177
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:179
msgid "Permalink"
msgstr "Permalien"
-#: application/netscape/NetscapeBookmarkUtils.php:42
-msgid "Invalid export selection:"
-msgstr "Sélection d'export invalide :"
+#: application/front/controller/admin/ConfigureController.php:54
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
+msgid "Configure"
+msgstr "Configurer"
-#: application/netscape/NetscapeBookmarkUtils.php:87
-#, php-format
-msgid "File %s (%d bytes) "
-msgstr "Le fichier %s (%d octets) "
+#: application/front/controller/admin/ConfigureController.php:102
+#: application/legacy/LegacyUpdater.php:537
+msgid "You have enabled or changed thumbnails mode."
+msgstr "Vous avez activé ou changé le mode de miniatures."
-#: application/netscape/NetscapeBookmarkUtils.php:89
-msgid "has an unknown file format. Nothing was imported."
-msgstr "a un format inconnu. Rien n'a été importé."
+#: application/front/controller/admin/ConfigureController.php:103
+#: application/legacy/LegacyUpdater.php:538
+msgid "Please synchronize them."
+msgstr "Merci de les synchroniser."
+
+#: application/front/controller/admin/ConfigureController.php:113
+#: application/front/controller/visitor/InstallController.php:136
+msgid "Error while writing config file after configuration update."
+msgstr ""
+"Une erreur s'est produite lors de la sauvegarde du fichier de configuration."
+
+#: application/front/controller/admin/ConfigureController.php:122
+msgid "Configuration was saved."
+msgstr "La configuration a été sauvegardée."
+
+#: application/front/controller/admin/ExportController.php:26
+#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:64
+msgid "Export"
+msgstr "Exporter"
-#: application/netscape/NetscapeBookmarkUtils.php:93
+#: application/front/controller/admin/ExportController.php:42
+msgid "Please select an export mode."
+msgstr "Merci de choisir un mode d'export."
+
+#: application/front/controller/admin/ImportController.php:41
+#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83
+msgid "Import"
+msgstr "Importer"
+
+#: application/front/controller/admin/ImportController.php:55
+msgid "No import file provided."
+msgstr "Aucun fichier à importer n'a été fourni."
+
+#: application/front/controller/admin/ImportController.php:66
#, php-format
msgid ""
-"was successfully processed in %d seconds: %d links imported, %d links "
-"overwritten, %d links skipped."
+"The file you are trying to upload is probably bigger than what this "
+"webserver can accept (%s). Please upload in smaller chunks."
msgstr ""
-"a été importé avec succès en %d secondes : %d liens importés, %d liens "
-"écrasés, %d liens ignorés."
+"Le fichier que vous essayer d'envoyer est probablement plus lourd que ce que "
+"le serveur web peut accepter (%s). Merci de l'envoyer en parties plus "
+"légères."
-#: application/plugin/exception/PluginFileNotFoundException.php:21
+#: application/front/controller/admin/ManageShaareController.php:29
+#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
+msgid "Shaare a new link"
+msgstr "Partager un nouveau lien"
+
+#: application/front/controller/admin/ManageShaareController.php:78
+msgid "Note: "
+msgstr "Note : "
+
+#: application/front/controller/admin/ManageShaareController.php:109
+#: application/front/controller/admin/ManageShaareController.php:206
+#: application/front/controller/admin/ManageShaareController.php:275
+#: application/front/controller/admin/ManageShaareController.php:315
#, php-format
-msgid "Plugin \"%s\" files not found."
-msgstr "Les fichiers de l'extension \"%s\" sont introuvables."
+msgid "Bookmark with identifier %s could not be found."
+msgstr "Le lien avec l'identifiant %s n'a pas pu être trouvé."
-#: application/render/PageBuilder.php:209
-msgid "The page you are trying to reach does not exist or has been deleted."
-msgstr "La page que vous essayez de consulter n'existe pas ou a été supprimée."
+#: application/front/controller/admin/ManageShaareController.php:194
+#: application/front/controller/admin/ManageShaareController.php:252
+msgid "Invalid bookmark ID provided."
+msgstr "ID du lien non valide."
-#: application/render/PageBuilder.php:211
-msgid "404 Not Found"
-msgstr "404 Introuvable"
+#: application/front/controller/admin/ManageShaareController.php:260
+msgid "Invalid visibility provided."
+msgstr "Visibilité du lien non valide."
-#: application/updater/Updater.php:99
-#, fuzzy
-#| msgid "Couldn't retrieve Updater class methods."
-msgid "Couldn't retrieve updater class methods."
-msgstr "Impossible de récupérer les méthodes de la classe Updater."
+#: application/front/controller/admin/ManageShaareController.php:363
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171
+msgid "Edit"
+msgstr "Modifier"
-#: application/updater/Updater.php:526 index.php:1034
-msgid ""
-"You have enabled or changed thumbnails mode. <a href=\"?do=thumbs_update"
-"\">Please synchronize them</a>."
-msgstr ""
-"Vous avez activé ou changé le mode de miniatures. <a href=\"?do=thumbs_update"
-"\">Merci de les synchroniser</a>."
+#: application/front/controller/admin/ManageShaareController.php:366
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:28
+msgid "Shaare"
+msgstr "Shaare"
+
+#: application/front/controller/admin/ManageTagController.php:29
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
+msgid "Manage tags"
+msgstr "Gérer les tags"
+
+#: application/front/controller/admin/ManageTagController.php:48
+msgid "Invalid tags provided."
+msgstr "Les tags fournis ne sont pas valides."
+
+#: application/front/controller/admin/ManageTagController.php:72
+#, php-format
+msgid "The tag was removed from %d bookmark."
+msgid_plural "The tag was removed from %d bookmarks."
+msgstr[0] "Le tag a été supprimé du %d lien."
+msgstr[1] "Le tag a été supprimé de %d liens."
+
+#: application/front/controller/admin/ManageTagController.php:77
+#, php-format
+msgid "The tag was renamed in %d bookmark."
+msgid_plural "The tag was renamed in %d bookmarks."
+msgstr[0] "Le tag a été renommé dans %d lien."
+msgstr[1] "Le tag a été renommé dans %d liens."
-#: application/updater/UpdaterUtils.php:32
-msgid "Updates file path is not set, can't write updates."
+#: application/front/controller/admin/PasswordController.php:28
+#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
+msgid "Change password"
+msgstr "Modifier le mot de passe"
+
+#: application/front/controller/admin/PasswordController.php:55
+msgid "You must provide the current and new password to change it."
msgstr ""
-"Le chemin vers le fichier de mise à jour n'est pas défini, impossible "
-"d'écrire les mises à jour."
+"Vous devez fournir les mots de passe actuel et nouveau pour pouvoir le "
+"modifier."
+
+#: application/front/controller/admin/PasswordController.php:71
+msgid "The old password is not correct."
+msgstr "L'ancien mot de passe est incorrect."
-#: application/updater/UpdaterUtils.php:37
-msgid "Unable to write updates in "
-msgstr "Impossible d'écrire les mises à jour dans "
+#: application/front/controller/admin/PasswordController.php:97
+msgid "Your password has been changed"
+msgstr "Votre mot de passe a été modifié"
-#: application/updater/exception/UpdaterException.php:51
-msgid "An error occurred while running the update "
-msgstr "Une erreur s'est produite lors de l'exécution de la mise à jour "
+#: application/front/controller/admin/PluginsController.php:45
+msgid "Plugin Administration"
+msgstr "Administration des plugins"
-#: index.php:145
-msgid "Shared links on "
-msgstr "Liens partagés sur "
+#: application/front/controller/admin/PluginsController.php:75
+msgid "Setting successfully saved."
+msgstr "Les paramètres ont été sauvegardés avec succès."
-#: index.php:167
-msgid "Insufficient permissions:"
-msgstr "Permissions insuffisantes :"
+#: application/front/controller/admin/PluginsController.php:78
+msgid "Error while saving plugin configuration: "
+msgstr ""
+"Une erreur s'est produite lors de la sauvegarde de la configuration des "
+"plugins : "
-#: index.php:203
-msgid "I said: NO. You are banned for the moment. Go away."
-msgstr "NON. Vous êtes banni pour le moment. Revenez plus tard."
+#: application/front/controller/admin/ThumbnailsController.php:37
+#: tmp/thumbnails.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
+msgid "Thumbnails update"
+msgstr "Mise à jour des miniatures"
-#: index.php:275
-msgid "Wrong login/password."
-msgstr "Nom d'utilisateur ou mot de passe incorrect(s)."
+#: application/front/controller/admin/ToolsController.php:31
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:33
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:33
+msgid "Tools"
+msgstr "Outils"
+
+#: application/front/controller/visitor/BookmarkListController.php:115
+msgid "Search: "
+msgstr "Recherche : "
-#: index.php:398 index.php:404
+#: application/front/controller/visitor/DailyController.php:45
msgid "Today"
msgstr "Aujourd'hui"
-#: index.php:400
+#: application/front/controller/visitor/DailyController.php:47
msgid "Yesterday"
msgstr "Hier"
-#: index.php:484 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46
-#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:46
+#: application/front/controller/visitor/DailyController.php:85
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:48
msgid "Daily"
msgstr "Quotidien"
-#: index.php:593 tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
-#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
-#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:75
-#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:99
-#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:75
-#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:99
+#: application/front/controller/visitor/ErrorController.php:36
+msgid "An unexpected error occurred."
+msgstr "Une erreur inattendue s'est produite."
+
+#: application/front/controller/visitor/InstallController.php:73
+#, php-format
+msgid ""
+"<pre>Sessions do not seem to work correctly on your server.<br>Make sure the "
+"variable \"session.save_path\" is set correctly in your PHP config, and that "
+"you have write access to it.<br>It currently points to %s.<br>On some "
+"browsers, accessing your server via a hostname like 'localhost' or any "
+"custom hostname without a dot causes cookie storage to fail. We recommend "
+"accessing your server via it's IP address or Fully Qualified Domain Name.<br>"
+msgstr ""
+"<pre>Les sesssions ne semblent pas fonctionner sur ce serveur.<br>Assurez "
+"vous que la variable « session.save_path » est correctement définie dans "
+"votre fichier de configuration PHP, et que vous avez les droits d'écriture "
+"dessus.<br>Ce paramètre pointe actuellement sur %s.<br>Sur certains "
+"navigateurs, accéder à votre serveur depuis un nom d'hôte comme « localhost "
+"» ou autre nom personnalisé sans point '.' entraine l'échec de la sauvegarde "
+"des cookies. Nous vous recommandons d'accéder à votre serveur depuis son "
+"adresse IP ou un <em>Fully Qualified Domain Name</em>.<br>"
+
+#: application/front/controller/visitor/InstallController.php:144
+msgid ""
+"Shaarli is now configured. Please login and start shaaring your bookmarks!"
+msgstr ""
+"Shaarli est maintenant configuré. Vous pouvez vous connecter et commencez à "
+"shaare vos liens !"
+
+#: application/front/controller/visitor/InstallController.php:158
+msgid "Insufficient permissions:"
+msgstr "Permissions insuffisantes :"
+
+#: application/front/controller/visitor/LoginController.php:46
+#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
+#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:101
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:77
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:101
msgid "Login"
msgstr "Connexion"
-#: index.php:608 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
-#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:41
+#: application/front/controller/visitor/LoginController.php:78
+msgid "Wrong login/password."
+msgstr "Nom d'utilisateur ou mot de passe incorrect(s)."
+
+#: application/front/controller/visitor/PictureWallController.php:29
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:43
msgid "Picture wall"
msgstr "Mur d'images"
-#: index.php:683 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
-#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:36
-#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
-msgid "Tag cloud"
-msgstr "Nuage de tags"
-
-#: index.php:715 tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
-msgid "Tag list"
+#: application/front/controller/visitor/TagCloudController.php:80
+#, fuzzy
+#| msgid "Tag list"
+msgid "Tag "
msgstr "Liste des tags"
-#: index.php:944 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31
-#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:31
-msgid "Tools"
-msgstr "Outils"
+#: application/front/exceptions/AlreadyInstalledException.php:11
+msgid "Shaarli has already been installed. Login to edit the configuration."
+msgstr ""
+"Shaarli est déjà installé. Connectez-vous pour modifier la configuration."
-#: index.php:952
+#: application/front/exceptions/LoginBannedException.php:11
+msgid ""
+"You have been banned after too many failed login attempts. Try again later."
+msgstr ""
+"Vous avez été banni après trop d'échecs d'authentification. Merci de "
+"réessayer plus tard."
+
+#: application/front/exceptions/OpenShaarliPasswordException.php:16
msgid "You are not supposed to change a password on an Open Shaarli."
msgstr ""
"Vous n'êtes pas censé modifier le mot de passe d'un Shaarli en mode ouvert."
-#: index.php:957 index.php:1007 index.php:1094 index.php:1124 index.php:1234
-#: index.php:1281
+#: application/front/exceptions/ThumbnailsDisabledException.php:11
+msgid "Picture wall unavailable (thumbnails are disabled)."
+msgstr ""
+"Le mur d'images n'est pas disponible (les miniatures sont désactivées)."
+
+#: application/front/exceptions/WrongTokenException.php:16
msgid "Wrong token."
msgstr "Jeton invalide."
-#: index.php:966
-msgid "The old password is not correct."
-msgstr "L'ancien mot de passe est incorrect."
+#: application/legacy/LegacyLinkDB.php:131
+msgid "You are not authorized to add a link."
+msgstr "Vous n'êtes pas autorisé à ajouter un lien."
-#: index.php:993
-msgid "Your password has been changed"
-msgstr "Votre mot de passe a été modifié"
+#: application/legacy/LegacyLinkDB.php:134
+msgid "Internal Error: A link should always have an id and URL."
+msgstr "Erreur interne : un lien devrait toujours avoir un ID et une URL."
-#: index.php:997
-#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
-msgid "Change password"
-msgstr "Modifier le mot de passe"
+#: application/legacy/LegacyLinkDB.php:137
+msgid "You must specify an integer as a key."
+msgstr "Vous devez utiliser un entier comme clé."
-#: index.php:1054
-msgid "Configuration was saved."
-msgstr "La configuration a été sauvegardée."
+#: application/legacy/LegacyLinkDB.php:140
+msgid "Array offset and link ID must be equal."
+msgstr "La clé du tableau et l'ID du lien doivent être identiques."
-#: index.php:1078 tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
-msgid "Configure"
-msgstr "Configurer"
+#: application/legacy/LegacyUpdater.php:104
+msgid "Couldn't retrieve updater class methods."
+msgstr "Impossible de récupérer les méthodes de la classe Updater."
-#: index.php:1088 tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
-msgid "Manage tags"
-msgstr "Gérer les tags"
+#: application/legacy/LegacyUpdater.php:538
+msgid "<a href=\"./admin/thumbnails\">"
+msgstr "<a href=\"./admin/thumbnails\">"
-#: index.php:1107
-#, php-format
-msgid "The tag was removed from %d link."
-msgid_plural "The tag was removed from %d links."
-msgstr[0] "Le tag a été supprimé de %d lien."
-msgstr[1] "Le tag a été supprimé de %d liens."
+#: application/netscape/NetscapeBookmarkUtils.php:63
+msgid "Invalid export selection:"
+msgstr "Sélection d'export invalide :"
-#: index.php:1108
+#: application/netscape/NetscapeBookmarkUtils.php:215
#, php-format
-msgid "The tag was renamed in %d link."
-msgid_plural "The tag was renamed in %d links."
-msgstr[0] "Le tag a été renommé dans %d lien."
-msgstr[1] "Le tag a été renommé dans %d liens."
-
-#: index.php:1115 tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
-msgid "Shaare a new link"
-msgstr "Partager un nouveau lien"
-
-#: index.php:1344 tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169
-msgid "Edit"
-msgstr "Modifier"
-
-#: index.php:1344 index.php:1416
-#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
-#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:26
-msgid "Shaare"
-msgstr "Shaare"
-
-#: index.php:1385
-msgid "Note: "
-msgstr "Note : "
-
-#: index.php:1424
-msgid "Invalid link ID provided"
-msgstr "ID du lien non valide"
-
-#: index.php:1444 tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:65
-msgid "Export"
-msgstr "Exporter"
+msgid "File %s (%d bytes) "
+msgstr "Le fichier %s (%d octets) "
-#: index.php:1506 tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83
-msgid "Import"
-msgstr "Importer"
+#: application/netscape/NetscapeBookmarkUtils.php:217
+msgid "has an unknown file format. Nothing was imported."
+msgstr "a un format inconnu. Rien n'a été importé."
-#: index.php:1516
+#: application/netscape/NetscapeBookmarkUtils.php:221
#, php-format
msgid ""
-"The file you are trying to upload is probably bigger than what this "
-"webserver can accept (%s). Please upload in smaller chunks."
+"was successfully processed in %d seconds: %d bookmarks imported, %d "
+"bookmarks overwritten, %d bookmarks skipped."
msgstr ""
-"Le fichier que vous essayer d'envoyer est probablement plus lourd que ce que "
-"le serveur web peut accepter (%s). Merci de l'envoyer en parties plus "
-"légères."
-
-#: index.php:1561 tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22
-msgid "Plugin administration"
-msgstr "Administration des plugins"
+"a été importé avec succès en %d secondes : %d liens importés, %d liens "
+"écrasés, %d liens ignorés."
-#: index.php:1616 tmp/thumbnails.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
-msgid "Thumbnails update"
-msgstr "Mise à jour des miniatures"
+#: application/plugin/PluginManager.php:122
+msgid " [plugin incompatibility]: "
+msgstr " [incompatibilité de l'extension] : "
-#: index.php:1782
-msgid "Search: "
-msgstr "Recherche : "
+#: application/plugin/exception/PluginFileNotFoundException.php:21
+#, php-format
+msgid "Plugin \"%s\" files not found."
+msgstr "Les fichiers de l'extension \"%s\" sont introuvables."
-#: index.php:1825
+#: application/render/PageCacheManager.php:32
#, php-format
-msgid ""
-"<pre>Sessions do not seem to work correctly on your server.<br>Make sure the "
-"variable \"session.save_path\" is set correctly in your PHP config, and that "
-"you have write access to it.<br>It currently points to %s.<br>On some "
-"browsers, accessing your server via a hostname like 'localhost' or any "
-"custom hostname without a dot causes cookie storage to fail. We recommend "
-"accessing your server via it's IP address or Fully Qualified Domain Name.<br>"
-msgstr ""
-"<pre>Les sesssions ne semblent pas fonctionner sur ce serveur.<br>Assurez "
-"vous que la variable « session.save_path » est correctement définie dans "
-"votre fichier de configuration PHP, et que vous avez les droits d'écriture "
-"dessus.<br>Ce paramètre pointe actuellement sur %s.<br>Sur certains "
-"navigateurs, accéder à votre serveur depuis un nom d'hôte comme « localhost "
-"» ou autre nom personnalisé sans point '.' entraine l'échec de la sauvegarde "
-"des cookies. Nous vous recommandons d'accéder à votre serveur depuis son "
-"adresse IP ou un <em>Fully Qualified Domain Name</em>.<br>"
+msgid "Cannot purge %s: no directory"
+msgstr "Impossible de purger %s : le répertoire n'existe pas"
-#: index.php:1835
-msgid "Click to try again."
-msgstr "Cliquer ici pour réessayer."
+#: application/updater/exception/UpdaterException.php:51
+msgid "An error occurred while running the update "
+msgstr "Une erreur s'est produite lors de l'exécution de la mise à jour "
+
+#: index.php:62
+msgid "Shared bookmarks on "
+msgstr "Liens partagés sur "
#: plugins/addlink_toolbar/addlink_toolbar.php:31
msgid "URI"
msgid "Adds the addlink input on the linklist page."
msgstr "Ajoute le formulaire d'ajout de liens sur la page principale."
-#: plugins/archiveorg/archiveorg.php:25
+#: plugins/archiveorg/archiveorg.php:26
msgid "View on archive.org"
msgstr "Voir sur archive.org"
-#: plugins/archiveorg/archiveorg.php:38
+#: plugins/archiveorg/archiveorg.php:39
msgid "For each link, add an Archive.org icon."
msgstr "Pour chaque lien, ajoute une icône pour Archive.org."
msgid "Dark main color (e.g. visited links)"
msgstr "Couleur principale sombre (ex : les liens visités)"
-#: plugins/demo_plugin/demo_plugin.php:482
+#: plugins/demo_plugin/demo_plugin.php:477
msgid ""
"A demo plugin covering all use cases for template designers and plugin "
"developers."
"Une extension de démonstration couvrant tous les cas d'utilisation pour les "
"designers de thèmes et les développeurs d'extensions."
-#: plugins/demo_plugin/demo_plugin.php:483
+#: plugins/demo_plugin/demo_plugin.php:478
msgid "This is a parameter dedicated to the demo plugin. It'll be suffixed."
msgstr "Ceci est un paramètre dédié au plugin de démo. Il sera suffixé."
-#: plugins/demo_plugin/demo_plugin.php:484
+#: plugins/demo_plugin/demo_plugin.php:479
msgid "Other demo parameter"
msgstr "Un autre paramètre de démo"
msgid "Isso server URL (without 'http://')"
msgstr "URL du serveur Isso (sans 'http://')"
-#: plugins/markdown/markdown.php:163
-msgid "Description will be rendered with"
-msgstr "La description sera générée avec"
-
-#: plugins/markdown/markdown.php:164
-msgid "Markdown syntax documentation"
-msgstr "Documentation sur la syntaxe Markdown"
-
-#: plugins/markdown/markdown.php:165
-msgid "Markdown syntax"
-msgstr "la syntaxe Markdown"
-
-#: plugins/markdown/markdown.php:361
-msgid ""
-"Render shaare description with Markdown syntax.<br><strong>Warning</"
-"strong>:\n"
-"If your shaared descriptions contained HTML tags before enabling the "
-"markdown plugin,\n"
-"enabling it might break your page.\n"
-"See the <a href=\"https://github.com/shaarli/Shaarli/tree/master/plugins/"
-"markdown#html-rendering\">README</a>."
-msgstr ""
-"Utilise la syntaxe Markdown pour la description des liens."
-"<br><strong>Attention</strong> :\n"
-"Si vous aviez des descriptions contenant du HTML avant d'activer cette "
-"extension,\n"
-"l'activer pourrait déformer vos pages.\n"
-"Voir le <a href=\"https://github.com/shaarli/Shaarli/tree/master/plugins/"
-"markdown#html-rendering\">README</a>."
-
#: plugins/piwik/piwik.php:23
msgid ""
"Piwik plugin error: Please define PIWIK_URL and PIWIK_SITEID in the plugin "
msgid "Enable PubSubHubbub feed publishing."
msgstr "Active la publication de flux vers PubSubHubbub."
-#: plugins/qrcode/qrcode.php:72 plugins/wallabag/wallabag.php:68
+#: plugins/qrcode/qrcode.php:73 plugins/wallabag/wallabag.php:70
msgid "For each link, add a QRCode icon."
msgstr "Pour chaque lien, ajouter une icône de QRCode."
msgid "Save to wallabag"
msgstr "Sauvegarder dans Wallabag"
-#: plugins/wallabag/wallabag.php:69
+#: plugins/wallabag/wallabag.php:71
msgid "Wallabag API URL"
msgstr "URL de l'API Wallabag"
-#: plugins/wallabag/wallabag.php:70
+#: plugins/wallabag/wallabag.php:72
msgid "Wallabag API version (1 or 2)"
msgstr "Version de l'API Wallabag (1 ou 2)"
-#: tests/LanguagesTest.php:214 tests/LanguagesTest.php:227
-#: tests/languages/fr/LanguagesFrTest.php:159
-#: tests/languages/fr/LanguagesFrTest.php:172
-#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:85
-#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:85
-msgid "Search"
-msgid_plural "Search"
-msgstr[0] "Rechercher"
-msgstr[1] "Rechercher"
-
#: tmp/404.b91ef64efc3688266305ea9b42e5017e.rtpl.php:12
msgid "Sorry, nothing to see here."
msgstr "Désolé, il y a rien à voir ici."
msgstr "Renommer"
#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
-#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77
-#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171
-#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:145
-#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:145
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:93
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:173
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:147
+#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:67
msgid "Delete"
msgstr "Supprimer"
msgid "tag list"
msgstr "liste des tags"
-#: tmp/configure.90100d2eaf5d3705e14b9b4f78ecddc9.rtpl.php:143
-#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:312
-#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31
-msgid "All"
-msgstr "Tous"
-
-#: tmp/configure.90100d2eaf5d3705e14b9b4f78ecddc9.rtpl.php:147
-#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:316
-msgid "Only common media hosts"
-msgstr "Seulement les hébergeurs de média connus"
-
-#: tmp/configure.90100d2eaf5d3705e14b9b4f78ecddc9.rtpl.php:151
-#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:320
-msgid "None"
-msgstr "Aucune"
-
-#: tmp/configure.90100d2eaf5d3705e14b9b4f78ecddc9.rtpl.php:158
-#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:297
-msgid "You need to enable the extension <code>php-gd</code> to use thumbnails."
-msgstr ""
-"Vous devez activer l'extension <code>php-gd</code> pour utiliser les "
-"miniatures."
-
-#: tmp/configure.90100d2eaf5d3705e14b9b4f78ecddc9.rtpl.php:162
-msgid "Synchonize thumbnails"
-msgstr "Synchroniser les miniatures"
-
#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
msgid "title"
msgstr "titre"
msgid "Theme"
msgstr "Thème"
-#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:87
-#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:78
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:85
+msgid "Description formatter"
+msgstr "Format des descriptions"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:114
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77
msgid "Language"
msgstr "Langue"
-#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:116
-#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:143
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:101
msgid "Timezone"
msgstr "Fuseau horaire"
-#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117
-#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:144
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
msgid "Continent"
msgstr "Continent"
-#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117
-#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:144
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
msgid "City"
msgstr "Ville"
-#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:164
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:191
msgid "Disable session cookie hijacking protection"
msgstr "Désactiver la protection contre le détournement de cookies"
-#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:166
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:193
msgid "Check this if you get disconnected or if your IP address changes often"
msgstr ""
"Cocher cette case si vous êtes souvent déconnecté ou si votre adresse IP "
"change souvent"
-#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:183
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:210
msgid "Private links by default"
msgstr "Liens privés par défaut"
-#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:184
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:211
msgid "All new links are private by default"
msgstr "Tous les nouveaux liens sont privés par défaut"
-#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:226
msgid "RSS direct links"
msgstr "Liens directs dans le flux RSS"
-#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:200
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:227
msgid "Check this to use direct URL instead of permalink in feeds"
msgstr ""
"Cocher cette case pour utiliser des liens directs au lieu des permaliens "
"dans le flux RSS"
-#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:215
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:242
msgid "Hide public links"
msgstr "Cacher les liens publics"
-#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:216
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:243
msgid "Do not show any links if the user is not logged in"
msgstr "N'afficher aucun lien sans être connecté"
-#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:231
-#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:150
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:258
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:149
msgid "Check updates"
msgstr "Vérifier les mises à jour"
-#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:232
-#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:152
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:259
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:151
msgid "Notify me when a new release is ready"
msgstr "Me notifier lorsqu'une nouvelle version est disponible"
-#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:247
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:274
msgid "Automatically retrieve description for new bookmarks"
msgstr "Récupérer automatiquement la description"
-#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:248
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:275
msgid "Shaarli will try to retrieve the description from meta HTML headers"
msgstr ""
"Shaarli essaiera de récupérer la description depuis les balises HTML meta "
"dans les entêtes"
-#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:263
-#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:290
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:168
msgid "Enable REST API"
msgstr "Activer l'API REST"
-#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:264
-#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:170
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:291
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169
msgid "Allow third party software to use Shaarli such as mobile application"
msgstr ""
"Permet aux applications tierces d'utiliser Shaarli, par exemple les "
"applications mobiles"
-#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:279
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:306
msgid "API secret"
msgstr "Clé d'API secrète"
-#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:293
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:320
msgid "Enable thumbnails"
msgstr "Activer les miniatures"
-#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:301
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:324
+msgid "You need to enable the extension <code>php-gd</code> to use thumbnails."
+msgstr ""
+"Vous devez activer l'extension <code>php-gd</code> pour utiliser les "
+"miniatures."
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:328
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:56
msgid "Synchronize thumbnails"
msgstr "Synchroniser les miniatures"
-#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:328
-#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:339
+#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30
+msgid "All"
+msgstr "Tous"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:343
+msgid "Only common media hosts"
+msgstr "Seulement les hébergeurs de média connus"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:347
+msgid "None"
+msgstr "Aucune"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:355
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:88
#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139
#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199
msgid "Save"
msgid "Next day"
msgstr "Jour suivant"
-#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18
msgid "Edit Shaare"
msgstr "Modifier le Shaare"
-#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18
msgid "New Shaare"
msgstr "Nouveau Shaare"
-#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
msgid "Created:"
msgstr "Création :"
-#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
msgid "URL"
msgstr "URL"
-#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:32
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
msgid "Title"
msgstr "Titre"
-#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:38
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:75
#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:99
msgid "Description"
msgstr "Description"
-#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47
msgid "Tags"
msgstr "Tags"
-#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:57
-#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
-#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:167
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:60
+#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169
msgid "Private"
msgstr "Privé"
-#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:66
+msgid "Description will be rendered with"
+msgstr "La description sera générée avec"
+
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:68
+msgid "Markdown syntax documentation"
+msgstr "Documentation sur la syntaxe Markdown"
+
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:69
+msgid "Markdown syntax"
+msgstr "la syntaxe Markdown"
+
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:88
msgid "Apply Changes"
msgstr "Appliquer les changements"
msgid "Export Database"
msgstr "Exporter les données"
-#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
+#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
msgid "Selection"
msgstr "Choisir"
-#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
+#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40
msgid "Public"
msgstr "Publics"
-#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:52
+#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:51
msgid "Prepend note permalinks with this Shaarli instance's URL"
msgstr "Préfixer les liens de note avec l'URL de l'instance de Shaarli"
-#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:53
+#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:52
msgid "Useful to import bookmarks in a web browser"
msgstr "Utile pour importer les marques-pages dans un navigateur"
"Il semblerait que ça soit la première fois que vous lancez Shaarli. Merci de "
"le configurer."
-#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:33
-#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30
-#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:165
-#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:165
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:32
+#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:167
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:167
msgid "Username"
msgstr "Nom d'utilisateur"
-#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
-#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34
-#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:166
-#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:166
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47
+#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:20
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:168
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:168
msgid "Password"
msgstr "Mot de passe"
-#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:63
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:62
msgid "Shaarli title"
msgstr "Titre du Shaarli"
-#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:69
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:68
msgid "My links"
msgstr "Mes liens"
-#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:182
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:181
msgid "Install"
msgstr "Installer"
msgstr[1] "liens privés"
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30
-#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:121
-#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:121
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:123
msgid "Search text"
msgstr "Recherche texte"
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:37
-#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:128
-#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:128
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:130
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:130
#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
-#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:64
+#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:65
#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74
msgid "Filter by tag"
msgstr "Filtrer par tag"
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:87
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:87
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:139
+#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46
+#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:45
+msgid "Search"
+msgstr "Rechercher"
+
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:110
msgid "Nothing found."
msgstr "Aucun résultat."
msgstr "taggé"
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:133
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134
msgid "Remove tag"
msgstr "Retirer le tag"
-#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:142
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:144
msgid "with status"
msgstr "avec le statut"
-#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:153
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:155
msgid "without any tag"
msgstr "sans tag"
-#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:173
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:175
#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43
#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:43
msgid "Fold"
msgstr "Replier"
-#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:175
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:177
msgid "Edited: "
msgstr "Modifié : "
-#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:179
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:181
msgid "permalink"
msgstr "permalien"
-#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:181
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:183
msgid "Add tag"
msgstr "Ajouter un tag"
-#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:183
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:185
msgid "Toggle sticky"
msgstr "Changer statut épinglé"
-#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:185
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:187
msgid "Sticky"
msgstr "Épinglé"
msgid "Links per page"
msgstr "Liens par page"
-#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
-msgid ""
-"You have been banned after too many failed login attempts. Try again later."
-msgstr ""
-"Vous avez été banni après trop d'échecs d'authentification. Merci de "
-"réessayer plus tard."
-
-#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
-#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169
-#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:169
+#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:171
msgid "Remember me"
msgstr "Rester connecté"
msgid "Are you sure you want to delete this link?"
msgstr "Êtes-vous sûr de vouloir supprimer ce lien ?"
-#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:65
-#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:90
-#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:65
-#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:90
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:11
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:11
+msgid "Menu"
+msgstr "Menu"
+
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:38
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:38
+#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
+msgid "Tag cloud"
+msgstr "Nuage de tags"
+
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:67
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:92
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:67
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:92
msgid "RSS Feed"
msgstr "Flux RSS"
-#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:70
-#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:106
-#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:70
-#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:106
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:108
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:72
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:108
msgid "Logout"
msgstr "Déconnexion"
-#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:150
-#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:150
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:152
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:152
msgid "Set public"
msgstr "Rendre public"
-#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:155
-#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:155
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:157
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:157
msgid "Set private"
msgstr "Rendre privé"
-#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:187
-#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:187
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:189
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:189
msgid "is available"
msgstr "est disponible"
-#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:194
-#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:194
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:196
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:196
msgid "Error"
msgstr "Erreur"
-#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
-msgid "Picture wall unavailable (thumbnails are disabled)."
-msgstr ""
-"Le mur d'images n'est pas disponible (les miniatures sont désactivées)."
+#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
+msgid "There is no cached thumbnail."
+msgstr "Il n'y a aucune miniature dans le cache."
-#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
-#, fuzzy
-#| msgid ""
-#| "You don't have any cached thumbnail. Try to <a href=\"?do=thumbs_update"
-#| "\">synchronize them</a>."
-msgid ""
-"There is no cached thumbnail. Try to <a href=\"?do=thumbs_update"
-"\">synchronize them</a>."
-msgstr ""
-"Il n'y a aucune miniature en cache. Essayer de <a href=\"?do=thumbs_update"
-"\">les synchroniser</a>."
+#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17
+msgid "Try to synchronize them."
+msgstr "Essayer de les synchroniser."
-#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
+#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
msgid "Picture Wall"
msgstr "Mur d'images"
-#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
+#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
msgid "pics"
msgstr "images"
msgstr ""
"Vous devez activer Javascript pour pouvoir modifier l'ordre des extensions."
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22
+msgid "Plugin administration"
+msgstr "Administration des plugins"
+
#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
msgid "Enabled Plugins"
msgstr "Extensions activées"
msgid "List all links with those tags"
msgstr "Lister tous les liens avec ces tags"
+#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
+msgid "Tag list"
+msgstr "Liste des tags"
+
+#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:68
+msgid "Rename tag"
+msgstr "Renommer le tag"
+
#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:3
#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:3
msgid "Sort by:"
"Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « "
"Ajouter aux favoris »"
+#, fuzzy
+#~| msgid "Selection"
+#~ msgid ".ui-selecting"
+#~ msgstr "Choisir"
+
+#, fuzzy
+#~| msgid "Documentation"
+#~ msgid "document"
+#~ msgstr "Documentation"
+
+#~ msgid "The page you are trying to reach does not exist or has been deleted."
+#~ msgstr ""
+#~ "La page que vous essayez de consulter n'existe pas ou a été supprimée."
+
+#~ msgid "404 Not Found"
+#~ msgstr "404 Introuvable"
+
+#~ msgid "Updates file path is not set, can't write updates."
+#~ msgstr ""
+#~ "Le chemin vers le fichier de mise à jour n'est pas défini, impossible "
+#~ "d'écrire les mises à jour."
+
+#~ msgid "Unable to write updates in "
+#~ msgstr "Impossible d'écrire les mises à jour dans "
+
+#~ msgid "I said: NO. You are banned for the moment. Go away."
+#~ msgstr "NON. Vous êtes banni pour le moment. Revenez plus tard."
+
+#~ msgid "Click to try again."
+#~ msgstr "Cliquer ici pour réessayer."
+
+#~ msgid ""
+#~ "Render shaare description with Markdown syntax.<br><strong>Warning</"
+#~ "strong>:\n"
+#~ "If your shaared descriptions contained HTML tags before enabling the "
+#~ "markdown plugin,\n"
+#~ "enabling it might break your page.\n"
+#~ "See the <a href=\"https://github.com/shaarli/Shaarli/tree/master/plugins/"
+#~ "markdown#html-rendering\">README</a>."
+#~ msgstr ""
+#~ "Utilise la syntaxe Markdown pour la description des liens."
+#~ "<br><strong>Attention</strong> :\n"
+#~ "Si vous aviez des descriptions contenant du HTML avant d'activer cette "
+#~ "extension,\n"
+#~ "l'activer pourrait déformer vos pages.\n"
+#~ "Voir le <a href=\"https://github.com/shaarli/Shaarli/tree/master/plugins/"
+#~ "markdown#html-rendering\">README</a>."
+
+#~ msgid "Synchonize thumbnails"
+#~ msgstr "Synchroniser les miniatures"
+
+#, fuzzy
+#~| msgid ""
+#~| "You don't have any cached thumbnail. Try to <a href=\"?do=thumbs_update"
+#~| "\">synchronize them</a>."
+#~ msgid ""
+#~ "There is no cached thumbnail. Try to <a href=\"?do=thumbs_update"
+#~ "\">synchronize them</a>."
+#~ msgstr ""
+#~ "Il n'y a aucune miniature en cache. Essayer de <a href=\"?do=thumbs_update"
+#~ "\">les synchroniser</a>."
+
#~ msgid ""
#~ "You need to browse your Shaarli over <strong>HTTPS</strong> to use this "
#~ "functionality."
* Licence: http://www.opensource.org/licenses/zlib-license.php
*/
-// Set 'UTC' as the default timezone if it is not defined in php.ini
-// See http://php.net/manual/en/datetime.configuration.php#ini.date.timezone
-if (date_default_timezone_get() == '') {
- date_default_timezone_set('UTC');
-}
-
-/*
- * PHP configuration
- */
-
-// http://server.com/x/shaarli --> /shaarli/
-define('WEB_PATH', substr($_SERVER['REQUEST_URI'], 0, 1+strrpos($_SERVER['REQUEST_URI'], '/', 0)));
-
-// High execution time in case of problematic imports/exports.
-ini_set('max_input_time', '60');
-
-// Try to set max upload file size and read
-ini_set('memory_limit', '128M');
-ini_set('post_max_size', '16M');
-ini_set('upload_max_filesize', '16M');
-
-// See all error except warnings
-error_reporting(E_ALL^E_WARNING);
-
-// 3rd-party libraries
-if (! file_exists(__DIR__ . '/vendor/autoload.php')) {
- header('Content-Type: text/plain; charset=utf-8');
- echo "Error: missing Composer configuration\n\n"
- ."If you installed Shaarli through Git or using the development branch,\n"
- ."please refer to the installation documentation to install PHP"
- ." dependencies using Composer:\n"
- ."- https://shaarli.readthedocs.io/en/master/Server-configuration/\n"
- ."- https://shaarli.readthedocs.io/en/master/Download-and-Installation/";
- exit;
-}
require_once 'inc/rain.tpl.class.php';
require_once __DIR__ . '/vendor/autoload.php';
// Shaarli library
require_once 'application/bookmark/LinkUtils.php';
require_once 'application/config/ConfigPlugin.php';
-require_once 'application/feed/Cache.php';
require_once 'application/http/HttpUtils.php';
require_once 'application/http/UrlUtils.php';
-require_once 'application/updater/UpdaterUtils.php';
-require_once 'application/FileUtils.php';
require_once 'application/TimeZone.php';
require_once 'application/Utils.php';
-use Shaarli\ApplicationUtils;
-use Shaarli\Bookmark\Bookmark;
-use Shaarli\Bookmark\BookmarkFileService;
-use Shaarli\Bookmark\BookmarkFilter;
-use Shaarli\Bookmark\BookmarkServiceInterface;
-use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
+require_once __DIR__ . '/init.php';
+
use Shaarli\Config\ConfigManager;
use Shaarli\Container\ContainerBuilder;
-use Shaarli\Feed\CachedPage;
-use Shaarli\Feed\FeedBuilder;
-use Shaarli\Formatter\BookmarkMarkdownFormatter;
-use Shaarli\Formatter\FormatterFactory;
-use Shaarli\History;
use Shaarli\Languages;
-use Shaarli\Netscape\NetscapeBookmarkUtils;
-use Shaarli\Plugin\PluginManager;
-use Shaarli\Render\PageBuilder;
-use Shaarli\Render\ThemeUtils;
-use Shaarli\Router;
+use Shaarli\Security\CookieManager;
use Shaarli\Security\LoginManager;
use Shaarli\Security\SessionManager;
-use Shaarli\Thumbnailer;
-use Shaarli\Updater\Updater;
-use Shaarli\Updater\UpdaterUtils;
use Slim\App;
-// Ensure the PHP version is supported
-try {
- ApplicationUtils::checkPHPVersion('7.1', PHP_VERSION);
-} catch (Exception $exc) {
- header('Content-Type: text/plain; charset=utf-8');
- echo $exc->getMessage();
- exit;
-}
-
-define('SHAARLI_VERSION', ApplicationUtils::getVersion(__DIR__ .'/'. ApplicationUtils::$VERSION_FILE));
-
-// Force cookie path (but do not change lifetime)
-$cookie = session_get_cookie_params();
-$cookiedir = '';
-if (dirname($_SERVER['SCRIPT_NAME']) != '/') {
- $cookiedir = dirname($_SERVER["SCRIPT_NAME"]).'/';
-}
-// Set default cookie expiration and path.
-session_set_cookie_params($cookie['lifetime'], $cookiedir, $_SERVER['SERVER_NAME']);
-// Set session parameters on server side.
-// Use cookies to store session.
-ini_set('session.use_cookies', 1);
-// Force cookies for session (phpsessionID forbidden in URL).
-ini_set('session.use_only_cookies', 1);
-// Prevent PHP form using sessionID in URL if cookies are disabled.
-ini_set('session.use_trans_sid', false);
-
-session_name('shaarli');
-// Start session if needed (Some server auto-start sessions).
-if (session_status() == PHP_SESSION_NONE) {
- session_start();
-}
-
-// Regenerate session ID if invalid or not defined in cookie.
-if (isset($_COOKIE['shaarli']) && !SessionManager::checkId($_COOKIE['shaarli'])) {
- session_regenerate_id(true);
- $_COOKIE['shaarli'] = session_id();
-}
-
$conf = new ConfigManager();
// In dev mode, throw exception on any warning
// See all errors (for debugging only)
error_reporting(-1);
- set_error_handler(function($errno, $errstr, $errfile, $errline, array $errcontext) {
+ set_error_handler(function ($errno, $errstr, $errfile, $errline, array $errcontext) {
throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
});
}
-$sessionManager = new SessionManager($_SESSION, $conf);
-$loginManager = new LoginManager($conf, $sessionManager);
+$sessionManager = new SessionManager($_SESSION, $conf, session_save_path());
+$sessionManager->initialize();
+$cookieManager = new CookieManager($_COOKIE);
+$loginManager = new LoginManager($conf, $sessionManager, $cookieManager);
$loginManager->generateStaySignedInToken($_SERVER['REMOTE_ADDR']);
-$clientIpId = client_ip_id($_SERVER);
-
-// LC_MESSAGES isn't defined without php-intl, in this case use LC_COLLATE locale instead.
-if (! defined('LC_MESSAGES')) {
- define('LC_MESSAGES', LC_COLLATE);
-}
// Sniff browser language and set date format accordingly.
if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
$conf->setEmpty('general.timezone', date_default_timezone_get());
$conf->setEmpty('general.title', t('Shared bookmarks on '). escape(index_url($_SERVER)));
+
RainTPL::$tpl_dir = $conf->get('resource.raintpl_tpl').'/'.$conf->get('resource.theme').'/'; // template directory
RainTPL::$cache_dir = $conf->get('resource.raintpl_tmp'); // cache directory
-$pluginManager = new PluginManager($conf);
-$pluginManager->load($conf->get('general.enabled_plugins'));
-
date_default_timezone_set($conf->get('general.timezone', 'UTC'));
-ob_start(); // Output buffering for the page cache.
-
-// Prevent caching on client side or proxy: (yes, it's ugly)
-header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
-header("Cache-Control: no-store, no-cache, must-revalidate");
-header("Cache-Control: post-check=0, pre-check=0", false);
-header("Pragma: no-cache");
-
-if (! is_file($conf->getConfigFileExt())) {
- // Ensure Shaarli has proper access to its resources
- $errors = ApplicationUtils::checkResourcePermissions($conf);
-
- if ($errors != array()) {
- $message = '<p>'. t('Insufficient permissions:') .'</p><ul>';
-
- foreach ($errors as $error) {
- $message .= '<li>'.$error.'</li>';
- }
- $message .= '</ul>';
-
- header('Content-Type: text/html; charset=utf-8');
- echo $message;
- exit;
- }
-
- // Display the installation form if no existing config is found
- install($conf, $sessionManager, $loginManager);
-}
-
-$loginManager->checkLoginState($_COOKIE, $clientIpId);
-
-/**
- * Adapter function to ensure compatibility with third-party templates
- *
- * @see https://github.com/shaarli/Shaarli/pull/1086
- *
- * @return bool true when the user is logged in, false otherwise
- */
-function isLoggedIn()
-{
- global $loginManager;
- return $loginManager->isLoggedIn();
-}
-
-
-// ------------------------------------------------------------------------------------------
-// Process login form: Check if login/password is correct.
-if (isset($_POST['login'])) {
- if (! $loginManager->canLogin($_SERVER)) {
- die(t('I said: NO. You are banned for the moment. Go away.'));
- }
- if (isset($_POST['password'])
- && $sessionManager->checkToken($_POST['token'])
- && $loginManager->checkCredentials($_SERVER['REMOTE_ADDR'], $clientIpId, $_POST['login'], $_POST['password'])
- ) {
- $loginManager->handleSuccessfulLogin($_SERVER);
-
- $cookiedir = '';
- if (dirname($_SERVER['SCRIPT_NAME']) != '/') {
- // Note: Never forget the trailing slash on the cookie path!
- $cookiedir = dirname($_SERVER["SCRIPT_NAME"]) . '/';
- }
-
- if (!empty($_POST['longlastingsession'])) {
- // Keep the session cookie even after the browser closes
- $sessionManager->setStaySignedIn(true);
- $expirationTime = $sessionManager->extendSession();
-
- setcookie(
- $loginManager::$STAY_SIGNED_IN_COOKIE,
- $loginManager->getStaySignedInToken(),
- $expirationTime,
- WEB_PATH
- );
- } else {
- // Standard session expiration (=when browser closes)
- $expirationTime = 0;
- }
-
- // Send cookie with the new expiration date to the browser
- session_destroy();
- session_set_cookie_params($expirationTime, $cookiedir, $_SERVER['SERVER_NAME']);
- session_start();
- session_regenerate_id(true);
-
- // Optional redirect after login:
- if (isset($_GET['post'])) {
- $uri = './?post='. urlencode($_GET['post']);
- foreach (array('description', 'source', 'title', 'tags') as $param) {
- if (!empty($_GET[$param])) {
- $uri .= '&'.$param.'='.urlencode($_GET[$param]);
- }
- }
- header('Location: '. $uri);
- exit;
- }
-
- if (isset($_GET['edit_link'])) {
- header('Location: ./?edit_link='. escape($_GET['edit_link']));
- exit;
- }
-
- if (isset($_POST['returnurl'])) {
- // Prevent loops over login screen.
- if (strpos($_POST['returnurl'], '/login') === false) {
- header('Location: '. generateLocation($_POST['returnurl'], $_SERVER['HTTP_HOST']));
- exit;
- }
- }
- header('Location: ./?');
- exit;
- } else {
- $loginManager->handleFailedLogin($_SERVER);
- $redir = '?username='. urlencode($_POST['login']);
- if (isset($_GET['post'])) {
- $redir .= '&post=' . urlencode($_GET['post']);
- foreach (array('description', 'source', 'title', 'tags') as $param) {
- if (!empty($_GET[$param])) {
- $redir .= '&' . $param . '=' . urlencode($_GET[$param]);
- }
- }
- }
- // Redirect to login screen.
- echo '<script>alert("'. t("Wrong login/password.") .'");document.location=\'./login'.$redir.'\';</script>';
- exit;
- }
-}
-
-// ------------------------------------------------------------------------------------------
-// Token management for XSRF protection
-// Token should be used in any form which acts on data (create,update,delete,import...).
-if (!isset($_SESSION['tokens'])) {
- $_SESSION['tokens']=array(); // Token are attached to the session.
-}
-
-/**
- * Daily RSS feed: 1 RSS entry per day giving all the bookmarks on that day.
- * Gives the last 7 days (which have bookmarks).
- * This RSS feed cannot be filtered.
- *
- * @param BookmarkServiceInterface $bookmarkService
- * @param ConfigManager $conf Configuration Manager instance
- * @param LoginManager $loginManager LoginManager instance
- */
-function showDailyRSS($bookmarkService, $conf, $loginManager)
-{
- // Cache system
- $query = $_SERVER['QUERY_STRING'];
- $cache = new CachedPage(
- $conf->get('config.PAGE_CACHE'),
- page_url($_SERVER),
- startsWith($query, 'do=dailyrss') && !$loginManager->isLoggedIn()
- );
- $cached = $cache->cachedVersion();
- if (!empty($cached)) {
- echo $cached;
- exit;
- }
-
- /* Some Shaarlies may have very few bookmarks, so we need to look
- back in time until we have enough days ($nb_of_days).
- */
- $nb_of_days = 7; // We take 7 days.
- $today = date('Ymd');
- $days = array();
-
- foreach ($bookmarkService->search() as $bookmark) {
- $day = $bookmark->getCreated()->format('Ymd'); // Extract day (without time)
- if (strcmp($day, $today) < 0) {
- if (empty($days[$day])) {
- $days[$day] = array();
- }
- $days[$day][] = $bookmark;
- }
-
- if (count($days) > $nb_of_days) {
- break; // Have we collected enough days?
- }
- }
-
- // Build the RSS feed.
- header('Content-Type: application/rss+xml; charset=utf-8');
- $pageaddr = escape(index_url($_SERVER));
- echo '<?xml version="1.0" encoding="UTF-8"?><rss version="2.0">';
- echo '<channel>';
- echo '<title>Daily - '. $conf->get('general.title') . '</title>';
- echo '<link>'. $pageaddr .'</link>';
- echo '<description>Daily shared bookmarks</description>';
- echo '<language>en-en</language>';
- echo '<copyright>'. $pageaddr .'</copyright>'. PHP_EOL;
-
- $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
- $formatter = $factory->getFormatter();
- $formatter->addContextData('index_url', index_url($_SERVER));
- // For each day.
- /** @var Bookmark[] $bookmarks */
- foreach ($days as $day => $bookmarks) {
- $formattedBookmarks = [];
- $dayDate = DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, $day.'_000000');
- $absurl = escape(index_url($_SERVER).'?do=daily&day='.$day); // Absolute URL of the corresponding "Daily" page.
-
- // We pre-format some fields for proper output.
- foreach ($bookmarks as $key => $bookmark) {
- $formattedBookmarks[$key] = $formatter->format($bookmark);
- // This page is a bit specific, we need raw description to calculate the length
- $formattedBookmarks[$key]['formatedDescription'] = $formattedBookmarks[$key]['description'];
- $formattedBookmarks[$key]['description'] = $bookmark->getDescription();
-
- if ($bookmark->isNote()) {
- $link['url'] = index_url($_SERVER) . $bookmark->getUrl(); // make permalink URL absolute
- }
- }
-
- // Then build the HTML for this day:
- $tpl = new RainTPL();
- $tpl->assign('title', $conf->get('general.title'));
- $tpl->assign('daydate', $dayDate->getTimestamp());
- $tpl->assign('absurl', $absurl);
- $tpl->assign('links', $formattedBookmarks);
- $tpl->assign('rssdate', escape($dayDate->format(DateTime::RSS)));
- $tpl->assign('hide_timestamps', $conf->get('privacy.hide_timestamps', false));
- $tpl->assign('index_url', $pageaddr);
- $html = $tpl->draw('dailyrss', true);
-
- echo $html . PHP_EOL;
- }
- echo '</channel></rss><!-- Cached version of '. escape(page_url($_SERVER)) .' -->';
-
- $cache->cache(ob_get_contents());
- ob_end_flush();
- exit;
-}
-
-/**
- * Show the 'Daily' page.
- *
- * @param PageBuilder $pageBuilder Template engine wrapper.
- * @param BookmarkServiceInterface $bookmarkService instance.
- * @param ConfigManager $conf Configuration Manager instance.
- * @param PluginManager $pluginManager Plugin Manager instance.
- * @param LoginManager $loginManager Login Manager instance
- */
-function showDaily($pageBuilder, $bookmarkService, $conf, $pluginManager, $loginManager)
-{
- if (isset($_GET['day'])) {
- $day = $_GET['day'];
- if ($day === date('Ymd', strtotime('now'))) {
- $pageBuilder->assign('dayDesc', t('Today'));
- } elseif ($day === date('Ymd', strtotime('-1 days'))) {
- $pageBuilder->assign('dayDesc', t('Yesterday'));
- }
- } else {
- $day = date('Ymd', strtotime('now')); // Today, in format YYYYMMDD.
- $pageBuilder->assign('dayDesc', t('Today'));
- }
-
- $days = $bookmarkService->days();
- $i = array_search($day, $days);
- if ($i === false && count($days)) {
- // no bookmarks for day, but at least one day with bookmarks
- $i = count($days) - 1;
- $day = $days[$i];
- }
- $previousday = '';
- $nextday = '';
-
- if ($i !== false) {
- if ($i >= 1) {
- $previousday = $days[$i - 1];
- }
- if ($i < count($days) - 1) {
- $nextday = $days[$i + 1];
- }
- }
- try {
- $linksToDisplay = $bookmarkService->filterDay($day);
- } catch (Exception $exc) {
- error_log($exc);
- $linksToDisplay = [];
- }
-
- $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
- $formatter = $factory->getFormatter();
- // We pre-format some fields for proper output.
- foreach ($linksToDisplay as $key => $bookmark) {
- $linksToDisplay[$key] = $formatter->format($bookmark);
- // This page is a bit specific, we need raw description to calculate the length
- $linksToDisplay[$key]['formatedDescription'] = $linksToDisplay[$key]['description'];
- $linksToDisplay[$key]['description'] = $bookmark->getDescription();
- }
-
- $dayDate = DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, $day.'_000000');
- $data = array(
- 'pagetitle' => $conf->get('general.title') .' - '. format_date($dayDate, false),
- 'linksToDisplay' => $linksToDisplay,
- 'day' => $dayDate->getTimestamp(),
- 'dayDate' => $dayDate,
- 'previousday' => $previousday,
- 'nextday' => $nextday,
- );
-
- /* Hook is called before column construction so that plugins don't have
- to deal with columns. */
- $pluginManager->executeHooks('render_daily', $data, array('loggedin' => $loginManager->isLoggedIn()));
-
- /* We need to spread the articles on 3 columns.
- I did not want to use a JavaScript lib like http://masonry.desandro.com/
- so I manually spread entries with a simple method: I roughly evaluate the
- height of a div according to title and description length.
- */
- $columns = array(array(), array(), array()); // Entries to display, for each column.
- $fill = array(0, 0, 0); // Rough estimate of columns fill.
- foreach ($data['linksToDisplay'] as $key => $bookmark) {
- // Roughly estimate length of entry (by counting characters)
- // Title: 30 chars = 1 line. 1 line is 30 pixels height.
- // Description: 836 characters gives roughly 342 pixel height.
- // This is not perfect, but it's usually OK.
- $length = strlen($bookmark['title']) + (342 * strlen($bookmark['description'])) / 836;
- if (! empty($bookmark['thumbnail'])) {
- $length += 100; // 1 thumbnails roughly takes 100 pixels height.
- }
- // Then put in column which is the less filled:
- $smallest = min($fill); // find smallest value in array.
- $index = array_search($smallest, $fill); // find index of this smallest value.
- array_push($columns[$index], $bookmark); // Put entry in this column.
- $fill[$index] += $length;
- }
-
- $data['cols'] = $columns;
-
- foreach ($data as $key => $value) {
- $pageBuilder->assign($key, $value);
- }
-
- $pageBuilder->assign('pagetitle', t('Daily') .' - '. $conf->get('general.title', 'Shaarli'));
- $pageBuilder->renderPage('daily');
- exit;
-}
-
-/**
- * Renders the linklist
- *
- * @param pageBuilder $PAGE pageBuilder instance.
- * @param BookmarkServiceInterface $linkDb instance.
- * @param ConfigManager $conf Configuration Manager instance.
- * @param PluginManager $pluginManager Plugin Manager instance.
- */
-function showLinkList($PAGE, $linkDb, $conf, $pluginManager, $loginManager)
-{
- buildLinkList($PAGE, $linkDb, $conf, $pluginManager, $loginManager);
- $PAGE->renderPage('linklist');
-}
-
-/**
- * Render HTML page (according to URL parameters and user rights)
- *
- * @param ConfigManager $conf Configuration Manager instance.
- * @param PluginManager $pluginManager Plugin Manager instance,
- * @param BookmarkServiceInterface $bookmarkService
- * @param History $history instance
- * @param SessionManager $sessionManager SessionManager instance
- * @param LoginManager $loginManager LoginManager instance
- */
-function renderPage($conf, $pluginManager, $bookmarkService, $history, $sessionManager, $loginManager)
-{
- $updater = new Updater(
- UpdaterUtils::read_updates_file($conf->get('resource.updates')),
- $bookmarkService,
- $conf,
- $loginManager->isLoggedIn()
- );
- try {
- $newUpdates = $updater->update();
- if (! empty($newUpdates)) {
- UpdaterUtils::write_updates_file(
- $conf->get('resource.updates'),
- $updater->getDoneUpdates()
- );
- }
- } catch (Exception $e) {
- die($e->getMessage());
- }
-
- $PAGE = new PageBuilder($conf, $_SESSION, $bookmarkService, $sessionManager->generateToken(), $loginManager->isLoggedIn());
- $PAGE->assign('linkcount', $bookmarkService->count(BookmarkFilter::$ALL));
- $PAGE->assign('privateLinkcount', $bookmarkService->count(BookmarkFilter::$PRIVATE));
- $PAGE->assign('plugin_errors', $pluginManager->getErrors());
-
- // Determine which page will be rendered.
- $query = (isset($_SERVER['QUERY_STRING'])) ? $_SERVER['QUERY_STRING'] : '';
- $targetPage = Router::findPage($query, $_GET, $loginManager->isLoggedIn());
-
- if (// if the user isn't logged in
- !$loginManager->isLoggedIn() &&
- // and Shaarli doesn't have public content...
- $conf->get('privacy.hide_public_links') &&
- // and is configured to enforce the login
- $conf->get('privacy.force_login') &&
- // and the current page isn't already the login page
- $targetPage !== Router::$PAGE_LOGIN &&
- // and the user is not requesting a feed (which would lead to a different content-type as expected)
- $targetPage !== Router::$PAGE_FEED_ATOM &&
- $targetPage !== Router::$PAGE_FEED_RSS
- ) {
- // force current page to be the login page
- $targetPage = Router::$PAGE_LOGIN;
- }
-
- // Call plugin hooks for header, footer and includes, specifying which page will be rendered.
- // Then assign generated data to RainTPL.
- $common_hooks = array(
- 'includes',
- 'header',
- 'footer',
- );
-
- foreach ($common_hooks as $name) {
- $plugin_data = array();
- $pluginManager->executeHooks(
- 'render_' . $name,
- $plugin_data,
- array(
- 'target' => $targetPage,
- 'loggedin' => $loginManager->isLoggedIn()
- )
- );
- $PAGE->assign('plugins_' . $name, $plugin_data);
- }
-
- // -------- Display login form.
- if ($targetPage == Router::$PAGE_LOGIN) {
- header('Location: ./login');
- exit;
- }
- // -------- User wants to logout.
- if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=logout')) {
- invalidateCaches($conf->get('resource.page_cache'));
- $sessionManager->logout();
- setcookie(LoginManager::$STAY_SIGNED_IN_COOKIE, 'false', 0, WEB_PATH);
- header('Location: ?');
- exit;
- }
-
- // -------- Picture wall
- if ($targetPage == Router::$PAGE_PICWALL) {
- $PAGE->assign('pagetitle', t('Picture wall') .' - '. $conf->get('general.title', 'Shaarli'));
- if (! $conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) === Thumbnailer::MODE_NONE) {
- $PAGE->assign('linksToDisplay', []);
- $PAGE->renderPage('picwall');
- exit;
- }
-
- // Optionally filter the results:
- $links = $bookmarkService->search($_GET);
- $linksToDisplay = [];
-
- // Get only bookmarks which have a thumbnail.
- // Note: we do not retrieve thumbnails here, the request is too heavy.
- $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
- $formatter = $factory->getFormatter();
- foreach ($links as $key => $link) {
- if ($link->getThumbnail() !== false) {
- $linksToDisplay[] = $formatter->format($link);
- }
- }
-
- $data = [
- 'linksToDisplay' => $linksToDisplay,
- ];
- $pluginManager->executeHooks('render_picwall', $data, ['loggedin' => $loginManager->isLoggedIn()]);
-
- foreach ($data as $key => $value) {
- $PAGE->assign($key, $value);
- }
-
- $PAGE->renderPage('picwall');
- exit;
- }
-
- // -------- Tag cloud
- if ($targetPage == Router::$PAGE_TAGCLOUD) {
- $visibility = ! empty($_SESSION['visibility']) ? $_SESSION['visibility'] : '';
- $filteringTags = isset($_GET['searchtags']) ? explode(' ', $_GET['searchtags']) : [];
- $tags = $bookmarkService->bookmarksCountPerTag($filteringTags, $visibility);
-
- // We sort tags alphabetically, then choose a font size according to count.
- // First, find max value.
- $maxcount = 0;
- foreach ($tags as $value) {
- $maxcount = max($maxcount, $value);
- }
-
- alphabetical_sort($tags, false, true);
-
- $logMaxCount = $maxcount > 1 ? log($maxcount, 30) : 1;
- $tagList = array();
- foreach ($tags as $key => $value) {
- if (in_array($key, $filteringTags)) {
- continue;
- }
- // Tag font size scaling:
- // default 15 and 30 logarithm bases affect scaling,
- // 2.2 and 0.8 are arbitrary font sizes in em.
- $size = log($value, 15) / $logMaxCount * 2.2 + 0.8;
- $tagList[$key] = array(
- 'count' => $value,
- 'size' => number_format($size, 2, '.', ''),
- );
- }
-
- $searchTags = implode(' ', escape($filteringTags));
- $data = array(
- 'search_tags' => $searchTags,
- 'tags' => $tagList,
- );
- $pluginManager->executeHooks('render_tagcloud', $data, array('loggedin' => $loginManager->isLoggedIn()));
-
- foreach ($data as $key => $value) {
- $PAGE->assign($key, $value);
- }
-
- $searchTags = ! empty($searchTags) ? $searchTags .' - ' : '';
- $PAGE->assign('pagetitle', $searchTags. t('Tag cloud') .' - '. $conf->get('general.title', 'Shaarli'));
- $PAGE->renderPage('tag.cloud');
- exit;
- }
-
- // -------- Tag list
- if ($targetPage == Router::$PAGE_TAGLIST) {
- $visibility = ! empty($_SESSION['visibility']) ? $_SESSION['visibility'] : '';
- $filteringTags = isset($_GET['searchtags']) ? explode(' ', $_GET['searchtags']) : [];
- $tags = $bookmarkService->bookmarksCountPerTag($filteringTags, $visibility);
- foreach ($filteringTags as $tag) {
- if (array_key_exists($tag, $tags)) {
- unset($tags[$tag]);
- }
- }
-
- if (! empty($_GET['sort']) && $_GET['sort'] === 'alpha') {
- alphabetical_sort($tags, false, true);
- }
-
- $searchTags = implode(' ', escape($filteringTags));
- $data = [
- 'search_tags' => $searchTags,
- 'tags' => $tags,
- ];
- $pluginManager->executeHooks('render_taglist', $data, ['loggedin' => $loginManager->isLoggedIn()]);
-
- foreach ($data as $key => $value) {
- $PAGE->assign($key, $value);
- }
-
- $searchTags = ! empty($searchTags) ? $searchTags .' - ' : '';
- $PAGE->assign('pagetitle', $searchTags . t('Tag list') .' - '. $conf->get('general.title', 'Shaarli'));
- $PAGE->renderPage('tag.list');
- exit;
- }
-
- // Daily page.
- if ($targetPage == Router::$PAGE_DAILY) {
- showDaily($PAGE, $bookmarkService, $conf, $pluginManager, $loginManager);
- }
-
- // 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(),
- $feedType,
- $_SERVER,
- $_GET,
- $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();
-
- // 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();
- exit;
- }
-
- // Display opensearch plugin (XML)
- if ($targetPage == Router::$PAGE_OPENSEARCH) {
- header('Content-Type: application/xml; charset=utf-8');
- $PAGE->assign('serverurl', index_url($_SERVER));
- $PAGE->renderPage('opensearch');
- exit;
- }
-
- // -------- User clicks on a tag in a link: The tag is added to the list of searched tags (searchtags=...)
- if (isset($_GET['addtag'])) {
- // Get previous URL (http_referer) and add the tag to the searchtags parameters in query.
- if (empty($_SERVER['HTTP_REFERER'])) {
- // In case browser does not send HTTP_REFERER
- header('Location: ?searchtags='.urlencode($_GET['addtag']));
- exit;
- }
- parse_str(parse_url($_SERVER['HTTP_REFERER'], PHP_URL_QUERY), $params);
-
- // Prevent redirection loop
- if (isset($params['addtag'])) {
- unset($params['addtag']);
- }
-
- // Check if this tag is already in the search query and ignore it if it is.
- // Each tag is always separated by a space
- if (isset($params['searchtags'])) {
- $current_tags = explode(' ', $params['searchtags']);
- } else {
- $current_tags = array();
- }
- $addtag = true;
- foreach ($current_tags as $value) {
- if ($value === $_GET['addtag']) {
- $addtag = false;
- break;
- }
- }
- // Append the tag if necessary
- if (empty($params['searchtags'])) {
- $params['searchtags'] = trim($_GET['addtag']);
- } elseif ($addtag) {
- $params['searchtags'] = trim($params['searchtags']).' '.trim($_GET['addtag']);
- }
-
- // We also remove page (keeping the same page has no sense, since the
- // results are different)
- unset($params['page']);
-
- header('Location: ?'.http_build_query($params));
- exit;
- }
-
- // -------- User clicks on a tag in result count: Remove the tag from the list of searched tags (searchtags=...)
- if (isset($_GET['removetag'])) {
- // Get previous URL (http_referer) and remove the tag from the searchtags parameters in query.
- if (empty($_SERVER['HTTP_REFERER'])) {
- header('Location: ?');
- exit;
- }
-
- // In case browser does not send HTTP_REFERER
- parse_str(parse_url($_SERVER['HTTP_REFERER'], PHP_URL_QUERY), $params);
+$loginManager->checkLoginState(client_ip_id($_SERVER));
- // Prevent redirection loop
- if (isset($params['removetag'])) {
- unset($params['removetag']);
- }
-
- if (isset($params['searchtags'])) {
- $tags = explode(' ', $params['searchtags']);
- // Remove value from array $tags.
- $tags = array_diff($tags, array($_GET['removetag']));
- $params['searchtags'] = implode(' ', $tags);
-
- if (empty($params['searchtags'])) {
- unset($params['searchtags']);
- }
-
- // We also remove page (keeping the same page has no sense, since
- // the results are different)
- unset($params['page']);
- }
- header('Location: ?'.http_build_query($params));
- exit;
- }
-
- // -------- User wants to change the number of bookmarks per page (linksperpage=...)
- if (isset($_GET['linksperpage'])) {
- if (is_numeric($_GET['linksperpage'])) {
- $_SESSION['LINKS_PER_PAGE']=abs(intval($_GET['linksperpage']));
- }
-
- if (! empty($_SERVER['HTTP_REFERER'])) {
- $location = generateLocation($_SERVER['HTTP_REFERER'], $_SERVER['HTTP_HOST'], array('linksperpage'));
- } else {
- $location = '?';
- }
- header('Location: '. $location);
- exit;
- }
-
- // -------- User wants to see only private bookmarks (toggle)
- if (isset($_GET['visibility'])) {
- if ($_GET['visibility'] === 'private') {
- // Visibility not set or not already private, set private, otherwise reset it
- if (empty($_SESSION['visibility']) || $_SESSION['visibility'] !== 'private') {
- // See only private bookmarks
- $_SESSION['visibility'] = 'private';
- } else {
- unset($_SESSION['visibility']);
- }
- } elseif ($_GET['visibility'] === 'public') {
- if (empty($_SESSION['visibility']) || $_SESSION['visibility'] !== 'public') {
- // See only public bookmarks
- $_SESSION['visibility'] = 'public';
- } else {
- unset($_SESSION['visibility']);
- }
- }
-
- if (! empty($_SERVER['HTTP_REFERER'])) {
- $location = generateLocation($_SERVER['HTTP_REFERER'], $_SERVER['HTTP_HOST'], array('visibility'));
- } else {
- $location = '?';
- }
- header('Location: '. $location);
- exit;
- }
-
- // -------- User wants to see only untagged bookmarks (toggle)
- if (isset($_GET['untaggedonly'])) {
- $_SESSION['untaggedonly'] = empty($_SESSION['untaggedonly']);
-
- if (! empty($_SERVER['HTTP_REFERER'])) {
- $location = generateLocation($_SERVER['HTTP_REFERER'], $_SERVER['HTTP_HOST'], array('untaggedonly'));
- } else {
- $location = '?';
- }
- header('Location: '. $location);
- exit;
- }
-
- // -------- Handle other actions allowed for non-logged in users:
- if (!$loginManager->isLoggedIn()) {
- // User tries to post new link but is not logged in:
- // Show login screen, then redirect to ?post=...
- if (isset($_GET['post'])) {
- header( // Redirect to login page, then back to post link.
- 'Location: /login?post='.urlencode($_GET['post']).
- (!empty($_GET['title'])?'&title='.urlencode($_GET['title']):'').
- (!empty($_GET['description'])?'&description='.urlencode($_GET['description']):'').
- (!empty($_GET['tags'])?'&tags='.urlencode($_GET['tags']):'').
- (!empty($_GET['source'])?'&source='.urlencode($_GET['source']):'')
- );
- exit;
- }
-
- showLinkList($PAGE, $bookmarkService, $conf, $pluginManager, $loginManager);
- if (isset($_GET['edit_link'])) {
- header('Location: /login?edit_link='. escape($_GET['edit_link']));
- exit;
- }
-
- exit; // Never remove this one! All operations below are reserved for logged in user.
- }
-
- // -------- All other functions are reserved for the registered user:
-
- // -------- Display the Tools menu if requested (import/export/bookmarklet...)
- if ($targetPage == Router::$PAGE_TOOLS) {
- $data = [
- 'pageabsaddr' => index_url($_SERVER),
- 'sslenabled' => is_https($_SERVER),
- ];
- $pluginManager->executeHooks('render_tools', $data);
-
- foreach ($data as $key => $value) {
- $PAGE->assign($key, $value);
- }
-
- $PAGE->assign('pagetitle', t('Tools') .' - '. $conf->get('general.title', 'Shaarli'));
- $PAGE->renderPage('tools');
- exit;
- }
-
- // -------- User wants to change his/her password.
- if ($targetPage == Router::$PAGE_CHANGEPASSWORD) {
- if ($conf->get('security.open_shaarli')) {
- die(t('You are not supposed to change a password on an Open Shaarli.'));
- }
-
- if (!empty($_POST['setpassword']) && !empty($_POST['oldpassword'])) {
- if (!$sessionManager->checkToken($_POST['token'])) {
- die(t('Wrong token.')); // Go away!
- }
-
- // Make sure old password is correct.
- $oldhash = sha1(
- $_POST['oldpassword'].$conf->get('credentials.login').$conf->get('credentials.salt')
- );
- if ($oldhash != $conf->get('credentials.hash')) {
- echo '<script>alert("'
- . t('The old password is not correct.')
- .'");document.location=\'?do=changepasswd\';</script>';
- exit;
- }
- // Save new password
- // Salt renders rainbow-tables attacks useless.
- $conf->set('credentials.salt', sha1(uniqid('', true) .'_'. mt_rand()));
- $conf->set(
- 'credentials.hash',
- sha1(
- $_POST['setpassword']
- . $conf->get('credentials.login')
- . $conf->get('credentials.salt')
- )
- );
- try {
- $conf->write($loginManager->isLoggedIn());
- } catch (Exception $e) {
- error_log(
- 'ERROR while writing config file after changing password.' . PHP_EOL .
- $e->getMessage()
- );
-
- // TODO: do not handle exceptions/errors in JS.
- echo '<script>alert("'. $e->getMessage() .'");document.location=\'?do=tools\';</script>';
- exit;
- }
- echo '<script>alert("'. t('Your password has been changed') .'");document.location=\'?do=tools\';</script>';
- exit;
- } else {
- // show the change password form.
- $PAGE->assign('pagetitle', t('Change password') .' - '. $conf->get('general.title', 'Shaarli'));
- $PAGE->renderPage('changepassword');
- exit;
- }
- }
-
- // -------- User wants to change configuration
- if ($targetPage == Router::$PAGE_CONFIGURE) {
- if (!empty($_POST['title'])) {
- if (!$sessionManager->checkToken($_POST['token'])) {
- die(t('Wrong token.')); // Go away!
- }
- $tz = 'UTC';
- if (!empty($_POST['continent']) && !empty($_POST['city'])
- && isTimeZoneValid($_POST['continent'], $_POST['city'])
- ) {
- $tz = $_POST['continent'] . '/' . $_POST['city'];
- }
- $conf->set('general.timezone', $tz);
- $conf->set('general.title', escape($_POST['title']));
- $conf->set('general.header_link', escape($_POST['titleLink']));
- $conf->set('general.retrieve_description', !empty($_POST['retrieveDescription']));
- $conf->set('resource.theme', escape($_POST['theme']));
- $conf->set('security.session_protection_disabled', !empty($_POST['disablesessionprotection']));
- $conf->set('privacy.default_private_links', !empty($_POST['privateLinkByDefault']));
- $conf->set('feed.rss_permalinks', !empty($_POST['enableRssPermalinks']));
- $conf->set('updates.check_updates', !empty($_POST['updateCheck']));
- $conf->set('privacy.hide_public_links', !empty($_POST['hidePublicLinks']));
- $conf->set('api.enabled', !empty($_POST['enableApi']));
- $conf->set('api.secret', escape($_POST['apiSecret']));
- $conf->set('formatter', escape($_POST['formatter']));
-
- if (! empty($_POST['language'])) {
- $conf->set('translation.language', escape($_POST['language']));
- }
-
- $thumbnailsMode = extension_loaded('gd') ? $_POST['enableThumbnails'] : Thumbnailer::MODE_NONE;
- if ($thumbnailsMode !== Thumbnailer::MODE_NONE
- && $thumbnailsMode !== $conf->get('thumbnails.mode', Thumbnailer::MODE_NONE)
- ) {
- $_SESSION['warnings'][] = t(
- 'You have enabled or changed thumbnails mode. '
- .'<a href="?do=thumbs_update">Please synchronize them</a>.'
- );
- }
- $conf->set('thumbnails.mode', $thumbnailsMode);
-
- try {
- $conf->write($loginManager->isLoggedIn());
- $history->updateSettings();
- invalidateCaches($conf->get('resource.page_cache'));
- } catch (Exception $e) {
- error_log(
- 'ERROR while writing config file after configuration update.' . PHP_EOL .
- $e->getMessage()
- );
-
- // TODO: do not handle exceptions/errors in JS.
- echo '<script>alert("'. $e->getMessage() .'");document.location=\'?do=configure\';</script>';
- exit;
- }
- echo '<script>alert("'. t('Configuration was saved.') .'");document.location=\'?do=configure\';</script>';
- exit;
- } else {
- // Show the configuration form.
- $PAGE->assign('title', $conf->get('general.title'));
- $PAGE->assign('theme', $conf->get('resource.theme'));
- $PAGE->assign('theme_available', ThemeUtils::getThemes($conf->get('resource.raintpl_tpl')));
- $PAGE->assign('formatter_available', ['default', 'markdown']);
- list($continents, $cities) = generateTimeZoneData(
- timezone_identifiers_list(),
- $conf->get('general.timezone')
- );
- $PAGE->assign('continents', $continents);
- $PAGE->assign('cities', $cities);
- $PAGE->assign('retrieve_description', $conf->get('general.retrieve_description'));
- $PAGE->assign('private_links_default', $conf->get('privacy.default_private_links', false));
- $PAGE->assign('session_protection_disabled', $conf->get('security.session_protection_disabled', false));
- $PAGE->assign('enable_rss_permalinks', $conf->get('feed.rss_permalinks', false));
- $PAGE->assign('enable_update_check', $conf->get('updates.check_updates', true));
- $PAGE->assign('hide_public_links', $conf->get('privacy.hide_public_links', false));
- $PAGE->assign('api_enabled', $conf->get('api.enabled', true));
- $PAGE->assign('api_secret', $conf->get('api.secret'));
- $PAGE->assign('languages', Languages::getAvailableLanguages());
- $PAGE->assign('gd_enabled', extension_loaded('gd'));
- $PAGE->assign('thumbnails_mode', $conf->get('thumbnails.mode', Thumbnailer::MODE_NONE));
- $PAGE->assign('pagetitle', t('Configure') .' - '. $conf->get('general.title', 'Shaarli'));
- $PAGE->renderPage('configure');
- exit;
- }
- }
-
- // -------- User wants to rename a tag or delete it
- if ($targetPage == Router::$PAGE_CHANGETAG) {
- if (empty($_POST['fromtag']) || (empty($_POST['totag']) && isset($_POST['renametag']))) {
- $PAGE->assign('fromtag', ! empty($_GET['fromtag']) ? escape($_GET['fromtag']) : '');
- $PAGE->assign('pagetitle', t('Manage tags') .' - '. $conf->get('general.title', 'Shaarli'));
- $PAGE->renderPage('changetag');
- exit;
- }
-
- if (!$sessionManager->checkToken($_POST['token'])) {
- die(t('Wrong token.'));
- }
-
- $toTag = isset($_POST['totag']) ? escape($_POST['totag']) : null;
- $fromTag = escape($_POST['fromtag']);
- $count = 0;
- $bookmarks = $bookmarkService->search(['searchtags' => $fromTag], BookmarkFilter::$ALL, true);
- foreach ($bookmarks as $bookmark) {
- if ($toTag) {
- $bookmark->renameTag($fromTag, $toTag);
- } else {
- $bookmark->deleteTag($fromTag);
- }
- $bookmarkService->set($bookmark, false);
- $history->updateLink($bookmark);
- $count++;
- }
- $bookmarkService->save();
- $delete = empty($_POST['totag']);
- $redirect = $delete ? 'do=changetag' : 'searchtags='. urlencode(escape($_POST['totag']));
- $alert = $delete
- ? sprintf(t('The tag was removed from %d link.', 'The tag was removed from %d bookmarks.', $count), $count)
- : sprintf(t('The tag was renamed in %d link.', 'The tag was renamed in %d bookmarks.', $count), $count);
- echo '<script>alert("'. $alert .'");document.location=\'?'. $redirect .'\';</script>';
- exit;
- }
-
- // -------- User wants to add a link without using the bookmarklet: Show form.
- if ($targetPage == Router::$PAGE_ADDLINK) {
- $PAGE->assign('pagetitle', t('Shaare a new link') .' - '. $conf->get('general.title', 'Shaarli'));
- $PAGE->renderPage('addlink');
- exit;
- }
-
- // -------- User clicked the "Save" button when editing a link: Save link to database.
- if (isset($_POST['save_edit'])) {
- // Go away!
- if (! $sessionManager->checkToken($_POST['token'])) {
- die(t('Wrong token.'));
- }
-
- // lf_id should only be present if the link exists.
- $id = isset($_POST['lf_id']) ? intval(escape($_POST['lf_id'])) : null;
- if ($id && $bookmarkService->exists($id)) {
- // Edit
- $bookmark = $bookmarkService->get($id);
- } else {
- // New link
- $bookmark = new Bookmark();
- }
-
- $bookmark->setTitle($_POST['lf_title']);
- $bookmark->setDescription($_POST['lf_description']);
- $bookmark->setUrl($_POST['lf_url'], $conf->get('security.allowed_protocols'));
- $bookmark->setPrivate(isset($_POST['lf_private']));
- $bookmark->setTagsString($_POST['lf_tags']);
-
- if ($conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE
- && ! $bookmark->isNote()
- ) {
- $thumbnailer = new Thumbnailer($conf);
- $bookmark->setThumbnail($thumbnailer->get($bookmark->getUrl()));
- }
- $bookmarkService->addOrSet($bookmark, false);
-
- // To preserve backward compatibility with 3rd parties, plugins still use arrays
- $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
- $formatter = $factory->getFormatter('raw');
- $data = $formatter->format($bookmark);
- $pluginManager->executeHooks('save_link', $data);
-
- $bookmark->fromArray($data);
- $bookmarkService->set($bookmark);
-
- // If we are called from the bookmarklet, we must close the popup:
- if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) {
- echo '<script>self.close();</script>';
- exit;
- }
-
- $returnurl = !empty($_POST['returnurl']) ? $_POST['returnurl'] : '?';
- $location = generateLocation($returnurl, $_SERVER['HTTP_HOST'], array('addlink', 'post', 'edit_link'));
- // Scroll to the link which has been edited.
- $location .= '#' . $bookmark->getShortUrl();
- // After saving the link, redirect to the page the user was on.
- header('Location: '. $location);
- exit;
- }
-
- // -------- User clicked the "Delete" button when editing a link: Delete link from database.
- if ($targetPage == Router::$PAGE_DELETELINK) {
- if (! $sessionManager->checkToken($_GET['token'])) {
- die(t('Wrong token.'));
- }
-
- $ids = trim($_GET['lf_linkdate']);
- if (strpos($ids, ' ') !== false) {
- // multiple, space-separated ids provided
- $ids = array_values(array_filter(
- preg_split('/\s+/', escape($ids)),
- function ($item) {
- return $item !== '';
- }
- ));
- } else {
- // only a single id provided
- $shortUrl = $bookmarkService->get($ids)->getShortUrl();
- $ids = [$ids];
- }
- // assert at least one id is given
- if (!count($ids)) {
- die('no id provided');
- }
- $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
- $formatter = $factory->getFormatter('raw');
- foreach ($ids as $id) {
- $id = (int) escape($id);
- $bookmark = $bookmarkService->get($id);
- $data = $formatter->format($bookmark);
- $pluginManager->executeHooks('delete_link', $data);
- $bookmarkService->remove($bookmark, false);
- }
- $bookmarkService->save();
-
- // If we are called from the bookmarklet, we must close the popup:
- if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) {
- echo '<script>self.close();</script>';
- exit;
- }
-
- $location = '?';
- if (isset($_SERVER['HTTP_REFERER'])) {
- // Don't redirect to where we were previously if it was a permalink or an edit_link, because it would 404.
- $location = generateLocation(
- $_SERVER['HTTP_REFERER'],
- $_SERVER['HTTP_HOST'],
- ['delete_link', 'edit_link', ! empty($shortUrl) ? $shortUrl : null]
- );
- }
-
- header('Location: ' . $location); // After deleting the link, redirect to appropriate location
- exit;
- }
-
- // -------- User clicked either "Set public" or "Set private" bulk operation
- if ($targetPage == Router::$PAGE_CHANGE_VISIBILITY) {
- if (! $sessionManager->checkToken($_GET['token'])) {
- die(t('Wrong token.'));
- }
-
- $ids = trim($_GET['ids']);
- if (strpos($ids, ' ') !== false) {
- // multiple, space-separated ids provided
- $ids = array_values(array_filter(preg_split('/\s+/', escape($ids))));
- } else {
- // only a single id provided
- $ids = [$ids];
- }
-
- // assert at least one id is given
- if (!count($ids)) {
- die('no id provided');
- }
- // assert that the visibility is valid
- if (!isset($_GET['newVisibility']) || !in_array($_GET['newVisibility'], ['public', 'private'])) {
- die('invalid visibility');
- } else {
- $private = $_GET['newVisibility'] === 'private';
- }
- $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
- $formatter = $factory->getFormatter('raw');
- foreach ($ids as $id) {
- $id = (int) escape($id);
- $bookmark = $bookmarkService->get($id);
- $bookmark->setPrivate($private);
-
- // To preserve backward compatibility with 3rd parties, plugins still use arrays
- $data = $formatter->format($bookmark);
- $pluginManager->executeHooks('save_link', $data);
- $bookmark->fromArray($data);
-
- $bookmarkService->set($bookmark);
- }
- $bookmarkService->save();
-
- $location = '?';
- if (isset($_SERVER['HTTP_REFERER'])) {
- $location = generateLocation(
- $_SERVER['HTTP_REFERER'],
- $_SERVER['HTTP_HOST']
- );
- }
- header('Location: ' . $location); // After deleting the link, redirect to appropriate location
- exit;
- }
-
- // -------- User clicked the "EDIT" button on a link: Display link edit form.
- if (isset($_GET['edit_link'])) {
- $id = (int) escape($_GET['edit_link']);
- try {
- $link = $bookmarkService->get($id); // Read database
- } catch (BookmarkNotFoundException $e) {
- // Link not found in database.
- header('Location: ?');
- exit;
- }
-
- $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
- $formatter = $factory->getFormatter('raw');
- $formattedLink = $formatter->format($link);
- $tags = $bookmarkService->bookmarksCountPerTag();
- if ($conf->get('formatter') === 'markdown') {
- $tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1;
- }
- $data = array(
- 'link' => $formattedLink,
- 'link_is_new' => false,
- 'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''),
- 'tags' => $tags,
- );
- $pluginManager->executeHooks('render_editlink', $data);
-
- foreach ($data as $key => $value) {
- $PAGE->assign($key, $value);
- }
-
- $PAGE->assign('pagetitle', t('Edit') .' '. t('Shaare') .' - '. $conf->get('general.title', 'Shaarli'));
- $PAGE->renderPage('editlink');
- exit;
- }
-
- // -------- User want to post a new link: Display link edit form.
- if (isset($_GET['post'])) {
- $url = cleanup_url($_GET['post']);
-
- $link_is_new = false;
- // Check if URL is not already in database (in this case, we will edit the existing link)
- $bookmark = $bookmarkService->findByUrl($url);
- if (! $bookmark) {
- $link_is_new = true;
- // Get title if it was provided in URL (by the bookmarklet).
- $title = empty($_GET['title']) ? '' : escape($_GET['title']);
- // Get description if it was provided in URL (by the bookmarklet). [Bronco added that]
- $description = empty($_GET['description']) ? '' : escape($_GET['description']);
- $tags = empty($_GET['tags']) ? '' : escape($_GET['tags']);
- $private = !empty($_GET['private']) && $_GET['private'] === "1" ? 1 : 0;
-
- // If this is an HTTP(S) link, we try go get the page to extract
- // the title (otherwise we will to straight to the edit form.)
- if (empty($title) && strpos(get_url_scheme($url), 'http') !== false) {
- $retrieveDescription = $conf->get('general.retrieve_description');
- // Short timeout to keep the application responsive
- // The callback will fill $charset and $title with data from the downloaded page.
- get_http_response(
- $url,
- $conf->get('general.download_timeout', 30),
- $conf->get('general.download_max_size', 4194304),
- get_curl_download_callback($charset, $title, $description, $tags, $retrieveDescription)
- );
- if (! empty($title) && strtolower($charset) != 'utf-8') {
- $title = mb_convert_encoding($title, 'utf-8', $charset);
- }
- }
-
- if ($url == '') {
- $title = $conf->get('general.default_note_title', t('Note: '));
- }
- $url = escape($url);
- $title = escape($title);
-
- $link = [
- 'title' => $title,
- 'url' => $url,
- 'description' => $description,
- 'tags' => $tags,
- 'private' => $private,
- ];
- } else {
- $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
- $formatter = $factory->getFormatter('raw');
- $link = $formatter->format($bookmark);
- }
-
- $tags = $bookmarkService->bookmarksCountPerTag();
- if ($conf->get('formatter') === 'markdown') {
- $tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1;
- }
- $data = [
- 'link' => $link,
- 'link_is_new' => $link_is_new,
- 'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''),
- 'source' => (isset($_GET['source']) ? $_GET['source'] : ''),
- 'tags' => $tags,
- 'default_private_links' => $conf->get('privacy.default_private_links', false),
- ];
- $pluginManager->executeHooks('render_editlink', $data);
-
- foreach ($data as $key => $value) {
- $PAGE->assign($key, $value);
- }
-
- $PAGE->assign('pagetitle', t('Shaare') .' - '. $conf->get('general.title', 'Shaarli'));
- $PAGE->renderPage('editlink');
- exit;
- }
-
- if ($targetPage == Router::$PAGE_PINLINK) {
- if (! isset($_GET['id']) || !$bookmarkService->exists($_GET['id'])) {
- // FIXME! Use a proper error system.
- $msg = t('Invalid link ID provided');
- echo '<script>alert("'. $msg .'");document.location=\''. index_url($_SERVER) .'\';</script>';
- exit;
- }
- if (! $sessionManager->checkToken($_GET['token'])) {
- die('Wrong token.');
- }
-
- $link = $bookmarkService->get($_GET['id']);
- $link->setSticky(! $link->isSticky());
- $bookmarkService->set($link);
- header('Location: '.index_url($_SERVER));
- exit;
- }
-
- if ($targetPage == Router::$PAGE_EXPORT) {
- // Export bookmarks as a Netscape Bookmarks file
-
- if (empty($_GET['selection'])) {
- $PAGE->assign('pagetitle', t('Export') .' - '. $conf->get('general.title', 'Shaarli'));
- $PAGE->renderPage('export');
- exit;
- }
-
- // export as bookmarks_(all|private|public)_YYYYmmdd_HHMMSS.html
- $selection = $_GET['selection'];
- if (isset($_GET['prepend_note_url'])) {
- $prependNoteUrl = $_GET['prepend_note_url'];
- } else {
- $prependNoteUrl = false;
- }
-
- try {
- $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
- $formatter = $factory->getFormatter('raw');
- $PAGE->assign(
- 'links',
- NetscapeBookmarkUtils::filterAndFormat(
- $bookmarkService,
- $formatter,
- $selection,
- $prependNoteUrl,
- index_url($_SERVER)
- )
- );
- } catch (Exception $exc) {
- header('Content-Type: text/plain; charset=utf-8');
- echo $exc->getMessage();
- exit;
- }
- $now = new DateTime();
- header('Content-Type: text/html; charset=utf-8');
- header(
- 'Content-disposition: attachment; filename=bookmarks_'
- .$selection.'_'.$now->format(Bookmark::LINK_DATE_FORMAT).'.html'
- );
- $PAGE->assign('date', $now->format(DateTime::RFC822));
- $PAGE->assign('eol', PHP_EOL);
- $PAGE->assign('selection', $selection);
- $PAGE->renderPage('export.bookmarks');
- exit;
- }
-
- if ($targetPage == Router::$PAGE_IMPORT) {
- // Upload a Netscape bookmark dump to import its contents
-
- if (! isset($_POST['token']) || ! isset($_FILES['filetoupload'])) {
- // Show import dialog
- $PAGE->assign(
- 'maxfilesize',
- get_max_upload_size(
- ini_get('post_max_size'),
- ini_get('upload_max_filesize'),
- false
- )
- );
- $PAGE->assign(
- 'maxfilesizeHuman',
- get_max_upload_size(
- ini_get('post_max_size'),
- ini_get('upload_max_filesize'),
- true
- )
- );
- $PAGE->assign('pagetitle', t('Import') .' - '. $conf->get('general.title', 'Shaarli'));
- $PAGE->renderPage('import');
- exit;
- }
-
- // Import bookmarks from an uploaded file
- if (isset($_FILES['filetoupload']['size']) && $_FILES['filetoupload']['size'] == 0) {
- // The file is too big or some form field may be missing.
- $msg = sprintf(
- t(
- 'The file you are trying to upload is probably bigger than what this webserver can accept'
- .' (%s). Please upload in smaller chunks.'
- ),
- get_max_upload_size(ini_get('post_max_size'), ini_get('upload_max_filesize'))
- );
- echo '<script>alert("'. $msg .'");document.location=\'?do='.Router::$PAGE_IMPORT .'\';</script>';
- exit;
- }
- if (! $sessionManager->checkToken($_POST['token'])) {
- die('Wrong token.');
- }
- $status = NetscapeBookmarkUtils::import(
- $_POST,
- $_FILES,
- $bookmarkService,
- $conf,
- $history
- );
- echo '<script>alert("'.$status.'");document.location=\'?do='
- .Router::$PAGE_IMPORT .'\';</script>';
- exit;
- }
-
- // Plugin administration page
- if ($targetPage == Router::$PAGE_PLUGINSADMIN) {
- $pluginMeta = $pluginManager->getPluginsMeta();
-
- // Split plugins into 2 arrays: ordered enabled plugins and disabled.
- $enabledPlugins = array_filter($pluginMeta, function ($v) {
- return $v['order'] !== false;
- });
- // Load parameters.
- $enabledPlugins = load_plugin_parameter_values($enabledPlugins, $conf->get('plugins', array()));
- uasort(
- $enabledPlugins,
- function ($a, $b) {
- return $a['order'] - $b['order'];
- }
- );
- $disabledPlugins = array_filter($pluginMeta, function ($v) {
- return $v['order'] === false;
- });
-
- $PAGE->assign('enabledPlugins', $enabledPlugins);
- $PAGE->assign('disabledPlugins', $disabledPlugins);
- $PAGE->assign('pagetitle', t('Plugin administration') .' - '. $conf->get('general.title', 'Shaarli'));
- $PAGE->renderPage('pluginsadmin');
- exit;
- }
-
- // Plugin administration form action
- if ($targetPage == Router::$PAGE_SAVE_PLUGINSADMIN) {
- try {
- if (isset($_POST['parameters_form'])) {
- $pluginManager->executeHooks('save_plugin_parameters', $_POST);
- unset($_POST['parameters_form']);
- foreach ($_POST as $param => $value) {
- $conf->set('plugins.'. $param, escape($value));
- }
- } else {
- $conf->set('general.enabled_plugins', save_plugin_config($_POST));
- }
- $conf->write($loginManager->isLoggedIn());
- $history->updateSettings();
- } catch (Exception $e) {
- error_log(
- 'ERROR while saving plugin configuration:.' . PHP_EOL .
- $e->getMessage()
- );
-
- // TODO: do not handle exceptions/errors in JS.
- echo '<script>alert("'
- . $e->getMessage()
- .'");document.location=\'?do='
- . Router::$PAGE_PLUGINSADMIN
- .'\';</script>';
- exit;
- }
- header('Location: ?do='. Router::$PAGE_PLUGINSADMIN);
- exit;
- }
-
- // Get a fresh token
- if ($targetPage == Router::$GET_TOKEN) {
- header('Content-Type:text/plain');
- echo $sessionManager->generateToken();
- exit;
- }
-
- // -------- Thumbnails Update
- if ($targetPage == Router::$PAGE_THUMBS_UPDATE) {
- $ids = [];
- foreach ($bookmarkService->search() as $bookmark) {
- // A note or not HTTP(S)
- if ($bookmark->isNote() || ! startsWith(strtolower($bookmark->getUrl()), 'http')) {
- continue;
- }
- $ids[] = $bookmark->getId();
- }
- $PAGE->assign('ids', $ids);
- $PAGE->assign('pagetitle', t('Thumbnails update') .' - '. $conf->get('general.title', 'Shaarli'));
- $PAGE->renderPage('thumbnails');
- exit;
- }
-
- // -------- Single Thumbnail Update
- if ($targetPage == Router::$AJAX_THUMB_UPDATE) {
- if (! isset($_POST['id']) || ! ctype_digit($_POST['id'])) {
- http_response_code(400);
- exit;
- }
- $id = (int) $_POST['id'];
- if (! $bookmarkService->exists($id)) {
- http_response_code(404);
- exit;
- }
- $thumbnailer = new Thumbnailer($conf);
- $bookmark = $bookmarkService->get($id);
- $bookmark->setThumbnail($thumbnailer->get($bookmark->getUrl()));
- $bookmarkService->set($bookmark);
-
- $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
- echo json_encode($factory->getFormatter('raw')->format($bookmark));
- exit;
- }
-
- // -------- Otherwise, simply display search form and bookmarks:
- showLinkList($PAGE, $bookmarkService, $conf, $pluginManager, $loginManager);
- exit;
-}
-
-/**
- * Template for the list of bookmarks (<div id="linklist">)
- * This function fills all the necessary fields in the $PAGE for the template 'linklist.html'
- *
- * @param pageBuilder $PAGE pageBuilder instance.
- * @param BookmarkServiceInterface $linkDb LinkDB instance.
- * @param ConfigManager $conf Configuration Manager instance.
- * @param PluginManager $pluginManager Plugin Manager instance.
- * @param LoginManager $loginManager LoginManager instance
- */
-function buildLinkList($PAGE, $linkDb, $conf, $pluginManager, $loginManager)
-{
- $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
- $formatter = $factory->getFormatter();
-
- // Used in templates
- if (isset($_GET['searchtags'])) {
- if (! empty($_GET['searchtags'])) {
- $searchtags = escape(normalize_spaces($_GET['searchtags']));
- } else {
- $searchtags = false;
- }
- } else {
- $searchtags = '';
- }
- $searchterm = !empty($_GET['searchterm']) ? escape(normalize_spaces($_GET['searchterm'])) : '';
-
- // Smallhash filter
- if (! empty($_SERVER['QUERY_STRING'])
- && preg_match('/^[a-zA-Z0-9-_@]{6}($|&|#)/', $_SERVER['QUERY_STRING'])) {
- try {
- $linksToDisplay = $linkDb->findByHash($_SERVER['QUERY_STRING']);
- } catch (BookmarkNotFoundException $e) {
- $PAGE->render404($e->getMessage());
- exit;
- }
- } else {
- // Filter bookmarks according search parameters.
- $visibility = ! empty($_SESSION['visibility']) ? $_SESSION['visibility'] : null;
- $request = [
- 'searchtags' => $searchtags,
- 'searchterm' => $searchterm,
- ];
- $linksToDisplay = $linkDb->search($request, $visibility, false, !empty($_SESSION['untaggedonly']));
- }
-
- // ---- Handle paging.
- $keys = array();
- foreach ($linksToDisplay as $key => $value) {
- $keys[] = $key;
- }
-
- // Select articles according to paging.
- $pagecount = ceil(count($keys) / $_SESSION['LINKS_PER_PAGE']);
- $pagecount = $pagecount == 0 ? 1 : $pagecount;
- $page= empty($_GET['page']) ? 1 : intval($_GET['page']);
- $page = $page < 1 ? 1 : $page;
- $page = $page > $pagecount ? $pagecount : $page;
- // Start index.
- $i = ($page-1) * $_SESSION['LINKS_PER_PAGE'];
- $end = $i + $_SESSION['LINKS_PER_PAGE'];
-
- $thumbnailsEnabled = $conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE;
- if ($thumbnailsEnabled) {
- $thumbnailer = new Thumbnailer($conf);
- }
-
- $linkDisp = array();
- while ($i<$end && $i<count($keys)) {
- $link = $formatter->format($linksToDisplay[$keys[$i]]);
-
- // Logged in, thumbnails enabled, not a note,
- // and (never retrieved yet or no valid cache file)
- if ($loginManager->isLoggedIn()
- && $thumbnailsEnabled
- && !$linksToDisplay[$keys[$i]]->isNote()
- && $linksToDisplay[$keys[$i]]->getThumbnail() !== false
- && ! is_file($linksToDisplay[$keys[$i]]->getThumbnail())
- ) {
- $linksToDisplay[$keys[$i]]->setThumbnail($thumbnailer->get($link['url']));
- $linkDb->set($linksToDisplay[$keys[$i]], false);
- $updateDB = true;
- $link['thumbnail'] = $linksToDisplay[$keys[$i]]->getThumbnail();
- }
-
- // Check for both signs of a note: starting with ? and 7 chars long.
-// if ($link['url'][0] === '?' && strlen($link['url']) === 7) {
-// $link['url'] = index_url($_SERVER) . $link['url'];
-// }
-
- $linkDisp[$keys[$i]] = $link;
- $i++;
- }
-
- // If we retrieved new thumbnails, we update the database.
- if (!empty($updateDB)) {
- $linkDb->save();
- }
+$containerBuilder = new ContainerBuilder($conf, $sessionManager, $cookieManager, $loginManager);
+$container = $containerBuilder->build();
+$app = new App($container);
- // Compute paging navigation
- $searchtagsUrl = $searchtags === '' ? '' : '&searchtags=' . urlencode($searchtags);
- $searchtermUrl = empty($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;
- }
+// Main Shaarli routes
+$app->group('', function () {
+ $this->get('/install', '\Shaarli\Front\Controller\Visitor\InstallController:index')->setName('displayInstall');
+ $this->get('/install/session-test', '\Shaarli\Front\Controller\Visitor\InstallController:sessionTest');
+ $this->post('/install', '\Shaarli\Front\Controller\Visitor\InstallController:save')->setName('saveInstall');
+
+ /* -- PUBLIC --*/
+ $this->get('/', '\Shaarli\Front\Controller\Visitor\BookmarkListController:index');
+ $this->get('/shaare/{hash}', '\Shaarli\Front\Controller\Visitor\BookmarkListController:permalink');
+ $this->get('/login', '\Shaarli\Front\Controller\Visitor\LoginController:index')->setName('login');
+ $this->post('/login', '\Shaarli\Front\Controller\Visitor\LoginController:login')->setName('processLogin');
+ $this->get('/picture-wall', '\Shaarli\Front\Controller\Visitor\PictureWallController:index');
+ $this->get('/tags/cloud', '\Shaarli\Front\Controller\Visitor\TagCloudController:cloud');
+ $this->get('/tags/list', '\Shaarli\Front\Controller\Visitor\TagCloudController:list');
+ $this->get('/daily', '\Shaarli\Front\Controller\Visitor\DailyController:index');
+ $this->get('/daily-rss', '\Shaarli\Front\Controller\Visitor\DailyController:rss')->setName('rss');
+ $this->get('/feed/atom', '\Shaarli\Front\Controller\Visitor\FeedController:atom')->setName('atom');
+ $this->get('/feed/rss', '\Shaarli\Front\Controller\Visitor\FeedController:rss');
+ $this->get('/open-search', '\Shaarli\Front\Controller\Visitor\OpenSearchController:index');
+
+ $this->get('/add-tag/{newTag}', '\Shaarli\Front\Controller\Visitor\TagController:addTag');
+ $this->get('/remove-tag/{tag}', '\Shaarli\Front\Controller\Visitor\TagController:removeTag');
+ $this->get('/links-per-page', '\Shaarli\Front\Controller\Visitor\PublicSessionFilterController:linksPerPage');
+ $this->get('/untagged-only', '\Shaarli\Front\Controller\Admin\PublicSessionFilterController:untaggedOnly');
+})->add('\Shaarli\Front\ShaarliMiddleware');
- // Fill all template fields.
- $data = array(
- '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' => ! empty($_SESSION['visibility']) ? $_SESSION['visibility'] : '',
- 'links' => $linkDisp,
+$app->group('/admin', function () {
+ $this->get('/logout', '\Shaarli\Front\Controller\Admin\LogoutController:index');
+ $this->get('/tools', '\Shaarli\Front\Controller\Admin\ToolsController:index');
+ $this->get('/password', '\Shaarli\Front\Controller\Admin\PasswordController:index');
+ $this->post('/password', '\Shaarli\Front\Controller\Admin\PasswordController:change');
+ $this->get('/configure', '\Shaarli\Front\Controller\Admin\ConfigureController:index');
+ $this->post('/configure', '\Shaarli\Front\Controller\Admin\ConfigureController:save');
+ $this->get('/tags', '\Shaarli\Front\Controller\Admin\ManageTagController:index');
+ $this->post('/tags', '\Shaarli\Front\Controller\Admin\ManageTagController:save');
+ $this->get('/add-shaare', '\Shaarli\Front\Controller\Admin\ManageShaareController:addShaare');
+ $this->get('/shaare', '\Shaarli\Front\Controller\Admin\ManageShaareController:displayCreateForm');
+ $this->get('/shaare/{id:[0-9]+}', '\Shaarli\Front\Controller\Admin\ManageShaareController:displayEditForm');
+ $this->post('/shaare', '\Shaarli\Front\Controller\Admin\ManageShaareController:save');
+ $this->get('/shaare/delete', '\Shaarli\Front\Controller\Admin\ManageShaareController:deleteBookmark');
+ $this->get('/shaare/visibility', '\Shaarli\Front\Controller\Admin\ManageShaareController:changeVisibility');
+ $this->get('/shaare/{id:[0-9]+}/pin', '\Shaarli\Front\Controller\Admin\ManageShaareController:pinBookmark');
+ $this->patch(
+ '/shaare/{id:[0-9]+}/update-thumbnail',
+ '\Shaarli\Front\Controller\Admin\ThumbnailsController:ajaxUpdate'
);
+ $this->get('/export', '\Shaarli\Front\Controller\Admin\ExportController:index');
+ $this->post('/export', '\Shaarli\Front\Controller\Admin\ExportController:export');
+ $this->get('/import', '\Shaarli\Front\Controller\Admin\ImportController:index');
+ $this->post('/import', '\Shaarli\Front\Controller\Admin\ImportController:import');
+ $this->get('/plugins', '\Shaarli\Front\Controller\Admin\PluginsController:index');
+ $this->post('/plugins', '\Shaarli\Front\Controller\Admin\PluginsController:save');
+ $this->get('/token', '\Shaarli\Front\Controller\Admin\TokenController:getToken');
+ $this->get('/thumbnails', '\Shaarli\Front\Controller\Admin\ThumbnailsController:index');
- // If there is only a single link, we change on-the-fly the title of the page.
- if (count($linksToDisplay) == 1) {
- $data['pagetitle'] = $linksToDisplay[$keys[0]]->getTitle() .' - '. $conf->get('general.title');
- } elseif (! 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'] .= '- '. $conf->get('general.title');
- }
-
- $pluginManager->executeHooks('render_linklist', $data, array('loggedin' => $loginManager->isLoggedIn()));
-
- foreach ($data as $key => $value) {
- $PAGE->assign($key, $value);
- }
-
- return;
-}
-
-/**
- * Installation
- * This function should NEVER be called if the file data/config.php exists.
- *
- * @param ConfigManager $conf Configuration Manager instance.
- * @param SessionManager $sessionManager SessionManager instance
- * @param LoginManager $loginManager LoginManager instance
- */
-function install($conf, $sessionManager, $loginManager)
-{
- // On free.fr host, make sure the /sessions directory exists, otherwise login will not work.
- if (endsWith($_SERVER['HTTP_HOST'], '.free.fr') && !is_dir($_SERVER['DOCUMENT_ROOT'].'/sessions')) {
- mkdir($_SERVER['DOCUMENT_ROOT'].'/sessions', 0705);
- }
-
-
- // This part makes sure sessions works correctly.
- // (Because on some hosts, session.save_path may not be set correctly,
- // or we may not have write access to it.)
- if (isset($_GET['test_session'])
- && ( !isset($_SESSION) || !isset($_SESSION['session_tested']) || $_SESSION['session_tested']!='Working')) {
- // Step 2: Check if data in session is correct.
- $msg = t(
- '<pre>Sessions do not seem to work correctly on your server.<br>'.
- 'Make sure the variable "session.save_path" is set correctly in your PHP config, '.
- 'and that you have write access to it.<br>'.
- 'It currently points to %s.<br>'.
- 'On some browsers, accessing your server via a hostname like \'localhost\' '.
- 'or any custom hostname without a dot causes cookie storage to fail. '.
- 'We recommend accessing your server via it\'s IP address or Fully Qualified Domain Name.<br>'
- );
- $msg = sprintf($msg, session_save_path());
- echo $msg;
- echo '<br><a href="?">'. t('Click to try again.') .'</a></pre>';
- die;
- }
- if (!isset($_SESSION['session_tested'])) {
- // Step 1 : Try to store data in session and reload page.
- $_SESSION['session_tested'] = 'Working'; // Try to set a variable in session.
- header('Location: '.index_url($_SERVER).'?test_session'); // Redirect to check stored data.
- }
- if (isset($_GET['test_session'])) {
- // Step 3: Sessions are OK. Remove test parameter from URL.
- header('Location: '.index_url($_SERVER));
- }
-
-
- if (!empty($_POST['setlogin']) && !empty($_POST['setpassword'])) {
- $tz = 'UTC';
- if (!empty($_POST['continent']) && !empty($_POST['city'])
- && isTimeZoneValid($_POST['continent'], $_POST['city'])
- ) {
- $tz = $_POST['continent'].'/'.$_POST['city'];
- }
- $conf->set('general.timezone', $tz);
- $login = $_POST['setlogin'];
- $conf->set('credentials.login', $login);
- $salt = sha1(uniqid('', true) .'_'. mt_rand());
- $conf->set('credentials.salt', $salt);
- $conf->set('credentials.hash', sha1($_POST['setpassword'] . $login . $salt));
- if (!empty($_POST['title'])) {
- $conf->set('general.title', escape($_POST['title']));
- } else {
- $conf->set('general.title', 'Shared bookmarks on '.escape(index_url($_SERVER)));
- }
- $conf->set('translation.language', escape($_POST['language']));
- $conf->set('updates.check_updates', !empty($_POST['updateCheck']));
- $conf->set('api.enabled', !empty($_POST['enableApi']));
- $conf->set(
- 'api.secret',
- generate_api_secret(
- $conf->get('credentials.login'),
- $conf->get('credentials.salt')
- )
- );
- try {
- // Everything is ok, let's create config file.
- $conf->write($loginManager->isLoggedIn());
- } catch (Exception $e) {
- error_log(
- 'ERROR while writing config file after installation.' . PHP_EOL .
- $e->getMessage()
- );
-
- // TODO: do not handle exceptions/errors in JS.
- echo '<script>alert("'. $e->getMessage() .'");document.location=\'?\';</script>';
- exit;
- }
-
- $history = new History($conf->get('resource.history'));
- $bookmarkService = new BookmarkFileService($conf, $history, true);
- if ($bookmarkService->count() === 0) {
- $bookmarkService->initialize();
- }
+ $this->get('/visibility/{visibility}', '\Shaarli\Front\Controller\Admin\SessionFilterController:visibility');
+})->add('\Shaarli\Front\ShaarliAdminMiddleware');
- echo '<script>alert('
- .'"Shaarli is now configured. '
- .'Please enter your login/password and start shaaring your bookmarks!"'
- .');document.location=\'./login\';</script>';
- exit;
- }
-
- $PAGE = new PageBuilder($conf, $_SESSION, null, $sessionManager->generateToken());
- list($continents, $cities) = generateTimeZoneData(timezone_identifiers_list(), date_default_timezone_get());
- $PAGE->assign('continents', $continents);
- $PAGE->assign('cities', $cities);
- $PAGE->assign('languages', Languages::getAvailableLanguages());
- $PAGE->renderPage('install');
- exit;
-}
-
-if (!isset($_SESSION['LINKS_PER_PAGE'])) {
- $_SESSION['LINKS_PER_PAGE'] = $conf->get('general.links_per_page', 20);
-}
-
-try {
- $history = new History($conf->get('resource.history'));
-} catch (Exception $e) {
- die($e->getMessage());
-}
-
-$linkDb = new BookmarkFileService($conf, $history, $loginManager->isLoggedIn());
-
-if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=dailyrss')) {
- showDailyRSS($linkDb, $conf, $loginManager);
- exit;
-}
-
-$containerBuilder = new ContainerBuilder($conf, $sessionManager, $loginManager);
-$container = $containerBuilder->build();
-$app = new App($container);
// REST API routes
$app->group('/api/v1', function () {
$this->get('/history', '\Shaarli\Api\Controllers\HistoryController:getHistory')->setName('getHistory');
})->add('\Shaarli\Api\ApiMiddleware');
-$app->group('', function () {
- $this->get('/login', '\Shaarli\Front\Controller\LoginController:index')->setName('login');
-})->add('\Shaarli\Front\ShaarliMiddleware');
-
$response = $app->run(true);
-// Hack to make Slim and Shaarli router work together:
-// If a Slim route isn't found and NOT API call, we call renderPage().
-if ($response->getStatusCode() == 404 && strpos($_SERVER['REQUEST_URI'], '/api/v1') === false) {
- // We use UTF-8 for proper international characters handling.
- header('Content-Type: text/html; charset=utf-8');
- renderPage($conf, $pluginManager, $linkDb, $history, $sessionManager, $loginManager);
-} else {
- $response = $response
- ->withHeader('Access-Control-Allow-Origin', '*')
- ->withHeader(
- 'Access-Control-Allow-Headers',
- 'X-Requested-With, Content-Type, Accept, Origin, Authorization'
- )
- ->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
- $app->respond($response);
-}
+$app->respond($response);
--- /dev/null
+<?php
+
+require_once __DIR__ . '/vendor/autoload.php';
+
+use Shaarli\ApplicationUtils;
+use Shaarli\Security\SessionManager;
+
+// Set 'UTC' as the default timezone if it is not defined in php.ini
+// See http://php.net/manual/en/datetime.configuration.php#ini.date.timezone
+if (date_default_timezone_get() == '') {
+ date_default_timezone_set('UTC');
+}
+
+// High execution time in case of problematic imports/exports.
+ini_set('max_input_time', '60');
+
+// Try to set max upload file size and read
+ini_set('memory_limit', '128M');
+ini_set('post_max_size', '16M');
+ini_set('upload_max_filesize', '16M');
+
+// See all error except warnings
+error_reporting(E_ALL^E_WARNING);
+
+// 3rd-party libraries
+if (! file_exists(__DIR__ . '/vendor/autoload.php')) {
+ header('Content-Type: text/plain; charset=utf-8');
+ echo "Error: missing Composer configuration\n\n"
+ ."If you installed Shaarli through Git or using the development branch,\n"
+ ."please refer to the installation documentation to install PHP"
+ ." dependencies using Composer:\n"
+ ."- https://shaarli.readthedocs.io/en/master/Server-configuration/\n"
+ ."- https://shaarli.readthedocs.io/en/master/Download-and-Installation/";
+ exit;
+}
+
+// Ensure the PHP version is supported
+try {
+ ApplicationUtils::checkPHPVersion('7.1', PHP_VERSION);
+} catch (Exception $exc) {
+ header('Content-Type: text/plain; charset=utf-8');
+ echo $exc->getMessage();
+ exit;
+}
+
+// Force cookie path (but do not change lifetime)
+$cookie = session_get_cookie_params();
+$cookiedir = '';
+if (dirname($_SERVER['SCRIPT_NAME']) != '/') {
+ $cookiedir = dirname($_SERVER["SCRIPT_NAME"]).'/';
+}
+// Set default cookie expiration and path.
+session_set_cookie_params($cookie['lifetime'], $cookiedir, $_SERVER['SERVER_NAME']);
+// Set session parameters on server side.
+// Use cookies to store session.
+ini_set('session.use_cookies', 1);
+// Force cookies for session (phpsessionID forbidden in URL).
+ini_set('session.use_only_cookies', 1);
+// Prevent PHP form using sessionID in URL if cookies are disabled.
+ini_set('session.use_trans_sid', false);
+
+define('SHAARLI_VERSION', ApplicationUtils::getVersion(__DIR__ .'/'. ApplicationUtils::$VERSION_FILE));
+
+session_name('shaarli');
+// Start session if needed (Some server auto-start sessions).
+if (session_status() == PHP_SESSION_NONE) {
+ session_start();
+}
+
+// Regenerate session ID if invalid or not defined in cookie.
+if (isset($_COOKIE['shaarli']) && !SessionManager::checkId($_COOKIE['shaarli'])) {
+ session_regenerate_id(true);
+ $_COOKIE['shaarli'] = session_id();
+}
+
+// LC_MESSAGES isn't defined without php-intl, in this case use LC_COLLATE locale instead.
+if (! defined('LC_MESSAGES')) {
+ define('LC_MESSAGES', LC_COLLATE);
+}
+
+// Prevent caching on client side or proxy: (yes, it's ugly)
+header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
+header("Cache-Control: no-store, no-cache, must-revalidate");
+header("Cache-Control: post-check=0, pre-check=0", false);
+header("Pragma: no-cache");
* Adds the addlink input on the linklist page.
*/
-use Shaarli\Router;
+use Shaarli\Render\TemplatePage;
/**
* When linklist is displayed, add play videos to header's toolbar.
*/
function hook_addlink_toolbar_render_header($data)
{
- if ($data['_PAGE_'] == Router::$PAGE_LINKLIST && $data['_LOGGEDIN_'] === true) {
+ if ($data['_PAGE_'] == TemplatePage::LINKLIST && $data['_LOGGEDIN_'] === true) {
$form = array(
'attr' => array(
'method' => 'GET',
- 'action' => '',
+ 'action' => $data['_BASE_PATH_'] . '/admin/shaare',
'name' => 'addform',
'class' => 'addform',
),
<span>
<a href="https://web.archive.org/web/%s">
- <img class="linklist-plugin-icon" src="plugins/archiveorg/internetarchive.png" title="%s" alt="archive.org" />
+ <img class="linklist-plugin-icon" src="%s/archiveorg/internetarchive.png" title="%s" alt="archive.org" />
</a>
</span>
function hook_archiveorg_render_linklist($data)
{
$archive_html = file_get_contents(PluginManager::$PLUGINS_PATH . '/archiveorg/archiveorg.html');
+ $path = ($data['_BASE_PATH_'] ?? '') . '/' . PluginManager::$PLUGINS_PATH;
foreach ($data['links'] as &$value) {
if ($value['private'] && preg_match('/^\?[a-zA-Z0-9-_@]{6}($|&|#)/', $value['real_url'])) {
continue;
}
- $archive = sprintf($archive_html, $value['url'], t('View on archive.org'));
+ $archive = sprintf($archive_html, $value['url'], $path, t('View on archive.org'));
$value['link_plugin'][] = $archive;
}
use Shaarli\Config\ConfigManager;
use Shaarli\Plugin\PluginManager;
-use Shaarli\Router;
+use Shaarli\Render\TemplatePage;
/**
* In the footer hook, there is a working example of a translation extension for Shaarli.
function hook_demo_plugin_render_header($data)
{
// Only execute when linklist is rendered.
- if ($data['_PAGE_'] == Router::$PAGE_LINKLIST) {
+ if ($data['_PAGE_'] == TemplatePage::LINKLIST) {
// If loggedin
if ($data['_LOGGEDIN_'] === true) {
/*
$form = array(
'attr' => array(
'method' => 'GET',
- 'action' => '?',
+ 'action' => $data['_BASE_PATH_'] . '/',
'class' => 'addform',
),
'inputs' => array(
function hook_demo_plugin_render_feed($data)
{
foreach ($data['links'] as &$link) {
- if ($data['_PAGE_'] == Router::$PAGE_FEED_ATOM) {
+ if ($data['_PAGE_'] == TemplatePage::FEED_ATOM) {
$link['description'] .= ' - ATOM Feed' ;
- } elseif ($data['_PAGE_'] == Router::$PAGE_FEED_RSS) {
+ } elseif ($data['_PAGE_'] == TemplatePage::FEED_RSS) {
$link['description'] .= ' - RSS Feed';
}
}
use Shaarli\Config\ConfigManager;
use Shaarli\Plugin\PluginManager;
-use Shaarli\Router;
+use Shaarli\Render\TemplatePage;
/**
* Display an error everywhere if the plugin is enabled without configuration.
*/
function hook_isso_render_includes($data)
{
- if ($data['_PAGE_'] == Router::$PAGE_LINKLIST) {
+ if ($data['_PAGE_'] == TemplatePage::LINKLIST) {
$data['css_files'][] = PluginManager::$PLUGINS_PATH . '/isso/isso.css';
}
+++ /dev/null
-<span>
- <a href="?%s#isso-thread">
- <img class="linklist-plugin-icon" src="plugins/archiveorg/internetarchive.png" title="%s" alt="archive.org" />
- </a>
-</span>
*/
use Shaarli\Plugin\PluginManager;
-use Shaarli\Router;
+use Shaarli\Render\TemplatePage;
/**
* When linklist is displayed, add play videos to header's toolbar.
*/
function hook_playvideos_render_header($data)
{
- if ($data['_PAGE_'] == Router::$PAGE_LINKLIST) {
+ if ($data['_PAGE_'] == TemplatePage::LINKLIST) {
$playvideo = array(
'attr' => array(
'href' => '#',
*/
function hook_playvideos_render_footer($data)
{
- if ($data['_PAGE_'] == Router::$PAGE_LINKLIST) {
+ if ($data['_PAGE_'] == TemplatePage::LINKLIST) {
$data['js_files'][] = PluginManager::$PLUGINS_PATH . '/playvideos/jquery-1.11.2.min.js';
$data['js_files'][] = PluginManager::$PLUGINS_PATH . '/playvideos/youtube_playlist.js';
}
use Shaarli\Config\ConfigManager;
use Shaarli\Feed\FeedBuilder;
use Shaarli\Plugin\PluginManager;
-use Shaarli\Router;
+use Shaarli\Render\TemplatePage;
/**
* Plugin init function - set the hub to the default appspot one.
*/
function hook_pubsubhubbub_render_feed($data, $conf)
{
- $feedType = $data['_PAGE_'] == Router::$PAGE_FEED_RSS ? FeedBuilder::$FEED_RSS : FeedBuilder::$FEED_ATOM;
+ $feedType = $data['_PAGE_'] == TemplatePage::FEED_RSS ? FeedBuilder::$FEED_RSS : FeedBuilder::$FEED_ATOM;
$template = file_get_contents(PluginManager::$PLUGINS_PATH . '/pubsubhubbub/hub.'. $feedType .'.xml');
$data['feed_plugins_header'][] = sprintf($template, $conf->get('plugins.PUBSUBHUB_URL'));
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';
*/
use Shaarli\Plugin\PluginManager;
-use Shaarli\Router;
+use Shaarli\Render\TemplatePage;
/**
* Add qrcode icon to link_plugin when rendering linklist.
{
$qrcode_html = file_get_contents(PluginManager::$PLUGINS_PATH . '/qrcode/qrcode.html');
+ $path = ($data['_BASE_PATH_'] ?? '') . '/' . PluginManager::$PLUGINS_PATH;
foreach ($data['links'] as &$value) {
$qrcode = sprintf(
$qrcode_html,
$value['url'],
- PluginManager::$PLUGINS_PATH
+ $path
);
$value['link_plugin'][] = $qrcode;
}
*/
function hook_qrcode_render_footer($data)
{
- if ($data['_PAGE_'] == Router::$PAGE_LINKLIST) {
- $data['js_files'][] = PluginManager::$PLUGINS_PATH . '/qrcode/shaarli-qrcode.js';
+ if ($data['_PAGE_'] == TemplatePage::LINKLIST) {
+ $data['js_files'][] = PluginManager::$PLUGINS_PATH . '/qrcode/shaarli-qrcode.js';
}
return $data;
*/
function hook_qrcode_render_includes($data)
{
- if ($data['_PAGE_'] == Router::$PAGE_LINKLIST) {
+ if ($data['_PAGE_'] == TemplatePage::LINKLIST) {
$data['css_files'][] = PluginManager::$PLUGINS_PATH . '/qrcode/qrcode.css';
}
// Show the QR-Code of a permalink (when the QR-Code icon is clicked).
function showQrCode(caller,loading)
-{
+{
// Dynamic javascript lib loading: We only load qr.js if the QR code icon is clicked:
if (typeof(qr) == 'undefined') // Load qr.js only if not present.
{
if (!loading) // If javascript lib is still loading, do not append script to body.
{
- var element = document.createElement("script");
- element.src = "plugins/qrcode/qr-1.1.3.min.js";
+ var basePath = document.querySelector('input[name="js_base_path"]').value;
+ var element = document.createElement("script");
+ element.src = basePath + "/plugins/qrcode/qr-1.1.3.min.js";
document.body.appendChild(element);
}
setTimeout(function() { showQrCode(caller,true);}, 200); // Retry in 200 milliseconds.
// Remove previous qrcode if present.
removeQrcode();
-
+
// Build the div which contains the QR-Code:
var element = document.createElement('div');
element.id = 'permalinkQrcode';
// Damn IE
element.setAttribute('onclick', 'this.parentNode.removeChild(this);' );
}
-
+
// Build the QR-Code:
var image = qr.image({size: 8,value: caller.dataset.permalink});
if (image)
- {
+ {
element.appendChild(image);
element.innerHTML += "<br>Click to close";
caller.parentNode.appendChild(element);
elem.parentNode.removeChild(elem);
}
return false;
-}
\ No newline at end of file
+}
To enable the plugin, you can either:
- * enable it in the plugins administration page (`?do=pluginadmin`).
+ * enable it in the plugins administration page (`/admin/plugins`).
* add `wallabag` to your list of enabled plugins in `data/config.json.php` (`general.enabled_plugins` section).
### Configuration
$wallabagHtml = file_get_contents(PluginManager::$PLUGINS_PATH . '/wallabag/wallabag.html');
$linkTitle = t('Save to wallabag');
+ $path = ($data['_BASE_PATH_'] ?? '') . '/' . PluginManager::$PLUGINS_PATH;
+
foreach ($data['links'] as &$value) {
$wallabag = sprintf(
$wallabagHtml,
$wallabagInstance->getWallabagUrl(),
urlencode($value['url']),
- PluginManager::$PLUGINS_PATH,
+ $path,
$linkTitle
);
$value['link_plugin'][] = $wallabag;
*/
protected $pluginManager;
- public function setUp()
+ public function setUp(): void
{
$conf = new ConfigManager('');
$this->pluginManager = new PluginManager($conf);
/**
* Test plugin loading and hook execution.
- *
- * @return void
*/
- public function testPlugin()
+ public function testPlugin(): void
{
PluginManager::$PLUGINS_PATH = self::$pluginPath;
$this->pluginManager->load(array(self::$pluginName));
$this->assertEquals('loggedin', $data[1]);
}
+ /**
+ * Test plugin loading and hook execution with an error: raise an incompatibility error.
+ */
+ public function testPluginWithPhpError(): void
+ {
+ PluginManager::$PLUGINS_PATH = self::$pluginPath;
+ $this->pluginManager->load(array(self::$pluginName));
+
+ $this->assertTrue(function_exists('hook_test_error'));
+
+ $data = [];
+ $this->pluginManager->executeHooks('error', $data);
+
+ $this->assertSame(
+ 'test [plugin incompatibility]: Class \'Unknown\' not found',
+ $this->pluginManager->getErrors()[0]
+ );
+ }
+
/**
* Test missing plugin loading.
*/
- public function testPluginNotFound()
+ public function testPluginNotFound(): void
{
$this->pluginManager->load(array());
$this->pluginManager->load(array('nope', 'renope'));
/**
* Test plugin metadata loading.
*/
- public function testGetPluginsMeta()
+ public function testGetPluginsMeta(): void
{
PluginManager::$PLUGINS_PATH = self::$pluginPath;
$this->pluginManager->load(array(self::$pluginName));
$this->assertEquals($id, $data['id']);
// Check link elements
- $this->assertEquals('http://domain.tld/?WDWyig', $data['url']);
+ $this->assertEquals('http://domain.tld/shaare/WDWyig', $data['url']);
$this->assertEquals('WDWyig', $data['shorturl']);
$this->assertEquals('Link title: @website', $data['title']);
$this->assertEquals(
// Check first element fields
$first = $data[2];
- $this->assertEquals('http://domain.tld/?WDWyig', $first['url']);
+ $this->assertEquals('http://domain.tld/shaare/WDWyig', $first['url']);
$this->assertEquals('WDWyig', $first['shorturl']);
$this->assertEquals('Link title: @website', $first['title']);
$this->assertEquals(
$this->assertEquals(self::NB_FIELDS_LINK, count($data));
$this->assertEquals(43, $data['id']);
$this->assertRegExp('/[\w_-]{6}/', $data['shorturl']);
- $this->assertEquals('http://domain.tld/?' . $data['shorturl'], $data['url']);
- $this->assertEquals('?' . $data['shorturl'], $data['title']);
+ $this->assertEquals('http://domain.tld/shaare/' . $data['shorturl'], $data['url']);
+ $this->assertEquals('/shaare/' . $data['shorturl'], $data['title']);
$this->assertEquals('', $data['description']);
$this->assertEquals([], $data['tags']);
$this->assertEquals(true, $data['private']);
$this->assertEquals(self::NB_FIELDS_LINK, count($data));
$this->assertEquals($id, $data['id']);
$this->assertEquals('WDWyig', $data['shorturl']);
- $this->assertEquals('http://domain.tld/?WDWyig', $data['url']);
- $this->assertEquals('?WDWyig', $data['title']);
+ $this->assertEquals('http://domain.tld/shaare/WDWyig', $data['url']);
+ $this->assertEquals('/shaare/WDWyig', $data['title']);
$this->assertEquals('', $data['description']);
$this->assertEquals([], $data['tags']);
$this->assertEquals(true, $data['private']);
$bookmark = $this->privateLinkDB->get(43);
$this->assertEquals(43, $bookmark->getId());
- $this->assertRegExp('/\?[\w\-]{6}/', $bookmark->getUrl());
+ $this->assertRegExp('#/shaare/[\w\-]{6}#', $bookmark->getUrl());
$this->assertRegExp('/[\w\-]{6}/', $bookmark->getShortUrl());
$this->assertEquals($bookmark->getUrl(), $bookmark->getTitle());
$this->assertEmpty($bookmark->getDescription());
$bookmark = $this->privateLinkDB->get(43);
$this->assertEquals(43, $bookmark->getId());
- $this->assertRegExp('/\?[\w\-]{6}/', $bookmark->getUrl());
+ $this->assertRegExp('#/shaare/[\w\-]{6}#', $bookmark->getUrl());
$this->assertRegExp('/[\w\-]{6}/', $bookmark->getShortUrl());
$this->assertEquals($bookmark->getUrl(), $bookmark->getTitle());
$this->assertEmpty($bookmark->getDescription());
$bookmark = $this->privateLinkDB->get(42);
$this->assertEquals(42, $bookmark->getId());
- $this->assertEquals('?WDWyig', $bookmark->getUrl());
+ $this->assertEquals('/shaare/WDWyig', $bookmark->getUrl());
$this->assertEquals('1eYJ1Q', $bookmark->getShortUrl());
$this->assertEquals('Note: I have a big ID but an old date', $bookmark->getTitle());
$this->assertEquals('Used to test bookmarks reordering.', $bookmark->getDescription());
$bookmark = $this->privateLinkDB->get(42);
$this->assertEquals(42, $bookmark->getId());
- $this->assertEquals('?WDWyig', $bookmark->getUrl());
+ $this->assertEquals('/shaare/WDWyig', $bookmark->getUrl());
$this->assertEquals('1eYJ1Q', $bookmark->getShortUrl());
$this->assertEquals('Note: I have a big ID but an old date', $bookmark->getTitle());
$this->assertEquals('Used to test bookmarks reordering.', $bookmark->getDescription());
);
$this->assertEquals(
[
- 'web' => 4,
'cartoon' => 2,
'gnu' => 1,
'dev' => 1,
);
$this->assertEquals(
[
- 'web' => 1,
'html' => 1,
'w3c' => 1,
'css' => 1,
public function testFilterHashValid()
{
$request = smallHash('20150310_114651');
- $this->assertEquals(
- 1,
- count($this->publicLinkDB->findByHash($request))
+ $this->assertSame(
+ $request,
+ $this->publicLinkDB->findByHash($request)->getShortUrl()
);
$request = smallHash('20150310_114633' . 8);
- $this->assertEquals(
- 1,
- count($this->publicLinkDB->findByHash($request))
+ $this->assertSame(
+ $request,
+ $this->publicLinkDB->findByHash($request)->getShortUrl()
);
}
/**
* Test filterHash() with an invalid smallhash.
- *
- * @expectedException \Shaarli\Bookmark\Exception\BookmarkNotFoundException
*/
public function testFilterHashInValid1()
{
+ $this->expectException(BookmarkNotFoundException::class);
+
$request = 'blabla';
$this->publicLinkDB->findByHash($request);
}
/**
* Test filterHash() with an empty smallhash.
- *
- * @expectedException \Shaarli\Bookmark\Exception\BookmarkNotFoundException
*/
public function testFilterHashInValid()
{
+ $this->expectException(BookmarkNotFoundException::class);
+
$this->publicLinkDB->findByHash('');
}
public function testCountLinkPerTagAllWithFilter()
{
$expected = [
- 'gnu' => 2,
'hashtag' => 2,
'-exclude' => 1,
'.hidden' => 1,
public function testCountLinkPerTagPublicWithFilter()
{
$expected = [
- 'gnu' => 2,
'hashtag' => 2,
'-exclude' => 1,
'.hidden' => 1,
{
$expected = [
'cartoon' => 1,
- 'dev' => 1,
'tag1' => 1,
'tag2' => 1,
'tag3' => 1,
namespace Shaarli\Bookmark;
use PHPUnit\Framework\TestCase;
-use ReferenceLinkDB;
use Shaarli\Config\ConfigManager;
use Shaarli\History;
}
/**
- * Test initialize() with an empty data store.
+ * Test initialize() with a data store containing bookmarks.
*/
- public function testInitializeEmptyDataStore()
+ public function testInitializeNotEmptyDataStore(): void
{
$refDB = new \ReferenceLinkDB();
$refDB->write(self::$testDatastore);
);
$this->assertFalse($bookmark->isPrivate());
+ $this->bookmarkService->save();
+
// Reload from file
$this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true);
$this->assertEquals($refDB->countLinks() + 2, $this->bookmarkService->count());
}
/**
- * Test initialize() with a data store containing bookmarks.
+ * Test initialize() with an a non existent datastore file .
*/
- public function testInitializeNotEmptyDataStore()
+ public function testInitializeNonExistentDataStore(): void
{
+ $this->conf->set('resource.datastore', static::$testDatastore . '_empty');
+ $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true);
+
$this->initializer->initialize();
$this->assertEquals(2, $this->bookmarkService->count());
$this->assertEquals(1, $bookmark->getId());
$this->assertEquals('abc', $bookmark->getShortUrl());
$this->assertEquals($date, $bookmark->getCreated());
- $this->assertEquals('?abc', $bookmark->getUrl());
- $this->assertEquals('?abc', $bookmark->getTitle());
+ $this->assertEquals('/shaare/abc', $bookmark->getUrl());
+ $this->assertEquals('/shaare/abc', $bookmark->getTitle());
$this->assertEquals('', $bookmark->getDescription());
$this->assertEquals([], $bookmark->getTags());
$this->assertEquals('', $bookmark->getTagsString());
namespace Shaarli\Bookmark;
use PHPUnit\Framework\TestCase;
-use ReferenceLinkDB;
-use Shaarli\Config\ConfigManager;
require_once 'tests/utils/CurlUtils.php';
*/
private function getHashtagLink($hashtag, $index = '')
{
- $hashtagLink = '<a href="' . $index . '?addtag=$1" title="Hashtag $1">#$1</a>';
+ $hashtagLink = '<a href="' . $index . './add-tag/$1" title="Hashtag $1">#$1</a>';
return str_replace('$1', $hashtag, $hashtagLink);
}
}
require_once 'application/Utils.php';
require_once 'application/http/UrlUtils.php';
require_once 'application/http/HttpUtils.php';
-require_once 'application/feed/Cache.php';
-require_once 'tests/utils/ReferenceLinkDB.php';
-require_once 'tests/utils/ReferenceHistory.php';
+require_once 'tests/container/ShaarliTestContainer.php';
+require_once 'tests/front/controller/visitor/FrontControllerMockHelper.php';
+require_once 'tests/front/controller/admin/FrontAdminControllerMockHelper.php';
+require_once 'tests/updater/DummyUpdater.php';
require_once 'tests/utils/FakeBookmarkService.php';
+require_once 'tests/utils/FakeConfigManager.php';
+require_once 'tests/utils/ReferenceHistory.php';
+require_once 'tests/utils/ReferenceLinkDB.php';
+require_once 'tests/utils/ReferenceSessionIdHashes.php';
+
+\ReferenceSessionIdHashes::genAllHashes();
namespace Shaarli\Config;
use Shaarli\Config\Exception\PluginConfigOrderException;
+use Shaarli\Plugin\PluginManager;
require_once 'application/config/ConfigPlugin.php';
*/
public function testSavePluginConfigValid()
{
- $data = array(
+ $data = [
'order_plugin1' => 2, // no plugin related
'plugin2' => 0, // new - at the end
'plugin3' => 0, // 2nd
'order_plugin3' => 8,
'plugin4' => 0, // 1st
'order_plugin4' => 5,
- );
+ ];
- $expected = array(
+ $expected = [
'plugin3',
'plugin4',
'plugin2',
- );
+ ];
+
+ mkdir($path = __DIR__ . '/folder');
+ PluginManager::$PLUGINS_PATH = $path;
+ array_map(function (string $plugin) use ($path) { touch($path . '/' . $plugin); }, $expected);
$out = save_plugin_config($data);
$this->assertEquals($expected, $out);
+
+ array_map(function (string $plugin) use ($path) { unlink($path . '/' . $plugin); }, $expected);
+ rmdir($path);
}
/**
use PHPUnit\Framework\TestCase;
use Shaarli\Bookmark\BookmarkServiceInterface;
use Shaarli\Config\ConfigManager;
+use Shaarli\Feed\FeedBuilder;
+use Shaarli\Formatter\FormatterFactory;
+use Shaarli\Front\Controller\Visitor\ErrorController;
use Shaarli\History;
+use Shaarli\Http\HttpAccess;
+use Shaarli\Netscape\NetscapeBookmarkUtils;
+use Shaarli\Plugin\PluginManager;
use Shaarli\Render\PageBuilder;
+use Shaarli\Render\PageCacheManager;
+use Shaarli\Security\CookieManager;
use Shaarli\Security\LoginManager;
use Shaarli\Security\SessionManager;
+use Shaarli\Thumbnailer;
+use Shaarli\Updater\Updater;
+use Slim\Http\Environment;
class ContainerBuilderTest extends TestCase
{
/** @var ContainerBuilder */
protected $containerBuilder;
+ /** @var CookieManager */
+ protected $cookieManager;
+
public function setUp(): void
{
$this->conf = new ConfigManager('tests/utils/config/configJson');
$this->sessionManager = $this->createMock(SessionManager::class);
+ $this->cookieManager = $this->createMock(CookieManager::class);
+
$this->loginManager = $this->createMock(LoginManager::class);
+ $this->loginManager->method('isLoggedIn')->willReturn(true);
- $this->containerBuilder = new ContainerBuilder($this->conf, $this->sessionManager, $this->loginManager);
+ $this->containerBuilder = new ContainerBuilder(
+ $this->conf,
+ $this->sessionManager,
+ $this->cookieManager,
+ $this->loginManager
+ );
}
public function testBuildContainer(): void
{
$container = $this->containerBuilder->build();
+ static::assertInstanceOf(BookmarkServiceInterface::class, $container->bookmarkService);
+ static::assertInstanceOf(CookieManager::class, $container->cookieManager);
static::assertInstanceOf(ConfigManager::class, $container->conf);
- static::assertInstanceOf(SessionManager::class, $container->sessionManager);
- static::assertInstanceOf(LoginManager::class, $container->loginManager);
+ static::assertInstanceOf(ErrorController::class, $container->errorHandler);
+ static::assertInstanceOf(Environment::class, $container->environment);
+ static::assertInstanceOf(FeedBuilder::class, $container->feedBuilder);
+ static::assertInstanceOf(FormatterFactory::class, $container->formatterFactory);
static::assertInstanceOf(History::class, $container->history);
- static::assertInstanceOf(BookmarkServiceInterface::class, $container->bookmarkService);
+ static::assertInstanceOf(HttpAccess::class, $container->httpAccess);
+ static::assertInstanceOf(LoginManager::class, $container->loginManager);
+ static::assertInstanceOf(NetscapeBookmarkUtils::class, $container->netscapeBookmarkUtils);
static::assertInstanceOf(PageBuilder::class, $container->pageBuilder);
+ static::assertInstanceOf(PageCacheManager::class, $container->pageCacheManager);
+ static::assertInstanceOf(ErrorController::class, $container->phpErrorHandler);
+ static::assertInstanceOf(PluginManager::class, $container->pluginManager);
+ static::assertInstanceOf(SessionManager::class, $container->sessionManager);
+ static::assertInstanceOf(Thumbnailer::class, $container->thumbnailer);
+ static::assertInstanceOf(Updater::class, $container->updater);
+
+ // Set by the middleware
+ static::assertNull($container->basePath);
}
}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Container;
+
+use PHPUnit\Framework\MockObject\MockObject;
+use Shaarli\Bookmark\BookmarkServiceInterface;
+use Shaarli\Config\ConfigManager;
+use Shaarli\Feed\FeedBuilder;
+use Shaarli\Formatter\FormatterFactory;
+use Shaarli\History;
+use Shaarli\Http\HttpAccess;
+use Shaarli\Plugin\PluginManager;
+use Shaarli\Render\PageBuilder;
+use Shaarli\Render\PageCacheManager;
+use Shaarli\Security\LoginManager;
+use Shaarli\Security\SessionManager;
+use Shaarli\Thumbnailer;
+
+/**
+ * Test helper allowing auto-completion for MockObjects.
+ *
+ * @property mixed[] $environment $_SERVER automatically injected by Slim
+ * @property MockObject|ConfigManager $conf
+ * @property MockObject|SessionManager $sessionManager
+ * @property MockObject|LoginManager $loginManager
+ * @property MockObject|string $webPath
+ * @property MockObject|History $history
+ * @property MockObject|BookmarkServiceInterface $bookmarkService
+ * @property MockObject|PageBuilder $pageBuilder
+ * @property MockObject|PluginManager $pluginManager
+ * @property MockObject|FormatterFactory $formatterFactory
+ * @property MockObject|PageCacheManager $pageCacheManager
+ * @property MockObject|FeedBuilder $feedBuilder
+ * @property MockObject|Thumbnailer $thumbnailer
+ * @property MockObject|HttpAccess $httpAccess
+ */
+class ShaarliTestContainer extends ShaarliContainer
+{
+
+}
{
// 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;
/**
{
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);
}
);
}
- /**
- * Test GetTypeLanguage().
- */
- public function testGetTypeLanguage()
- {
- $feedBuilder = new FeedBuilder(null, self::$formatter, FeedBuilder::$FEED_ATOM, null, null, false);
- $feedBuilder->setLocale(self::$LOCALE);
- $this->assertEquals(self::$ATOM_LANGUAGUE, $feedBuilder->getTypeLanguage());
- $feedBuilder = new FeedBuilder(null, self::$formatter, FeedBuilder::$FEED_RSS, null, null, false);
- $feedBuilder->setLocale(self::$LOCALE);
- $this->assertEquals(self::$RSS_LANGUAGE, $feedBuilder->getTypeLanguage());
- $feedBuilder = new FeedBuilder(null, self::$formatter, FeedBuilder::$FEED_ATOM, null, null, false);
- $this->assertEquals('en', $feedBuilder->getTypeLanguage());
- $feedBuilder = new FeedBuilder(null, self::$formatter, FeedBuilder::$FEED_RSS, null, null, false);
- $this->assertEquals('en-en', $feedBuilder->getTypeLanguage());
- }
-
/**
* Test buildData with RSS feed.
*/
$feedBuilder = new FeedBuilder(
self::$bookmarkService,
self::$formatter,
- FeedBuilder::$FEED_RSS,
- self::$serverInfo,
- null,
+ static::$serverInfo,
false
);
$feedBuilder->setLocale(self::$LOCALE);
- $data = $feedBuilder->buildData();
+ $data = $feedBuilder->buildData(FeedBuilder::$FEED_RSS, null);
// Test headers (RSS)
$this->assertEquals(self::$RSS_LANGUAGE, $data['language']);
$this->assertRegExp('/Wed, 03 Aug 2016 09:30:33 \+\d{4}/', $data['last_update']);
$link = $data['links'][array_keys($data['links'])[2]];
$this->assertEquals(41, $link['id']);
$this->assertEquals(DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114651'), $link['created']);
- $this->assertEquals('http://host.tld/?WDWyig', $link['guid']);
- $this->assertEquals('http://host.tld/?WDWyig', $link['url']);
+ $this->assertEquals('http://host.tld/shaare/WDWyig', $link['guid']);
+ $this->assertEquals('http://host.tld/shaare/WDWyig', $link['url']);
$this->assertRegExp('/Tue, 10 Mar 2015 11:46:51 \+\d{4}/', $link['pub_iso_date']);
$pub = DateTime::createFromFormat(DateTime::RSS, $link['pub_iso_date']);
$up = DateTime::createFromFormat(DateTime::ATOM, $link['up_iso_date']);
$this->assertEquals($pub, $up);
$this->assertContains('Stallman has a beard', $link['description']);
$this->assertContains('Permalink', $link['description']);
- $this->assertContains('http://host.tld/?WDWyig', $link['description']);
+ $this->assertContains('http://host.tld/shaare/WDWyig', $link['description']);
$this->assertEquals(1, count($link['taglist']));
$this->assertEquals('sTuff', $link['taglist'][0]);
$feedBuilder = new FeedBuilder(
self::$bookmarkService,
self::$formatter,
- FeedBuilder::$FEED_ATOM,
- self::$serverInfo,
- null,
+ static::$serverInfo,
false
);
$feedBuilder->setLocale(self::$LOCALE);
- $data = $feedBuilder->buildData();
+ $data = $feedBuilder->buildData(FeedBuilder::$FEED_ATOM, null);
$this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links']));
$this->assertRegExp('/2016-08-03T09:30:33\+\d{2}:\d{2}/', $data['last_update']);
$link = $data['links'][array_keys($data['links'])[2]];
$feedBuilder = new FeedBuilder(
self::$bookmarkService,
self::$formatter,
- FeedBuilder::$FEED_ATOM,
- self::$serverInfo,
- $criteria,
+ static::$serverInfo,
false
);
$feedBuilder->setLocale(self::$LOCALE);
- $data = $feedBuilder->buildData();
+ $data = $feedBuilder->buildData(FeedBuilder::$FEED_ATOM, $criteria);
$this->assertEquals(1, count($data['links']));
$link = array_shift($data['links']);
$this->assertEquals(41, $link['id']);
$feedBuilder = new FeedBuilder(
self::$bookmarkService,
self::$formatter,
- FeedBuilder::$FEED_ATOM,
- self::$serverInfo,
- $criteria,
+ static::$serverInfo,
false
);
$feedBuilder->setLocale(self::$LOCALE);
- $data = $feedBuilder->buildData();
+ $data = $feedBuilder->buildData(FeedBuilder::$FEED_ATOM, $criteria);
$this->assertEquals(3, count($data['links']));
$link = $data['links'][array_keys($data['links'])[2]];
$this->assertEquals(41, $link['id']);
$feedBuilder = new FeedBuilder(
self::$bookmarkService,
self::$formatter,
- FeedBuilder::$FEED_ATOM,
- self::$serverInfo,
- null,
+ static::$serverInfo,
false
);
$feedBuilder->setLocale(self::$LOCALE);
$feedBuilder->setUsePermalinks(true);
- $data = $feedBuilder->buildData();
+ $data = $feedBuilder->buildData(FeedBuilder::$FEED_ATOM, null);
$this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links']));
$this->assertTrue($data['usepermalinks']);
// First link is a permalink
$link = $data['links'][array_keys($data['links'])[2]];
$this->assertEquals(41, $link['id']);
$this->assertEquals(DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114651'), $link['created']);
- $this->assertEquals('http://host.tld/?WDWyig', $link['guid']);
- $this->assertEquals('http://host.tld/?WDWyig', $link['url']);
+ $this->assertEquals('http://host.tld/shaare/WDWyig', $link['guid']);
+ $this->assertEquals('http://host.tld/shaare/WDWyig', $link['url']);
$this->assertContains('Direct link', $link['description']);
- $this->assertContains('http://host.tld/?WDWyig', $link['description']);
+ $this->assertContains('http://host.tld/shaare/WDWyig', $link['description']);
// Second link is a direct link
$link = $data['links'][array_keys($data['links'])[3]];
$this->assertEquals(8, $link['id']);
$this->assertEquals(DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114633'), $link['created']);
- $this->assertEquals('http://host.tld/?RttfEw', $link['guid']);
+ $this->assertEquals('http://host.tld/shaare/RttfEw', $link['guid']);
$this->assertEquals('https://static.fsf.org/nosvn/faif-2.0.pdf', $link['url']);
$this->assertContains('Direct link', $link['description']);
$this->assertContains('https://static.fsf.org/nosvn/faif-2.0.pdf', $link['description']);
$feedBuilder = new FeedBuilder(
self::$bookmarkService,
self::$formatter,
- FeedBuilder::$FEED_ATOM,
- self::$serverInfo,
- null,
+ static::$serverInfo,
false
);
$feedBuilder->setLocale(self::$LOCALE);
$feedBuilder->setHideDates(true);
- $data = $feedBuilder->buildData();
+ $data = $feedBuilder->buildData(FeedBuilder::$FEED_ATOM, null);
$this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links']));
$this->assertFalse($data['show_dates']);
$feedBuilder = new FeedBuilder(
self::$bookmarkService,
self::$formatter,
- FeedBuilder::$FEED_ATOM,
- self::$serverInfo,
- null,
+ static::$serverInfo,
true
);
$feedBuilder->setLocale(self::$LOCALE);
$feedBuilder->setHideDates(true);
- $data = $feedBuilder->buildData();
+ $data = $feedBuilder->buildData(FeedBuilder::$FEED_ATOM, null);
$this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links']));
$this->assertTrue($data['show_dates']);
}
$feedBuilder = new FeedBuilder(
self::$bookmarkService,
self::$formatter,
- FeedBuilder::$FEED_ATOM,
$serverInfo,
- null,
false
);
$feedBuilder->setLocale(self::$LOCALE);
- $data = $feedBuilder->buildData();
+ $data = $feedBuilder->buildData(FeedBuilder::$FEED_ATOM, null);
$this->assertEquals(
'http://host.tld:8080/~user/shaarli/index.php?do=feed',
// Test first link (note link)
$link = $data['links'][array_keys($data['links'])[2]];
- $this->assertEquals('http://host.tld:8080/~user/shaarli/?WDWyig', $link['guid']);
- $this->assertEquals('http://host.tld:8080/~user/shaarli/?WDWyig', $link['url']);
- $this->assertContains('http://host.tld:8080/~user/shaarli/?addtag=hashtag', $link['description']);
+ $this->assertEquals('http://host.tld:8080/~user/shaarli/shaare/WDWyig', $link['guid']);
+ $this->assertEquals('http://host.tld:8080/~user/shaarli/shaare/WDWyig', $link['url']);
+ $this->assertContains('http://host.tld:8080/~user/shaarli/./add-tag/hashtag', $link['description']);
}
}
$description[0] = 'This a <strong>description</strong><br />';
$url = 'https://sub.domain.tld?query=here&for=real#hash';
$description[1] = 'text <a href="'. $url .'">'. $url .'</a> more text<br />';
- $description[2] = 'Also, there is an <a href="?addtag=hashtag" '.
+ $description[2] = 'Also, there is an <a href="./add-tag/hashtag" '.
'title="Hashtag hashtag">#hashtag</a> added<br />';
$description[3] = ' A N D KEEP '.
'SPACES ! <br />';
$this->assertEquals($root . $short, $link['url']);
$this->assertEquals($root . $short, $link['real_url']);
$this->assertEquals(
- 'Text <a href="'. $root .'?addtag=hashtag" title="Hashtag hashtag">'.
+ 'Text <a href="'. $root .'./add-tag/hashtag" title="Hashtag hashtag">'.
'#hashtag</a> more text',
$link['description']
);
$description .= 'This a <strong>description</strong><br />'. PHP_EOL;
$url = 'https://sub.domain.tld?query=here&for=real#hash';
$description .= 'text <a href="'. $url .'">'. $url .'</a> more text<br />'. PHP_EOL;
- $description .= 'Also, there is an <a href="?addtag=hashtag">#hashtag</a> added<br />'. PHP_EOL;
+ $description .= 'Also, there is an <a href="./add-tag/hashtag">#hashtag</a> added<br />'. PHP_EOL;
$description .= 'A N D KEEP SPACES ! ';
$description .= '</p></div>';
$this->formatter->addContextData('index_url', $root = 'https://domain.tld/hithere/');
$description = '<div class="markdown"><p>';
- $description .= 'Text <a href="'. $root .'?addtag=hashtag">#hashtag</a> more text';
+ $description .= 'Text <a href="'. $root .'./add-tag/hashtag">#hashtag</a> more text';
$description .= '</p></div>';
$link = $this->formatter->format($bookmark);
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front;
+
+use PHPUnit\Framework\TestCase;
+use Shaarli\Config\ConfigManager;
+use Shaarli\Container\ShaarliContainer;
+use Shaarli\Security\LoginManager;
+use Shaarli\Updater\Updater;
+use Slim\Http\Request;
+use Slim\Http\Response;
+use Slim\Http\Uri;
+
+class ShaarliAdminMiddlewareTest extends TestCase
+{
+ protected const TMP_MOCK_FILE = '.tmp';
+
+ /** @var ShaarliContainer */
+ protected $container;
+
+ /** @var ShaarliMiddleware */
+ protected $middleware;
+
+ public function setUp(): void
+ {
+ $this->container = $this->createMock(ShaarliContainer::class);
+
+ touch(static::TMP_MOCK_FILE);
+
+ $this->container->conf = $this->createMock(ConfigManager::class);
+ $this->container->conf->method('getConfigFileExt')->willReturn(static::TMP_MOCK_FILE);
+
+ $this->container->loginManager = $this->createMock(LoginManager::class);
+ $this->container->updater = $this->createMock(Updater::class);
+
+ $this->container->environment = ['REQUEST_URI' => 'http://shaarli/subfolder/path'];
+
+ $this->middleware = new ShaarliAdminMiddleware($this->container);
+ }
+
+ public function tearDown(): void
+ {
+ unlink(static::TMP_MOCK_FILE);
+ }
+
+ /**
+ * Try to access an admin controller while logged out -> redirected to login page.
+ */
+ public function testMiddlewareWhileLoggedOut(): void
+ {
+ $this->container->loginManager->expects(static::once())->method('isLoggedIn')->willReturn(false);
+
+ $request = $this->createMock(Request::class);
+ $request->method('getUri')->willReturnCallback(function (): Uri {
+ $uri = $this->createMock(Uri::class);
+ $uri->method('getBasePath')->willReturn('/subfolder');
+
+ return $uri;
+ });
+
+ $response = new Response();
+
+ /** @var Response $result */
+ $result = $this->middleware->__invoke($request, $response, function () {});
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(
+ '/subfolder/login?returnurl=' . urlencode('http://shaarli/subfolder/path'),
+ $result->getHeader('location')[0]
+ );
+ }
+
+ /**
+ * Process controller while logged in.
+ */
+ public function testMiddlewareWhileLoggedIn(): void
+ {
+ $this->container->loginManager->method('isLoggedIn')->willReturn(true);
+
+ $request = $this->createMock(Request::class);
+ $request->method('getUri')->willReturnCallback(function (): Uri {
+ $uri = $this->createMock(Uri::class);
+ $uri->method('getBasePath')->willReturn('/subfolder');
+
+ return $uri;
+ });
+
+ $response = new Response();
+ $controller = function (Request $request, Response $response): Response {
+ return $response->withStatus(418); // I'm a tea pot
+ };
+
+ /** @var Response $result */
+ $result = $this->middleware->__invoke($request, $response, $controller);
+
+ static::assertSame(418, $result->getStatusCode());
+ }
+}
use Shaarli\Config\ConfigManager;
use Shaarli\Container\ShaarliContainer;
use Shaarli\Front\Exception\LoginBannedException;
+use Shaarli\Front\Exception\UnauthorizedException;
use Shaarli\Render\PageBuilder;
+use Shaarli\Render\PageCacheManager;
+use Shaarli\Security\LoginManager;
+use Shaarli\Updater\Updater;
use Slim\Http\Request;
use Slim\Http\Response;
+use Slim\Http\Uri;
class ShaarliMiddlewareTest extends TestCase
{
+ protected const TMP_MOCK_FILE = '.tmp';
+
/** @var ShaarliContainer */
protected $container;
public function setUp(): void
{
$this->container = $this->createMock(ShaarliContainer::class);
+
+ touch(static::TMP_MOCK_FILE);
+
+ $this->container->conf = $this->createMock(ConfigManager::class);
+ $this->container->conf->method('getConfigFileExt')->willReturn(static::TMP_MOCK_FILE);
+
+ $this->container->loginManager = $this->createMock(LoginManager::class);
+
+ $this->container->environment = ['REQUEST_URI' => 'http://shaarli/subfolder/path'];
+
$this->middleware = new ShaarliMiddleware($this->container);
}
+ public function tearDown(): void
+ {
+ unlink(static::TMP_MOCK_FILE);
+ }
+
+ /**
+ * Test middleware execution with valid controller call
+ */
public function testMiddlewareExecution(): void
{
$request = $this->createMock(Request::class);
+ $request->method('getUri')->willReturnCallback(function (): Uri {
+ $uri = $this->createMock(Uri::class);
+ $uri->method('getBasePath')->willReturn('/subfolder');
+
+ return $uri;
+ });
+
$response = new Response();
$controller = function (Request $request, Response $response): Response {
return $response->withStatus(418); // I'm a tea pot
static::assertSame(418, $result->getStatusCode());
}
- public function testMiddlewareExecutionWithException(): void
+ /**
+ * Test middleware execution with controller throwing a known front exception.
+ * The exception should be thrown to be later handled by the error handler.
+ */
+ public function testMiddlewareExecutionWithFrontException(): void
{
$request = $this->createMock(Request::class);
+ $request->method('getUri')->willReturnCallback(function (): Uri {
+ $uri = $this->createMock(Uri::class);
+ $uri->method('getBasePath')->willReturn('/subfolder');
+
+ return $uri;
+ });
+
$response = new Response();
$controller = function (): void {
$exception = new LoginBannedException();
});
$this->container->pageBuilder = $pageBuilder;
- $conf = $this->createMock(ConfigManager::class);
- $this->container->conf = $conf;
+ $this->expectException(LoginBannedException::class);
+
+ $this->middleware->__invoke($request, $response, $controller);
+ }
+
+ /**
+ * Test middleware execution with controller throwing a not authorized exception
+ * The middle should send a redirection response to the login page.
+ */
+ public function testMiddlewareExecutionWithUnauthorizedException(): void
+ {
+ $request = $this->createMock(Request::class);
+ $request->method('getUri')->willReturnCallback(function (): Uri {
+ $uri = $this->createMock(Uri::class);
+ $uri->method('getBasePath')->willReturn('/subfolder');
+
+ return $uri;
+ });
+
+ $response = new Response();
+ $controller = function (): void {
+ throw new UnauthorizedException();
+ };
+
+ /** @var Response $result */
+ $result = $this->middleware->__invoke($request, $response, $controller);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(
+ '/subfolder/login?returnurl=' . urlencode('http://shaarli/subfolder/path'),
+ $result->getHeader('location')[0]
+ );
+ }
+
+ /**
+ * Test middleware execution with controller throwing a not authorized exception.
+ * The exception should be thrown to be later handled by the error handler.
+ */
+ public function testMiddlewareExecutionWithServerException(): void
+ {
+ $request = $this->createMock(Request::class);
+ $request->method('getUri')->willReturnCallback(function (): Uri {
+ $uri = $this->createMock(Uri::class);
+ $uri->method('getBasePath')->willReturn('/subfolder');
+
+ return $uri;
+ });
+
+ $dummyException = new class() extends \Exception {};
+
+ $response = new Response();
+ $controller = function () use ($dummyException): void {
+ throw $dummyException;
+ };
+
+ $parameters = [];
+ $this->container->pageBuilder = $this->createMock(PageBuilder::class);
+ $this->container->pageBuilder->method('render')->willReturnCallback(function (string $message): string {
+ return $message;
+ });
+ $this->container->pageBuilder
+ ->method('assign')
+ ->willReturnCallback(function (string $key, string $value) use (&$parameters): void {
+ $parameters[$key] = $value;
+ })
+ ;
+
+ $this->expectException(get_class($dummyException));
+
+ $this->middleware->__invoke($request, $response, $controller);
+ }
+
+ public function testMiddlewareExecutionWithUpdates(): void
+ {
+ $request = $this->createMock(Request::class);
+ $request->method('getUri')->willReturnCallback(function (): Uri {
+ $uri = $this->createMock(Uri::class);
+ $uri->method('getBasePath')->willReturn('/subfolder');
+
+ return $uri;
+ });
+
+ $response = new Response();
+ $controller = function (Request $request, Response $response): Response {
+ return $response->withStatus(418); // I'm a tea pot
+ };
+
+ $this->container->loginManager = $this->createMock(LoginManager::class);
+ $this->container->loginManager->method('isLoggedIn')->willReturn(true);
+
+ $this->container->conf = $this->createMock(ConfigManager::class);
+ $this->container->conf->method('get')->willReturnCallback(function (string $key): string {
+ return $key;
+ });
+ $this->container->conf->method('getConfigFileExt')->willReturn(static::TMP_MOCK_FILE);
+
+ $this->container->pageCacheManager = $this->createMock(PageCacheManager::class);
+ $this->container->pageCacheManager->expects(static::once())->method('invalidateCaches');
+
+ $this->container->updater = $this->createMock(Updater::class);
+ $this->container->updater
+ ->expects(static::once())
+ ->method('update')
+ ->willReturn(['update123'])
+ ;
+ $this->container->updater->method('getDoneUpdates')->willReturn($updates = ['update123', 'other']);
+ $this->container->updater
+ ->expects(static::once())
+ ->method('writeUpdates')
+ ->with('resource.updates', $updates)
+ ;
/** @var Response $result */
$result = $this->middleware->__invoke($request, $response, $controller);
static::assertInstanceOf(Response::class, $result);
- static::assertSame(401, $result->getStatusCode());
- static::assertContains('error', (string) $result->getBody());
+ static::assertSame(418, $result->getStatusCode());
}
}
+++ /dev/null
-<?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\Front\Exception\LoginBannedException;
-use Shaarli\Plugin\PluginManager;
-use Shaarli\Render\PageBuilder;
-use Shaarli\Security\LoginManager;
-use Slim\Http\Request;
-use Slim\Http\Response;
-
-class LoginControllerTest extends TestCase
-{
- /** @var ShaarliContainer */
- protected $container;
-
- /** @var LoginController */
- protected $controller;
-
- public function setUp(): void
- {
- $this->container = $this->createMock(ShaarliContainer::class);
- $this->controller = new LoginController($this->container);
- }
-
- public function testValidControllerInvoke(): void
- {
- $this->createValidContainerMockSet();
-
- $request = $this->createMock(Request::class);
- $request->expects(static::once())->method('getServerParam')->willReturn('> referer');
- $response = new Response();
-
- $assignedVariables = [];
- $this->container->pageBuilder
- ->method('assign')
- ->willReturnCallback(function ($key, $value) use (&$assignedVariables) {
- $assignedVariables[$key] = $value;
-
- return $this;
- })
- ;
-
- $result = $this->controller->index($request, $response);
-
- static::assertInstanceOf(Response::class, $result);
- static::assertSame(200, $result->getStatusCode());
- static::assertSame('loginform', (string) $result->getBody());
-
- static::assertSame('> referer', $assignedVariables['returnurl']);
- static::assertSame(true, $assignedVariables['remember_user_default']);
- static::assertSame('Login - Shaarli', $assignedVariables['pagetitle']);
- }
-
- public function testValidControllerInvokeWithUserName(): void
- {
- $this->createValidContainerMockSet();
-
- $request = $this->createMock(Request::class);
- $request->expects(static::once())->method('getServerParam')->willReturn('> referer');
- $request->expects(static::exactly(2))->method('getParam')->willReturn('myUser>');
- $response = new Response();
-
- $assignedVariables = [];
- $this->container->pageBuilder
- ->method('assign')
- ->willReturnCallback(function ($key, $value) use (&$assignedVariables) {
- $assignedVariables[$key] = $value;
-
- return $this;
- })
- ;
-
- $result = $this->controller->index($request, $response);
-
- static::assertInstanceOf(Response::class, $result);
- static::assertSame(200, $result->getStatusCode());
- static::assertSame('loginform', (string) $result->getBody());
-
- static::assertSame('myUser>', $assignedVariables['username']);
- static::assertSame('> referer', $assignedVariables['returnurl']);
- static::assertSame(true, $assignedVariables['remember_user_default']);
- static::assertSame('Login - Shaarli', $assignedVariables['pagetitle']);
- }
-
- public function testLoginControllerWhileLoggedIn(): void
- {
- $request = $this->createMock(Request::class);
- $response = new Response();
-
- $loginManager = $this->createMock(LoginManager::class);
- $loginManager->expects(static::once())->method('isLoggedIn')->willReturn(true);
- $this->container->loginManager = $loginManager;
-
- $result = $this->controller->index($request, $response);
-
- static::assertInstanceOf(Response::class, $result);
- static::assertSame(302, $result->getStatusCode());
- static::assertSame(['./'], $result->getHeader('Location'));
- }
-
- public function testLoginControllerOpenShaarli(): void
- {
- $this->createValidContainerMockSet();
-
- $request = $this->createMock(Request::class);
- $response = new Response();
-
- $conf = $this->createMock(ConfigManager::class);
- $conf->method('get')->willReturnCallback(function (string $parameter, $default) {
- if ($parameter === 'security.open_shaarli') {
- return true;
- }
- return $default;
- });
- $this->container->conf = $conf;
-
- $result = $this->controller->index($request, $response);
-
- static::assertInstanceOf(Response::class, $result);
- static::assertSame(302, $result->getStatusCode());
- static::assertSame(['./'], $result->getHeader('Location'));
- }
-
- public function testLoginControllerWhileBanned(): void
- {
- $this->createValidContainerMockSet();
-
- $request = $this->createMock(Request::class);
- $response = new Response();
-
- $loginManager = $this->createMock(LoginManager::class);
- $loginManager->method('isLoggedIn')->willReturn(false);
- $loginManager->method('canLogin')->willReturn(false);
- $this->container->loginManager = $loginManager;
-
- $this->expectException(LoginBannedException::class);
-
- $this->controller->index($request, $response);
- }
-
- protected function createValidContainerMockSet(): void
- {
- // User logged out
- $loginManager = $this->createMock(LoginManager::class);
- $loginManager->method('isLoggedIn')->willReturn(false);
- $loginManager->method('canLogin')->willReturn(true);
- $this->container->loginManager = $loginManager;
-
- // Config
- $conf = $this->createMock(ConfigManager::class);
- $conf->method('get')->willReturnCallback(function (string $parameter, $default) {
- return $default;
- });
- $this->container->conf = $conf;
-
- // PageBuilder
- $pageBuilder = $this->createMock(PageBuilder::class);
- $pageBuilder
- ->method('render')
- ->willReturnCallback(function (string $template): string {
- return $template;
- })
- ;
- $this->container->pageBuilder = $pageBuilder;
-
- $pluginManager = $this->createMock(PluginManager::class);
- $this->container->pluginManager = $pluginManager;
- $bookmarkService = $this->createMock(BookmarkServiceInterface::class);
- $this->container->bookmarkService = $bookmarkService;
- }
-}
+++ /dev/null
-<?php
-
-declare(strict_types=1);
-
-namespace Shaarli\Front\Controller;
-
-use PHPUnit\Framework\TestCase;
-use Shaarli\Bookmark\BookmarkFilter;
-use Shaarli\Bookmark\BookmarkServiceInterface;
-use Shaarli\Container\ShaarliContainer;
-use Shaarli\Plugin\PluginManager;
-use Shaarli\Render\PageBuilder;
-use Shaarli\Security\LoginManager;
-
-/**
- * Class ShaarliControllerTest
- *
- * This class is used to test default behavior of ShaarliController abstract class.
- * It uses a dummy non abstract controller.
- */
-class ShaarliControllerTest extends TestCase
-{
- /** @var ShaarliContainer */
- protected $container;
-
- /** @var LoginController */
- protected $controller;
-
- /** @var mixed[] List of variable assigned to the template */
- protected $assignedValues;
-
- public function setUp(): void
- {
- $this->container = $this->createMock(ShaarliContainer::class);
- $this->controller = new class($this->container) extends ShaarliController
- {
- public function assignView(string $key, $value): ShaarliController
- {
- return parent::assignView($key, $value);
- }
-
- public function render(string $template): string
- {
- return parent::render($template);
- }
- };
- $this->assignedValues = [];
- }
-
- public function testAssignView(): void
- {
- $this->createValidContainerMockSet();
-
- $self = $this->controller->assignView('variableName', 'variableValue');
-
- static::assertInstanceOf(ShaarliController::class, $self);
- static::assertSame('variableValue', $this->assignedValues['variableName']);
- }
-
- public function testRender(): void
- {
- $this->createValidContainerMockSet();
-
- $render = $this->controller->render('templateName');
-
- static::assertSame('templateName', $render);
-
- static::assertSame(10, $this->assignedValues['linkcount']);
- static::assertSame(5, $this->assignedValues['privateLinkcount']);
- static::assertSame(['error'], $this->assignedValues['plugin_errors']);
-
- static::assertSame('templateName', $this->assignedValues['plugins_includes']['render_includes']['target']);
- static::assertTrue($this->assignedValues['plugins_includes']['render_includes']['loggedin']);
- static::assertSame('templateName', $this->assignedValues['plugins_header']['render_header']['target']);
- static::assertTrue($this->assignedValues['plugins_header']['render_header']['loggedin']);
- static::assertSame('templateName', $this->assignedValues['plugins_footer']['render_footer']['target']);
- static::assertTrue($this->assignedValues['plugins_footer']['render_footer']['loggedin']);
- }
-
- protected function createValidContainerMockSet(): void
- {
- $pageBuilder = $this->createMock(PageBuilder::class);
- $pageBuilder
- ->method('assign')
- ->willReturnCallback(function (string $key, $value): void {
- $this->assignedValues[$key] = $value;
- });
- $pageBuilder
- ->method('render')
- ->willReturnCallback(function (string $template): string {
- return $template;
- });
- $this->container->pageBuilder = $pageBuilder;
-
- $bookmarkService = $this->createMock(BookmarkServiceInterface::class);
- $bookmarkService
- ->method('count')
- ->willReturnCallback(function (string $visibility): int {
- return $visibility === BookmarkFilter::$PRIVATE ? 5 : 10;
- });
- $this->container->bookmarkService = $bookmarkService;
-
- $pluginManager = $this->createMock(PluginManager::class);
- $pluginManager
- ->method('executeHooks')
- ->willReturnCallback(function (string $hook, array &$data, array $params): array {
- return $data[$hook] = $params;
- });
- $pluginManager->method('getErrors')->willReturn(['error']);
- $this->container->pluginManager = $pluginManager;
-
- $loginManager = $this->createMock(LoginManager::class);
- $loginManager->method('isLoggedIn')->willReturn(true);
- $this->container->loginManager = $loginManager;
- }
-}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Admin;
+
+use PHPUnit\Framework\TestCase;
+use Shaarli\Config\ConfigManager;
+use Shaarli\Front\Exception\WrongTokenException;
+use Shaarli\Security\SessionManager;
+use Shaarli\Thumbnailer;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+class ConfigureControllerTest extends TestCase
+{
+ use FrontAdminControllerMockHelper;
+
+ /** @var ConfigureController */
+ protected $controller;
+
+ public function setUp(): void
+ {
+ $this->createContainer();
+
+ $this->controller = new ConfigureController($this->container);
+ }
+
+ /**
+ * Test displaying configure page - it should display all config variables
+ */
+ public function testIndex(): void
+ {
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->container->conf = $this->createMock(ConfigManager::class);
+ $this->container->conf->method('get')->willReturnCallback(function (string $key) {
+ return $key;
+ });
+
+ $result = $this->controller->index($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('configure', (string) $result->getBody());
+
+ static::assertSame('Configure - general.title', $assignedVariables['pagetitle']);
+ static::assertSame('general.title', $assignedVariables['title']);
+ static::assertSame('resource.theme', $assignedVariables['theme']);
+ static::assertEmpty($assignedVariables['theme_available']);
+ static::assertSame(['default', 'markdown'], $assignedVariables['formatter_available']);
+ static::assertNotEmpty($assignedVariables['continents']);
+ static::assertNotEmpty($assignedVariables['cities']);
+ static::assertSame('general.retrieve_description', $assignedVariables['retrieve_description']);
+ static::assertSame('privacy.default_private_links', $assignedVariables['private_links_default']);
+ static::assertSame('security.session_protection_disabled', $assignedVariables['session_protection_disabled']);
+ static::assertSame('feed.rss_permalinks', $assignedVariables['enable_rss_permalinks']);
+ static::assertSame('updates.check_updates', $assignedVariables['enable_update_check']);
+ static::assertSame('privacy.hide_public_links', $assignedVariables['hide_public_links']);
+ static::assertSame('api.enabled', $assignedVariables['api_enabled']);
+ static::assertSame('api.secret', $assignedVariables['api_secret']);
+ static::assertCount(4, $assignedVariables['languages']);
+ static::assertArrayHasKey('gd_enabled', $assignedVariables);
+ static::assertSame('thumbnails.mode', $assignedVariables['thumbnails_mode']);
+ }
+
+ /**
+ * Test posting a new config - make sure that everything is saved properly, without errors.
+ */
+ public function testSaveNewConfig(): void
+ {
+ $session = [];
+ $this->assignSessionVars($session);
+
+ $parameters = [
+ 'token' => 'token',
+ 'continent' => 'Europe',
+ 'city' => 'Moscow',
+ 'title' => 'Shaarli',
+ 'titleLink' => './',
+ 'retrieveDescription' => 'on',
+ 'theme' => 'vintage',
+ 'disablesessionprotection' => null,
+ 'privateLinkByDefault' => true,
+ 'enableRssPermalinks' => true,
+ 'updateCheck' => false,
+ 'hidePublicLinks' => 'on',
+ 'enableApi' => 'on',
+ 'apiSecret' => 'abcdef',
+ 'formatter' => 'markdown',
+ 'language' => 'fr',
+ 'enableThumbnails' => Thumbnailer::MODE_NONE,
+ ];
+
+ $parametersConfigMapping = [
+ 'general.timezone' => $parameters['continent'] . '/' . $parameters['city'],
+ 'general.title' => $parameters['title'],
+ 'general.header_link' => $parameters['titleLink'],
+ 'general.retrieve_description' => !!$parameters['retrieveDescription'],
+ 'resource.theme' => $parameters['theme'],
+ 'security.session_protection_disabled' => !!$parameters['disablesessionprotection'],
+ 'privacy.default_private_links' => !!$parameters['privateLinkByDefault'],
+ 'feed.rss_permalinks' => !!$parameters['enableRssPermalinks'],
+ 'updates.check_updates' => !!$parameters['updateCheck'],
+ 'privacy.hide_public_links' => !!$parameters['hidePublicLinks'],
+ 'api.enabled' => !!$parameters['enableApi'],
+ 'api.secret' => $parameters['apiSecret'],
+ 'formatter' => $parameters['formatter'],
+ 'translation.language' => $parameters['language'],
+ 'thumbnails.mode' => $parameters['enableThumbnails'],
+ ];
+
+ $request = $this->createMock(Request::class);
+ $request
+ ->expects(static::atLeastOnce())
+ ->method('getParam')->willReturnCallback(function (string $key) use ($parameters) {
+ if (false === array_key_exists($key, $parameters)) {
+ static::fail('unknown key: ' . $key);
+ }
+
+ return $parameters[$key];
+ }
+ );
+
+ $response = new Response();
+
+ $this->container->conf = $this->createMock(ConfigManager::class);
+ $this->container->conf
+ ->expects(static::atLeastOnce())
+ ->method('set')
+ ->willReturnCallback(function (string $key, $value) use ($parametersConfigMapping): void {
+ if (false === array_key_exists($key, $parametersConfigMapping)) {
+ static::fail('unknown key: ' . $key);
+ }
+
+ static::assertSame($parametersConfigMapping[$key], $value);
+ }
+ );
+
+ $result = $this->controller->save($request, $response);
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/admin/configure'], $result->getHeader('Location'));
+
+ static::assertArrayNotHasKey(SessionManager::KEY_WARNING_MESSAGES, $session);
+ static::assertArrayNotHasKey(SessionManager::KEY_ERROR_MESSAGES, $session);
+ static::assertArrayHasKey(SessionManager::KEY_SUCCESS_MESSAGES, $session);
+ static::assertSame(['Configuration was saved.'], $session[SessionManager::KEY_SUCCESS_MESSAGES]);
+ }
+
+ /**
+ * Test posting a new config - wrong token.
+ */
+ public function testSaveNewConfigWrongToken(): void
+ {
+ $this->container->sessionManager = $this->createMock(SessionManager::class);
+ $this->container->sessionManager->method('checkToken')->willReturn(false);
+
+ $this->container->conf->expects(static::never())->method('set');
+ $this->container->conf->expects(static::never())->method('write');
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->expectException(WrongTokenException::class);
+
+ $this->controller->save($request, $response);
+ }
+
+ /**
+ * Test posting a new config - thumbnail activation.
+ */
+ public function testSaveNewConfigThumbnailsActivation(): void
+ {
+ $session = [];
+ $this->assignSessionVars($session);
+
+ $request = $this->createMock(Request::class);
+ $request
+ ->expects(static::atLeastOnce())
+ ->method('getParam')->willReturnCallback(function (string $key) {
+ if ('enableThumbnails' === $key) {
+ return Thumbnailer::MODE_ALL;
+ }
+
+ return $key;
+ })
+ ;
+ $response = new Response();
+
+ $result = $this->controller->save($request, $response);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/admin/configure'], $result->getHeader('Location'));
+
+ static::assertArrayNotHasKey(SessionManager::KEY_ERROR_MESSAGES, $session);
+ static::assertArrayHasKey(SessionManager::KEY_WARNING_MESSAGES, $session);
+ static::assertStringContainsString(
+ 'You have enabled or changed thumbnails mode',
+ $session[SessionManager::KEY_WARNING_MESSAGES][0]
+ );
+ static::assertArrayHasKey(SessionManager::KEY_SUCCESS_MESSAGES, $session);
+ static::assertSame(['Configuration was saved.'], $session[SessionManager::KEY_SUCCESS_MESSAGES]);
+ }
+
+ /**
+ * Test posting a new config - thumbnail activation.
+ */
+ public function testSaveNewConfigThumbnailsAlreadyActive(): void
+ {
+ $session = [];
+ $this->assignSessionVars($session);
+
+ $request = $this->createMock(Request::class);
+ $request
+ ->expects(static::atLeastOnce())
+ ->method('getParam')->willReturnCallback(function (string $key) {
+ if ('enableThumbnails' === $key) {
+ return Thumbnailer::MODE_ALL;
+ }
+
+ return $key;
+ })
+ ;
+ $response = new Response();
+
+ $this->container->conf = $this->createMock(ConfigManager::class);
+ $this->container->conf
+ ->expects(static::atLeastOnce())
+ ->method('get')
+ ->willReturnCallback(function (string $key): string {
+ if ('thumbnails.mode' === $key) {
+ return Thumbnailer::MODE_ALL;
+ }
+
+ return $key;
+ })
+ ;
+
+ $result = $this->controller->save($request, $response);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/admin/configure'], $result->getHeader('Location'));
+
+ static::assertArrayNotHasKey(SessionManager::KEY_ERROR_MESSAGES, $session);
+ static::assertArrayNotHasKey(SessionManager::KEY_WARNING_MESSAGES, $session);
+ static::assertArrayHasKey(SessionManager::KEY_SUCCESS_MESSAGES, $session);
+ static::assertSame(['Configuration was saved.'], $session[SessionManager::KEY_SUCCESS_MESSAGES]);
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Admin;
+
+use PHPUnit\Framework\TestCase;
+use Shaarli\Bookmark\Bookmark;
+use Shaarli\Formatter\BookmarkFormatter;
+use Shaarli\Formatter\BookmarkRawFormatter;
+use Shaarli\Netscape\NetscapeBookmarkUtils;
+use Shaarli\Security\SessionManager;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+class ExportControllerTest extends TestCase
+{
+ use FrontAdminControllerMockHelper;
+
+ /** @var ExportController */
+ protected $controller;
+
+ public function setUp(): void
+ {
+ $this->createContainer();
+
+ $this->controller = new ExportController($this->container);
+ }
+
+ /**
+ * Test displaying export page
+ */
+ public function testIndex(): void
+ {
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $result = $this->controller->index($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('export', (string) $result->getBody());
+
+ static::assertSame('Export - Shaarli', $assignedVariables['pagetitle']);
+ }
+
+ /**
+ * Test posting an export request
+ */
+ public function testExportDefault(): void
+ {
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ $parameters = [
+ 'selection' => 'all',
+ 'prepend_note_url' => 'on',
+ ];
+
+ $request = $this->createMock(Request::class);
+ $request->method('getParam')->willReturnCallback(function (string $key) use ($parameters) {
+ return $parameters[$key] ?? null;
+ });
+ $response = new Response();
+
+ $bookmarks = [
+ (new Bookmark())->setUrl('http://link1.tld')->setTitle('Title 1'),
+ (new Bookmark())->setUrl('http://link2.tld')->setTitle('Title 2'),
+ ];
+
+ $this->container->netscapeBookmarkUtils = $this->createMock(NetscapeBookmarkUtils::class);
+ $this->container->netscapeBookmarkUtils
+ ->expects(static::once())
+ ->method('filterAndFormat')
+ ->willReturnCallback(
+ function (
+ BookmarkFormatter $formatter,
+ string $selection,
+ bool $prependNoteUrl,
+ string $indexUrl
+ ) use ($parameters, $bookmarks): array {
+ static::assertInstanceOf(BookmarkRawFormatter::class, $formatter);
+ static::assertSame($parameters['selection'], $selection);
+ static::assertTrue($prependNoteUrl);
+ static::assertSame('http://shaarli', $indexUrl);
+
+ return $bookmarks;
+ }
+ )
+ ;
+
+ $result = $this->controller->export($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('export.bookmarks', (string) $result->getBody());
+ static::assertSame(['text/html; charset=utf-8'], $result->getHeader('content-type'));
+ static::assertRegExp(
+ '/attachment; filename=bookmarks_all_[\d]{8}_[\d]{6}\.html/',
+ $result->getHeader('content-disposition')[0]
+ );
+
+ static::assertNotEmpty($assignedVariables['date']);
+ static::assertSame(PHP_EOL, $assignedVariables['eol']);
+ static::assertSame('all', $assignedVariables['selection']);
+ static::assertSame($bookmarks, $assignedVariables['links']);
+ }
+
+ /**
+ * Test posting an export request - without selection parameter
+ */
+ public function testExportSelectionMissing(): void
+ {
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->container->sessionManager
+ ->expects(static::once())
+ ->method('setSessionParameter')
+ ->with(SessionManager::KEY_ERROR_MESSAGES, ['Please select an export mode.'])
+ ;
+
+ $result = $this->controller->export($request, $response);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/admin/export'], $result->getHeader('location'));
+ }
+
+ /**
+ * Test posting an export request - without selection parameter
+ */
+ public function testExportErrorEncountered(): void
+ {
+ $parameters = [
+ 'selection' => 'all',
+ ];
+
+ $request = $this->createMock(Request::class);
+ $request->method('getParam')->willReturnCallback(function (string $key) use ($parameters) {
+ return $parameters[$key] ?? null;
+ });
+ $response = new Response();
+
+ $this->container->netscapeBookmarkUtils = $this->createMock(NetscapeBookmarkUtils::class);
+ $this->container->netscapeBookmarkUtils
+ ->expects(static::once())
+ ->method('filterAndFormat')
+ ->willThrowException(new \Exception($message = 'error message'));
+ ;
+
+ $this->container->sessionManager
+ ->expects(static::once())
+ ->method('setSessionParameter')
+ ->with(SessionManager::KEY_ERROR_MESSAGES, [$message])
+ ;
+
+ $result = $this->controller->export($request, $response);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/admin/export'], $result->getHeader('location'));
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Admin;
+
+use Shaarli\Container\ShaarliTestContainer;
+use Shaarli\Front\Controller\Visitor\FrontControllerMockHelper;
+use Shaarli\History;
+
+/**
+ * Trait FrontControllerMockHelper
+ *
+ * Helper trait used to initialize the ShaarliContainer and mock its services for admin controller tests.
+ *
+ * @property ShaarliTestContainer $container
+ */
+trait FrontAdminControllerMockHelper
+{
+ use FrontControllerMockHelper {
+ FrontControllerMockHelper::createContainer as parentCreateContainer;
+ }
+
+ /**
+ * Mock the container instance
+ */
+ protected function createContainer(): void
+ {
+ $this->parentCreateContainer();
+
+ $this->container->history = $this->createMock(History::class);
+
+ $this->container->loginManager->method('isLoggedIn')->willReturn(true);
+ $this->container->sessionManager->method('checkToken')->willReturn(true);
+ }
+
+
+ /**
+ * Pass a reference of an array which will be populated by `sessionManager->setSessionParameter`
+ * calls during execution.
+ *
+ * @param mixed $variables Array reference to populate.
+ */
+ protected function assignSessionVars(array &$variables): void
+ {
+ $this->container->sessionManager
+ ->expects(static::atLeastOnce())
+ ->method('setSessionParameter')
+ ->willReturnCallback(function ($key, $value) use (&$variables) {
+ $variables[$key] = $value;
+
+ return $this->container->sessionManager;
+ })
+ ;
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Admin;
+
+use PHPUnit\Framework\TestCase;
+use Psr\Http\Message\UploadedFileInterface;
+use Shaarli\Netscape\NetscapeBookmarkUtils;
+use Shaarli\Security\SessionManager;
+use Slim\Http\Request;
+use Slim\Http\Response;
+use Slim\Http\UploadedFile;
+
+class ImportControllerTest extends TestCase
+{
+ use FrontAdminControllerMockHelper;
+
+ /** @var ImportController */
+ protected $controller;
+
+ public function setUp(): void
+ {
+ $this->createContainer();
+
+ $this->controller = new ImportController($this->container);
+ }
+
+ /**
+ * Test displaying import page
+ */
+ public function testIndex(): void
+ {
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $result = $this->controller->index($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('import', (string) $result->getBody());
+
+ static::assertSame('Import - Shaarli', $assignedVariables['pagetitle']);
+ static::assertIsInt($assignedVariables['maxfilesize']);
+ static::assertRegExp('/\d+[KM]iB/', $assignedVariables['maxfilesizeHuman']);
+ }
+
+ /**
+ * Test importing a file with default and valid parameters
+ */
+ public function testImportDefault(): void
+ {
+ $parameters = [
+ 'abc' => 'def',
+ 'other' => 'param',
+ ];
+
+ $requestFile = new UploadedFile('file', 'name', 'type', 123);
+
+ $request = $this->createMock(Request::class);
+ $request->method('getParams')->willReturnCallback(function () use ($parameters) {
+ return $parameters;
+ });
+ $request->method('getUploadedFiles')->willReturn(['filetoupload' => $requestFile]);
+ $response = new Response();
+
+ $this->container->netscapeBookmarkUtils = $this->createMock(NetscapeBookmarkUtils::class);
+ $this->container->netscapeBookmarkUtils
+ ->expects(static::once())
+ ->method('import')
+ ->willReturnCallback(
+ function (
+ array $post,
+ UploadedFileInterface $file
+ ) use ($parameters, $requestFile): string {
+ static::assertSame($parameters, $post);
+ static::assertSame($requestFile, $file);
+
+ return 'status';
+ }
+ )
+ ;
+
+ $this->container->sessionManager
+ ->expects(static::once())
+ ->method('setSessionParameter')
+ ->with(SessionManager::KEY_SUCCESS_MESSAGES, ['status'])
+ ;
+
+ $result = $this->controller->import($request, $response);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/admin/import'], $result->getHeader('location'));
+ }
+
+ /**
+ * Test posting an import request - without import file
+ */
+ public function testImportFileMissing(): void
+ {
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->container->sessionManager
+ ->expects(static::once())
+ ->method('setSessionParameter')
+ ->with(SessionManager::KEY_ERROR_MESSAGES, ['No import file provided.'])
+ ;
+
+ $result = $this->controller->import($request, $response);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/admin/import'], $result->getHeader('location'));
+ }
+
+ /**
+ * Test posting an import request - with an empty file
+ */
+ public function testImportEmptyFile(): void
+ {
+ $requestFile = new UploadedFile('file', 'name', 'type', 0);
+
+ $request = $this->createMock(Request::class);
+ $request->method('getUploadedFiles')->willReturn(['filetoupload' => $requestFile]);
+ $response = new Response();
+
+ $this->container->netscapeBookmarkUtils = $this->createMock(NetscapeBookmarkUtils::class);
+ $this->container->netscapeBookmarkUtils->expects(static::never())->method('filterAndFormat');
+
+ $this->container->sessionManager
+ ->expects(static::once())
+ ->method('setSessionParameter')
+ ->willReturnCallback(function (string $key, array $value): SessionManager {
+ static::assertSame(SessionManager::KEY_ERROR_MESSAGES, $key);
+ static::assertStringStartsWith('The file you are trying to upload is probably bigger', $value[0]);
+
+ return $this->container->sessionManager;
+ })
+ ;
+
+ $result = $this->controller->import($request, $response);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/admin/import'], $result->getHeader('location'));
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Admin;
+
+use PHPUnit\Framework\TestCase;
+use Shaarli\Security\CookieManager;
+use Shaarli\Security\LoginManager;
+use Shaarli\Security\SessionManager;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+class LogoutControllerTest extends TestCase
+{
+ use FrontAdminControllerMockHelper;
+
+ /** @var LogoutController */
+ protected $controller;
+
+ public function setUp(): void
+ {
+ $this->createContainer();
+
+ $this->controller = new LogoutController($this->container);
+ }
+
+ public function testValidControllerInvoke(): void
+ {
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->container->pageCacheManager->expects(static::once())->method('invalidateCaches');
+
+ $this->container->sessionManager = $this->createMock(SessionManager::class);
+ $this->container->sessionManager->expects(static::once())->method('logout');
+
+ $this->container->cookieManager = $this->createMock(CookieManager::class);
+ $this->container->cookieManager
+ ->expects(static::once())
+ ->method('setCookieParameter')
+ ->with(CookieManager::STAY_SIGNED_IN, 'false', 0, '/subfolder/')
+ ;
+
+ $result = $this->controller->index($request, $response);
+
+ static::assertInstanceOf(Response::class, $result);
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/'], $result->getHeader('location'));
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest;
+
+use PHPUnit\Framework\TestCase;
+use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
+use Shaarli\Front\Controller\Admin\ManageShaareController;
+use Shaarli\Http\HttpAccess;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+class AddShaareTest extends TestCase
+{
+ use FrontAdminControllerMockHelper;
+
+ /** @var ManageShaareController */
+ protected $controller;
+
+ public function setUp(): void
+ {
+ $this->createContainer();
+
+ $this->container->httpAccess = $this->createMock(HttpAccess::class);
+ $this->controller = new ManageShaareController($this->container);
+ }
+
+ /**
+ * Test displaying add link page
+ */
+ public function testAddShaare(): void
+ {
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $result = $this->controller->addShaare($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('addlink', (string) $result->getBody());
+
+ static::assertSame('Shaare a new link - Shaarli', $assignedVariables['pagetitle']);
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest;
+
+use PHPUnit\Framework\TestCase;
+use Shaarli\Bookmark\Bookmark;
+use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
+use Shaarli\Formatter\BookmarkFormatter;
+use Shaarli\Formatter\BookmarkRawFormatter;
+use Shaarli\Formatter\FormatterFactory;
+use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
+use Shaarli\Front\Controller\Admin\ManageShaareController;
+use Shaarli\Http\HttpAccess;
+use Shaarli\Security\SessionManager;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+class ChangeVisibilityBookmarkTest extends TestCase
+{
+ use FrontAdminControllerMockHelper;
+
+ /** @var ManageShaareController */
+ protected $controller;
+
+ public function setUp(): void
+ {
+ $this->createContainer();
+
+ $this->container->httpAccess = $this->createMock(HttpAccess::class);
+ $this->controller = new ManageShaareController($this->container);
+ }
+
+ /**
+ * Change bookmark visibility - Set private - Single public bookmark with valid parameters
+ */
+ public function testSetSingleBookmarkPrivate(): void
+ {
+ $parameters = ['id' => '123', 'newVisibility' => 'private'];
+
+ $request = $this->createMock(Request::class);
+ $request
+ ->method('getParam')
+ ->willReturnCallback(function (string $key) use ($parameters): ?string {
+ return $parameters[$key] ?? null;
+ })
+ ;
+ $response = new Response();
+
+ $bookmark = (new Bookmark())->setId(123)->setUrl('http://domain.tld')->setTitle('Title 123')->setPrivate(false);
+
+ static::assertFalse($bookmark->isPrivate());
+
+ $this->container->bookmarkService->expects(static::once())->method('get')->with(123)->willReturn($bookmark);
+ $this->container->bookmarkService->expects(static::once())->method('set')->with($bookmark, false);
+ $this->container->bookmarkService->expects(static::once())->method('save');
+ $this->container->formatterFactory = $this->createMock(FormatterFactory::class);
+ $this->container->formatterFactory
+ ->expects(static::once())
+ ->method('getFormatter')
+ ->with('raw')
+ ->willReturnCallback(function () use ($bookmark): BookmarkFormatter {
+ return new BookmarkRawFormatter($this->container->conf, true);
+ })
+ ;
+
+ // Make sure that PluginManager hook is triggered
+ $this->container->pluginManager
+ ->expects(static::once())
+ ->method('executeHooks')
+ ->with('save_link')
+ ;
+
+ $result = $this->controller->changeVisibility($request, $response);
+
+ static::assertTrue($bookmark->isPrivate());
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/'], $result->getHeader('location'));
+ }
+
+ /**
+ * Change bookmark visibility - Set public - Single private bookmark with valid parameters
+ */
+ public function testSetSingleBookmarkPublic(): void
+ {
+ $parameters = ['id' => '123', 'newVisibility' => 'public'];
+
+ $request = $this->createMock(Request::class);
+ $request
+ ->method('getParam')
+ ->willReturnCallback(function (string $key) use ($parameters): ?string {
+ return $parameters[$key] ?? null;
+ })
+ ;
+ $response = new Response();
+
+ $bookmark = (new Bookmark())->setId(123)->setUrl('http://domain.tld')->setTitle('Title 123')->setPrivate(true);
+
+ static::assertTrue($bookmark->isPrivate());
+
+ $this->container->bookmarkService->expects(static::once())->method('get')->with(123)->willReturn($bookmark);
+ $this->container->bookmarkService->expects(static::once())->method('set')->with($bookmark, false);
+ $this->container->bookmarkService->expects(static::once())->method('save');
+ $this->container->formatterFactory = $this->createMock(FormatterFactory::class);
+ $this->container->formatterFactory
+ ->expects(static::once())
+ ->method('getFormatter')
+ ->with('raw')
+ ->willReturn(new BookmarkRawFormatter($this->container->conf, true))
+ ;
+
+ // Make sure that PluginManager hook is triggered
+ $this->container->pluginManager
+ ->expects(static::once())
+ ->method('executeHooks')
+ ->with('save_link')
+ ;
+
+ $result = $this->controller->changeVisibility($request, $response);
+
+ static::assertFalse($bookmark->isPrivate());
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/'], $result->getHeader('location'));
+ }
+
+ /**
+ * Change bookmark visibility - Set private on single already private bookmark
+ */
+ public function testSetSinglePrivateBookmarkPrivate(): void
+ {
+ $parameters = ['id' => '123', 'newVisibility' => 'private'];
+
+ $request = $this->createMock(Request::class);
+ $request
+ ->method('getParam')
+ ->willReturnCallback(function (string $key) use ($parameters): ?string {
+ return $parameters[$key] ?? null;
+ })
+ ;
+ $response = new Response();
+
+ $bookmark = (new Bookmark())->setId(123)->setUrl('http://domain.tld')->setTitle('Title 123')->setPrivate(true);
+
+ static::assertTrue($bookmark->isPrivate());
+
+ $this->container->bookmarkService->expects(static::once())->method('get')->with(123)->willReturn($bookmark);
+ $this->container->bookmarkService->expects(static::once())->method('set')->with($bookmark, false);
+ $this->container->bookmarkService->expects(static::once())->method('save');
+ $this->container->formatterFactory = $this->createMock(FormatterFactory::class);
+ $this->container->formatterFactory
+ ->expects(static::once())
+ ->method('getFormatter')
+ ->with('raw')
+ ->willReturn(new BookmarkRawFormatter($this->container->conf, true))
+ ;
+
+ // Make sure that PluginManager hook is triggered
+ $this->container->pluginManager
+ ->expects(static::once())
+ ->method('executeHooks')
+ ->with('save_link')
+ ;
+
+ $result = $this->controller->changeVisibility($request, $response);
+
+ static::assertTrue($bookmark->isPrivate());
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/'], $result->getHeader('location'));
+ }
+
+ /**
+ * Change bookmark visibility - Set multiple bookmarks private
+ */
+ public function testSetMultipleBookmarksPrivate(): void
+ {
+ $parameters = ['id' => '123 456 789', 'newVisibility' => 'private'];
+
+ $request = $this->createMock(Request::class);
+ $request
+ ->method('getParam')
+ ->willReturnCallback(function (string $key) use ($parameters): ?string {
+ return $parameters[$key] ?? null;
+ })
+ ;
+ $response = new Response();
+
+ $bookmarks = [
+ (new Bookmark())->setId(123)->setUrl('http://domain.tld')->setTitle('Title 123')->setPrivate(false),
+ (new Bookmark())->setId(456)->setUrl('http://domain.tld')->setTitle('Title 456')->setPrivate(true),
+ (new Bookmark())->setId(789)->setUrl('http://domain.tld')->setTitle('Title 789')->setPrivate(false),
+ ];
+
+ $this->container->bookmarkService
+ ->expects(static::exactly(3))
+ ->method('get')
+ ->withConsecutive([123], [456], [789])
+ ->willReturnOnConsecutiveCalls(...$bookmarks)
+ ;
+ $this->container->bookmarkService
+ ->expects(static::exactly(3))
+ ->method('set')
+ ->withConsecutive(...array_map(function (Bookmark $bookmark): array {
+ return [$bookmark, false];
+ }, $bookmarks))
+ ;
+ $this->container->bookmarkService->expects(static::once())->method('save');
+ $this->container->formatterFactory = $this->createMock(FormatterFactory::class);
+ $this->container->formatterFactory
+ ->expects(static::once())
+ ->method('getFormatter')
+ ->with('raw')
+ ->willReturn(new BookmarkRawFormatter($this->container->conf, true))
+ ;
+
+ // Make sure that PluginManager hook is triggered
+ $this->container->pluginManager
+ ->expects(static::exactly(3))
+ ->method('executeHooks')
+ ->with('save_link')
+ ;
+
+ $result = $this->controller->changeVisibility($request, $response);
+
+ static::assertTrue($bookmarks[0]->isPrivate());
+ static::assertTrue($bookmarks[1]->isPrivate());
+ static::assertTrue($bookmarks[2]->isPrivate());
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/'], $result->getHeader('location'));
+ }
+
+ /**
+ * Change bookmark visibility - Single bookmark not found.
+ */
+ public function testChangeVisibilitySingleBookmarkNotFound(): void
+ {
+ $parameters = ['id' => '123', 'newVisibility' => 'private'];
+
+ $request = $this->createMock(Request::class);
+ $request
+ ->method('getParam')
+ ->willReturnCallback(function (string $key) use ($parameters): ?string {
+ return $parameters[$key] ?? null;
+ })
+ ;
+ $response = new Response();
+
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('get')
+ ->willThrowException(new BookmarkNotFoundException())
+ ;
+ $this->container->bookmarkService->expects(static::never())->method('set');
+ $this->container->bookmarkService->expects(static::never())->method('save');
+ $this->container->formatterFactory = $this->createMock(FormatterFactory::class);
+ $this->container->formatterFactory
+ ->expects(static::once())
+ ->method('getFormatter')
+ ->with('raw')
+ ->willReturn(new BookmarkRawFormatter($this->container->conf, true))
+ ;
+
+ // Make sure that PluginManager hook is not triggered
+ $this->container->pluginManager
+ ->expects(static::never())
+ ->method('executeHooks')
+ ->with('save_link')
+ ;
+
+ $result = $this->controller->changeVisibility($request, $response);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/'], $result->getHeader('location'));
+ }
+
+ /**
+ * Change bookmark visibility - Multiple bookmarks with one not found.
+ */
+ public function testChangeVisibilityMultipleBookmarksOneNotFound(): void
+ {
+ $parameters = ['id' => '123 456 789', 'newVisibility' => 'public'];
+
+ $request = $this->createMock(Request::class);
+ $request
+ ->method('getParam')
+ ->willReturnCallback(function (string $key) use ($parameters): ?string {
+ return $parameters[$key] ?? null;
+ })
+ ;
+ $response = new Response();
+
+ $bookmarks = [
+ (new Bookmark())->setId(123)->setUrl('http://domain.tld')->setTitle('Title 123')->setPrivate(true),
+ (new Bookmark())->setId(789)->setUrl('http://domain.tld')->setTitle('Title 789')->setPrivate(false),
+ ];
+
+ $this->container->bookmarkService
+ ->expects(static::exactly(3))
+ ->method('get')
+ ->withConsecutive([123], [456], [789])
+ ->willReturnCallback(function (int $id) use ($bookmarks): Bookmark {
+ if ($id === 123) {
+ return $bookmarks[0];
+ }
+ if ($id === 789) {
+ return $bookmarks[1];
+ }
+ throw new BookmarkNotFoundException();
+ })
+ ;
+ $this->container->bookmarkService
+ ->expects(static::exactly(2))
+ ->method('set')
+ ->withConsecutive(...array_map(function (Bookmark $bookmark): array {
+ return [$bookmark, false];
+ }, $bookmarks))
+ ;
+ $this->container->bookmarkService->expects(static::once())->method('save');
+
+ // Make sure that PluginManager hook is not triggered
+ $this->container->pluginManager
+ ->expects(static::exactly(2))
+ ->method('executeHooks')
+ ->with('save_link')
+ ;
+
+ $this->container->sessionManager
+ ->expects(static::once())
+ ->method('setSessionParameter')
+ ->with(SessionManager::KEY_ERROR_MESSAGES, ['Bookmark with identifier 456 could not be found.'])
+ ;
+
+ $result = $this->controller->changeVisibility($request, $response);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/'], $result->getHeader('location'));
+ }
+
+ /**
+ * Change bookmark visibility - Invalid ID
+ */
+ public function testChangeVisibilityInvalidId(): void
+ {
+ $parameters = ['id' => 'nope not an ID', 'newVisibility' => 'private'];
+
+ $request = $this->createMock(Request::class);
+ $request
+ ->method('getParam')
+ ->willReturnCallback(function (string $key) use ($parameters): ?string {
+ return $parameters[$key] ?? null;
+ })
+ ;
+ $response = new Response();
+
+ $this->container->sessionManager
+ ->expects(static::once())
+ ->method('setSessionParameter')
+ ->with(SessionManager::KEY_ERROR_MESSAGES, ['Invalid bookmark ID provided.'])
+ ;
+
+ $result = $this->controller->changeVisibility($request, $response);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/'], $result->getHeader('location'));
+ }
+
+ /**
+ * Change bookmark visibility - Empty ID
+ */
+ public function testChangeVisibilityEmptyId(): void
+ {
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->container->sessionManager
+ ->expects(static::once())
+ ->method('setSessionParameter')
+ ->with(SessionManager::KEY_ERROR_MESSAGES, ['Invalid bookmark ID provided.'])
+ ;
+
+ $result = $this->controller->changeVisibility($request, $response);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/'], $result->getHeader('location'));
+ }
+
+ /**
+ * Change bookmark visibility - with invalid visibility
+ */
+ public function testChangeVisibilityWithInvalidVisibility(): void
+ {
+ $parameters = ['id' => '123', 'newVisibility' => 'invalid'];
+
+ $request = $this->createMock(Request::class);
+ $request
+ ->method('getParam')
+ ->willReturnCallback(function (string $key) use ($parameters): ?string {
+ return $parameters[$key] ?? null;
+ })
+ ;
+ $response = new Response();
+
+ $this->container->sessionManager
+ ->expects(static::once())
+ ->method('setSessionParameter')
+ ->with(SessionManager::KEY_ERROR_MESSAGES, ['Invalid visibility provided.'])
+ ;
+
+ $result = $this->controller->changeVisibility($request, $response);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/'], $result->getHeader('location'));
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest;
+
+use PHPUnit\Framework\TestCase;
+use Shaarli\Bookmark\Bookmark;
+use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
+use Shaarli\Formatter\BookmarkFormatter;
+use Shaarli\Formatter\FormatterFactory;
+use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
+use Shaarli\Front\Controller\Admin\ManageShaareController;
+use Shaarli\Http\HttpAccess;
+use Shaarli\Security\SessionManager;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+class DeleteBookmarkTest extends TestCase
+{
+ use FrontAdminControllerMockHelper;
+
+ /** @var ManageShaareController */
+ protected $controller;
+
+ public function setUp(): void
+ {
+ $this->createContainer();
+
+ $this->container->httpAccess = $this->createMock(HttpAccess::class);
+ $this->controller = new ManageShaareController($this->container);
+ }
+
+ /**
+ * Delete bookmark - Single bookmark with valid parameters
+ */
+ public function testDeleteSingleBookmark(): void
+ {
+ $parameters = ['id' => '123'];
+
+ $request = $this->createMock(Request::class);
+ $request
+ ->method('getParam')
+ ->willReturnCallback(function (string $key) use ($parameters): ?string {
+ return $parameters[$key] ?? null;
+ })
+ ;
+ $response = new Response();
+
+ $bookmark = (new Bookmark())->setId(123)->setUrl('http://domain.tld')->setTitle('Title 123');
+
+ $this->container->bookmarkService->expects(static::once())->method('get')->with(123)->willReturn($bookmark);
+ $this->container->bookmarkService->expects(static::once())->method('remove')->with($bookmark, false);
+ $this->container->bookmarkService->expects(static::once())->method('save');
+ $this->container->formatterFactory = $this->createMock(FormatterFactory::class);
+ $this->container->formatterFactory
+ ->expects(static::once())
+ ->method('getFormatter')
+ ->with('raw')
+ ->willReturnCallback(function () use ($bookmark): BookmarkFormatter {
+ $formatter = $this->createMock(BookmarkFormatter::class);
+ $formatter
+ ->expects(static::once())
+ ->method('format')
+ ->with($bookmark)
+ ->willReturn(['formatted' => $bookmark])
+ ;
+
+ return $formatter;
+ })
+ ;
+
+ // Make sure that PluginManager hook is triggered
+ $this->container->pluginManager
+ ->expects(static::once())
+ ->method('executeHooks')
+ ->with('delete_link', ['formatted' => $bookmark])
+ ;
+
+ $result = $this->controller->deleteBookmark($request, $response);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/'], $result->getHeader('location'));
+ }
+
+ /**
+ * Delete bookmark - Multiple bookmarks with valid parameters
+ */
+ public function testDeleteMultipleBookmarks(): void
+ {
+ $parameters = ['id' => '123 456 789'];
+
+ $request = $this->createMock(Request::class);
+ $request
+ ->method('getParam')
+ ->willReturnCallback(function (string $key) use ($parameters): ?string {
+ return $parameters[$key] ?? null;
+ })
+ ;
+ $response = new Response();
+
+ $bookmarks = [
+ (new Bookmark())->setId(123)->setUrl('http://domain.tld')->setTitle('Title 123'),
+ (new Bookmark())->setId(456)->setUrl('http://domain.tld')->setTitle('Title 456'),
+ (new Bookmark())->setId(789)->setUrl('http://domain.tld')->setTitle('Title 789'),
+ ];
+
+ $this->container->bookmarkService
+ ->expects(static::exactly(3))
+ ->method('get')
+ ->withConsecutive([123], [456], [789])
+ ->willReturnOnConsecutiveCalls(...$bookmarks)
+ ;
+ $this->container->bookmarkService
+ ->expects(static::exactly(3))
+ ->method('remove')
+ ->withConsecutive(...array_map(function (Bookmark $bookmark): array {
+ return [$bookmark, false];
+ }, $bookmarks))
+ ;
+ $this->container->bookmarkService->expects(static::once())->method('save');
+ $this->container->formatterFactory = $this->createMock(FormatterFactory::class);
+ $this->container->formatterFactory
+ ->expects(static::once())
+ ->method('getFormatter')
+ ->with('raw')
+ ->willReturnCallback(function () use ($bookmarks): BookmarkFormatter {
+ $formatter = $this->createMock(BookmarkFormatter::class);
+
+ $formatter
+ ->expects(static::exactly(3))
+ ->method('format')
+ ->withConsecutive(...array_map(function (Bookmark $bookmark): array {
+ return [$bookmark];
+ }, $bookmarks))
+ ->willReturnOnConsecutiveCalls(...array_map(function (Bookmark $bookmark): array {
+ return ['formatted' => $bookmark];
+ }, $bookmarks))
+ ;
+
+ return $formatter;
+ })
+ ;
+
+ // Make sure that PluginManager hook is triggered
+ $this->container->pluginManager
+ ->expects(static::exactly(3))
+ ->method('executeHooks')
+ ->with('delete_link')
+ ;
+
+ $result = $this->controller->deleteBookmark($request, $response);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/'], $result->getHeader('location'));
+ }
+
+ /**
+ * Delete bookmark - Single bookmark not found in the data store
+ */
+ public function testDeleteSingleBookmarkNotFound(): void
+ {
+ $parameters = ['id' => '123'];
+
+ $request = $this->createMock(Request::class);
+ $request
+ ->method('getParam')
+ ->willReturnCallback(function (string $key) use ($parameters): ?string {
+ return $parameters[$key] ?? null;
+ })
+ ;
+ $response = new Response();
+
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('get')
+ ->willThrowException(new BookmarkNotFoundException())
+ ;
+ $this->container->bookmarkService->expects(static::never())->method('remove');
+ $this->container->bookmarkService->expects(static::never())->method('save');
+ $this->container->formatterFactory = $this->createMock(FormatterFactory::class);
+ $this->container->formatterFactory
+ ->expects(static::once())
+ ->method('getFormatter')
+ ->with('raw')
+ ->willReturnCallback(function (): BookmarkFormatter {
+ $formatter = $this->createMock(BookmarkFormatter::class);
+
+ $formatter->expects(static::never())->method('format');
+
+ return $formatter;
+ })
+ ;
+ // Make sure that PluginManager hook is not triggered
+ $this->container->pluginManager
+ ->expects(static::never())
+ ->method('executeHooks')
+ ->with('delete_link')
+ ;
+
+ $result = $this->controller->deleteBookmark($request, $response);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/'], $result->getHeader('location'));
+ }
+
+ /**
+ * Delete bookmark - Multiple bookmarks with one not found in the data store
+ */
+ public function testDeleteMultipleBookmarksOneNotFound(): void
+ {
+ $parameters = ['id' => '123 456 789'];
+
+ $request = $this->createMock(Request::class);
+ $request
+ ->method('getParam')
+ ->willReturnCallback(function (string $key) use ($parameters): ?string {
+ return $parameters[$key] ?? null;
+ })
+ ;
+ $response = new Response();
+
+ $bookmarks = [
+ (new Bookmark())->setId(123)->setUrl('http://domain.tld')->setTitle('Title 123'),
+ (new Bookmark())->setId(789)->setUrl('http://domain.tld')->setTitle('Title 789'),
+ ];
+
+ $this->container->bookmarkService
+ ->expects(static::exactly(3))
+ ->method('get')
+ ->withConsecutive([123], [456], [789])
+ ->willReturnCallback(function (int $id) use ($bookmarks): Bookmark {
+ if ($id === 123) {
+ return $bookmarks[0];
+ }
+ if ($id === 789) {
+ return $bookmarks[1];
+ }
+ throw new BookmarkNotFoundException();
+ })
+ ;
+ $this->container->bookmarkService
+ ->expects(static::exactly(2))
+ ->method('remove')
+ ->withConsecutive(...array_map(function (Bookmark $bookmark): array {
+ return [$bookmark, false];
+ }, $bookmarks))
+ ;
+ $this->container->bookmarkService->expects(static::once())->method('save');
+ $this->container->formatterFactory = $this->createMock(FormatterFactory::class);
+ $this->container->formatterFactory
+ ->expects(static::once())
+ ->method('getFormatter')
+ ->with('raw')
+ ->willReturnCallback(function () use ($bookmarks): BookmarkFormatter {
+ $formatter = $this->createMock(BookmarkFormatter::class);
+
+ $formatter
+ ->expects(static::exactly(2))
+ ->method('format')
+ ->withConsecutive(...array_map(function (Bookmark $bookmark): array {
+ return [$bookmark];
+ }, $bookmarks))
+ ->willReturnOnConsecutiveCalls(...array_map(function (Bookmark $bookmark): array {
+ return ['formatted' => $bookmark];
+ }, $bookmarks))
+ ;
+
+ return $formatter;
+ })
+ ;
+
+ // Make sure that PluginManager hook is not triggered
+ $this->container->pluginManager
+ ->expects(static::exactly(2))
+ ->method('executeHooks')
+ ->with('delete_link')
+ ;
+
+ $this->container->sessionManager
+ ->expects(static::once())
+ ->method('setSessionParameter')
+ ->with(SessionManager::KEY_ERROR_MESSAGES, ['Bookmark with identifier 456 could not be found.'])
+ ;
+
+ $result = $this->controller->deleteBookmark($request, $response);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/'], $result->getHeader('location'));
+ }
+
+ /**
+ * Delete bookmark - Invalid ID
+ */
+ public function testDeleteInvalidId(): void
+ {
+ $parameters = ['id' => 'nope not an ID'];
+
+ $request = $this->createMock(Request::class);
+ $request
+ ->method('getParam')
+ ->willReturnCallback(function (string $key) use ($parameters): ?string {
+ return $parameters[$key] ?? null;
+ })
+ ;
+ $response = new Response();
+
+ $this->container->sessionManager
+ ->expects(static::once())
+ ->method('setSessionParameter')
+ ->with(SessionManager::KEY_ERROR_MESSAGES, ['Invalid bookmark ID provided.'])
+ ;
+
+ $result = $this->controller->deleteBookmark($request, $response);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/'], $result->getHeader('location'));
+ }
+
+ /**
+ * Delete bookmark - Empty ID
+ */
+ public function testDeleteEmptyId(): void
+ {
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->container->sessionManager
+ ->expects(static::once())
+ ->method('setSessionParameter')
+ ->with(SessionManager::KEY_ERROR_MESSAGES, ['Invalid bookmark ID provided.'])
+ ;
+
+ $result = $this->controller->deleteBookmark($request, $response);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/'], $result->getHeader('location'));
+ }
+
+ /**
+ * Delete bookmark - from bookmarklet
+ */
+ public function testDeleteBookmarkFromBookmarklet(): void
+ {
+ $parameters = [
+ 'id' => '123',
+ 'source' => 'bookmarklet',
+ ];
+
+ $request = $this->createMock(Request::class);
+ $request
+ ->method('getParam')
+ ->willReturnCallback(function (string $key) use ($parameters): ?string {
+ return $parameters[$key] ?? null;
+ })
+ ;
+ $response = new Response();
+
+ $this->container->formatterFactory = $this->createMock(FormatterFactory::class);
+ $this->container->formatterFactory
+ ->expects(static::once())
+ ->method('getFormatter')
+ ->willReturnCallback(function (): BookmarkFormatter {
+ $formatter = $this->createMock(BookmarkFormatter::class);
+ $formatter->method('format')->willReturn(['formatted']);
+
+ return $formatter;
+ })
+ ;
+
+ $result = $this->controller->deleteBookmark($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('<script>self.close();</script>', (string) $result->getBody('location'));
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest;
+
+use PHPUnit\Framework\TestCase;
+use Shaarli\Bookmark\Bookmark;
+use Shaarli\Config\ConfigManager;
+use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
+use Shaarli\Front\Controller\Admin\ManageShaareController;
+use Shaarli\Http\HttpAccess;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+class DisplayCreateFormTest extends TestCase
+{
+ use FrontAdminControllerMockHelper;
+
+ /** @var ManageShaareController */
+ protected $controller;
+
+ public function setUp(): void
+ {
+ $this->createContainer();
+
+ $this->container->httpAccess = $this->createMock(HttpAccess::class);
+ $this->controller = new ManageShaareController($this->container);
+ }
+
+ /**
+ * Test displaying bookmark create form
+ * Ensure that every step of the standard workflow works properly.
+ */
+ public function testDisplayCreateFormWithUrl(): void
+ {
+ $this->container->environment = [
+ 'HTTP_REFERER' => $referer = 'http://shaarli/subfolder/controller/?searchtag=abc'
+ ];
+
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ $url = 'http://url.tld/other?part=3&utm_ad=pay#hash';
+ $expectedUrl = str_replace('&utm_ad=pay', '', $url);
+ $remoteTitle = 'Remote Title';
+ $remoteDesc = 'Sometimes the meta description is relevant.';
+ $remoteTags = 'abc def';
+
+ $request = $this->createMock(Request::class);
+ $request->method('getParam')->willReturnCallback(function (string $key) use ($url): ?string {
+ return $key === 'post' ? $url : null;
+ });
+ $response = new Response();
+
+ $this->container->httpAccess
+ ->expects(static::once())
+ ->method('getCurlDownloadCallback')
+ ->willReturnCallback(
+ function (&$charset, &$title, &$description, &$tags) use (
+ $remoteTitle,
+ $remoteDesc,
+ $remoteTags
+ ): callable {
+ return function () use (
+ &$charset,
+ &$title,
+ &$description,
+ &$tags,
+ $remoteTitle,
+ $remoteDesc,
+ $remoteTags
+ ): void {
+ $charset = 'ISO-8859-1';
+ $title = $remoteTitle;
+ $description = $remoteDesc;
+ $tags = $remoteTags;
+ };
+ }
+ )
+ ;
+ $this->container->httpAccess
+ ->expects(static::once())
+ ->method('getHttpResponse')
+ ->with($expectedUrl, 30, 4194304)
+ ->willReturnCallback(function($url, $timeout, $maxBytes, $callback): void {
+ $callback();
+ })
+ ;
+
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('bookmarksCountPerTag')
+ ->willReturn($tags = ['tag1' => 2, 'tag2' => 1])
+ ;
+
+ // Make sure that PluginManager hook is triggered
+ $this->container->pluginManager
+ ->expects(static::at(0))
+ ->method('executeHooks')
+ ->willReturnCallback(function (string $hook, array $data) use ($remoteTitle, $remoteDesc): array {
+ static::assertSame('render_editlink', $hook);
+ static::assertSame($remoteTitle, $data['link']['title']);
+ static::assertSame($remoteDesc, $data['link']['description']);
+
+ return $data;
+ })
+ ;
+
+ $result = $this->controller->displayCreateForm($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('editlink', (string) $result->getBody());
+
+ static::assertSame('Shaare - Shaarli', $assignedVariables['pagetitle']);
+
+ static::assertSame($expectedUrl, $assignedVariables['link']['url']);
+ static::assertSame($remoteTitle, $assignedVariables['link']['title']);
+ static::assertSame($remoteDesc, $assignedVariables['link']['description']);
+ static::assertSame($remoteTags, $assignedVariables['link']['tags']);
+ static::assertFalse($assignedVariables['link']['private']);
+
+ static::assertTrue($assignedVariables['link_is_new']);
+ static::assertSame($referer, $assignedVariables['http_referer']);
+ static::assertSame($tags, $assignedVariables['tags']);
+ static::assertArrayHasKey('source', $assignedVariables);
+ static::assertArrayHasKey('default_private_links', $assignedVariables);
+ }
+
+ /**
+ * Test displaying bookmark create form
+ * Ensure all available query parameters are handled properly.
+ */
+ public function testDisplayCreateFormWithFullParameters(): void
+ {
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ $parameters = [
+ 'post' => 'http://url.tld/other?part=3&utm_ad=pay#hash',
+ 'title' => 'Provided Title',
+ 'description' => 'Provided description.',
+ 'tags' => 'abc def',
+ 'private' => '1',
+ 'source' => 'apps',
+ ];
+ $expectedUrl = str_replace('&utm_ad=pay', '', $parameters['post']);
+
+ $request = $this->createMock(Request::class);
+ $request
+ ->method('getParam')
+ ->willReturnCallback(function (string $key) use ($parameters): ?string {
+ return $parameters[$key] ?? null;
+ });
+ $response = new Response();
+
+ $result = $this->controller->displayCreateForm($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('editlink', (string) $result->getBody());
+
+ static::assertSame('Shaare - Shaarli', $assignedVariables['pagetitle']);
+
+ static::assertSame($expectedUrl, $assignedVariables['link']['url']);
+ static::assertSame($parameters['title'], $assignedVariables['link']['title']);
+ static::assertSame($parameters['description'], $assignedVariables['link']['description']);
+ static::assertSame($parameters['tags'], $assignedVariables['link']['tags']);
+ static::assertTrue($assignedVariables['link']['private']);
+ static::assertTrue($assignedVariables['link_is_new']);
+ static::assertSame($parameters['source'], $assignedVariables['source']);
+ }
+
+ /**
+ * Test displaying bookmark create form
+ * Without any parameter.
+ */
+ public function testDisplayCreateFormEmpty(): void
+ {
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->container->httpAccess->expects(static::never())->method('getHttpResponse');
+ $this->container->httpAccess->expects(static::never())->method('getCurlDownloadCallback');
+
+ $result = $this->controller->displayCreateForm($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('editlink', (string) $result->getBody());
+ static::assertSame('', $assignedVariables['link']['url']);
+ static::assertSame('Note: ', $assignedVariables['link']['title']);
+ static::assertSame('', $assignedVariables['link']['description']);
+ static::assertSame('', $assignedVariables['link']['tags']);
+ static::assertFalse($assignedVariables['link']['private']);
+ static::assertTrue($assignedVariables['link_is_new']);
+ }
+
+ /**
+ * Test displaying bookmark create form
+ * URL not using HTTP protocol: do not try to retrieve the title
+ */
+ public function testDisplayCreateFormNotHttp(): void
+ {
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ $url = 'magnet://kubuntu.torrent';
+ $request = $this->createMock(Request::class);
+ $request
+ ->method('getParam')
+ ->willReturnCallback(function (string $key) use ($url): ?string {
+ return $key === 'post' ? $url : null;
+ });
+ $response = new Response();
+
+ $this->container->httpAccess->expects(static::never())->method('getHttpResponse');
+ $this->container->httpAccess->expects(static::never())->method('getCurlDownloadCallback');
+
+ $result = $this->controller->displayCreateForm($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('editlink', (string) $result->getBody());
+ static::assertSame($url, $assignedVariables['link']['url']);
+ static::assertTrue($assignedVariables['link_is_new']);
+ }
+
+ /**
+ * Test displaying bookmark create form
+ * When markdown formatter is enabled, the no markdown tag should be added to existing tags.
+ */
+ public function testDisplayCreateFormWithMarkdownEnabled(): void
+ {
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ $this->container->conf = $this->createMock(ConfigManager::class);
+ $this->container->conf
+ ->expects(static::atLeastOnce())
+ ->method('get')->willReturnCallback(function (string $key): ?string {
+ if ($key === 'formatter') {
+ return 'markdown';
+ }
+
+ return $key;
+ })
+ ;
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $result = $this->controller->displayCreateForm($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('editlink', (string) $result->getBody());
+ static::assertSame(['nomarkdown' => 1], $assignedVariables['tags']);
+ }
+
+ /**
+ * Test displaying bookmark create form
+ * When an existing URL is submitted, we want to edit the existing link.
+ */
+ public function testDisplayCreateFormWithExistingUrl(): void
+ {
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ $url = 'http://url.tld/other?part=3&utm_ad=pay#hash';
+ $expectedUrl = str_replace('&utm_ad=pay', '', $url);
+
+ $request = $this->createMock(Request::class);
+ $request
+ ->method('getParam')
+ ->willReturnCallback(function (string $key) use ($url): ?string {
+ return $key === 'post' ? $url : null;
+ });
+ $response = new Response();
+
+ $this->container->httpAccess->expects(static::never())->method('getHttpResponse');
+ $this->container->httpAccess->expects(static::never())->method('getCurlDownloadCallback');
+
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('findByUrl')
+ ->with($expectedUrl)
+ ->willReturn(
+ (new Bookmark())
+ ->setId($id = 23)
+ ->setUrl($expectedUrl)
+ ->setTitle($title = 'Bookmark Title')
+ ->setDescription($description = 'Bookmark description.')
+ ->setTags($tags = ['abc', 'def'])
+ ->setPrivate(true)
+ ->setCreated($createdAt = new \DateTime('2020-06-10 18:45:44'))
+ )
+ ;
+
+ $result = $this->controller->displayCreateForm($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('editlink', (string) $result->getBody());
+
+ static::assertSame('Edit Shaare - Shaarli', $assignedVariables['pagetitle']);
+ static::assertFalse($assignedVariables['link_is_new']);
+
+ static::assertSame($id, $assignedVariables['link']['id']);
+ static::assertSame($expectedUrl, $assignedVariables['link']['url']);
+ static::assertSame($title, $assignedVariables['link']['title']);
+ static::assertSame($description, $assignedVariables['link']['description']);
+ static::assertSame(implode(' ', $tags), $assignedVariables['link']['tags']);
+ static::assertTrue($assignedVariables['link']['private']);
+ static::assertSame($createdAt, $assignedVariables['link']['created']);
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest;
+
+use PHPUnit\Framework\TestCase;
+use Shaarli\Bookmark\Bookmark;
+use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
+use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
+use Shaarli\Front\Controller\Admin\ManageShaareController;
+use Shaarli\Http\HttpAccess;
+use Shaarli\Security\SessionManager;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+class DisplayEditFormTest extends TestCase
+{
+ use FrontAdminControllerMockHelper;
+
+ /** @var ManageShaareController */
+ protected $controller;
+
+ public function setUp(): void
+ {
+ $this->createContainer();
+
+ $this->container->httpAccess = $this->createMock(HttpAccess::class);
+ $this->controller = new ManageShaareController($this->container);
+ }
+
+ /**
+ * Test displaying bookmark edit form
+ * When an existing ID is provided, ensure that default workflow works properly.
+ */
+ public function testDisplayEditFormDefault(): void
+ {
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ $id = 11;
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->container->httpAccess->expects(static::never())->method('getHttpResponse');
+ $this->container->httpAccess->expects(static::never())->method('getCurlDownloadCallback');
+
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('get')
+ ->with($id)
+ ->willReturn(
+ (new Bookmark())
+ ->setId($id)
+ ->setUrl($url = 'http://domain.tld')
+ ->setTitle($title = 'Bookmark Title')
+ ->setDescription($description = 'Bookmark description.')
+ ->setTags($tags = ['abc', 'def'])
+ ->setPrivate(true)
+ ->setCreated($createdAt = new \DateTime('2020-06-10 18:45:44'))
+ )
+ ;
+
+ $result = $this->controller->displayEditForm($request, $response, ['id' => (string) $id]);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('editlink', (string) $result->getBody());
+
+ static::assertSame('Edit Shaare - Shaarli', $assignedVariables['pagetitle']);
+ static::assertFalse($assignedVariables['link_is_new']);
+
+ static::assertSame($id, $assignedVariables['link']['id']);
+ static::assertSame($url, $assignedVariables['link']['url']);
+ static::assertSame($title, $assignedVariables['link']['title']);
+ static::assertSame($description, $assignedVariables['link']['description']);
+ static::assertSame(implode(' ', $tags), $assignedVariables['link']['tags']);
+ static::assertTrue($assignedVariables['link']['private']);
+ static::assertSame($createdAt, $assignedVariables['link']['created']);
+ }
+
+ /**
+ * Test displaying bookmark edit form
+ * Invalid ID provided.
+ */
+ public function testDisplayEditFormInvalidId(): void
+ {
+ $id = 'invalid';
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->container->sessionManager
+ ->expects(static::once())
+ ->method('setSessionParameter')
+ ->with(SessionManager::KEY_ERROR_MESSAGES, ['Bookmark with identifier invalid could not be found.'])
+ ;
+
+ $result = $this->controller->displayEditForm($request, $response, ['id' => $id]);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/'], $result->getHeader('location'));
+ }
+
+ /**
+ * Test displaying bookmark edit form
+ * ID not provided.
+ */
+ public function testDisplayEditFormIdNotProvided(): void
+ {
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->container->sessionManager
+ ->expects(static::once())
+ ->method('setSessionParameter')
+ ->with(SessionManager::KEY_ERROR_MESSAGES, ['Bookmark with identifier could not be found.'])
+ ;
+
+ $result = $this->controller->displayEditForm($request, $response, []);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/'], $result->getHeader('location'));
+ }
+
+ /**
+ * Test displaying bookmark edit form
+ * Bookmark not found.
+ */
+ public function testDisplayEditFormBookmarkNotFound(): void
+ {
+ $id = 123;
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('get')
+ ->with($id)
+ ->willThrowException(new BookmarkNotFoundException())
+ ;
+
+ $this->container->sessionManager
+ ->expects(static::once())
+ ->method('setSessionParameter')
+ ->with(SessionManager::KEY_ERROR_MESSAGES, ['Bookmark with identifier 123 could not be found.'])
+ ;
+
+ $result = $this->controller->displayEditForm($request, $response, ['id' => (string) $id]);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/'], $result->getHeader('location'));
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest;
+
+use PHPUnit\Framework\TestCase;
+use Shaarli\Bookmark\Bookmark;
+use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
+use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
+use Shaarli\Front\Controller\Admin\ManageShaareController;
+use Shaarli\Http\HttpAccess;
+use Shaarli\Security\SessionManager;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+class PinBookmarkTest extends TestCase
+{
+ use FrontAdminControllerMockHelper;
+
+ /** @var ManageShaareController */
+ protected $controller;
+
+ public function setUp(): void
+ {
+ $this->createContainer();
+
+ $this->container->httpAccess = $this->createMock(HttpAccess::class);
+ $this->controller = new ManageShaareController($this->container);
+ }
+
+ /**
+ * Test pin bookmark - with valid input
+ *
+ * @dataProvider initialStickyValuesProvider()
+ */
+ public function testPinBookmarkIsStickyNull(?bool $sticky, bool $expectedValue): void
+ {
+ $id = 123;
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $bookmark = (new Bookmark())
+ ->setId(123)
+ ->setUrl('http://domain.tld')
+ ->setTitle('Title 123')
+ ->setSticky($sticky)
+ ;
+
+ $this->container->bookmarkService->expects(static::once())->method('get')->with(123)->willReturn($bookmark);
+ $this->container->bookmarkService->expects(static::once())->method('set')->with($bookmark, true);
+
+ // Make sure that PluginManager hook is triggered
+ $this->container->pluginManager
+ ->expects(static::once())
+ ->method('executeHooks')
+ ->with('save_link')
+ ;
+
+ $result = $this->controller->pinBookmark($request, $response, ['id' => (string) $id]);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/'], $result->getHeader('location'));
+
+ static::assertSame($expectedValue, $bookmark->isSticky());
+ }
+
+ public function initialStickyValuesProvider(): array
+ {
+ // [initialStickyState, isStickyAfterPin]
+ return [[null, true], [false, true], [true, false]];
+ }
+
+ /**
+ * Test pin bookmark - invalid bookmark ID
+ */
+ public function testDisplayEditFormInvalidId(): void
+ {
+ $id = 'invalid';
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->container->sessionManager
+ ->expects(static::once())
+ ->method('setSessionParameter')
+ ->with(SessionManager::KEY_ERROR_MESSAGES, ['Bookmark with identifier invalid could not be found.'])
+ ;
+
+ $result = $this->controller->pinBookmark($request, $response, ['id' => $id]);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/'], $result->getHeader('location'));
+ }
+
+ /**
+ * Test pin bookmark - Bookmark ID not provided
+ */
+ public function testDisplayEditFormIdNotProvided(): void
+ {
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->container->sessionManager
+ ->expects(static::once())
+ ->method('setSessionParameter')
+ ->with(SessionManager::KEY_ERROR_MESSAGES, ['Bookmark with identifier could not be found.'])
+ ;
+
+ $result = $this->controller->pinBookmark($request, $response, []);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/'], $result->getHeader('location'));
+ }
+
+ /**
+ * Test pin bookmark - bookmark not found
+ */
+ public function testDisplayEditFormBookmarkNotFound(): void
+ {
+ $id = 123;
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('get')
+ ->with($id)
+ ->willThrowException(new BookmarkNotFoundException())
+ ;
+
+ $this->container->sessionManager
+ ->expects(static::once())
+ ->method('setSessionParameter')
+ ->with(SessionManager::KEY_ERROR_MESSAGES, ['Bookmark with identifier 123 could not be found.'])
+ ;
+
+ $result = $this->controller->pinBookmark($request, $response, ['id' => (string) $id]);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/'], $result->getHeader('location'));
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest;
+
+use PHPUnit\Framework\TestCase;
+use Shaarli\Bookmark\Bookmark;
+use Shaarli\Config\ConfigManager;
+use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
+use Shaarli\Front\Controller\Admin\ManageShaareController;
+use Shaarli\Front\Exception\WrongTokenException;
+use Shaarli\Http\HttpAccess;
+use Shaarli\Security\SessionManager;
+use Shaarli\Thumbnailer;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+class SaveBookmarkTest extends TestCase
+{
+ use FrontAdminControllerMockHelper;
+
+ /** @var ManageShaareController */
+ protected $controller;
+
+ public function setUp(): void
+ {
+ $this->createContainer();
+
+ $this->container->httpAccess = $this->createMock(HttpAccess::class);
+ $this->controller = new ManageShaareController($this->container);
+ }
+
+ /**
+ * Test save a new bookmark
+ */
+ public function testSaveBookmark(): void
+ {
+ $id = 21;
+ $parameters = [
+ 'lf_url' => 'http://url.tld/other?part=3#hash',
+ 'lf_title' => 'Provided Title',
+ 'lf_description' => 'Provided description.',
+ 'lf_tags' => 'abc def',
+ 'lf_private' => '1',
+ 'returnurl' => 'http://shaarli.tld/subfolder/admin/add-shaare'
+ ];
+
+ $request = $this->createMock(Request::class);
+ $request
+ ->method('getParam')
+ ->willReturnCallback(function (string $key) use ($parameters): ?string {
+ return $parameters[$key] ?? null;
+ })
+ ;
+ $response = new Response();
+
+ $checkBookmark = function (Bookmark $bookmark) use ($parameters) {
+ static::assertSame($parameters['lf_url'], $bookmark->getUrl());
+ static::assertSame($parameters['lf_title'], $bookmark->getTitle());
+ static::assertSame($parameters['lf_description'], $bookmark->getDescription());
+ static::assertSame($parameters['lf_tags'], $bookmark->getTagsString());
+ static::assertTrue($bookmark->isPrivate());
+ };
+
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('addOrSet')
+ ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): void {
+ static::assertFalse($save);
+
+ $checkBookmark($bookmark);
+
+ $bookmark->setId($id);
+ })
+ ;
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('set')
+ ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): void {
+ static::assertTrue($save);
+
+ $checkBookmark($bookmark);
+
+ static::assertSame($id, $bookmark->getId());
+ })
+ ;
+
+ // Make sure that PluginManager hook is triggered
+ $this->container->pluginManager
+ ->expects(static::at(0))
+ ->method('executeHooks')
+ ->willReturnCallback(function (string $hook, array $data) use ($parameters, $id): array {
+ static::assertSame('save_link', $hook);
+
+ static::assertSame($id, $data['id']);
+ static::assertSame($parameters['lf_url'], $data['url']);
+ static::assertSame($parameters['lf_title'], $data['title']);
+ static::assertSame($parameters['lf_description'], $data['description']);
+ static::assertSame($parameters['lf_tags'], $data['tags']);
+ static::assertTrue($data['private']);
+
+ return $data;
+ })
+ ;
+
+ $result = $this->controller->save($request, $response);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertRegExp('@/subfolder/#[\w\-]{6}@', $result->getHeader('location')[0]);
+ }
+
+
+ /**
+ * Test save an existing bookmark
+ */
+ public function testSaveExistingBookmark(): void
+ {
+ $id = 21;
+ $parameters = [
+ 'lf_id' => (string) $id,
+ 'lf_url' => 'http://url.tld/other?part=3#hash',
+ 'lf_title' => 'Provided Title',
+ 'lf_description' => 'Provided description.',
+ 'lf_tags' => 'abc def',
+ 'lf_private' => '1',
+ 'returnurl' => 'http://shaarli.tld/subfolder/?page=2'
+ ];
+
+ $request = $this->createMock(Request::class);
+ $request
+ ->method('getParam')
+ ->willReturnCallback(function (string $key) use ($parameters): ?string {
+ return $parameters[$key] ?? null;
+ })
+ ;
+ $response = new Response();
+
+ $checkBookmark = function (Bookmark $bookmark) use ($parameters, $id) {
+ static::assertSame($id, $bookmark->getId());
+ static::assertSame($parameters['lf_url'], $bookmark->getUrl());
+ static::assertSame($parameters['lf_title'], $bookmark->getTitle());
+ static::assertSame($parameters['lf_description'], $bookmark->getDescription());
+ static::assertSame($parameters['lf_tags'], $bookmark->getTagsString());
+ static::assertTrue($bookmark->isPrivate());
+ };
+
+ $this->container->bookmarkService->expects(static::atLeastOnce())->method('exists')->willReturn(true);
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('get')
+ ->willReturn((new Bookmark())->setId($id)->setUrl('http://other.url'))
+ ;
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('addOrSet')
+ ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): void {
+ static::assertFalse($save);
+
+ $checkBookmark($bookmark);
+ })
+ ;
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('set')
+ ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): void {
+ static::assertTrue($save);
+
+ $checkBookmark($bookmark);
+
+ static::assertSame($id, $bookmark->getId());
+ })
+ ;
+
+ // Make sure that PluginManager hook is triggered
+ $this->container->pluginManager
+ ->expects(static::at(0))
+ ->method('executeHooks')
+ ->willReturnCallback(function (string $hook, array $data) use ($parameters, $id): array {
+ static::assertSame('save_link', $hook);
+
+ static::assertSame($id, $data['id']);
+ static::assertSame($parameters['lf_url'], $data['url']);
+ static::assertSame($parameters['lf_title'], $data['title']);
+ static::assertSame($parameters['lf_description'], $data['description']);
+ static::assertSame($parameters['lf_tags'], $data['tags']);
+ static::assertTrue($data['private']);
+
+ return $data;
+ })
+ ;
+
+ $result = $this->controller->save($request, $response);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertRegExp('@/subfolder/\?page=2#[\w\-]{6}@', $result->getHeader('location')[0]);
+ }
+
+ /**
+ * Test save a bookmark - try to retrieve the thumbnail
+ */
+ public function testSaveBookmarkWithThumbnail(): void
+ {
+ $parameters = ['lf_url' => 'http://url.tld/other?part=3#hash'];
+
+ $request = $this->createMock(Request::class);
+ $request
+ ->method('getParam')
+ ->willReturnCallback(function (string $key) use ($parameters): ?string {
+ return $parameters[$key] ?? null;
+ })
+ ;
+ $response = new Response();
+
+ $this->container->conf = $this->createMock(ConfigManager::class);
+ $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) {
+ return $key === 'thumbnails.mode' ? Thumbnailer::MODE_ALL : $default;
+ });
+
+ $this->container->thumbnailer = $this->createMock(Thumbnailer::class);
+ $this->container->thumbnailer
+ ->expects(static::once())
+ ->method('get')
+ ->with($parameters['lf_url'])
+ ->willReturn($thumb = 'http://thumb.url')
+ ;
+
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('addOrSet')
+ ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($thumb): void {
+ static::assertSame($thumb, $bookmark->getThumbnail());
+ })
+ ;
+
+ $result = $this->controller->save($request, $response);
+
+ static::assertSame(302, $result->getStatusCode());
+ }
+
+ /**
+ * Change the password with a wrong existing password
+ */
+ public function testSaveBookmarkFromBookmarklet(): void
+ {
+ $parameters = ['source' => 'bookmarklet'];
+
+ $request = $this->createMock(Request::class);
+ $request
+ ->method('getParam')
+ ->willReturnCallback(function (string $key) use ($parameters): ?string {
+ return $parameters[$key] ?? null;
+ })
+ ;
+ $response = new Response();
+
+ $result = $this->controller->save($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('<script>self.close();</script>', (string) $result->getBody());
+ }
+
+ /**
+ * Change the password with a wrong existing password
+ */
+ public function testSaveBookmarkWrongToken(): void
+ {
+ $this->container->sessionManager = $this->createMock(SessionManager::class);
+ $this->container->sessionManager->method('checkToken')->willReturn(false);
+
+ $this->container->bookmarkService->expects(static::never())->method('addOrSet');
+ $this->container->bookmarkService->expects(static::never())->method('set');
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->expectException(WrongTokenException::class);
+
+ $this->controller->save($request, $response);
+ }
+
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Admin;
+
+use PHPUnit\Framework\TestCase;
+use Shaarli\Bookmark\Bookmark;
+use Shaarli\Bookmark\BookmarkFilter;
+use Shaarli\Front\Exception\WrongTokenException;
+use Shaarli\Security\SessionManager;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+class ManageTagControllerTest extends TestCase
+{
+ use FrontAdminControllerMockHelper;
+
+ /** @var ManageTagController */
+ protected $controller;
+
+ public function setUp(): void
+ {
+ $this->createContainer();
+
+ $this->controller = new ManageTagController($this->container);
+ }
+
+ /**
+ * Test displaying manage tag page
+ */
+ public function testIndex(): void
+ {
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ $request = $this->createMock(Request::class);
+ $request->method('getParam')->with('fromtag')->willReturn('fromtag');
+ $response = new Response();
+
+ $result = $this->controller->index($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('changetag', (string) $result->getBody());
+
+ static::assertSame('fromtag', $assignedVariables['fromtag']);
+ static::assertSame('Manage tags - Shaarli', $assignedVariables['pagetitle']);
+ }
+
+ /**
+ * Test posting a tag update - rename tag - valid info provided.
+ */
+ public function testSaveRenameTagValid(): void
+ {
+ $session = [];
+ $this->assignSessionVars($session);
+
+ $requestParameters = [
+ 'renametag' => 'rename',
+ 'fromtag' => 'old-tag',
+ 'totag' => 'new-tag',
+ ];
+ $request = $this->createMock(Request::class);
+ $request
+ ->expects(static::atLeastOnce())
+ ->method('getParam')
+ ->willReturnCallback(function (string $key) use ($requestParameters): ?string {
+ return $requestParameters[$key] ?? null;
+ })
+ ;
+ $response = new Response();
+
+ $bookmark1 = $this->createMock(Bookmark::class);
+ $bookmark2 = $this->createMock(Bookmark::class);
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('search')
+ ->with(['searchtags' => 'old-tag'], BookmarkFilter::$ALL, true)
+ ->willReturnCallback(function () use ($bookmark1, $bookmark2): array {
+ $bookmark1->expects(static::once())->method('renameTag')->with('old-tag', 'new-tag');
+ $bookmark2->expects(static::once())->method('renameTag')->with('old-tag', 'new-tag');
+
+ return [$bookmark1, $bookmark2];
+ })
+ ;
+ $this->container->bookmarkService
+ ->expects(static::exactly(2))
+ ->method('set')
+ ->withConsecutive([$bookmark1, false], [$bookmark2, false])
+ ;
+ $this->container->bookmarkService->expects(static::once())->method('save');
+
+ $result = $this->controller->save($request, $response);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/?searchtags=new-tag'], $result->getHeader('location'));
+
+ static::assertArrayNotHasKey(SessionManager::KEY_ERROR_MESSAGES, $session);
+ static::assertArrayNotHasKey(SessionManager::KEY_WARNING_MESSAGES, $session);
+ static::assertArrayHasKey(SessionManager::KEY_SUCCESS_MESSAGES, $session);
+ static::assertSame(['The tag was renamed in 2 bookmarks.'], $session[SessionManager::KEY_SUCCESS_MESSAGES]);
+ }
+
+ /**
+ * Test posting a tag update - delete tag - valid info provided.
+ */
+ public function testSaveDeleteTagValid(): void
+ {
+ $session = [];
+ $this->assignSessionVars($session);
+
+ $requestParameters = [
+ 'deletetag' => 'delete',
+ 'fromtag' => 'old-tag',
+ ];
+ $request = $this->createMock(Request::class);
+ $request
+ ->expects(static::atLeastOnce())
+ ->method('getParam')
+ ->willReturnCallback(function (string $key) use ($requestParameters): ?string {
+ return $requestParameters[$key] ?? null;
+ })
+ ;
+ $response = new Response();
+
+ $bookmark1 = $this->createMock(Bookmark::class);
+ $bookmark2 = $this->createMock(Bookmark::class);
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('search')
+ ->with(['searchtags' => 'old-tag'], BookmarkFilter::$ALL, true)
+ ->willReturnCallback(function () use ($bookmark1, $bookmark2): array {
+ $bookmark1->expects(static::once())->method('deleteTag')->with('old-tag');
+ $bookmark2->expects(static::once())->method('deleteTag')->with('old-tag');
+
+ return [$bookmark1, $bookmark2];
+ })
+ ;
+ $this->container->bookmarkService
+ ->expects(static::exactly(2))
+ ->method('set')
+ ->withConsecutive([$bookmark1, false], [$bookmark2, false])
+ ;
+ $this->container->bookmarkService->expects(static::once())->method('save');
+
+ $result = $this->controller->save($request, $response);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/admin/tags'], $result->getHeader('location'));
+
+ static::assertArrayNotHasKey(SessionManager::KEY_ERROR_MESSAGES, $session);
+ static::assertArrayNotHasKey(SessionManager::KEY_WARNING_MESSAGES, $session);
+ static::assertArrayHasKey(SessionManager::KEY_SUCCESS_MESSAGES, $session);
+ static::assertSame(['The tag was removed from 2 bookmarks.'], $session[SessionManager::KEY_SUCCESS_MESSAGES]);
+ }
+
+ /**
+ * Test posting a tag update - wrong token.
+ */
+ public function testSaveWrongToken(): void
+ {
+ $this->container->sessionManager = $this->createMock(SessionManager::class);
+ $this->container->sessionManager->method('checkToken')->willReturn(false);
+
+ $this->container->conf->expects(static::never())->method('set');
+ $this->container->conf->expects(static::never())->method('write');
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->expectException(WrongTokenException::class);
+
+ $this->controller->save($request, $response);
+ }
+
+ /**
+ * Test posting a tag update - rename tag - missing "FROM" tag.
+ */
+ public function testSaveRenameTagMissingFrom(): void
+ {
+ $session = [];
+ $this->assignSessionVars($session);
+
+ $requestParameters = [
+ 'renametag' => 'rename',
+ ];
+ $request = $this->createMock(Request::class);
+ $request
+ ->expects(static::atLeastOnce())
+ ->method('getParam')
+ ->willReturnCallback(function (string $key) use ($requestParameters): ?string {
+ return $requestParameters[$key] ?? null;
+ })
+ ;
+ $response = new Response();
+
+ $result = $this->controller->save($request, $response);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/admin/tags'], $result->getHeader('location'));
+
+ static::assertArrayNotHasKey(SessionManager::KEY_ERROR_MESSAGES, $session);
+ static::assertArrayHasKey(SessionManager::KEY_WARNING_MESSAGES, $session);
+ static::assertArrayNotHasKey(SessionManager::KEY_SUCCESS_MESSAGES, $session);
+ static::assertSame(['Invalid tags provided.'], $session[SessionManager::KEY_WARNING_MESSAGES]);
+ }
+
+ /**
+ * Test posting a tag update - delete tag - missing "FROM" tag.
+ */
+ public function testSaveDeleteTagMissingFrom(): void
+ {
+ $session = [];
+ $this->assignSessionVars($session);
+
+ $requestParameters = [
+ 'deletetag' => 'delete',
+ ];
+ $request = $this->createMock(Request::class);
+ $request
+ ->expects(static::atLeastOnce())
+ ->method('getParam')
+ ->willReturnCallback(function (string $key) use ($requestParameters): ?string {
+ return $requestParameters[$key] ?? null;
+ })
+ ;
+ $response = new Response();
+
+ $result = $this->controller->save($request, $response);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/admin/tags'], $result->getHeader('location'));
+
+ static::assertArrayNotHasKey(SessionManager::KEY_ERROR_MESSAGES, $session);
+ static::assertArrayHasKey(SessionManager::KEY_WARNING_MESSAGES, $session);
+ static::assertArrayNotHasKey(SessionManager::KEY_SUCCESS_MESSAGES, $session);
+ static::assertSame(['Invalid tags provided.'], $session[SessionManager::KEY_WARNING_MESSAGES]);
+ }
+
+ /**
+ * Test posting a tag update - rename tag - missing "TO" tag.
+ */
+ public function testSaveRenameTagMissingTo(): void
+ {
+ $session = [];
+ $this->assignSessionVars($session);
+
+ $requestParameters = [
+ 'renametag' => 'rename',
+ 'fromtag' => 'old-tag'
+ ];
+ $request = $this->createMock(Request::class);
+ $request
+ ->expects(static::atLeastOnce())
+ ->method('getParam')
+ ->willReturnCallback(function (string $key) use ($requestParameters): ?string {
+ return $requestParameters[$key] ?? null;
+ })
+ ;
+ $response = new Response();
+
+ $result = $this->controller->save($request, $response);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/admin/tags'], $result->getHeader('location'));
+
+ static::assertArrayNotHasKey(SessionManager::KEY_ERROR_MESSAGES, $session);
+ static::assertArrayHasKey(SessionManager::KEY_WARNING_MESSAGES, $session);
+ static::assertArrayNotHasKey(SessionManager::KEY_SUCCESS_MESSAGES, $session);
+ static::assertSame(['Invalid tags provided.'], $session[SessionManager::KEY_WARNING_MESSAGES]);
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Admin;
+
+use PHPUnit\Framework\TestCase;
+use Shaarli\Config\ConfigManager;
+use Shaarli\Front\Exception\OpenShaarliPasswordException;
+use Shaarli\Front\Exception\WrongTokenException;
+use Shaarli\Security\SessionManager;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+class PasswordControllerTest extends TestCase
+{
+ use FrontAdminControllerMockHelper;
+
+ /** @var PasswordController */
+ protected $controller;
+
+ /** @var mixed[] Variables assigned to the template */
+ protected $assignedVariables = [];
+
+ public function setUp(): void
+ {
+ $this->createContainer();
+ $this->assignTemplateVars($this->assignedVariables);
+
+ $this->controller = new PasswordController($this->container);
+ }
+
+ /**
+ * Test displaying the change password page.
+ */
+ public function testGetPage(): void
+ {
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $result = $this->controller->index($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('changepassword', (string) $result->getBody());
+ static::assertSame('Change password - Shaarli', $this->assignedVariables['pagetitle']);
+ }
+
+ /**
+ * Change the password with valid parameters
+ */
+ public function testPostNewPasswordDefault(): void
+ {
+ $request = $this->createMock(Request::class);
+ $request->method('getParam')->willReturnCallback(function (string $key): string {
+ if ('oldpassword' === $key) {
+ return 'old';
+ }
+ if ('setpassword' === $key) {
+ return 'new';
+ }
+
+ return $key;
+ });
+ $response = new Response();
+
+ $this->container->conf = $this->createMock(ConfigManager::class);
+ $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) {
+ if ('credentials.hash' === $key) {
+ return sha1('old' . 'credentials.login' . 'credentials.salt');
+ }
+
+ return strpos($key, 'credentials') !== false ? $key : $default;
+ });
+ $this->container->conf->expects(static::once())->method('write')->with(true);
+
+ $this->container->conf
+ ->method('set')
+ ->willReturnCallback(function (string $key, string $value) {
+ if ('credentials.hash' === $key) {
+ static::assertSame(sha1('new' . 'credentials.login' . 'credentials.salt'), $value);
+ }
+ })
+ ;
+
+ $result = $this->controller->change($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('changepassword', (string) $result->getBody());
+ static::assertSame('Change password - Shaarli', $this->assignedVariables['pagetitle']);
+ }
+
+ /**
+ * Change the password with a wrong existing password
+ */
+ public function testPostNewPasswordWrongOldPassword(): void
+ {
+ $request = $this->createMock(Request::class);
+ $request->method('getParam')->willReturnCallback(function (string $key): string {
+ if ('oldpassword' === $key) {
+ return 'wrong';
+ }
+ if ('setpassword' === $key) {
+ return 'new';
+ }
+
+ return $key;
+ });
+ $response = new Response();
+
+ $this->container->conf = $this->createMock(ConfigManager::class);
+ $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) {
+ if ('credentials.hash' === $key) {
+ return sha1('old' . 'credentials.login' . 'credentials.salt');
+ }
+
+ return strpos($key, 'credentials') !== false ? $key : $default;
+ });
+
+ $this->container->conf->expects(static::never())->method('set');
+ $this->container->conf->expects(static::never())->method('write');
+
+ $this->container->sessionManager
+ ->expects(static::once())
+ ->method('setSessionParameter')
+ ->with(SessionManager::KEY_ERROR_MESSAGES, ['The old password is not correct.'])
+ ;
+
+ $result = $this->controller->change($request, $response);
+
+ static::assertSame(400, $result->getStatusCode());
+ static::assertSame('changepassword', (string) $result->getBody());
+ static::assertSame('Change password - Shaarli', $this->assignedVariables['pagetitle']);
+ }
+
+ /**
+ * Change the password with a wrong existing password
+ */
+ public function testPostNewPasswordWrongToken(): void
+ {
+ $this->container->sessionManager = $this->createMock(SessionManager::class);
+ $this->container->sessionManager->method('checkToken')->willReturn(false);
+
+ $this->container->conf->expects(static::never())->method('set');
+ $this->container->conf->expects(static::never())->method('write');
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->expectException(WrongTokenException::class);
+
+ $this->controller->change($request, $response);
+ }
+
+ /**
+ * Change the password with an empty new password
+ */
+ public function testPostNewEmptyPassword(): void
+ {
+ $this->container->sessionManager
+ ->expects(static::once())
+ ->method('setSessionParameter')
+ ->with(SessionManager::KEY_ERROR_MESSAGES, ['You must provide the current and new password to change it.'])
+ ;
+
+ $this->container->conf->expects(static::never())->method('set');
+ $this->container->conf->expects(static::never())->method('write');
+
+ $request = $this->createMock(Request::class);
+ $request->method('getParam')->willReturnCallback(function (string $key): string {
+ if ('oldpassword' === $key) {
+ return 'old';
+ }
+ if ('setpassword' === $key) {
+ return '';
+ }
+
+ return $key;
+ });
+ $response = new Response();
+
+ $result = $this->controller->change($request, $response);
+
+ static::assertSame(400, $result->getStatusCode());
+ static::assertSame('changepassword', (string) $result->getBody());
+ static::assertSame('Change password - Shaarli', $this->assignedVariables['pagetitle']);
+ }
+
+ /**
+ * Change the password on an open shaarli
+ */
+ public function testPostNewPasswordOnOpenShaarli(): void
+ {
+ $this->container->conf = $this->createMock(ConfigManager::class);
+ $this->container->conf->method('get')->with('security.open_shaarli')->willReturn(true);
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->expectException(OpenShaarliPasswordException::class);
+
+ $this->controller->change($request, $response);
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Admin;
+
+use PHPUnit\Framework\TestCase;
+use Shaarli\Config\ConfigManager;
+use Shaarli\Front\Exception\WrongTokenException;
+use Shaarli\Plugin\PluginManager;
+use Shaarli\Security\SessionManager;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+class PluginsControllerTest extends TestCase
+{
+ use FrontAdminControllerMockHelper;
+
+ const PLUGIN_NAMES = ['plugin1', 'plugin2', 'plugin3', 'plugin4'];
+
+ /** @var PluginsController */
+ protected $controller;
+
+ public function setUp(): void
+ {
+ $this->createContainer();
+
+ $this->controller = new PluginsController($this->container);
+
+ mkdir($path = __DIR__ . '/folder');
+ PluginManager::$PLUGINS_PATH = $path;
+ array_map(function (string $plugin) use ($path) { touch($path . '/' . $plugin); }, static::PLUGIN_NAMES);
+ }
+
+ public function tearDown()
+ {
+ $path = __DIR__ . '/folder';
+ array_map(function (string $plugin) use ($path) { unlink($path . '/' . $plugin); }, static::PLUGIN_NAMES);
+ rmdir($path);
+ }
+
+ /**
+ * Test displaying plugins admin page
+ */
+ public function testIndex(): void
+ {
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $data = [
+ 'plugin1' => ['order' => 2, 'other' => 'field'],
+ 'plugin2' => ['order' => 1],
+ 'plugin3' => ['order' => false, 'abc' => 'def'],
+ 'plugin4' => [],
+ ];
+
+ $this->container->pluginManager
+ ->expects(static::once())
+ ->method('getPluginsMeta')
+ ->willReturn($data);
+
+ $result = $this->controller->index($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('pluginsadmin', (string) $result->getBody());
+
+ static::assertSame('Plugin Administration - Shaarli', $assignedVariables['pagetitle']);
+ static::assertSame(
+ ['plugin2' => $data['plugin2'], 'plugin1' => $data['plugin1']],
+ $assignedVariables['enabledPlugins']
+ );
+ static::assertSame(
+ ['plugin3' => $data['plugin3'], 'plugin4' => $data['plugin4']],
+ $assignedVariables['disabledPlugins']
+ );
+ }
+
+ /**
+ * Test save plugins admin page
+ */
+ public function testSaveEnabledPlugins(): void
+ {
+ $parameters = [
+ 'plugin1' => 'on',
+ 'order_plugin1' => '2',
+ 'plugin2' => 'on',
+ ];
+
+ $request = $this->createMock(Request::class);
+ $request
+ ->expects(static::atLeastOnce())
+ ->method('getParams')
+ ->willReturnCallback(function () use ($parameters): array {
+ return $parameters;
+ })
+ ;
+ $response = new Response();
+
+ $this->container->pluginManager
+ ->expects(static::once())
+ ->method('executeHooks')
+ ->with('save_plugin_parameters', $parameters)
+ ;
+ $this->container->conf
+ ->expects(static::atLeastOnce())
+ ->method('set')
+ ->with('general.enabled_plugins', ['plugin1', 'plugin2'])
+ ;
+
+ $result = $this->controller->save($request, $response);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/admin/plugins'], $result->getHeader('location'));
+ }
+
+ /**
+ * Test save plugin parameters
+ */
+ public function testSavePluginParameters(): void
+ {
+ $parameters = [
+ 'parameters_form' => true,
+ 'parameter1' => 'blip',
+ 'parameter2' => 'blop',
+ ];
+
+ $request = $this->createMock(Request::class);
+ $request
+ ->expects(static::atLeastOnce())
+ ->method('getParams')
+ ->willReturnCallback(function () use ($parameters): array {
+ return $parameters;
+ })
+ ;
+ $response = new Response();
+
+ $this->container->pluginManager
+ ->expects(static::once())
+ ->method('executeHooks')
+ ->with('save_plugin_parameters', $parameters)
+ ;
+ $this->container->conf
+ ->expects(static::atLeastOnce())
+ ->method('set')
+ ->withConsecutive(['plugins.parameter1', 'blip'], ['plugins.parameter2', 'blop'])
+ ;
+
+ $result = $this->controller->save($request, $response);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/admin/plugins'], $result->getHeader('location'));
+ }
+
+ /**
+ * Test save plugin parameters - error encountered
+ */
+ public function testSaveWithError(): void
+ {
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->container->conf = $this->createMock(ConfigManager::class);
+ $this->container->conf
+ ->expects(static::atLeastOnce())
+ ->method('write')
+ ->willThrowException(new \Exception($message = 'error message'))
+ ;
+
+ $this->container->sessionManager = $this->createMock(SessionManager::class);
+ $this->container->sessionManager->method('checkToken')->willReturn(true);
+ $this->container->sessionManager
+ ->expects(static::once())
+ ->method('setSessionParameter')
+ ->with(
+ SessionManager::KEY_ERROR_MESSAGES,
+ ['Error while saving plugin configuration: ' . PHP_EOL . $message]
+ )
+ ;
+
+ $result = $this->controller->save($request, $response);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/admin/plugins'], $result->getHeader('location'));
+ }
+
+ /**
+ * Test save plugin parameters - wrong token
+ */
+ public function testSaveWrongToken(): void
+ {
+ $this->container->sessionManager = $this->createMock(SessionManager::class);
+ $this->container->sessionManager->method('checkToken')->willReturn(false);
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->expectException(WrongTokenException::class);
+
+ $this->controller->save($request, $response);
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Admin;
+
+use PHPUnit\Framework\TestCase;
+use Shaarli\Security\LoginManager;
+use Shaarli\Security\SessionManager;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+class SessionFilterControllerTest extends TestCase
+{
+ use FrontAdminControllerMockHelper;
+
+ /** @var SessionFilterController */
+ protected $controller;
+
+ public function setUp(): void
+ {
+ $this->createContainer();
+
+ $this->controller = new SessionFilterController($this->container);
+ }
+
+ /**
+ * Visibility - Default call for private filter while logged in without current value
+ */
+ public function testVisibility(): void
+ {
+ $arg = ['visibility' => 'private'];
+
+ $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/subfolder/controller/?searchtag=abc'];
+
+ $this->container->loginManager->method('isLoggedIn')->willReturn(true);
+ $this->container->sessionManager
+ ->expects(static::once())
+ ->method('setSessionParameter')
+ ->with(SessionManager::KEY_VISIBILITY, 'private')
+ ;
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $result = $this->controller->visibility($request, $response, $arg);
+
+ static::assertInstanceOf(Response::class, $result);
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/controller/?searchtag=abc'], $result->getHeader('location'));
+ }
+
+ /**
+ * Visibility - Toggle off private visibility
+ */
+ public function testVisibilityToggleOff(): void
+ {
+ $arg = ['visibility' => 'private'];
+
+ $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/subfolder/controller/?searchtag=abc'];
+
+ $this->container->loginManager->method('isLoggedIn')->willReturn(true);
+ $this->container->sessionManager
+ ->method('getSessionParameter')
+ ->with(SessionManager::KEY_VISIBILITY)
+ ->willReturn('private')
+ ;
+ $this->container->sessionManager
+ ->expects(static::never())
+ ->method('setSessionParameter')
+ ;
+ $this->container->sessionManager
+ ->expects(static::once())
+ ->method('deleteSessionParameter')
+ ->with(SessionManager::KEY_VISIBILITY)
+ ;
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $result = $this->controller->visibility($request, $response, $arg);
+
+ static::assertInstanceOf(Response::class, $result);
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/controller/?searchtag=abc'], $result->getHeader('location'));
+ }
+
+ /**
+ * Visibility - Change private to public
+ */
+ public function testVisibilitySwitch(): void
+ {
+ $arg = ['visibility' => 'private'];
+
+ $this->container->loginManager->method('isLoggedIn')->willReturn(true);
+ $this->container->sessionManager
+ ->method('getSessionParameter')
+ ->with(SessionManager::KEY_VISIBILITY)
+ ->willReturn('public')
+ ;
+ $this->container->sessionManager
+ ->expects(static::once())
+ ->method('setSessionParameter')
+ ->with(SessionManager::KEY_VISIBILITY, 'private')
+ ;
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $result = $this->controller->visibility($request, $response, $arg);
+
+ static::assertInstanceOf(Response::class, $result);
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/'], $result->getHeader('location'));
+ }
+
+ /**
+ * Visibility - With invalid value - should remove any visibility setting
+ */
+ public function testVisibilityInvalidValue(): void
+ {
+ $arg = ['visibility' => 'test'];
+
+ $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/subfolder/controller/?searchtag=abc'];
+
+ $this->container->loginManager->method('isLoggedIn')->willReturn(true);
+ $this->container->sessionManager
+ ->expects(static::never())
+ ->method('setSessionParameter')
+ ;
+ $this->container->sessionManager
+ ->expects(static::once())
+ ->method('deleteSessionParameter')
+ ->with(SessionManager::KEY_VISIBILITY)
+ ;
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $result = $this->controller->visibility($request, $response, $arg);
+
+ static::assertInstanceOf(Response::class, $result);
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/controller/?searchtag=abc'], $result->getHeader('location'));
+ }
+
+ /**
+ * Visibility - Try to change visibility while logged out
+ */
+ public function testVisibilityLoggedOut(): void
+ {
+ $arg = ['visibility' => 'test'];
+
+ $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/subfolder/controller/?searchtag=abc'];
+
+ $this->container->loginManager = $this->createMock(LoginManager::class);
+ $this->container->loginManager->method('isLoggedIn')->willReturn(false);
+ $this->container->sessionManager
+ ->expects(static::never())
+ ->method('setSessionParameter')
+ ;
+ $this->container->sessionManager
+ ->expects(static::never())
+ ->method('deleteSessionParameter')
+ ->with(SessionManager::KEY_VISIBILITY)
+ ;
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $result = $this->controller->visibility($request, $response, $arg);
+
+ static::assertInstanceOf(Response::class, $result);
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/controller/?searchtag=abc'], $result->getHeader('location'));
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Admin;
+
+use PHPUnit\Framework\TestCase;
+use Shaarli\Front\Exception\WrongTokenException;
+use Shaarli\Security\SessionManager;
+use Slim\Http\Request;
+
+/**
+ * Class ShaarliControllerTest
+ *
+ * This class is used to test default behavior of ShaarliAdminController abstract class.
+ * It uses a dummy non abstract controller.
+ */
+class ShaarliAdminControllerTest extends TestCase
+{
+ use FrontAdminControllerMockHelper;
+
+ /** @var ShaarliAdminController */
+ protected $controller;
+
+ public function setUp(): void
+ {
+ $this->createContainer();
+
+ $this->controller = new class($this->container) extends ShaarliAdminController
+ {
+ public function checkToken(Request $request): bool
+ {
+ return parent::checkToken($request);
+ }
+
+ public function saveSuccessMessage(string $message): void
+ {
+ parent::saveSuccessMessage($message);
+ }
+
+ public function saveWarningMessage(string $message): void
+ {
+ parent::saveWarningMessage($message);
+ }
+
+ public function saveErrorMessage(string $message): void
+ {
+ parent::saveErrorMessage($message);
+ }
+ };
+ }
+
+ /**
+ * Trigger controller's checkToken with a valid token.
+ */
+ public function testCheckTokenWithValidToken(): void
+ {
+ $request = $this->createMock(Request::class);
+ $request->method('getParam')->with('token')->willReturn($token = '12345');
+
+ $this->container->sessionManager = $this->createMock(SessionManager::class);
+ $this->container->sessionManager->method('checkToken')->with($token)->willReturn(true);
+
+ static::assertTrue($this->controller->checkToken($request));
+ }
+
+ /**
+ * Trigger controller's checkToken with na valid token should raise an exception.
+ */
+ public function testCheckTokenWithNotValidToken(): void
+ {
+ $request = $this->createMock(Request::class);
+ $request->method('getParam')->with('token')->willReturn($token = '12345');
+
+ $this->container->sessionManager = $this->createMock(SessionManager::class);
+ $this->container->sessionManager->method('checkToken')->with($token)->willReturn(false);
+
+ $this->expectException(WrongTokenException::class);
+
+ $this->controller->checkToken($request);
+ }
+
+ /**
+ * Test saveSuccessMessage() with a first message.
+ */
+ public function testSaveSuccessMessage(): void
+ {
+ $this->container->sessionManager
+ ->expects(static::once())
+ ->method('setSessionParameter')
+ ->with(SessionManager::KEY_SUCCESS_MESSAGES, [$message = 'bravo!'])
+ ;
+
+ $this->controller->saveSuccessMessage($message);
+ }
+
+ /**
+ * Test saveSuccessMessage() with existing messages.
+ */
+ public function testSaveSuccessMessageWithExistingMessages(): void
+ {
+ $this->container->sessionManager
+ ->expects(static::once())
+ ->method('getSessionParameter')
+ ->with(SessionManager::KEY_SUCCESS_MESSAGES)
+ ->willReturn(['success1', 'success2'])
+ ;
+ $this->container->sessionManager
+ ->expects(static::once())
+ ->method('setSessionParameter')
+ ->with(SessionManager::KEY_SUCCESS_MESSAGES, ['success1', 'success2', $message = 'bravo!'])
+ ;
+
+ $this->controller->saveSuccessMessage($message);
+ }
+
+ /**
+ * Test saveWarningMessage() with a first message.
+ */
+ public function testSaveWarningMessage(): void
+ {
+ $this->container->sessionManager
+ ->expects(static::once())
+ ->method('setSessionParameter')
+ ->with(SessionManager::KEY_WARNING_MESSAGES, [$message = 'warning!'])
+ ;
+
+ $this->controller->saveWarningMessage($message);
+ }
+
+ /**
+ * Test saveWarningMessage() with existing messages.
+ */
+ public function testSaveWarningMessageWithExistingMessages(): void
+ {
+ $this->container->sessionManager
+ ->expects(static::once())
+ ->method('getSessionParameter')
+ ->with(SessionManager::KEY_WARNING_MESSAGES)
+ ->willReturn(['warning1', 'warning2'])
+ ;
+ $this->container->sessionManager
+ ->expects(static::once())
+ ->method('setSessionParameter')
+ ->with(SessionManager::KEY_WARNING_MESSAGES, ['warning1', 'warning2', $message = 'warning!'])
+ ;
+
+ $this->controller->saveWarningMessage($message);
+ }
+
+ /**
+ * Test saveErrorMessage() with a first message.
+ */
+ public function testSaveErrorMessage(): void
+ {
+ $this->container->sessionManager
+ ->expects(static::once())
+ ->method('setSessionParameter')
+ ->with(SessionManager::KEY_ERROR_MESSAGES, [$message = 'error!'])
+ ;
+
+ $this->controller->saveErrorMessage($message);
+ }
+
+ /**
+ * Test saveErrorMessage() with existing messages.
+ */
+ public function testSaveErrorMessageWithExistingMessages(): void
+ {
+ $this->container->sessionManager
+ ->expects(static::once())
+ ->method('getSessionParameter')
+ ->with(SessionManager::KEY_ERROR_MESSAGES)
+ ->willReturn(['error1', 'error2'])
+ ;
+ $this->container->sessionManager
+ ->expects(static::once())
+ ->method('setSessionParameter')
+ ->with(SessionManager::KEY_ERROR_MESSAGES, ['error1', 'error2', $message = 'error!'])
+ ;
+
+ $this->controller->saveErrorMessage($message);
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Admin;
+
+use PHPUnit\Framework\TestCase;
+use Shaarli\Bookmark\Bookmark;
+use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
+use Shaarli\Thumbnailer;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+class ThumbnailsControllerTest extends TestCase
+{
+ use FrontAdminControllerMockHelper;
+
+ /** @var ThumbnailsController */
+ protected $controller;
+
+ public function setUp(): void
+ {
+ $this->createContainer();
+
+ $this->controller = new ThumbnailsController($this->container);
+ }
+
+ /**
+ * Test displaying the thumbnails update page
+ * Note that only non-note and HTTP bookmarks should be returned.
+ */
+ public function testIndex(): void
+ {
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('search')
+ ->willReturn([
+ (new Bookmark())->setId(1)->setUrl('http://url1.tld')->setTitle('Title 1'),
+ (new Bookmark())->setId(2)->setUrl('?abcdef')->setTitle('Note 1'),
+ (new Bookmark())->setId(3)->setUrl('http://url2.tld')->setTitle('Title 2'),
+ (new Bookmark())->setId(4)->setUrl('ftp://domain.tld', ['ftp'])->setTitle('FTP'),
+ ])
+ ;
+
+ $result = $this->controller->index($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('thumbnails', (string) $result->getBody());
+
+ static::assertSame('Thumbnails update - Shaarli', $assignedVariables['pagetitle']);
+ static::assertSame([1, 3], $assignedVariables['ids']);
+ }
+
+ /**
+ * Test updating a bookmark thumbnail with valid parameters
+ */
+ public function testAjaxUpdateValid(): void
+ {
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $bookmark = (new Bookmark())
+ ->setId($id = 123)
+ ->setUrl($url = 'http://url1.tld')
+ ->setTitle('Title 1')
+ ->setThumbnail(false)
+ ;
+
+ $this->container->thumbnailer = $this->createMock(Thumbnailer::class);
+ $this->container->thumbnailer
+ ->expects(static::once())
+ ->method('get')
+ ->with($url)
+ ->willReturn($thumb = 'http://img.tld/pic.png')
+ ;
+
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('get')
+ ->with($id)
+ ->willReturn($bookmark)
+ ;
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('set')
+ ->willReturnCallback(function (Bookmark $bookmark) use ($thumb) {
+ static::assertSame($thumb, $bookmark->getThumbnail());
+ })
+ ;
+
+ $result = $this->controller->ajaxUpdate($request, $response, ['id' => (string) $id]);
+
+ static::assertSame(200, $result->getStatusCode());
+
+ $payload = json_decode((string) $result->getBody(), true);
+
+ static::assertSame($id, $payload['id']);
+ static::assertSame($url, $payload['url']);
+ static::assertSame($thumb, $payload['thumbnail']);
+ }
+
+ /**
+ * Test updating a bookmark thumbnail - Invalid ID
+ */
+ public function testAjaxUpdateInvalidId(): void
+ {
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $result = $this->controller->ajaxUpdate($request, $response, ['id' => 'nope']);
+
+ static::assertSame(400, $result->getStatusCode());
+ }
+
+ /**
+ * Test updating a bookmark thumbnail - No ID
+ */
+ public function testAjaxUpdateNoId(): void
+ {
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $result = $this->controller->ajaxUpdate($request, $response, []);
+
+ static::assertSame(400, $result->getStatusCode());
+ }
+
+ /**
+ * Test updating a bookmark thumbnail with valid parameters
+ */
+ public function testAjaxUpdateBookmarkNotFound(): void
+ {
+ $id = 123;
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('get')
+ ->with($id)
+ ->willThrowException(new BookmarkNotFoundException())
+ ;
+
+ $result = $this->controller->ajaxUpdate($request, $response, ['id' => (string) $id]);
+
+ static::assertSame(404, $result->getStatusCode());
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Admin;
+
+use PHPUnit\Framework\TestCase;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+class TokenControllerTest extends TestCase
+{
+ use FrontAdminControllerMockHelper;
+
+ /** @var TokenController */
+ protected $controller;
+
+ public function setUp(): void
+ {
+ $this->createContainer();
+
+ $this->controller = new TokenController($this->container);
+ }
+
+ public function testGetToken(): void
+ {
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->container->sessionManager
+ ->expects(static::once())
+ ->method('generateToken')
+ ->willReturn($token = 'token1234')
+ ;
+
+ $result = $this->controller->getToken($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame($token, (string) $result->getBody());
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Admin;
+
+use PHPUnit\Framework\TestCase;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+class ToolsControllerTestControllerTest extends TestCase
+{
+ use FrontAdminControllerMockHelper;
+
+ /** @var ToolsController */
+ protected $controller;
+
+ public function setUp(): void
+ {
+ $this->createContainer();
+
+ $this->controller = new ToolsController($this->container);
+ }
+
+ public function testDefaultInvokeWithHttps(): void
+ {
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->container->environment = [
+ 'SERVER_NAME' => 'shaarli',
+ 'SERVER_PORT' => 443,
+ 'HTTPS' => 'on',
+ ];
+
+ // Save RainTPL assigned variables
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ $result = $this->controller->index($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('tools', (string) $result->getBody());
+ static::assertSame('https://shaarli', $assignedVariables['pageabsaddr']);
+ static::assertTrue($assignedVariables['sslenabled']);
+ }
+
+ public function testDefaultInvokeWithoutHttps(): void
+ {
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->container->environment = [
+ 'SERVER_NAME' => 'shaarli',
+ 'SERVER_PORT' => 80,
+ ];
+
+ // Save RainTPL assigned variables
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ $result = $this->controller->index($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('tools', (string) $result->getBody());
+ static::assertSame('http://shaarli', $assignedVariables['pageabsaddr']);
+ static::assertFalse($assignedVariables['sslenabled']);
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Visitor;
+
+use PHPUnit\Framework\TestCase;
+use Shaarli\Bookmark\Bookmark;
+use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
+use Shaarli\Config\ConfigManager;
+use Shaarli\Security\LoginManager;
+use Shaarli\Thumbnailer;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+class BookmarkListControllerTest extends TestCase
+{
+ use FrontControllerMockHelper;
+
+ /** @var BookmarkListController */
+ protected $controller;
+
+ public function setUp(): void
+ {
+ $this->createContainer();
+
+ $this->controller = new BookmarkListController($this->container);
+ }
+
+ /**
+ * Test rendering list of bookmarks with default parameters (first page).
+ */
+ public function testIndexDefaultFirstPage(): void
+ {
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('search')
+ ->with(
+ ['searchtags' => '', 'searchterm' => ''],
+ null,
+ false,
+ false
+ )
+ ->willReturn([
+ (new Bookmark())->setId(1)->setUrl('http://url1.tld')->setTitle('Title 1'),
+ (new Bookmark())->setId(2)->setUrl('http://url2.tld')->setTitle('Title 2'),
+ (new Bookmark())->setId(3)->setUrl('http://url3.tld')->setTitle('Title 3'),
+ ]
+ );
+
+ $this->container->sessionManager
+ ->method('getSessionParameter')
+ ->willReturnCallback(function (string $parameter, $default = null) {
+ if ('LINKS_PER_PAGE' === $parameter) {
+ return 2;
+ }
+
+ return $default;
+ })
+ ;
+
+ $result = $this->controller->index($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('linklist', (string) $result->getBody());
+
+ static::assertSame('Shaarli', $assignedVariables['pagetitle']);
+ static::assertSame('?page=2', $assignedVariables['previous_page_url']);
+ static::assertSame('', $assignedVariables['next_page_url']);
+ static::assertSame(2, $assignedVariables['page_max']);
+ static::assertSame('', $assignedVariables['search_tags']);
+ static::assertSame(3, $assignedVariables['result_count']);
+ static::assertSame(1, $assignedVariables['page_current']);
+ static::assertSame('', $assignedVariables['search_term']);
+ static::assertNull($assignedVariables['visibility']);
+ static::assertCount(2, $assignedVariables['links']);
+
+ $link = $assignedVariables['links'][0];
+
+ static::assertSame(1, $link['id']);
+ static::assertSame('http://url1.tld', $link['url']);
+ static::assertSame('Title 1', $link['title']);
+
+ $link = $assignedVariables['links'][1];
+
+ static::assertSame(2, $link['id']);
+ static::assertSame('http://url2.tld', $link['url']);
+ static::assertSame('Title 2', $link['title']);
+ }
+
+ /**
+ * Test rendering list of bookmarks with default parameters (second page).
+ */
+ public function testIndexDefaultSecondPage(): void
+ {
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ $request = $this->createMock(Request::class);
+ $request->method('getParam')->willReturnCallback(function (string $key) {
+ if ('page' === $key) {
+ return '2';
+ }
+
+ return null;
+ });
+ $response = new Response();
+
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('search')
+ ->with(
+ ['searchtags' => '', 'searchterm' => ''],
+ null,
+ false,
+ false
+ )
+ ->willReturn([
+ (new Bookmark())->setId(1)->setUrl('http://url1.tld')->setTitle('Title 1'),
+ (new Bookmark())->setId(2)->setUrl('http://url2.tld')->setTitle('Title 2'),
+ (new Bookmark())->setId(3)->setUrl('http://url3.tld')->setTitle('Title 3'),
+ ])
+ ;
+
+ $this->container->sessionManager
+ ->method('getSessionParameter')
+ ->willReturnCallback(function (string $parameter, $default = null) {
+ if ('LINKS_PER_PAGE' === $parameter) {
+ return 2;
+ }
+
+ return $default;
+ })
+ ;
+
+ $result = $this->controller->index($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('linklist', (string) $result->getBody());
+
+ static::assertSame('Shaarli', $assignedVariables['pagetitle']);
+ static::assertSame('', $assignedVariables['previous_page_url']);
+ static::assertSame('?page=1', $assignedVariables['next_page_url']);
+ static::assertSame(2, $assignedVariables['page_max']);
+ static::assertSame('', $assignedVariables['search_tags']);
+ static::assertSame(3, $assignedVariables['result_count']);
+ static::assertSame(2, $assignedVariables['page_current']);
+ static::assertSame('', $assignedVariables['search_term']);
+ static::assertNull($assignedVariables['visibility']);
+ static::assertCount(1, $assignedVariables['links']);
+
+ $link = $assignedVariables['links'][2];
+
+ static::assertSame(3, $link['id']);
+ static::assertSame('http://url3.tld', $link['url']);
+ static::assertSame('Title 3', $link['title']);
+ }
+
+ /**
+ * Test rendering list of bookmarks with filters.
+ */
+ public function testIndexDefaultWithFilters(): void
+ {
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ $request = $this->createMock(Request::class);
+ $request->method('getParam')->willReturnCallback(function (string $key) {
+ if ('searchtags' === $key) {
+ return 'abc def';
+ }
+ if ('searchterm' === $key) {
+ return 'ghi jkl';
+ }
+
+ return null;
+ });
+ $response = new Response();
+
+ $this->container->sessionManager
+ ->method('getSessionParameter')
+ ->willReturnCallback(function (string $key, $default) {
+ if ('LINKS_PER_PAGE' === $key) {
+ return 2;
+ }
+ if ('visibility' === $key) {
+ return 'private';
+ }
+ if ('untaggedonly' === $key) {
+ return true;
+ }
+
+ return $default;
+ })
+ ;
+
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('search')
+ ->with(
+ ['searchtags' => 'abc def', 'searchterm' => 'ghi jkl'],
+ 'private',
+ false,
+ true
+ )
+ ->willReturn([
+ (new Bookmark())->setId(1)->setUrl('http://url1.tld')->setTitle('Title 1'),
+ (new Bookmark())->setId(2)->setUrl('http://url2.tld')->setTitle('Title 2'),
+ (new Bookmark())->setId(3)->setUrl('http://url3.tld')->setTitle('Title 3'),
+ ])
+ ;
+
+ $result = $this->controller->index($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('linklist', (string) $result->getBody());
+
+ static::assertSame('Search: ghi jkl [abc] [def] - Shaarli', $assignedVariables['pagetitle']);
+ static::assertSame('?page=2&searchterm=ghi+jkl&searchtags=abc+def', $assignedVariables['previous_page_url']);
+ }
+
+ /**
+ * Test displaying a permalink with valid parameters
+ */
+ public function testPermalinkValid(): void
+ {
+ $hash = 'abcdef';
+
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('findByHash')
+ ->with($hash)
+ ->willReturn((new Bookmark())->setId(123)->setTitle('Title 1')->setUrl('http://url1.tld'))
+ ;
+
+ $result = $this->controller->permalink($request, $response, ['hash' => $hash]);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('linklist', (string) $result->getBody());
+
+ static::assertSame('Title 1 - Shaarli', $assignedVariables['pagetitle']);
+ static::assertCount(1, $assignedVariables['links']);
+
+ $link = $assignedVariables['links'][0];
+
+ static::assertSame(123, $link['id']);
+ static::assertSame('http://url1.tld', $link['url']);
+ static::assertSame('Title 1', $link['title']);
+ }
+
+ /**
+ * Test displaying a permalink with an unknown small hash : renders a 404 template error
+ */
+ public function testPermalinkNotFound(): void
+ {
+ $hash = 'abcdef';
+
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('findByHash')
+ ->with($hash)
+ ->willThrowException(new BookmarkNotFoundException())
+ ;
+
+ $result = $this->controller->permalink($request, $response, ['hash' => $hash]);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('404', (string) $result->getBody());
+
+ static::assertSame(
+ 'The link you are trying to reach does not exist or has been deleted.',
+ $assignedVariables['error_message']
+ );
+ }
+
+ /**
+ * Test getting link list with thumbnail updates.
+ * -> 2 thumbnails update, only 1 datastore write
+ */
+ public function testThumbnailUpdateFromLinkList(): void
+ {
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->container->loginManager = $this->createMock(LoginManager::class);
+ $this->container->loginManager->method('isLoggedIn')->willReturn(true);
+
+ $this->container->conf = $this->createMock(ConfigManager::class);
+ $this->container->conf
+ ->method('get')
+ ->willReturnCallback(function (string $key, $default) {
+ return $key === 'thumbnails.mode' ? Thumbnailer::MODE_ALL : $default;
+ })
+ ;
+
+ $this->container->thumbnailer = $this->createMock(Thumbnailer::class);
+ $this->container->thumbnailer
+ ->expects(static::exactly(2))
+ ->method('get')
+ ->withConsecutive(['https://url2.tld'], ['https://url4.tld'])
+ ;
+
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('search')
+ ->willReturn([
+ (new Bookmark())->setId(1)->setUrl('https://url1.tld')->setTitle('Title 1')->setThumbnail(false),
+ $b1 = (new Bookmark())->setId(2)->setUrl('https://url2.tld')->setTitle('Title 2'),
+ (new Bookmark())->setId(3)->setUrl('https://url3.tld')->setTitle('Title 3')->setThumbnail(false),
+ $b2 = (new Bookmark())->setId(2)->setUrl('https://url4.tld')->setTitle('Title 4'),
+ (new Bookmark())->setId(2)->setUrl('ftp://url5.tld', ['ftp'])->setTitle('Title 5'),
+ ])
+ ;
+ $this->container->bookmarkService
+ ->expects(static::exactly(2))
+ ->method('set')
+ ->withConsecutive([$b1, false], [$b2, false])
+ ;
+ $this->container->bookmarkService->expects(static::once())->method('save');
+
+ $result = $this->controller->index($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('linklist', (string) $result->getBody());
+ }
+
+ /**
+ * Test getting a permalink with thumbnail update.
+ */
+ public function testThumbnailUpdateFromPermalink(): void
+ {
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->container->loginManager = $this->createMock(LoginManager::class);
+ $this->container->loginManager->method('isLoggedIn')->willReturn(true);
+
+ $this->container->conf = $this->createMock(ConfigManager::class);
+ $this->container->conf
+ ->method('get')
+ ->willReturnCallback(function (string $key, $default) {
+ return $key === 'thumbnails.mode' ? Thumbnailer::MODE_ALL : $default;
+ })
+ ;
+
+ $this->container->thumbnailer = $this->createMock(Thumbnailer::class);
+ $this->container->thumbnailer->expects(static::once())->method('get')->withConsecutive(['https://url.tld']);
+
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('findByHash')
+ ->willReturn($bookmark = (new Bookmark())->setId(2)->setUrl('https://url.tld')->setTitle('Title 1'))
+ ;
+ $this->container->bookmarkService->expects(static::once())->method('set')->with($bookmark, true);
+ $this->container->bookmarkService->expects(static::never())->method('save');
+
+ $result = $this->controller->permalink($request, $response, ['hash' => 'abc']);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('linklist', (string) $result->getBody());
+ }
+
+ /**
+ * Trigger legacy controller in link list controller: permalink
+ */
+ public function testLegacyControllerPermalink(): void
+ {
+ $hash = 'abcdef';
+ $this->container->environment['QUERY_STRING'] = $hash;
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $result = $this->controller->index($request, $response);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame('/subfolder/shaare/' . $hash, $result->getHeader('location')[0]);
+ }
+
+ /**
+ * Trigger legacy controller in link list controller: ?do= query parameter
+ */
+ public function testLegacyControllerDoPage(): void
+ {
+ $request = $this->createMock(Request::class);
+ $request->method('getQueryParam')->with('do')->willReturn('picwall');
+ $response = new Response();
+
+ $result = $this->controller->index($request, $response);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame('/subfolder/picture-wall', $result->getHeader('location')[0]);
+ }
+
+ /**
+ * Trigger legacy controller in link list controller: ?do= query parameter with unknown legacy route
+ */
+ public function testLegacyControllerUnknownDoPage(): void
+ {
+ $request = $this->createMock(Request::class);
+ $request->method('getQueryParam')->with('do')->willReturn('nope');
+ $response = new Response();
+
+ $result = $this->controller->index($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('linklist', (string) $result->getBody());
+ }
+
+ /**
+ * Trigger legacy controller in link list controller: other GET route (e.g. ?post)
+ */
+ public function testLegacyControllerGetParameter(): void
+ {
+ $request = $this->createMock(Request::class);
+ $request->method('getQueryParams')->willReturn(['post' => $url = 'http://url.tld']);
+ $response = new Response();
+
+ $this->container->loginManager = $this->createMock(LoginManager::class);
+ $this->container->loginManager->method('isLoggedIn')->willReturn(true);
+
+ $result = $this->controller->index($request, $response);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(
+ '/subfolder/admin/shaare?post=' . urlencode($url),
+ $result->getHeader('location')[0]
+ );
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Visitor;
+
+use PHPUnit\Framework\TestCase;
+use Shaarli\Bookmark\Bookmark;
+use Shaarli\Feed\CachedPage;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+class DailyControllerTest extends TestCase
+{
+ use FrontControllerMockHelper;
+
+ /** @var DailyController */
+ protected $controller;
+
+ public function setUp(): void
+ {
+ $this->createContainer();
+
+ $this->controller = new DailyController($this->container);
+ DailyController::$DAILY_RSS_NB_DAYS = 2;
+ }
+
+ public function testValidIndexControllerInvokeDefault(): void
+ {
+ $currentDay = new \DateTimeImmutable('2020-05-13');
+
+ $request = $this->createMock(Request::class);
+ $request->method('getQueryParam')->willReturn($currentDay->format('Ymd'));
+ $response = new Response();
+
+ // Save RainTPL assigned variables
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ // Links dataset: 2 links with thumbnails
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('days')
+ ->willReturnCallback(function () use ($currentDay): array {
+ return [
+ '20200510',
+ $currentDay->format('Ymd'),
+ '20200516',
+ ];
+ })
+ ;
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('filterDay')
+ ->willReturnCallback(function (): array {
+ return [
+ (new Bookmark())
+ ->setId(1)
+ ->setUrl('http://url.tld')
+ ->setTitle(static::generateString(50))
+ ->setDescription(static::generateString(500))
+ ,
+ (new Bookmark())
+ ->setId(2)
+ ->setUrl('http://url2.tld')
+ ->setTitle(static::generateString(50))
+ ->setDescription(static::generateString(500))
+ ,
+ (new Bookmark())
+ ->setId(3)
+ ->setUrl('http://url3.tld')
+ ->setTitle(static::generateString(50))
+ ->setDescription(static::generateString(500))
+ ,
+ ];
+ })
+ ;
+
+ // Make sure that PluginManager hook is triggered
+ $this->container->pluginManager
+ ->expects(static::at(0))
+ ->method('executeHooks')
+ ->willReturnCallback(function (string $hook, array $data, array $param) use ($currentDay): array {
+ static::assertSame('render_daily', $hook);
+
+ static::assertArrayHasKey('linksToDisplay', $data);
+ static::assertCount(3, $data['linksToDisplay']);
+ static::assertSame(1, $data['linksToDisplay'][0]['id']);
+ static::assertSame($currentDay->getTimestamp(), $data['day']);
+ static::assertSame('20200510', $data['previousday']);
+ static::assertSame('20200516', $data['nextday']);
+
+ static::assertArrayHasKey('loggedin', $param);
+
+ return $data;
+ })
+ ;
+
+ $result = $this->controller->index($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('daily', (string) $result->getBody());
+ static::assertSame(
+ 'Daily - '. format_date($currentDay, false, true) .' - Shaarli',
+ $assignedVariables['pagetitle']
+ );
+ static::assertEquals($currentDay, $assignedVariables['dayDate']);
+ static::assertEquals($currentDay->getTimestamp(), $assignedVariables['day']);
+ static::assertCount(3, $assignedVariables['linksToDisplay']);
+
+ $link = $assignedVariables['linksToDisplay'][0];
+
+ static::assertSame(1, $link['id']);
+ static::assertSame('http://url.tld', $link['url']);
+ static::assertNotEmpty($link['title']);
+ static::assertNotEmpty($link['description']);
+ static::assertNotEmpty($link['formatedDescription']);
+
+ $link = $assignedVariables['linksToDisplay'][1];
+
+ static::assertSame(2, $link['id']);
+ static::assertSame('http://url2.tld', $link['url']);
+ static::assertNotEmpty($link['title']);
+ static::assertNotEmpty($link['description']);
+ static::assertNotEmpty($link['formatedDescription']);
+
+ $link = $assignedVariables['linksToDisplay'][2];
+
+ static::assertSame(3, $link['id']);
+ static::assertSame('http://url3.tld', $link['url']);
+ static::assertNotEmpty($link['title']);
+ static::assertNotEmpty($link['description']);
+ static::assertNotEmpty($link['formatedDescription']);
+
+ static::assertCount(3, $assignedVariables['cols']);
+ static::assertCount(1, $assignedVariables['cols'][0]);
+ static::assertCount(1, $assignedVariables['cols'][1]);
+ static::assertCount(1, $assignedVariables['cols'][2]);
+
+ $link = $assignedVariables['cols'][0][0];
+
+ static::assertSame(1, $link['id']);
+ static::assertSame('http://url.tld', $link['url']);
+ static::assertNotEmpty($link['title']);
+ static::assertNotEmpty($link['description']);
+ static::assertNotEmpty($link['formatedDescription']);
+
+ $link = $assignedVariables['cols'][1][0];
+
+ static::assertSame(2, $link['id']);
+ static::assertSame('http://url2.tld', $link['url']);
+ static::assertNotEmpty($link['title']);
+ static::assertNotEmpty($link['description']);
+ static::assertNotEmpty($link['formatedDescription']);
+
+ $link = $assignedVariables['cols'][2][0];
+
+ static::assertSame(3, $link['id']);
+ static::assertSame('http://url3.tld', $link['url']);
+ static::assertNotEmpty($link['title']);
+ static::assertNotEmpty($link['description']);
+ static::assertNotEmpty($link['formatedDescription']);
+ }
+
+ /**
+ * Daily page - test that everything goes fine with no future or past bookmarks
+ */
+ public function testValidIndexControllerInvokeNoFutureOrPast(): void
+ {
+ $currentDay = new \DateTimeImmutable('2020-05-13');
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ // Save RainTPL assigned variables
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ // Links dataset: 2 links with thumbnails
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('days')
+ ->willReturnCallback(function () use ($currentDay): array {
+ return [
+ $currentDay->format($currentDay->format('Ymd')),
+ ];
+ })
+ ;
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('filterDay')
+ ->willReturnCallback(function (): array {
+ return [
+ (new Bookmark())
+ ->setId(1)
+ ->setUrl('http://url.tld')
+ ->setTitle(static::generateString(50))
+ ->setDescription(static::generateString(500))
+ ,
+ ];
+ })
+ ;
+
+ // Make sure that PluginManager hook is triggered
+ $this->container->pluginManager
+ ->expects(static::at(0))
+ ->method('executeHooks')
+ ->willReturnCallback(function (string $hook, array $data, array $param) use ($currentDay): array {
+ static::assertSame('render_daily', $hook);
+
+ static::assertArrayHasKey('linksToDisplay', $data);
+ static::assertCount(1, $data['linksToDisplay']);
+ static::assertSame(1, $data['linksToDisplay'][0]['id']);
+ static::assertSame($currentDay->getTimestamp(), $data['day']);
+ static::assertEmpty($data['previousday']);
+ static::assertEmpty($data['nextday']);
+
+ static::assertArrayHasKey('loggedin', $param);
+
+ return $data;
+ });
+
+ $result = $this->controller->index($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('daily', (string) $result->getBody());
+ static::assertSame(
+ 'Daily - '. format_date($currentDay, false, true) .' - Shaarli',
+ $assignedVariables['pagetitle']
+ );
+ static::assertCount(1, $assignedVariables['linksToDisplay']);
+
+ $link = $assignedVariables['linksToDisplay'][0];
+ static::assertSame(1, $link['id']);
+ }
+
+ /**
+ * Daily page - test that height adjustment in columns is working
+ */
+ public function testValidIndexControllerInvokeHeightAdjustment(): void
+ {
+ $currentDay = new \DateTimeImmutable('2020-05-13');
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ // Save RainTPL assigned variables
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ // Links dataset: 2 links with thumbnails
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('days')
+ ->willReturnCallback(function () use ($currentDay): array {
+ return [
+ $currentDay->format($currentDay->format('Ymd')),
+ ];
+ })
+ ;
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('filterDay')
+ ->willReturnCallback(function (): array {
+ return [
+ (new Bookmark())->setId(1)->setUrl('http://url.tld')->setTitle('title'),
+ (new Bookmark())
+ ->setId(2)
+ ->setUrl('http://url.tld')
+ ->setTitle(static::generateString(50))
+ ->setDescription(static::generateString(5000))
+ ,
+ (new Bookmark())->setId(3)->setUrl('http://url.tld')->setTitle('title'),
+ (new Bookmark())->setId(4)->setUrl('http://url.tld')->setTitle('title'),
+ (new Bookmark())->setId(5)->setUrl('http://url.tld')->setTitle('title'),
+ (new Bookmark())->setId(6)->setUrl('http://url.tld')->setTitle('title'),
+ (new Bookmark())->setId(7)->setUrl('http://url.tld')->setTitle('title'),
+ ];
+ })
+ ;
+
+ // Make sure that PluginManager hook is triggered
+ $this->container->pluginManager
+ ->expects(static::at(0))
+ ->method('executeHooks')
+ ->willReturnCallback(function (string $hook, array $data, array $param): array {
+ return $data;
+ })
+ ;
+
+ $result = $this->controller->index($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('daily', (string) $result->getBody());
+ static::assertCount(7, $assignedVariables['linksToDisplay']);
+
+ $columnIds = function (array $column): array {
+ return array_map(function (array $item): int { return $item['id']; }, $column);
+ };
+
+ static::assertSame([1, 4, 6], $columnIds($assignedVariables['cols'][0]));
+ static::assertSame([2], $columnIds($assignedVariables['cols'][1]));
+ static::assertSame([3, 5, 7], $columnIds($assignedVariables['cols'][2]));
+ }
+
+ /**
+ * Daily page - no bookmark
+ */
+ public function testValidIndexControllerInvokeNoBookmark(): void
+ {
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ // Save RainTPL assigned variables
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ // Links dataset: 2 links with thumbnails
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('days')
+ ->willReturnCallback(function (): array {
+ return [];
+ })
+ ;
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('filterDay')
+ ->willReturnCallback(function (): array {
+ return [];
+ })
+ ;
+
+ // Make sure that PluginManager hook is triggered
+ $this->container->pluginManager
+ ->expects(static::at(0))
+ ->method('executeHooks')
+ ->willReturnCallback(function (string $hook, array $data, array $param): array {
+ return $data;
+ })
+ ;
+
+ $result = $this->controller->index($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('daily', (string) $result->getBody());
+ static::assertCount(0, $assignedVariables['linksToDisplay']);
+ static::assertSame('Today', $assignedVariables['dayDesc']);
+ static::assertEquals((new \DateTime())->setTime(0, 0)->getTimestamp(), $assignedVariables['day']);
+ static::assertEquals((new \DateTime())->setTime(0, 0), $assignedVariables['dayDate']);
+ }
+
+ /**
+ * Daily RSS - default behaviour
+ */
+ public function testValidRssControllerInvokeDefault(): void
+ {
+ $dates = [
+ new \DateTimeImmutable('2020-05-17'),
+ new \DateTimeImmutable('2020-05-15'),
+ new \DateTimeImmutable('2020-05-13'),
+ ];
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->container->bookmarkService->expects(static::once())->method('search')->willReturn([
+ (new Bookmark())->setId(1)->setCreated($dates[0])->setUrl('http://domain.tld/1'),
+ (new Bookmark())->setId(2)->setCreated($dates[1])->setUrl('http://domain.tld/2'),
+ (new Bookmark())->setId(3)->setCreated($dates[1])->setUrl('http://domain.tld/3'),
+ (new Bookmark())->setId(4)->setCreated($dates[2])->setUrl('http://domain.tld/4'),
+ ]);
+
+ $this->container->pageCacheManager
+ ->expects(static::once())
+ ->method('getCachePage')
+ ->willReturnCallback(function (): CachedPage {
+ $cachedPage = $this->createMock(CachedPage::class);
+ $cachedPage->expects(static::once())->method('cache')->with('dailyrss');
+
+ return $cachedPage;
+ }
+ );
+
+ // Save RainTPL assigned variables
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ $result = $this->controller->rss($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertStringContainsString('application/rss', $result->getHeader('Content-Type')[0]);
+ static::assertSame('dailyrss', (string) $result->getBody());
+ static::assertSame('Shaarli', $assignedVariables['title']);
+ static::assertSame('http://shaarli', $assignedVariables['index_url']);
+ static::assertSame('http://shaarli/daily-rss', $assignedVariables['page_url']);
+ static::assertFalse($assignedVariables['hide_timestamps']);
+ static::assertCount(2, $assignedVariables['days']);
+
+ $day = $assignedVariables['days'][$dates[0]->format('Ymd')];
+
+ static::assertEquals($dates[0], $day['date']);
+ static::assertSame($dates[0]->format(\DateTime::RSS), $day['date_rss']);
+ static::assertSame(format_date($dates[0], false), $day['date_human']);
+ static::assertSame('http://shaarli/daily?day='. $dates[0]->format('Ymd'), $day['absolute_url']);
+ static::assertCount(1, $day['links']);
+ static::assertSame(1, $day['links'][0]['id']);
+ static::assertSame('http://domain.tld/1', $day['links'][0]['url']);
+ static::assertEquals($dates[0], $day['links'][0]['created']);
+
+ $day = $assignedVariables['days'][$dates[1]->format('Ymd')];
+
+ static::assertEquals($dates[1], $day['date']);
+ static::assertSame($dates[1]->format(\DateTime::RSS), $day['date_rss']);
+ static::assertSame(format_date($dates[1], false), $day['date_human']);
+ static::assertSame('http://shaarli/daily?day='. $dates[1]->format('Ymd'), $day['absolute_url']);
+ static::assertCount(2, $day['links']);
+
+ static::assertSame(2, $day['links'][0]['id']);
+ static::assertSame('http://domain.tld/2', $day['links'][0]['url']);
+ static::assertEquals($dates[1], $day['links'][0]['created']);
+ static::assertSame(3, $day['links'][1]['id']);
+ static::assertSame('http://domain.tld/3', $day['links'][1]['url']);
+ static::assertEquals($dates[1], $day['links'][1]['created']);
+ }
+
+ /**
+ * Daily RSS - trigger cache rendering
+ */
+ public function testValidRssControllerInvokeTriggerCache(): void
+ {
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->container->pageCacheManager->method('getCachePage')->willReturnCallback(function (): CachedPage {
+ $cachedPage = $this->createMock(CachedPage::class);
+ $cachedPage->method('cachedVersion')->willReturn('this is cache!');
+
+ return $cachedPage;
+ });
+
+ $this->container->bookmarkService->expects(static::never())->method('search');
+
+ $result = $this->controller->rss($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertStringContainsString('application/rss', $result->getHeader('Content-Type')[0]);
+ static::assertSame('this is cache!', (string) $result->getBody());
+ }
+
+ /**
+ * Daily RSS - No bookmark
+ */
+ public function testValidRssControllerInvokeNoBookmark(): void
+ {
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->container->bookmarkService->expects(static::once())->method('search')->willReturn([]);
+
+ // Save RainTPL assigned variables
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ $result = $this->controller->rss($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertStringContainsString('application/rss', $result->getHeader('Content-Type')[0]);
+ static::assertSame('dailyrss', (string) $result->getBody());
+ static::assertSame('Shaarli', $assignedVariables['title']);
+ static::assertSame('http://shaarli', $assignedVariables['index_url']);
+ static::assertSame('http://shaarli/daily-rss', $assignedVariables['page_url']);
+ static::assertFalse($assignedVariables['hide_timestamps']);
+ static::assertCount(0, $assignedVariables['days']);
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Visitor;
+
+use PHPUnit\Framework\TestCase;
+use Shaarli\Front\Exception\ShaarliFrontException;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+class ErrorControllerTest extends TestCase
+{
+ use FrontControllerMockHelper;
+
+ /** @var ErrorController */
+ protected $controller;
+
+ public function setUp(): void
+ {
+ $this->createContainer();
+
+ $this->controller = new ErrorController($this->container);
+ }
+
+ /**
+ * Test displaying error with a ShaarliFrontException: display exception message and use its code for HTTTP code
+ */
+ public function testDisplayFrontExceptionError(): void
+ {
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $message = 'error message';
+ $errorCode = 418;
+
+ // Save RainTPL assigned variables
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ $result = ($this->controller)(
+ $request,
+ $response,
+ new class($message, $errorCode) extends ShaarliFrontException {}
+ );
+
+ static::assertSame($errorCode, $result->getStatusCode());
+ static::assertSame($message, $assignedVariables['message']);
+ static::assertArrayNotHasKey('stacktrace', $assignedVariables);
+ }
+
+ /**
+ * Test displaying error with any exception (no debug): only display an error occurred with HTTP 500.
+ */
+ public function testDisplayAnyExceptionErrorNoDebug(): void
+ {
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ // Save RainTPL assigned variables
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ $result = ($this->controller)($request, $response, new \Exception('abc'));
+
+ static::assertSame(500, $result->getStatusCode());
+ static::assertSame('An unexpected error occurred.', $assignedVariables['message']);
+ static::assertArrayNotHasKey('stacktrace', $assignedVariables);
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Visitor;
+
+use PHPUnit\Framework\TestCase;
+use Shaarli\Feed\FeedBuilder;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+class FeedControllerTest extends TestCase
+{
+ use FrontControllerMockHelper;
+
+ /** @var FeedController */
+ protected $controller;
+
+ public function setUp(): void
+ {
+ $this->createContainer();
+
+ $this->container->feedBuilder = $this->createMock(FeedBuilder::class);
+
+ $this->controller = new FeedController($this->container);
+ }
+
+ /**
+ * Feed Controller - RSS default behaviour
+ */
+ public function testDefaultRssController(): void
+ {
+ $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
+ {
+ $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
+ {
+ $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']);
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Visitor;
+
+use PHPUnit\Framework\MockObject\MockObject;
+use Shaarli\Bookmark\BookmarkServiceInterface;
+use Shaarli\Config\ConfigManager;
+use Shaarli\Container\ShaarliTestContainer;
+use Shaarli\Formatter\BookmarkFormatter;
+use Shaarli\Formatter\BookmarkRawFormatter;
+use Shaarli\Formatter\FormatterFactory;
+use Shaarli\Plugin\PluginManager;
+use Shaarli\Render\PageBuilder;
+use Shaarli\Render\PageCacheManager;
+use Shaarli\Security\LoginManager;
+use Shaarli\Security\SessionManager;
+
+/**
+ * Trait FrontControllerMockHelper
+ *
+ * Helper trait used to initialize the ShaarliContainer and mock its services for controller tests.
+ *
+ * @property ShaarliTestContainer $container
+ * @package Shaarli\Front\Controller
+ */
+trait FrontControllerMockHelper
+{
+ /** @var ShaarliTestContainer */
+ protected $container;
+
+ /**
+ * Mock the container instance and initialize container's services used by tests
+ */
+ protected function createContainer(): void
+ {
+ $this->container = $this->createMock(ShaarliTestContainer::class);
+
+ $this->container->loginManager = $this->createMock(LoginManager::class);
+
+ // Config
+ $this->container->conf = $this->createMock(ConfigManager::class);
+ $this->container->conf->method('get')->willReturnCallback(function (string $parameter, $default) {
+ return $default === null ? $parameter : $default;
+ });
+
+ // PageBuilder
+ $this->container->pageBuilder = $this->createMock(PageBuilder::class);
+ $this->container->pageBuilder
+ ->method('render')
+ ->willReturnCallback(function (string $template): string {
+ return $template;
+ })
+ ;
+
+ // Plugin Manager
+ $this->container->pluginManager = $this->createMock(PluginManager::class);
+
+ // BookmarkService
+ $this->container->bookmarkService = $this->createMock(BookmarkServiceInterface::class);
+
+ // Formatter
+ $this->container->formatterFactory = $this->createMock(FormatterFactory::class);
+ $this->container->formatterFactory
+ ->method('getFormatter')
+ ->willReturnCallback(function (): BookmarkFormatter {
+ return new BookmarkRawFormatter($this->container->conf, true);
+ })
+ ;
+
+ // CacheManager
+ $this->container->pageCacheManager = $this->createMock(PageCacheManager::class);
+
+ // SessionManager
+ $this->container->sessionManager = $this->createMock(SessionManager::class);
+
+ // $_SERVER
+ $this->container->environment = [
+ 'SERVER_NAME' => 'shaarli',
+ 'SERVER_PORT' => '80',
+ 'REQUEST_URI' => '/daily-rss',
+ 'REMOTE_ADDR' => '1.2.3.4',
+ ];
+
+ $this->container->basePath = '/subfolder';
+ }
+
+ /**
+ * Pass a reference of an array which will be populated by `pageBuilder->assign` calls during execution.
+ *
+ * @param mixed $variables Array reference to populate.
+ */
+ 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;
+ })
+ ;
+ }
+
+ protected static function generateString(int $length): string
+ {
+ // bin2hex(random_bytes) generates string twice as long as given parameter
+ $length = (int) ceil($length / 2);
+
+ return bin2hex(random_bytes($length));
+ }
+
+ /**
+ * Force to be used in PHPUnit context.
+ */
+ protected abstract function createMock($originalClassName): MockObject;
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Visitor;
+
+use PHPUnit\Framework\TestCase;
+use Shaarli\Config\ConfigManager;
+use Shaarli\Front\Exception\AlreadyInstalledException;
+use Shaarli\Security\SessionManager;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+class InstallControllerTest extends TestCase
+{
+ use FrontControllerMockHelper;
+
+ const MOCK_FILE = '.tmp';
+
+ /** @var InstallController */
+ protected $controller;
+
+ public function setUp(): void
+ {
+ $this->createContainer();
+
+ $this->container->conf = $this->createMock(ConfigManager::class);
+ $this->container->conf->method('getConfigFileExt')->willReturn(static::MOCK_FILE);
+ $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) {
+ if ($key === 'resource.raintpl_tpl') {
+ return '.';
+ }
+
+ return $default ?? $key;
+ });
+
+ $this->controller = new InstallController($this->container);
+ }
+
+ protected function tearDown(): void
+ {
+ if (file_exists(static::MOCK_FILE)) {
+ unlink(static::MOCK_FILE);
+ }
+ }
+
+ /**
+ * Test displaying install page with valid session.
+ */
+ public function testInstallIndexWithValidSession(): void
+ {
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->container->sessionManager = $this->createMock(SessionManager::class);
+ $this->container->sessionManager
+ ->method('getSessionParameter')
+ ->willReturnCallback(function (string $key, $default) {
+ return $key === 'session_tested' ? 'Working' : $default;
+ })
+ ;
+
+ $result = $this->controller->index($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('install', (string) $result->getBody());
+
+ static::assertIsArray($assignedVariables['continents']);
+ static::assertSame('Africa', $assignedVariables['continents'][0]);
+ static::assertSame('UTC', $assignedVariables['continents']['selected']);
+
+ static::assertIsArray($assignedVariables['cities']);
+ static::assertSame(['continent' => 'Africa', 'city' => 'Abidjan'], $assignedVariables['cities'][0]);
+ static::assertSame('UTC', $assignedVariables['continents']['selected']);
+
+ static::assertIsArray($assignedVariables['languages']);
+ static::assertSame('Automatic', $assignedVariables['languages']['auto']);
+ static::assertSame('French', $assignedVariables['languages']['fr']);
+ }
+
+ /**
+ * Instantiate the install controller with an existing config file: exception.
+ */
+ public function testInstallWithExistingConfigFile(): void
+ {
+ $this->expectException(AlreadyInstalledException::class);
+
+ touch(static::MOCK_FILE);
+
+ $this->controller = new InstallController($this->container);
+ }
+
+ /**
+ * Call controller without session yet defined, redirect to test session install page.
+ */
+ public function testInstallRedirectToSessionTest(): void
+ {
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->container->sessionManager = $this->createMock(SessionManager::class);
+ $this->container->sessionManager
+ ->expects(static::once())
+ ->method('setSessionParameter')
+ ->with(InstallController::SESSION_TEST_KEY, InstallController::SESSION_TEST_VALUE)
+ ;
+
+ $result = $this->controller->index($request, $response);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame('/subfolder/install/session-test', $result->getHeader('location')[0]);
+ }
+
+ /**
+ * Call controller in session test mode: valid session then redirect to install page.
+ */
+ public function testInstallSessionTestValid(): void
+ {
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->container->sessionManager = $this->createMock(SessionManager::class);
+ $this->container->sessionManager
+ ->method('getSessionParameter')
+ ->with(InstallController::SESSION_TEST_KEY)
+ ->willReturn(InstallController::SESSION_TEST_VALUE)
+ ;
+
+ $result = $this->controller->sessionTest($request, $response);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame('/subfolder/install', $result->getHeader('location')[0]);
+ }
+
+ /**
+ * Call controller in session test mode: invalid session then redirect to error page.
+ */
+ public function testInstallSessionTestError(): void
+ {
+ $assignedVars = [];
+ $this->assignTemplateVars($assignedVars);
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->container->sessionManager = $this->createMock(SessionManager::class);
+ $this->container->sessionManager
+ ->method('getSessionParameter')
+ ->with(InstallController::SESSION_TEST_KEY)
+ ->willReturn('KO')
+ ;
+
+ $result = $this->controller->sessionTest($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('error', (string) $result->getBody());
+ static::assertStringStartsWith(
+ '<pre>Sessions do not seem to work correctly on your server',
+ $assignedVars['message']
+ );
+ }
+
+ /**
+ * Test saving valid data from install form. Also initialize datastore.
+ */
+ public function testSaveInstallValid(): void
+ {
+ $providedParameters = [
+ 'continent' => 'Europe',
+ 'city' => 'Berlin',
+ 'setlogin' => 'bob',
+ 'setpassword' => 'password',
+ 'title' => 'Shaarli',
+ 'language' => 'fr',
+ 'updateCheck' => true,
+ 'enableApi' => true,
+ ];
+
+ $expectedSettings = [
+ 'general.timezone' => 'Europe/Berlin',
+ 'credentials.login' => 'bob',
+ 'credentials.salt' => '_NOT_EMPTY',
+ 'credentials.hash' => '_NOT_EMPTY',
+ 'general.title' => 'Shaarli',
+ 'translation.language' => 'en',
+ 'updates.check_updates' => true,
+ 'api.enabled' => true,
+ 'api.secret' => '_NOT_EMPTY',
+ 'general.header_link' => '/subfolder',
+ ];
+
+ $request = $this->createMock(Request::class);
+ $request->method('getParam')->willReturnCallback(function (string $key) use ($providedParameters) {
+ return $providedParameters[$key] ?? null;
+ });
+ $response = new Response();
+
+ $this->container->conf = $this->createMock(ConfigManager::class);
+ $this->container->conf
+ ->method('get')
+ ->willReturnCallback(function (string $key, $value) {
+ if ($key === 'credentials.login') {
+ return 'bob';
+ } elseif ($key === 'credentials.salt') {
+ return 'salt';
+ }
+
+ return $value;
+ })
+ ;
+ $this->container->conf
+ ->expects(static::exactly(count($expectedSettings)))
+ ->method('set')
+ ->willReturnCallback(function (string $key, $value) use ($expectedSettings) {
+ if ($expectedSettings[$key] ?? null === '_NOT_EMPTY') {
+ static::assertNotEmpty($value);
+ } else {
+ static::assertSame($expectedSettings[$key], $value);
+ }
+ })
+ ;
+ $this->container->conf->expects(static::once())->method('write');
+
+ $this->container->sessionManager
+ ->expects(static::once())
+ ->method('setSessionParameter')
+ ->with(SessionManager::KEY_SUCCESS_MESSAGES)
+ ;
+
+ $result = $this->controller->save($request, $response);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame('/subfolder/login', $result->getHeader('location')[0]);
+ }
+
+ /**
+ * Test default settings (timezone and title).
+ * Also check that bookmarks are not initialized if
+ */
+ public function testSaveInstallDefaultValues(): void
+ {
+ $confSettings = [];
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->container->conf->method('set')->willReturnCallback(function (string $key, $value) use (&$confSettings) {
+ $confSettings[$key] = $value;
+ });
+
+ $result = $this->controller->save($request, $response);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame('/subfolder/login', $result->getHeader('location')[0]);
+
+ static::assertSame('UTC', $confSettings['general.timezone']);
+ static::assertSame('Shared bookmarks on http://shaarli', $confSettings['general.title']);
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Visitor;
+
+use PHPUnit\Framework\TestCase;
+use Shaarli\Config\ConfigManager;
+use Shaarli\Front\Exception\LoginBannedException;
+use Shaarli\Front\Exception\WrongTokenException;
+use Shaarli\Render\TemplatePage;
+use Shaarli\Security\CookieManager;
+use Shaarli\Security\SessionManager;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+class LoginControllerTest extends TestCase
+{
+ use FrontControllerMockHelper;
+
+ /** @var LoginController */
+ protected $controller;
+
+ public function setUp(): void
+ {
+ $this->createContainer();
+
+ $this->container->cookieManager = $this->createMock(CookieManager::class);
+ $this->container->sessionManager->method('checkToken')->willReturn(true);
+
+ $this->controller = new LoginController($this->container);
+ }
+
+ /**
+ * Test displaying login form with valid parameters.
+ */
+ public function testValidControllerInvoke(): void
+ {
+ $request = $this->createMock(Request::class);
+ $request
+ ->expects(static::atLeastOnce())
+ ->method('getParam')
+ ->willReturnCallback(function (string $key) {
+ return 'returnurl' === $key ? '> referer' : null;
+ })
+ ;
+ $response = new Response();
+
+ $assignedVariables = [];
+ $this->container->pageBuilder
+ ->method('assign')
+ ->willReturnCallback(function ($key, $value) use (&$assignedVariables) {
+ $assignedVariables[$key] = $value;
+
+ return $this;
+ })
+ ;
+
+ $this->container->loginManager->method('canLogin')->willReturn(true);
+
+ $result = $this->controller->index($request, $response);
+
+ static::assertInstanceOf(Response::class, $result);
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame(TemplatePage::LOGIN, (string) $result->getBody());
+
+ static::assertSame('> referer', $assignedVariables['returnurl']);
+ static::assertSame(true, $assignedVariables['remember_user_default']);
+ static::assertSame('Login - Shaarli', $assignedVariables['pagetitle']);
+ }
+
+ /**
+ * Test displaying login form with username defined in the request.
+ */
+ public function testValidControllerInvokeWithUserName(): void
+ {
+ $this->container->environment = ['HTTP_REFERER' => '> referer'];
+
+ $request = $this->createMock(Request::class);
+ $request
+ ->expects(static::atLeastOnce())
+ ->method('getParam')
+ ->willReturnCallback(function (string $key, $default) {
+ if ('login' === $key) {
+ return 'myUser>';
+ }
+
+ return $default;
+ })
+ ;
+ $response = new Response();
+
+ $assignedVariables = [];
+ $this->container->pageBuilder
+ ->method('assign')
+ ->willReturnCallback(function ($key, $value) use (&$assignedVariables) {
+ $assignedVariables[$key] = $value;
+
+ return $this;
+ })
+ ;
+
+ $this->container->loginManager->expects(static::once())->method('canLogin')->willReturn(true);
+
+ $result = $this->controller->index($request, $response);
+
+ static::assertInstanceOf(Response::class, $result);
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('loginform', (string) $result->getBody());
+
+ static::assertSame('myUser>', $assignedVariables['username']);
+ static::assertSame('> referer', $assignedVariables['returnurl']);
+ static::assertSame(true, $assignedVariables['remember_user_default']);
+ static::assertSame('Login - Shaarli', $assignedVariables['pagetitle']);
+ }
+
+ /**
+ * Test displaying login page while being logged in.
+ */
+ public function testLoginControllerWhileLoggedIn(): void
+ {
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->container->loginManager->expects(static::once())->method('isLoggedIn')->willReturn(true);
+
+ $result = $this->controller->index($request, $response);
+
+ static::assertInstanceOf(Response::class, $result);
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/'], $result->getHeader('Location'));
+ }
+
+ /**
+ * Test displaying login page with open shaarli configured: redirect to homepage.
+ */
+ public function testLoginControllerOpenShaarli(): void
+ {
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $conf = $this->createMock(ConfigManager::class);
+ $conf->method('get')->willReturnCallback(function (string $parameter, $default) {
+ if ($parameter === 'security.open_shaarli') {
+ return true;
+ }
+ return $default;
+ });
+ $this->container->conf = $conf;
+
+ $result = $this->controller->index($request, $response);
+
+ static::assertInstanceOf(Response::class, $result);
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/'], $result->getHeader('Location'));
+ }
+
+ /**
+ * Test displaying login page while being banned.
+ */
+ public function testLoginControllerWhileBanned(): void
+ {
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->container->loginManager->method('isLoggedIn')->willReturn(false);
+ $this->container->loginManager->method('canLogin')->willReturn(false);
+
+ $this->expectException(LoginBannedException::class);
+
+ $this->controller->index($request, $response);
+ }
+
+ /**
+ * Test processing login with valid parameters.
+ */
+ public function testProcessLoginWithValidParameters(): void
+ {
+ $parameters = [
+ 'login' => 'bob',
+ 'password' => 'pass',
+ ];
+ $request = $this->createMock(Request::class);
+ $request
+ ->expects(static::atLeastOnce())
+ ->method('getParam')
+ ->willReturnCallback(function (string $key) use ($parameters) {
+ return $parameters[$key] ?? null;
+ })
+ ;
+ $response = new Response();
+
+ $this->container->loginManager->method('canLogin')->willReturn(true);
+ $this->container->loginManager->expects(static::once())->method('handleSuccessfulLogin');
+ $this->container->loginManager
+ ->expects(static::once())
+ ->method('checkCredentials')
+ ->with('1.2.3.4', '1.2.3.4', 'bob', 'pass')
+ ->willReturn(true)
+ ;
+ $this->container->loginManager->method('getStaySignedInToken')->willReturn(bin2hex(random_bytes(8)));
+
+ $this->container->sessionManager->expects(static::never())->method('extendSession');
+ $this->container->sessionManager->expects(static::once())->method('destroy');
+ $this->container->sessionManager
+ ->expects(static::once())
+ ->method('cookieParameters')
+ ->with(0, '/subfolder/', 'shaarli')
+ ;
+ $this->container->sessionManager->expects(static::once())->method('start');
+ $this->container->sessionManager->expects(static::once())->method('regenerateId')->with(true);
+
+ $result = $this->controller->login($request, $response);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame('/subfolder/', $result->getHeader('location')[0]);
+ }
+
+ /**
+ * Test processing login with return URL.
+ */
+ public function testProcessLoginWithReturnUrl(): void
+ {
+ $parameters = [
+ 'returnurl' => 'http://shaarli/subfolder/admin/shaare',
+ ];
+ $request = $this->createMock(Request::class);
+ $request
+ ->expects(static::atLeastOnce())
+ ->method('getParam')
+ ->willReturnCallback(function (string $key) use ($parameters) {
+ return $parameters[$key] ?? null;
+ })
+ ;
+ $response = new Response();
+
+ $this->container->loginManager->method('canLogin')->willReturn(true);
+ $this->container->loginManager->expects(static::once())->method('handleSuccessfulLogin');
+ $this->container->loginManager->expects(static::once())->method('checkCredentials')->willReturn(true);
+ $this->container->loginManager->method('getStaySignedInToken')->willReturn(bin2hex(random_bytes(8)));
+
+ $result = $this->controller->login($request, $response);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame('/subfolder/admin/shaare', $result->getHeader('location')[0]);
+ }
+
+ /**
+ * Test processing login with remember me session enabled.
+ */
+ public function testProcessLoginLongLastingSession(): void
+ {
+ $parameters = [
+ 'longlastingsession' => true,
+ ];
+ $request = $this->createMock(Request::class);
+ $request
+ ->expects(static::atLeastOnce())
+ ->method('getParam')
+ ->willReturnCallback(function (string $key) use ($parameters) {
+ return $parameters[$key] ?? null;
+ })
+ ;
+ $response = new Response();
+
+ $this->container->loginManager->method('canLogin')->willReturn(true);
+ $this->container->loginManager->expects(static::once())->method('handleSuccessfulLogin');
+ $this->container->loginManager->expects(static::once())->method('checkCredentials')->willReturn(true);
+ $this->container->loginManager->method('getStaySignedInToken')->willReturn(bin2hex(random_bytes(8)));
+
+ $this->container->sessionManager->expects(static::once())->method('destroy');
+ $this->container->sessionManager
+ ->expects(static::once())
+ ->method('cookieParameters')
+ ->with(42, '/subfolder/', 'shaarli')
+ ;
+ $this->container->sessionManager->expects(static::once())->method('start');
+ $this->container->sessionManager->expects(static::once())->method('regenerateId')->with(true);
+ $this->container->sessionManager->expects(static::once())->method('extendSession')->willReturn(42);
+
+ $this->container->cookieManager = $this->createMock(CookieManager::class);
+ $this->container->cookieManager
+ ->expects(static::once())
+ ->method('setCookieParameter')
+ ->willReturnCallback(function (string $name): CookieManager {
+ static::assertSame(CookieManager::STAY_SIGNED_IN, $name);
+
+ return $this->container->cookieManager;
+ })
+ ;
+
+ $result = $this->controller->login($request, $response);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame('/subfolder/', $result->getHeader('location')[0]);
+ }
+
+ /**
+ * Test processing login with invalid credentials
+ */
+ public function testProcessLoginWrongCredentials(): void
+ {
+ $parameters = [
+ 'returnurl' => 'http://shaarli/subfolder/admin/shaare',
+ ];
+ $request = $this->createMock(Request::class);
+ $request
+ ->expects(static::atLeastOnce())
+ ->method('getParam')
+ ->willReturnCallback(function (string $key) use ($parameters) {
+ return $parameters[$key] ?? null;
+ })
+ ;
+ $response = new Response();
+
+ $this->container->loginManager->method('canLogin')->willReturn(true);
+ $this->container->loginManager->expects(static::once())->method('handleFailedLogin');
+ $this->container->loginManager->expects(static::once())->method('checkCredentials')->willReturn(false);
+
+ $this->container->sessionManager
+ ->expects(static::once())
+ ->method('setSessionParameter')
+ ->with(SessionManager::KEY_ERROR_MESSAGES, ['Wrong login/password.'])
+ ;
+
+ $result = $this->controller->login($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame(TemplatePage::LOGIN, (string) $result->getBody());
+ }
+
+ /**
+ * Test processing login with wrong token
+ */
+ public function testProcessLoginWrongToken(): void
+ {
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->container->sessionManager = $this->createMock(SessionManager::class);
+ $this->container->sessionManager->method('checkToken')->willReturn(false);
+
+ $this->expectException(WrongTokenException::class);
+
+ $this->controller->login($request, $response);
+ }
+
+ /**
+ * Test processing login with wrong token
+ */
+ public function testProcessLoginAlreadyLoggedIn(): void
+ {
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->container->loginManager->method('isLoggedIn')->willReturn(true);
+ $this->container->loginManager->expects(static::never())->method('handleSuccessfulLogin');
+ $this->container->loginManager->expects(static::never())->method('handleFailedLogin');
+
+ $result = $this->controller->login($request, $response);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame('/subfolder/', $result->getHeader('location')[0]);
+ }
+
+ /**
+ * Test processing login with wrong token
+ */
+ public function testProcessLoginInOpenShaarli(): void
+ {
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->container->conf = $this->createMock(ConfigManager::class);
+ $this->container->conf->method('get')->willReturnCallback(function (string $key, $value) {
+ return 'security.open_shaarli' === $key ? true : $value;
+ });
+
+ $this->container->loginManager->expects(static::never())->method('handleSuccessfulLogin');
+ $this->container->loginManager->expects(static::never())->method('handleFailedLogin');
+
+ $result = $this->controller->login($request, $response);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame('/subfolder/', $result->getHeader('location')[0]);
+ }
+
+ /**
+ * Test processing login while being banned
+ */
+ public function testProcessLoginWhileBanned(): void
+ {
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->container->loginManager->method('canLogin')->willReturn(false);
+ $this->container->loginManager->expects(static::never())->method('handleSuccessfulLogin');
+ $this->container->loginManager->expects(static::never())->method('handleFailedLogin');
+
+ $this->expectException(LoginBannedException::class);
+
+ $this->controller->login($request, $response);
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Visitor;
+
+use PHPUnit\Framework\TestCase;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+class OpenSearchControllerTest extends TestCase
+{
+ use FrontControllerMockHelper;
+
+ /** @var OpenSearchController */
+ protected $controller;
+
+ public function setUp(): void
+ {
+ $this->createContainer();
+
+ $this->controller = new OpenSearchController($this->container);
+ }
+
+ public function testOpenSearchController(): void
+ {
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ // Save RainTPL assigned variables
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ $result = $this->controller->index($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertStringContainsString(
+ 'application/opensearchdescription+xml',
+ $result->getHeader('Content-Type')[0]
+ );
+ static::assertSame('opensearch', (string) $result->getBody());
+ static::assertSame('http://shaarli', $assignedVariables['serverurl']);
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Visitor;
+
+use PHPUnit\Framework\TestCase;
+use Shaarli\Bookmark\Bookmark;
+use Shaarli\Config\ConfigManager;
+use Shaarli\Front\Exception\ThumbnailsDisabledException;
+use Shaarli\Thumbnailer;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+class PictureWallControllerTest extends TestCase
+{
+ use FrontControllerMockHelper;
+
+ /** @var PictureWallController */
+ protected $controller;
+
+ public function setUp(): void
+ {
+ $this->createContainer();
+
+ $this->controller = new PictureWallController($this->container);
+ }
+
+ public function testValidControllerInvokeDefault(): void
+ {
+ $request = $this->createMock(Request::class);
+ $request->expects(static::once())->method('getQueryParams')->willReturn([]);
+ $response = new Response();
+
+ // ConfigManager: thumbnails are enabled
+ $this->container->conf = $this->createMock(ConfigManager::class);
+ $this->container->conf->method('get')->willReturnCallback(function (string $parameter, $default) {
+ if ($parameter === 'thumbnails.mode') {
+ return Thumbnailer::MODE_COMMON;
+ }
+
+ return $default;
+ });
+
+ // Save RainTPL assigned variables
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ // Links dataset: 2 links with thumbnails
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('search')
+ ->willReturnCallback(function (array $parameters, ?string $visibility): array {
+ // Visibility is set through the container, not the call
+ static::assertNull($visibility);
+
+ // No query parameters
+ if (count($parameters) === 0) {
+ return [
+ (new Bookmark())->setId(1)->setUrl('http://url.tld')->setThumbnail('thumb1'),
+ (new Bookmark())->setId(2)->setUrl('http://url2.tld'),
+ (new Bookmark())->setId(3)->setUrl('http://url3.tld')->setThumbnail('thumb2'),
+ ];
+ }
+ })
+ ;
+
+ // Make sure that PluginManager hook is triggered
+ $this->container->pluginManager
+ ->expects(static::at(0))
+ ->method('executeHooks')
+ ->willReturnCallback(function (string $hook, array $data, array $param): array {
+ static::assertSame('render_picwall', $hook);
+ static::assertArrayHasKey('linksToDisplay', $data);
+ static::assertCount(2, $data['linksToDisplay']);
+ static::assertSame(1, $data['linksToDisplay'][0]['id']);
+ static::assertSame(3, $data['linksToDisplay'][1]['id']);
+ static::assertArrayHasKey('loggedin', $param);
+
+ return $data;
+ });
+
+ $result = $this->controller->index($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('picwall', (string) $result->getBody());
+ static::assertSame('Picture wall - Shaarli', $assignedVariables['pagetitle']);
+ static::assertCount(2, $assignedVariables['linksToDisplay']);
+
+ $link = $assignedVariables['linksToDisplay'][0];
+
+ static::assertSame(1, $link['id']);
+ static::assertSame('http://url.tld', $link['url']);
+ static::assertSame('thumb1', $link['thumbnail']);
+
+ $link = $assignedVariables['linksToDisplay'][1];
+
+ static::assertSame(3, $link['id']);
+ static::assertSame('http://url3.tld', $link['url']);
+ static::assertSame('thumb2', $link['thumbnail']);
+ }
+
+ public function testControllerWithThumbnailsDisabled(): void
+ {
+ $this->expectException(ThumbnailsDisabledException::class);
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ // ConfigManager: thumbnails are disabled
+ $this->container->conf->method('get')->willReturnCallback(function (string $parameter, $default) {
+ if ($parameter === 'thumbnails.mode') {
+ return Thumbnailer::MODE_NONE;
+ }
+
+ return $default;
+ });
+
+ $this->controller->index($request, $response);
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Visitor;
+
+use PHPUnit\Framework\TestCase;
+use Shaarli\Security\SessionManager;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+class PublicSessionFilterControllerTest extends TestCase
+{
+ use FrontControllerMockHelper;
+
+ /** @var PublicSessionFilterController */
+ protected $controller;
+
+ public function setUp(): void
+ {
+ $this->createContainer();
+
+ $this->controller = new PublicSessionFilterController($this->container);
+ }
+
+ /**
+ * Link per page - Default call with valid parameter and a referer.
+ */
+ public function testLinksPerPage(): void
+ {
+ $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/subfolder/controller/?searchtag=abc'];
+
+ $request = $this->createMock(Request::class);
+ $request->method('getParam')->with('nb')->willReturn('8');
+ $response = new Response();
+
+ $this->container->sessionManager
+ ->expects(static::once())
+ ->method('setSessionParameter')
+ ->with(SessionManager::KEY_LINKS_PER_PAGE, 8)
+ ;
+
+ $result = $this->controller->linksPerPage($request, $response);
+
+ static::assertInstanceOf(Response::class, $result);
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/controller/?searchtag=abc'], $result->getHeader('location'));
+ }
+
+ /**
+ * Link per page - Invalid value, should use default value (20)
+ */
+ public function testLinksPerPageNotValid(): void
+ {
+ $request = $this->createMock(Request::class);
+ $request->method('getParam')->with('nb')->willReturn('test');
+ $response = new Response();
+
+ $this->container->sessionManager
+ ->expects(static::once())
+ ->method('setSessionParameter')
+ ->with(SessionManager::KEY_LINKS_PER_PAGE, 20)
+ ;
+
+ $result = $this->controller->linksPerPage($request, $response);
+
+ static::assertInstanceOf(Response::class, $result);
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/'], $result->getHeader('location'));
+ }
+
+ /**
+ * Untagged only - valid call
+ */
+ public function testUntaggedOnly(): void
+ {
+ $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/subfolder/controller/?searchtag=abc'];
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->container->sessionManager
+ ->expects(static::once())
+ ->method('setSessionParameter')
+ ->with(SessionManager::KEY_UNTAGGED_ONLY, true)
+ ;
+
+ $result = $this->controller->untaggedOnly($request, $response);
+
+ static::assertInstanceOf(Response::class, $result);
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/controller/?searchtag=abc'], $result->getHeader('location'));
+ }
+
+ /**
+ * Untagged only - toggle off
+ */
+ public function testUntaggedOnlyToggleOff(): void
+ {
+ $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/subfolder/controller/?searchtag=abc'];
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->container->sessionManager
+ ->method('getSessionParameter')
+ ->with(SessionManager::KEY_UNTAGGED_ONLY)
+ ->willReturn(true)
+ ;
+ $this->container->sessionManager
+ ->expects(static::once())
+ ->method('setSessionParameter')
+ ->with(SessionManager::KEY_UNTAGGED_ONLY, false)
+ ;
+
+ $result = $this->controller->untaggedOnly($request, $response);
+
+ static::assertInstanceOf(Response::class, $result);
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/controller/?searchtag=abc'], $result->getHeader('location'));
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Visitor;
+
+use PHPUnit\Framework\TestCase;
+use Shaarli\Bookmark\BookmarkFilter;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+/**
+ * Class ShaarliControllerTest
+ *
+ * This class is used to test default behavior of ShaarliVisitorController abstract class.
+ * It uses a dummy non abstract controller.
+ */
+class ShaarliVisitorControllerTest extends TestCase
+{
+ use FrontControllerMockHelper;
+
+ /** @var LoginController */
+ protected $controller;
+
+ /** @var mixed[] List of variable assigned to the template */
+ protected $assignedValues;
+
+ /** @var Request */
+ protected $request;
+
+ public function setUp(): void
+ {
+ $this->createContainer();
+
+ $this->controller = new class($this->container) extends ShaarliVisitorController
+ {
+ public function assignView(string $key, $value): ShaarliVisitorController
+ {
+ return parent::assignView($key, $value);
+ }
+
+ public function render(string $template): string
+ {
+ return parent::render($template);
+ }
+
+ public function redirectFromReferer(
+ Request $request,
+ Response $response,
+ array $loopTerms = [],
+ array $clearParams = [],
+ string $anchor = null
+ ): Response {
+ return parent::redirectFromReferer($request, $response, $loopTerms, $clearParams, $anchor);
+ }
+ };
+ $this->assignedValues = [];
+
+ $this->request = $this->createMock(Request::class);
+ }
+
+ public function testAssignView(): void
+ {
+ $this->assignTemplateVars($this->assignedValues);
+
+ $self = $this->controller->assignView('variableName', 'variableValue');
+
+ static::assertInstanceOf(ShaarliVisitorController::class, $self);
+ static::assertSame('variableValue', $this->assignedValues['variableName']);
+ }
+
+ public function testRender(): void
+ {
+ $this->assignTemplateVars($this->assignedValues);
+
+ $this->container->bookmarkService
+ ->method('count')
+ ->willReturnCallback(function (string $visibility): int {
+ return $visibility === BookmarkFilter::$PRIVATE ? 5 : 10;
+ })
+ ;
+
+ $this->container->pluginManager
+ ->method('executeHooks')
+ ->willReturnCallback(function (string $hook, array &$data, array $params): array {
+ return $data[$hook] = $params;
+ });
+ $this->container->pluginManager->method('getErrors')->willReturn(['error']);
+
+ $this->container->loginManager->method('isLoggedIn')->willReturn(true);
+
+ $render = $this->controller->render('templateName');
+
+ static::assertSame('templateName', $render);
+
+ static::assertSame(10, $this->assignedValues['linkcount']);
+ static::assertSame(5, $this->assignedValues['privateLinkcount']);
+ static::assertSame(['error'], $this->assignedValues['plugin_errors']);
+
+ static::assertSame('templateName', $this->assignedValues['plugins_includes']['render_includes']['target']);
+ static::assertTrue($this->assignedValues['plugins_includes']['render_includes']['loggedin']);
+ static::assertSame('templateName', $this->assignedValues['plugins_header']['render_header']['target']);
+ static::assertTrue($this->assignedValues['plugins_header']['render_header']['loggedin']);
+ static::assertSame('templateName', $this->assignedValues['plugins_footer']['render_footer']['target']);
+ static::assertTrue($this->assignedValues['plugins_footer']['render_footer']['loggedin']);
+ }
+
+ /**
+ * Test redirectFromReferer() - Default behaviour
+ */
+ public function testRedirectFromRefererDefault(): void
+ {
+ $this->container->environment['HTTP_REFERER'] = 'http://shaarli.tld/subfolder/controller?query=param&other=2';
+
+ $response = new Response();
+
+ $result = $this->controller->redirectFromReferer($this->request, $response);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/controller?query=param&other=2'], $result->getHeader('location'));
+ }
+
+ /**
+ * Test redirectFromReferer() - With a loop term not matched in the referer
+ */
+ public function testRedirectFromRefererWithUnmatchedLoopTerm(): void
+ {
+ $this->container->environment['HTTP_REFERER'] = 'http://shaarli.tld/subfolder/controller?query=param&other=2';
+
+ $response = new Response();
+
+ $result = $this->controller->redirectFromReferer($this->request, $response, ['nope']);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/controller?query=param&other=2'], $result->getHeader('location'));
+ }
+
+ /**
+ * Test redirectFromReferer() - With a loop term matching the referer in its path -> redirect to default
+ */
+ public function testRedirectFromRefererWithMatchingLoopTermInPath(): void
+ {
+ $this->container->environment['HTTP_REFERER'] = 'http://shaarli.tld/subfolder/controller?query=param&other=2';
+
+ $response = new Response();
+
+ $result = $this->controller->redirectFromReferer($this->request, $response, ['nope', 'controller']);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/'], $result->getHeader('location'));
+ }
+
+ /**
+ * Test redirectFromReferer() - With a loop term matching the referer in its query parameters -> redirect to default
+ */
+ public function testRedirectFromRefererWithMatchingLoopTermInQueryParam(): void
+ {
+ $this->container->environment['HTTP_REFERER'] = 'http://shaarli.tld/subfolder/controller?query=param&other=2';
+
+ $response = new Response();
+
+ $result = $this->controller->redirectFromReferer($this->request, $response, ['nope', 'other']);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/'], $result->getHeader('location'));
+ }
+
+ /**
+ * Test redirectFromReferer() - With a loop term matching the referer in its query value
+ * -> we do not block redirection for query parameter values.
+ */
+ public function testRedirectFromRefererWithMatchingLoopTermInQueryValue(): void
+ {
+ $this->container->environment['HTTP_REFERER'] = 'http://shaarli.tld/subfolder/controller?query=param&other=2';
+
+ $response = new Response();
+
+ $result = $this->controller->redirectFromReferer($this->request, $response, ['nope', 'param']);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/controller?query=param&other=2'], $result->getHeader('location'));
+ }
+
+ /**
+ * Test redirectFromReferer() - With a loop term matching the referer in its domain name
+ * -> we do not block redirection for shaarli's hosts
+ */
+ public function testRedirectFromRefererWithLoopTermInDomain(): void
+ {
+ $this->container->environment['HTTP_REFERER'] = 'http://shaarli.tld/subfolder/controller?query=param&other=2';
+
+ $response = new Response();
+
+ $result = $this->controller->redirectFromReferer($this->request, $response, ['shaarli']);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/controller?query=param&other=2'], $result->getHeader('location'));
+ }
+
+ /**
+ * Test redirectFromReferer() - With a loop term matching a query parameter AND clear this query param
+ * -> the param should be cleared before checking if it matches the redir loop terms
+ */
+ public function testRedirectFromRefererWithMatchingClearedParam(): void
+ {
+ $this->container->environment['HTTP_REFERER'] = 'http://shaarli.tld/subfolder/controller?query=param&other=2';
+
+ $response = new Response();
+
+ $result = $this->controller->redirectFromReferer($this->request, $response, ['query'], ['query']);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/controller?other=2'], $result->getHeader('location'));
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Visitor;
+
+use PHPUnit\Framework\TestCase;
+use Shaarli\Bookmark\BookmarkFilter;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+class TagCloudControllerTest extends TestCase
+{
+ use FrontControllerMockHelper;
+
+ /** @var TagCloudController */
+ protected $controller;
+
+ public function setUp(): void
+ {
+ $this->createContainer();
+
+ $this->controller = new TagCloudController($this->container);
+ }
+
+ /**
+ * Tag Cloud - default parameters
+ */
+ public function testValidCloudControllerInvokeDefault(): void
+ {
+ $allTags = [
+ 'ghi' => 1,
+ 'abc' => 3,
+ 'def' => 12,
+ ];
+ $expectedOrder = ['abc', 'def', 'ghi'];
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ // Save RainTPL assigned variables
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('bookmarksCountPerTag')
+ ->with([], null)
+ ->willReturnCallback(function () use ($allTags): array {
+ return $allTags;
+ })
+ ;
+
+ // Make sure that PluginManager hook is triggered
+ $this->container->pluginManager
+ ->expects(static::at(0))
+ ->method('executeHooks')
+ ->willReturnCallback(function (string $hook, array $data, array $param): array {
+ static::assertSame('render_tagcloud', $hook);
+ static::assertSame('', $data['search_tags']);
+ static::assertCount(3, $data['tags']);
+
+ static::assertArrayHasKey('loggedin', $param);
+
+ return $data;
+ })
+ ;
+
+ $result = $this->controller->cloud($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('tag.cloud', (string) $result->getBody());
+ static::assertSame('Tag cloud - Shaarli', $assignedVariables['pagetitle']);
+
+ static::assertSame('', $assignedVariables['search_tags']);
+ static::assertCount(3, $assignedVariables['tags']);
+ static::assertSame($expectedOrder, array_keys($assignedVariables['tags']));
+
+ foreach ($allTags as $tag => $count) {
+ static::assertArrayHasKey($tag, $assignedVariables['tags']);
+ static::assertSame($count, $assignedVariables['tags'][$tag]['count']);
+ static::assertGreaterThan(0, $assignedVariables['tags'][$tag]['size']);
+ static::assertLessThan(5, $assignedVariables['tags'][$tag]['size']);
+ }
+ }
+
+ /**
+ * Tag Cloud - Additional parameters:
+ * - logged in
+ * - visibility private
+ * - search tags: `ghi` and `def` (note that filtered tags are not displayed anymore)
+ */
+ public function testValidCloudControllerInvokeWithParameters(): void
+ {
+ $request = $this->createMock(Request::class);
+ $request
+ ->method('getQueryParam')
+ ->with()
+ ->willReturnCallback(function (string $key): ?string {
+ if ('searchtags' === $key) {
+ return 'ghi def';
+ }
+
+ return null;
+ })
+ ;
+ $response = new Response();
+
+ // Save RainTPL assigned variables
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ $this->container->loginManager->method('isLoggedin')->willReturn(true);
+ $this->container->sessionManager->expects(static::once())->method('getSessionParameter')->willReturn('private');
+
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('bookmarksCountPerTag')
+ ->with(['ghi', 'def'], BookmarkFilter::$PRIVATE)
+ ->willReturnCallback(function (): array {
+ return ['abc' => 3];
+ })
+ ;
+
+ // Make sure that PluginManager hook is triggered
+ $this->container->pluginManager
+ ->expects(static::at(0))
+ ->method('executeHooks')
+ ->willReturnCallback(function (string $hook, array $data, array $param): array {
+ static::assertSame('render_tagcloud', $hook);
+ static::assertSame('ghi def', $data['search_tags']);
+ static::assertCount(1, $data['tags']);
+
+ static::assertArrayHasKey('loggedin', $param);
+
+ return $data;
+ })
+ ;
+
+ $result = $this->controller->cloud($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('tag.cloud', (string) $result->getBody());
+ static::assertSame('ghi def - Tag cloud - Shaarli', $assignedVariables['pagetitle']);
+
+ static::assertSame('ghi def', $assignedVariables['search_tags']);
+ static::assertCount(1, $assignedVariables['tags']);
+
+ static::assertArrayHasKey('abc', $assignedVariables['tags']);
+ static::assertSame(3, $assignedVariables['tags']['abc']['count']);
+ static::assertGreaterThan(0, $assignedVariables['tags']['abc']['size']);
+ static::assertLessThan(5, $assignedVariables['tags']['abc']['size']);
+ }
+
+ /**
+ * Tag Cloud - empty
+ */
+ public function testEmptyCloud(): void
+ {
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ // Save RainTPL assigned variables
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('bookmarksCountPerTag')
+ ->with([], null)
+ ->willReturnCallback(function (array $parameters, ?string $visibility): array {
+ return [];
+ })
+ ;
+
+ // Make sure that PluginManager hook is triggered
+ $this->container->pluginManager
+ ->expects(static::at(0))
+ ->method('executeHooks')
+ ->willReturnCallback(function (string $hook, array $data, array $param): array {
+ static::assertSame('render_tagcloud', $hook);
+ static::assertSame('', $data['search_tags']);
+ static::assertCount(0, $data['tags']);
+
+ static::assertArrayHasKey('loggedin', $param);
+
+ return $data;
+ })
+ ;
+
+ $result = $this->controller->cloud($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('tag.cloud', (string) $result->getBody());
+ static::assertSame('Tag cloud - Shaarli', $assignedVariables['pagetitle']);
+
+ static::assertSame('', $assignedVariables['search_tags']);
+ static::assertCount(0, $assignedVariables['tags']);
+ }
+
+ /**
+ * Tag List - Default sort is by usage DESC
+ */
+ public function testValidListControllerInvokeDefault(): void
+ {
+ $allTags = [
+ 'def' => 12,
+ 'abc' => 3,
+ 'ghi' => 1,
+ ];
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ // Save RainTPL assigned variables
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('bookmarksCountPerTag')
+ ->with([], null)
+ ->willReturnCallback(function () use ($allTags): array {
+ return $allTags;
+ })
+ ;
+
+ // Make sure that PluginManager hook is triggered
+ $this->container->pluginManager
+ ->expects(static::at(0))
+ ->method('executeHooks')
+ ->willReturnCallback(function (string $hook, array $data, array $param): array {
+ static::assertSame('render_taglist', $hook);
+ static::assertSame('', $data['search_tags']);
+ static::assertCount(3, $data['tags']);
+
+ static::assertArrayHasKey('loggedin', $param);
+
+ return $data;
+ })
+ ;
+
+ $result = $this->controller->list($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('tag.list', (string) $result->getBody());
+ static::assertSame('Tag list - Shaarli', $assignedVariables['pagetitle']);
+
+ static::assertSame('', $assignedVariables['search_tags']);
+ static::assertCount(3, $assignedVariables['tags']);
+
+ foreach ($allTags as $tag => $count) {
+ static::assertSame($count, $assignedVariables['tags'][$tag]);
+ }
+ }
+
+ /**
+ * Tag List - Additional parameters:
+ * - logged in
+ * - visibility private
+ * - search tags: `ghi` and `def` (note that filtered tags are not displayed anymore)
+ * - sort alphabetically
+ */
+ public function testValidListControllerInvokeWithParameters(): void
+ {
+ $request = $this->createMock(Request::class);
+ $request
+ ->method('getQueryParam')
+ ->with()
+ ->willReturnCallback(function (string $key): ?string {
+ if ('searchtags' === $key) {
+ return 'ghi def';
+ } elseif ('sort' === $key) {
+ return 'alpha';
+ }
+
+ return null;
+ })
+ ;
+ $response = new Response();
+
+ // Save RainTPL assigned variables
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ $this->container->loginManager->method('isLoggedin')->willReturn(true);
+ $this->container->sessionManager->expects(static::once())->method('getSessionParameter')->willReturn('private');
+
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('bookmarksCountPerTag')
+ ->with(['ghi', 'def'], BookmarkFilter::$PRIVATE)
+ ->willReturnCallback(function (): array {
+ return ['abc' => 3];
+ })
+ ;
+
+ // Make sure that PluginManager hook is triggered
+ $this->container->pluginManager
+ ->expects(static::at(0))
+ ->method('executeHooks')
+ ->willReturnCallback(function (string $hook, array $data, array $param): array {
+ static::assertSame('render_taglist', $hook);
+ static::assertSame('ghi def', $data['search_tags']);
+ static::assertCount(1, $data['tags']);
+
+ static::assertArrayHasKey('loggedin', $param);
+
+ return $data;
+ })
+ ;
+
+ $result = $this->controller->list($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('tag.list', (string) $result->getBody());
+ static::assertSame('ghi def - Tag list - Shaarli', $assignedVariables['pagetitle']);
+
+ static::assertSame('ghi def', $assignedVariables['search_tags']);
+ static::assertCount(1, $assignedVariables['tags']);
+ static::assertSame(3, $assignedVariables['tags']['abc']);
+ }
+
+ /**
+ * Tag List - empty
+ */
+ public function testEmptyList(): void
+ {
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ // Save RainTPL assigned variables
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('bookmarksCountPerTag')
+ ->with([], null)
+ ->willReturnCallback(function (array $parameters, ?string $visibility): array {
+ return [];
+ })
+ ;
+
+ // Make sure that PluginManager hook is triggered
+ $this->container->pluginManager
+ ->expects(static::at(0))
+ ->method('executeHooks')
+ ->willReturnCallback(function (string $hook, array $data, array $param): array {
+ static::assertSame('render_taglist', $hook);
+ static::assertSame('', $data['search_tags']);
+ static::assertCount(0, $data['tags']);
+
+ static::assertArrayHasKey('loggedin', $param);
+
+ return $data;
+ })
+ ;
+
+ $result = $this->controller->list($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('tag.list', (string) $result->getBody());
+ static::assertSame('Tag list - Shaarli', $assignedVariables['pagetitle']);
+
+ static::assertSame('', $assignedVariables['search_tags']);
+ static::assertCount(0, $assignedVariables['tags']);
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Visitor;
+
+use PHPUnit\Framework\TestCase;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+class TagControllerTest extends TestCase
+{
+ use FrontControllerMockHelper;
+
+ /** @var TagController */ protected $controller;
+
+ public function setUp(): void
+ {
+ $this->createContainer();
+
+ $this->controller = new TagController($this->container);
+ }
+
+ public function testAddTagWithReferer(): void
+ {
+ $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/'];
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $tags = ['newTag' => 'abc'];
+
+ $result = $this->controller->addTag($request, $response, $tags);
+
+ static::assertInstanceOf(Response::class, $result);
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/controller/?searchtags=abc'], $result->getHeader('location'));
+ }
+
+ public function testAddTagWithRefererAndExistingSearch(): void
+ {
+ $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/?searchtags=def'];
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $tags = ['newTag' => 'abc'];
+
+ $result = $this->controller->addTag($request, $response, $tags);
+
+ static::assertInstanceOf(Response::class, $result);
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/controller/?searchtags=def+abc'], $result->getHeader('location'));
+ }
+
+ public function testAddTagWithoutRefererAndExistingSearch(): void
+ {
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $tags = ['newTag' => 'abc'];
+
+ $result = $this->controller->addTag($request, $response, $tags);
+
+ static::assertInstanceOf(Response::class, $result);
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/?searchtags=abc'], $result->getHeader('location'));
+ }
+
+ public function testAddTagRemoveLegacyQueryParam(): void
+ {
+ $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/?searchtags=def&addtag=abc'];
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $tags = ['newTag' => 'abc'];
+
+ $result = $this->controller->addTag($request, $response, $tags);
+
+ static::assertInstanceOf(Response::class, $result);
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/controller/?searchtags=def+abc'], $result->getHeader('location'));
+ }
+
+ public function testAddTagResetPagination(): void
+ {
+ $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/?searchtags=def&page=12'];
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $tags = ['newTag' => 'abc'];
+
+ $result = $this->controller->addTag($request, $response, $tags);
+
+ static::assertInstanceOf(Response::class, $result);
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/controller/?searchtags=def+abc'], $result->getHeader('location'));
+ }
+
+ public function testAddTagWithRefererAndEmptySearch(): void
+ {
+ $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/?searchtags='];
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $tags = ['newTag' => 'abc'];
+
+ $result = $this->controller->addTag($request, $response, $tags);
+
+ static::assertInstanceOf(Response::class, $result);
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/controller/?searchtags=abc'], $result->getHeader('location'));
+ }
+
+ public function testAddTagWithoutNewTagWithReferer(): void
+ {
+ $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/?searchtags=def'];
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $result = $this->controller->addTag($request, $response, []);
+
+ static::assertInstanceOf(Response::class, $result);
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/controller/?searchtags=def'], $result->getHeader('location'));
+ }
+
+ public function testAddTagWithoutNewTagWithoutReferer(): void
+ {
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $result = $this->controller->addTag($request, $response, []);
+
+ static::assertInstanceOf(Response::class, $result);
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/'], $result->getHeader('location'));
+ }
+
+ public function testRemoveTagWithoutMatchingTag(): void
+ {
+ $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/?searchtags=def'];
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $tags = ['tag' => 'abc'];
+
+ $result = $this->controller->removeTag($request, $response, $tags);
+
+ static::assertInstanceOf(Response::class, $result);
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/controller/?searchtags=def'], $result->getHeader('location'));
+ }
+
+ public function testRemoveTagWithoutTagsearch(): void
+ {
+ $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/'];
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $tags = ['tag' => 'abc'];
+
+ $result = $this->controller->removeTag($request, $response, $tags);
+
+ static::assertInstanceOf(Response::class, $result);
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/controller/'], $result->getHeader('location'));
+ }
+
+ public function testRemoveTagWithoutReferer(): void
+ {
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $tags = ['tag' => 'abc'];
+
+ $result = $this->controller->removeTag($request, $response, $tags);
+
+ static::assertInstanceOf(Response::class, $result);
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/'], $result->getHeader('location'));
+ }
+
+ public function testRemoveTagWithoutTag(): void
+ {
+ $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/?searchtag=abc'];
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $result = $this->controller->removeTag($request, $response, []);
+
+ static::assertInstanceOf(Response::class, $result);
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/controller/?searchtag=abc'], $result->getHeader('location'));
+ }
+
+ public function testRemoveTagWithoutTagWithoutReferer(): void
+ {
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $result = $this->controller->removeTag($request, $response, []);
+
+ static::assertInstanceOf(Response::class, $result);
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/'], $result->getHeader('location'));
+ }
+}
)
);
}
+
+ /**
+ * The route is stored in REQUEST_URI
+ */
+ public function testPageUrlWithRoute()
+ {
+ $this->assertEquals(
+ 'http://host.tld/picture-wall',
+ page_url(
+ array(
+ 'HTTPS' => 'Off',
+ 'SERVER_NAME' => 'host.tld',
+ 'SERVER_PORT' => '80',
+ 'SCRIPT_NAME' => '/index.php',
+ 'REQUEST_URI' => '/picture-wall',
+ )
+ )
+ );
+
+ $this->assertEquals(
+ 'http://host.tld/admin/picture-wall',
+ page_url(
+ array(
+ 'HTTPS' => 'Off',
+ 'SERVER_NAME' => 'host.tld',
+ 'SERVER_PORT' => '80',
+ 'SCRIPT_NAME' => '/admin/index.php',
+ 'REQUEST_URI' => '/admin/picture-wall',
+ )
+ )
+ );
+ }
}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Legacy;
+
+use PHPUnit\Framework\TestCase;
+use Shaarli\Front\Controller\Visitor\FrontControllerMockHelper;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+class LegacyControllerTest extends TestCase
+{
+ use FrontControllerMockHelper;
+
+ /** @var LegacyController */
+ protected $controller;
+
+ public function setUp(): void
+ {
+ $this->createContainer();
+
+ $this->controller = new LegacyController($this->container);
+ }
+
+ /**
+ * @dataProvider getProcessProvider
+ */
+ public function testProcess(string $legacyRoute, array $queryParameters, string $slimRoute, bool $isLoggedIn): void
+ {
+ $request = $this->createMock(Request::class);
+ $request->method('getQueryParams')->willReturn($queryParameters);
+ $request
+ ->method('getParam')
+ ->willReturnCallback(function (string $key) use ($queryParameters): ?string {
+ return $queryParameters[$key] ?? null;
+ })
+ ;
+ $response = new Response();
+
+ $this->container->loginManager->method('isLoggedIn')->willReturn($isLoggedIn);
+
+ $result = $this->controller->process($request, $response, $legacyRoute);
+
+ static::assertSame('/subfolder' . $slimRoute, $result->getHeader('location')[0]);
+ }
+
+ public function testProcessNotFound(): void
+ {
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->expectException(UnknowLegacyRouteException::class);
+
+ $this->controller->process($request, $response, 'nope');
+ }
+
+ /**
+ * @return array[] Parameters:
+ * - string legacyRoute
+ * - array queryParameters
+ * - string slimRoute
+ * - bool isLoggedIn
+ */
+ public function getProcessProvider(): array
+ {
+ return [
+ ['post', [], '/admin/shaare', true],
+ ['post', [], '/login', false],
+ ['post', ['title' => 'test'], '/admin/shaare?title=test', true],
+ ['post', ['title' => 'test'], '/login?title=test', false],
+ ['addlink', [], '/admin/add-shaare', true],
+ ['addlink', [], '/login', false],
+ ['login', [], '/login', true],
+ ['login', [], '/login', false],
+ ['logout', [], '/admin/logout', true],
+ ['logout', [], '/admin/logout', false],
+ ['picwall', [], '/picture-wall', false],
+ ['picwall', [], '/picture-wall', true],
+ ['tagcloud', [], '/tags/cloud', false],
+ ['tagcloud', [], '/tags/cloud', true],
+ ['taglist', [], '/tags/list', false],
+ ['taglist', [], '/tags/list', true],
+ ['daily', [], '/daily', false],
+ ['daily', [], '/daily', true],
+ ['daily', ['day' => '123456789', 'discard' => '1'], '/daily?day=123456789', false],
+ ['rss', [], '/feed/rss', false],
+ ['rss', [], '/feed/rss', true],
+ ['rss', ['search' => 'filter123', 'other' => 'param'], '/feed/rss?search=filter123&other=param', false],
+ ['atom', [], '/feed/atom', false],
+ ['atom', [], '/feed/atom', true],
+ ['atom', ['search' => 'filter123', 'other' => 'param'], '/feed/atom?search=filter123&other=param', false],
+ ['opensearch', [], '/open-search', false],
+ ['opensearch', [], '/open-search', true],
+ ['dailyrss', [], '/daily-rss', false],
+ ['dailyrss', [], '/daily-rss', true],
+ ];
+ }
+}
use Shaarli;
use Shaarli\Bookmark\Bookmark;
-require_once 'application/feed/Cache.php';
require_once 'application/Utils.php';
require_once 'tests/utils/ReferenceLinkDB.php';
<?php
-namespace Shaarli;
+
+namespace Shaarli\Legacy;
+
+use PHPUnit\Framework\TestCase;
/**
* Unit tests for Router
*/
-class RouterTest extends \PHPUnit\Framework\TestCase
+class LegacyRouterTest extends TestCase
{
/**
* Test findPage: login page output.
public function testFindPageLoginValid()
{
$this->assertEquals(
- Router::$PAGE_LOGIN,
- Router::findPage('do=login', array(), false)
+ LegacyRouter::$PAGE_LOGIN,
+ LegacyRouter::findPage('do=login', array(), false)
);
$this->assertEquals(
- Router::$PAGE_LOGIN,
- Router::findPage('do=login', array(), 1)
+ LegacyRouter::$PAGE_LOGIN,
+ LegacyRouter::findPage('do=login', array(), 1)
);
$this->assertEquals(
- Router::$PAGE_LOGIN,
- Router::findPage('do=login&stuff', array(), false)
+ LegacyRouter::$PAGE_LOGIN,
+ LegacyRouter::findPage('do=login&stuff', array(), false)
);
}
public function testFindPageLoginInvalid()
{
$this->assertNotEquals(
- Router::$PAGE_LOGIN,
- Router::findPage('do=login', array(), true)
+ LegacyRouter::$PAGE_LOGIN,
+ LegacyRouter::findPage('do=login', array(), true)
);
$this->assertNotEquals(
- Router::$PAGE_LOGIN,
- Router::findPage('do=other', array(), false)
+ LegacyRouter::$PAGE_LOGIN,
+ LegacyRouter::findPage('do=other', array(), false)
);
}
public function testFindPagePicwallValid()
{
$this->assertEquals(
- Router::$PAGE_PICWALL,
- Router::findPage('do=picwall', array(), false)
+ LegacyRouter::$PAGE_PICWALL,
+ LegacyRouter::findPage('do=picwall', array(), false)
);
$this->assertEquals(
- Router::$PAGE_PICWALL,
- Router::findPage('do=picwall', array(), true)
+ LegacyRouter::$PAGE_PICWALL,
+ LegacyRouter::findPage('do=picwall', array(), true)
);
}
public function testFindPagePicwallInvalid()
{
$this->assertEquals(
- Router::$PAGE_PICWALL,
- Router::findPage('do=picwall&stuff', array(), false)
+ LegacyRouter::$PAGE_PICWALL,
+ LegacyRouter::findPage('do=picwall&stuff', array(), false)
);
$this->assertNotEquals(
- Router::$PAGE_PICWALL,
- Router::findPage('do=other', array(), false)
+ LegacyRouter::$PAGE_PICWALL,
+ LegacyRouter::findPage('do=other', array(), false)
);
}
public function testFindPageTagcloudValid()
{
$this->assertEquals(
- Router::$PAGE_TAGCLOUD,
- Router::findPage('do=tagcloud', array(), false)
+ LegacyRouter::$PAGE_TAGCLOUD,
+ LegacyRouter::findPage('do=tagcloud', array(), false)
);
$this->assertEquals(
- Router::$PAGE_TAGCLOUD,
- Router::findPage('do=tagcloud', array(), true)
+ LegacyRouter::$PAGE_TAGCLOUD,
+ LegacyRouter::findPage('do=tagcloud', array(), true)
);
$this->assertEquals(
- Router::$PAGE_TAGCLOUD,
- Router::findPage('do=tagcloud&stuff', array(), false)
+ LegacyRouter::$PAGE_TAGCLOUD,
+ LegacyRouter::findPage('do=tagcloud&stuff', array(), false)
);
}
public function testFindPageTagcloudInvalid()
{
$this->assertNotEquals(
- Router::$PAGE_TAGCLOUD,
- Router::findPage('do=other', array(), false)
+ LegacyRouter::$PAGE_TAGCLOUD,
+ LegacyRouter::findPage('do=other', array(), false)
);
}
public function testFindPageLinklistValid()
{
$this->assertEquals(
- Router::$PAGE_LINKLIST,
- Router::findPage('', array(), true)
+ LegacyRouter::$PAGE_LINKLIST,
+ LegacyRouter::findPage('', array(), true)
);
$this->assertEquals(
- Router::$PAGE_LINKLIST,
- Router::findPage('whatever', array(), true)
+ LegacyRouter::$PAGE_LINKLIST,
+ LegacyRouter::findPage('whatever', array(), true)
);
$this->assertEquals(
- Router::$PAGE_LINKLIST,
- Router::findPage('whatever', array(), false)
+ LegacyRouter::$PAGE_LINKLIST,
+ LegacyRouter::findPage('whatever', array(), false)
);
$this->assertEquals(
- Router::$PAGE_LINKLIST,
- Router::findPage('do=tools', array(), false)
+ LegacyRouter::$PAGE_LINKLIST,
+ LegacyRouter::findPage('do=tools', array(), false)
);
}
public function testFindPageToolsValid()
{
$this->assertEquals(
- Router::$PAGE_TOOLS,
- Router::findPage('do=tools', array(), true)
+ LegacyRouter::$PAGE_TOOLS,
+ LegacyRouter::findPage('do=tools', array(), true)
);
$this->assertEquals(
- Router::$PAGE_TOOLS,
- Router::findPage('do=tools&stuff', array(), true)
+ LegacyRouter::$PAGE_TOOLS,
+ LegacyRouter::findPage('do=tools&stuff', array(), true)
);
}
public function testFindPageToolsInvalid()
{
$this->assertNotEquals(
- Router::$PAGE_TOOLS,
- Router::findPage('do=tools', array(), 1)
+ LegacyRouter::$PAGE_TOOLS,
+ LegacyRouter::findPage('do=tools', array(), 1)
);
$this->assertNotEquals(
- Router::$PAGE_TOOLS,
- Router::findPage('do=tools', array(), false)
+ LegacyRouter::$PAGE_TOOLS,
+ LegacyRouter::findPage('do=tools', array(), false)
);
$this->assertNotEquals(
- Router::$PAGE_TOOLS,
- Router::findPage('do=other', array(), true)
+ LegacyRouter::$PAGE_TOOLS,
+ LegacyRouter::findPage('do=other', array(), true)
);
}
public function testFindPageChangepasswdValid()
{
$this->assertEquals(
- Router::$PAGE_CHANGEPASSWORD,
- Router::findPage('do=changepasswd', array(), true)
+ LegacyRouter::$PAGE_CHANGEPASSWORD,
+ LegacyRouter::findPage('do=changepasswd', array(), true)
);
$this->assertEquals(
- Router::$PAGE_CHANGEPASSWORD,
- Router::findPage('do=changepasswd&stuff', array(), true)
+ LegacyRouter::$PAGE_CHANGEPASSWORD,
+ LegacyRouter::findPage('do=changepasswd&stuff', array(), true)
);
}
public function testFindPageChangepasswdInvalid()
{
$this->assertNotEquals(
- Router::$PAGE_CHANGEPASSWORD,
- Router::findPage('do=changepasswd', array(), 1)
+ LegacyRouter::$PAGE_CHANGEPASSWORD,
+ LegacyRouter::findPage('do=changepasswd', array(), 1)
);
$this->assertNotEquals(
- Router::$PAGE_CHANGEPASSWORD,
- Router::findPage('do=changepasswd', array(), false)
+ LegacyRouter::$PAGE_CHANGEPASSWORD,
+ LegacyRouter::findPage('do=changepasswd', array(), false)
);
$this->assertNotEquals(
- Router::$PAGE_CHANGEPASSWORD,
- Router::findPage('do=other', array(), true)
+ LegacyRouter::$PAGE_CHANGEPASSWORD,
+ LegacyRouter::findPage('do=other', array(), true)
);
}
/**
public function testFindPageConfigureValid()
{
$this->assertEquals(
- Router::$PAGE_CONFIGURE,
- Router::findPage('do=configure', array(), true)
+ LegacyRouter::$PAGE_CONFIGURE,
+ LegacyRouter::findPage('do=configure', array(), true)
);
$this->assertEquals(
- Router::$PAGE_CONFIGURE,
- Router::findPage('do=configure&stuff', array(), true)
+ LegacyRouter::$PAGE_CONFIGURE,
+ LegacyRouter::findPage('do=configure&stuff', array(), true)
);
}
public function testFindPageConfigureInvalid()
{
$this->assertNotEquals(
- Router::$PAGE_CONFIGURE,
- Router::findPage('do=configure', array(), 1)
+ LegacyRouter::$PAGE_CONFIGURE,
+ LegacyRouter::findPage('do=configure', array(), 1)
);
$this->assertNotEquals(
- Router::$PAGE_CONFIGURE,
- Router::findPage('do=configure', array(), false)
+ LegacyRouter::$PAGE_CONFIGURE,
+ LegacyRouter::findPage('do=configure', array(), false)
);
$this->assertNotEquals(
- Router::$PAGE_CONFIGURE,
- Router::findPage('do=other', array(), true)
+ LegacyRouter::$PAGE_CONFIGURE,
+ LegacyRouter::findPage('do=other', array(), true)
);
}
public function testFindPageChangetagValid()
{
$this->assertEquals(
- Router::$PAGE_CHANGETAG,
- Router::findPage('do=changetag', array(), true)
+ LegacyRouter::$PAGE_CHANGETAG,
+ LegacyRouter::findPage('do=changetag', array(), true)
);
$this->assertEquals(
- Router::$PAGE_CHANGETAG,
- Router::findPage('do=changetag&stuff', array(), true)
+ LegacyRouter::$PAGE_CHANGETAG,
+ LegacyRouter::findPage('do=changetag&stuff', array(), true)
);
}
public function testFindPageChangetagInvalid()
{
$this->assertNotEquals(
- Router::$PAGE_CHANGETAG,
- Router::findPage('do=changetag', array(), 1)
+ LegacyRouter::$PAGE_CHANGETAG,
+ LegacyRouter::findPage('do=changetag', array(), 1)
);
$this->assertNotEquals(
- Router::$PAGE_CHANGETAG,
- Router::findPage('do=changetag', array(), false)
+ LegacyRouter::$PAGE_CHANGETAG,
+ LegacyRouter::findPage('do=changetag', array(), false)
);
$this->assertNotEquals(
- Router::$PAGE_CHANGETAG,
- Router::findPage('do=other', array(), true)
+ LegacyRouter::$PAGE_CHANGETAG,
+ LegacyRouter::findPage('do=other', array(), true)
);
}
public function testFindPageAddlinkValid()
{
$this->assertEquals(
- Router::$PAGE_ADDLINK,
- Router::findPage('do=addlink', array(), true)
+ LegacyRouter::$PAGE_ADDLINK,
+ LegacyRouter::findPage('do=addlink', array(), true)
);
$this->assertEquals(
- Router::$PAGE_ADDLINK,
- Router::findPage('do=addlink&stuff', array(), true)
+ LegacyRouter::$PAGE_ADDLINK,
+ LegacyRouter::findPage('do=addlink&stuff', array(), true)
);
}
public function testFindPageAddlinkInvalid()
{
$this->assertNotEquals(
- Router::$PAGE_ADDLINK,
- Router::findPage('do=addlink', array(), 1)
+ LegacyRouter::$PAGE_ADDLINK,
+ LegacyRouter::findPage('do=addlink', array(), 1)
);
$this->assertNotEquals(
- Router::$PAGE_ADDLINK,
- Router::findPage('do=addlink', array(), false)
+ LegacyRouter::$PAGE_ADDLINK,
+ LegacyRouter::findPage('do=addlink', array(), false)
);
$this->assertNotEquals(
- Router::$PAGE_ADDLINK,
- Router::findPage('do=other', array(), true)
+ LegacyRouter::$PAGE_ADDLINK,
+ LegacyRouter::findPage('do=other', array(), true)
);
}
public function testFindPageExportValid()
{
$this->assertEquals(
- Router::$PAGE_EXPORT,
- Router::findPage('do=export', array(), true)
+ LegacyRouter::$PAGE_EXPORT,
+ LegacyRouter::findPage('do=export', array(), true)
);
$this->assertEquals(
- Router::$PAGE_EXPORT,
- Router::findPage('do=export&stuff', array(), true)
+ LegacyRouter::$PAGE_EXPORT,
+ LegacyRouter::findPage('do=export&stuff', array(), true)
);
}
public function testFindPageExportInvalid()
{
$this->assertNotEquals(
- Router::$PAGE_EXPORT,
- Router::findPage('do=export', array(), 1)
+ LegacyRouter::$PAGE_EXPORT,
+ LegacyRouter::findPage('do=export', array(), 1)
);
$this->assertNotEquals(
- Router::$PAGE_EXPORT,
- Router::findPage('do=export', array(), false)
+ LegacyRouter::$PAGE_EXPORT,
+ LegacyRouter::findPage('do=export', array(), false)
);
$this->assertNotEquals(
- Router::$PAGE_EXPORT,
- Router::findPage('do=other', array(), true)
+ LegacyRouter::$PAGE_EXPORT,
+ LegacyRouter::findPage('do=other', array(), true)
);
}
public function testFindPageImportValid()
{
$this->assertEquals(
- Router::$PAGE_IMPORT,
- Router::findPage('do=import', array(), true)
+ LegacyRouter::$PAGE_IMPORT,
+ LegacyRouter::findPage('do=import', array(), true)
);
$this->assertEquals(
- Router::$PAGE_IMPORT,
- Router::findPage('do=import&stuff', array(), true)
+ LegacyRouter::$PAGE_IMPORT,
+ LegacyRouter::findPage('do=import&stuff', array(), true)
);
}
public function testFindPageImportInvalid()
{
$this->assertNotEquals(
- Router::$PAGE_IMPORT,
- Router::findPage('do=import', array(), 1)
+ LegacyRouter::$PAGE_IMPORT,
+ LegacyRouter::findPage('do=import', array(), 1)
);
$this->assertNotEquals(
- Router::$PAGE_IMPORT,
- Router::findPage('do=import', array(), false)
+ LegacyRouter::$PAGE_IMPORT,
+ LegacyRouter::findPage('do=import', array(), false)
);
$this->assertNotEquals(
- Router::$PAGE_IMPORT,
- Router::findPage('do=other', array(), true)
+ LegacyRouter::$PAGE_IMPORT,
+ LegacyRouter::findPage('do=other', array(), true)
);
}
public function testFindPageEditlinkValid()
{
$this->assertEquals(
- Router::$PAGE_EDITLINK,
- Router::findPage('whatever', array('edit_link' => 1), true)
+ LegacyRouter::$PAGE_EDITLINK,
+ LegacyRouter::findPage('whatever', array('edit_link' => 1), true)
);
$this->assertEquals(
- Router::$PAGE_EDITLINK,
- Router::findPage('', array('edit_link' => 1), true)
+ LegacyRouter::$PAGE_EDITLINK,
+ LegacyRouter::findPage('', array('edit_link' => 1), true)
);
$this->assertEquals(
- Router::$PAGE_EDITLINK,
- Router::findPage('whatever', array('post' => 1), true)
+ LegacyRouter::$PAGE_EDITLINK,
+ LegacyRouter::findPage('whatever', array('post' => 1), true)
);
$this->assertEquals(
- Router::$PAGE_EDITLINK,
- Router::findPage('whatever', array('post' => 1, 'edit_link' => 1), true)
+ LegacyRouter::$PAGE_EDITLINK,
+ LegacyRouter::findPage('whatever', array('post' => 1, 'edit_link' => 1), true)
);
}
public function testFindPageEditlinkInvalid()
{
$this->assertNotEquals(
- Router::$PAGE_EDITLINK,
- Router::findPage('whatever', array('edit_link' => 1), false)
+ LegacyRouter::$PAGE_EDITLINK,
+ LegacyRouter::findPage('whatever', array('edit_link' => 1), false)
);
$this->assertNotEquals(
- Router::$PAGE_EDITLINK,
- Router::findPage('whatever', array('edit_link' => 1), 1)
+ LegacyRouter::$PAGE_EDITLINK,
+ LegacyRouter::findPage('whatever', array('edit_link' => 1), 1)
);
$this->assertNotEquals(
- Router::$PAGE_EDITLINK,
- Router::findPage('whatever', array(), true)
+ LegacyRouter::$PAGE_EDITLINK,
+ LegacyRouter::findPage('whatever', array(), true)
);
}
}
<?php
+
namespace Shaarli\Netscape;
+use PHPUnit\Framework\TestCase;
use Shaarli\Bookmark\BookmarkFileService;
-use Shaarli\Bookmark\LinkDB;
use Shaarli\Config\ConfigManager;
-use Shaarli\Formatter\FormatterFactory;
use Shaarli\Formatter\BookmarkFormatter;
+use Shaarli\Formatter\FormatterFactory;
use Shaarli\History;
require_once 'tests/utils/ReferenceLinkDB.php';
/**
* Netscape bookmark export
*/
-class BookmarkExportTest extends \PHPUnit\Framework\TestCase
+class BookmarkExportTest extends TestCase
{
/**
* @var string datastore to test write operations
*/
protected static $testDatastore = 'sandbox/datastore.php';
+ /**
+ * @var ConfigManager instance.
+ */
+ protected static $conf;
+
/**
* @var \ReferenceLinkDB instance.
*/
*/
protected static $formatter;
+ /**
+ * @var History instance
+ */
+ protected static $history;
+
+ /**
+ * @var NetscapeBookmarkUtils
+ */
+ protected $netscapeBookmarkUtils;
+
/**
* Instantiate reference data
*/
public static function setUpBeforeClass()
{
- $conf = new ConfigManager('tests/utils/config/configJson');
- $conf->set('resource.datastore', self::$testDatastore);
- self::$refDb = new \ReferenceLinkDB();
- self::$refDb->write(self::$testDatastore);
- $history = new History('sandbox/history.php');
- self::$bookmarkService = new BookmarkFileService($conf, $history, true);
- $factory = new FormatterFactory($conf, true);
- self::$formatter = $factory->getFormatter('raw');
+ static::$conf = new ConfigManager('tests/utils/config/configJson');
+ static::$conf->set('resource.datastore', static::$testDatastore);
+ static::$refDb = new \ReferenceLinkDB();
+ static::$refDb->write(static::$testDatastore);
+ static::$history = new History('sandbox/history.php');
+ static::$bookmarkService = new BookmarkFileService(static::$conf, static::$history, true);
+ $factory = new FormatterFactory(static::$conf, true);
+ static::$formatter = $factory->getFormatter('raw');
+ }
+
+ public function setUp(): void
+ {
+ $this->netscapeBookmarkUtils = new NetscapeBookmarkUtils(
+ static::$bookmarkService,
+ static::$conf,
+ static::$history
+ );
}
/**
*/
public function testFilterAndFormatInvalid()
{
- NetscapeBookmarkUtils::filterAndFormat(
- self::$bookmarkService,
+ $this->netscapeBookmarkUtils->filterAndFormat(
self::$formatter,
'derp',
false,
*/
public function testFilterAndFormatAll()
{
- $links = NetscapeBookmarkUtils::filterAndFormat(
- self::$bookmarkService,
+ $links = $this->netscapeBookmarkUtils->filterAndFormat(
self::$formatter,
'all',
false,
*/
public function testFilterAndFormatPrivate()
{
- $links = NetscapeBookmarkUtils::filterAndFormat(
- self::$bookmarkService,
+ $links = $this->netscapeBookmarkUtils->filterAndFormat(
self::$formatter,
'private',
false,
*/
public function testFilterAndFormatPublic()
{
- $links = NetscapeBookmarkUtils::filterAndFormat(
- self::$bookmarkService,
+ $links = $this->netscapeBookmarkUtils->filterAndFormat(
self::$formatter,
'public',
false,
*/
public function testFilterAndFormatDoNotPrependNoteUrl()
{
- $links = NetscapeBookmarkUtils::filterAndFormat(
- self::$bookmarkService,
+ $links = $this->netscapeBookmarkUtils->filterAndFormat(
self::$formatter,
'public',
false,
''
);
$this->assertEquals(
- '?WDWyig',
+ '/shaare/WDWyig',
$links[2]['url']
);
}
public function testFilterAndFormatPrependNoteUrl()
{
$indexUrl = 'http://localhost:7469/shaarli/';
- $links = NetscapeBookmarkUtils::filterAndFormat(
- self::$bookmarkService,
+ $links = $this->netscapeBookmarkUtils->filterAndFormat(
self::$formatter,
'public',
true,
$indexUrl
);
$this->assertEquals(
- $indexUrl . '?WDWyig',
+ $indexUrl . 'shaare/WDWyig',
$links[2]['url']
);
}
<?php
+
namespace Shaarli\Netscape;
use DateTime;
+use PHPUnit\Framework\TestCase;
+use Psr\Http\Message\UploadedFileInterface;
use Shaarli\Bookmark\Bookmark;
-use Shaarli\Bookmark\BookmarkFilter;
use Shaarli\Bookmark\BookmarkFileService;
-use Shaarli\Bookmark\LinkDB;
+use Shaarli\Bookmark\BookmarkFilter;
use Shaarli\Config\ConfigManager;
use Shaarli\History;
+use Slim\Http\UploadedFile;
/**
* Utility function to load a file's metadata in a $_FILES-like array
*
* @param string $filename Basename of the file
*
- * @return array A $_FILES-like array
+ * @return UploadedFileInterface Upload file in PSR-7 compatible object
*/
function file2array($filename)
{
- return array(
- 'filetoupload' => array(
- 'name' => $filename,
- 'tmp_name' => __DIR__ . '/input/' . $filename,
- 'size' => filesize(__DIR__ . '/input/' . $filename)
- )
+ return new UploadedFile(
+ __DIR__ . '/input/' . $filename,
+ $filename,
+ null,
+ filesize(__DIR__ . '/input/' . $filename)
);
}
/**
* Netscape bookmark import
*/
-class BookmarkImportTest extends \PHPUnit\Framework\TestCase
+class BookmarkImportTest extends TestCase
{
/**
* @var string datastore to test write operations
*/
protected $history;
+ /**
+ * @var NetscapeBookmarkUtils
+ */
+ protected $netscapeBookmarkUtils;
+
/**
* @var string Save the current timezone.
*/
$this->conf->set('resource.datastore', self::$testDatastore);
$this->history = new History(self::$historyFilePath);
$this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true);
+ $this->netscapeBookmarkUtils = new NetscapeBookmarkUtils($this->bookmarkService, $this->conf, $this->history);
}
/**
$this->assertEquals(
'File empty.htm (0 bytes) has an unknown file format.'
.' Nothing was imported.',
- NetscapeBookmarkUtils::import(null, $files, null, $this->conf, $this->history)
+ $this->netscapeBookmarkUtils->import(null, $files)
);
$this->assertEquals(0, $this->bookmarkService->count());
}
$files = file2array('no_doctype.htm');
$this->assertEquals(
'File no_doctype.htm (350 bytes) has an unknown file format. Nothing was imported.',
- NetscapeBookmarkUtils::import(null, $files, null, $this->conf, $this->history)
+ $this->netscapeBookmarkUtils->import(null, $files)
);
$this->assertEquals(0, $this->bookmarkService->count());
}
$this->assertStringMatchesFormat(
'File lowercase_doctype.htm (386 bytes) was successfully processed in %d seconds:'
.' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
- NetscapeBookmarkUtils::import(null, $files, $this->bookmarkService, $this->conf, $this->history)
+ $this->netscapeBookmarkUtils->import(null, $files)
);
$this->assertEquals(2, $this->bookmarkService->count());
}
$this->assertStringMatchesFormat(
'File internet_explorer_encoding.htm (356 bytes) was successfully processed in %d seconds:'
.' 1 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
- NetscapeBookmarkUtils::import([], $files, $this->bookmarkService, $this->conf, $this->history)
+ $this->netscapeBookmarkUtils->import([], $files)
);
$this->assertEquals(1, $this->bookmarkService->count());
$this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
$this->assertStringMatchesFormat(
'File netscape_nested.htm (1337 bytes) was successfully processed in %d seconds:'
.' 8 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
- NetscapeBookmarkUtils::import([], $files, $this->bookmarkService, $this->conf, $this->history)
+ $this->netscapeBookmarkUtils->import([], $files)
);
$this->assertEquals(8, $this->bookmarkService->count());
$this->assertEquals(2, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
$this->assertStringMatchesFormat(
'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
- NetscapeBookmarkUtils::import([], $files, $this->bookmarkService, $this->conf, $this->history)
+ $this->netscapeBookmarkUtils->import([], $files)
);
$this->assertEquals(2, $this->bookmarkService->count());
$this->assertStringMatchesFormat(
'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
- NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history)
+ $this->netscapeBookmarkUtils->import($post, $files)
);
$this->assertEquals(2, $this->bookmarkService->count());
$this->assertStringMatchesFormat(
'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
- NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history)
+ $this->netscapeBookmarkUtils->import($post, $files)
);
$this->assertEquals(2, $this->bookmarkService->count());
$this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
$this->assertStringMatchesFormat(
'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
- NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history)
+ $this->netscapeBookmarkUtils->import($post, $files)
);
$this->assertEquals(2, $this->bookmarkService->count());
$this->assertEquals(2, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
$this->assertStringMatchesFormat(
'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
- NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history)
+ $this->netscapeBookmarkUtils->import($post, $files)
);
$this->assertEquals(2, $this->bookmarkService->count());
$this->assertEquals(2, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
$this->assertStringMatchesFormat(
'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 bookmarks imported, 2 bookmarks overwritten, 0 bookmarks skipped.',
- NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history)
+ $this->netscapeBookmarkUtils->import($post, $files)
);
$this->assertEquals(2, $this->bookmarkService->count());
$this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
$this->assertStringMatchesFormat(
'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
- NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history)
+ $this->netscapeBookmarkUtils->import($post, $files)
);
$this->assertEquals(2, $this->bookmarkService->count());
$this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
$this->assertStringMatchesFormat(
'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 bookmarks imported, 2 bookmarks overwritten, 0 bookmarks skipped.',
- NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history)
+ $this->netscapeBookmarkUtils->import($post, $files)
);
$this->assertEquals(2, $this->bookmarkService->count());
$this->assertEquals(2, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
$this->assertStringMatchesFormat(
'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
- NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history)
+ $this->netscapeBookmarkUtils->import($post, $files)
);
$this->assertEquals(2, $this->bookmarkService->count());
$this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
$this->assertStringMatchesFormat(
'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 0 bookmarks imported, 0 bookmarks overwritten, 2 bookmarks skipped.',
- NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history)
+ $this->netscapeBookmarkUtils->import($post, $files)
);
$this->assertEquals(2, $this->bookmarkService->count());
$this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
$this->assertStringMatchesFormat(
'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
- NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history)
+ $this->netscapeBookmarkUtils->import($post, $files)
);
$this->assertEquals(2, $this->bookmarkService->count());
$this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
$this->assertStringMatchesFormat(
'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
- NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history)
+ $this->netscapeBookmarkUtils->import($post, $files)
);
$this->assertEquals(2, $this->bookmarkService->count());
$this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
$this->assertStringMatchesFormat(
'File same_date.htm (453 bytes) was successfully processed in %d seconds:'
.' 3 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
- NetscapeBookmarkUtils::import(array(), $files, $this->bookmarkService, $this->conf, $this->history)
+ $this->netscapeBookmarkUtils->import(array(), $files)
);
$this->assertEquals(3, $this->bookmarkService->count());
$this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
'overwrite' => 'true',
];
$files = file2array('netscape_basic.htm');
- NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history);
+ $this->netscapeBookmarkUtils->import($post, $files);
$history = $this->history->getHistory();
$this->assertEquals(1, count($history));
$this->assertEquals(History::IMPORT, $history[0]['event']);
$this->assertTrue(new DateTime('-5 seconds') < $history[0]['datetime']);
// re-import as private, enable overwriting
- NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history);
+ $this->netscapeBookmarkUtils->import($post, $files);
$history = $this->history->getHistory();
$this->assertEquals(2, count($history));
$this->assertEquals(History::IMPORT, $history[0]['event']);
namespace Shaarli\Plugin\Addlink;
use Shaarli\Plugin\PluginManager;
-use Shaarli\Router;
+use Shaarli\Render\TemplatePage;
require_once 'plugins/addlink_toolbar/addlink_toolbar.php';
{
$str = 'stuff';
$data = array($str => $str);
- $data['_PAGE_'] = Router::$PAGE_LINKLIST;
+ $data['_PAGE_'] = TemplatePage::LINKLIST;
$data['_LOGGEDIN_'] = true;
+ $data['_BASE_PATH_'] = '/subfolder';
$data = hook_addlink_toolbar_render_header($data);
$this->assertEquals($str, $data[$str]);
$data = array($str => $str);
$data['_PAGE_'] = $str;
$data['_LOGGEDIN_'] = true;
+ $data['_BASE_PATH_'] = '/subfolder';
+
$data = hook_addlink_toolbar_render_header($data);
$this->assertEquals($str, $data[$str]);
$this->assertArrayNotHasKey('fields_toolbar', $data);
{
$str = 'stuff';
$data = array($str => $str);
- $data['_PAGE_'] = Router::$PAGE_LINKLIST;
+ $data['_PAGE_'] = TemplatePage::LINKLIST;
$data['_LOGGEDIN_'] = false;
+ $data['_BASE_PATH_'] = '/subfolder';
$data = hook_addlink_toolbar_render_header($data);
$this->assertEquals($str, $data[$str]);
*/
use Shaarli\Plugin\PluginManager;
-use Shaarli\Router;
+use Shaarli\Render\TemplatePage;
require_once 'plugins/playvideos/playvideos.php';
{
$str = 'stuff';
$data = array($str => $str);
- $data['_PAGE_'] = Router::$PAGE_LINKLIST;
+ $data['_PAGE_'] = TemplatePage::LINKLIST;
$data = hook_playvideos_render_header($data);
$this->assertEquals($str, $data[$str]);
{
$str = 'stuff';
$data = array($str => $str);
- $data['_PAGE_'] = Router::$PAGE_LINKLIST;
+ $data['_PAGE_'] = TemplatePage::LINKLIST;
$data = hook_playvideos_render_footer($data);
$this->assertEquals($str, $data[$str]);
use Shaarli\Config\ConfigManager;
use Shaarli\Plugin\PluginManager;
-use Shaarli\Router;
+use Shaarli\Render\TemplatePage;
require_once 'plugins/pubsubhubbub/pubsubhubbub.php';
$hub = 'http://domain.hub';
$conf = new ConfigManager(self::$configFile);
$conf->set('plugins.PUBSUBHUB_URL', $hub);
- $data['_PAGE_'] = Router::$PAGE_FEED_RSS;
+ $data['_PAGE_'] = TemplatePage::FEED_RSS;
$data = hook_pubsubhubbub_render_feed($data, $conf);
$expected = '<atom:link rel="hub" href="'. $hub .'" />';
$hub = 'http://domain.hub';
$conf = new ConfigManager(self::$configFile);
$conf->set('plugins.PUBSUBHUB_URL', $hub);
- $data['_PAGE_'] = Router::$PAGE_FEED_ATOM;
+ $data['_PAGE_'] = TemplatePage::FEED_ATOM;
$data = hook_pubsubhubbub_render_feed($data, $conf);
$expected = '<link rel="hub" href="'. $hub .'" />';
*/
use Shaarli\Plugin\PluginManager;
-use Shaarli\Router;
+use Shaarli\Render\TemplatePage;
require_once 'plugins/qrcode/qrcode.php';
{
$str = 'stuff';
$data = array($str => $str);
- $data['_PAGE_'] = Router::$PAGE_LINKLIST;
+ $data['_PAGE_'] = TemplatePage::LINKLIST;
$data = hook_qrcode_render_footer($data);
$this->assertEquals($str, $data[$str]);
+++ /dev/null
-[#lol](?addtag=lol)
-
- #test
-
-`#test2`
-
-```
-bla #bli blo
-#bla
-```
+++ /dev/null
-#lol
-
- #test
-
-`#test2`
-
-```
-bla #bli blo
-#bla
-```
+++ /dev/null
-<div class="markdown"><ul>
-<li>test:
-<ul>
-<li><a href="http://link.tld">zero</a></li>
-<li><a href="http://link.tld">two</a></li>
-<li><a href="http://link.tld">three</a></li>
-</ul></li>
-</ul>
-<ol>
-<li><a href="http://link.tld">zero</a>
-<ol start="2">
-<li><a href="http://link.tld">two</a></li>
-<li><a href="http://link.tld">three</a></li>
-<li><a href="http://link.tld">four</a></li>
-<li>foo <a href="?addtag=foobar">#foobar</a></li>
-</ol></li>
-</ol>
-<p><a href="?addtag=foobar">#foobar</a> foo <code>lol #foo</code> <a href="?addtag=bar">#bar</a></p>
-<p>fsdfs <a href="http://link.tld">http://link.tld</a> <a href="?addtag=foobar">#foobar</a> <code>http://link.tld</code></p>
-<pre><code>http://link.tld #foobar
-next #foo</code></pre>
-<p>Block:</p>
-<pre><code>lorem ipsum #foobar http://link.tld
-#foobar http://link.tld</code></pre>
-<p><a href="?123456">link</a><br />
-<img src="/img/train.png" alt="link" /><br />
-<a href="http://test.tld/path/?query=value#hash">link</a><br />
-<a href="http://test.tld/path/?query=value#hash">link</a><br />
-<a href="https://test.tld/path/?query=value#hash">link</a><br />
-<a href="ftp://test.tld/path/?query=value#hash">link</a><br />
-<a href="magnet:test.tld/path/?query=value#hash">link</a><br />
-<a href="http://alert('xss')">link</a><br />
-<a href="http://test.tld/path/?query=value#hash">link</a></p></div>
+++ /dev/null
-* test:
- * [zero](http://link.tld)
- + [two](http://link.tld)
- - [three](http://link.tld)
-
-1. [zero](http://link.tld)
- 2. [two](http://link.tld)
- 3. [three](http://link.tld)
- 4. [four](http://link.tld)
- 5. foo #foobar
-
-#foobar foo `lol #foo` #bar
-
-fsdfs http://link.tld #foobar `http://link.tld`
-
- http://link.tld #foobar
- next #foo
-
-Block:
-
-```
-lorem ipsum #foobar http://link.tld
-#foobar http://link.tld
-```
-
-[link](?123456)
-![link](/img/train.png)
-[link](test.tld/path/?query=value#hash)
-[link](http://test.tld/path/?query=value#hash)
-[link](https://test.tld/path/?query=value#hash)
-[link](ftp://test.tld/path/?query=value#hash)
-[link](magnet:test.tld/path/?query=value#hash)
-[link](javascript:alert('xss'))
-[link](other://test.tld/path/?query=value#hash)
return $data;
}
+
+function hook_test_error()
+{
+ new Unknown();
+}
<?php
+
/**
* Cache tests
*/
-namespace Shaarli\Feed;
-// required to access $_SESSION array
-session_start();
+namespace Shaarli\Render;
-require_once 'application/feed/Cache.php';
+use PHPUnit\Framework\TestCase;
+use Shaarli\Security\SessionManager;
/**
* Unitary tests for cached pages
*/
-class CacheTest extends \PHPUnit\Framework\TestCase
+class PageCacheManagerTest extends TestCase
{
// test cache directory
protected static $testCacheDir = 'sandbox/dummycache';
// dummy cached file names / content
protected static $pages = array('a', 'toto', 'd7b59c');
+ /** @var PageCacheManager */
+ protected $cacheManager;
+
+ /** @var SessionManager */
+ protected $sessionManager;
/**
* Populate the cache with dummy files
*/
public function setUp()
{
+ $this->cacheManager = new PageCacheManager(static::$testCacheDir, true);
+
if (!is_dir(self::$testCacheDir)) {
mkdir(self::$testCacheDir);
} else {
*/
public function testPurgeCachedPages()
{
- purgeCachedPages(self::$testCacheDir);
+ $this->cacheManager->purgeCachedPages();
foreach (self::$pages as $page) {
$this->assertFileNotExists(self::$testCacheDir . '/' . $page . '.cache');
}
*/
public function testPurgeCachedPagesMissingDir()
{
+ $this->cacheManager = new PageCacheManager(self::$testCacheDir . '_missing', true);
+
$oldlog = ini_get('error_log');
ini_set('error_log', '/dev/null');
$this->assertEquals(
'Cannot purge sandbox/dummycache_missing: no directory',
- purgeCachedPages(self::$testCacheDir . '_missing')
+ $this->cacheManager->purgeCachedPages()
);
ini_set('error_log', $oldlog);
}
-
- /**
- * Purge cached pages and session cache
- */
- public function testInvalidateCaches()
- {
- $this->assertArrayNotHasKey('tags', $_SESSION);
- $_SESSION['tags'] = array('goodbye', 'cruel', 'world');
-
- invalidateCaches(self::$testCacheDir);
- foreach (self::$pages as $page) {
- $this->assertFileNotExists(self::$testCacheDir . '/' . $page . '.cache');
- }
-
- $this->assertArrayNotHasKey('tags', $_SESSION);
- }
}
<?php
-namespace Shaarli\Security;
-require_once 'tests/utils/FakeConfigManager.php';
+namespace Shaarli\Security;
use PHPUnit\Framework\TestCase;
/** @var string Salt used by hash functions */
protected $salt = '669e24fa9c5a59a613f98e8e38327384504a4af2';
+ /** @var CookieManager */
+ protected $cookieManager;
+
/**
* Prepare or reset test resources
*/
$this->cookie = [];
$this->session = [];
- $this->sessionManager = new SessionManager($this->session, $this->configManager);
- $this->loginManager = new LoginManager($this->configManager, $this->sessionManager);
+ $this->cookieManager = $this->createMock(CookieManager::class);
+ $this->cookieManager->method('getCookieParameter')->willReturnCallback(function (string $key) {
+ return $this->cookie[$key] ?? null;
+ });
+ $this->sessionManager = new SessionManager($this->session, $this->configManager, 'session_path');
+ $this->loginManager = new LoginManager($this->configManager, $this->sessionManager, $this->cookieManager);
$this->server['REMOTE_ADDR'] = $this->ipAddr;
}
$configManager = new \FakeConfigManager([
'resource.ban_file' => $this->banFile,
]);
- $loginManager = new LoginManager($configManager, null);
- $loginManager->checkLoginState([], '');
+ $loginManager = new LoginManager($configManager, null, $this->cookieManager);
+ $loginManager->checkLoginState('');
$this->assertFalse($loginManager->isLoggedIn());
}
'expires_on' => time() + 100,
];
$this->loginManager->generateStaySignedInToken($this->clientIpAddress);
- $this->cookie[LoginManager::$STAY_SIGNED_IN_COOKIE] = 'nope';
+ $this->cookie[CookieManager::STAY_SIGNED_IN] = 'nope';
- $this->loginManager->checkLoginState($this->cookie, $this->clientIpAddress);
+ $this->loginManager->checkLoginState($this->clientIpAddress);
$this->assertTrue($this->loginManager->isLoggedIn());
$this->assertTrue(empty($this->session['username']));
public function testCheckLoginStateStaySignedInWithValidToken()
{
$this->loginManager->generateStaySignedInToken($this->clientIpAddress);
- $this->cookie[LoginManager::$STAY_SIGNED_IN_COOKIE] = $this->loginManager->getStaySignedInToken();
+ $this->cookie[CookieManager::STAY_SIGNED_IN] = $this->loginManager->getStaySignedInToken();
- $this->loginManager->checkLoginState($this->cookie, $this->clientIpAddress);
+ $this->loginManager->checkLoginState($this->clientIpAddress);
$this->assertTrue($this->loginManager->isLoggedIn());
$this->assertEquals($this->login, $this->session['username']);
$this->loginManager->generateStaySignedInToken($this->clientIpAddress);
$this->session['expires_on'] = time() - 100;
- $this->loginManager->checkLoginState($this->cookie, $this->clientIpAddress);
+ $this->loginManager->checkLoginState($this->clientIpAddress);
$this->assertFalse($this->loginManager->isLoggedIn());
}
{
$this->loginManager->generateStaySignedInToken($this->clientIpAddress);
- $this->loginManager->checkLoginState($this->cookie, '10.7.157.98');
+ $this->loginManager->checkLoginState('10.7.157.98');
$this->assertFalse($this->loginManager->isLoggedIn());
}
<?php
-require_once 'tests/utils/FakeConfigManager.php';
-// Initialize reference data _before_ PHPUnit starts a session
-require_once 'tests/utils/ReferenceSessionIdHashes.php';
-ReferenceSessionIdHashes::genAllHashes();
+namespace Shaarli\Security;
use PHPUnit\Framework\TestCase;
-use Shaarli\Security\SessionManager;
/**
* Test coverage for SessionManager
*/
public static function setUpBeforeClass()
{
- self::$sidHashes = ReferenceSessionIdHashes::getHashes();
+ self::$sidHashes = \ReferenceSessionIdHashes::getHashes();
}
/**
*/
public function setUp()
{
- $this->conf = new FakeConfigManager([
+ $this->conf = new \FakeConfigManager([
'credentials.login' => 'johndoe',
'credentials.salt' => 'salt',
'security.session_protection_disabled' => false,
]);
$this->session = [];
- $this->sessionManager = new SessionManager($this->session, $this->conf);
+ $this->sessionManager = new SessionManager($this->session, $this->conf, 'session_path');
}
/**
$token => 1,
],
];
- $sessionManager = new SessionManager($session, $this->conf);
+ $sessionManager = new SessionManager($session, $this->conf, 'session_path');
// check and destroy the token
$this->assertTrue($sessionManager->checkToken($token));
$this->session['ip'] = 'ip_id_one';
$this->assertTrue($this->sessionManager->hasClientIpChanged('ip_id_two'));
}
+
+ /**
+ * Test creating an entry in the session array
+ */
+ public function testSetSessionParameterCreate(): void
+ {
+ $this->sessionManager->setSessionParameter('abc', 'def');
+
+ static::assertSame('def', $this->session['abc']);
+ }
+
+ /**
+ * Test updating an entry in the session array
+ */
+ public function testSetSessionParameterUpdate(): void
+ {
+ $this->session['abc'] = 'ghi';
+
+ $this->sessionManager->setSessionParameter('abc', 'def');
+
+ static::assertSame('def', $this->session['abc']);
+ }
+
+ /**
+ * Test updating an entry in the session array with null value
+ */
+ public function testSetSessionParameterUpdateNull(): void
+ {
+ $this->session['abc'] = 'ghi';
+
+ $this->sessionManager->setSessionParameter('abc', null);
+
+ static::assertArrayHasKey('abc', $this->session);
+ static::assertNull($this->session['abc']);
+ }
+
+ /**
+ * Test deleting an existing entry in the session array
+ */
+ public function testDeleteSessionParameter(): void
+ {
+ $this->session['abc'] = 'def';
+
+ $this->sessionManager->deleteSessionParameter('abc');
+
+ static::assertArrayNotHasKey('abc', $this->session);
+ }
+
+ /**
+ * Test deleting a non existent entry in the session array
+ */
+ public function testDeleteSessionParameterNotExisting(): void
+ {
+ $this->sessionManager->deleteSessionParameter('abc');
+
+ static::assertArrayNotHasKey('abc', $this->session);
+ }
}
namespace Shaarli\Updater;
use Exception;
+use PHPUnit\Framework\TestCase;
+use Shaarli\Bookmark\BookmarkFileService;
+use Shaarli\Bookmark\BookmarkServiceInterface;
use Shaarli\Config\ConfigManager;
+use Shaarli\History;
-require_once 'tests/updater/DummyUpdater.php';
-require_once 'tests/utils/ReferenceLinkDB.php';
-require_once 'inc/rain.tpl.class.php';
/**
* Class UpdaterTest.
* Runs unit tests against the updater class.
*/
-class UpdaterTest extends \PHPUnit\Framework\TestCase
+class UpdaterTest extends TestCase
{
/**
* @var string Path to test datastore.
*/
protected $conf;
+ /** @var BookmarkServiceInterface */
+ protected $bookmarkService;
+
+ /** @var \ReferenceLinkDB */
+ protected $refDB;
+
+ /** @var Updater */
+ protected $updater;
+
/**
* Executed before each test.
*/
public function setUp()
{
+ $this->refDB = new \ReferenceLinkDB();
+ $this->refDB->write(self::$testDatastore);
+
copy('tests/utils/config/configJson.json.php', self::$configFile .'.json.php');
$this->conf = new ConfigManager(self::$configFile);
+ $this->bookmarkService = new BookmarkFileService($this->conf, $this->createMock(History::class), true);
+ $this->updater = new Updater([], $this->bookmarkService, $this->conf, true);
}
/**
$updater = new DummyUpdater($updates, array(), $this->conf, true);
$updater->update();
}
+
+ public function testUpdateMethodRelativeHomeLinkRename(): void
+ {
+ $this->updater->setBasePath('/subfolder');
+ $this->conf->set('general.header_link', '?');
+
+ $this->updater->updateMethodRelativeHomeLink();
+
+ static::assertSame('/subfolder/', $this->conf->get('general.header_link'));
+ }
+
+ public function testUpdateMethodRelativeHomeLinkDoNotRename(): void
+ {
+ $this->conf->set('general.header_link', '~/my-blog');
+
+ $this->updater->updateMethodRelativeHomeLink();
+
+ static::assertSame('~/my-blog', $this->conf->get('general.header_link'));
+ }
+
+ public function testUpdateMethodMigrateExistingNotesUrl(): void
+ {
+ $this->updater->updateMethodMigrateExistingNotesUrl();
+
+ static::assertSame($this->refDB->getLinks()[0]->getUrl(), $this->bookmarkService->get(0)->getUrl());
+ static::assertSame($this->refDB->getLinks()[1]->getUrl(), $this->bookmarkService->get(1)->getUrl());
+ static::assertSame($this->refDB->getLinks()[4]->getUrl(), $this->bookmarkService->get(4)->getUrl());
+ static::assertSame($this->refDB->getLinks()[6]->getUrl(), $this->bookmarkService->get(6)->getUrl());
+ static::assertSame($this->refDB->getLinks()[7]->getUrl(), $this->bookmarkService->get(7)->getUrl());
+ static::assertSame($this->refDB->getLinks()[8]->getUrl(), $this->bookmarkService->get(8)->getUrl());
+ static::assertSame($this->refDB->getLinks()[9]->getUrl(), $this->bookmarkService->get(9)->getUrl());
+ static::assertSame('/shaare/WDWyig', $this->bookmarkService->get(42)->getUrl());
+ static::assertSame('/shaare/WDWyig', $this->bookmarkService->get(41)->getUrl());
+ static::assertSame('/shaare/0gCTjQ', $this->bookmarkService->get(10)->getUrl());
+ static::assertSame('/shaare/PCRizQ', $this->bookmarkService->get(11)->getUrl());
+ }
}
$this->addLink(
11,
'Pined older',
- '?PCRizQ',
+ '/shaare/PCRizQ',
'This is an older pinned link',
0,
DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20100309_101010'),
$this->addLink(
10,
'Pined',
- '?0gCTjQ',
+ '/shaare/0gCTjQ',
'This is a pinned link',
0,
DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20121207_152312'),
$this->addLink(
41,
'Link title: @website',
- '?WDWyig',
+ '/shaare/WDWyig',
'Stallman has a beard and is part of the Free Software Foundation (or not). Seriously, read this. #hashtag',
0,
DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114651'),
$this->addLink(
42,
'Note: I have a big ID but an old date',
- '?WDWyig',
+ '/shaare/WDWyig',
'Used to test bookmarks reordering.',
0,
DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20100310_101010'),
{include="page.header"}
<div id="pageError" class="page-error-container center">
<h2>{'Sorry, nothing to see here.'|t}</h2>
- <img src="img/sad_star.png" alt="">
+ <img src="{$asset_path}/img/sad_star.png#" alt="">
<p>{$error_message}</p>
</div>
{include="page.footer"}
<div class="pure-u-lg-1-3 pure-u-1-24"></div>
<div id="addlink-form" class="page-form page-form-light pure-u-lg-1-3 pure-u-22-24">
<h2 class="window-title">{"Shaare a new link"|t}</h2>
- <form method="GET" action="#" name="addform" class="addform">
+ <form method="GET" action="{$base_path}/admin/shaare" name="addform" class="addform">
<div>
<label for="shaare">{'URL or leave empty to post a note'|t}</label>
<input type="text" name="post" id="shaare" class="autofocus">
<div class="pure-u-lg-1-3 pure-u-1-24"></div>
<div id="addlink-form" class="page-form page-form-light pure-u-lg-1-3 pure-u-22-24">
<h2 class="window-title">{"Change password"|t}</h2>
- <form method="POST" action="#" name="changepasswordform" id="changepasswordform">
+ <form method="POST" action="{$base_path}/admin/password" name="changepasswordform" id="changepasswordform">
<div>
<input type="password" name="oldpassword" aria-label="{'Current password'|t}" placeholder="{'Current password'|t}" class="autofocus">
</div>
<div class="pure-u-lg-1-3 pure-u-1-24"></div>
<div id="addlink-form" class="page-form page-form-light pure-u-lg-1-3 pure-u-22-24">
<h2 class="window-title">{"Manage tags"|t}</h2>
- <form method="POST" action="#" name="changetag" id="changetag">
+ <form method="POST" action="{$base_path}/admin/tags" name="changetag" id="changetag">
<div>
<input type="text" name="fromtag" aria-label="{'Tag'|t}" placeholder="{'Tag'|t}" value="{$fromtag}"
list="tagsList" autocomplete="off" class="awesomplete autofocus" data-minChars="1">
</div>
</form>
- <p>{'You can also edit tags in the'|t} <a href="?do=taglist&sort=usage">{'tag list'|t}</a>.</p>
+ <p>{'You can also edit tags in the'|t} <a href="{$base_path}/tags/list?sort=usage">{'tag list'|t}</a>.</p>
</div>
</div>
{include="page.footer"}
{$ratioInput='7-12'}
{$ratioInputMobile='1-8'}
-<form method="POST" action="#" name="configform" id="configform">
+<form method="POST" action="{$base_path}/admin/configure" name="configform" id="configform">
<div class="pure-g">
<div class="pure-u-lg-1-8 pure-u-1-24"></div>
<div class="pure-u-lg-3-4 pure-u-22-24 page-form page-form-complete">
<div class="form-label">
<label for="titleLink">
<span class="label-name">{'Home link'|t}</span><br>
- <span class="label-desc">{'Default value'|t}: ?</span>
+ <span class="label-desc">{'Default value'|t}: {$base_path}/</span>
</label>
</div>
</div>
{if="! $gd_enabled"}
{'You need to enable the extension <code>php-gd</code> to use thumbnails.'|t}
{elseif="$thumbnails_enabled"}
- <a href="?do=thumbs_update">{'Synchronize thumbnails'|t}</a>
+ <a href="{$base_path}/admin/thumbnails">{'Synchronize thumbnails'|t}</a>
{/if}
</span>
</label>
<div class="pure-u-lg-2-3 pure-u-22-24 page-form page-visitor" id="daily">
<h2 class="window-title">
{'The Daily Shaarli'|t}
- <a href="?do=dailyrss" title="{'1 RSS entry per day'|t}"><i class="fa fa-rss"></i></a>
+ <a href="{$base_path}/daily-rss" title="{'1 RSS entry per day'|t}"><i class="fa fa-rss"></i></a>
</h2>
<div id="plugin_zone_start_daily" class="plugin_zone">
<div class="pure-g">
<div class="pure-u-lg-1-3 pure-u-1 center">
{if="$previousday"}
- <a href="?do=daily&day={$previousday}">
+ <a href="{$base_path}/daily?day={$previousday}">
<i class="fa fa-arrow-left"></i>
{'Previous day'|t}
</a>
</div>
<div class="pure-u-lg-1-3 pure-u-1 center">
{if="$nextday"}
- <a href="?do=daily&day={$nextday}">
+ <a href="{$base_path}/daily?day={$nextday}">
{'Next day'|t}
<i class="fa fa-arrow-right"></i>
</a>
{$link=$value}
<div class="daily-entry">
<div class="daily-entry-title center">
- <a href="?{$link.shorturl}" title="{'Permalink'|t}">
+ <a href="{$base_path}/?{$link.shorturl}" title="{'Permalink'|t}">
<i class="fa fa-link"></i>
</a>
<a href="{$link.real_url}">{$link.title}</a>
{if="$link.tags"}
<div class="daily-entry-tags center">
{loop="link.taglist"}
- <span class="label label-tag" title="Add tag">
+ <span class="label label-tag">
{$value}
</span>
{/loop}
</div>
</div>
{include="page.footer"}
-<script src="js/thumbnails.min.js?v={$version_hash}"></script>
+<script src="{$asset_path}/js/thumbnails.min.js?v={$version_hash}#"></script>
</body>
</html>
-<item>
- <title>{$title} - {function="strftime('%A %e %B %Y', $daydate)"}</title>
- <guid>{$absurl}</guid>
- <link>{$absurl}</link>
- <pubDate>{$rssdate}</pubDate>
- <description><![CDATA[
- {loop="links"}
- <h3><a href="{$value.url}">{$value.title}</a></h3>
- <small>{if="!$hide_timestamps"}{function="strftime('%c', $value.timestamp)"} - {/if}{if="$value.tags"}{$value.tags}{/if}<br>
- {$value.url}</small><br>
- {if="$value.thumbnail"}<img src="{$index_url}{$value.thumbnail}#" alt="thumbnail" />{/if}<br>
- {if="$value.description"}{$value.formatedDescription}{/if}
- <br><br><hr>
- {/loop}
- ]]></description>
-</item>
+<?xml version="1.0" encoding="UTF-8"?>
+<rss version="2.0">
+ <channel>
+ <title>Daily - {$title}</title>
+ <link>{$index_url}</link>
+ <description>Daily shaared bookmarks</description>
+ <language>{$language}</language>
+ <copyright>{$index_url}</copyright>
+ <generator>Shaarli</generator>
+
+ {loop="$days"}
+ <item>
+ <title>{$value.date_human} - {$title}</title>
+ <guid>{$value.absolute_url}</guid>
+ <link>{$value.absolute_url}</link>
+ <pubDate>{$value.date_rss}</pubDate>
+ <description><![CDATA[
+ {loop="$value.links"}
+ <h3><a href="{$value.url}">{$value.title}</a></h3>
+ <small>
+ {if="!$hide_timestamps"}{$value.created|format_date} - {/if}{if="$value.tags"}{$value.tags}{/if}<br>
+ {$value.url}
+ </small><br>
+ {if="$value.thumbnail"}<img src="{$index_url}{$value.thumbnail}#" alt="thumbnail" />{/if}<br>
+ {if="$value.description"}{$value.description}{/if}
+ <br><br><hr>
+ {/loop}
+ ]]></description>
+ </item>
+ {/loop}
+ </channel>
+</rss><!-- Cached version of {$page_url} -->
{include="page.header"}
<div id="editlinkform" class="edit-link-container" class="pure-g">
<div class="pure-u-lg-1-5 pure-u-1-24"></div>
- <form method="post" name="linkform" class="page-form pure-u-lg-3-5 pure-u-22-24 page-form page-form-light">
+ <form method="post"
+ name="linkform"
+ action="{$base_path}/admin/shaare"
+ class="page-form pure-u-lg-3-5 pure-u-22-24 page-form page-form-light"
+ >
<h2 class="window-title">
{if="!$link_is_new"}{'Edit Shaare'|t}{else}{'New Shaare'|t}{/if}
</h2>
<input type="submit" name="save_edit" class="" id="button-save-edit"
value="{if="$link_is_new"}{'Save'|t}{else}{'Apply Changes'|t}{/if}">
{if="!$link_is_new"}
- <a href="?delete_link&lf_linkdate={$link.id}&token={$token}"
+ <a href="{$base_path}/admin/shaare/delete?id={$link.id}&token={$token}"
title="" name="delete_link" class="button button-red confirm-delete">
{'Delete'|t}
</a>
</div>
<input type="hidden" name="token" value="{$token}">
+ <input type="hidden" name="source" value="{$source}">
{if="$http_referer"}
<input type="hidden" name="returnurl" value="{$http_referer}">
{/if}
</pre>
{/if}
- <img src="img/sad_star.png" alt="">
+ <img src="{$asset_path}/img/sad_star.png#" alt="">
</div>
{include="page.footer"}
</body>
<body>
{include="page.header"}
-<form method="GET" action="#" name="exportform" id="exportform">
+<form method="POST" action="{$base_path}/admin/export" name="exportform" id="exportform">
<div class="pure-g">
<div class="pure-u-lg-1-4 pure-u-1-24"></div>
<div class="pure-u-lg-1-2 pure-u-22-24 page-form page-form-complete">
<div>
<h2 class="window-title">{"Export Database"|t}</h2>
</div>
- <input type="hidden" name="do" value="export">
<input type="hidden" name="token" value="{$token}">
<div class="pure-g">
<updated>{$last_update}</updated>
{/if}
<link rel="self" href="{$self_link}#" />
+ <link rel="search" type="application/opensearchdescription+xml" href="{$index_url}open-search#"
+ title="Shaarli search - {$shaarlititle}" />
{loop="$plugins_feed_header"}
{$value}
{/loop}
<language>{$language}</language>
<copyright>{$index_url}</copyright>
<generator>Shaarli</generator>
- <atom:link rel="self" href="{$self_link}" />
+ <atom:link rel="self" href="{$self_link}" />
+ <atom:link rel="search" type="application/opensearchdescription+xml" href="{$index_url}open-search#"
+ title="Shaarli search - {$shaarlititle}" />
{loop="$plugins_feed_header"}
{$value}
{/loop}
<body>
{include="page.header"}
-<form method="POST" action="?do=import" enctype="multipart/form-data" name="uploadform" id="uploadform">
+<form method="POST" action="{$base_path}/admin/import" enctype="multipart/form-data" name="uploadform" id="uploadform">
<div class="pure-g">
<div class="pure-u-lg-1-4 pure-u-1-24"></div>
<div class="pure-u-lg-1-2 pure-u-22-24 page-form page-form-complete">
<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 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}" />
+<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="{$asset_path}/img/favicon.png#" rel="shortcut icon" type="image/png" />
+<link href="{$asset_path}/img/apple-touch-icon.png#" rel="apple-touch-icon" sizes="180x180" />
+<link type="text/css" rel="stylesheet" href="{$asset_path}/css/shaarli.min.css?v={$version_hash}#" />
{if="$formatter==='markdown'"}
- <link type="text/css" rel="stylesheet" href="css/markdown.min.css?v={$version_hash}" />
+ <link type="text/css" rel="stylesheet" href="{$asset_path}/css/markdown.min.css?v={$version_hash}#" />
{/if}
{loop="$plugins_includes.css_files"}
- <link type="text/css" rel="stylesheet" href="{$value}?v={$version_hash}#"/>
+ <link type="text/css" rel="stylesheet" href="{$base_path}/{$value}?v={$version_hash}#"/>
{/loop}
{if="is_file('data/user.css')"}
- <link type="text/css" rel="stylesheet" href="data/user.css#" />
+ <link type="text/css" rel="stylesheet" href="{$base_path}/data/user.css#" />
{/if}
-<link rel="search" type="application/opensearchdescription+xml" href="?do=opensearch#" title="Shaarli search - {$shaarlititle}"/>
+<link rel="search" type="application/opensearchdescription+xml" href="{$base_path}/open-search#"
+ title="Shaarli search - {$shaarlititle}" />
{if="! empty($links) && count($links) === 1"}
{$link=reset($links)}
<meta property="og:title" content="{$link.title}" />
{$ratioLabelMobile='7-8'}
{$ratioInputMobile='1-8'}
-<form method="POST" action="#" name="installform" id="installform">
+<form method="POST" action="{$base_path}/install" name="installform" id="installform">
<div class="pure-g">
<div class="pure-u-lg-1-6 pure-u-1-24"></div>
<div class="pure-u-lg-2-3 pure-u-22-24 page-form page-form-complete">
{'tagged'|t}
{loop="$exploded_tags"}
<span class="label label-tag" title="{'Remove tag'|t}">
- <a href="?removetag={function="urlencode($value)"}" aria-label="{'Remove tag'|t}">{$value}<span class="remove"><i class="fa fa-times" aria-hidden="true"></i></span></a>
+ <a href="{$base_path}/remove-tag/{function="urlencode($value)"}" aria-label="{'Remove tag'|t}">
+ {$value}<span class="remove"><i class="fa fa-times" aria-hidden="true"></i></span>
+ </a>
</span>
{/loop}
{/if}
<div class="thumbnail">
{ignore}RainTPL hack: put the 2 src on two different line to avoid path replace bug{/ignore}
<a href="{$value.real_url}" aria-hidden="true" tabindex="-1">
- <img data-src="{$value.thumbnail}#" class="b-lazy"
+ <img data-src="{$base_path}/{$value.thumbnail}#" class="b-lazy"
src=""
alt="" width="{$thumbnails_width}" height="{$thumbnails_height}" />
</a>
{$tag_counter=count($value.taglist)}
{loop="value.taglist"}
<span class="label label-tag" title="{$strAddTag}">
- <a href="?addtag={$value|urlencode}">{$value}</a>
+ <a href="{$base_path}/add-tag/{$value|urlencode}">{$value}</a>
</span>
{if="$tag_counter - 1 != $counter"}·{/if}
{/loop}
<input type="checkbox" class="link-checkbox" value="{$value.id}">
</span>
<span class="linklist-item-infos-controls-item ctrl-edit">
- <a href="?edit_link={$value.id}" aria-label="{$strEdit}" title="{$strEdit}"><i class="fa fa-pencil-square-o edit-link" aria-hidden="true"></i></a>
+ <a href="{$base_path}/admin/shaare/{$value.id}" aria-label="{$strEdit}" title="{$strEdit}"><i class="fa fa-pencil-square-o edit-link" aria-hidden="true"></i></a>
</span>
<span class="linklist-item-infos-controls-item ctrl-delete">
- <a href="?delete_link&lf_linkdate={$value.id}&token={$token}" aria-label="{$strDelete}"
+ <a href="{$base_path}/admin/shaare/delete?id={$value.id}&token={$token}" aria-label="{$strDelete}"
title="{$strDelete}" class="delete-link pure-u-0 pure-u-lg-visible confirm-delete">
<i class="fa fa-trash" aria-hidden="true"></i>
</a>
</span>
<span class="linklist-item-infos-controls-item ctrl-pin">
- <a href="?do=pin&id={$value.id}&token={$token}"
+ <a href="{$base_path}/admin/shaare/{$value.id}/pin?token={$token}"
title="{$strToggleSticky}" aria-label="{$strToggleSticky}" class="pin-link {if="$value.sticky"}pinned-link{/if} pure-u-0 pure-u-lg-visible">
<i class="fa fa-thumb-tack" aria-hidden="true"></i>
</a>
</div>
{/if}
{/if}
- <a href="?{$value.shorturl}" title="{$strPermalink}">
+ <a href="{$base_path}/shaare/{$value.shorturl}" title="{$strPermalink}">
{if="!$hide_timestamps || $is_logged_in"}
{$updated=$value.updated_timestamp ? $strEdited. format_date($value.updated) : $strPermalink}
<span class="linkdate" title="{$updated}">
{/if}
{if="$is_logged_in"}
·
- <a href="?delete_link&lf_linkdate={$value.id}&token={$token}" aria-label="{$strDelete}"
+ <a href="{$base_path}/admin/shaare/delete?id={$value.id}&token={$token}" aria-label="{$strDelete}"
title="{$strDelete}" class="delete-link confirm-delete">
<i class="fa fa-trash" aria-hidden="true"></i>
</a>
·
- <a href="?edit_link={$value.id}" aria-label="{$strEdit}" title="{$strEdit}"><i class="fa fa-pencil-square-o edit-link" aria-hidden="true"></i></a>
+ <a href="{$base_path}/admin/shaare/{$value.id}" aria-label="{$strEdit}" title="{$strEdit}"><i class="fa fa-pencil-square-o edit-link" aria-hidden="true"></i></a>
{/if}
</div>
</div>
</div>
{include="page.footer"}
-<script src="js/thumbnails.min.js?v={$version_hash}"></script>
+<script src="{$asset_path}/js/thumbnails.min.js?v={$version_hash}#"></script>
</body>
</html>
{'Filters'|t}
</span>
{if="$is_logged_in"}
- <a href="?visibility=private" aria-label="{'Only display private links'|t}" title="{'Only display private links'|t}"
+ <a href="{$base_path}/admin/visibility/private" aria-label="{'Only display private links'|t}" title="{'Only display private links'|t}"
class="{if="$visibility==='private'"}filter-on{else}filter-off{/if}"
><i class="fa fa-user-secret" aria-hidden="true"></i></a>
- <a href="?visibility=public" aria-label="{'Only display public links'|t}" title="{'Only display public links'|t}"
+ <a href="{$base_path}/admin/visibility/public" aria-label="{'Only display public links'|t}" title="{'Only display public links'|t}"
class="{if="$visibility==='public'"}filter-on{else}filter-off{/if}"
><i class="fa fa-globe" aria-hidden="true"></i></a>
{/if}
- <a href="?untaggedonly" aria-label="{'Filter untagged links'|t}" title="{'Filter untagged links'|t}"
+ <a href="{$base_path}/untagged-only" aria-label="{'Filter untagged links'|t}" title="{'Filter untagged links'|t}"
class={if="$untaggedonly"}"filter-on"{else}"filter-off"{/if}
><i class="fa fa-tag" aria-hidden="true"></i></a>
<a href="#" aria-label="{'Select all'|t}" title="{'Select all'|t}"
<div class="linksperpage pure-u-1-3">
<div class="pure-u-0 pure-u-lg-visible">{'Links per page'|t}</div>
- <a href="?linksperpage=20">20</a>
- <a href="?linksperpage=50">50</a>
- <a href="?linksperpage=100">100</a>
- <form method="GET" class="pure-u-0 pure-u-lg-visible">
- <input type="text" name="linksperpage" placeholder="133">
+ <a href="{$base_path}/links-per-page?nb=20">20</a>
+ <a href="{$base_path}/links-per-page?nb=50">50</a>
+ <a href="{$base_path}/links-per-page?nb=100">100</a>
+ <form method="GET" class="pure-u-0 pure-u-lg-visible" action="{$base_path}/links-per-page">
+ <input type="text" name="nb" placeholder="133">
</form>
<a href="#" class="filter-off fold-all pure-u-0 pure-u-lg-visible" aria-label="{'Fold all'|t}" title="{'Fold all'|t}">
<i class="fa fa-chevron-up" aria-hidden="true"></i>
<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&searchterm={searchTerms}"/>
- <Url type="application/rss+xml" template="{$serverurl}?do=rss&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
{/if}
·
{'The personal, minimalist, super-fast, database free, bookmarking service'|t} {'by the Shaarli community'|t} ·
- <a href="doc/html/index.html" rel="nofollow">{'Documentation'|t}</a>
+ <a href="{$base_path}/doc/html/index.html" rel="nofollow">{'Documentation'|t}</a>
{loop="$plugins_footer.text"}
{$value}
{/loop}
{/loop}
{loop="$plugins_footer.js_files"}
- <script src="{$value}#"></script>
+ <script src="{$base_path}/{$value}#"></script>
{/loop}
<div id="js-translations" class="hidden">
</span>
</div>
-<script src="js/shaarli.min.js?v={$version_hash}"></script>
+<input type="hidden" name="js_base_path" value="{$base_path}" />
+<script src="{$asset_path}/js/shaarli.min.js?v={$version_hash}#"></script>
</li>
{if="$is_logged_in || $openshaarli"}
<li class="pure-menu-item">
- <a href="?do=addlink" class="pure-menu-link" id="shaarli-menu-shaare">
+ <a href="{$base_path}/admin/add-shaare" class="pure-menu-link" id="shaarli-menu-shaare">
<i class="fa fa-plus" aria-hidden="true"></i> {'Shaare'|t}
</a>
</li>
<li class="pure-menu-item" id="shaarli-menu-tools">
- <a href="?do=tools" class="pure-menu-link">{'Tools'|t}</a>
+ <a href="{$base_path}/admin/tools" class="pure-menu-link">{'Tools'|t}</a>
</li>
{/if}
<li class="pure-menu-item" id="shaarli-menu-tags">
- <a href="?do=tagcloud" class="pure-menu-link">{'Tag cloud'|t}</a>
+ <a href="{$base_path}/tags/cloud" class="pure-menu-link">{'Tag cloud'|t}</a>
</li>
{if="$thumbnails_enabled"}
<li class="pure-menu-item" id="shaarli-menu-picwall">
- <a href="?do=picwall{$searchcrits}" class="pure-menu-link">{'Picture wall'|t}</a>
+ <a href="{$base_path}/picture-wall?{function="ltrim($searchcrits, '&')"}" class="pure-menu-link">{'Picture wall'|t}</a>
</li>
{/if}
<li class="pure-menu-item" id="shaarli-menu-daily">
- <a href="?do=daily" class="pure-menu-link">{'Daily'|t}</a>
+ <a href="{$base_path}/daily" class="pure-menu-link">{'Daily'|t}</a>
</li>
{loop="$plugins_header.buttons_toolbar"}
<li class="pure-menu-item shaarli-menu-plugin">
</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="{$base_path}/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">
- <a href="?do=logout" class="pure-menu-link">{'Logout'|t}</a>
+ <a href="{$base_path}/admin/logout" class="pure-menu-link">{'Logout'|t}</a>
</li>
{else}
<li class="pure-menu-item pure-u-lg-0 shaarli-menu-mobile" id="shaarli-menu-mobile-login">
- <a href="/login" class="pure-menu-link">{'Login'|t}</a>
+ <a href="{$base_path}/login" class="pure-menu-link">{'Login'|t}</a>
</li>
{/if}
</ul>
</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="{$base_path}/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>
{if="!$is_logged_in"}
<li class="pure-menu-item" id="shaarli-menu-desktop-login">
- <a href="/login" class="pure-menu-link"
+ <a href="{$base_path}/login" class="pure-menu-link"
data-open-id="header-login-form"
id="login-button" aria-label="{'Login'|t}" title="{'Login'|t}">
<i class="fa fa-user" aria-hidden="true"></i>
</li>
{else}
<li class="pure-menu-item" id="shaarli-menu-desktop-logout">
- <a href="?do=logout" class="pure-menu-link" aria-label="{'Logout'|t}" title="{'Logout'|t}">
+ <a href="{$base_path}/admin/logout" class="pure-menu-link" aria-label="{'Logout'|t}" title="{'Logout'|t}">
<i class="fa fa-sign-out" aria-hidden="true"></i>
</a>
</li>
<main id="content" class="container" role="main">
<div id="search" class="subheader-form searchform-block header-search">
- <form method="GET" class="pure-form searchform" name="searchform">
+ <form method="GET" class="pure-form searchform" name="searchform" action="{$base_path}/">
<input type="text" id="searchform_value" name="searchterm" aria-label="{'Search text'|t}" placeholder="{'Search text'|t}"
{if="!empty($search_term)"}
value="{$search_term}"
</div>
{/if}
-{if="!empty($global_warnings) && $is_logged_in"}
- <div class="pure-g pure-alert pure-alert-warning pure-alert-closable" id="shaarli-warnings-alert">
+{if="!empty($global_errors)"}
+ <div class="pure-g header-alert-message pure-alert pure-alert-error pure-alert-closable" id="shaarli-errors-alert">
+ <div class="pure-u-2-24"></div>
+ <div class="pure-u-20-24">
+ {loop="$global_errors"}
+ <p>{$value}</p>
+ {/loop}
+ </div>
+ <div class="pure-u-2-24">
+ <i class="fa fa-times pure-alert-close"></i>
+ </div>
+ </div>
+{/if}
+
+{if="!empty($global_warnings)"}
+ <div class="pure-g header-alert-message pure-alert pure-alert-warning pure-alert-closable" id="shaarli-warnings-alert">
<div class="pure-u-2-24"></div>
<div class="pure-u-20-24">
{loop="global_warnings"}
</div>
{/if}
+{if="!empty($global_successes)"}
+ <div class="pure-g header-alert-message new-version-message pure-alert pure-alert-success pure-alert-closable" id="shaarli-success-alert">
+ <div class="pure-u-2-24"></div>
+ <div class="pure-u-20-24">
+ {loop="$global_successes"}
+ <p>{$value}</p>
+ {/loop}
+ </div>
+ <div class="pure-u-2-24">
+ <i class="fa fa-times pure-alert-close"></i>
+ </div>
+ </div>
+{/if}
+
<div class="clear"></div>
</head>
<body>
{include="page.header"}
-{if="!$thumbnails_enabled"}
-<div class="pure-g pure-alert pure-alert-warning page-single-alert">
- <div class="pure-u-1 center">
- {'Picture wall unavailable (thumbnails are disabled).'|t}
- </div>
-</div>
-{else}
- {if="count($linksToDisplay)===0 && $is_logged_in"}
- <div class="pure-g pure-alert pure-alert-warning page-single-alert">
- <div class="pure-u-1 center">
- {'There is no cached thumbnail. Try to <a href="?do=thumbs_update">synchronize them</a>.'|t}
- </div>
+
+{if="count($linksToDisplay)===0 && $is_logged_in"}
+ <div class="pure-g pure-alert pure-alert-warning page-single-alert">
+ <div class="pure-u-1 center">
+ {'There is no cached thumbnail.'|t}
+ <a href="{$base_path}/admin/thumbnails">{'Try to synchronize them.'|t}</a>
</div>
- {/if}
+ </div>
+{/if}
- <div class="pure-g">
- <div class="pure-u-lg-1-6 pure-u-1-24"></div>
- <div class="pure-u-lg-2-3 pure-u-22-24 page-form page-visitor">
- {$countPics=count($linksToDisplay)}
- <h2 class="window-title">{'Picture Wall'|t} - {$countPics} {'pics'|t}</h2>
+<div class="pure-g">
+ <div class="pure-u-lg-1-6 pure-u-1-24"></div>
+ <div class="pure-u-lg-2-3 pure-u-22-24 page-form page-visitor">
+ {$countPics=count($linksToDisplay)}
+ <h2 class="window-title">{'Picture Wall'|t} - {$countPics} {'pics'|t}</h2>
- <div id="plugin_zone_start_picwall" class="plugin_zone">
- {loop="$plugin_start_zone"}
- {$value}
- {/loop}
- </div>
+ <div id="plugin_zone_start_picwall" class="plugin_zone">
+ {loop="$plugin_start_zone"}
+ {$value}
+ {/loop}
+ </div>
- <div id="picwall-container" class="picwall-container" role="list">
- {loop="$linksToDisplay"}
- <div class="picwall-pictureframe" role="listitem">
- {ignore}RainTPL hack: put the 2 src on two different line to avoid path replace bug{/ignore}
- <img data-src="{$value.thumbnail}#" class="b-lazy"
- src=""
- alt="" width="{$thumbnails_width}" height="{$thumbnails_height}" />
- <a href="{$value.real_url}"><span class="info">{$value.title}</span></a>
- {loop="$value.picwall_plugin"}
- {$value}
- {/loop}
- </div>
- {/loop}
- <div class="clear"></div>
- </div>
+ <div id="picwall-container" class="picwall-container" role="list">
+ {loop="$linksToDisplay"}
+ <div class="picwall-pictureframe" role="listitem">
+ {ignore}RainTPL hack: put the 2 src on two different line to avoid path replace bug{/ignore}
+ <img data-src="{$value.thumbnail}#" class="b-lazy"
+ src=""
+ alt="" width="{$thumbnails_width}" height="{$thumbnails_height}" />
+ <a href="{$value.real_url}"><span class="info">{$value.title}</span></a>
+ {loop="$value.picwall_plugin"}
+ {$value}
+ {/loop}
+ </div>
+ {/loop}
+ <div class="clear"></div>
+ </div>
- <div id="plugin_zone_end_picwall" class="plugin_zone">
- {loop="$plugin_end_zone"}
- {$value}
- {/loop}
- </div>
+ <div id="plugin_zone_end_picwall" class="plugin_zone">
+ {loop="$plugin_end_zone"}
+ {$value}
+ {/loop}
</div>
- <div class="pure-u-lg-1-6 pure-u-1-24"></div>
</div>
-{/if}
+ <div class="pure-u-lg-1-6 pure-u-1-24"></div>
+</div>
{include="page.footer"}
-<script src="js/thumbnails.min.js?v={$version_hash}"></script>
+<script src="{$asset_path}/js/thumbnails.min.js?v={$version_hash}#"></script>
</body>
</html>
<div class="clear"></div>
</noscript>
-<form method="POST" action="?do=save_pluginadmin" name="pluginform" id="pluginform" class="pluginform-container">
+<form method="POST" action="{$base_path}/admin/plugins" name="pluginform" id="pluginform" class="pluginform-container">
<div class="pure-g">
<div class="pure-u-lg-1-8 pure-u-1-24"></div>
<div class="pure-u-lg-3-4 pure-u-22-24 page-form page-form-complete">
<input type="hidden" name="token" value="{$token}">
</form>
-<form action="?do=save_pluginadmin" method="POST">
+<form action="{$base_path}/admin/plugins" method="POST">
<div class="pure-g">
<div class="pure-u-lg-1-8 pure-u-1-24"></div>
<div class="pure-u-lg-3-4 pure-u-22-24 page-form page-form-light">
</section>
</div>
</div>
+ <input type="hidden" name="token" value="{$token}">
</form>
{include="page.footer"}
-<script src="js/pluginsadmin.min.js?v={$version_hash}"></script>
+<script src="{$asset_path}/js/pluginsadmin.min.js?v={$version_hash}#"></script>
</body>
</html>
<h2 class="window-title">{'Tag cloud'|t} - {$countTags} {'tags'|t}</h2>
{if="!empty($search_tags)"}
<p class="center">
- <a href="?searchtags={$search_tags|urlencode}" class="pure-button pure-button-shaarli">
+ <a href="{$base_path}/?searchtags={$search_tags|urlencode}" class="pure-button pure-button-shaarli">
{'List all links with those tags'|t}
</a>
</p>
<div id="cloudtag" class="cloudtag-container">
{loop="tags"}
- <a href="?searchtags={$key|urlencode} {$search_tags|urlencode}" style="font-size:{$value.size}em;">{$key}</a
- ><a href="?addtag={$key|urlencode}" title="{'Filter by tag'|t}" class="count">{$value.count}</a>
+ <a href="{$base_path}/?searchtags={$key|urlencode} {$search_tags|urlencode}" style="font-size:{$value.size}em;">{$key}</a
+ ><a href="{$base_path}/add-tag/{$key|urlencode}" title="{'Filter by tag'|t}" class="count">{$value.count}</a>
{loop="$value.tag_plugin"}
{$value}
{/loop}
<h2 class="window-title">{'Tag list'|t} - {$countTags} {'tags'|t}</h2>
{if="!empty($search_tags)"}
<p class="center">
- <a href="?searchtags={$search_tags|urlencode}" class="pure-button pure-button-shaarli">
+ <a href="{$base_path}/?searchtags={$search_tags|urlencode}" class="pure-button pure-button-shaarli">
{'List all links with those tags'|t}
</a>
</p>
<div class="pure-u-1">
{if="$is_logged_in===true"}
<a href="#" class="delete-tag" aria-label="{'Delete'|t}"><i class="fa fa-trash" aria-hidden="true"></i></a>
- <a href="?do=changetag&fromtag={$key|urlencode}" class="rename-tag" aria-label="{'Rename tag'|t}">
+ <a href="{$base_path}/admin/tags?fromtag={$key|urlencode}" class="rename-tag" aria-label="{'Rename tag'|t}">
<i class="fa fa-pencil-square-o {$key}" aria-hidden="true"></i>
</a>
{/if}
- <a href="?addtag={$key|urlencode}" title="{'Filter by tag'|t}" class="count">{$value}</a>
- <a href="?searchtags={$key|urlencode} {$search_tags|urlencode}" class="tag-link">{$key}</a>
+ <a href="{$base_path}/add-tag/{$key|urlencode}" title="{'Filter by tag'|t}" class="count">{$value}</a>
+ <a href="{$base_path}/?searchtags={$key|urlencode} {$search_tags|urlencode}" class="tag-link">{$key}</a>
{loop="$value.tag_plugin"}
{$value}
<div class="pure-g">
<div class="pure-u-1 pure-alert pure-alert-success tag-sort">
{'Sort by:'|t}
- <a href="?do=tagcloud">{'Cloud'|t}</a> ·
- <a href="?do=taglist&sort=usage">{'Most used'|t}</a> ·
- <a href="?do=taglist&sort=alpha">{'Alphabetical'|t}</a>
+ <a href="{$base_path}/tags/cloud">{'Cloud'|t}</a> ·
+ <a href="{$base_path}/tags/list?sort=usage">{'Most used'|t}</a> ·
+ <a href="{$base_path}/tags/list?sort=alpha">{'Alphabetical'|t}</a>
</div>
-</div>
\ No newline at end of file
+</div>
</div>
{include="page.footer"}
-<script src="js/thumbnails_update.min.js?v={$version_hash}"></script>
+<script src="{$asset_path}/js/thumbnails_update.min.js?v={$version_hash}#"></script>
</body>
</html>
<div class="pure-u-lg-1-3 pure-u-22-24 page-form page-form-light">
<h2 class="window-title">{'Settings'|t}</h2>
<div class="tools-item">
- <a href="?do=configure" title="{'Change Shaarli settings: title, timezone, etc.'|t}">
+ <a href="{$base_path}/admin/configure" title="{'Change Shaarli settings: title, timezone, etc.'|t}">
<span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Configure your Shaarli'|t}</span>
</a>
</div>
<div class="tools-item">
- <a href="?do=pluginadmin" title="{'Enable, disable and configure plugins'|t}">
+ <a href="{$base_path}/admin/plugins" title="{'Enable, disable and configure plugins'|t}">
<span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Plugin administration'|t}</span>
</a>
</div>
{if="!$openshaarli"}
<div class="tools-item">
- <a href="?do=changepasswd" title="{'Change your password'|t}">
+ <a href="{$base_path}/admin/password" title="{'Change your password'|t}">
<span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Change password'|t}</span>
</a>
</div>
{/if}
<div class="tools-item">
- <a href="?do=changetag" title="{'Rename or delete a tag in all links'|t}">
+ <a href="{$base_path}/admin/tags" title="{'Rename or delete a tag in all links'|t}">
<span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Manage tags'|t}</span>
</a>
</div>
<div class="tools-item">
- <a href="?do=import"
+ <a href="{$base_path}/admin/import"
title="{'Import Netscape HTML bookmarks (as exported from Firefox, Chrome, Opera, delicious...)'|t}">
<span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Import links'|t}</span>
</a>
</div>
<div class="tools-item">
- <a href="?do=export"
+ <a href="{$base_path}/admin/export"
title="{'Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, Opera, delicious...)'|t}">
<span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Export database'|t}</span>
</a>
{if="$thumbnails_enabled"}
<div class="tools-item">
- <a href="?do=thumbs_update" title="{'Synchronize all link thumbnails'|t}">
+ <a href="{$base_path}/admin/thumbnails" title="{'Synchronize all link thumbnails'|t}">
<span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Synchronize thumbnails'|t}</span>
</a>
</div>
alert('{function="str_replace(' ', '%20', t('The selected text is too long, it will be truncated.'))"}');
}
window.open(
- '{$pageabsaddr}?post='%20+%20encodeURIComponent(url)+
+ '{$pageabsaddr}admin/shaare?post='%20+%20encodeURIComponent(url)+
'&title='%20+%20encodeURIComponent(title)+
'&description='%20+%20encodeURIComponent(desc)+
'&source=bookmarklet','_blank','menubar=no,height=800,width=600,toolbar=no,scrollbars=yes,status=no,dialog=1'
<div class="error-container">
<h1>404 Not found <small>Oh crap!</small></h1>
<p>{$error_message}</p>
- <p>Would you mind <a href="?">clicking here</a>?</p>
+ <p>Would you mind <a href="{$base_path}/">clicking here</a>?</p>
</div>
{include="page.footer"}
</body>
<div id="pageheader">
{include="page.header"}
<div id="headerform">
- <form method="GET" action="" name="addform" class="addform">
+ <form method="GET" action="{$base_path}/admin/shaare" name="addform" class="addform">
<input type="text" name="post" class="linkurl">
<input type="submit" value="Add link" class="bigbutton">
</form>
<body onload="document.changepasswordform.oldpassword.focus();">
<div id="pageheader">
{include="page.header"}
- <form method="POST" action="#" name="changepasswordform" id="changepasswordform">
+ <form method="POST" action="{$base_path}/admin/password" name="changepasswordform" id="changepasswordform">
Old password: <input type="password" name="oldpassword">
New password: <input type="password" name="setpassword">
<input type="hidden" name="token" value="{$token}">
</div>
{include="page.footer"}
</body>
-</html>
\ No newline at end of file
+</html>
<body onload="document.changetag.fromtag.focus();">
<div id="pageheader">
{include="page.header"}
- <form method="POST" action="" name="changetag" id="changetag">
+ <form method="POST" action="{$base_path}/admin/tags" name="changetag" id="changetag">
<input type="hidden" name="token" value="{$token}">
<div>
<label for="fromtag">Tag:</label>
<body onload="document.configform.title.focus();">
<div id="pageheader">
{include="page.header"}
- <form method="POST" action="#" name="configform" id="configform">
+ <form method="POST" action="{$base_path}/admin/configure" name="configform" id="configform">
<input type="hidden" name="token" value="{$token}">
<table id="configuration_table">
<tr>
<td><b>Home link:</b></td>
<td><input type="text" name="titleLink" id="titleLink" size="50" value="{$titleLink}"><br/><label
- for="titleLink">(default value is: ?)</label></td>
+ for="titleLink">(default value is: {$base_path}/)</label></td>
</tr>
<tr>
{if="! $gd_enabled"}
{'You need to enable the extension <code>php-gd</code> to use thumbnails.'|t}
{elseif="$thumbnails_enabled"}
- <a href="?do=thumbs_update">{'Synchonize thumbnails'|t}</a>
+ <a href="{$base_path}/admin/thumbnails">{'Synchonize thumbnails'|t}</a>
{/if}
</label>
</td>
<div class="dailyAbout">
All links of one day<br>in a single page.<br>
- {if="$previousday"} <a href="?do=daily&day={$previousday}"><b><</b>Previous day</a>{else}<b><</b>Previous day{/if}
+ {if="$previousday"} <a href="{$base_path}/daily&day={$previousday}"><b><</b>Previous day</a>{else}<b><</b>Previous day{/if}
-
- {if="$nextday"}<a href="?do=daily&day={$nextday}">Next day<b>></b></a>{else}Next day<b>></b>{/if}
+ {if="$nextday"}<a href="{$base_path}/daily&day={$nextday}">Next day<b>></b></a>{else}Next day<b>></b>{/if}
<br>
{loop="$daily_about_plugin"}
{/loop}
<br>
- <a href="?do=dailyrss" title="1 RSS entry per day"><img src="img/feed-icon-14x14.png" alt="rss_feed">Daily RSS Feed</a>
+ <a href="{$base_path}/daily-rss" title="1 RSS entry per day"><img src="{$asset_path}/img/feed-icon-14x14.png#" alt="rss_feed">Daily RSS Feed</a>
</div>
<div class="dailyTitle">
- <img src="img/floral_left.png" width="51" height="50" class="nomobile" alt="floral_left">
+ <img src="{$asset_path}/img/floral_left.png#" width="51" height="50" class="nomobile" alt="floral_left">
The Daily Shaarli
- <img src="img/floral_right.png" width="51" height="50" class="nomobile" alt="floral_right">
+ <img src="{$asset_path}/img/floral_right.png#" width="51" height="50" class="nomobile" alt="floral_right">
</div>
<div class="dailyDate">
{$link=$value}
<div class="dailyEntry">
<div class="dailyEntryPermalink">
- <a href="?{$value.shorturl}">
- <img src="img/squiggle.png" width="25" height="26" title="permalink" alt="permalink">
+ <a href="{$base_path}/?{$value.shorturl}">
+ <img src="{$asset_path}/img/squiggle.png#" width="25" height="26" title="permalink" alt="permalink">
</a>
</div>
{if="!$hide_timestamps || $is_logged_in"}
<div class="dailyEntryLinkdate">
- <a href="?{$value.shorturl}">{function="strftime('%c', $link.timestamp)"}</a>
+ <a href="{$base_path}/?{$value.shorturl}">{function="strftime('%c', $link.timestamp)"}</a>
</div>
{/if}
{if="$link.tags"}
{$value}
{/loop}
</div>
- <div id="closing"><img src="img/squiggle_closing.png" width="66" height="61" alt="-"></div>
+ <div id="closing"><img src="{$asset_path}/img/squiggle_closing.png#" width="66" height="61" alt="-"></div>
</div>
{include="page.footer"}
-<script src="js/thumbnails.min.js?v={$version_hash}"></script>
+<script src="{$asset_path}/js/thumbnails.min.js?v={$version_hash}#"></script>
</body>
</html>
-<item>
- <title>{$title} - {function="strftime('%A %e %B %Y', $daydate)"}</title>
- <guid>{$absurl}</guid>
- <link>{$absurl}</link>
- <pubDate>{$rssdate}</pubDate>
- <description><![CDATA[
- {loop="links"}
- <h3><a href="{$value.url}">{$value.title}</a></h3>
- <small>{if="!$hide_timestamps"}{function="strftime('%c', $value.timestamp)"} - {/if}{if="$value.tags"}{$value.tags}{/if}<br>
- {$value.url}</small><br>
- {if="$value.thumbnail"}<img src="{$index_url}{$value.thumbnail}#" alt="thumbnail" />{/if}<br>
- {if="$value.description"}{$value.formatedDescription}{/if}
- <br><br><hr>
+<?xml version="1.0" encoding="UTF-8"?>
+<rss version="2.0">
+ <channel>
+ <title>Daily - {$title}</title>
+ <link>{$index_url}</link>
+ <description>Daily shaared bookmarks</description>
+ <language>{$language}</language>
+ <copyright>{$index_url}</copyright>
+ <generator>Shaarli</generator>
+
+ {loop="$days"}
+ <item>
+ <title>{$value.date_human} - {$title}</title>
+ <guid>{$value.absolute_url}</guid>
+ <link>{$value.absolute_url}</link>
+ <pubDate>{$value.date_rss}</pubDate>
+ <description><![CDATA[
+ {loop="$value.links"}
+ <h3><a href="{$value.url}">{$value.title}</a></h3>
+ <small>
+ {if="!$hide_timestamps"}{$value.created|format_date} - {/if}{if="$value.tags"}{$value.tags}{/if}<br>
+ {$value.url}
+ </small><br>
+ {if="$value.thumbnail"}<img src="{$index_url}{$value.thumbnail}#" alt="thumbnail" />{/if}<br>
+ {if="$value.description"}{$value.description}{/if}
+ <br><br><hr>
{/loop}
- ]]></description>
-</item>
+ ]]></description>
+ </item>
+ {/loop}
+ </channel>
+</rss><!-- Cached version of {$page_url} -->
<!DOCTYPE html>
<html>
<head>{include="includes"}
- <link type="text/css" rel="stylesheet" href="inc/awesomplete.css#" />
</head>
<body
{if="$link.title==''"}onload="document.linkform.lf_title.focus();"
{elseif="$link.description==''"}onload="document.linkform.lf_description.focus();"
{else}onload="document.linkform.lf_tags.focus();"{/if} >
<div id="pageheader">
- {if="$source !== 'firefoxsocialapi'"}
{include="page.header"}
- {else}
<div id="shaarli_title"><a href="{$titleLink}">{$shaarlititle}</a></div>
- {/if}
<div id="editlinkform">
- <form method="post" name="linkform">
+ <form method="post" name="linkform" action="{$base_path}/admin/shaare">
<input type="hidden" name="lf_linkdate" value="{$link.linkdate}">
{if="isset($link.id)"}
<input type="hidden" name="lf_id" value="{$link.id}">
{/if}
<input type="submit" value="Save" name="save_edit" class="bigbutton">
{if="!$link_is_new && isset($link.id)"}
- <a href="?delete_link&lf_linkdate={$link.id}&token={$token}"
+ <a href="{$base_path}/admin/shaare/delete?id={$link.id}&token={$token}"
name="delete_link" class="bigbutton"
onClick="return confirmDeleteLink();">
{'Delete'|t}
</a>
{/if}
<input type="hidden" name="token" value="{$token}">
+ <input type="hidden" name="source" value="{$source}">
{if="$http_referer"}<input type="hidden" name="returnurl" value="{$http_referer}">{/if}
</form>
</div>
</div>
-{if="$source !== 'firefoxsocialapi'"}
{include="page.footer"}
-{/if}
</body>
</html>
</pre>
{/if}
- <p>Would you mind <a href="?">clicking here</a>?</p>
+ <p>Would you mind <a href="{$base_path}/">clicking here</a>?</p>
</div>
{include="page.footer"}
</body>
<div id="pageheader">
{include="page.header"}
<div id="toolsdiv">
- <form method="GET">
- <input type="hidden" name="do" value="export">
+ <form method="POST" action="{$base_path}/admin/export">
Selection:<br>
<input type="radio" name="selection" value="all" checked="true"> All<br>
<input type="radio" name="selection" value="private"> Private<br>
<input type="radio" name="selection" value="public"> Public<br>
+ <input type="hidden" name="token" value="{$token}">
+
<br>
<input type="checkbox" name="prepend_note_url" id="prepend_note_url">
<label for="prepend_note_url">
<updated>{$last_update}</updated>
{/if}
<link rel="self" href="{$self_link}#" />
- <link rel="search" type="application/opensearchdescription+xml" href="{$index_url}?do=opensearch#"
- title="Shaarli search - {$shaarlititle}" />
+ <link rel="search" type="application/opensearchdescription+xml" href="{$index_url}open-search#"
+ title="Shaarli search - {$shaarlititle}" />
{loop="$feed_plugins_header"}
{$value}
{/loop}
<copyright>{$index_url}</copyright>
<generator>Shaarli</generator>
<atom:link rel="self" href="{$self_link}" />
- <atom:link rel="search" type="application/opensearchdescription+xml" href="{$index_url}?do=opensearch#"
+ <atom:link rel="search" type="application/opensearchdescription+xml" href="{$index_url}open-search#"
title="Shaarli search - {$shaarlititle}" />
{loop="$feed_plugins_header"}
{$value}
{include="page.header"}
<div id="uploaddiv">
Import Netscape HTML bookmarks (as exported from Firefox/Chrome/Opera/Delicious/Diigo...) (Max: {$maxfilesize}).
- <form method="POST" action="?do=import" enctype="multipart/form-data"
+ <form method="POST" action="{$base_path}/admin/import" enctype="multipart/form-data"
name="uploadform" id="uploadform">
<input type="hidden" name="token" value="{$token}">
<input type="hidden" name="MAX_FILE_SIZE" value="{$maxfilesize}">
<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" />
+<link type="text/css" rel="stylesheet" href="{$asset_path}/css/shaarli.min.css#" />
{if="$formatter==='markdown'"}
- <link type="text/css" rel="stylesheet" href="css/markdown.min.css?v={$version_hash}" />
+ <link type="text/css" rel="stylesheet" href="{$asset_path}/css/markdown.min.css?v={$version_hash}#" />
{/if}
{loop="$plugins_includes.css_files"}
-<link type="text/css" rel="stylesheet" href="{$value}#"/>
+<link type="text/css" rel="stylesheet" href="{$base_path}/{$value}#"/>
{/loop}
-{if="is_file('data/user.css')"}<link type="text/css" rel="stylesheet" href="data/user.css#" />{/if}
-<link rel="search" type="application/opensearchdescription+xml" href="?do=opensearch#" title="Shaarli search - {$shaarlititle|htmlspecialchars}"/>
+{if="is_file('data/user.css')"}<link type="text/css" rel="stylesheet" href="{$base_path}/data/user.css#" />{/if}
+<link rel="search" type="application/opensearchdescription+xml" href="{$base_path}/open-search#"
+ title="Shaarli search - {$shaarlititle|htmlspecialchars}" />
{if="! empty($links) && count($links) === 1"}
{$link=reset($links)}
<meta property="og:title" content="{$link.title}" />
<div id="install">
<h1>Shaarli</h1>
It looks like it's the first time you run Shaarli. Please configure it:<br>
- <form method="POST" action="#" name="installform" id="installform">
+ <form method="POST" action="{$base_path}/install" name="installform" id="installform">
<table>
<tr><td><b>Login:</b></td><td><input type="text" name="setlogin" size="30"></td></tr>
<tr><td><b>Password:</b></td><td><input type="password" name="setpassword" size="30"></td></tr>
<!DOCTYPE html>
<html>
<head>
- <link type="text/css" rel="stylesheet" href="inc/awesomplete.css#" />
{include="includes"}
</head>
<body>
tagged
{loop="$exploded_tags"}
<span class="linktag" title="Remove tag">
- <a href="?removetag={function="urlencode($value)"}">{$value} <span class="remove">x</span></a>
+ <a href="{$base_path}/remove-tag/{function="urlencode($value)"}">{$value} <span class="remove">x</span></a>
</span>
{/loop}
{elseif="$search_tags === false"}
<span class="linktag" title="Remove tag">
- <a href="?">untagged <span class="remove">x</span></a>
+ <a href="{$base_path}/">untagged <span class="remove">x</span></a>
</span>
{/if}
</div>
<div class="thumbnail">
<a href="{$value.real_url}">
{ignore}RainTPL hack: put the 2 src on two different line to avoid path replace bug{/ignore}
- <img data-src="{$value.thumbnail}#" class="b-lazy"
+ <img data-src="{$base_path}/{$value.thumbnail}#" class="b-lazy"
src=""
alt="thumbnail" width="{$thumbnails_width}" height="{$thumbnails_height}" />
</a>
<div class="linkcontainer">
{if="$is_logged_in"}
<div class="linkeditbuttons">
- <form method="GET" class="buttoneditform">
- <input type="hidden" name="edit_link" value="{$value.id}">
- <input type="image" alt="Edit" src="img/edit_icon.png" title="Edit" class="button_edit">
- </form><br>
- <form method="GET" class="buttoneditform">
- <input type="hidden" name="lf_linkdate" value="{$value.id}">
- <input type="hidden" name="token" value="{$token}">
- <input type="hidden" name="delete_link">
- <input type="image" alt="Delete" src="img/delete_icon.png" title="Delete"
- class="button_delete" onClick="return confirmDeleteLink();">
- </form>
+ <a href="{$base_path}/admin/shaare/{$value.id}" title="Edit" class="button_edit">
+ <img src="{$asset_path}/img/edit_icon.png#">
+ </a>
+ <br>
+ <a href="{$base_path}/admin/shaare/delete?id={$value.id}&token={$token}" label="Delete"
+ onClick="return confirmDeleteLink();"
+ class="button_delete"
+ >
+ <img src="{$asset_path}/img/delete_icon.png#">
+ </a>
</div>
{/if}
<span class="linktitle">
{if="!$hide_timestamps || $is_logged_in"}
{$updated=$value.updated_timestamp ? 'Edited: '. format_date($value.updated) : 'Permalink'}
<span class="linkdate" title="Permalink">
- <a href="?{$value.shorturl}">
+ <a href="{$base_path}/shaare/{$value.shorturl}">
<span title="{$updated}">
{$value.created|format_date}
{if="$value.updated_timestamp"}*{/if}
</a> -
</span>
{else}
- <span class="linkdate" title="Short link here"><a href="?{$value.shorturl}">permalink</a> - </span>
+ <span class="linkdate" title="Short link here"><a href="{$base_path}/shaare/{$value.shorturl}">permalink</a> - </span>
{/if}
{loop="$value.link_plugin"}
<a href="{$value.real_url}"><span class="linkurl" title="Short link">{$value.url}</span></a><br>
{if="$value.tags"}
<div class="linktaglist">
- {loop="$value.taglist"}<span class="linktag" title="Add tag"><a href="?addtag={$value|urlencode}">{$value}</a></span> {/loop}
+ {loop="$value.taglist"}<span class="linktag" title="Add tag"><a href="{$base_path}/add-tag/{$value|urlencode}">{$value}</a></span> {/loop}
</div>
{/if}
</div>
{include="page.footer"}
-<script src="js/thumbnails.min.js"></script>
+<script src="{$asset_path}/js/thumbnails.min.js#"></script>
</body>
</html>
<div class="paging">
{if="$is_logged_in"}
<div class="paging_privatelinks">
- <a href="?visibility=private">
+ <a href="{$base_path}/admin/isibility/private">
{if="$visibility=='private'"}
- <img src="img/private_16x16_active.png" width="16" height="16" title="Click to see all links" alt="Click to see all links">
+ <img src="{$asset_path}/img/private_16x16_active.png#" width="16" height="16" title="Click to see all links" alt="Click to see all links">
{else}
- <img src="img/private_16x16.png" width="16" height="16" title="Click to see only private links" alt="Click to see only private links">
+ <img src="{$asset_path}/img/private_16x16.png#" width="16" height="16" title="Click to see only private links" alt="Click to see only private links">
{/if}
</a>
</div>
{/loop}
<div class="paging_linksperpage">
- Links per page: <a href="?linksperpage=20">20</a> <a href="?linksperpage=50">50</a> <a href="?linksperpage=100">100</a>
- <form method="GET" class="linksperpage"><input type="text" name="linksperpage" size="2"></form>
+ Links per page:
+ <a href="{$base_path}/links-per-page?nb=20">20</a>
+ <a href="{$base_path}/links-per-page?nb=50">50</a>
+ <a href="{$base_path}/links-per-page?nb=100">100</a>
+ <form method="GET" class="linksperpage" action="{$base_path}/links-per-page">
+ <input type="text" name="nb" size="2">
+ </form>
</div>
{if="$previous_page_url"} <a href="{$previous_page_url}" class="paging_older">◄Older</a> {/if}
<div class="paging_current">page {$page_current} / {$page_max} </div>
{include="page.header"}
<div id="headerform">
- <form method="post" name="loginform">
+ <form method="post" name="loginform" action="{$base_path}/login">
<label for="login">Login: <input type="text" id="login" name="login" tabindex="1"
{if="!empty($username)"}value="{$username}"{/if}>
</label>
<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&searchterm={searchTerms}"/>
- <Url type="application/rss+xml" template="{$serverurl}?do=rss&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
</div>
{/if}
-<script src="js/shaarli.min.js"></script>
+<script src="{$asset_path}/js/shaarli.min.js#"></script>
{if="$is_logged_in"}
<script>function confirmDeleteLink() { var agree=confirm("Are you sure you want to delete this link ?"); if (agree) return true ; else return false ; }</script>
{/if}
{loop="$plugins_footer.js_files"}
- <script src="{$value}#"></script>
+ <script src="{$base_path}/{$value}#"></script>
{/loop}
+
+<input type="hidden" name="js_base_path" value="{$base_path}" />
{else}
<li><a href="{$titleLink}" class="nomobile">Home</a></li>
{if="$is_logged_in"}
- <li><a href="?do=logout">Logout</a></li>
- <li><a href="?do=tools">Tools</a></li>
- <li><a href="?do=addlink">Add link</a></li>
+ <li><a href="{$base_path}/admin/logout">Logout</a></li>
+ <li><a href="{$base_path}/admin/tools">Tools</a></li>
+ <li><a href="{$base_path}/admin/add-shaare">Add link</a></li>
{elseif="$openshaarli"}
- <li><a href="?do=tools">Tools</a></li>
- <li><a href="?do=addlink">Add link</a></li>
+ <li><a href="{$base_path}/admin/tools">Tools</a></li>
+ <li><a href="{$base_path}/admin/add-shaare">Add link</a></li>
{else}
- <li><a href="/login">Login</a></li>
+ <li><a href="{$base_path}/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="?do=tagcloud">Tag cloud</a></li>
- <li><a href="?do=picwall{$searchcrits}">Picture wall</a></li>
- <li><a href="?do=daily">Daily</a></li>
+ <li><a href="{$base_path}/tags/cloud">Tag cloud</a></li>
+ <li><a href="{$base_path}/picture-wall{function="ltrim($searchcrits, '&')"}">Picture wall</a></li>
+ <li><a href="{$base_path}/daily">Daily</a></li>
{loop="$plugins_header.buttons_toolbar"}
<li><a
{loop="$value.attr"}
{include="page.footer"}
-<script src="js/thumbnails.min.js"></script>
+<script src="{$asset_path}/js/thumbnails.min.js#"></script>
</body>
</html>
</noscript>
<div id="pluginsadmin">
- <form action="?do=save_pluginadmin" method="POST">
+ <form action="{$base_path}/admin/plugins" method="POST">
<section id="enabled_plugins">
<h1>Enabled Plugins</h1>
<input type="submit" value="Save"/>
</div>
</section>
+ <input type="hidden" name="token" value="{$token}">
</form>
- <form action="?do=save_pluginadmin" method="POST">
+ <form action="{$base_path}/admin/plugins" method="POST">
<section id="plugin_parameters">
<h1>Enabled Plugin Parameters</h1>
</div>
</div>
</section>
+ <input type="hidden" name="token" value="{$token}">
</form>
</div>
<div id="cloudtag">
{loop="$tags"}
- <a href="?addtag={$key|urlencode}" class="count">{$value.count}</a><a
- href="?searchtags={$key|urlencode}" style="font-size:{$value.size}em;">{$key}</a>
+ <a href="{$base_path}/add-tag/{$key|urlencode}" class="count">{$value.count}</a><a
+ href="{$base_path}/?searchtags={$key|urlencode}" style="font-size:{$value.size}em;">{$key}</a>
{loop="$value.tag_plugin"}
{$value}
{/loop}
<input type="hidden" name="ids" value="{function="implode(',', $ids)"}" />
{include="page.footer"}
-<script src="js/thumbnails_update.min.js?v={$version_hash}"></script>
+<script src="{$asset_path}/js/thumbnails_update.min.js?v={$version_hash}#"></script>
</body>
</html>
<div id="pageheader">
{include="page.header"}
<div id="toolsdiv">
- <a href="?do=configure"><b>Configure your Shaarli</b><span>: Change Title, timezone...</span></a>
+ <a href="{$base_path}/admin/configure"><b>Configure your Shaarli</b><span>: Change Title, timezone...</span></a>
<br><br>
- <a href="?do=pluginadmin"><b>Plugin administration</b><span>: Enable, disable and configure plugins.</span></a>
+ <a href="{$base_path}/admin/plugins"><b>Plugin administration</b><span>: Enable, disable and configure plugins.</span></a>
<br><br>
- {if="!$openshaarli"}<a href="?do=changepasswd"><b>Change password</b><span>: Change your password.</span></a>
+ {if="!$openshaarli"}<a href="{$base_path}/admin/password"><b>Change password</b><span>: Change your password.</span></a>
<br><br>{/if}
- <a href="?do=changetag"><b>Rename/delete tags</b><span>: Rename or delete a tag in all links</span></a>
+ <a href="{$base_path}/admin/tags"><b>Rename/delete tags</b><span>: Rename or delete a tag in all links</span></a>
<br><br>
- <a href="?do=import"><b>Import</b><span>: Import Netscape html bookmarks (as exported from Firefox, Chrome, Opera, delicious...)</span></a>
+ <a href="{$base_path}/admin/import"><b>Import</b><span>: Import Netscape html bookmarks (as exported from Firefox, Chrome, Opera, delicious...)</span></a>
<br><br>
- <a href="?do=export"><b>Export</b><span>: Export Netscape html bookmarks (which can be imported in Firefox, Chrome, Opera, delicious...)</span></a>
+ <a href="{$base_path}/admin/export"><b>Export</b><span>: Export Netscape html bookmarks (which can be imported in Firefox, Chrome, Opera, delicious...)</span></a>
<br><br>
<a class="smallbutton"
onclick="return alertBookmarklet();"
var%20url%20=%20location.href;
var%20title%20=%20document.title%20||%20url;
window.open(
- '{$pageabsaddr}?post='%20+%20encodeURIComponent(url)+
+ '{$pageabsaddr}admin/shaare?post='%20+%20encodeURIComponent(url)+
'&title='%20+%20encodeURIComponent(title)+
'&description='%20+%20encodeURIComponent(document.getSelection())+
'&source=bookmarklet','_blank','menubar=no,height=390,width=600,toolbar=no,scrollbars=no,status=no,dialog=1'