Merge pull request #1511 from ArthurHoaro/wip-slim-routing
This commit is contained in:
commit
af41d5ab5d
225 changed files with 12764 additions and 4036 deletions
|
@ -14,7 +14,7 @@ indent_size = 4
|
|||
indent_size = 2
|
||||
|
||||
[*.php]
|
||||
max_line_length = 100
|
||||
max_line_length = 120
|
||||
|
||||
[Dockerfile]
|
||||
max_line_length = 80
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
use Shaarli\Config\ConfigManager;
|
||||
use WebThumbnailer\Application\ConfigManager as WTConfigManager;
|
||||
use WebThumbnailer\Exception\WebThumbnailerException;
|
||||
use WebThumbnailer\WebThumbnailer;
|
||||
|
||||
/**
|
||||
|
@ -90,7 +89,7 @@ public function get($url)
|
|||
|
||||
try {
|
||||
return $this->wt->thumbnail($url);
|
||||
} catch (WebThumbnailerException $e) {
|
||||
} catch (\Throwable $e) {
|
||||
// Exceptions are only thrown in debug mode.
|
||||
error_log(get_class($e) . ': ' . $e->getMessage());
|
||||
}
|
||||
|
|
|
@ -87,10 +87,14 @@ function endsWith($haystack, $needle, $case = true)
|
|||
*
|
||||
* @param mixed $input Data to escape: a single string or an array of strings.
|
||||
*
|
||||
* @return string escaped.
|
||||
* @return string|array escaped.
|
||||
*/
|
||||
function escape($input)
|
||||
{
|
||||
if (null === $input) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_bool($input)) {
|
||||
return $input;
|
||||
}
|
||||
|
@ -294,15 +298,15 @@ function normalize_spaces($string)
|
|||
* Requires php-intl to display international datetimes,
|
||||
* otherwise default format '%c' will be returned.
|
||||
*
|
||||
* @param DateTime $date to format.
|
||||
* @param bool $time Displays time if true.
|
||||
* @param bool $intl Use international format if true.
|
||||
* @param DateTimeInterface $date to format.
|
||||
* @param bool $time Displays time if true.
|
||||
* @param bool $intl Use international format if true.
|
||||
*
|
||||
* @return bool|string Formatted date, or false if the input is invalid.
|
||||
*/
|
||||
function format_date($date, $time = true, $intl = true)
|
||||
{
|
||||
if (! $date instanceof DateTime) {
|
||||
if (! $date instanceof DateTimeInterface) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -71,7 +71,14 @@ public function __invoke($request, $response, $next)
|
|||
$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')
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -67,7 +67,7 @@ public static function formatLink($bookmark, $indexUrl)
|
|||
if (! $bookmark->isNote()) {
|
||||
$out['url'] = $bookmark->getUrl();
|
||||
} else {
|
||||
$out['url'] = $indexUrl . $bookmark->getUrl();
|
||||
$out['url'] = rtrim($indexUrl, '/') . '/' . ltrim($bookmark->getUrl(), '/');
|
||||
}
|
||||
$out['shorturl'] = $bookmark->getShortUrl();
|
||||
$out['title'] = $bookmark->getTitle();
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
namespace Shaarli\Bookmark;
|
||||
|
||||
use DateTime;
|
||||
use DateTimeInterface;
|
||||
use Shaarli\Bookmark\Exception\InvalidBookmarkException;
|
||||
|
||||
/**
|
||||
|
@ -36,16 +37,16 @@ class Bookmark
|
|||
/** @var array List of bookmark's tags */
|
||||
protected $tags;
|
||||
|
||||
/** @var string Thumbnail's URL - false if no thumbnail could be found */
|
||||
/** @var string|bool|null Thumbnail's URL - initialized at null, false if no thumbnail could be found */
|
||||
protected $thumbnail;
|
||||
|
||||
/** @var bool Set to true if the bookmark is set as sticky */
|
||||
protected $sticky;
|
||||
|
||||
/** @var DateTime Creation datetime */
|
||||
/** @var DateTimeInterface Creation datetime */
|
||||
protected $created;
|
||||
|
||||
/** @var DateTime Update datetime */
|
||||
/** @var DateTimeInterface datetime */
|
||||
protected $updated;
|
||||
|
||||
/** @var bool True if the bookmark can only be seen while logged in */
|
||||
|
@ -100,12 +101,12 @@ public function validate()
|
|||
|| ! is_int($this->id)
|
||||
|| empty($this->shortUrl)
|
||||
|| empty($this->created)
|
||||
|| ! $this->created instanceof DateTime
|
||||
|| ! $this->created instanceof DateTimeInterface
|
||||
) {
|
||||
throw new InvalidBookmarkException($this);
|
||||
}
|
||||
if (empty($this->url)) {
|
||||
$this->url = '?'. $this->shortUrl;
|
||||
$this->url = '/shaare/'. $this->shortUrl;
|
||||
}
|
||||
if (empty($this->title)) {
|
||||
$this->title = $this->url;
|
||||
|
@ -188,7 +189,7 @@ public function getDescription()
|
|||
/**
|
||||
* Get the Created.
|
||||
*
|
||||
* @return DateTime
|
||||
* @return DateTimeInterface
|
||||
*/
|
||||
public function getCreated()
|
||||
{
|
||||
|
@ -198,7 +199,7 @@ public function getCreated()
|
|||
/**
|
||||
* Get the Updated.
|
||||
*
|
||||
* @return DateTime
|
||||
* @return DateTimeInterface
|
||||
*/
|
||||
public function getUpdated()
|
||||
{
|
||||
|
@ -270,7 +271,7 @@ public function setDescription($description)
|
|||
* Set the Created.
|
||||
* Note: you shouldn't set this manually except for special cases (like bookmark import)
|
||||
*
|
||||
* @param DateTime $created
|
||||
* @param DateTimeInterface $created
|
||||
*
|
||||
* @return Bookmark
|
||||
*/
|
||||
|
@ -284,7 +285,7 @@ public function setCreated($created)
|
|||
/**
|
||||
* Set the Updated.
|
||||
*
|
||||
* @param DateTime $updated
|
||||
* @param DateTimeInterface $updated
|
||||
*
|
||||
* @return Bookmark
|
||||
*/
|
||||
|
@ -346,7 +347,7 @@ public function setTags($tags)
|
|||
/**
|
||||
* 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()
|
||||
{
|
||||
|
@ -356,7 +357,7 @@ public function getThumbnail()
|
|||
/**
|
||||
* Set the Thumbnail.
|
||||
*
|
||||
* @param string|bool $thumbnail
|
||||
* @param string|bool $thumbnail Thumbnail's URL - false if no thumbnail could be found
|
||||
*
|
||||
* @return Bookmark
|
||||
*/
|
||||
|
@ -405,7 +406,7 @@ public function getTagsString()
|
|||
public function isNote()
|
||||
{
|
||||
// We check empty value to get a valid result if the link has not been saved yet
|
||||
return empty($this->url) || $this->url[0] === '?';
|
||||
return empty($this->url) || startsWith($this->url, '/shaare/') || $this->url[0] === '?';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -6,12 +6,14 @@
|
|||
|
||||
use Exception;
|
||||
use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
|
||||
use Shaarli\Bookmark\Exception\DatastoreNotInitializedException;
|
||||
use Shaarli\Bookmark\Exception\EmptyDataStoreException;
|
||||
use Shaarli\Config\ConfigManager;
|
||||
use Shaarli\Formatter\BookmarkMarkdownFormatter;
|
||||
use Shaarli\History;
|
||||
use Shaarli\Legacy\LegacyLinkDB;
|
||||
use Shaarli\Legacy\LegacyUpdater;
|
||||
use Shaarli\Render\PageCacheManager;
|
||||
use Shaarli\Updater\UpdaterUtils;
|
||||
|
||||
/**
|
||||
|
@ -39,6 +41,9 @@ class BookmarkFileService implements BookmarkServiceInterface
|
|||
/** @var History instance */
|
||||
protected $history;
|
||||
|
||||
/** @var PageCacheManager instance */
|
||||
protected $pageCacheManager;
|
||||
|
||||
/** @var bool true for logged in users. Default value to retrieve private bookmarks. */
|
||||
protected $isLoggedIn;
|
||||
|
||||
|
@ -49,6 +54,7 @@ public function __construct(ConfigManager $conf, History $history, $isLoggedIn)
|
|||
{
|
||||
$this->conf = $conf;
|
||||
$this->history = $history;
|
||||
$this->pageCacheManager = new PageCacheManager($this->conf->get('resource.page_cache'), $isLoggedIn);
|
||||
$this->bookmarksIO = new BookmarkIO($this->conf);
|
||||
$this->isLoggedIn = $isLoggedIn;
|
||||
|
||||
|
@ -57,10 +63,16 @@ public function __construct(ConfigManager $conf, History $history, $isLoggedIn)
|
|||
} else {
|
||||
try {
|
||||
$this->bookmarks = $this->bookmarksIO->read();
|
||||
} catch (EmptyDataStoreException $e) {
|
||||
} catch (EmptyDataStoreException|DatastoreNotInitializedException $e) {
|
||||
$this->bookmarks = new BookmarkArray();
|
||||
if ($isLoggedIn) {
|
||||
$this->save();
|
||||
|
||||
if ($this->isLoggedIn) {
|
||||
// Datastore file does not exists, we initialize it with default bookmarks.
|
||||
if ($e instanceof DatastoreNotInitializedException) {
|
||||
$this->initialize();
|
||||
} else {
|
||||
$this->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -88,7 +100,7 @@ public function findByHash($hash)
|
|||
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)
|
||||
{
|
||||
if ($this->isLoggedIn !== true) {
|
||||
if (true !== $this->isLoggedIn) {
|
||||
throw new Exception(t('You\'re not authorized to alter the datastore'));
|
||||
}
|
||||
if (! $bookmark instanceof Bookmark) {
|
||||
|
@ -174,7 +186,7 @@ public function set($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'));
|
||||
}
|
||||
if (! $bookmark instanceof Bookmark) {
|
||||
|
@ -199,7 +211,7 @@ public function add($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'));
|
||||
}
|
||||
if (! $bookmark instanceof Bookmark) {
|
||||
|
@ -216,7 +228,7 @@ public function addOrSet($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'));
|
||||
}
|
||||
if (! $bookmark instanceof Bookmark) {
|
||||
|
@ -269,13 +281,14 @@ public function count($visibility = null)
|
|||
*/
|
||||
public function save()
|
||||
{
|
||||
if (!$this->isLoggedIn) {
|
||||
if (true !== $this->isLoggedIn) {
|
||||
// TODO: raise an Exception instead
|
||||
die('You are not authorized to change the database.');
|
||||
}
|
||||
|
||||
$this->bookmarks->reorder();
|
||||
$this->bookmarksIO->write($this->bookmarks);
|
||||
invalidateCaches($this->conf->get('resource.page_cache'));
|
||||
$this->pageCacheManager->invalidateCaches();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -291,6 +304,7 @@ public function bookmarksCountPerTag($filteringTags = [], $visibility = null)
|
|||
if (empty($tag)
|
||||
|| (! $this->isLoggedIn && startsWith($tag, '.'))
|
||||
|| $tag === BookmarkMarkdownFormatter::NO_MD_TAG
|
||||
|| in_array($tag, $filteringTags, true)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
@ -349,6 +363,10 @@ public function initialize()
|
|||
{
|
||||
$initializer = new BookmarkInitializer($this);
|
||||
$initializer->initialize();
|
||||
|
||||
if (true === $this->isLoggedIn) {
|
||||
$this->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -436,7 +436,7 @@ public function filterDay($day)
|
|||
throw new Exception('Invalid date format');
|
||||
}
|
||||
|
||||
$filtered = array();
|
||||
$filtered = [];
|
||||
foreach ($this->bookmarks as $key => $l) {
|
||||
if ($l->getCreated()->format('Ymd') == $day) {
|
||||
$filtered[$key] = $l;
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Shaarli\Bookmark;
|
||||
|
||||
use Shaarli\Bookmark\Exception\DatastoreNotInitializedException;
|
||||
use Shaarli\Bookmark\Exception\EmptyDataStoreException;
|
||||
use Shaarli\Bookmark\Exception\NotWritableDataStoreException;
|
||||
use Shaarli\Config\ConfigManager;
|
||||
|
@ -52,13 +53,14 @@ public function __construct($conf)
|
|||
*
|
||||
* @return BookmarkArray instance
|
||||
*
|
||||
* @throws NotWritableDataStoreException Data couldn't be loaded
|
||||
* @throws EmptyDataStoreException Datastore doesn't exist
|
||||
* @throws NotWritableDataStoreException Data couldn't be loaded
|
||||
* @throws EmptyDataStoreException Datastore file exists but does not contain any bookmark
|
||||
* @throws DatastoreNotInitializedException File does not exists
|
||||
*/
|
||||
public function read()
|
||||
{
|
||||
if (! file_exists($this->datastore)) {
|
||||
throw new EmptyDataStoreException();
|
||||
throw new DatastoreNotInitializedException();
|
||||
}
|
||||
|
||||
if (!is_writable($this->datastore)) {
|
||||
|
@ -102,7 +104,5 @@ public function write($links)
|
|||
$this->datastore,
|
||||
self::$phpPrefix.base64_encode(gzdeflate(serialize($links))).self::$phpSuffix
|
||||
);
|
||||
|
||||
invalidateCaches($this->conf->get('resource.page_cache'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,8 +6,7 @@
|
|||
* Class BookmarkInitializer
|
||||
*
|
||||
* This class is used to initialized default bookmarks after a fresh install of Shaarli.
|
||||
* It is no longer call when the data store is empty,
|
||||
* because user might want to delete default bookmarks after the install.
|
||||
* It should be only called if the datastore file does not exist(users might want to delete the default bookmarks).
|
||||
*
|
||||
* To prevent data corruption, it does not overwrite existing bookmarks,
|
||||
* even though there should not be any.
|
||||
|
@ -36,11 +35,11 @@ public function initialize()
|
|||
{
|
||||
$bookmark = new Bookmark();
|
||||
$bookmark->setTitle(t('My secret stuff... - Pastebin.com'));
|
||||
$bookmark->setUrl('http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=', []);
|
||||
$bookmark->setUrl('http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=');
|
||||
$bookmark->setDescription(t('Shhhh! I\'m a private link only YOU can see. You can delete me too.'));
|
||||
$bookmark->setTagsString('secretstuff');
|
||||
$bookmark->setPrivate(true);
|
||||
$this->bookmarkService->add($bookmark);
|
||||
$this->bookmarkService->add($bookmark, false);
|
||||
|
||||
$bookmark = new Bookmark();
|
||||
$bookmark->setTitle(t('The personal, minimalist, super-fast, database free, bookmarking service'));
|
||||
|
@ -54,6 +53,6 @@ public function initialize()
|
|||
You use the community supported version of the original Shaarli project, by Sebastien Sauvage.'
|
||||
));
|
||||
$bookmark->setTagsString('opensource software');
|
||||
$this->bookmarkService->add($bookmark);
|
||||
$this->bookmarkService->add($bookmark, false);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
|
||||
use Shaarli\Bookmark\Exception\NotWritableDataStoreException;
|
||||
use Shaarli\Config\ConfigManager;
|
||||
use Shaarli\Exceptions\IOException;
|
||||
use Shaarli\History;
|
||||
|
||||
/**
|
||||
|
|
|
@ -2,112 +2,6 @@
|
|||
|
||||
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.
|
||||
*
|
||||
|
@ -220,7 +114,7 @@ function hashtag_autolink($description, $indexUrl = '')
|
|||
* \p{Mn} - any non marking space (accents, umlauts, etc)
|
||||
*/
|
||||
$regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui';
|
||||
$replacement = '$1<a href="'. $indexUrl .'?addtag=$2" title="Hashtag $2">#$2</a>';
|
||||
$replacement = '$1<a href="'. $indexUrl .'./add-tag/$2" title="Hashtag $2">#$2</a>';
|
||||
return preg_replace($regex, $replacement, $description);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shaarli\Bookmark\Exception;
|
||||
|
||||
class DatastoreNotInitializedException extends \Exception
|
||||
{
|
||||
|
||||
}
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
use Shaarli\Config\Exception\MissingFieldConfigException;
|
||||
use Shaarli\Config\Exception\UnauthorizedConfigException;
|
||||
use Shaarli\Thumbnailer;
|
||||
|
||||
/**
|
||||
* Class ConfigManager
|
||||
|
@ -361,7 +362,7 @@ protected function setDefaultValues()
|
|||
$this->setEmpty('security.open_shaarli', false);
|
||||
$this->setEmpty('security.allowed_protocols', ['ftp', 'ftps', 'magnet']);
|
||||
|
||||
$this->setEmpty('general.header_link', '?');
|
||||
$this->setEmpty('general.header_link', '/');
|
||||
$this->setEmpty('general.links_per_page', 20);
|
||||
$this->setEmpty('general.enabled_plugins', self::$DEFAULT_PLUGINS);
|
||||
$this->setEmpty('general.default_note_title', 'Note: ');
|
||||
|
@ -381,6 +382,7 @@ protected function setDefaultValues()
|
|||
// default state of the 'remember me' checkbox of the login form
|
||||
$this->setEmpty('privacy.remember_user_default', true);
|
||||
|
||||
$this->setEmpty('thumbnails.mode', Thumbnailer::MODE_ALL);
|
||||
$this->setEmpty('thumbnails.width', '125');
|
||||
$this->setEmpty('thumbnails.height', '90');
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<?php
|
||||
|
||||
use Shaarli\Config\Exception\PluginConfigOrderException;
|
||||
use Shaarli\Plugin\PluginManager;
|
||||
|
||||
/**
|
||||
* Plugin configuration helper functions.
|
||||
|
@ -19,6 +20,20 @@
|
|||
*/
|
||||
function save_plugin_config($formData)
|
||||
{
|
||||
// We can only save existing plugins
|
||||
$directories = str_replace(
|
||||
PluginManager::$PLUGINS_PATH . '/',
|
||||
'',
|
||||
glob(PluginManager::$PLUGINS_PATH . '/*')
|
||||
);
|
||||
$formData = array_filter(
|
||||
$formData,
|
||||
function ($value, string $key) use ($directories) {
|
||||
return startsWith($key, 'order') || in_array($key, $directories);
|
||||
},
|
||||
ARRAY_FILTER_USE_BOTH
|
||||
);
|
||||
|
||||
// Make sure there are no duplicates in orders.
|
||||
if (!validate_plugin_order($formData)) {
|
||||
throw new PluginConfigOrderException();
|
||||
|
@ -69,7 +84,7 @@ function validate_plugin_order($formData)
|
|||
$orders = array();
|
||||
foreach ($formData as $key => $value) {
|
||||
// No duplicate order allowed.
|
||||
if (in_array($value, $orders)) {
|
||||
if (in_array($value, $orders, true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -7,11 +7,21 @@
|
|||
use Shaarli\Bookmark\BookmarkFileService;
|
||||
use Shaarli\Bookmark\BookmarkServiceInterface;
|
||||
use Shaarli\Config\ConfigManager;
|
||||
use Shaarli\Feed\FeedBuilder;
|
||||
use Shaarli\Formatter\FormatterFactory;
|
||||
use Shaarli\Front\Controller\Visitor\ErrorController;
|
||||
use Shaarli\History;
|
||||
use Shaarli\Http\HttpAccess;
|
||||
use Shaarli\Netscape\NetscapeBookmarkUtils;
|
||||
use Shaarli\Plugin\PluginManager;
|
||||
use Shaarli\Render\PageBuilder;
|
||||
use Shaarli\Render\PageCacheManager;
|
||||
use Shaarli\Security\CookieManager;
|
||||
use Shaarli\Security\LoginManager;
|
||||
use Shaarli\Security\SessionManager;
|
||||
use Shaarli\Thumbnailer;
|
||||
use Shaarli\Updater\Updater;
|
||||
use Shaarli\Updater\UpdaterUtils;
|
||||
|
||||
/**
|
||||
* Class ContainerBuilder
|
||||
|
@ -30,22 +40,37 @@ class ContainerBuilder
|
|||
/** @var SessionManager */
|
||||
protected $session;
|
||||
|
||||
/** @var CookieManager */
|
||||
protected $cookieManager;
|
||||
|
||||
/** @var LoginManager */
|
||||
protected $login;
|
||||
|
||||
public function __construct(ConfigManager $conf, SessionManager $session, LoginManager $login)
|
||||
{
|
||||
/** @var string|null */
|
||||
protected $basePath = null;
|
||||
|
||||
public function __construct(
|
||||
ConfigManager $conf,
|
||||
SessionManager $session,
|
||||
CookieManager $cookieManager,
|
||||
LoginManager $login
|
||||
) {
|
||||
$this->conf = $conf;
|
||||
$this->session = $session;
|
||||
$this->login = $login;
|
||||
$this->cookieManager = $cookieManager;
|
||||
}
|
||||
|
||||
public function build(): ShaarliContainer
|
||||
{
|
||||
$container = new ShaarliContainer();
|
||||
|
||||
$container['conf'] = $this->conf;
|
||||
$container['sessionManager'] = $this->session;
|
||||
$container['cookieManager'] = $this->cookieManager;
|
||||
$container['loginManager'] = $this->login;
|
||||
$container['basePath'] = $this->basePath;
|
||||
|
||||
$container['plugins'] = function (ShaarliContainer $container): PluginManager {
|
||||
return new PluginManager($container->conf);
|
||||
};
|
||||
|
@ -73,7 +98,59 @@ public function build(): ShaarliContainer
|
|||
};
|
||||
|
||||
$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;
|
||||
|
|
|
@ -4,25 +4,45 @@
|
|||
|
||||
namespace Shaarli\Container;
|
||||
|
||||
use http\Cookie;
|
||||
use Shaarli\Bookmark\BookmarkServiceInterface;
|
||||
use Shaarli\Config\ConfigManager;
|
||||
use Shaarli\Feed\FeedBuilder;
|
||||
use Shaarli\Formatter\FormatterFactory;
|
||||
use Shaarli\History;
|
||||
use Shaarli\Http\HttpAccess;
|
||||
use Shaarli\Netscape\NetscapeBookmarkUtils;
|
||||
use Shaarli\Plugin\PluginManager;
|
||||
use Shaarli\Render\PageBuilder;
|
||||
use Shaarli\Render\PageCacheManager;
|
||||
use Shaarli\Security\CookieManager;
|
||||
use Shaarli\Security\LoginManager;
|
||||
use Shaarli\Security\SessionManager;
|
||||
use Shaarli\Thumbnailer;
|
||||
use Shaarli\Updater\Updater;
|
||||
use Slim\Container;
|
||||
|
||||
/**
|
||||
* Extension of Slim container to document the injected objects.
|
||||
*
|
||||
* @property ConfigManager $conf
|
||||
* @property SessionManager $sessionManager
|
||||
* @property LoginManager $loginManager
|
||||
* @property History $history
|
||||
* @property string $basePath Shaarli's instance base path (e.g. `/shaarli/`)
|
||||
* @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 PageCacheManager $pageCacheManager
|
||||
* @property PluginManager $pluginManager
|
||||
* @property SessionManager $sessionManager
|
||||
* @property Thumbnailer $thumbnailer
|
||||
* @property Updater $updater
|
||||
*/
|
||||
class ShaarliContainer extends Container
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -43,21 +43,9 @@ class FeedBuilder
|
|||
*/
|
||||
protected $formatter;
|
||||
|
||||
/**
|
||||
* @var string RSS or ATOM feed.
|
||||
*/
|
||||
protected $feedType;
|
||||
|
||||
/**
|
||||
* @var array $_SERVER
|
||||
*/
|
||||
/** @var mixed[] $_SERVER */
|
||||
protected $serverInfo;
|
||||
|
||||
/**
|
||||
* @var array $_GET
|
||||
*/
|
||||
protected $userInput;
|
||||
|
||||
/**
|
||||
* @var boolean True if the user is currently logged in, false otherwise.
|
||||
*/
|
||||
|
@ -77,7 +65,6 @@ class FeedBuilder
|
|||
* @var string server locale.
|
||||
*/
|
||||
protected $locale;
|
||||
|
||||
/**
|
||||
* @var DateTime Latest item date.
|
||||
*/
|
||||
|
@ -88,37 +75,36 @@ class FeedBuilder
|
|||
*
|
||||
* @param BookmarkServiceInterface $linkDB LinkDB instance.
|
||||
* @param BookmarkFormatter $formatter instance.
|
||||
* @param string $feedType Type of feed.
|
||||
* @param array $serverInfo $_SERVER.
|
||||
* @param array $userInput $_GET.
|
||||
* @param boolean $isLoggedIn True if the user is currently logged in, false otherwise.
|
||||
*/
|
||||
public function __construct($linkDB, $formatter, $feedType, $serverInfo, $userInput, $isLoggedIn)
|
||||
public function __construct($linkDB, $formatter, $serverInfo, $isLoggedIn)
|
||||
{
|
||||
$this->linkDB = $linkDB;
|
||||
$this->formatter = $formatter;
|
||||
$this->feedType = $feedType;
|
||||
$this->serverInfo = $serverInfo;
|
||||
$this->userInput = $userInput;
|
||||
$this->isLoggedIn = $isLoggedIn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build data for feed templates.
|
||||
*
|
||||
* @param string $feedType Type of feed (RSS/ATOM).
|
||||
* @param array $userInput $_GET.
|
||||
*
|
||||
* @return array Formatted data for feeds templates.
|
||||
*/
|
||||
public function buildData()
|
||||
public function buildData(string $feedType, ?array $userInput)
|
||||
{
|
||||
// Search for untagged bookmarks
|
||||
if (isset($this->userInput['searchtags']) && empty($this->userInput['searchtags'])) {
|
||||
$this->userInput['searchtags'] = false;
|
||||
if (isset($this->userInput['searchtags']) && empty($userInput['searchtags'])) {
|
||||
$userInput['searchtags'] = false;
|
||||
}
|
||||
|
||||
// Optionally filter the results:
|
||||
$linksToDisplay = $this->linkDB->search($this->userInput);
|
||||
$linksToDisplay = $this->linkDB->search($userInput);
|
||||
|
||||
$nblinksToDisplay = $this->getNbLinks(count($linksToDisplay));
|
||||
$nblinksToDisplay = $this->getNbLinks(count($linksToDisplay), $userInput);
|
||||
|
||||
// Can't use array_keys() because $link is a LinkDB instance and not a real array.
|
||||
$keys = array();
|
||||
|
@ -130,11 +116,11 @@ public function buildData()
|
|||
$this->formatter->addContextData('index_url', $pageaddr);
|
||||
$linkDisplayed = array();
|
||||
for ($i = 0; $i < $nblinksToDisplay && $i < count($keys); $i++) {
|
||||
$linkDisplayed[$keys[$i]] = $this->buildItem($linksToDisplay[$keys[$i]], $pageaddr);
|
||||
$linkDisplayed[$keys[$i]] = $this->buildItem($feedType, $linksToDisplay[$keys[$i]], $pageaddr);
|
||||
}
|
||||
|
||||
$data['language'] = $this->getTypeLanguage();
|
||||
$data['last_update'] = $this->getLatestDateFormatted();
|
||||
$data['language'] = $this->getTypeLanguage($feedType);
|
||||
$data['last_update'] = $this->getLatestDateFormatted($feedType);
|
||||
$data['show_dates'] = !$this->hideDates || $this->isLoggedIn;
|
||||
// Remove leading slash from REQUEST_URI.
|
||||
$data['self_link'] = escape(server_url($this->serverInfo))
|
||||
|
@ -146,45 +132,6 @@ public function buildData()
|
|||
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>— ' . $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.
|
||||
*
|
||||
|
@ -215,22 +162,64 @@ public function setLocale($locale)
|
|||
$this->locale = strtolower($locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a feed item (one per shaare).
|
||||
*
|
||||
* @param string $feedType Type of feed (RSS/ATOM).
|
||||
* @param Bookmark $link Single link array extracted from LinkDB.
|
||||
* @param string $pageaddr Index URL.
|
||||
*
|
||||
* @return array Link array with feed attributes.
|
||||
*/
|
||||
protected function buildItem(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>— ' . $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:
|
||||
*
|
||||
* - RSS format: en-us (default: 'en-en').
|
||||
* - ATOM format: fr (default: 'en').
|
||||
*
|
||||
* @param string $feedType Type of feed (RSS/ATOM).
|
||||
*
|
||||
* @return string The language.
|
||||
*/
|
||||
public function getTypeLanguage()
|
||||
protected function getTypeLanguage(string $feedType)
|
||||
{
|
||||
// Use the locale do define the language, if available.
|
||||
if (!empty($this->locale) && preg_match('/^\w{2}[_\-]\w{2}/', $this->locale)) {
|
||||
$length = ($this->feedType === self::$FEED_RSS) ? 5 : 2;
|
||||
$length = ($feedType === self::$FEED_RSS) ? 5 : 2;
|
||||
return str_replace('_', '-', substr($this->locale, 0, $length));
|
||||
}
|
||||
return ($this->feedType === self::$FEED_RSS) ? 'en-en' : 'en';
|
||||
return ($feedType === self::$FEED_RSS) ? 'en-en' : 'en';
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -238,32 +227,35 @@ public function getTypeLanguage()
|
|||
*
|
||||
* Return an empty string if invalid DateTime is passed.
|
||||
*
|
||||
* @param string $feedType Type of feed (RSS/ATOM).
|
||||
*
|
||||
* @return string Formatted date.
|
||||
*/
|
||||
protected function getLatestDateFormatted()
|
||||
protected function getLatestDateFormatted(string $feedType)
|
||||
{
|
||||
if (empty($this->latestDate) || !$this->latestDate instanceof DateTime) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$type = ($this->feedType == self::$FEED_RSS) ? DateTime::RSS : DateTime::ATOM;
|
||||
$type = ($feedType == self::$FEED_RSS) ? DateTime::RSS : DateTime::ATOM;
|
||||
return $this->latestDate->format($type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ISO date from DateTime according to feed type.
|
||||
*
|
||||
* @param string $feedType Type of feed (RSS/ATOM).
|
||||
* @param DateTime $date Date to format.
|
||||
* @param string|bool $format Force format.
|
||||
*
|
||||
* @return string Formatted date.
|
||||
*/
|
||||
protected function getIsoDate(DateTime $date, $format = false)
|
||||
protected function getIsoDate(string $feedType, DateTime $date, $format = false)
|
||||
{
|
||||
if ($format !== false) {
|
||||
return $date->format($format);
|
||||
}
|
||||
if ($this->feedType == self::$FEED_RSS) {
|
||||
if ($feedType == self::$FEED_RSS) {
|
||||
return $date->format(DateTime::RSS);
|
||||
}
|
||||
return $date->format(DateTime::ATOM);
|
||||
|
@ -275,21 +267,22 @@ protected function getIsoDate(DateTime $date, $format = false)
|
|||
* If 'nb' not set or invalid, default value: $DEFAULT_NB_LINKS.
|
||||
* If 'nb' is set to 'all', display all filtered bookmarks (max parameter).
|
||||
*
|
||||
* @param int $max maximum number of bookmarks to display.
|
||||
* @param int $max maximum number of bookmarks to display.
|
||||
* @param array $userInput $_GET.
|
||||
*
|
||||
* @return int number of bookmarks to display.
|
||||
*/
|
||||
public function getNbLinks($max)
|
||||
protected function getNbLinks($max, ?array $userInput)
|
||||
{
|
||||
if (empty($this->userInput['nb'])) {
|
||||
if (empty($userInput['nb'])) {
|
||||
return self::$DEFAULT_NB_LINKS;
|
||||
}
|
||||
|
||||
if ($this->userInput['nb'] == 'all') {
|
||||
if ($userInput['nb'] == 'all') {
|
||||
return $max;
|
||||
}
|
||||
|
||||
$intNb = intval($this->userInput['nb']);
|
||||
$intNb = intval($userInput['nb']);
|
||||
if (!is_int($intNb) || $intNb == 0) {
|
||||
return self::$DEFAULT_NB_LINKS;
|
||||
}
|
||||
|
|
|
@ -50,11 +50,10 @@ public function formatTagString($bookmark)
|
|||
*/
|
||||
public function formatUrl($bookmark)
|
||||
{
|
||||
if (! empty($this->contextData['index_url']) && (
|
||||
startsWith($bookmark->getUrl(), '?') || startsWith($bookmark->getUrl(), '/')
|
||||
)) {
|
||||
return $this->contextData['index_url'] . escape($bookmark->getUrl());
|
||||
if ($bookmark->isNote() && isset($this->contextData['index_url'])) {
|
||||
return rtrim($this->contextData['index_url'], '/') . '/' . escape(ltrim($bookmark->getUrl(), '/'));
|
||||
}
|
||||
|
||||
return escape($bookmark->getUrl());
|
||||
}
|
||||
|
||||
|
@ -63,11 +62,18 @@ public function formatUrl($bookmark)
|
|||
*/
|
||||
protected function formatRealUrl($bookmark)
|
||||
{
|
||||
if (! empty($this->contextData['index_url']) && (
|
||||
startsWith($bookmark->getUrl(), '?') || startsWith($bookmark->getUrl(), '/')
|
||||
)) {
|
||||
return $this->contextData['index_url'] . escape($bookmark->getUrl());
|
||||
if ($bookmark->isNote()) {
|
||||
if (isset($this->contextData['index_url'])) {
|
||||
$prefix = rtrim($this->contextData['index_url'], '/') . '/';
|
||||
}
|
||||
|
||||
if (isset($this->contextData['base_path'])) {
|
||||
$prefix = rtrim($this->contextData['base_path'], '/') . '/';
|
||||
}
|
||||
|
||||
return escape($prefix ?? '') . escape(ltrim($bookmark->getUrl(), '/'));
|
||||
}
|
||||
|
||||
return escape($bookmark->getUrl());
|
||||
}
|
||||
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
namespace Shaarli\Formatter;
|
||||
|
||||
use DateTime;
|
||||
use Shaarli\Config\ConfigManager;
|
||||
use Shaarli\Bookmark\Bookmark;
|
||||
use Shaarli\Config\ConfigManager;
|
||||
|
||||
/**
|
||||
* Class BookmarkFormatter
|
||||
|
@ -80,6 +80,8 @@ public function format($bookmark)
|
|||
public function addContextData($key, $value)
|
||||
{
|
||||
$this->contextData[$key] = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -128,7 +130,7 @@ protected function formatUrl($bookmark)
|
|||
*/
|
||||
protected function formatRealUrl($bookmark)
|
||||
{
|
||||
return $bookmark->getUrl();
|
||||
return $this->formatUrl($bookmark);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -114,7 +114,7 @@ function ($match) use ($allowedProtocols, $indexUrl) {
|
|||
|
||||
/**
|
||||
* Replace hashtag in Markdown links format
|
||||
* E.g. `#hashtag` becomes `[#hashtag](?addtag=hashtag)`
|
||||
* E.g. `#hashtag` becomes `[#hashtag](./add-tag/hashtag)`
|
||||
* It includes the index URL if specified.
|
||||
*
|
||||
* @param string $description
|
||||
|
@ -133,7 +133,7 @@ protected function formatHashTags($description)
|
|||
* \p{Mn} - any non marking space (accents, umlauts, etc)
|
||||
*/
|
||||
$regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui';
|
||||
$replacement = '$1[#$2]('. $indexUrl .'?addtag=$2)';
|
||||
$replacement = '$1[#$2]('. $indexUrl .'./add-tag/$2)';
|
||||
|
||||
$descriptionLines = explode(PHP_EOL, $description);
|
||||
$descriptionOut = '';
|
||||
|
|
|
@ -38,7 +38,7 @@ public function __construct(ConfigManager $conf, bool $isLoggedIn)
|
|||
*
|
||||
* @return BookmarkFormatter instance.
|
||||
*/
|
||||
public function getFormatter(string $type = null)
|
||||
public function getFormatter(string $type = null): BookmarkFormatter
|
||||
{
|
||||
$type = $type ? $type : $this->conf->get('formatter', 'default');
|
||||
$className = '\\Shaarli\\Formatter\\Bookmark'. ucfirst($type) .'Formatter';
|
||||
|
|
27
application/front/ShaarliAdminMiddleware.php
Normal file
27
application/front/ShaarliAdminMiddleware.php
Normal 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);
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
namespace Shaarli\Front;
|
||||
|
||||
use Shaarli\Container\ShaarliContainer;
|
||||
use Shaarli\Front\Exception\ShaarliException;
|
||||
use Shaarli\Front\Exception\UnauthorizedException;
|
||||
use Slim\Http\Request;
|
||||
use Slim\Http\Response;
|
||||
|
||||
|
@ -24,6 +24,8 @@ public function __construct(ShaarliContainer $container)
|
|||
|
||||
/**
|
||||
* Middleware execution:
|
||||
* - run updates
|
||||
* - if not logged in open shaarli, redirect to login
|
||||
* - execute the controller
|
||||
* - return the response
|
||||
*
|
||||
|
@ -35,23 +37,78 @@ public function __construct(ShaarliContainer $container)
|
|||
*
|
||||
* @return Response response.
|
||||
*/
|
||||
public function __invoke(Request $request, Response $response, callable $next)
|
||||
public function __invoke(Request $request, Response $response, callable $next): Response
|
||||
{
|
||||
$this->initBasePath($request);
|
||||
|
||||
try {
|
||||
$response = $next($request, $response);
|
||||
} catch (ShaarliException $e) {
|
||||
$this->container->pageBuilder->assign('message', $e->getMessage());
|
||||
if ($this->container->conf->get('dev.debug', false)) {
|
||||
$this->container->pageBuilder->assign(
|
||||
'stacktrace',
|
||||
nl2br(get_class($this) .': '. $e->getTraceAsString())
|
||||
);
|
||||
if (!is_file($this->container->conf->getConfigFileExt())
|
||||
&& !in_array($next->getName(), ['displayInstall', 'saveInstall'], true)
|
||||
) {
|
||||
return $response->withRedirect($this->container->basePath . '/install');
|
||||
}
|
||||
|
||||
$response = $response->withStatus($e->getCode());
|
||||
$response = $response->write($this->container->pageBuilder->render('error'));
|
||||
$this->runUpdates();
|
||||
$this->checkOpenShaarli($request, $response, $next);
|
||||
|
||||
return $next($request, $response);
|
||||
} catch (UnauthorizedException $e) {
|
||||
$returnUrl = urlencode($this->container->environment['REQUEST_URI']);
|
||||
|
||||
return $response->withRedirect($this->container->basePath . '/login?returnurl=' . $returnUrl);
|
||||
}
|
||||
// Other exceptions are handled by ErrorController
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the updater for every requests processed while logged in.
|
||||
*/
|
||||
protected function runUpdates(): void
|
||||
{
|
||||
if ($this->container->loginManager->isLoggedIn() !== true) {
|
||||
return;
|
||||
}
|
||||
|
||||
return $response;
|
||||
$this->container->updater->setBasePath($this->container->basePath);
|
||||
$newUpdates = $this->container->updater->update();
|
||||
if (!empty($newUpdates)) {
|
||||
$this->container->updater->writeUpdates(
|
||||
$this->container->conf->get('resource.updates'),
|
||||
$this->container->updater->getDoneUpdates()
|
||||
);
|
||||
|
||||
$this->container->pageCacheManager->invalidateCaches();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Access is denied to most pages with `hide_public_links` + `force_login` settings.
|
||||
*/
|
||||
protected function checkOpenShaarli(Request $request, Response $response, callable $next): bool
|
||||
{
|
||||
if (// if the user isn't logged in
|
||||
!$this->container->loginManager->isLoggedIn()
|
||||
// and Shaarli doesn't have public content...
|
||||
&& $this->container->conf->get('privacy.hide_public_links')
|
||||
// and is configured to enforce the login
|
||||
&& $this->container->conf->get('privacy.force_login')
|
||||
// and the current page isn't already the login page
|
||||
// and the user is not requesting a feed (which would lead to a different content-type as expected)
|
||||
&& !in_array($next->getName(), ['login', 'atom', 'rss'], true)
|
||||
) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
return 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(), '/');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
126
application/front/controller/admin/ConfigureController.php
Normal file
126
application/front/controller/admin/ConfigureController.php
Normal 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');
|
||||
}
|
||||
}
|
80
application/front/controller/admin/ExportController.php
Normal file
80
application/front/controller/admin/ExportController.php
Normal 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));
|
||||
}
|
||||
}
|
82
application/front/controller/admin/ImportController.php
Normal file
82
application/front/controller/admin/ImportController.php
Normal 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');
|
||||
}
|
||||
}
|
33
application/front/controller/admin/LogoutController.php
Normal file
33
application/front/controller/admin/LogoutController.php
Normal 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, '/');
|
||||
}
|
||||
}
|
371
application/front/controller/admin/ManageShaareController.php
Normal file
371
application/front/controller/admin/ManageShaareController.php
Normal 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));
|
||||
}
|
||||
}
|
88
application/front/controller/admin/ManageTagController.php
Normal file
88
application/front/controller/admin/ManageTagController.php
Normal 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);
|
||||
}
|
||||
}
|
101
application/front/controller/admin/PasswordController.php
Normal file
101
application/front/controller/admin/PasswordController.php
Normal 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));
|
||||
}
|
||||
}
|
84
application/front/controller/admin/PluginsController.php
Normal file
84
application/front/controller/admin/PluginsController.php
Normal 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');
|
||||
}
|
||||
}
|
|
@ -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']);
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
65
application/front/controller/admin/ThumbnailsController.php
Normal file
65
application/front/controller/admin/ThumbnailsController.php
Normal 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));
|
||||
}
|
||||
}
|
26
application/front/controller/admin/TokenController.php
Normal file
26
application/front/controller/admin/TokenController.php
Normal 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());
|
||||
}
|
||||
}
|
35
application/front/controller/admin/ToolsController.php
Normal file
35
application/front/controller/admin/ToolsController.php
Normal 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));
|
||||
}
|
||||
}
|
240
application/front/controller/visitor/BookmarkListController.php
Normal file
240
application/front/controller/visitor/BookmarkListController.php
Normal 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;
|
||||
}
|
||||
}
|
192
application/front/controller/visitor/DailyController.php
Normal file
192
application/front/controller/visitor/DailyController.php
Normal 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;
|
||||
}
|
||||
}
|
45
application/front/controller/visitor/ErrorController.php
Normal file
45
application/front/controller/visitor/ErrorController.php
Normal 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'));
|
||||
}
|
||||
}
|
58
application/front/controller/visitor/FeedController.php
Normal file
58
application/front/controller/visitor/FeedController.php
Normal 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);
|
||||
}
|
||||
}
|
165
application/front/controller/visitor/InstallController.php
Normal file
165
application/front/controller/visitor/InstallController.php
Normal 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);
|
||||
}
|
||||
}
|
154
application/front/controller/visitor/LoginController.php
Normal file
154
application/front/controller/visitor/LoginController.php
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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']);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
113
application/front/controller/visitor/TagCloudController.php
Normal file
113
application/front/controller/visitor/TagCloudController.php
Normal 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;
|
||||
}
|
||||
}
|
118
application/front/controller/visitor/TagController.php
Normal file
118
application/front/controller/visitor/TagController.php
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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'));
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
15
application/front/exceptions/AlreadyInstalledException.php
Normal file
15
application/front/exceptions/AlreadyInstalledException.php
Normal 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);
|
||||
}
|
||||
}
|
10
application/front/exceptions/CantLoginException.php
Normal file
10
application/front/exceptions/CantLoginException.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shaarli\Front\Exception;
|
||||
|
||||
class CantLoginException extends \Exception
|
||||
{
|
||||
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
namespace Shaarli\Front\Exception;
|
||||
|
||||
class LoginBannedException extends ShaarliException
|
||||
class LoginBannedException extends ShaarliFrontException
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
13
application/front/exceptions/ResourcePermissionException.php
Normal file
13
application/front/exceptions/ResourcePermissionException.php
Normal 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);
|
||||
}
|
||||
}
|
|
@ -9,11 +9,11 @@
|
|||
/**
|
||||
* Class ShaarliException
|
||||
*
|
||||
* Abstract exception class used to defined any custom exception thrown during front rendering.
|
||||
* Exception class used to defined any custom exception thrown during front rendering.
|
||||
*
|
||||
* @package Front\Exception
|
||||
*/
|
||||
abstract class ShaarliException extends \Exception
|
||||
class ShaarliFrontException extends \Exception
|
||||
{
|
||||
/** Override parent constructor to force $message and $httpCode parameters to be set. */
|
||||
public function __construct(string $message, int $httpCode, Throwable $previous = null)
|
15
application/front/exceptions/ThumbnailsDisabledException.php
Normal file
15
application/front/exceptions/ThumbnailsDisabledException.php
Normal 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);
|
||||
}
|
||||
}
|
15
application/front/exceptions/UnauthorizedException.php
Normal file
15
application/front/exceptions/UnauthorizedException.php
Normal 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
|
||||
{
|
||||
|
||||
}
|
18
application/front/exceptions/WrongTokenException.php
Normal file
18
application/front/exceptions/WrongTokenException.php
Normal 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);
|
||||
}
|
||||
}
|
39
application/http/HttpAccess.php
Normal file
39
application/http/HttpAccess.php
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
|
@ -369,7 +369,7 @@ function server_url($server)
|
|||
*/
|
||||
function index_url($server)
|
||||
{
|
||||
$scriptname = $server['SCRIPT_NAME'];
|
||||
$scriptname = $server['SCRIPT_NAME'] ?? '';
|
||||
if (endsWith($scriptname, 'index.php')) {
|
||||
$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)
|
||||
*
|
||||
|
@ -387,10 +387,17 @@ function index_url($server)
|
|||
*/
|
||||
function page_url($server)
|
||||
{
|
||||
if (! empty($server['QUERY_STRING'])) {
|
||||
return index_url($server).'?'.$server['QUERY_STRING'];
|
||||
$scriptname = $server['SCRIPT_NAME'] ?? '';
|
||||
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']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
};
|
||||
}
|
||||
|
|
130
application/legacy/LegacyController.php
Normal file
130
application/legacy/LegacyController.php
Normal 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);
|
||||
}
|
||||
}
|
|
@ -9,6 +9,7 @@
|
|||
use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
|
||||
use Shaarli\Exceptions\IOException;
|
||||
use Shaarli\FileUtils;
|
||||
use Shaarli\Render\PageCacheManager;
|
||||
|
||||
/**
|
||||
* Data storage for bookmarks.
|
||||
|
@ -352,7 +353,8 @@ public function save($pageCacheDir)
|
|||
|
||||
$this->write();
|
||||
|
||||
invalidateCaches($pageCacheDir);
|
||||
$pageCacheManager = new PageCacheManager($pageCacheDir, $this->loggedIn);
|
||||
$pageCacheManager->invalidateCaches();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
<?php
|
||||
namespace Shaarli;
|
||||
|
||||
namespace Shaarli\Legacy;
|
||||
|
||||
/**
|
||||
* Class Router
|
||||
*
|
||||
* (only displayable pages here)
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
class Router
|
||||
class LegacyRouter
|
||||
{
|
||||
public static $AJAX_THUMB_UPDATE = 'ajax_thumb_update';
|
||||
|
|
@ -10,9 +10,9 @@
|
|||
use Shaarli\ApplicationUtils;
|
||||
use Shaarli\Bookmark\Bookmark;
|
||||
use Shaarli\Bookmark\BookmarkArray;
|
||||
use Shaarli\Bookmark\LinkDB;
|
||||
use Shaarli\Bookmark\BookmarkFilter;
|
||||
use Shaarli\Bookmark\BookmarkIO;
|
||||
use Shaarli\Bookmark\LinkDB;
|
||||
use Shaarli\Config\ConfigJson;
|
||||
use Shaarli\Config\ConfigManager;
|
||||
use Shaarli\Config\ConfigPhp;
|
||||
|
@ -534,7 +534,8 @@ public function updateMethodWebThumbnailer()
|
|||
|
||||
if ($thumbnailsEnabled) {
|
||||
$this->session['warnings'][] = t(
|
||||
'You have enabled or changed thumbnails mode. <a href="?do=thumbs_update">Please synchronize them</a>.'
|
||||
t('You have enabled or changed thumbnails mode.') .
|
||||
'<a href="./admin/thumbnails">' . t('Please synchronize them.') . '</a>'
|
||||
);
|
||||
}
|
||||
|
||||
|
|
9
application/legacy/UnknowLegacyRouteException.php
Normal file
9
application/legacy/UnknowLegacyRouteException.php
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shaarli\Legacy;
|
||||
|
||||
class UnknowLegacyRouteException extends \Exception
|
||||
{
|
||||
}
|
|
@ -6,6 +6,7 @@
|
|||
use DateTimeZone;
|
||||
use Exception;
|
||||
use Katzgrau\KLogger\Logger;
|
||||
use Psr\Http\Message\UploadedFileInterface;
|
||||
use Psr\Log\LogLevel;
|
||||
use Shaarli\Bookmark\Bookmark;
|
||||
use Shaarli\Bookmark\BookmarkServiceInterface;
|
||||
|
@ -16,10 +17,24 @@
|
|||
|
||||
/**
|
||||
* Utilities to import and export bookmarks using the Netscape format
|
||||
* TODO: Not static, use a container.
|
||||
*/
|
||||
class NetscapeBookmarkUtils
|
||||
{
|
||||
/** @var BookmarkServiceInterface */
|
||||
protected $bookmarkService;
|
||||
|
||||
/** @var ConfigManager */
|
||||
protected $conf;
|
||||
|
||||
/** @var History */
|
||||
protected $history;
|
||||
|
||||
public function __construct(BookmarkServiceInterface $bookmarkService, ConfigManager $conf, History $history)
|
||||
{
|
||||
$this->bookmarkService = $bookmarkService;
|
||||
$this->conf = $conf;
|
||||
$this->history = $history;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters bookmarks and adds Netscape-formatted fields
|
||||
|
@ -28,18 +43,16 @@ class NetscapeBookmarkUtils
|
|||
* - timestamp link addition date, using the Unix epoch format
|
||||
* - taglist comma-separated tag list
|
||||
*
|
||||
* @param BookmarkServiceInterface $bookmarkService Link datastore
|
||||
* @param BookmarkFormatter $formatter instance
|
||||
* @param string $selection Which bookmarks to export: (all|private|public)
|
||||
* @param bool $prependNoteUrl Prepend note permalinks with the server's URL
|
||||
* @param string $indexUrl Absolute URL of the Shaarli index page
|
||||
*
|
||||
* @return array The bookmarks to be exported, with additional fields
|
||||
*@throws Exception Invalid export selection
|
||||
*
|
||||
* @throws Exception Invalid export selection
|
||||
*/
|
||||
public static function filterAndFormat(
|
||||
$bookmarkService,
|
||||
public function filterAndFormat(
|
||||
$formatter,
|
||||
$selection,
|
||||
$prependNoteUrl,
|
||||
|
@ -51,11 +64,11 @@ public static function filterAndFormat(
|
|||
}
|
||||
|
||||
$bookmarkLinks = array();
|
||||
foreach ($bookmarkService->search([], $selection) as $bookmark) {
|
||||
foreach ($this->bookmarkService->search([], $selection) as $bookmark) {
|
||||
$link = $formatter->format($bookmark);
|
||||
$link['taglist'] = implode(',', $bookmark->getTags());
|
||||
if ($bookmark->isNote() && $prependNoteUrl) {
|
||||
$link['url'] = $indexUrl . $link['url'];
|
||||
$link['url'] = rtrim($indexUrl, '/') . '/' . ltrim($link['url'], '/');
|
||||
}
|
||||
|
||||
$bookmarkLinks[] = $link;
|
||||
|
@ -64,61 +77,23 @@ public static function filterAndFormat(
|
|||
return $bookmarkLinks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an import status summary
|
||||
*
|
||||
* @param string $filename name of the file to import
|
||||
* @param int $filesize size of the file to import
|
||||
* @param int $importCount how many bookmarks were imported
|
||||
* @param int $overwriteCount how many bookmarks were overwritten
|
||||
* @param int $skipCount how many bookmarks were skipped
|
||||
* @param int $duration how many seconds did the import take
|
||||
*
|
||||
* @return string Summary of the bookmark import status
|
||||
*/
|
||||
private static function importStatus(
|
||||
$filename,
|
||||
$filesize,
|
||||
$importCount = 0,
|
||||
$overwriteCount = 0,
|
||||
$skipCount = 0,
|
||||
$duration = 0
|
||||
) {
|
||||
$status = sprintf(t('File %s (%d bytes) '), $filename, $filesize);
|
||||
if ($importCount == 0 && $overwriteCount == 0 && $skipCount == 0) {
|
||||
$status .= t('has an unknown file format. Nothing was imported.');
|
||||
} else {
|
||||
$status .= vsprintf(
|
||||
t(
|
||||
'was successfully processed in %d seconds: '
|
||||
. '%d bookmarks imported, %d bookmarks overwritten, %d bookmarks skipped.'
|
||||
),
|
||||
[$duration, $importCount, $overwriteCount, $skipCount]
|
||||
);
|
||||
}
|
||||
return $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports Web bookmarks from an uploaded Netscape bookmark dump
|
||||
*
|
||||
* @param array $post Server $_POST parameters
|
||||
* @param array $files Server $_FILES parameters
|
||||
* @param BookmarkServiceInterface $bookmarkService Loaded LinkDB instance
|
||||
* @param ConfigManager $conf instance
|
||||
* @param History $history History instance
|
||||
* @param array $post Server $_POST parameters
|
||||
* @param UploadedFileInterface $file File in PSR-7 object format
|
||||
*
|
||||
* @return string Summary of the bookmark import status
|
||||
*/
|
||||
public static function import($post, $files, $bookmarkService, $conf, $history)
|
||||
public function import($post, UploadedFileInterface $file)
|
||||
{
|
||||
$start = time();
|
||||
$filename = $files['filetoupload']['name'];
|
||||
$filesize = $files['filetoupload']['size'];
|
||||
$data = file_get_contents($files['filetoupload']['tmp_name']);
|
||||
$filename = $file->getClientFilename();
|
||||
$filesize = $file->getSize();
|
||||
$data = (string) $file->getStream();
|
||||
|
||||
if (preg_match('/<!DOCTYPE NETSCAPE-Bookmark-file-1>/i', $data) === 0) {
|
||||
return self::importStatus($filename, $filesize);
|
||||
return $this->importStatus($filename, $filesize);
|
||||
}
|
||||
|
||||
// Overwrite existing bookmarks?
|
||||
|
@ -141,11 +116,11 @@ public static function import($post, $files, $bookmarkService, $conf, $history)
|
|||
true, // nested tag support
|
||||
$defaultTags, // additional user-specified tags
|
||||
strval(1 - $defaultPrivacy), // defaultPub = 1 - defaultPrivacy
|
||||
$conf->get('resource.data_dir') // log path, will be overridden
|
||||
$this->conf->get('resource.data_dir') // log path, will be overridden
|
||||
);
|
||||
$logger = new Logger(
|
||||
$conf->get('resource.data_dir'),
|
||||
!$conf->get('dev.debug') ? LogLevel::INFO : LogLevel::DEBUG,
|
||||
$this->conf->get('resource.data_dir'),
|
||||
!$this->conf->get('dev.debug') ? LogLevel::INFO : LogLevel::DEBUG,
|
||||
[
|
||||
'prefix' => 'import.',
|
||||
'extension' => 'log',
|
||||
|
@ -171,7 +146,7 @@ public static function import($post, $files, $bookmarkService, $conf, $history)
|
|||
$private = 0;
|
||||
}
|
||||
|
||||
$link = $bookmarkService->findByUrl($bkm['uri']);
|
||||
$link = $this->bookmarkService->findByUrl($bkm['uri']);
|
||||
$existingLink = $link !== null;
|
||||
if (! $existingLink) {
|
||||
$link = new Bookmark();
|
||||
|
@ -193,20 +168,21 @@ public static function import($post, $files, $bookmarkService, $conf, $history)
|
|||
}
|
||||
|
||||
$link->setTitle($bkm['title']);
|
||||
$link->setUrl($bkm['uri'], $conf->get('security.allowed_protocols'));
|
||||
$link->setUrl($bkm['uri'], $this->conf->get('security.allowed_protocols'));
|
||||
$link->setDescription($bkm['note']);
|
||||
$link->setPrivate($private);
|
||||
$link->setTagsString($bkm['tags']);
|
||||
|
||||
$bookmarkService->addOrSet($link, false);
|
||||
$this->bookmarkService->addOrSet($link, false);
|
||||
$importCount++;
|
||||
}
|
||||
|
||||
$bookmarkService->save();
|
||||
$history->importLinks();
|
||||
$this->bookmarkService->save();
|
||||
$this->history->importLinks();
|
||||
|
||||
$duration = time() - $start;
|
||||
return self::importStatus(
|
||||
|
||||
return $this->importStatus(
|
||||
$filename,
|
||||
$filesize,
|
||||
$importCount,
|
||||
|
@ -215,4 +191,39 @@ public static function import($post, $files, $bookmarkService, $conf, $history)
|
|||
$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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ class PluginManager
|
|||
*
|
||||
* @var array $authorizedPlugins
|
||||
*/
|
||||
private $authorizedPlugins;
|
||||
private $authorizedPlugins = [];
|
||||
|
||||
/**
|
||||
* List of loaded plugins.
|
||||
|
@ -108,6 +108,10 @@ public function executeHooks($hook, &$data, $params = array())
|
|||
$data['_LOGGEDIN_'] = $params['loggedin'];
|
||||
}
|
||||
|
||||
if (isset($params['basePath'])) {
|
||||
$data['_BASE_PATH_'] = $params['basePath'];
|
||||
}
|
||||
|
||||
foreach ($this->loadedPlugins as $plugin) {
|
||||
$hookFunction = $this->buildHookName($hook, $plugin);
|
||||
|
||||
|
|
|
@ -3,10 +3,12 @@
|
|||
namespace Shaarli\Render;
|
||||
|
||||
use Exception;
|
||||
use exceptions\MissingBasePathException;
|
||||
use RainTPL;
|
||||
use Shaarli\ApplicationUtils;
|
||||
use Shaarli\Bookmark\BookmarkServiceInterface;
|
||||
use Shaarli\Config\ConfigManager;
|
||||
use Shaarli\Security\SessionManager;
|
||||
use Shaarli\Thumbnailer;
|
||||
|
||||
/**
|
||||
|
@ -68,6 +70,15 @@ public function __construct(&$conf, $session, $linkDB = null, $token = null, $is
|
|||
$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.
|
||||
*/
|
||||
|
@ -136,17 +147,40 @@ private function initialize()
|
|||
$this->tpl->assign('thumbnails_width', $this->conf->get('thumbnails.width'));
|
||||
$this->tpl->assign('thumbnails_height', $this->conf->get('thumbnails.height'));
|
||||
|
||||
if (!empty($_SESSION['warnings'])) {
|
||||
$this->tpl->assign('global_warnings', $_SESSION['warnings']);
|
||||
unset($_SESSION['warnings']);
|
||||
}
|
||||
|
||||
$this->tpl->assign('formatter', $this->conf->get('formatter', 'default'));
|
||||
|
||||
// To be removed with a proper theme configuration.
|
||||
$this->tpl->assign('conf', $this->conf);
|
||||
}
|
||||
|
||||
/**
|
||||
* Affect variable after controller processing.
|
||||
* Used for alert messages.
|
||||
*/
|
||||
protected function finalize(string $basePath): void
|
||||
{
|
||||
// TODO: use the SessionManager
|
||||
$messageKeys = [
|
||||
SessionManager::KEY_SUCCESS_MESSAGES,
|
||||
SessionManager::KEY_WARNING_MESSAGES,
|
||||
SessionManager::KEY_ERROR_MESSAGES
|
||||
];
|
||||
foreach ($messageKeys as $messageKey) {
|
||||
if (!empty($_SESSION[$messageKey])) {
|
||||
$this->tpl->assign('global_' . $messageKey, $_SESSION[$messageKey]);
|
||||
unset($_SESSION[$messageKey]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->assign('base_path', $basePath);
|
||||
$this->assign(
|
||||
'asset_path',
|
||||
$basePath . '/' .
|
||||
rtrim($this->conf->get('resource.raintpl_tpl', 'tpl'), '/') . '/' .
|
||||
$this->conf->get('resource.theme', 'default')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The following assign() method is basically the same as RainTPL (except lazy loading)
|
||||
*
|
||||
|
@ -184,21 +218,6 @@ public function assignAll($data)
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a specific page (using a template file).
|
||||
* e.g. $pb->renderPage('picwall');
|
||||
*
|
||||
* @param string $page Template filename (without extension).
|
||||
*/
|
||||
public function renderPage($page)
|
||||
{
|
||||
if ($this->tpl === false) {
|
||||
$this->initialize();
|
||||
}
|
||||
|
||||
$this->tpl->draw($page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a specific page as string (using a template file).
|
||||
* e.g. $pb->render('picwall');
|
||||
|
@ -207,28 +226,14 @@ public function renderPage($page)
|
|||
*
|
||||
* @return string Processed template content
|
||||
*/
|
||||
public function render(string $page): string
|
||||
public function render(string $page, string $basePath): string
|
||||
{
|
||||
if ($this->tpl === false) {
|
||||
$this->initialize();
|
||||
}
|
||||
|
||||
$this->finalize($basePath);
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
|
60
application/render/PageCacheManager.php
Normal file
60
application/render/PageCacheManager.php
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
33
application/render/TemplatePage.php
Normal file
33
application/render/TemplatePage.php
Normal 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';
|
||||
}
|
33
application/security/CookieManager.php
Normal file
33
application/security/CookieManager.php
Normal 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;
|
||||
}
|
||||
}
|
|
@ -9,9 +9,6 @@
|
|||
*/
|
||||
class LoginManager
|
||||
{
|
||||
/** @var string Name of the cookie set after logging in **/
|
||||
public static $STAY_SIGNED_IN_COOKIE = 'shaarli_staySignedIn';
|
||||
|
||||
/** @var array A reference to the $_GLOBALS array */
|
||||
protected $globals = [];
|
||||
|
||||
|
@ -32,17 +29,21 @@ class LoginManager
|
|||
|
||||
/** @var string User sign-in token depending on remote IP and credentials */
|
||||
protected $staySignedInToken = '';
|
||||
/** @var CookieManager */
|
||||
protected $cookieManager;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param ConfigManager $configManager Configuration Manager instance
|
||||
* @param SessionManager $sessionManager SessionManager instance
|
||||
* @param CookieManager $cookieManager CookieManager instance
|
||||
*/
|
||||
public function __construct($configManager, $sessionManager)
|
||||
public function __construct($configManager, $sessionManager, $cookieManager)
|
||||
{
|
||||
$this->configManager = $configManager;
|
||||
$this->sessionManager = $sessionManager;
|
||||
$this->cookieManager = $cookieManager;
|
||||
$this->banManager = new BanManager(
|
||||
$this->configManager->get('security.trusted_proxies', []),
|
||||
$this->configManager->get('security.ban_after'),
|
||||
|
@ -86,10 +87,9 @@ public function getStaySignedInToken()
|
|||
/**
|
||||
* Check user session state and validity (expiration)
|
||||
*
|
||||
* @param array $cookie The $_COOKIE array
|
||||
* @param string $clientIpId Client IP address identifier
|
||||
*/
|
||||
public function checkLoginState($cookie, $clientIpId)
|
||||
public function checkLoginState($clientIpId)
|
||||
{
|
||||
if (! $this->configManager->exists('credentials.login')) {
|
||||
// Shaarli is not configured yet
|
||||
|
@ -97,9 +97,7 @@ public function checkLoginState($cookie, $clientIpId)
|
|||
return;
|
||||
}
|
||||
|
||||
if (isset($cookie[self::$STAY_SIGNED_IN_COOKIE])
|
||||
&& $cookie[self::$STAY_SIGNED_IN_COOKIE] === $this->staySignedInToken
|
||||
) {
|
||||
if ($this->staySignedInToken === $this->cookieManager->getCookieParameter(CookieManager::STAY_SIGNED_IN)) {
|
||||
// The user client has a valid stay-signed-in cookie
|
||||
// Session information is updated with the current client information
|
||||
$this->sessionManager->storeLoginInfo($clientIpId);
|
||||
|
|
|
@ -8,6 +8,14 @@
|
|||
*/
|
||||
class SessionManager
|
||||
{
|
||||
public const KEY_LINKS_PER_PAGE = 'LINKS_PER_PAGE';
|
||||
public const KEY_VISIBILITY = 'visibility';
|
||||
public const KEY_UNTAGGED_ONLY = 'untaggedonly';
|
||||
|
||||
public const KEY_SUCCESS_MESSAGES = 'successes';
|
||||
public const KEY_WARNING_MESSAGES = 'warnings';
|
||||
public const KEY_ERROR_MESSAGES = 'errors';
|
||||
|
||||
/** @var int Session expiration timeout, in seconds */
|
||||
public static $SHORT_TIMEOUT = 3600; // 1 hour
|
||||
|
||||
|
@ -23,16 +31,35 @@ class SessionManager
|
|||
/** @var bool Whether the user should stay signed in (LONG_TIMEOUT) */
|
||||
protected $staySignedIn = false;
|
||||
|
||||
/** @var string */
|
||||
protected $savePath;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param array $session The $_SESSION array (reference)
|
||||
* @param ConfigManager $conf ConfigManager instance
|
||||
* @param array $session The $_SESSION array (reference)
|
||||
* @param ConfigManager $conf ConfigManager instance
|
||||
* @param string $savePath Session save path returned by builtin function session_save_path()
|
||||
*/
|
||||
public function __construct(& $session, $conf)
|
||||
public function __construct(&$session, $conf, string $savePath)
|
||||
{
|
||||
$this->session = &$session;
|
||||
$this->conf = $conf;
|
||||
$this->savePath = $savePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize XSRF token and links per page session variables.
|
||||
*/
|
||||
public function initialize(): void
|
||||
{
|
||||
if (!isset($this->session['tokens'])) {
|
||||
$this->session['tokens'] = [];
|
||||
}
|
||||
|
||||
if (!isset($this->session['LINKS_PER_PAGE'])) {
|
||||
$this->session['LINKS_PER_PAGE'] = $this->conf->get('general.links_per_page', 20);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -202,4 +229,78 @@ public function getSession(): array
|
|||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
namespace Shaarli\Updater;
|
||||
|
||||
use Shaarli\Config\ConfigManager;
|
||||
use Shaarli\Bookmark\BookmarkServiceInterface;
|
||||
use Shaarli\Config\ConfigManager;
|
||||
use Shaarli\Updater\Exception\UpdaterException;
|
||||
|
||||
/**
|
||||
|
@ -21,7 +21,7 @@ class Updater
|
|||
/**
|
||||
* @var BookmarkServiceInterface instance.
|
||||
*/
|
||||
protected $linkServices;
|
||||
protected $bookmarkService;
|
||||
|
||||
/**
|
||||
* @var ConfigManager $conf Configuration Manager instance.
|
||||
|
@ -38,6 +38,11 @@ class Updater
|
|||
*/
|
||||
protected $methods;
|
||||
|
||||
/**
|
||||
* @var string $basePath Shaarli root directory (from HTTP Request)
|
||||
*/
|
||||
protected $basePath = null;
|
||||
|
||||
/**
|
||||
* Object constructor.
|
||||
*
|
||||
|
@ -49,7 +54,7 @@ class Updater
|
|||
public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn)
|
||||
{
|
||||
$this->doneUpdates = $doneUpdates;
|
||||
$this->linkServices = $linkDB;
|
||||
$this->bookmarkService = $linkDB;
|
||||
$this->conf = $conf;
|
||||
$this->isLoggedIn = $isLoggedIn;
|
||||
|
||||
|
@ -62,13 +67,15 @@ public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn)
|
|||
* Run all new updates.
|
||||
* Update methods have to start with 'updateMethod' and return true (on success).
|
||||
*
|
||||
* @param string $basePath Shaarli root directory (from HTTP Request)
|
||||
*
|
||||
* @return array An array containing ran updates.
|
||||
*
|
||||
* @throws UpdaterException If something went wrong.
|
||||
*/
|
||||
public function update()
|
||||
public function update(string $basePath = null)
|
||||
{
|
||||
$updatesRan = array();
|
||||
$updatesRan = [];
|
||||
|
||||
// If the user isn't logged in, exit without updating.
|
||||
if ($this->isLoggedIn !== true) {
|
||||
|
@ -111,4 +118,62 @@ public function getDoneUpdates()
|
|||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,13 +10,14 @@
|
|||
* It contains a recursive call to retrieve the thumb of the next link when it succeed.
|
||||
* It also update the progress bar and other visual feedback elements.
|
||||
*
|
||||
* @param {string} basePath Shaarli subfolder for XHR requests
|
||||
* @param {array} ids List of LinkID to update
|
||||
* @param {int} i Current index in ids
|
||||
* @param {object} elements List of DOM element to avoid retrieving them at each iteration
|
||||
*/
|
||||
function updateThumb(ids, i, elements) {
|
||||
function updateThumb(basePath, ids, i, elements) {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', '?do=ajax_thumb_update');
|
||||
xhr.open('PATCH', `${basePath}/admin/shaare/${ids[i]}/update-thumbnail`);
|
||||
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
|
||||
xhr.responseType = 'json';
|
||||
xhr.onload = () => {
|
||||
|
@ -29,17 +30,18 @@ function updateThumb(ids, i, elements) {
|
|||
elements.current.innerHTML = i;
|
||||
elements.title.innerHTML = response.title;
|
||||
if (response.thumbnail !== false) {
|
||||
elements.thumbnail.innerHTML = `<img src="${response.thumbnail}">`;
|
||||
elements.thumbnail.innerHTML = `<img src="${basePath}/${response.thumbnail}">`;
|
||||
}
|
||||
if (i < ids.length) {
|
||||
updateThumb(ids, i, elements);
|
||||
updateThumb(basePath, ids, i, elements);
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.send(`id=${ids[i]}`);
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
(() => {
|
||||
const basePath = document.querySelector('input[name="js_base_path"]').value;
|
||||
const ids = document.getElementsByName('ids')[0].value.split(',');
|
||||
const elements = {
|
||||
progressBar: document.querySelector('.progressbar > div'),
|
||||
|
@ -47,5 +49,5 @@ function updateThumb(ids, i, elements) {
|
|||
thumbnail: document.querySelector('.thumbnail-placeholder'),
|
||||
title: document.querySelector('.thumbnail-link-title'),
|
||||
};
|
||||
updateThumb(ids, 0, elements);
|
||||
updateThumb(basePath, ids, 0, elements);
|
||||
})();
|
||||
|
|
|
@ -25,12 +25,16 @@ function findParent(element, tagName, attributes) {
|
|||
/**
|
||||
* Ajax request to refresh the CSRF token.
|
||||
*/
|
||||
function refreshToken() {
|
||||
function refreshToken(basePath) {
|
||||
console.log('refresh');
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', '?do=token');
|
||||
xhr.open('GET', `${basePath}/admin/token`);
|
||||
xhr.onload = () => {
|
||||
const token = document.getElementById('token');
|
||||
token.setAttribute('value', xhr.responseText);
|
||||
const elements = document.querySelectorAll('input[name="token"]');
|
||||
[...elements].forEach((element) => {
|
||||
console.log(element);
|
||||
element.setAttribute('value', xhr.responseText);
|
||||
});
|
||||
};
|
||||
xhr.send();
|
||||
}
|
||||
|
@ -215,6 +219,8 @@ function init(description) {
|
|||
}
|
||||
|
||||
(() => {
|
||||
const basePath = document.querySelector('input[name="js_base_path"]').value;
|
||||
|
||||
/**
|
||||
* Handle responsive menu.
|
||||
* Source: http://purecss.io/layouts/tucked-menu-vertical/
|
||||
|
@ -461,7 +467,7 @@ function init(description) {
|
|||
});
|
||||
|
||||
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);
|
||||
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 fromtag = block.getAttribute('data-tag');
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', '?do=changetag');
|
||||
xhr.open('POST', `${basePath}/admin/tags`);
|
||||
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
|
||||
xhr.onload = () => {
|
||||
if (xhr.status !== 200) {
|
||||
|
@ -558,8 +565,12 @@ function init(description) {
|
|||
input.setAttribute('value', totag);
|
||||
findParent(input, 'div', { class: 'rename-tag-form' }).style.display = 'none';
|
||||
block.querySelector('a.tag-link').innerHTML = htmlEntities(totag);
|
||||
block.querySelector('a.tag-link').setAttribute('href', `?searchtags=${encodeURIComponent(totag)}`);
|
||||
block.querySelector('a.rename-tag').setAttribute('href', `?do=changetag&fromtag=${encodeURIComponent(totag)}`);
|
||||
block
|
||||
.querySelector('a.tag-link')
|
||||
.setAttribute('href', `${basePath}/?searchtags=${encodeURIComponent(totag)}`);
|
||||
block
|
||||
.querySelector('a.rename-tag')
|
||||
.setAttribute('href', `${basePath}/admin/tags?fromtag=${encodeURIComponent(totag)}`);
|
||||
|
||||
// Refresh awesomplete values
|
||||
existingTags = existingTags.map(tag => (tag === fromtag ? totag : tag));
|
||||
|
@ -567,7 +578,7 @@ function init(description) {
|
|||
}
|
||||
};
|
||||
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}"?`)) {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', '?do=changetag');
|
||||
xhr.open('POST', `${basePath}/admin/tags`);
|
||||
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
|
||||
xhr.onload = () => {
|
||||
block.remove();
|
||||
};
|
||||
xhr.send(encodeURI(`deletetag=1&fromtag=${tag}&token=${refreshedToken}`));
|
||||
refreshToken();
|
||||
refreshToken(basePath);
|
||||
|
||||
existingTags = existingTags.filter(tagItem => tagItem !== tag);
|
||||
awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes);
|
||||
|
|
|
@ -490,6 +490,10 @@ body,
|
|||
}
|
||||
}
|
||||
|
||||
.header-alert-message {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
// CONTENT - GENERAL
|
||||
.container {
|
||||
position: relative;
|
||||
|
|
|
@ -53,7 +53,8 @@
|
|||
"Shaarli\\Feed\\": "application/feed",
|
||||
"Shaarli\\Formatter\\": "application/formatter",
|
||||
"Shaarli\\Front\\": "application/front",
|
||||
"Shaarli\\Front\\Controller\\": "application/front/controllers",
|
||||
"Shaarli\\Front\\Controller\\Admin\\": "application/front/controller/admin",
|
||||
"Shaarli\\Front\\Controller\\Visitor\\": "application/front/controller/visitor",
|
||||
"Shaarli\\Front\\Exception\\": "application/front/exceptions",
|
||||
"Shaarli\\Http\\": "application/http",
|
||||
"Shaarli\\Legacy\\": "application/legacy",
|
||||
|
|
211
composer.lock
generated
211
composer.lock
generated
|
@ -508,16 +508,16 @@
|
|||
},
|
||||
{
|
||||
"name": "psr/log",
|
||||
"version": "1.1.2",
|
||||
"version": "1.1.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-fig/log.git",
|
||||
"reference": "446d54b4cb6bf489fc9d75f55843658e6f25d801"
|
||||
"reference": "0f73288fd15629204f9d42b7055f72dacbe811fc"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-fig/log/zipball/446d54b4cb6bf489fc9d75f55843658e6f25d801",
|
||||
"reference": "446d54b4cb6bf489fc9d75f55843658e6f25d801",
|
||||
"url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc",
|
||||
"reference": "0f73288fd15629204f9d42b7055f72dacbe811fc",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -551,7 +551,7 @@
|
|||
"psr",
|
||||
"psr-3"
|
||||
],
|
||||
"time": "2019-11-01T11:05:21+00:00"
|
||||
"time": "2020-03-23T09:12:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "pubsubhubbub/publisher",
|
||||
|
@ -936,24 +936,21 @@
|
|||
},
|
||||
{
|
||||
"name": "phpdocumentor/reflection-common",
|
||||
"version": "2.0.0",
|
||||
"version": "2.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpDocumentor/ReflectionCommon.git",
|
||||
"reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a"
|
||||
"reference": "6568f4687e5b41b054365f9ae03fcb1ed5f2069b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/63a995caa1ca9e5590304cd845c15ad6d482a62a",
|
||||
"reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a",
|
||||
"url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/6568f4687e5b41b054365f9ae03fcb1ed5f2069b",
|
||||
"reference": "6568f4687e5b41b054365f9ae03fcb1ed5f2069b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "~6"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
|
@ -984,7 +981,7 @@
|
|||
"reflection",
|
||||
"static analysis"
|
||||
],
|
||||
"time": "2018-08-07T13:53:10+00:00"
|
||||
"time": "2020-04-27T09:25:28+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpdocumentor/reflection-docblock",
|
||||
|
@ -1087,24 +1084,24 @@
|
|||
},
|
||||
{
|
||||
"name": "phpspec/prophecy",
|
||||
"version": "1.10.1",
|
||||
"version": "v1.10.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpspec/prophecy.git",
|
||||
"reference": "cbe1df668b3fe136bcc909126a0f529a78d4cbbc"
|
||||
"reference": "451c3cd1418cf640de218914901e51b064abb093"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpspec/prophecy/zipball/cbe1df668b3fe136bcc909126a0f529a78d4cbbc",
|
||||
"reference": "cbe1df668b3fe136bcc909126a0f529a78d4cbbc",
|
||||
"url": "https://api.github.com/repos/phpspec/prophecy/zipball/451c3cd1418cf640de218914901e51b064abb093",
|
||||
"reference": "451c3cd1418cf640de218914901e51b064abb093",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"doctrine/instantiator": "^1.0.2",
|
||||
"php": "^5.3|^7.0",
|
||||
"phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0",
|
||||
"sebastian/comparator": "^1.2.3|^2.0|^3.0",
|
||||
"sebastian/recursion-context": "^1.0|^2.0|^3.0"
|
||||
"sebastian/comparator": "^1.2.3|^2.0|^3.0|^4.0",
|
||||
"sebastian/recursion-context": "^1.0|^2.0|^3.0|^4.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpspec/phpspec": "^2.5 || ^3.2",
|
||||
|
@ -1146,7 +1143,7 @@
|
|||
"spy",
|
||||
"stub"
|
||||
],
|
||||
"time": "2019-12-22T21:05:45+00:00"
|
||||
"time": "2020-03-05T15:02:03+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-code-coverage",
|
||||
|
@ -1501,12 +1498,12 @@
|
|||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Roave/SecurityAdvisories.git",
|
||||
"reference": "67ac6ea8f4a078c3c9b7aec5d7ae70f098c37389"
|
||||
"reference": "5a342e2dc0408d026b97ee3176b5b406e54e3766"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/67ac6ea8f4a078c3c9b7aec5d7ae70f098c37389",
|
||||
"reference": "67ac6ea8f4a078c3c9b7aec5d7ae70f098c37389",
|
||||
"url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/5a342e2dc0408d026b97ee3176b5b406e54e3766",
|
||||
"reference": "5a342e2dc0408d026b97ee3176b5b406e54e3766",
|
||||
"shasum": ""
|
||||
},
|
||||
"conflict": {
|
||||
|
@ -1518,11 +1515,17 @@
|
|||
"api-platform/core": ">=2.2,<2.2.10|>=2.3,<2.3.6",
|
||||
"asymmetricrypt/asymmetricrypt": ">=0,<9.9.99",
|
||||
"aws/aws-sdk-php": ">=3,<3.2.1",
|
||||
"bagisto/bagisto": "<0.1.5",
|
||||
"barrelstrength/sprout-base-email": "<3.9",
|
||||
"bolt/bolt": "<3.6.10",
|
||||
"brightlocal/phpwhois": "<=4.2.5",
|
||||
"buddypress/buddypress": "<5.1.2",
|
||||
"bugsnag/bugsnag-laravel": ">=2,<2.0.2",
|
||||
"cakephp/cakephp": ">=1.3,<1.3.18|>=2,<2.4.99|>=2.5,<2.5.99|>=2.6,<2.6.12|>=2.7,<2.7.6|>=3,<3.5.18|>=3.6,<3.6.15|>=3.7,<3.7.7",
|
||||
"cart2quote/module-quotation": ">=4.1.6,<=4.4.5|>=5,<5.4.4",
|
||||
"cartalyst/sentry": "<=2.1.6",
|
||||
"centreon/centreon": "<18.10.8|>=19,<19.4.5",
|
||||
"cesnet/simplesamlphp-module-proxystatistics": "<3.1",
|
||||
"codeigniter/framework": "<=3.0.6",
|
||||
"composer/composer": "<=1-alpha.11",
|
||||
"contao-components/mediaelement": ">=2.14.2,<2.21.1",
|
||||
|
@ -1540,22 +1543,32 @@
|
|||
"doctrine/mongodb-odm": ">=1,<1.0.2",
|
||||
"doctrine/mongodb-odm-bundle": ">=2,<3.0.1",
|
||||
"doctrine/orm": ">=2,<2.4.8|>=2.5,<2.5.1",
|
||||
"dolibarr/dolibarr": "<=10.0.6",
|
||||
"dompdf/dompdf": ">=0.6,<0.6.2",
|
||||
"drupal/core": ">=7,<7.69|>=8,<8.7.11|>=8.8,<8.8.1",
|
||||
"drupal/drupal": ">=7,<7.69|>=8,<8.7.11|>=8.8,<8.8.1",
|
||||
"drupal/core": ">=7,<7.69|>=8,<8.7.12|>=8.8,<8.8.4",
|
||||
"drupal/drupal": ">=7,<7.69|>=8,<8.7.12|>=8.8,<8.8.4",
|
||||
"endroid/qr-code-bundle": "<3.4.2",
|
||||
"enshrined/svg-sanitize": "<0.13.1",
|
||||
"erusev/parsedown": "<1.7.2",
|
||||
"ezsystems/ezplatform-admin-ui": ">=1.3,<1.3.5|>=1.4,<1.4.4",
|
||||
"ezsystems/ezpublish-kernel": ">=5.3,<5.3.12.1|>=5.4,<5.4.13.1|>=6,<6.7.9.1|>=6.8,<6.13.5.1|>=7,<7.2.4.1|>=7.3,<7.3.2.1",
|
||||
"ezsystems/ezpublish-legacy": ">=5.3,<5.3.12.6|>=5.4,<5.4.12.3|>=2011,<2017.12.4.3|>=2018.6,<2018.6.1.4|>=2018.9,<2018.9.1.3",
|
||||
"ezsystems/demobundle": ">=5.4,<5.4.6.1",
|
||||
"ezsystems/ezdemo-ls-extension": ">=5.4,<5.4.2.1",
|
||||
"ezsystems/ezfind-ls": ">=5.3,<5.3.6.1|>=5.4,<5.4.11.1|>=2017.12,<2017.12.0.1",
|
||||
"ezsystems/ezplatform": ">=1.7,<1.7.9.1|>=1.13,<1.13.5.1|>=2.5,<2.5.4",
|
||||
"ezsystems/ezplatform-admin-ui": ">=1.3,<1.3.5|>=1.4,<1.4.6",
|
||||
"ezsystems/ezplatform-admin-ui-assets": ">=4,<4.2",
|
||||
"ezsystems/ezplatform-user": ">=1,<1.0.1",
|
||||
"ezsystems/ezpublish-kernel": ">=5.3,<5.3.12.1|>=5.4,<5.4.14.1|>=6,<6.7.9.1|>=6.8,<6.13.6.2|>=7,<7.2.4.1|>=7.3,<7.3.2.1|>=7.5,<7.5.6.2",
|
||||
"ezsystems/ezpublish-legacy": ">=5.3,<5.3.12.6|>=5.4,<5.4.14.1|>=2011,<2017.12.7.2|>=2018.6,<2018.6.1.4|>=2018.9,<2018.9.1.3|>=2019.3,<2019.3.4.2",
|
||||
"ezsystems/repository-forms": ">=2.3,<2.3.2.1",
|
||||
"ezyang/htmlpurifier": "<4.1.1",
|
||||
"firebase/php-jwt": "<2",
|
||||
"fooman/tcpdf": "<6.2.22",
|
||||
"fossar/tcpdf-parser": "<6.2.22",
|
||||
"friendsofsymfony/oauth2-php": "<1.3",
|
||||
"friendsofsymfony/rest-bundle": ">=1.2,<1.2.2",
|
||||
"friendsofsymfony/user-bundle": ">=1.2,<1.3.5",
|
||||
"fuel/core": "<1.8.1",
|
||||
"getgrav/grav": "<1.7-beta.8",
|
||||
"gree/jose": "<=2.2",
|
||||
"gregwar/rst": "<1.0.3",
|
||||
"guzzlehttp/guzzle": ">=4-rc.2,<4.2.4|>=5,<5.3.1|>=6,<6.2.1",
|
||||
|
@ -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/database": ">=4,<4.0.99|>=4.1,<4.1.29",
|
||||
"illuminate/encryption": ">=4,<=4.0.11|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.40|>=5.6,<5.6.15",
|
||||
"illuminate/view": ">=7,<7.1.2",
|
||||
"ivankristianto/phpwhois": "<=4.3",
|
||||
"james-heinrich/getid3": "<1.9.9",
|
||||
"joomla/session": "<1.3.1",
|
||||
|
@ -1570,15 +1584,19 @@
|
|||
"kazist/phpwhois": "<=4.2.6",
|
||||
"kreait/firebase-php": ">=3.2,<3.8.1",
|
||||
"la-haute-societe/tcpdf": "<6.2.22",
|
||||
"laravel/framework": ">=4,<4.0.99|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.42|>=5.6,<5.6.30",
|
||||
"laravel/framework": ">=4,<4.0.99|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.42|>=5.6,<5.6.30|>=7,<7.1.2",
|
||||
"laravel/socialite": ">=1,<1.0.99|>=2,<2.0.10",
|
||||
"league/commonmark": "<0.18.3",
|
||||
"librenms/librenms": "<1.53",
|
||||
"magento/community-edition": ">=2,<2.2.10|>=2.3,<2.3.3",
|
||||
"magento/magento1ce": "<1.9.4.3",
|
||||
"magento/magento1ee": ">=1,<1.14.4.3",
|
||||
"magento/product-community-edition": ">=2,<2.2.10|>=2.3,<2.3.2-p.2",
|
||||
"monolog/monolog": ">=1.8,<1.12",
|
||||
"namshi/jose": "<2.2",
|
||||
"nzo/url-encryptor-bundle": ">=4,<4.3.2|>=5,<5.0.1",
|
||||
"onelogin/php-saml": "<2.10.4",
|
||||
"oneup/uploader-bundle": "<1.9.3|>=2,<2.1.5",
|
||||
"openid/php-openid": "<2.3",
|
||||
"oro/crm": ">=1.7,<1.7.4",
|
||||
"oro/platform": ">=1.7,<1.7.4",
|
||||
|
@ -1587,49 +1605,67 @@
|
|||
"paragonie/random_compat": "<2",
|
||||
"paypal/merchant-sdk-php": "<3.12",
|
||||
"pear/archive_tar": "<1.4.4",
|
||||
"phpfastcache/phpfastcache": ">=5,<5.0.13",
|
||||
"phpmailer/phpmailer": ">=5,<5.2.27|>=6,<6.0.6",
|
||||
"phpoffice/phpexcel": "<=1.8.1",
|
||||
"phpoffice/phpspreadsheet": "<=1.5",
|
||||
"phpmyadmin/phpmyadmin": "<4.9.2",
|
||||
"phpoffice/phpexcel": "<1.8.2",
|
||||
"phpoffice/phpspreadsheet": "<1.8",
|
||||
"phpunit/phpunit": ">=4.8.19,<4.8.28|>=5.0.10,<5.6.3",
|
||||
"phpwhois/phpwhois": "<=4.2.5",
|
||||
"phpxmlrpc/extras": "<0.6.1",
|
||||
"pimcore/pimcore": "<6.3",
|
||||
"prestashop/autoupgrade": ">=4,<4.10.1",
|
||||
"prestashop/gamification": "<2.3.2",
|
||||
"prestashop/ps_facetedsearch": "<3.4.1",
|
||||
"privatebin/privatebin": "<1.2.2|>=1.3,<1.3.2",
|
||||
"propel/propel": ">=2-alpha.1,<=2-alpha.7",
|
||||
"propel/propel1": ">=1,<=1.7.1",
|
||||
"pusher/pusher-php-server": "<2.2.1",
|
||||
"robrichards/xmlseclibs": ">=1,<3.0.4",
|
||||
"robrichards/xmlseclibs": "<3.0.4",
|
||||
"sabre/dav": ">=1.6,<1.6.99|>=1.7,<1.7.11|>=1.8,<1.8.9",
|
||||
"scheb/two-factor-bundle": ">=0,<3.26|>=4,<4.11",
|
||||
"sensiolabs/connect": "<4.2.3",
|
||||
"serluck/phpwhois": "<=4.2.6",
|
||||
"shopware/shopware": "<5.3.7",
|
||||
"silverstripe/cms": ">=3,<=3.0.11|>=3.1,<3.1.11",
|
||||
"silverstripe/admin": ">=1.0.3,<1.0.4|>=1.1,<1.1.1",
|
||||
"silverstripe/assets": ">=1,<1.4.7|>=1.5,<1.5.2",
|
||||
"silverstripe/cms": "<4.3.6|>=4.4,<4.4.4",
|
||||
"silverstripe/comments": ">=1.3,<1.9.99|>=2,<2.9.99|>=3,<3.1.1",
|
||||
"silverstripe/forum": "<=0.6.1|>=0.7,<=0.7.3",
|
||||
"silverstripe/framework": ">=3,<3.6.7|>=3.7,<3.7.3|>=4,<4.4",
|
||||
"silverstripe/framework": "<4.4.5|>=4.5,<4.5.2",
|
||||
"silverstripe/graphql": ">=2,<2.0.5|>=3,<3.1.2",
|
||||
"silverstripe/registry": ">=2.1,<2.1.2|>=2.2,<2.2.1",
|
||||
"silverstripe/restfulserver": ">=1,<1.0.9|>=2,<2.0.4",
|
||||
"silverstripe/subsites": ">=2,<2.1.1",
|
||||
"silverstripe/taxonomy": ">=1.3,<1.3.1|>=2,<2.0.1",
|
||||
"silverstripe/userforms": "<3",
|
||||
"simple-updates/phpwhois": "<=1",
|
||||
"simplesamlphp/saml2": "<1.10.6|>=2,<2.3.8|>=3,<3.1.4",
|
||||
"simplesamlphp/simplesamlphp": "<1.17.8",
|
||||
"simplesamlphp/simplesamlphp": "<1.18.6",
|
||||
"simplesamlphp/simplesamlphp-module-infocard": "<1.0.1",
|
||||
"simplito/elliptic-php": "<1.0.6",
|
||||
"slim/slim": "<2.6",
|
||||
"smarty/smarty": "<3.1.33",
|
||||
"socalnick/scn-social-auth": "<1.15.2",
|
||||
"spoonity/tcpdf": "<6.2.22",
|
||||
"squizlabs/php_codesniffer": ">=1,<2.8.1|>=3,<3.0.1",
|
||||
"ssddanbrown/bookstack": "<0.29.2",
|
||||
"stormpath/sdk": ">=0,<9.9.99",
|
||||
"studio-42/elfinder": "<2.1.48",
|
||||
"studio-42/elfinder": "<2.1.49",
|
||||
"swiftmailer/swiftmailer": ">=4,<5.4.5",
|
||||
"sylius/admin-bundle": ">=1,<1.0.17|>=1.1,<1.1.9|>=1.2,<1.2.2",
|
||||
"sylius/grid": ">=1,<1.1.19|>=1.2,<1.2.18|>=1.3,<1.3.13|>=1.4,<1.4.5|>=1.5,<1.5.1",
|
||||
"sylius/grid-bundle": ">=1,<1.1.19|>=1.2,<1.2.18|>=1.3,<1.3.13|>=1.4,<1.4.5|>=1.5,<1.5.1",
|
||||
"sylius/sylius": ">=1,<1.1.18|>=1.2,<1.2.17|>=1.3,<1.3.12|>=1.4,<1.4.4",
|
||||
"sylius/resource-bundle": "<1.3.13|>=1.4,<1.4.6|>=1.5,<1.5.1|>=1.6,<1.6.3",
|
||||
"sylius/sylius": "<1.3.16|>=1.4,<1.4.12|>=1.5,<1.5.9|>=1.6,<1.6.5",
|
||||
"symbiote/silverstripe-multivaluefield": ">=3,<3.0.99",
|
||||
"symbiote/silverstripe-versionedfiles": "<=2.0.3",
|
||||
"symfony/cache": ">=3.1,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8",
|
||||
"symfony/dependency-injection": ">=2,<2.0.17|>=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7",
|
||||
"symfony/error-handler": ">=4.4,<4.4.4|>=5,<5.0.4",
|
||||
"symfony/form": ">=2.3,<2.3.35|>=2.4,<2.6.12|>=2.7,<2.7.50|>=2.8,<2.8.49|>=3,<3.4.20|>=4,<4.0.15|>=4.1,<4.1.9|>=4.2,<4.2.1",
|
||||
"symfony/framework-bundle": ">=2,<2.3.18|>=2.4,<2.4.8|>=2.5,<2.5.2|>=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7",
|
||||
"symfony/http-foundation": ">=2,<2.8.52|>=3,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8",
|
||||
"symfony/http-foundation": ">=2,<2.8.52|>=3,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8|>=4.4,<4.4.7|>=5,<5.0.7",
|
||||
"symfony/http-kernel": ">=2,<2.8.52|>=3,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8",
|
||||
"symfony/intl": ">=2.7,<2.7.38|>=2.8,<2.8.31|>=3,<3.2.14|>=3.3,<3.3.13",
|
||||
"symfony/mime": ">=4.3,<4.3.8",
|
||||
|
@ -1638,14 +1674,14 @@
|
|||
"symfony/polyfill-php55": ">=1,<1.10",
|
||||
"symfony/proxy-manager-bridge": ">=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7",
|
||||
"symfony/routing": ">=2,<2.0.19",
|
||||
"symfony/security": ">=2,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7",
|
||||
"symfony/security": ">=2,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7|>=4.4,<4.4.7|>=5,<5.0.7",
|
||||
"symfony/security-bundle": ">=2,<2.7.48|>=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11",
|
||||
"symfony/security-core": ">=2.4,<2.6.13|>=2.7,<2.7.9|>=2.7.30,<2.7.32|>=2.8,<2.8.37|>=3,<3.3.17|>=3.4,<3.4.7|>=4,<4.0.7",
|
||||
"symfony/security-csrf": ">=2.4,<2.7.48|>=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11",
|
||||
"symfony/security-guard": ">=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11",
|
||||
"symfony/security-http": ">=2.3,<2.3.41|>=2.4,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.2.12|>=4.3,<4.3.8",
|
||||
"symfony/security-http": ">=2.3,<2.3.41|>=2.4,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.2.12|>=4.3,<4.3.8|>=4.4,<4.4.7|>=5,<5.0.7",
|
||||
"symfony/serializer": ">=2,<2.0.11",
|
||||
"symfony/symfony": ">=2,<2.8.52|>=3,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8",
|
||||
"symfony/symfony": ">=2,<2.8.52|>=3,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8|>=4.4,<4.4.7|>=5,<5.0.7",
|
||||
"symfony/translation": ">=2,<2.0.17",
|
||||
"symfony/validator": ">=2,<2.0.24|>=2.1,<2.1.12|>=2.2,<2.2.5|>=2.3,<2.3.3",
|
||||
"symfony/var-exporter": ">=4.2,<4.2.12|>=4.3,<4.3.8",
|
||||
|
@ -1658,14 +1694,17 @@
|
|||
"titon/framework": ">=0,<9.9.99",
|
||||
"truckersmp/phpwhois": "<=4.3.1",
|
||||
"twig/twig": "<1.38|>=2,<2.7",
|
||||
"typo3/cms": ">=6.2,<6.2.30|>=7,<7.6.32|>=8,<8.7.30|>=9,<9.5.12|>=10,<10.2.1",
|
||||
"typo3/cms-core": ">=8,<8.7.30|>=9,<9.5.12|>=10,<10.2.1",
|
||||
"typo3/cms": ">=6.2,<6.2.30|>=7,<7.6.32|>=8,<8.7.30|>=9,<9.5.17|>=10,<10.4.2",
|
||||
"typo3/cms-core": ">=8,<8.7.30|>=9,<9.5.17|>=10,<10.4.2",
|
||||
"typo3/flow": ">=1,<1.0.4|>=1.1,<1.1.1|>=2,<2.0.1|>=2.3,<2.3.16|>=3,<3.0.10|>=3.1,<3.1.7|>=3.2,<3.2.7|>=3.3,<3.3.5",
|
||||
"typo3/neos": ">=1.1,<1.1.3|>=1.2,<1.2.13|>=2,<2.0.4",
|
||||
"typo3/phar-stream-wrapper": ">=1,<2.1.1|>=3,<3.1.1",
|
||||
"ua-parser/uap-php": "<3.8",
|
||||
"usmanhalalit/pixie": "<1.0.3|>=2,<2.0.2",
|
||||
"verot/class.upload.php": "<=1.0.3|>=2,<=2.0.4",
|
||||
"wallabag/tcpdf": "<6.2.22",
|
||||
"willdurand/js-translation-bundle": "<2.1.1",
|
||||
"yii2mod/yii2-cms": "<1.9.2",
|
||||
"yiisoft/yii": ">=1.1.14,<1.1.15",
|
||||
"yiisoft/yii2": "<2.0.15",
|
||||
"yiisoft/yii2-bootstrap": "<2.0.4",
|
||||
|
@ -1674,6 +1713,7 @@
|
|||
"yiisoft/yii2-gii": "<2.0.4",
|
||||
"yiisoft/yii2-jui": "<2.0.4",
|
||||
"yiisoft/yii2-redis": "<2.0.8",
|
||||
"yourls/yourls": "<1.7.4",
|
||||
"zendframework/zend-cache": ">=2.4,<2.4.8|>=2.5,<2.5.3",
|
||||
"zendframework/zend-captcha": ">=2,<2.4.9|>=2.5,<2.5.2",
|
||||
"zendframework/zend-crypt": ">=2,<2.4.9|>=2.5,<2.5.2",
|
||||
|
@ -1710,10 +1750,15 @@
|
|||
"name": "Marco Pivetta",
|
||||
"email": "ocramius@gmail.com",
|
||||
"role": "maintainer"
|
||||
},
|
||||
{
|
||||
"name": "Ilya Tribusean",
|
||||
"email": "slash3b@gmail.com",
|
||||
"role": "maintainer"
|
||||
}
|
||||
],
|
||||
"description": "Prevents installation of composer packages with known security vulnerabilities: no API, simply require it",
|
||||
"time": "2020-01-06T19:16:46+00:00"
|
||||
"time": "2020-05-12T11:18:47+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/code-unit-reverse-lookup",
|
||||
|
@ -2326,16 +2371,16 @@
|
|||
},
|
||||
{
|
||||
"name": "squizlabs/php_codesniffer",
|
||||
"version": "3.5.3",
|
||||
"version": "3.5.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
|
||||
"reference": "557a1fc7ac702c66b0bbfe16ab3d55839ef724cb"
|
||||
"reference": "73e2e7f57d958e7228fce50dc0c61f58f017f9f6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/557a1fc7ac702c66b0bbfe16ab3d55839ef724cb",
|
||||
"reference": "557a1fc7ac702c66b0bbfe16ab3d55839ef724cb",
|
||||
"url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/73e2e7f57d958e7228fce50dc0c61f58f017f9f6",
|
||||
"reference": "73e2e7f57d958e7228fce50dc0c61f58f017f9f6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -2373,20 +2418,20 @@
|
|||
"phpcs",
|
||||
"standards"
|
||||
],
|
||||
"time": "2019-12-04T04:46:47+00:00"
|
||||
"time": "2020-04-17T01:09:41+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/console",
|
||||
"version": "v4.4.2",
|
||||
"version": "v4.4.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/console.git",
|
||||
"reference": "82437719dab1e6bdd28726af14cb345c2ec816d0"
|
||||
"reference": "10bb3ee3c97308869d53b3e3d03f6ac23ff985f7"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/82437719dab1e6bdd28726af14cb345c2ec816d0",
|
||||
"reference": "82437719dab1e6bdd28726af14cb345c2ec816d0",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/10bb3ee3c97308869d53b3e3d03f6ac23ff985f7",
|
||||
"reference": "10bb3ee3c97308869d53b3e3d03f6ac23ff985f7",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -2449,20 +2494,20 @@
|
|||
],
|
||||
"description": "Symfony Console Component",
|
||||
"homepage": "https://symfony.com",
|
||||
"time": "2019-12-17T10:32:23+00:00"
|
||||
"time": "2020-03-30T11:41:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/finder",
|
||||
"version": "v4.4.2",
|
||||
"version": "v4.4.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/finder.git",
|
||||
"reference": "ce8743441da64c41e2a667b8eb66070444ed911e"
|
||||
"reference": "5729f943f9854c5781984ed4907bbb817735776b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/finder/zipball/ce8743441da64c41e2a667b8eb66070444ed911e",
|
||||
"reference": "ce8743441da64c41e2a667b8eb66070444ed911e",
|
||||
"url": "https://api.github.com/repos/symfony/finder/zipball/5729f943f9854c5781984ed4907bbb817735776b",
|
||||
"reference": "5729f943f9854c5781984ed4907bbb817735776b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -2498,20 +2543,20 @@
|
|||
],
|
||||
"description": "Symfony Finder Component",
|
||||
"homepage": "https://symfony.com",
|
||||
"time": "2019-11-17T21:56:56+00:00"
|
||||
"time": "2020-03-27T16:54:36+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-ctype",
|
||||
"version": "v1.13.1",
|
||||
"version": "v1.17.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-ctype.git",
|
||||
"reference": "f8f0b461be3385e56d6de3dbb5a0df24c0c275e3"
|
||||
"reference": "e94c8b1bbe2bc77507a1056cdb06451c75b427f9"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/f8f0b461be3385e56d6de3dbb5a0df24c0c275e3",
|
||||
"reference": "f8f0b461be3385e56d6de3dbb5a0df24c0c275e3",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e94c8b1bbe2bc77507a1056cdb06451c75b427f9",
|
||||
"reference": "e94c8b1bbe2bc77507a1056cdb06451c75b427f9",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -2523,7 +2568,7 @@
|
|||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.13-dev"
|
||||
"dev-master": "1.17-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
|
@ -2556,20 +2601,20 @@
|
|||
"polyfill",
|
||||
"portable"
|
||||
],
|
||||
"time": "2019-11-27T13:56:44+00:00"
|
||||
"time": "2020-05-12T16:14:59+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-mbstring",
|
||||
"version": "v1.13.1",
|
||||
"version": "v1.17.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-mbstring.git",
|
||||
"reference": "7b4aab9743c30be783b73de055d24a39cf4b954f"
|
||||
"reference": "fa79b11539418b02fc5e1897267673ba2c19419c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/7b4aab9743c30be783b73de055d24a39cf4b954f",
|
||||
"reference": "7b4aab9743c30be783b73de055d24a39cf4b954f",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fa79b11539418b02fc5e1897267673ba2c19419c",
|
||||
"reference": "fa79b11539418b02fc5e1897267673ba2c19419c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -2581,7 +2626,7 @@
|
|||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.13-dev"
|
||||
"dev-master": "1.17-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
|
@ -2615,20 +2660,20 @@
|
|||
"portable",
|
||||
"shim"
|
||||
],
|
||||
"time": "2019-11-27T14:18:11+00:00"
|
||||
"time": "2020-05-12T16:47:27+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php73",
|
||||
"version": "v1.13.1",
|
||||
"version": "v1.17.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php73.git",
|
||||
"reference": "4b0e2222c55a25b4541305a053013d5647d3a25f"
|
||||
"reference": "a760d8964ff79ab9bf057613a5808284ec852ccc"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/4b0e2222c55a25b4541305a053013d5647d3a25f",
|
||||
"reference": "4b0e2222c55a25b4541305a053013d5647d3a25f",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/a760d8964ff79ab9bf057613a5808284ec852ccc",
|
||||
"reference": "a760d8964ff79ab9bf057613a5808284ec852ccc",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -2637,7 +2682,7 @@
|
|||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.13-dev"
|
||||
"dev-master": "1.17-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
|
@ -2673,7 +2718,7 @@
|
|||
"portable",
|
||||
"shim"
|
||||
],
|
||||
"time": "2019-11-27T16:25:15+00:00"
|
||||
"time": "2020-05-12T16:47:27+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/service-contracts",
|
||||
|
@ -2815,16 +2860,16 @@
|
|||
},
|
||||
{
|
||||
"name": "webmozart/assert",
|
||||
"version": "1.6.0",
|
||||
"version": "1.8.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/webmozart/assert.git",
|
||||
"reference": "573381c0a64f155a0d9a23f4b0c797194805b925"
|
||||
"reference": "ab2cb0b3b559010b75981b1bdce728da3ee90ad6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/webmozart/assert/zipball/573381c0a64f155a0d9a23f4b0c797194805b925",
|
||||
"reference": "573381c0a64f155a0d9a23f4b0c797194805b925",
|
||||
"url": "https://api.github.com/repos/webmozart/assert/zipball/ab2cb0b3b559010b75981b1bdce728da3ee90ad6",
|
||||
"reference": "ab2cb0b3b559010b75981b1bdce728da3ee90ad6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -2832,7 +2877,7 @@
|
|||
"symfony/polyfill-ctype": "^1.8"
|
||||
},
|
||||
"conflict": {
|
||||
"vimeo/psalm": "<3.6.0"
|
||||
"vimeo/psalm": "<3.9.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^4.8.36 || ^7.5.13"
|
||||
|
@ -2859,7 +2904,7 @@
|
|||
"check",
|
||||
"validate"
|
||||
],
|
||||
"time": "2019-11-24T13:36:37+00:00"
|
||||
"time": "2020-04-18T12:12:48+00:00"
|
||||
}
|
||||
],
|
||||
"aliases": [],
|
||||
|
|
|
@ -537,7 +537,7 @@ At the end of the menu:
|
|||
|
||||
At the end of file, before clearing floating blocks:
|
||||
|
||||
{if="!empty($plugin_errors) && isLoggedIn()"}
|
||||
{if="!empty($plugin_errors) && $is_logged_in"}
|
||||
<ul class="errors">
|
||||
{loop="plugin_errors"}
|
||||
<li>{$value}</li>
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
### Feeds options
|
||||
|
||||
Feeds are available in ATOM with `?do=atom` and RSS with `do=RSS`.
|
||||
Feeds are available in ATOM with `/feed/atom` and RSS with `/feed/rss`.
|
||||
|
||||
Options:
|
||||
|
||||
- You can use `permalinks` in the feed URL to get permalink to Shaares instead of direct link to shaared URL.
|
||||
- E.G. `https://my.shaarli.domain/?do=atom&permalinks`.
|
||||
- E.G. `https://my.shaarli.domain/feed/atom?permalinks`.
|
||||
- You can use `nb` parameter in the feed URL to specify the number of Shaares you want in a feed (default if not specified: `50`). The keyword `all` is available if you want everything.
|
||||
- `https://my.shaarli.domain/?do=atom&permalinks&nb=42`
|
||||
- `https://my.shaarli.domain/?do=atom&permalinks&nb=all`
|
||||
- `https://my.shaarli.domain/feed/atom?permalinks&nb=42`
|
||||
- `https://my.shaarli.domain/feed/atom?permalinks&nb=all`
|
||||
|
||||
### RSS Feeds or Picture Wall for a specific search/tag
|
||||
|
||||
|
@ -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`?)
|
||||
- You can also build the URLs manually:
|
||||
- `https://my.shaarli.domain/?do=rss&searchtags=nature`
|
||||
- `https://my.shaarli.domain/links/?do=picwall&searchterm=poney`
|
||||
- `https://my.shaarli.domain/links/picture-wall?searchterm=poney`
|
||||
|
||||
![](images/rss-filter-1.png) ![](images/rss-filter-2.png)
|
||||
|
|
|
@ -32,20 +32,20 @@ Here is a list :
|
|||
```
|
||||
http://<replace_domain>/
|
||||
http://<replace_domain>/?nonope
|
||||
http://<replace_domain>/?do=addlink
|
||||
http://<replace_domain>/?do=changepasswd
|
||||
http://<replace_domain>/?do=changetag
|
||||
http://<replace_domain>/?do=configure
|
||||
http://<replace_domain>/?do=tools
|
||||
http://<replace_domain>/?do=daily
|
||||
http://<replace_domain>/?post
|
||||
http://<replace_domain>/?do=export
|
||||
http://<replace_domain>/?do=import
|
||||
http://<replace_domain>/admin/add-shaare
|
||||
http://<replace_domain>/admin/password
|
||||
http://<replace_domain>/admin/tags
|
||||
http://<replace_domain>/admin/configure
|
||||
http://<replace_domain>/admin/tools
|
||||
http://<replace_domain>/daily
|
||||
http://<replace_domain>/admin/shaare
|
||||
http://<replace_domain>/admin/export
|
||||
http://<replace_domain>/admin/import
|
||||
http://<replace_domain>/login
|
||||
http://<replace_domain>/?do=picwall
|
||||
http://<replace_domain>/?do=pluginadmin
|
||||
http://<replace_domain>/?do=tagcloud
|
||||
http://<replace_domain>/?do=taglist
|
||||
http://<replace_domain>/picture-wall
|
||||
http://<replace_domain>/admin/plugins
|
||||
http://<replace_domain>/tags/cloud
|
||||
http://<replace_domain>/tags/list
|
||||
```
|
||||
|
||||
#### Improve existing translation
|
||||
|
|
File diff suppressed because it is too large
Load diff
85
init.php
Normal file
85
init.php
Normal 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");
|
|
@ -5,7 +5,7 @@
|
|||
* 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.
|
||||
|
@ -16,11 +16,11 @@
|
|||
*/
|
||||
function hook_addlink_toolbar_render_header($data)
|
||||
{
|
||||
if ($data['_PAGE_'] == Router::$PAGE_LINKLIST && $data['_LOGGEDIN_'] === true) {
|
||||
if ($data['_PAGE_'] == TemplatePage::LINKLIST && $data['_LOGGEDIN_'] === true) {
|
||||
$form = array(
|
||||
'attr' => array(
|
||||
'method' => 'GET',
|
||||
'action' => '',
|
||||
'action' => $data['_BASE_PATH_'] . '/admin/shaare',
|
||||
'name' => 'addform',
|
||||
'class' => 'addform',
|
||||
),
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<span>
|
||||
<a href="https://web.archive.org/web/%s">
|
||||
<img class="linklist-plugin-icon" src="plugins/archiveorg/internetarchive.png" title="%s" alt="archive.org" />
|
||||
<img class="linklist-plugin-icon" src="%s/archiveorg/internetarchive.png" title="%s" alt="archive.org" />
|
||||
</a>
|
||||
</span>
|
||||
|
|
|
@ -17,12 +17,13 @@
|
|||
function hook_archiveorg_render_linklist($data)
|
||||
{
|
||||
$archive_html = file_get_contents(PluginManager::$PLUGINS_PATH . '/archiveorg/archiveorg.html');
|
||||
$path = ($data['_BASE_PATH_'] ?? '') . '/' . PluginManager::$PLUGINS_PATH;
|
||||
|
||||
foreach ($data['links'] as &$value) {
|
||||
if ($value['private'] && preg_match('/^\?[a-zA-Z0-9-_@]{6}($|&|#)/', $value['real_url'])) {
|
||||
continue;
|
||||
}
|
||||
$archive = sprintf($archive_html, $value['url'], t('View on archive.org'));
|
||||
$archive = sprintf($archive_html, $value['url'], $path, t('View on archive.org'));
|
||||
$value['link_plugin'][] = $archive;
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
use Shaarli\Config\ConfigManager;
|
||||
use Shaarli\Plugin\PluginManager;
|
||||
use Shaarli\Router;
|
||||
use Shaarli\Render\TemplatePage;
|
||||
|
||||
/**
|
||||
* In the footer hook, there is a working example of a translation extension for Shaarli.
|
||||
|
@ -74,7 +74,7 @@ function demo_plugin_init($conf)
|
|||
function hook_demo_plugin_render_header($data)
|
||||
{
|
||||
// Only execute when linklist is rendered.
|
||||
if ($data['_PAGE_'] == Router::$PAGE_LINKLIST) {
|
||||
if ($data['_PAGE_'] == TemplatePage::LINKLIST) {
|
||||
// If loggedin
|
||||
if ($data['_LOGGEDIN_'] === true) {
|
||||
/*
|
||||
|
@ -118,7 +118,7 @@ function hook_demo_plugin_render_header($data)
|
|||
$form = array(
|
||||
'attr' => array(
|
||||
'method' => 'GET',
|
||||
'action' => '?',
|
||||
'action' => $data['_BASE_PATH_'] . '/',
|
||||
'class' => 'addform',
|
||||
),
|
||||
'inputs' => array(
|
||||
|
@ -441,9 +441,9 @@ function hook_demo_plugin_delete_link($data)
|
|||
function hook_demo_plugin_render_feed($data)
|
||||
{
|
||||
foreach ($data['links'] as &$link) {
|
||||
if ($data['_PAGE_'] == Router::$PAGE_FEED_ATOM) {
|
||||
if ($data['_PAGE_'] == TemplatePage::FEED_ATOM) {
|
||||
$link['description'] .= ' - ATOM Feed' ;
|
||||
} elseif ($data['_PAGE_'] == Router::$PAGE_FEED_RSS) {
|
||||
} elseif ($data['_PAGE_'] == TemplatePage::FEED_RSS) {
|
||||
$link['description'] .= ' - RSS Feed';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
use Shaarli\Config\ConfigManager;
|
||||
use Shaarli\Plugin\PluginManager;
|
||||
use Shaarli\Router;
|
||||
use Shaarli\Render\TemplatePage;
|
||||
|
||||
/**
|
||||
* Display an error everywhere if the plugin is enabled without configuration.
|
||||
|
@ -76,7 +76,7 @@ function hook_isso_render_linklist($data, $conf)
|
|||
*/
|
||||
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';
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
use Shaarli\Plugin\PluginManager;
|
||||
use Shaarli\Router;
|
||||
use Shaarli\Render\TemplatePage;
|
||||
|
||||
/**
|
||||
* When linklist is displayed, add play videos to header's toolbar.
|
||||
|
@ -18,7 +18,7 @@
|
|||
*/
|
||||
function hook_playvideos_render_header($data)
|
||||
{
|
||||
if ($data['_PAGE_'] == Router::$PAGE_LINKLIST) {
|
||||
if ($data['_PAGE_'] == TemplatePage::LINKLIST) {
|
||||
$playvideo = array(
|
||||
'attr' => array(
|
||||
'href' => '#',
|
||||
|
@ -42,7 +42,7 @@ function hook_playvideos_render_header($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/youtube_playlist.js';
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
use Shaarli\Config\ConfigManager;
|
||||
use Shaarli\Feed\FeedBuilder;
|
||||
use Shaarli\Plugin\PluginManager;
|
||||
use Shaarli\Router;
|
||||
use Shaarli\Render\TemplatePage;
|
||||
|
||||
/**
|
||||
* Plugin init function - set the hub to the default appspot one.
|
||||
|
@ -41,7 +41,7 @@ function pubsubhubbub_init($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');
|
||||
$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)
|
||||
{
|
||||
$feeds = array(
|
||||
index_url($_SERVER) .'?do=atom',
|
||||
index_url($_SERVER) .'?do=rss',
|
||||
index_url($_SERVER) .'feed/atom',
|
||||
index_url($_SERVER) .'feed/rss',
|
||||
);
|
||||
|
||||
$httpPost = function_exists('curl_version') ? false : 'nocurl_http_post';
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
use Shaarli\Plugin\PluginManager;
|
||||
use Shaarli\Router;
|
||||
use Shaarli\Render\TemplatePage;
|
||||
|
||||
/**
|
||||
* 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');
|
||||
|
||||
$path = ($data['_BASE_PATH_'] ?? '') . '/' . PluginManager::$PLUGINS_PATH;
|
||||
foreach ($data['links'] as &$value) {
|
||||
$qrcode = sprintf(
|
||||
$qrcode_html,
|
||||
$value['url'],
|
||||
PluginManager::$PLUGINS_PATH
|
||||
$path
|
||||
);
|
||||
$value['link_plugin'][] = $qrcode;
|
||||
}
|
||||
|
@ -40,8 +41,8 @@ function hook_qrcode_render_linklist($data)
|
|||
*/
|
||||
function hook_qrcode_render_footer($data)
|
||||
{
|
||||
if ($data['_PAGE_'] == Router::$PAGE_LINKLIST) {
|
||||
$data['js_files'][] = PluginManager::$PLUGINS_PATH . '/qrcode/shaarli-qrcode.js';
|
||||
if ($data['_PAGE_'] == TemplatePage::LINKLIST) {
|
||||
$data['js_files'][] = PluginManager::$PLUGINS_PATH . '/qrcode/shaarli-qrcode.js';
|
||||
}
|
||||
|
||||
return $data;
|
||||
|
@ -56,7 +57,7 @@ function hook_qrcode_render_footer($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';
|
||||
}
|
||||
|
||||
|
|
|
@ -34,8 +34,9 @@ function showQrCode(caller,loading)
|
|||
{
|
||||
if (!loading) // If javascript lib is still loading, do not append script to body.
|
||||
{
|
||||
var element = document.createElement("script");
|
||||
element.src = "plugins/qrcode/qr-1.1.3.min.js";
|
||||
var basePath = document.querySelector('input[name="js_base_path"]').value;
|
||||
var element = document.createElement("script");
|
||||
element.src = basePath + "/plugins/qrcode/qr-1.1.3.min.js";
|
||||
document.body.appendChild(element);
|
||||
}
|
||||
setTimeout(function() { showQrCode(caller,true);}, 200); // Retry in 200 milliseconds.
|
||||
|
|
|
@ -21,7 +21,7 @@ The directory structure should look like:
|
|||
|
||||
To enable the plugin, you can either:
|
||||
|
||||
* enable it in the plugins administration page (`?do=pluginadmin`).
|
||||
* enable it in the plugins administration page (`/admin/plugins`).
|
||||
* add `wallabag` to your list of enabled plugins in `data/config.json.php` (`general.enabled_plugins` section).
|
||||
|
||||
### Configuration
|
||||
|
|
|
@ -45,12 +45,14 @@ function hook_wallabag_render_linklist($data, $conf)
|
|||
$wallabagHtml = file_get_contents(PluginManager::$PLUGINS_PATH . '/wallabag/wallabag.html');
|
||||
|
||||
$linkTitle = t('Save to wallabag');
|
||||
$path = ($data['_BASE_PATH_'] ?? '') . '/' . PluginManager::$PLUGINS_PATH;
|
||||
|
||||
foreach ($data['links'] as &$value) {
|
||||
$wallabag = sprintf(
|
||||
$wallabagHtml,
|
||||
$wallabagInstance->getWallabagUrl(),
|
||||
urlencode($value['url']),
|
||||
PluginManager::$PLUGINS_PATH,
|
||||
$path,
|
||||
$linkTitle
|
||||
);
|
||||
$value['link_plugin'][] = $wallabag;
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue