Merge pull request #1511 from ArthurHoaro/wip-slim-routing

This commit is contained in:
ArthurHoaro 2020-08-27 10:27:34 +02:00 committed by GitHub
commit af41d5ab5d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
225 changed files with 12764 additions and 4036 deletions

View file

@ -14,7 +14,7 @@ indent_size = 4
indent_size = 2 indent_size = 2
[*.php] [*.php]
max_line_length = 100 max_line_length = 120
[Dockerfile] [Dockerfile]
max_line_length = 80 max_line_length = 80

View file

@ -4,7 +4,6 @@
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use WebThumbnailer\Application\ConfigManager as WTConfigManager; use WebThumbnailer\Application\ConfigManager as WTConfigManager;
use WebThumbnailer\Exception\WebThumbnailerException;
use WebThumbnailer\WebThumbnailer; use WebThumbnailer\WebThumbnailer;
/** /**
@ -90,7 +89,7 @@ public function get($url)
try { try {
return $this->wt->thumbnail($url); return $this->wt->thumbnail($url);
} catch (WebThumbnailerException $e) { } catch (\Throwable $e) {
// Exceptions are only thrown in debug mode. // Exceptions are only thrown in debug mode.
error_log(get_class($e) . ': ' . $e->getMessage()); error_log(get_class($e) . ': ' . $e->getMessage());
} }

View file

@ -87,10 +87,14 @@ function endsWith($haystack, $needle, $case = true)
* *
* @param mixed $input Data to escape: a single string or an array of strings. * @param mixed $input Data to escape: a single string or an array of strings.
* *
* @return string escaped. * @return string|array escaped.
*/ */
function escape($input) function escape($input)
{ {
if (null === $input) {
return null;
}
if (is_bool($input)) { if (is_bool($input)) {
return $input; return $input;
} }
@ -294,7 +298,7 @@ function normalize_spaces($string)
* Requires php-intl to display international datetimes, * Requires php-intl to display international datetimes,
* otherwise default format '%c' will be returned. * otherwise default format '%c' will be returned.
* *
* @param DateTime $date to format. * @param DateTimeInterface $date to format.
* @param bool $time Displays time if true. * @param bool $time Displays time if true.
* @param bool $intl Use international format if true. * @param bool $intl Use international format if true.
* *
@ -302,7 +306,7 @@ function normalize_spaces($string)
*/ */
function format_date($date, $time = true, $intl = true) function format_date($date, $time = true, $intl = true)
{ {
if (! $date instanceof DateTime) { if (! $date instanceof DateTimeInterface) {
return false; return false;
} }

View file

@ -71,7 +71,14 @@ public function __invoke($request, $response, $next)
$response = $e->getApiResponse(); $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')
;
} }
/** /**

View file

@ -67,7 +67,7 @@ public static function formatLink($bookmark, $indexUrl)
if (! $bookmark->isNote()) { if (! $bookmark->isNote()) {
$out['url'] = $bookmark->getUrl(); $out['url'] = $bookmark->getUrl();
} else { } else {
$out['url'] = $indexUrl . $bookmark->getUrl(); $out['url'] = rtrim($indexUrl, '/') . '/' . ltrim($bookmark->getUrl(), '/');
} }
$out['shorturl'] = $bookmark->getShortUrl(); $out['shorturl'] = $bookmark->getShortUrl();
$out['title'] = $bookmark->getTitle(); $out['title'] = $bookmark->getTitle();

View file

@ -3,6 +3,7 @@
namespace Shaarli\Bookmark; namespace Shaarli\Bookmark;
use DateTime; use DateTime;
use DateTimeInterface;
use Shaarli\Bookmark\Exception\InvalidBookmarkException; use Shaarli\Bookmark\Exception\InvalidBookmarkException;
/** /**
@ -36,16 +37,16 @@ class Bookmark
/** @var array List of bookmark's tags */ /** @var array List of bookmark's tags */
protected $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; protected $thumbnail;
/** @var bool Set to true if the bookmark is set as sticky */ /** @var bool Set to true if the bookmark is set as sticky */
protected $sticky; protected $sticky;
/** @var DateTime Creation datetime */ /** @var DateTimeInterface Creation datetime */
protected $created; protected $created;
/** @var DateTime Update datetime */ /** @var DateTimeInterface datetime */
protected $updated; protected $updated;
/** @var bool True if the bookmark can only be seen while logged in */ /** @var bool True if the bookmark can only be seen while logged in */
@ -100,12 +101,12 @@ public function validate()
|| ! is_int($this->id) || ! is_int($this->id)
|| empty($this->shortUrl) || empty($this->shortUrl)
|| empty($this->created) || empty($this->created)
|| ! $this->created instanceof DateTime || ! $this->created instanceof DateTimeInterface
) { ) {
throw new InvalidBookmarkException($this); throw new InvalidBookmarkException($this);
} }
if (empty($this->url)) { if (empty($this->url)) {
$this->url = '?'. $this->shortUrl; $this->url = '/shaare/'. $this->shortUrl;
} }
if (empty($this->title)) { if (empty($this->title)) {
$this->title = $this->url; $this->title = $this->url;
@ -188,7 +189,7 @@ public function getDescription()
/** /**
* Get the Created. * Get the Created.
* *
* @return DateTime * @return DateTimeInterface
*/ */
public function getCreated() public function getCreated()
{ {
@ -198,7 +199,7 @@ public function getCreated()
/** /**
* Get the Updated. * Get the Updated.
* *
* @return DateTime * @return DateTimeInterface
*/ */
public function getUpdated() public function getUpdated()
{ {
@ -270,7 +271,7 @@ public function setDescription($description)
* Set the Created. * Set the Created.
* Note: you shouldn't set this manually except for special cases (like bookmark import) * Note: you shouldn't set this manually except for special cases (like bookmark import)
* *
* @param DateTime $created * @param DateTimeInterface $created
* *
* @return Bookmark * @return Bookmark
*/ */
@ -284,7 +285,7 @@ public function setCreated($created)
/** /**
* Set the Updated. * Set the Updated.
* *
* @param DateTime $updated * @param DateTimeInterface $updated
* *
* @return Bookmark * @return Bookmark
*/ */
@ -346,7 +347,7 @@ public function setTags($tags)
/** /**
* Get the Thumbnail. * 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() public function getThumbnail()
{ {
@ -356,7 +357,7 @@ public function getThumbnail()
/** /**
* Set the Thumbnail. * Set the Thumbnail.
* *
* @param string|bool $thumbnail * @param string|bool $thumbnail Thumbnail's URL - false if no thumbnail could be found
* *
* @return Bookmark * @return Bookmark
*/ */
@ -405,7 +406,7 @@ public function getTagsString()
public function isNote() public function isNote()
{ {
// We check empty value to get a valid result if the link has not been saved yet // 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] === '?';
} }
/** /**

View file

@ -6,12 +6,14 @@
use Exception; use Exception;
use Shaarli\Bookmark\Exception\BookmarkNotFoundException; use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
use Shaarli\Bookmark\Exception\DatastoreNotInitializedException;
use Shaarli\Bookmark\Exception\EmptyDataStoreException; use Shaarli\Bookmark\Exception\EmptyDataStoreException;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\Formatter\BookmarkMarkdownFormatter; use Shaarli\Formatter\BookmarkMarkdownFormatter;
use Shaarli\History; use Shaarli\History;
use Shaarli\Legacy\LegacyLinkDB; use Shaarli\Legacy\LegacyLinkDB;
use Shaarli\Legacy\LegacyUpdater; use Shaarli\Legacy\LegacyUpdater;
use Shaarli\Render\PageCacheManager;
use Shaarli\Updater\UpdaterUtils; use Shaarli\Updater\UpdaterUtils;
/** /**
@ -39,6 +41,9 @@ class BookmarkFileService implements BookmarkServiceInterface
/** @var History instance */ /** @var History instance */
protected $history; protected $history;
/** @var PageCacheManager instance */
protected $pageCacheManager;
/** @var bool true for logged in users. Default value to retrieve private bookmarks. */ /** @var bool true for logged in users. Default value to retrieve private bookmarks. */
protected $isLoggedIn; protected $isLoggedIn;
@ -49,6 +54,7 @@ public function __construct(ConfigManager $conf, History $history, $isLoggedIn)
{ {
$this->conf = $conf; $this->conf = $conf;
$this->history = $history; $this->history = $history;
$this->pageCacheManager = new PageCacheManager($this->conf->get('resource.page_cache'), $isLoggedIn);
$this->bookmarksIO = new BookmarkIO($this->conf); $this->bookmarksIO = new BookmarkIO($this->conf);
$this->isLoggedIn = $isLoggedIn; $this->isLoggedIn = $isLoggedIn;
@ -57,12 +63,18 @@ public function __construct(ConfigManager $conf, History $history, $isLoggedIn)
} else { } else {
try { try {
$this->bookmarks = $this->bookmarksIO->read(); $this->bookmarks = $this->bookmarksIO->read();
} catch (EmptyDataStoreException $e) { } catch (EmptyDataStoreException|DatastoreNotInitializedException $e) {
$this->bookmarks = new BookmarkArray(); $this->bookmarks = new BookmarkArray();
if ($isLoggedIn) {
if ($this->isLoggedIn) {
// Datastore file does not exists, we initialize it with default bookmarks.
if ($e instanceof DatastoreNotInitializedException) {
$this->initialize();
} else {
$this->save(); $this->save();
} }
} }
}
if (! $this->bookmarks instanceof BookmarkArray) { if (! $this->bookmarks instanceof BookmarkArray) {
$this->migrate(); $this->migrate();
@ -88,7 +100,7 @@ public function findByHash($hash)
throw new Exception('Not authorized'); throw new Exception('Not authorized');
} }
return $bookmark; return $first;
} }
/** /**
@ -149,7 +161,7 @@ public function get($id, $visibility = null)
*/ */
public function set($bookmark, $save = true) 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')); throw new Exception(t('You\'re not authorized to alter the datastore'));
} }
if (! $bookmark instanceof Bookmark) { if (! $bookmark instanceof Bookmark) {
@ -174,7 +186,7 @@ public function set($bookmark, $save = true)
*/ */
public function add($bookmark, $save = true) 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')); throw new Exception(t('You\'re not authorized to alter the datastore'));
} }
if (! $bookmark instanceof Bookmark) { if (! $bookmark instanceof Bookmark) {
@ -199,7 +211,7 @@ public function add($bookmark, $save = true)
*/ */
public function addOrSet($bookmark, $save = true) 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')); throw new Exception(t('You\'re not authorized to alter the datastore'));
} }
if (! $bookmark instanceof Bookmark) { if (! $bookmark instanceof Bookmark) {
@ -216,7 +228,7 @@ public function addOrSet($bookmark, $save = true)
*/ */
public function remove($bookmark, $save = true) 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')); throw new Exception(t('You\'re not authorized to alter the datastore'));
} }
if (! $bookmark instanceof Bookmark) { if (! $bookmark instanceof Bookmark) {
@ -269,13 +281,14 @@ public function count($visibility = null)
*/ */
public function save() public function save()
{ {
if (!$this->isLoggedIn) { if (true !== $this->isLoggedIn) {
// TODO: raise an Exception instead // TODO: raise an Exception instead
die('You are not authorized to change the database.'); die('You are not authorized to change the database.');
} }
$this->bookmarks->reorder(); $this->bookmarks->reorder();
$this->bookmarksIO->write($this->bookmarks); $this->bookmarksIO->write($this->bookmarks);
invalidateCaches($this->conf->get('resource.page_cache')); $this->pageCacheManager->invalidateCaches();
} }
/** /**
@ -291,6 +304,7 @@ public function bookmarksCountPerTag($filteringTags = [], $visibility = null)
if (empty($tag) if (empty($tag)
|| (! $this->isLoggedIn && startsWith($tag, '.')) || (! $this->isLoggedIn && startsWith($tag, '.'))
|| $tag === BookmarkMarkdownFormatter::NO_MD_TAG || $tag === BookmarkMarkdownFormatter::NO_MD_TAG
|| in_array($tag, $filteringTags, true)
) { ) {
continue; continue;
} }
@ -349,6 +363,10 @@ public function initialize()
{ {
$initializer = new BookmarkInitializer($this); $initializer = new BookmarkInitializer($this);
$initializer->initialize(); $initializer->initialize();
if (true === $this->isLoggedIn) {
$this->save();
}
} }
/** /**

View file

@ -436,7 +436,7 @@ public function filterDay($day)
throw new Exception('Invalid date format'); throw new Exception('Invalid date format');
} }
$filtered = array(); $filtered = [];
foreach ($this->bookmarks as $key => $l) { foreach ($this->bookmarks as $key => $l) {
if ($l->getCreated()->format('Ymd') == $day) { if ($l->getCreated()->format('Ymd') == $day) {
$filtered[$key] = $l; $filtered[$key] = $l;

View file

@ -2,6 +2,7 @@
namespace Shaarli\Bookmark; namespace Shaarli\Bookmark;
use Shaarli\Bookmark\Exception\DatastoreNotInitializedException;
use Shaarli\Bookmark\Exception\EmptyDataStoreException; use Shaarli\Bookmark\Exception\EmptyDataStoreException;
use Shaarli\Bookmark\Exception\NotWritableDataStoreException; use Shaarli\Bookmark\Exception\NotWritableDataStoreException;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
@ -53,12 +54,13 @@ public function __construct($conf)
* @return BookmarkArray instance * @return BookmarkArray instance
* *
* @throws NotWritableDataStoreException Data couldn't be loaded * @throws NotWritableDataStoreException Data couldn't be loaded
* @throws EmptyDataStoreException Datastore doesn't exist * @throws EmptyDataStoreException Datastore file exists but does not contain any bookmark
* @throws DatastoreNotInitializedException File does not exists
*/ */
public function read() public function read()
{ {
if (! file_exists($this->datastore)) { if (! file_exists($this->datastore)) {
throw new EmptyDataStoreException(); throw new DatastoreNotInitializedException();
} }
if (!is_writable($this->datastore)) { if (!is_writable($this->datastore)) {
@ -102,7 +104,5 @@ public function write($links)
$this->datastore, $this->datastore,
self::$phpPrefix.base64_encode(gzdeflate(serialize($links))).self::$phpSuffix self::$phpPrefix.base64_encode(gzdeflate(serialize($links))).self::$phpSuffix
); );
invalidateCaches($this->conf->get('resource.page_cache'));
} }
} }

View file

@ -6,8 +6,7 @@
* Class BookmarkInitializer * Class BookmarkInitializer
* *
* This class is used to initialized default bookmarks after a fresh install of Shaarli. * 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, * It should be only called if the datastore file does not exist(users might want to delete the default bookmarks).
* because user might want to delete default bookmarks after the install.
* *
* To prevent data corruption, it does not overwrite existing bookmarks, * To prevent data corruption, it does not overwrite existing bookmarks,
* even though there should not be any. * even though there should not be any.
@ -36,11 +35,11 @@ public function initialize()
{ {
$bookmark = new Bookmark(); $bookmark = new Bookmark();
$bookmark->setTitle(t('My secret stuff... - Pastebin.com')); $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->setDescription(t('Shhhh! I\'m a private link only YOU can see. You can delete me too.'));
$bookmark->setTagsString('secretstuff'); $bookmark->setTagsString('secretstuff');
$bookmark->setPrivate(true); $bookmark->setPrivate(true);
$this->bookmarkService->add($bookmark); $this->bookmarkService->add($bookmark, false);
$bookmark = new Bookmark(); $bookmark = new Bookmark();
$bookmark->setTitle(t('The personal, minimalist, super-fast, database free, bookmarking service')); $bookmark->setTitle(t('The personal, minimalist, super-fast, database free, bookmarking service'));
@ -54,6 +53,6 @@ public function initialize()
You use the community supported version of the original Shaarli project, by Sebastien Sauvage.' You use the community supported version of the original Shaarli project, by Sebastien Sauvage.'
)); ));
$bookmark->setTagsString('opensource software'); $bookmark->setTagsString('opensource software');
$this->bookmarkService->add($bookmark); $this->bookmarkService->add($bookmark, false);
} }
} }

View file

@ -6,7 +6,6 @@
use Shaarli\Bookmark\Exception\BookmarkNotFoundException; use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
use Shaarli\Bookmark\Exception\NotWritableDataStoreException; use Shaarli\Bookmark\Exception\NotWritableDataStoreException;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\Exceptions\IOException;
use Shaarli\History; use Shaarli\History;
/** /**

View file

@ -2,112 +2,6 @@
use Shaarli\Bookmark\Bookmark; 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. * Extract title from an HTML document.
* *
@ -220,7 +114,7 @@ function hashtag_autolink($description, $indexUrl = '')
* \p{Mn} - any non marking space (accents, umlauts, etc) * \p{Mn} - any non marking space (accents, umlauts, etc)
*/ */
$regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui'; $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); return preg_replace($regex, $replacement, $description);
} }

View file

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Shaarli\Bookmark\Exception;
class DatastoreNotInitializedException extends \Exception
{
}

View file

@ -3,6 +3,7 @@
use Shaarli\Config\Exception\MissingFieldConfigException; use Shaarli\Config\Exception\MissingFieldConfigException;
use Shaarli\Config\Exception\UnauthorizedConfigException; use Shaarli\Config\Exception\UnauthorizedConfigException;
use Shaarli\Thumbnailer;
/** /**
* Class ConfigManager * Class ConfigManager
@ -361,7 +362,7 @@ protected function setDefaultValues()
$this->setEmpty('security.open_shaarli', false); $this->setEmpty('security.open_shaarli', false);
$this->setEmpty('security.allowed_protocols', ['ftp', 'ftps', 'magnet']); $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.links_per_page', 20);
$this->setEmpty('general.enabled_plugins', self::$DEFAULT_PLUGINS); $this->setEmpty('general.enabled_plugins', self::$DEFAULT_PLUGINS);
$this->setEmpty('general.default_note_title', 'Note: '); $this->setEmpty('general.default_note_title', 'Note: ');
@ -381,6 +382,7 @@ protected function setDefaultValues()
// default state of the 'remember me' checkbox of the login form // default state of the 'remember me' checkbox of the login form
$this->setEmpty('privacy.remember_user_default', true); $this->setEmpty('privacy.remember_user_default', true);
$this->setEmpty('thumbnails.mode', Thumbnailer::MODE_ALL);
$this->setEmpty('thumbnails.width', '125'); $this->setEmpty('thumbnails.width', '125');
$this->setEmpty('thumbnails.height', '90'); $this->setEmpty('thumbnails.height', '90');

View file

@ -1,6 +1,7 @@
<?php <?php
use Shaarli\Config\Exception\PluginConfigOrderException; use Shaarli\Config\Exception\PluginConfigOrderException;
use Shaarli\Plugin\PluginManager;
/** /**
* Plugin configuration helper functions. * Plugin configuration helper functions.
@ -19,6 +20,20 @@
*/ */
function save_plugin_config($formData) 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. // Make sure there are no duplicates in orders.
if (!validate_plugin_order($formData)) { if (!validate_plugin_order($formData)) {
throw new PluginConfigOrderException(); throw new PluginConfigOrderException();
@ -69,7 +84,7 @@ function validate_plugin_order($formData)
$orders = array(); $orders = array();
foreach ($formData as $key => $value) { foreach ($formData as $key => $value) {
// No duplicate order allowed. // No duplicate order allowed.
if (in_array($value, $orders)) { if (in_array($value, $orders, true)) {
return false; return false;
} }

View file

@ -7,11 +7,21 @@
use Shaarli\Bookmark\BookmarkFileService; use Shaarli\Bookmark\BookmarkFileService;
use Shaarli\Bookmark\BookmarkServiceInterface; use Shaarli\Bookmark\BookmarkServiceInterface;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\Feed\FeedBuilder;
use Shaarli\Formatter\FormatterFactory;
use Shaarli\Front\Controller\Visitor\ErrorController;
use Shaarli\History; use Shaarli\History;
use Shaarli\Http\HttpAccess;
use Shaarli\Netscape\NetscapeBookmarkUtils;
use Shaarli\Plugin\PluginManager; use Shaarli\Plugin\PluginManager;
use Shaarli\Render\PageBuilder; use Shaarli\Render\PageBuilder;
use Shaarli\Render\PageCacheManager;
use Shaarli\Security\CookieManager;
use Shaarli\Security\LoginManager; use Shaarli\Security\LoginManager;
use Shaarli\Security\SessionManager; use Shaarli\Security\SessionManager;
use Shaarli\Thumbnailer;
use Shaarli\Updater\Updater;
use Shaarli\Updater\UpdaterUtils;
/** /**
* Class ContainerBuilder * Class ContainerBuilder
@ -30,22 +40,37 @@ class ContainerBuilder
/** @var SessionManager */ /** @var SessionManager */
protected $session; protected $session;
/** @var CookieManager */
protected $cookieManager;
/** @var LoginManager */ /** @var LoginManager */
protected $login; 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->conf = $conf;
$this->session = $session; $this->session = $session;
$this->login = $login; $this->login = $login;
$this->cookieManager = $cookieManager;
} }
public function build(): ShaarliContainer public function build(): ShaarliContainer
{ {
$container = new ShaarliContainer(); $container = new ShaarliContainer();
$container['conf'] = $this->conf; $container['conf'] = $this->conf;
$container['sessionManager'] = $this->session; $container['sessionManager'] = $this->session;
$container['cookieManager'] = $this->cookieManager;
$container['loginManager'] = $this->login; $container['loginManager'] = $this->login;
$container['basePath'] = $this->basePath;
$container['plugins'] = function (ShaarliContainer $container): PluginManager { $container['plugins'] = function (ShaarliContainer $container): PluginManager {
return new PluginManager($container->conf); return new PluginManager($container->conf);
}; };
@ -73,7 +98,59 @@ public function build(): ShaarliContainer
}; };
$container['pluginManager'] = function (ShaarliContainer $container): PluginManager { $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);
}; };
return $container; return $container;

View file

@ -4,25 +4,45 @@
namespace Shaarli\Container; namespace Shaarli\Container;
use http\Cookie;
use Shaarli\Bookmark\BookmarkServiceInterface; use Shaarli\Bookmark\BookmarkServiceInterface;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\Feed\FeedBuilder;
use Shaarli\Formatter\FormatterFactory;
use Shaarli\History; use Shaarli\History;
use Shaarli\Http\HttpAccess;
use Shaarli\Netscape\NetscapeBookmarkUtils;
use Shaarli\Plugin\PluginManager; use Shaarli\Plugin\PluginManager;
use Shaarli\Render\PageBuilder; use Shaarli\Render\PageBuilder;
use Shaarli\Render\PageCacheManager;
use Shaarli\Security\CookieManager;
use Shaarli\Security\LoginManager; use Shaarli\Security\LoginManager;
use Shaarli\Security\SessionManager; use Shaarli\Security\SessionManager;
use Shaarli\Thumbnailer;
use Shaarli\Updater\Updater;
use Slim\Container; use Slim\Container;
/** /**
* Extension of Slim container to document the injected objects. * Extension of Slim container to document the injected objects.
* *
* @property ConfigManager $conf * @property string $basePath Shaarli's instance base path (e.g. `/shaarli/`)
* @property SessionManager $sessionManager
* @property LoginManager $loginManager
* @property History $history
* @property BookmarkServiceInterface $bookmarkService * @property BookmarkServiceInterface $bookmarkService
* @property CookieManager $cookieManager
* @property ConfigManager $conf
* @property mixed[] $environment $_SERVER automatically injected by Slim
* @property callable $errorHandler Overrides default Slim error display
* @property FeedBuilder $feedBuilder
* @property FormatterFactory $formatterFactory
* @property History $history
* @property HttpAccess $httpAccess
* @property LoginManager $loginManager
* @property NetscapeBookmarkUtils $netscapeBookmarkUtils
* @property PageBuilder $pageBuilder * @property PageBuilder $pageBuilder
* @property PageCacheManager $pageCacheManager
* @property PluginManager $pluginManager * @property PluginManager $pluginManager
* @property SessionManager $sessionManager
* @property Thumbnailer $thumbnailer
* @property Updater $updater
*/ */
class ShaarliContainer extends Container class ShaarliContainer extends Container
{ {

View file

@ -1,38 +0,0 @@
<?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);
}

View file

@ -43,21 +43,9 @@ class FeedBuilder
*/ */
protected $formatter; protected $formatter;
/** /** @var mixed[] $_SERVER */
* @var string RSS or ATOM feed.
*/
protected $feedType;
/**
* @var array $_SERVER
*/
protected $serverInfo; protected $serverInfo;
/**
* @var array $_GET
*/
protected $userInput;
/** /**
* @var boolean True if the user is currently logged in, false otherwise. * @var boolean True if the user is currently logged in, false otherwise.
*/ */
@ -77,7 +65,6 @@ class FeedBuilder
* @var string server locale. * @var string server locale.
*/ */
protected $locale; protected $locale;
/** /**
* @var DateTime Latest item date. * @var DateTime Latest item date.
*/ */
@ -88,37 +75,36 @@ class FeedBuilder
* *
* @param BookmarkServiceInterface $linkDB LinkDB instance. * @param BookmarkServiceInterface $linkDB LinkDB instance.
* @param BookmarkFormatter $formatter instance. * @param BookmarkFormatter $formatter instance.
* @param string $feedType Type of feed.
* @param array $serverInfo $_SERVER. * @param array $serverInfo $_SERVER.
* @param array $userInput $_GET.
* @param boolean $isLoggedIn True if the user is currently logged in, false otherwise. * @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->linkDB = $linkDB;
$this->formatter = $formatter; $this->formatter = $formatter;
$this->feedType = $feedType;
$this->serverInfo = $serverInfo; $this->serverInfo = $serverInfo;
$this->userInput = $userInput;
$this->isLoggedIn = $isLoggedIn; $this->isLoggedIn = $isLoggedIn;
} }
/** /**
* Build data for feed templates. * Build data for feed templates.
* *
* @param string $feedType Type of feed (RSS/ATOM).
* @param array $userInput $_GET.
*
* @return array Formatted data for feeds templates. * @return array Formatted data for feeds templates.
*/ */
public function buildData() public function buildData(string $feedType, ?array $userInput)
{ {
// Search for untagged bookmarks // Search for untagged bookmarks
if (isset($this->userInput['searchtags']) && empty($this->userInput['searchtags'])) { if (isset($this->userInput['searchtags']) && empty($userInput['searchtags'])) {
$this->userInput['searchtags'] = false; $userInput['searchtags'] = false;
} }
// Optionally filter the results: // 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. // Can't use array_keys() because $link is a LinkDB instance and not a real array.
$keys = array(); $keys = array();
@ -130,11 +116,11 @@ public function buildData()
$this->formatter->addContextData('index_url', $pageaddr); $this->formatter->addContextData('index_url', $pageaddr);
$linkDisplayed = array(); $linkDisplayed = array();
for ($i = 0; $i < $nblinksToDisplay && $i < count($keys); $i++) { 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['language'] = $this->getTypeLanguage($feedType);
$data['last_update'] = $this->getLatestDateFormatted(); $data['last_update'] = $this->getLatestDateFormatted($feedType);
$data['show_dates'] = !$this->hideDates || $this->isLoggedIn; $data['show_dates'] = !$this->hideDates || $this->isLoggedIn;
// Remove leading slash from REQUEST_URI. // Remove leading slash from REQUEST_URI.
$data['self_link'] = escape(server_url($this->serverInfo)) $data['self_link'] = escape(server_url($this->serverInfo))
@ -146,45 +132,6 @@ public function buildData()
return $data; return $data;
} }
/**
* Build a feed item (one per shaare).
*
* @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)
{
$data = $this->formatter->format($link);
$data['guid'] = $pageaddr . '?' . $data['shorturl'];
if ($this->usePermalinks === true) {
$permalink = '<a href="'. $data['url'] .'" title="'. t('Direct link') .'">'. t('Direct link') .'</a>';
} else {
$permalink = '<a href="'. $data['guid'] .'" title="'. t('Permalink') .'">'. t('Permalink') .'</a>';
}
$data['description'] .= PHP_EOL . PHP_EOL . '<br>&#8212; ' . $permalink;
$data['pub_iso_date'] = $this->getIsoDate($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);
} else {
$data['up_iso_date'] = $this->getIsoDate($data['created'], DateTime::ATOM);
}
// Save the more recent item.
if (empty($this->latestDate) || $this->latestDate < $data['created']) {
$this->latestDate = $data['created'];
}
if (!empty($data['updated']) && $this->latestDate < $data['updated']) {
$this->latestDate = $data['updated'];
}
return $data;
}
/** /**
* Set this to true to use permalinks instead of direct bookmarks. * Set this to true to use permalinks instead of direct bookmarks.
* *
@ -215,22 +162,64 @@ public function setLocale($locale)
$this->locale = strtolower($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(string $feedType, $link, $pageaddr)
{
$data = $this->formatter->format($link);
$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 {
$permalink = '<a href="'. $data['guid'] .'" title="'. t('Permalink') .'">'. t('Permalink') .'</a>';
}
$data['description'] .= PHP_EOL . PHP_EOL . '<br>&#8212; ' . $permalink;
$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($feedType, $data['updated'], DateTime::ATOM);
} else {
$data['up_iso_date'] = $this->getIsoDate($feedType, $data['created'], DateTime::ATOM);
}
// Save the more recent item.
if (empty($this->latestDate) || $this->latestDate < $data['created']) {
$this->latestDate = $data['created'];
}
if (!empty($data['updated']) && $this->latestDate < $data['updated']) {
$this->latestDate = $data['updated'];
}
return $data;
}
/** /**
* Get the language according to the feed type, based on the locale: * Get the language according to the feed type, based on the locale:
* *
* - RSS format: en-us (default: 'en-en'). * - RSS format: en-us (default: 'en-en').
* - ATOM format: fr (default: 'en'). * - ATOM format: fr (default: 'en').
* *
* @param string $feedType Type of feed (RSS/ATOM).
*
* @return string The language. * @return string The language.
*/ */
public function getTypeLanguage() protected function getTypeLanguage(string $feedType)
{ {
// Use the locale do define the language, if available. // Use the locale do define the language, if available.
if (!empty($this->locale) && preg_match('/^\w{2}[_\-]\w{2}/', $this->locale)) { 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 str_replace('_', '-', substr($this->locale, 0, $length));
} }
return ($this->feedType === self::$FEED_RSS) ? 'en-en' : 'en'; return ($feedType === self::$FEED_RSS) ? 'en-en' : 'en';
} }
/** /**
@ -238,32 +227,35 @@ public function getTypeLanguage()
* *
* Return an empty string if invalid DateTime is passed. * Return an empty string if invalid DateTime is passed.
* *
* @param string $feedType Type of feed (RSS/ATOM).
*
* @return string Formatted date. * @return string Formatted date.
*/ */
protected function getLatestDateFormatted() protected function getLatestDateFormatted(string $feedType)
{ {
if (empty($this->latestDate) || !$this->latestDate instanceof DateTime) { if (empty($this->latestDate) || !$this->latestDate instanceof DateTime) {
return ''; 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); return $this->latestDate->format($type);
} }
/** /**
* Get ISO date from DateTime according to feed 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 DateTime $date Date to format.
* @param string|bool $format Force format. * @param string|bool $format Force format.
* *
* @return string Formatted date. * @return string Formatted date.
*/ */
protected function getIsoDate(DateTime $date, $format = false) protected function getIsoDate(string $feedType, DateTime $date, $format = false)
{ {
if ($format !== false) { if ($format !== false) {
return $date->format($format); return $date->format($format);
} }
if ($this->feedType == self::$FEED_RSS) { if ($feedType == self::$FEED_RSS) {
return $date->format(DateTime::RSS); return $date->format(DateTime::RSS);
} }
return $date->format(DateTime::ATOM); return $date->format(DateTime::ATOM);
@ -276,20 +268,21 @@ protected function getIsoDate(DateTime $date, $format = false)
* If 'nb' is set to 'all', display all filtered bookmarks (max parameter). * 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. * @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; return self::$DEFAULT_NB_LINKS;
} }
if ($this->userInput['nb'] == 'all') { if ($userInput['nb'] == 'all') {
return $max; return $max;
} }
$intNb = intval($this->userInput['nb']); $intNb = intval($userInput['nb']);
if (!is_int($intNb) || $intNb == 0) { if (!is_int($intNb) || $intNb == 0) {
return self::$DEFAULT_NB_LINKS; return self::$DEFAULT_NB_LINKS;
} }

View file

@ -50,11 +50,10 @@ public function formatTagString($bookmark)
*/ */
public function formatUrl($bookmark) public function formatUrl($bookmark)
{ {
if (! empty($this->contextData['index_url']) && ( if ($bookmark->isNote() && isset($this->contextData['index_url'])) {
startsWith($bookmark->getUrl(), '?') || startsWith($bookmark->getUrl(), '/') return rtrim($this->contextData['index_url'], '/') . '/' . escape(ltrim($bookmark->getUrl(), '/'));
)) {
return $this->contextData['index_url'] . escape($bookmark->getUrl());
} }
return escape($bookmark->getUrl()); return escape($bookmark->getUrl());
} }
@ -63,11 +62,18 @@ public function formatUrl($bookmark)
*/ */
protected function formatRealUrl($bookmark) protected function formatRealUrl($bookmark)
{ {
if (! empty($this->contextData['index_url']) && ( if ($bookmark->isNote()) {
startsWith($bookmark->getUrl(), '?') || startsWith($bookmark->getUrl(), '/') if (isset($this->contextData['index_url'])) {
)) { $prefix = rtrim($this->contextData['index_url'], '/') . '/';
return $this->contextData['index_url'] . escape($bookmark->getUrl());
} }
if (isset($this->contextData['base_path'])) {
$prefix = rtrim($this->contextData['base_path'], '/') . '/';
}
return escape($prefix ?? '') . escape(ltrim($bookmark->getUrl(), '/'));
}
return escape($bookmark->getUrl()); return escape($bookmark->getUrl());
} }

View file

@ -3,8 +3,8 @@
namespace Shaarli\Formatter; namespace Shaarli\Formatter;
use DateTime; use DateTime;
use Shaarli\Config\ConfigManager;
use Shaarli\Bookmark\Bookmark; use Shaarli\Bookmark\Bookmark;
use Shaarli\Config\ConfigManager;
/** /**
* Class BookmarkFormatter * Class BookmarkFormatter
@ -80,6 +80,8 @@ public function format($bookmark)
public function addContextData($key, $value) public function addContextData($key, $value)
{ {
$this->contextData[$key] = $value; $this->contextData[$key] = $value;
return $this;
} }
/** /**
@ -128,7 +130,7 @@ protected function formatUrl($bookmark)
*/ */
protected function formatRealUrl($bookmark) protected function formatRealUrl($bookmark)
{ {
return $bookmark->getUrl(); return $this->formatUrl($bookmark);
} }
/** /**

View file

@ -114,7 +114,7 @@ function ($match) use ($allowedProtocols, $indexUrl) {
/** /**
* Replace hashtag in Markdown links format * 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. * It includes the index URL if specified.
* *
* @param string $description * @param string $description
@ -133,7 +133,7 @@ protected function formatHashTags($description)
* \p{Mn} - any non marking space (accents, umlauts, etc) * \p{Mn} - any non marking space (accents, umlauts, etc)
*/ */
$regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui'; $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); $descriptionLines = explode(PHP_EOL, $description);
$descriptionOut = ''; $descriptionOut = '';

View file

@ -38,7 +38,7 @@ public function __construct(ConfigManager $conf, bool $isLoggedIn)
* *
* @return BookmarkFormatter instance. * @return BookmarkFormatter instance.
*/ */
public function getFormatter(string $type = null) public function getFormatter(string $type = null): BookmarkFormatter
{ {
$type = $type ? $type : $this->conf->get('formatter', 'default'); $type = $type ? $type : $this->conf->get('formatter', 'default');
$className = '\\Shaarli\\Formatter\\Bookmark'. ucfirst($type) .'Formatter'; $className = '\\Shaarli\\Formatter\\Bookmark'. ucfirst($type) .'Formatter';

View file

@ -0,0 +1,27 @@
<?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);
}
}

View file

@ -3,7 +3,7 @@
namespace Shaarli\Front; namespace Shaarli\Front;
use Shaarli\Container\ShaarliContainer; use Shaarli\Container\ShaarliContainer;
use Shaarli\Front\Exception\ShaarliException; use Shaarli\Front\Exception\UnauthorizedException;
use Slim\Http\Request; use Slim\Http\Request;
use Slim\Http\Response; use Slim\Http\Response;
@ -24,6 +24,8 @@ public function __construct(ShaarliContainer $container)
/** /**
* Middleware execution: * Middleware execution:
* - run updates
* - if not logged in open shaarli, redirect to login
* - execute the controller * - execute the controller
* - return the response * - return the response
* *
@ -35,23 +37,78 @@ public function __construct(ShaarliContainer $container)
* *
* @return Response 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 { try {
$response = $next($request, $response); if (!is_file($this->container->conf->getConfigFileExt())
} catch (ShaarliException $e) { && !in_array($next->getName(), ['displayInstall', 'saveInstall'], true)
$this->container->pageBuilder->assign('message', $e->getMessage()); ) {
if ($this->container->conf->get('dev.debug', false)) { return $response->withRedirect($this->container->basePath . '/install');
$this->container->pageBuilder->assign( }
'stacktrace',
nl2br(get_class($this) .': '. $e->getTraceAsString()) $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();
}
} }
$response = $response->withStatus($e->getCode()); /**
$response = $response->write($this->container->pageBuilder->render('error')); * 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(), '/');
}
} }
} }

View file

@ -0,0 +1,126 @@
<?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');
}
}

View file

@ -0,0 +1,80 @@
<?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));
}
}

View file

@ -0,0 +1,82 @@
<?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');
}
}

View file

@ -0,0 +1,33 @@
<?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, '/');
}
}

View file

@ -0,0 +1,371 @@
<?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));
}
}

View file

@ -0,0 +1,88 @@
<?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);
}
}

View file

@ -0,0 +1,101 @@
<?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));
}
}

View file

@ -0,0 +1,84 @@
<?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');
}
}

View file

@ -0,0 +1,50 @@
<?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']);
}
}

View file

@ -0,0 +1,73 @@
<?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);
}
}

View file

@ -0,0 +1,65 @@
<?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));
}
}

View file

@ -0,0 +1,26 @@
<?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());
}
}

View file

@ -0,0 +1,35 @@
<?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));
}
}

View file

@ -0,0 +1,240 @@
<?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;
}
}

View file

@ -0,0 +1,192 @@
<?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;
}
}

View file

@ -0,0 +1,45 @@
<?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'));
}
}

View file

@ -0,0 +1,58 @@
<?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);
}
}

View file

@ -0,0 +1,165 @@
<?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);
}
}

View file

@ -0,0 +1,154 @@
<?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);
}
}

View file

@ -0,0 +1,27 @@
<?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));
}
}

View file

@ -0,0 +1,54 @@
<?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));
}
}

View file

@ -0,0 +1,46 @@
<?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']);
}
}

View file

@ -0,0 +1,170 @@
<?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->assignView('plugin_errors', $this->container->pluginManager->getErrors());
$this->executeDefaultHooks($template);
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);
}
}

View file

@ -0,0 +1,113 @@
<?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;
}
}

View file

@ -0,0 +1,118 @@
<?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);
}
}

View file

@ -1,48 +0,0 @@
<?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'));
}
}

View file

@ -1,69 +0,0 @@
<?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);
}
}
}

View file

@ -0,0 +1,15 @@
<?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);
}
}

View file

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Shaarli\Front\Exception;
class CantLoginException extends \Exception
{
}

View file

@ -4,7 +4,7 @@
namespace Shaarli\Front\Exception; namespace Shaarli\Front\Exception;
class LoginBannedException extends ShaarliException class LoginBannedException extends ShaarliFrontException
{ {
public function __construct() public function __construct()
{ {

View file

@ -0,0 +1,18 @@
<?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);
}
}

View file

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Shaarli\Front\Exception;
class ResourcePermissionException extends ShaarliFrontException
{
public function __construct(string $message)
{
parent::__construct($message, 500);
}
}

View file

@ -9,11 +9,11 @@
/** /**
* Class ShaarliException * 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 * @package Front\Exception
*/ */
abstract class ShaarliException extends \Exception class ShaarliFrontException extends \Exception
{ {
/** Override parent constructor to force $message and $httpCode parameters to be set. */ /** Override parent constructor to force $message and $httpCode parameters to be set. */
public function __construct(string $message, int $httpCode, Throwable $previous = null) public function __construct(string $message, int $httpCode, Throwable $previous = null)

View file

@ -0,0 +1,15 @@
<?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);
}
}

View file

@ -0,0 +1,15 @@
<?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
{
}

View file

@ -0,0 +1,18 @@
<?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);
}
}

View file

@ -0,0 +1,39 @@
<?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
);
}
}

View file

@ -369,7 +369,7 @@ function server_url($server)
*/ */
function index_url($server) function index_url($server)
{ {
$scriptname = $server['SCRIPT_NAME']; $scriptname = $server['SCRIPT_NAME'] ?? '';
if (endsWith($scriptname, 'index.php')) { if (endsWith($scriptname, 'index.php')) {
$scriptname = substr($scriptname, 0, -9); $scriptname = substr($scriptname, 0, -9);
} }
@ -377,7 +377,7 @@ function index_url($server)
} }
/** /**
* 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) * If the resource is "index.php", then it is removed (for better-looking URLs)
* *
@ -387,10 +387,17 @@ function index_url($server)
*/ */
function page_url($server) function page_url($server)
{ {
if (! empty($server['QUERY_STRING'])) { $scriptname = $server['SCRIPT_NAME'] ?? '';
return index_url($server).'?'.$server['QUERY_STRING']; if (endsWith($scriptname, 'index.php')) {
$scriptname = substr($scriptname, 0, -9);
} }
return index_url($server);
$route = ltrim($server['REQUEST_URI'] ?? '', $scriptname);
if (! empty($server['QUERY_STRING'])) {
return index_url($server) . $route . '?' . $server['QUERY_STRING'];
}
return index_url($server) . $route;
} }
/** /**
@ -477,3 +484,109 @@ function is_https($server)
return ! empty($server['HTTPS']); 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);
};
}

View file

@ -0,0 +1,130 @@
<?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);
}
}

View file

@ -9,6 +9,7 @@
use Shaarli\Bookmark\Exception\BookmarkNotFoundException; use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
use Shaarli\Exceptions\IOException; use Shaarli\Exceptions\IOException;
use Shaarli\FileUtils; use Shaarli\FileUtils;
use Shaarli\Render\PageCacheManager;
/** /**
* Data storage for bookmarks. * Data storage for bookmarks.
@ -352,7 +353,8 @@ public function save($pageCacheDir)
$this->write(); $this->write();
invalidateCaches($pageCacheDir); $pageCacheManager = new PageCacheManager($pageCacheDir, $this->loggedIn);
$pageCacheManager->invalidateCaches();
} }
/** /**

View file

@ -1,12 +1,15 @@
<?php <?php
namespace Shaarli;
namespace Shaarli\Legacy;
/** /**
* Class Router * Class Router
* *
* (only displayable pages here) * (only displayable pages here)
*
* @deprecated
*/ */
class Router class LegacyRouter
{ {
public static $AJAX_THUMB_UPDATE = 'ajax_thumb_update'; public static $AJAX_THUMB_UPDATE = 'ajax_thumb_update';

View file

@ -10,9 +10,9 @@
use Shaarli\ApplicationUtils; use Shaarli\ApplicationUtils;
use Shaarli\Bookmark\Bookmark; use Shaarli\Bookmark\Bookmark;
use Shaarli\Bookmark\BookmarkArray; use Shaarli\Bookmark\BookmarkArray;
use Shaarli\Bookmark\LinkDB;
use Shaarli\Bookmark\BookmarkFilter; use Shaarli\Bookmark\BookmarkFilter;
use Shaarli\Bookmark\BookmarkIO; use Shaarli\Bookmark\BookmarkIO;
use Shaarli\Bookmark\LinkDB;
use Shaarli\Config\ConfigJson; use Shaarli\Config\ConfigJson;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\Config\ConfigPhp; use Shaarli\Config\ConfigPhp;
@ -534,7 +534,8 @@ public function updateMethodWebThumbnailer()
if ($thumbnailsEnabled) { if ($thumbnailsEnabled) {
$this->session['warnings'][] = t( $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>'
); );
} }

View file

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Shaarli\Legacy;
class UnknowLegacyRouteException extends \Exception
{
}

View file

@ -6,6 +6,7 @@
use DateTimeZone; use DateTimeZone;
use Exception; use Exception;
use Katzgrau\KLogger\Logger; use Katzgrau\KLogger\Logger;
use Psr\Http\Message\UploadedFileInterface;
use Psr\Log\LogLevel; use Psr\Log\LogLevel;
use Shaarli\Bookmark\Bookmark; use Shaarli\Bookmark\Bookmark;
use Shaarli\Bookmark\BookmarkServiceInterface; use Shaarli\Bookmark\BookmarkServiceInterface;
@ -16,10 +17,24 @@
/** /**
* Utilities to import and export bookmarks using the Netscape format * Utilities to import and export bookmarks using the Netscape format
* TODO: Not static, use a container.
*/ */
class NetscapeBookmarkUtils 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 * Filters bookmarks and adds Netscape-formatted fields
@ -28,18 +43,16 @@ class NetscapeBookmarkUtils
* - timestamp link addition date, using the Unix epoch format * - timestamp link addition date, using the Unix epoch format
* - taglist comma-separated tag list * - taglist comma-separated tag list
* *
* @param BookmarkServiceInterface $bookmarkService Link datastore
* @param BookmarkFormatter $formatter instance * @param BookmarkFormatter $formatter instance
* @param string $selection Which bookmarks to export: (all|private|public) * @param string $selection Which bookmarks to export: (all|private|public)
* @param bool $prependNoteUrl Prepend note permalinks with the server's URL * @param bool $prependNoteUrl Prepend note permalinks with the server's URL
* @param string $indexUrl Absolute URL of the Shaarli index page * @param string $indexUrl Absolute URL of the Shaarli index page
* *
* @return array The bookmarks to be exported, with additional fields * @return array The bookmarks to be exported, with additional fields
*@throws Exception Invalid export selection
* *
* @throws Exception Invalid export selection
*/ */
public static function filterAndFormat( public function filterAndFormat(
$bookmarkService,
$formatter, $formatter,
$selection, $selection,
$prependNoteUrl, $prependNoteUrl,
@ -51,11 +64,11 @@ public static function filterAndFormat(
} }
$bookmarkLinks = array(); $bookmarkLinks = array();
foreach ($bookmarkService->search([], $selection) as $bookmark) { foreach ($this->bookmarkService->search([], $selection) as $bookmark) {
$link = $formatter->format($bookmark); $link = $formatter->format($bookmark);
$link['taglist'] = implode(',', $bookmark->getTags()); $link['taglist'] = implode(',', $bookmark->getTags());
if ($bookmark->isNote() && $prependNoteUrl) { if ($bookmark->isNote() && $prependNoteUrl) {
$link['url'] = $indexUrl . $link['url']; $link['url'] = rtrim($indexUrl, '/') . '/' . ltrim($link['url'], '/');
} }
$bookmarkLinks[] = $link; $bookmarkLinks[] = $link;
@ -64,61 +77,23 @@ public static function filterAndFormat(
return $bookmarkLinks; 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 * Imports Web bookmarks from an uploaded Netscape bookmark dump
* *
* @param array $post Server $_POST parameters * @param array $post Server $_POST parameters
* @param array $files Server $_FILES parameters * @param UploadedFileInterface $file File in PSR-7 object format
* @param BookmarkServiceInterface $bookmarkService Loaded LinkDB instance
* @param ConfigManager $conf instance
* @param History $history History instance
* *
* @return string Summary of the bookmark import status * @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(); $start = time();
$filename = $files['filetoupload']['name']; $filename = $file->getClientFilename();
$filesize = $files['filetoupload']['size']; $filesize = $file->getSize();
$data = file_get_contents($files['filetoupload']['tmp_name']); $data = (string) $file->getStream();
if (preg_match('/<!DOCTYPE NETSCAPE-Bookmark-file-1>/i', $data) === 0) { if (preg_match('/<!DOCTYPE NETSCAPE-Bookmark-file-1>/i', $data) === 0) {
return self::importStatus($filename, $filesize); return $this->importStatus($filename, $filesize);
} }
// Overwrite existing bookmarks? // Overwrite existing bookmarks?
@ -141,11 +116,11 @@ public static function import($post, $files, $bookmarkService, $conf, $history)
true, // nested tag support true, // nested tag support
$defaultTags, // additional user-specified tags $defaultTags, // additional user-specified tags
strval(1 - $defaultPrivacy), // defaultPub = 1 - defaultPrivacy 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( $logger = new Logger(
$conf->get('resource.data_dir'), $this->conf->get('resource.data_dir'),
!$conf->get('dev.debug') ? LogLevel::INFO : LogLevel::DEBUG, !$this->conf->get('dev.debug') ? LogLevel::INFO : LogLevel::DEBUG,
[ [
'prefix' => 'import.', 'prefix' => 'import.',
'extension' => 'log', 'extension' => 'log',
@ -171,7 +146,7 @@ public static function import($post, $files, $bookmarkService, $conf, $history)
$private = 0; $private = 0;
} }
$link = $bookmarkService->findByUrl($bkm['uri']); $link = $this->bookmarkService->findByUrl($bkm['uri']);
$existingLink = $link !== null; $existingLink = $link !== null;
if (! $existingLink) { if (! $existingLink) {
$link = new Bookmark(); $link = new Bookmark();
@ -193,20 +168,21 @@ public static function import($post, $files, $bookmarkService, $conf, $history)
} }
$link->setTitle($bkm['title']); $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->setDescription($bkm['note']);
$link->setPrivate($private); $link->setPrivate($private);
$link->setTagsString($bkm['tags']); $link->setTagsString($bkm['tags']);
$bookmarkService->addOrSet($link, false); $this->bookmarkService->addOrSet($link, false);
$importCount++; $importCount++;
} }
$bookmarkService->save(); $this->bookmarkService->save();
$history->importLinks(); $this->history->importLinks();
$duration = time() - $start; $duration = time() - $start;
return self::importStatus(
return $this->importStatus(
$filename, $filename,
$filesize, $filesize,
$importCount, $importCount,
@ -215,4 +191,39 @@ public static function import($post, $files, $bookmarkService, $conf, $history)
$duration $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;
}
} }

View file

@ -16,7 +16,7 @@ class PluginManager
* *
* @var array $authorizedPlugins * @var array $authorizedPlugins
*/ */
private $authorizedPlugins; private $authorizedPlugins = [];
/** /**
* List of loaded plugins. * List of loaded plugins.
@ -108,6 +108,10 @@ public function executeHooks($hook, &$data, $params = array())
$data['_LOGGEDIN_'] = $params['loggedin']; $data['_LOGGEDIN_'] = $params['loggedin'];
} }
if (isset($params['basePath'])) {
$data['_BASE_PATH_'] = $params['basePath'];
}
foreach ($this->loadedPlugins as $plugin) { foreach ($this->loadedPlugins as $plugin) {
$hookFunction = $this->buildHookName($hook, $plugin); $hookFunction = $this->buildHookName($hook, $plugin);

View file

@ -3,10 +3,12 @@
namespace Shaarli\Render; namespace Shaarli\Render;
use Exception; use Exception;
use exceptions\MissingBasePathException;
use RainTPL; use RainTPL;
use Shaarli\ApplicationUtils; use Shaarli\ApplicationUtils;
use Shaarli\Bookmark\BookmarkServiceInterface; use Shaarli\Bookmark\BookmarkServiceInterface;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\Security\SessionManager;
use Shaarli\Thumbnailer; use Shaarli\Thumbnailer;
/** /**
@ -68,6 +70,15 @@ public function __construct(&$conf, $session, $linkDB = null, $token = null, $is
$this->isLoggedIn = $isLoggedIn; $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. * Initialize all default tpl tags.
*/ */
@ -136,17 +147,40 @@ private function initialize()
$this->tpl->assign('thumbnails_width', $this->conf->get('thumbnails.width')); $this->tpl->assign('thumbnails_width', $this->conf->get('thumbnails.width'));
$this->tpl->assign('thumbnails_height', $this->conf->get('thumbnails.height')); $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')); $this->tpl->assign('formatter', $this->conf->get('formatter', 'default'));
// To be removed with a proper theme configuration. // To be removed with a proper theme configuration.
$this->tpl->assign('conf', $this->conf); $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) * The following assign() method is basically the same as RainTPL (except lazy loading)
* *
@ -184,21 +218,6 @@ public function assignAll($data)
return true; 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). * Render a specific page as string (using a template file).
* e.g. $pb->render('picwall'); * e.g. $pb->render('picwall');
@ -207,28 +226,14 @@ public function renderPage($page)
* *
* @return string Processed template content * @return string Processed template content
*/ */
public function render(string $page): string public function render(string $page, string $basePath): string
{ {
if ($this->tpl === false) { if ($this->tpl === false) {
$this->initialize(); $this->initialize();
} }
$this->finalize($basePath);
return $this->tpl->draw($page, true); return $this->tpl->draw($page, true);
} }
/**
* 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');
}
} }

View file

@ -0,0 +1,60 @@
<?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
);
}
}

View file

@ -0,0 +1,33 @@
<?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';
}

View file

@ -0,0 +1,33 @@
<?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;
}
}

View file

@ -9,9 +9,6 @@
*/ */
class LoginManager 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 */ /** @var array A reference to the $_GLOBALS array */
protected $globals = []; protected $globals = [];
@ -32,17 +29,21 @@ class LoginManager
/** @var string User sign-in token depending on remote IP and credentials */ /** @var string User sign-in token depending on remote IP and credentials */
protected $staySignedInToken = ''; protected $staySignedInToken = '';
/** @var CookieManager */
protected $cookieManager;
/** /**
* Constructor * Constructor
* *
* @param ConfigManager $configManager Configuration Manager instance * @param ConfigManager $configManager Configuration Manager instance
* @param SessionManager $sessionManager SessionManager 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->configManager = $configManager;
$this->sessionManager = $sessionManager; $this->sessionManager = $sessionManager;
$this->cookieManager = $cookieManager;
$this->banManager = new BanManager( $this->banManager = new BanManager(
$this->configManager->get('security.trusted_proxies', []), $this->configManager->get('security.trusted_proxies', []),
$this->configManager->get('security.ban_after'), $this->configManager->get('security.ban_after'),
@ -86,10 +87,9 @@ public function getStaySignedInToken()
/** /**
* Check user session state and validity (expiration) * Check user session state and validity (expiration)
* *
* @param array $cookie The $_COOKIE array
* @param string $clientIpId Client IP address identifier * @param string $clientIpId Client IP address identifier
*/ */
public function checkLoginState($cookie, $clientIpId) public function checkLoginState($clientIpId)
{ {
if (! $this->configManager->exists('credentials.login')) { if (! $this->configManager->exists('credentials.login')) {
// Shaarli is not configured yet // Shaarli is not configured yet
@ -97,9 +97,7 @@ public function checkLoginState($cookie, $clientIpId)
return; return;
} }
if (isset($cookie[self::$STAY_SIGNED_IN_COOKIE]) if ($this->staySignedInToken === $this->cookieManager->getCookieParameter(CookieManager::STAY_SIGNED_IN)) {
&& $cookie[self::$STAY_SIGNED_IN_COOKIE] === $this->staySignedInToken
) {
// The user client has a valid stay-signed-in cookie // The user client has a valid stay-signed-in cookie
// Session information is updated with the current client information // Session information is updated with the current client information
$this->sessionManager->storeLoginInfo($clientIpId); $this->sessionManager->storeLoginInfo($clientIpId);

View file

@ -8,6 +8,14 @@
*/ */
class SessionManager 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 */ /** @var int Session expiration timeout, in seconds */
public static $SHORT_TIMEOUT = 3600; // 1 hour public static $SHORT_TIMEOUT = 3600; // 1 hour
@ -23,16 +31,35 @@ class SessionManager
/** @var bool Whether the user should stay signed in (LONG_TIMEOUT) */ /** @var bool Whether the user should stay signed in (LONG_TIMEOUT) */
protected $staySignedIn = false; protected $staySignedIn = false;
/** @var string */
protected $savePath;
/** /**
* Constructor * Constructor
* *
* @param array $session The $_SESSION array (reference) * @param array $session The $_SESSION array (reference)
* @param ConfigManager $conf ConfigManager instance * @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->session = &$session;
$this->conf = $conf; $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);
}
} }
/** /**
@ -202,4 +229,78 @@ public function getSession(): array
{ {
return $this->session; 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);
}
} }

View file

@ -2,8 +2,8 @@
namespace Shaarli\Updater; namespace Shaarli\Updater;
use Shaarli\Config\ConfigManager;
use Shaarli\Bookmark\BookmarkServiceInterface; use Shaarli\Bookmark\BookmarkServiceInterface;
use Shaarli\Config\ConfigManager;
use Shaarli\Updater\Exception\UpdaterException; use Shaarli\Updater\Exception\UpdaterException;
/** /**
@ -21,7 +21,7 @@ class Updater
/** /**
* @var BookmarkServiceInterface instance. * @var BookmarkServiceInterface instance.
*/ */
protected $linkServices; protected $bookmarkService;
/** /**
* @var ConfigManager $conf Configuration Manager instance. * @var ConfigManager $conf Configuration Manager instance.
@ -38,6 +38,11 @@ class Updater
*/ */
protected $methods; protected $methods;
/**
* @var string $basePath Shaarli root directory (from HTTP Request)
*/
protected $basePath = null;
/** /**
* Object constructor. * Object constructor.
* *
@ -49,7 +54,7 @@ class Updater
public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn) public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn)
{ {
$this->doneUpdates = $doneUpdates; $this->doneUpdates = $doneUpdates;
$this->linkServices = $linkDB; $this->bookmarkService = $linkDB;
$this->conf = $conf; $this->conf = $conf;
$this->isLoggedIn = $isLoggedIn; $this->isLoggedIn = $isLoggedIn;
@ -62,13 +67,15 @@ public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn)
* Run all new updates. * Run all new updates.
* Update methods have to start with 'updateMethod' and return true (on success). * 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. * @return array An array containing ran updates.
* *
* @throws UpdaterException If something went wrong. * @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 the user isn't logged in, exit without updating.
if ($this->isLoggedIn !== true) { if ($this->isLoggedIn !== true) {
@ -111,4 +118,62 @@ public function getDoneUpdates()
{ {
return $this->doneUpdates; 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;
}
} }

View file

@ -10,13 +10,14 @@
* It contains a recursive call to retrieve the thumb of the next link when it succeed. * 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. * 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 {array} ids List of LinkID to update
* @param {int} i Current index in ids * @param {int} i Current index in ids
* @param {object} elements List of DOM element to avoid retrieving them at each iteration * @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(); 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.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.responseType = 'json'; xhr.responseType = 'json';
xhr.onload = () => { xhr.onload = () => {
@ -29,17 +30,18 @@ function updateThumb(ids, i, elements) {
elements.current.innerHTML = i; elements.current.innerHTML = i;
elements.title.innerHTML = response.title; elements.title.innerHTML = response.title;
if (response.thumbnail !== false) { if (response.thumbnail !== false) {
elements.thumbnail.innerHTML = `<img src="${response.thumbnail}">`; elements.thumbnail.innerHTML = `<img src="${basePath}/${response.thumbnail}">`;
} }
if (i < ids.length) { 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 ids = document.getElementsByName('ids')[0].value.split(',');
const elements = { const elements = {
progressBar: document.querySelector('.progressbar > div'), progressBar: document.querySelector('.progressbar > div'),
@ -47,5 +49,5 @@ function updateThumb(ids, i, elements) {
thumbnail: document.querySelector('.thumbnail-placeholder'), thumbnail: document.querySelector('.thumbnail-placeholder'),
title: document.querySelector('.thumbnail-link-title'), title: document.querySelector('.thumbnail-link-title'),
}; };
updateThumb(ids, 0, elements); updateThumb(basePath, ids, 0, elements);
})(); })();

View file

@ -25,12 +25,16 @@ function findParent(element, tagName, attributes) {
/** /**
* Ajax request to refresh the CSRF token. * Ajax request to refresh the CSRF token.
*/ */
function refreshToken() { function refreshToken(basePath) {
console.log('refresh');
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
xhr.open('GET', '?do=token'); xhr.open('GET', `${basePath}/admin/token`);
xhr.onload = () => { xhr.onload = () => {
const token = document.getElementById('token'); const elements = document.querySelectorAll('input[name="token"]');
token.setAttribute('value', xhr.responseText); [...elements].forEach((element) => {
console.log(element);
element.setAttribute('value', xhr.responseText);
});
}; };
xhr.send(); xhr.send();
} }
@ -215,6 +219,8 @@ function init(description) {
} }
(() => { (() => {
const basePath = document.querySelector('input[name="js_base_path"]').value;
/** /**
* Handle responsive menu. * Handle responsive menu.
* Source: http://purecss.io/layouts/tucked-menu-vertical/ * Source: http://purecss.io/layouts/tucked-menu-vertical/
@ -461,7 +467,7 @@ function init(description) {
}); });
if (window.confirm(message)) { 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}`;
} }
}); });
} }
@ -483,7 +489,8 @@ function init(description) {
}); });
const ids = links.map(item => item.id); 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('+')}`;
}); });
}); });
} }
@ -546,7 +553,7 @@ function init(description) {
const refreshedToken = document.getElementById('token').value; const refreshedToken = document.getElementById('token').value;
const fromtag = block.getAttribute('data-tag'); const fromtag = block.getAttribute('data-tag');
const xhr = new XMLHttpRequest(); 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.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onload = () => { xhr.onload = () => {
if (xhr.status !== 200) { if (xhr.status !== 200) {
@ -558,8 +565,12 @@ function init(description) {
input.setAttribute('value', totag); input.setAttribute('value', totag);
findParent(input, 'div', { class: 'rename-tag-form' }).style.display = 'none'; findParent(input, 'div', { class: 'rename-tag-form' }).style.display = 'none';
block.querySelector('a.tag-link').innerHTML = htmlEntities(totag); block.querySelector('a.tag-link').innerHTML = htmlEntities(totag);
block.querySelector('a.tag-link').setAttribute('href', `?searchtags=${encodeURIComponent(totag)}`); block
block.querySelector('a.rename-tag').setAttribute('href', `?do=changetag&fromtag=${encodeURIComponent(totag)}`); .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 // Refresh awesomplete values
existingTags = existingTags.map(tag => (tag === fromtag ? totag : tag)); existingTags = existingTags.map(tag => (tag === fromtag ? totag : tag));
@ -567,7 +578,7 @@ function init(description) {
} }
}; };
xhr.send(`renametag=1&fromtag=${encodeURIComponent(fromtag)}&totag=${encodeURIComponent(totag)}&token=${refreshedToken}`); xhr.send(`renametag=1&fromtag=${encodeURIComponent(fromtag)}&totag=${encodeURIComponent(totag)}&token=${refreshedToken}`);
refreshToken(); refreshToken(basePath);
}); });
}); });
@ -593,13 +604,13 @@ function init(description) {
if (confirm(`Are you sure you want to delete the tag "${tag}"?`)) { if (confirm(`Are you sure you want to delete the tag "${tag}"?`)) {
const xhr = new XMLHttpRequest(); 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.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onload = () => { xhr.onload = () => {
block.remove(); block.remove();
}; };
xhr.send(encodeURI(`deletetag=1&fromtag=${tag}&token=${refreshedToken}`)); xhr.send(encodeURI(`deletetag=1&fromtag=${tag}&token=${refreshedToken}`));
refreshToken(); refreshToken(basePath);
existingTags = existingTags.filter(tagItem => tagItem !== tag); existingTags = existingTags.filter(tagItem => tagItem !== tag);
awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes); awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes);

View file

@ -490,6 +490,10 @@ body,
} }
} }
.header-alert-message {
text-align: center;
}
// CONTENT - GENERAL // CONTENT - GENERAL
.container { .container {
position: relative; position: relative;

View file

@ -53,7 +53,8 @@
"Shaarli\\Feed\\": "application/feed", "Shaarli\\Feed\\": "application/feed",
"Shaarli\\Formatter\\": "application/formatter", "Shaarli\\Formatter\\": "application/formatter",
"Shaarli\\Front\\": "application/front", "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\\Front\\Exception\\": "application/front/exceptions",
"Shaarli\\Http\\": "application/http", "Shaarli\\Http\\": "application/http",
"Shaarli\\Legacy\\": "application/legacy", "Shaarli\\Legacy\\": "application/legacy",

211
composer.lock generated
View file

@ -508,16 +508,16 @@
}, },
{ {
"name": "psr/log", "name": "psr/log",
"version": "1.1.2", "version": "1.1.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/php-fig/log.git", "url": "https://github.com/php-fig/log.git",
"reference": "446d54b4cb6bf489fc9d75f55843658e6f25d801" "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/php-fig/log/zipball/446d54b4cb6bf489fc9d75f55843658e6f25d801", "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc",
"reference": "446d54b4cb6bf489fc9d75f55843658e6f25d801", "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -551,7 +551,7 @@
"psr", "psr",
"psr-3" "psr-3"
], ],
"time": "2019-11-01T11:05:21+00:00" "time": "2020-03-23T09:12:05+00:00"
}, },
{ {
"name": "pubsubhubbub/publisher", "name": "pubsubhubbub/publisher",
@ -936,24 +936,21 @@
}, },
{ {
"name": "phpdocumentor/reflection-common", "name": "phpdocumentor/reflection-common",
"version": "2.0.0", "version": "2.1.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/phpDocumentor/ReflectionCommon.git", "url": "https://github.com/phpDocumentor/ReflectionCommon.git",
"reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a" "reference": "6568f4687e5b41b054365f9ae03fcb1ed5f2069b"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/63a995caa1ca9e5590304cd845c15ad6d482a62a", "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/6568f4687e5b41b054365f9ae03fcb1ed5f2069b",
"reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a", "reference": "6568f4687e5b41b054365f9ae03fcb1ed5f2069b",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": ">=7.1" "php": ">=7.1"
}, },
"require-dev": {
"phpunit/phpunit": "~6"
},
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
@ -984,7 +981,7 @@
"reflection", "reflection",
"static analysis" "static analysis"
], ],
"time": "2018-08-07T13:53:10+00:00" "time": "2020-04-27T09:25:28+00:00"
}, },
{ {
"name": "phpdocumentor/reflection-docblock", "name": "phpdocumentor/reflection-docblock",
@ -1087,24 +1084,24 @@
}, },
{ {
"name": "phpspec/prophecy", "name": "phpspec/prophecy",
"version": "1.10.1", "version": "v1.10.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/phpspec/prophecy.git", "url": "https://github.com/phpspec/prophecy.git",
"reference": "cbe1df668b3fe136bcc909126a0f529a78d4cbbc" "reference": "451c3cd1418cf640de218914901e51b064abb093"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpspec/prophecy/zipball/cbe1df668b3fe136bcc909126a0f529a78d4cbbc", "url": "https://api.github.com/repos/phpspec/prophecy/zipball/451c3cd1418cf640de218914901e51b064abb093",
"reference": "cbe1df668b3fe136bcc909126a0f529a78d4cbbc", "reference": "451c3cd1418cf640de218914901e51b064abb093",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"doctrine/instantiator": "^1.0.2", "doctrine/instantiator": "^1.0.2",
"php": "^5.3|^7.0", "php": "^5.3|^7.0",
"phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0", "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0",
"sebastian/comparator": "^1.2.3|^2.0|^3.0", "sebastian/comparator": "^1.2.3|^2.0|^3.0|^4.0",
"sebastian/recursion-context": "^1.0|^2.0|^3.0" "sebastian/recursion-context": "^1.0|^2.0|^3.0|^4.0"
}, },
"require-dev": { "require-dev": {
"phpspec/phpspec": "^2.5 || ^3.2", "phpspec/phpspec": "^2.5 || ^3.2",
@ -1146,7 +1143,7 @@
"spy", "spy",
"stub" "stub"
], ],
"time": "2019-12-22T21:05:45+00:00" "time": "2020-03-05T15:02:03+00:00"
}, },
{ {
"name": "phpunit/php-code-coverage", "name": "phpunit/php-code-coverage",
@ -1501,12 +1498,12 @@
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/Roave/SecurityAdvisories.git", "url": "https://github.com/Roave/SecurityAdvisories.git",
"reference": "67ac6ea8f4a078c3c9b7aec5d7ae70f098c37389" "reference": "5a342e2dc0408d026b97ee3176b5b406e54e3766"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/67ac6ea8f4a078c3c9b7aec5d7ae70f098c37389", "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/5a342e2dc0408d026b97ee3176b5b406e54e3766",
"reference": "67ac6ea8f4a078c3c9b7aec5d7ae70f098c37389", "reference": "5a342e2dc0408d026b97ee3176b5b406e54e3766",
"shasum": "" "shasum": ""
}, },
"conflict": { "conflict": {
@ -1518,11 +1515,17 @@
"api-platform/core": ">=2.2,<2.2.10|>=2.3,<2.3.6", "api-platform/core": ">=2.2,<2.2.10|>=2.3,<2.3.6",
"asymmetricrypt/asymmetricrypt": ">=0,<9.9.99", "asymmetricrypt/asymmetricrypt": ">=0,<9.9.99",
"aws/aws-sdk-php": ">=3,<3.2.1", "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", "brightlocal/phpwhois": "<=4.2.5",
"buddypress/buddypress": "<5.1.2",
"bugsnag/bugsnag-laravel": ">=2,<2.0.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", "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", "cart2quote/module-quotation": ">=4.1.6,<=4.4.5|>=5,<5.4.4",
"cartalyst/sentry": "<=2.1.6", "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", "codeigniter/framework": "<=3.0.6",
"composer/composer": "<=1-alpha.11", "composer/composer": "<=1-alpha.11",
"contao-components/mediaelement": ">=2.14.2,<2.21.1", "contao-components/mediaelement": ">=2.14.2,<2.21.1",
@ -1540,22 +1543,32 @@
"doctrine/mongodb-odm": ">=1,<1.0.2", "doctrine/mongodb-odm": ">=1,<1.0.2",
"doctrine/mongodb-odm-bundle": ">=2,<3.0.1", "doctrine/mongodb-odm-bundle": ">=2,<3.0.1",
"doctrine/orm": ">=2,<2.4.8|>=2.5,<2.5.1", "doctrine/orm": ">=2,<2.4.8|>=2.5,<2.5.1",
"dolibarr/dolibarr": "<=10.0.6",
"dompdf/dompdf": ">=0.6,<0.6.2", "dompdf/dompdf": ">=0.6,<0.6.2",
"drupal/core": ">=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.11|>=8.8,<8.8.1", "drupal/drupal": ">=7,<7.69|>=8,<8.7.12|>=8.8,<8.8.4",
"endroid/qr-code-bundle": "<3.4.2", "endroid/qr-code-bundle": "<3.4.2",
"enshrined/svg-sanitize": "<0.13.1",
"erusev/parsedown": "<1.7.2", "erusev/parsedown": "<1.7.2",
"ezsystems/ezplatform-admin-ui": ">=1.3,<1.3.5|>=1.4,<1.4.4", "ezsystems/demobundle": ">=5.4,<5.4.6.1",
"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/ezdemo-ls-extension": ">=5.4,<5.4.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/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", "ezsystems/repository-forms": ">=2.3,<2.3.2.1",
"ezyang/htmlpurifier": "<4.1.1", "ezyang/htmlpurifier": "<4.1.1",
"firebase/php-jwt": "<2", "firebase/php-jwt": "<2",
"fooman/tcpdf": "<6.2.22", "fooman/tcpdf": "<6.2.22",
"fossar/tcpdf-parser": "<6.2.22", "fossar/tcpdf-parser": "<6.2.22",
"friendsofsymfony/oauth2-php": "<1.3",
"friendsofsymfony/rest-bundle": ">=1.2,<1.2.2", "friendsofsymfony/rest-bundle": ">=1.2,<1.2.2",
"friendsofsymfony/user-bundle": ">=1.2,<1.3.5", "friendsofsymfony/user-bundle": ">=1.2,<1.3.5",
"fuel/core": "<1.8.1", "fuel/core": "<1.8.1",
"getgrav/grav": "<1.7-beta.8",
"gree/jose": "<=2.2", "gree/jose": "<=2.2",
"gregwar/rst": "<1.0.3", "gregwar/rst": "<1.0.3",
"guzzlehttp/guzzle": ">=4-rc.2,<4.2.4|>=5,<5.3.1|>=6,<6.2.1", "guzzlehttp/guzzle": ">=4-rc.2,<4.2.4|>=5,<5.3.1|>=6,<6.2.1",
@ -1563,6 +1576,7 @@
"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/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/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/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", "ivankristianto/phpwhois": "<=4.3",
"james-heinrich/getid3": "<1.9.9", "james-heinrich/getid3": "<1.9.9",
"joomla/session": "<1.3.1", "joomla/session": "<1.3.1",
@ -1570,15 +1584,19 @@
"kazist/phpwhois": "<=4.2.6", "kazist/phpwhois": "<=4.2.6",
"kreait/firebase-php": ">=3.2,<3.8.1", "kreait/firebase-php": ">=3.2,<3.8.1",
"la-haute-societe/tcpdf": "<6.2.22", "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", "laravel/socialite": ">=1,<1.0.99|>=2,<2.0.10",
"league/commonmark": "<0.18.3", "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/magento1ce": "<1.9.4.3",
"magento/magento1ee": ">=1,<1.14.4.3", "magento/magento1ee": ">=1,<1.14.4.3",
"magento/product-community-edition": ">=2,<2.2.10|>=2.3,<2.3.2-p.2", "magento/product-community-edition": ">=2,<2.2.10|>=2.3,<2.3.2-p.2",
"monolog/monolog": ">=1.8,<1.12", "monolog/monolog": ">=1.8,<1.12",
"namshi/jose": "<2.2", "namshi/jose": "<2.2",
"nzo/url-encryptor-bundle": ">=4,<4.3.2|>=5,<5.0.1",
"onelogin/php-saml": "<2.10.4", "onelogin/php-saml": "<2.10.4",
"oneup/uploader-bundle": "<1.9.3|>=2,<2.1.5",
"openid/php-openid": "<2.3", "openid/php-openid": "<2.3",
"oro/crm": ">=1.7,<1.7.4", "oro/crm": ">=1.7,<1.7.4",
"oro/platform": ">=1.7,<1.7.4", "oro/platform": ">=1.7,<1.7.4",
@ -1587,49 +1605,67 @@
"paragonie/random_compat": "<2", "paragonie/random_compat": "<2",
"paypal/merchant-sdk-php": "<3.12", "paypal/merchant-sdk-php": "<3.12",
"pear/archive_tar": "<1.4.4", "pear/archive_tar": "<1.4.4",
"phpfastcache/phpfastcache": ">=5,<5.0.13",
"phpmailer/phpmailer": ">=5,<5.2.27|>=6,<6.0.6", "phpmailer/phpmailer": ">=5,<5.2.27|>=6,<6.0.6",
"phpoffice/phpexcel": "<=1.8.1", "phpmyadmin/phpmyadmin": "<4.9.2",
"phpoffice/phpspreadsheet": "<=1.5", "phpoffice/phpexcel": "<1.8.2",
"phpoffice/phpspreadsheet": "<1.8",
"phpunit/phpunit": ">=4.8.19,<4.8.28|>=5.0.10,<5.6.3", "phpunit/phpunit": ">=4.8.19,<4.8.28|>=5.0.10,<5.6.3",
"phpwhois/phpwhois": "<=4.2.5", "phpwhois/phpwhois": "<=4.2.5",
"phpxmlrpc/extras": "<0.6.1", "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/propel": ">=2-alpha.1,<=2-alpha.7",
"propel/propel1": ">=1,<=1.7.1", "propel/propel1": ">=1,<=1.7.1",
"pusher/pusher-php-server": "<2.2.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", "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", "scheb/two-factor-bundle": ">=0,<3.26|>=4,<4.11",
"sensiolabs/connect": "<4.2.3", "sensiolabs/connect": "<4.2.3",
"serluck/phpwhois": "<=4.2.6", "serluck/phpwhois": "<=4.2.6",
"shopware/shopware": "<5.3.7", "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/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/graphql": ">=2,<2.0.5|>=3,<3.1.2",
"silverstripe/registry": ">=2.1,<2.1.2|>=2.2,<2.2.1", "silverstripe/registry": ">=2.1,<2.1.2|>=2.2,<2.2.1",
"silverstripe/restfulserver": ">=1,<1.0.9|>=2,<2.0.4", "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", "silverstripe/userforms": "<3",
"simple-updates/phpwhois": "<=1", "simple-updates/phpwhois": "<=1",
"simplesamlphp/saml2": "<1.10.6|>=2,<2.3.8|>=3,<3.1.4", "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", "simplesamlphp/simplesamlphp-module-infocard": "<1.0.1",
"simplito/elliptic-php": "<1.0.6",
"slim/slim": "<2.6", "slim/slim": "<2.6",
"smarty/smarty": "<3.1.33", "smarty/smarty": "<3.1.33",
"socalnick/scn-social-auth": "<1.15.2", "socalnick/scn-social-auth": "<1.15.2",
"spoonity/tcpdf": "<6.2.22", "spoonity/tcpdf": "<6.2.22",
"squizlabs/php_codesniffer": ">=1,<2.8.1|>=3,<3.0.1", "squizlabs/php_codesniffer": ">=1,<2.8.1|>=3,<3.0.1",
"ssddanbrown/bookstack": "<0.29.2",
"stormpath/sdk": ">=0,<9.9.99", "stormpath/sdk": ">=0,<9.9.99",
"studio-42/elfinder": "<2.1.48", "studio-42/elfinder": "<2.1.49",
"swiftmailer/swiftmailer": ">=4,<5.4.5", "swiftmailer/swiftmailer": ">=4,<5.4.5",
"sylius/admin-bundle": ">=1,<1.0.17|>=1.1,<1.1.9|>=1.2,<1.2.2", "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": ">=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/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/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/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/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/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/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/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/mime": ">=4.3,<4.3.8",
@ -1638,14 +1674,14 @@
"symfony/polyfill-php55": ">=1,<1.10", "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/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/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-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-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-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-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/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/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/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", "symfony/var-exporter": ">=4.2,<4.2.12|>=4.3,<4.3.8",
@ -1658,14 +1694,17 @@
"titon/framework": ">=0,<9.9.99", "titon/framework": ">=0,<9.9.99",
"truckersmp/phpwhois": "<=4.3.1", "truckersmp/phpwhois": "<=4.3.1",
"twig/twig": "<1.38|>=2,<2.7", "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": ">=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.12|>=10,<10.2.1", "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/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/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", "typo3/phar-stream-wrapper": ">=1,<2.1.1|>=3,<3.1.1",
"ua-parser/uap-php": "<3.8", "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", "wallabag/tcpdf": "<6.2.22",
"willdurand/js-translation-bundle": "<2.1.1", "willdurand/js-translation-bundle": "<2.1.1",
"yii2mod/yii2-cms": "<1.9.2",
"yiisoft/yii": ">=1.1.14,<1.1.15", "yiisoft/yii": ">=1.1.14,<1.1.15",
"yiisoft/yii2": "<2.0.15", "yiisoft/yii2": "<2.0.15",
"yiisoft/yii2-bootstrap": "<2.0.4", "yiisoft/yii2-bootstrap": "<2.0.4",
@ -1674,6 +1713,7 @@
"yiisoft/yii2-gii": "<2.0.4", "yiisoft/yii2-gii": "<2.0.4",
"yiisoft/yii2-jui": "<2.0.4", "yiisoft/yii2-jui": "<2.0.4",
"yiisoft/yii2-redis": "<2.0.8", "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-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-captcha": ">=2,<2.4.9|>=2.5,<2.5.2",
"zendframework/zend-crypt": ">=2,<2.4.9|>=2.5,<2.5.2", "zendframework/zend-crypt": ">=2,<2.4.9|>=2.5,<2.5.2",
@ -1710,10 +1750,15 @@
"name": "Marco Pivetta", "name": "Marco Pivetta",
"email": "ocramius@gmail.com", "email": "ocramius@gmail.com",
"role": "maintainer" "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", "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": "sebastian/code-unit-reverse-lookup",
@ -2326,16 +2371,16 @@
}, },
{ {
"name": "squizlabs/php_codesniffer", "name": "squizlabs/php_codesniffer",
"version": "3.5.3", "version": "3.5.5",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/squizlabs/PHP_CodeSniffer.git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
"reference": "557a1fc7ac702c66b0bbfe16ab3d55839ef724cb" "reference": "73e2e7f57d958e7228fce50dc0c61f58f017f9f6"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/557a1fc7ac702c66b0bbfe16ab3d55839ef724cb", "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/73e2e7f57d958e7228fce50dc0c61f58f017f9f6",
"reference": "557a1fc7ac702c66b0bbfe16ab3d55839ef724cb", "reference": "73e2e7f57d958e7228fce50dc0c61f58f017f9f6",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2373,20 +2418,20 @@
"phpcs", "phpcs",
"standards" "standards"
], ],
"time": "2019-12-04T04:46:47+00:00" "time": "2020-04-17T01:09:41+00:00"
}, },
{ {
"name": "symfony/console", "name": "symfony/console",
"version": "v4.4.2", "version": "v4.4.8",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/console.git", "url": "https://github.com/symfony/console.git",
"reference": "82437719dab1e6bdd28726af14cb345c2ec816d0" "reference": "10bb3ee3c97308869d53b3e3d03f6ac23ff985f7"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/82437719dab1e6bdd28726af14cb345c2ec816d0", "url": "https://api.github.com/repos/symfony/console/zipball/10bb3ee3c97308869d53b3e3d03f6ac23ff985f7",
"reference": "82437719dab1e6bdd28726af14cb345c2ec816d0", "reference": "10bb3ee3c97308869d53b3e3d03f6ac23ff985f7",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2449,20 +2494,20 @@
], ],
"description": "Symfony Console Component", "description": "Symfony Console Component",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"time": "2019-12-17T10:32:23+00:00" "time": "2020-03-30T11:41:10+00:00"
}, },
{ {
"name": "symfony/finder", "name": "symfony/finder",
"version": "v4.4.2", "version": "v4.4.8",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/finder.git", "url": "https://github.com/symfony/finder.git",
"reference": "ce8743441da64c41e2a667b8eb66070444ed911e" "reference": "5729f943f9854c5781984ed4907bbb817735776b"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/ce8743441da64c41e2a667b8eb66070444ed911e", "url": "https://api.github.com/repos/symfony/finder/zipball/5729f943f9854c5781984ed4907bbb817735776b",
"reference": "ce8743441da64c41e2a667b8eb66070444ed911e", "reference": "5729f943f9854c5781984ed4907bbb817735776b",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2498,20 +2543,20 @@
], ],
"description": "Symfony Finder Component", "description": "Symfony Finder Component",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"time": "2019-11-17T21:56:56+00:00" "time": "2020-03-27T16:54:36+00:00"
}, },
{ {
"name": "symfony/polyfill-ctype", "name": "symfony/polyfill-ctype",
"version": "v1.13.1", "version": "v1.17.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git", "url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "f8f0b461be3385e56d6de3dbb5a0df24c0c275e3" "reference": "e94c8b1bbe2bc77507a1056cdb06451c75b427f9"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/f8f0b461be3385e56d6de3dbb5a0df24c0c275e3", "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e94c8b1bbe2bc77507a1056cdb06451c75b427f9",
"reference": "f8f0b461be3385e56d6de3dbb5a0df24c0c275e3", "reference": "e94c8b1bbe2bc77507a1056cdb06451c75b427f9",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2523,7 +2568,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "1.13-dev" "dev-master": "1.17-dev"
} }
}, },
"autoload": { "autoload": {
@ -2556,20 +2601,20 @@
"polyfill", "polyfill",
"portable" "portable"
], ],
"time": "2019-11-27T13:56:44+00:00" "time": "2020-05-12T16:14:59+00:00"
}, },
{ {
"name": "symfony/polyfill-mbstring", "name": "symfony/polyfill-mbstring",
"version": "v1.13.1", "version": "v1.17.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git", "url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "7b4aab9743c30be783b73de055d24a39cf4b954f" "reference": "fa79b11539418b02fc5e1897267673ba2c19419c"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/7b4aab9743c30be783b73de055d24a39cf4b954f", "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fa79b11539418b02fc5e1897267673ba2c19419c",
"reference": "7b4aab9743c30be783b73de055d24a39cf4b954f", "reference": "fa79b11539418b02fc5e1897267673ba2c19419c",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2581,7 +2626,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "1.13-dev" "dev-master": "1.17-dev"
} }
}, },
"autoload": { "autoload": {
@ -2615,20 +2660,20 @@
"portable", "portable",
"shim" "shim"
], ],
"time": "2019-11-27T14:18:11+00:00" "time": "2020-05-12T16:47:27+00:00"
}, },
{ {
"name": "symfony/polyfill-php73", "name": "symfony/polyfill-php73",
"version": "v1.13.1", "version": "v1.17.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-php73.git", "url": "https://github.com/symfony/polyfill-php73.git",
"reference": "4b0e2222c55a25b4541305a053013d5647d3a25f" "reference": "a760d8964ff79ab9bf057613a5808284ec852ccc"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/4b0e2222c55a25b4541305a053013d5647d3a25f", "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/a760d8964ff79ab9bf057613a5808284ec852ccc",
"reference": "4b0e2222c55a25b4541305a053013d5647d3a25f", "reference": "a760d8964ff79ab9bf057613a5808284ec852ccc",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2637,7 +2682,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "1.13-dev" "dev-master": "1.17-dev"
} }
}, },
"autoload": { "autoload": {
@ -2673,7 +2718,7 @@
"portable", "portable",
"shim" "shim"
], ],
"time": "2019-11-27T16:25:15+00:00" "time": "2020-05-12T16:47:27+00:00"
}, },
{ {
"name": "symfony/service-contracts", "name": "symfony/service-contracts",
@ -2815,16 +2860,16 @@
}, },
{ {
"name": "webmozart/assert", "name": "webmozart/assert",
"version": "1.6.0", "version": "1.8.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/webmozart/assert.git", "url": "https://github.com/webmozart/assert.git",
"reference": "573381c0a64f155a0d9a23f4b0c797194805b925" "reference": "ab2cb0b3b559010b75981b1bdce728da3ee90ad6"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/webmozart/assert/zipball/573381c0a64f155a0d9a23f4b0c797194805b925", "url": "https://api.github.com/repos/webmozart/assert/zipball/ab2cb0b3b559010b75981b1bdce728da3ee90ad6",
"reference": "573381c0a64f155a0d9a23f4b0c797194805b925", "reference": "ab2cb0b3b559010b75981b1bdce728da3ee90ad6",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2832,7 +2877,7 @@
"symfony/polyfill-ctype": "^1.8" "symfony/polyfill-ctype": "^1.8"
}, },
"conflict": { "conflict": {
"vimeo/psalm": "<3.6.0" "vimeo/psalm": "<3.9.1"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^4.8.36 || ^7.5.13" "phpunit/phpunit": "^4.8.36 || ^7.5.13"
@ -2859,7 +2904,7 @@
"check", "check",
"validate" "validate"
], ],
"time": "2019-11-24T13:36:37+00:00" "time": "2020-04-18T12:12:48+00:00"
} }
], ],
"aliases": [], "aliases": [],

View file

@ -537,7 +537,7 @@ At the end of the menu:
At the end of file, before clearing floating blocks: At the end of file, before clearing floating blocks:
{if="!empty($plugin_errors) && isLoggedIn()"} {if="!empty($plugin_errors) && $is_logged_in"}
<ul class="errors"> <ul class="errors">
{loop="plugin_errors"} {loop="plugin_errors"}
<li>{$value}</li> <li>{$value}</li>

View file

@ -1,14 +1,14 @@
### Feeds options ### 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: Options:
- You can use `permalinks` in the feed URL to get permalink to Shaares instead of direct link to shaared URL. - 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. - 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/feed/atom?permalinks&nb=42`
- `https://my.shaarli.domain/?do=atom&permalinks&nb=all` - `https://my.shaarli.domain/feed/atom?permalinks&nb=all`
### RSS Feeds or Picture Wall for a specific search/tag ### RSS Feeds or Picture Wall for a specific search/tag
@ -23,6 +23,6 @@ For example, if you want to subscribe only to links tagged `photography`:
- The same method **also works for a full-text search** (_Search_ box) **and for the Picture Wall** (want to only see pictures about `nature`?) - 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/?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) ![](images/rss-filter-1.png) ![](images/rss-filter-2.png)

View file

@ -32,20 +32,20 @@ Here is a list :
``` ```
http://<replace_domain>/ http://<replace_domain>/
http://<replace_domain>/?nonope http://<replace_domain>/?nonope
http://<replace_domain>/?do=addlink http://<replace_domain>/admin/add-shaare
http://<replace_domain>/?do=changepasswd http://<replace_domain>/admin/password
http://<replace_domain>/?do=changetag http://<replace_domain>/admin/tags
http://<replace_domain>/?do=configure http://<replace_domain>/admin/configure
http://<replace_domain>/?do=tools http://<replace_domain>/admin/tools
http://<replace_domain>/?do=daily http://<replace_domain>/daily
http://<replace_domain>/?post http://<replace_domain>/admin/shaare
http://<replace_domain>/?do=export http://<replace_domain>/admin/export
http://<replace_domain>/?do=import http://<replace_domain>/admin/import
http://<replace_domain>/login http://<replace_domain>/login
http://<replace_domain>/?do=picwall http://<replace_domain>/picture-wall
http://<replace_domain>/?do=pluginadmin http://<replace_domain>/admin/plugins
http://<replace_domain>/?do=tagcloud http://<replace_domain>/tags/cloud
http://<replace_domain>/?do=taglist http://<replace_domain>/tags/list
``` ```
#### Improve existing translation #### Improve existing translation

File diff suppressed because it is too large Load diff

1951
index.php

File diff suppressed because it is too large Load diff

85
init.php Normal file
View file

@ -0,0 +1,85 @@
<?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");

View file

@ -5,7 +5,7 @@
* Adds the addlink input on the linklist page. * 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. * When linklist is displayed, add play videos to header's toolbar.
@ -16,11 +16,11 @@
*/ */
function hook_addlink_toolbar_render_header($data) 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( $form = array(
'attr' => array( 'attr' => array(
'method' => 'GET', 'method' => 'GET',
'action' => '', 'action' => $data['_BASE_PATH_'] . '/admin/shaare',
'name' => 'addform', 'name' => 'addform',
'class' => 'addform', 'class' => 'addform',
), ),

View file

@ -1,5 +1,5 @@
<span> <span>
<a href="https://web.archive.org/web/%s"> <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> </a>
</span> </span>

View file

@ -17,12 +17,13 @@
function hook_archiveorg_render_linklist($data) function hook_archiveorg_render_linklist($data)
{ {
$archive_html = file_get_contents(PluginManager::$PLUGINS_PATH . '/archiveorg/archiveorg.html'); $archive_html = file_get_contents(PluginManager::$PLUGINS_PATH . '/archiveorg/archiveorg.html');
$path = ($data['_BASE_PATH_'] ?? '') . '/' . PluginManager::$PLUGINS_PATH;
foreach ($data['links'] as &$value) { foreach ($data['links'] as &$value) {
if ($value['private'] && preg_match('/^\?[a-zA-Z0-9-_@]{6}($|&|#)/', $value['real_url'])) { if ($value['private'] && preg_match('/^\?[a-zA-Z0-9-_@]{6}($|&|#)/', $value['real_url'])) {
continue; 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; $value['link_plugin'][] = $archive;
} }

View file

@ -16,7 +16,7 @@
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\Plugin\PluginManager; 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. * In the footer hook, there is a working example of a translation extension for Shaarli.
@ -74,7 +74,7 @@ function demo_plugin_init($conf)
function hook_demo_plugin_render_header($data) function hook_demo_plugin_render_header($data)
{ {
// Only execute when linklist is rendered. // Only execute when linklist is rendered.
if ($data['_PAGE_'] == Router::$PAGE_LINKLIST) { if ($data['_PAGE_'] == TemplatePage::LINKLIST) {
// If loggedin // If loggedin
if ($data['_LOGGEDIN_'] === true) { if ($data['_LOGGEDIN_'] === true) {
/* /*
@ -118,7 +118,7 @@ function hook_demo_plugin_render_header($data)
$form = array( $form = array(
'attr' => array( 'attr' => array(
'method' => 'GET', 'method' => 'GET',
'action' => '?', 'action' => $data['_BASE_PATH_'] . '/',
'class' => 'addform', 'class' => 'addform',
), ),
'inputs' => array( 'inputs' => array(
@ -441,9 +441,9 @@ function hook_demo_plugin_delete_link($data)
function hook_demo_plugin_render_feed($data) function hook_demo_plugin_render_feed($data)
{ {
foreach ($data['links'] as &$link) { foreach ($data['links'] as &$link) {
if ($data['_PAGE_'] == Router::$PAGE_FEED_ATOM) { if ($data['_PAGE_'] == TemplatePage::FEED_ATOM) {
$link['description'] .= ' - ATOM Feed' ; $link['description'] .= ' - ATOM Feed' ;
} elseif ($data['_PAGE_'] == Router::$PAGE_FEED_RSS) { } elseif ($data['_PAGE_'] == TemplatePage::FEED_RSS) {
$link['description'] .= ' - RSS Feed'; $link['description'] .= ' - RSS Feed';
} }
} }

View file

@ -6,7 +6,7 @@
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\Plugin\PluginManager; use Shaarli\Plugin\PluginManager;
use Shaarli\Router; use Shaarli\Render\TemplatePage;
/** /**
* Display an error everywhere if the plugin is enabled without configuration. * Display an error everywhere if the plugin is enabled without configuration.
@ -76,7 +76,7 @@ function hook_isso_render_linklist($data, $conf)
*/ */
function hook_isso_render_includes($data) 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'; $data['css_files'][] = PluginManager::$PLUGINS_PATH . '/isso/isso.css';
} }

View file

@ -1,5 +0,0 @@
<span>
<a href="?%s#isso-thread">
<img class="linklist-plugin-icon" src="plugins/archiveorg/internetarchive.png" title="%s" alt="archive.org" />
</a>
</span>

View file

@ -7,7 +7,7 @@
*/ */
use Shaarli\Plugin\PluginManager; use Shaarli\Plugin\PluginManager;
use Shaarli\Router; use Shaarli\Render\TemplatePage;
/** /**
* When linklist is displayed, add play videos to header's toolbar. * When linklist is displayed, add play videos to header's toolbar.
@ -18,7 +18,7 @@
*/ */
function hook_playvideos_render_header($data) function hook_playvideos_render_header($data)
{ {
if ($data['_PAGE_'] == Router::$PAGE_LINKLIST) { if ($data['_PAGE_'] == TemplatePage::LINKLIST) {
$playvideo = array( $playvideo = array(
'attr' => array( 'attr' => array(
'href' => '#', 'href' => '#',
@ -42,7 +42,7 @@ function hook_playvideos_render_header($data)
*/ */
function hook_playvideos_render_footer($data) 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/jquery-1.11.2.min.js';
$data['js_files'][] = PluginManager::$PLUGINS_PATH . '/playvideos/youtube_playlist.js'; $data['js_files'][] = PluginManager::$PLUGINS_PATH . '/playvideos/youtube_playlist.js';
} }

View file

@ -13,7 +13,7 @@
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\Feed\FeedBuilder; use Shaarli\Feed\FeedBuilder;
use Shaarli\Plugin\PluginManager; use Shaarli\Plugin\PluginManager;
use Shaarli\Router; use Shaarli\Render\TemplatePage;
/** /**
* Plugin init function - set the hub to the default appspot one. * Plugin init function - set the hub to the default appspot one.
@ -41,7 +41,7 @@ function pubsubhubbub_init($conf)
*/ */
function hook_pubsubhubbub_render_feed($data, $conf) 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'); $template = file_get_contents(PluginManager::$PLUGINS_PATH . '/pubsubhubbub/hub.'. $feedType .'.xml');
$data['feed_plugins_header'][] = sprintf($template, $conf->get('plugins.PUBSUBHUB_URL')); $data['feed_plugins_header'][] = sprintf($template, $conf->get('plugins.PUBSUBHUB_URL'));
@ -60,8 +60,8 @@ function hook_pubsubhubbub_render_feed($data, $conf)
function hook_pubsubhubbub_save_link($data, $conf) function hook_pubsubhubbub_save_link($data, $conf)
{ {
$feeds = array( $feeds = array(
index_url($_SERVER) .'?do=atom', index_url($_SERVER) .'feed/atom',
index_url($_SERVER) .'?do=rss', index_url($_SERVER) .'feed/rss',
); );
$httpPost = function_exists('curl_version') ? false : 'nocurl_http_post'; $httpPost = function_exists('curl_version') ? false : 'nocurl_http_post';

View file

@ -6,7 +6,7 @@
*/ */
use Shaarli\Plugin\PluginManager; use Shaarli\Plugin\PluginManager;
use Shaarli\Router; use Shaarli\Render\TemplatePage;
/** /**
* Add qrcode icon to link_plugin when rendering linklist. * Add qrcode icon to link_plugin when rendering linklist.
@ -19,11 +19,12 @@ function hook_qrcode_render_linklist($data)
{ {
$qrcode_html = file_get_contents(PluginManager::$PLUGINS_PATH . '/qrcode/qrcode.html'); $qrcode_html = file_get_contents(PluginManager::$PLUGINS_PATH . '/qrcode/qrcode.html');
$path = ($data['_BASE_PATH_'] ?? '') . '/' . PluginManager::$PLUGINS_PATH;
foreach ($data['links'] as &$value) { foreach ($data['links'] as &$value) {
$qrcode = sprintf( $qrcode = sprintf(
$qrcode_html, $qrcode_html,
$value['url'], $value['url'],
PluginManager::$PLUGINS_PATH $path
); );
$value['link_plugin'][] = $qrcode; $value['link_plugin'][] = $qrcode;
} }
@ -40,7 +41,7 @@ function hook_qrcode_render_linklist($data)
*/ */
function hook_qrcode_render_footer($data) function hook_qrcode_render_footer($data)
{ {
if ($data['_PAGE_'] == Router::$PAGE_LINKLIST) { if ($data['_PAGE_'] == TemplatePage::LINKLIST) {
$data['js_files'][] = PluginManager::$PLUGINS_PATH . '/qrcode/shaarli-qrcode.js'; $data['js_files'][] = PluginManager::$PLUGINS_PATH . '/qrcode/shaarli-qrcode.js';
} }
@ -56,7 +57,7 @@ function hook_qrcode_render_footer($data)
*/ */
function hook_qrcode_render_includes($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'; $data['css_files'][] = PluginManager::$PLUGINS_PATH . '/qrcode/qrcode.css';
} }

View file

@ -34,8 +34,9 @@ function showQrCode(caller,loading)
{ {
if (!loading) // If javascript lib is still loading, do not append script to body. if (!loading) // If javascript lib is still loading, do not append script to body.
{ {
var basePath = document.querySelector('input[name="js_base_path"]').value;
var element = document.createElement("script"); var element = document.createElement("script");
element.src = "plugins/qrcode/qr-1.1.3.min.js"; element.src = basePath + "/plugins/qrcode/qr-1.1.3.min.js";
document.body.appendChild(element); document.body.appendChild(element);
} }
setTimeout(function() { showQrCode(caller,true);}, 200); // Retry in 200 milliseconds. setTimeout(function() { showQrCode(caller,true);}, 200); // Retry in 200 milliseconds.

View file

@ -21,7 +21,7 @@ The directory structure should look like:
To enable the plugin, you can either: 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). * add `wallabag` to your list of enabled plugins in `data/config.json.php` (`general.enabled_plugins` section).
### Configuration ### Configuration

View file

@ -45,12 +45,14 @@ function hook_wallabag_render_linklist($data, $conf)
$wallabagHtml = file_get_contents(PluginManager::$PLUGINS_PATH . '/wallabag/wallabag.html'); $wallabagHtml = file_get_contents(PluginManager::$PLUGINS_PATH . '/wallabag/wallabag.html');
$linkTitle = t('Save to wallabag'); $linkTitle = t('Save to wallabag');
$path = ($data['_BASE_PATH_'] ?? '') . '/' . PluginManager::$PLUGINS_PATH;
foreach ($data['links'] as &$value) { foreach ($data['links'] as &$value) {
$wallabag = sprintf( $wallabag = sprintf(
$wallabagHtml, $wallabagHtml,
$wallabagInstance->getWallabagUrl(), $wallabagInstance->getWallabagUrl(),
urlencode($value['url']), urlencode($value['url']),
PluginManager::$PLUGINS_PATH, $path,
$linkTitle $linkTitle
); );
$value['link_plugin'][] = $wallabag; $value['link_plugin'][] = $wallabag;

Some files were not shown because too many files have changed in this diff Show more