Handle shaare creation/edition/deletion through Slim controllers

This commit is contained in:
ArthurHoaro 2020-06-06 14:01:03 +02:00
parent 8eac2e5488
commit c22fa57a55
20 changed files with 1121 additions and 280 deletions

View file

@ -91,6 +91,10 @@ function endsWith($haystack, $needle, $case = true)
*/ */
function escape($input) function escape($input)
{ {
if (null === $input) {
return null;
}
if (is_bool($input)) { if (is_bool($input)) {
return $input; return $input;
} }

View file

@ -2,112 +2,6 @@
use Shaarli\Bookmark\Bookmark; use Shaarli\Bookmark\Bookmark;
/**
* Get cURL callback function for CURLOPT_WRITEFUNCTION
*
* @param string $charset to extract from the downloaded page (reference)
* @param string $title to extract from the downloaded page (reference)
* @param string $description to extract from the downloaded page (reference)
* @param string $keywords to extract from the downloaded page (reference)
* @param bool $retrieveDescription Automatically tries to retrieve description and keywords from HTML content
* @param string $curlGetInfo Optionally overrides curl_getinfo function
*
* @return Closure
*/
function get_curl_download_callback(
&$charset,
&$title,
&$description,
&$keywords,
$retrieveDescription,
$curlGetInfo = 'curl_getinfo'
) {
$isRedirected = false;
$currentChunk = 0;
$foundChunk = null;
/**
* cURL callback function for CURLOPT_WRITEFUNCTION (called during the download).
*
* While downloading the remote page, we check that the HTTP code is 200 and content type is 'html/text'
* Then we extract the title and the charset and stop the download when it's done.
*
* @param resource $ch cURL resource
* @param string $data chunk of data being downloaded
*
* @return int|bool length of $data or false if we need to stop the download
*/
return function (&$ch, $data) use (
$retrieveDescription,
$curlGetInfo,
&$charset,
&$title,
&$description,
&$keywords,
&$isRedirected,
&$currentChunk,
&$foundChunk
) {
$currentChunk++;
$responseCode = $curlGetInfo($ch, CURLINFO_RESPONSE_CODE);
if (!empty($responseCode) && in_array($responseCode, [301, 302])) {
$isRedirected = true;
return strlen($data);
}
if (!empty($responseCode) && $responseCode !== 200) {
return false;
}
// After a redirection, the content type will keep the previous request value
// until it finds the next content-type header.
if (! $isRedirected || strpos(strtolower($data), 'content-type') !== false) {
$contentType = $curlGetInfo($ch, CURLINFO_CONTENT_TYPE);
}
if (!empty($contentType) && strpos($contentType, 'text/html') === false) {
return false;
}
if (!empty($contentType) && empty($charset)) {
$charset = header_extract_charset($contentType);
}
if (empty($charset)) {
$charset = html_extract_charset($data);
}
if (empty($title)) {
$title = html_extract_title($data);
$foundChunk = ! empty($title) ? $currentChunk : $foundChunk;
}
if ($retrieveDescription && empty($description)) {
$description = html_extract_tag('description', $data);
$foundChunk = ! empty($description) ? $currentChunk : $foundChunk;
}
if ($retrieveDescription && empty($keywords)) {
$keywords = html_extract_tag('keywords', $data);
if (! empty($keywords)) {
$foundChunk = $currentChunk;
// Keywords use the format tag1, tag2 multiple words, tag
// So we format them to match Shaarli's separator and glue multiple words with '-'
$keywords = implode(' ', array_map(function($keyword) {
return implode('-', preg_split('/\s+/', trim($keyword)));
}, explode(',', $keywords)));
}
}
// We got everything we want, stop the download.
// If we already found either the title, description or keywords,
// it's highly unlikely that we'll found the other metas further than
// in the same chunk of data or the next one. So we also stop the download after that.
if ((!empty($responseCode) && !empty($contentType) && !empty($charset)) && $foundChunk !== null
&& (! $retrieveDescription
|| $foundChunk < $currentChunk
|| (!empty($title) && !empty($description) && !empty($keywords))
)
) {
return false;
}
return strlen($data);
};
}
/** /**
* Extract title from an HTML document. * Extract title from an HTML document.
* *

View file

@ -10,11 +10,13 @@
use Shaarli\Feed\FeedBuilder; use Shaarli\Feed\FeedBuilder;
use Shaarli\Formatter\FormatterFactory; use Shaarli\Formatter\FormatterFactory;
use Shaarli\History; use Shaarli\History;
use Shaarli\Http\HttpAccess;
use Shaarli\Plugin\PluginManager; use Shaarli\Plugin\PluginManager;
use Shaarli\Render\PageBuilder; use Shaarli\Render\PageBuilder;
use Shaarli\Render\PageCacheManager; use Shaarli\Render\PageCacheManager;
use Shaarli\Security\LoginManager; use Shaarli\Security\LoginManager;
use Shaarli\Security\SessionManager; use Shaarli\Security\SessionManager;
use Shaarli\Thumbnailer;
/** /**
* Class ContainerBuilder * Class ContainerBuilder
@ -110,6 +112,14 @@ public function build(): ShaarliContainer
); );
}; };
$container['thumbnailer'] = function (ShaarliContainer $container): Thumbnailer {
return new Thumbnailer($container->conf);
};
$container['httpAccess'] = function (): HttpAccess {
return new HttpAccess();
};
return $container; return $container;
} }
} }

View file

@ -9,11 +9,13 @@
use Shaarli\Feed\FeedBuilder; use Shaarli\Feed\FeedBuilder;
use Shaarli\Formatter\FormatterFactory; use Shaarli\Formatter\FormatterFactory;
use Shaarli\History; use Shaarli\History;
use Shaarli\Http\HttpAccess;
use Shaarli\Plugin\PluginManager; use Shaarli\Plugin\PluginManager;
use Shaarli\Render\PageBuilder; use Shaarli\Render\PageBuilder;
use Shaarli\Render\PageCacheManager; use Shaarli\Render\PageCacheManager;
use Shaarli\Security\LoginManager; use Shaarli\Security\LoginManager;
use Shaarli\Security\SessionManager; use Shaarli\Security\SessionManager;
use Shaarli\Thumbnailer;
use Slim\Container; use Slim\Container;
/** /**
@ -31,6 +33,8 @@
* @property FormatterFactory $formatterFactory * @property FormatterFactory $formatterFactory
* @property PageCacheManager $pageCacheManager * @property PageCacheManager $pageCacheManager
* @property FeedBuilder $feedBuilder * @property FeedBuilder $feedBuilder
* @property Thumbnailer $thumbnailer
* @property HttpAccess $httpAccess
*/ */
class ShaarliContainer extends Container class ShaarliContainer extends Container
{ {

View file

@ -0,0 +1,258 @@
<?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\Thumbnailer;
use Slim\Http\Request;
use Slim\Http\Response;
/**
* Class PostBookmarkController
*
* Slim controller used to handle Shaarli create or edit bookmarks.
*/
class PostBookmarkController extends ShaarliAdminController
{
/**
* GET /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('addlink'));
}
/**
* GET /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 /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($id); // Read database
} catch (BookmarkNotFoundException $e) {
$this->saveErrorMessage(t('Bookmark not found'));
return $response->withRedirect('./');
}
$formatter = $this->container->formatterFactory->getFormatter('raw');
$link = $formatter->format($bookmark);
return $this->displayForm($link, false, $request, $response);
}
/**
* POST /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);
$data = $this->executeHooks('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()
);
}
public function deleteBookmark(Request $request, Response $response): Response
{
$this->checkToken($request);
$ids = escape(trim($request->getParam('lf_linkdate')));
if (strpos($ids, ' ') !== false) {
// multiple, space-separated ids provided
$ids = array_values(array_filter(preg_split('/\s+/', $ids), 'strlen'));
} 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');
foreach ($ids as $id) {
$id = (int) $id;
// TODO: check if it exists
$bookmark = $this->container->bookmarkService->get($id);
$data = $formatter->format($bookmark);
$this->container->pluginManager->executeHooks('delete_link', $data);
$this->container->bookmarkService->remove($bookmark, false);
}
$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 $response->withRedirect('./');
}
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),
];
$data = $this->executeHooks('render_editlink', $data);
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('editlink'));
}
/**
* @param mixed[] $data Variables passed to the template engine
*
* @return mixed[] Template data after active plugins render_picwall hook execution.
*/
protected function executeHooks(string $hook, array $data): array
{
$this->container->pluginManager->executeHooks(
$hook,
$data
);
return $data;
}
}

View file

@ -21,7 +21,7 @@ public function index(Request $request, Response $response): Response
'sslenabled' => is_https($this->container->environment), 'sslenabled' => is_https($this->container->environment),
]; ];
$this->executeHooks($data); $data = $this->executeHooks($data);
foreach ($data as $key => $value) { foreach ($data as $key => $value) {
$this->assignView($key, $value); $this->assignView($key, $value);

View file

@ -71,7 +71,7 @@ public function index(Request $request, Response $response): Response
]; ];
// Hooks are called before column construction so that plugins don't have to deal with columns. // Hooks are called before column construction so that plugins don't have to deal with columns.
$this->executeHooks($data); $data = $this->executeHooks($data);
$data['cols'] = $this->calculateColumns($data['linksToDisplay']); $data['cols'] = $this->calculateColumns($data['linksToDisplay']);

View file

@ -46,7 +46,7 @@ protected function processRequest(string $feedType, Request $request, Response $
$data = $this->container->feedBuilder->buildData($feedType, $request->getParams()); $data = $this->container->feedBuilder->buildData($feedType, $request->getParams());
$this->executeHooks($data, $feedType); $data = $this->executeHooks($data, $feedType);
$this->assignAllView($data); $this->assignAllView($data);
$content = $this->render('feed.'. $feedType); $content = $this->render('feed.'. $feedType);

View file

@ -78,16 +78,16 @@ protected function executeDefaultHooks(string $template): void
]; ];
foreach ($common_hooks as $name) { foreach ($common_hooks as $name) {
$plugin_data = []; $pluginData = [];
$this->container->pluginManager->executeHooks( $this->container->pluginManager->executeHooks(
'render_' . $name, 'render_' . $name,
$plugin_data, $pluginData,
[ [
'target' => $template, 'target' => $template,
'loggedin' => $this->container->loginManager->isLoggedIn() 'loggedin' => $this->container->loginManager->isLoggedIn()
] ]
); );
$this->assignView('plugins_' . $name, $plugin_data); $this->assignView('plugins_' . $name, $pluginData);
} }
} }
@ -102,9 +102,10 @@ protected function redirectFromReferer(
Request $request, Request $request,
Response $response, Response $response,
array $loopTerms = [], array $loopTerms = [],
array $clearParams = [] array $clearParams = [],
string $anchor = null
): Response { ): Response {
$defaultPath = $request->getUri()->getBasePath(); $defaultPath = rtrim($request->getUri()->getBasePath(), '/') . '/';
$referer = $this->container->environment['HTTP_REFERER'] ?? null; $referer = $this->container->environment['HTTP_REFERER'] ?? null;
if (null !== $referer) { if (null !== $referer) {
@ -133,7 +134,8 @@ protected function redirectFromReferer(
} }
$queryString = count($params) > 0 ? '?'. http_build_query($params) : ''; $queryString = count($params) > 0 ? '?'. http_build_query($params) : '';
$anchor = $anchor ? '#' . $anchor : '';
return $response->withRedirect($path . $queryString); return $response->withRedirect($path . $queryString . $anchor);
} }
} }

View file

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Shaarli\Http;
/**
* Class HttpAccess
*
* This is mostly an OOP wrapper for HTTP functions defined in `HttpUtils`.
* It is used as dependency injection in Shaarli's container.
*
* @package Shaarli\Http
*/
class HttpAccess
{
public function getHttpResponse($url, $timeout = 30, $maxBytes = 4194304, $curlWriteFunction = null)
{
return get_http_response($url, $timeout, $maxBytes, $curlWriteFunction);
}
public function getCurlDownloadCallback(
&$charset,
&$title,
&$description,
&$keywords,
$retrieveDescription,
$curlGetInfo = 'curl_getinfo'
) {
return get_curl_download_callback(
$charset,
$title,
$description,
$keywords,
$retrieveDescription,
$curlGetInfo
);
}
}

View file

@ -484,3 +484,109 @@ function is_https($server)
return ! empty($server['HTTPS']); return ! empty($server['HTTPS']);
} }
/**
* Get cURL callback function for CURLOPT_WRITEFUNCTION
*
* @param string $charset to extract from the downloaded page (reference)
* @param string $title to extract from the downloaded page (reference)
* @param string $description to extract from the downloaded page (reference)
* @param string $keywords to extract from the downloaded page (reference)
* @param bool $retrieveDescription Automatically tries to retrieve description and keywords from HTML content
* @param string $curlGetInfo Optionally overrides curl_getinfo function
*
* @return Closure
*/
function get_curl_download_callback(
&$charset,
&$title,
&$description,
&$keywords,
$retrieveDescription,
$curlGetInfo = 'curl_getinfo'
) {
$isRedirected = false;
$currentChunk = 0;
$foundChunk = null;
/**
* cURL callback function for CURLOPT_WRITEFUNCTION (called during the download).
*
* While downloading the remote page, we check that the HTTP code is 200 and content type is 'html/text'
* Then we extract the title and the charset and stop the download when it's done.
*
* @param resource $ch cURL resource
* @param string $data chunk of data being downloaded
*
* @return int|bool length of $data or false if we need to stop the download
*/
return function (&$ch, $data) use (
$retrieveDescription,
$curlGetInfo,
&$charset,
&$title,
&$description,
&$keywords,
&$isRedirected,
&$currentChunk,
&$foundChunk
) {
$currentChunk++;
$responseCode = $curlGetInfo($ch, CURLINFO_RESPONSE_CODE);
if (!empty($responseCode) && in_array($responseCode, [301, 302])) {
$isRedirected = true;
return strlen($data);
}
if (!empty($responseCode) && $responseCode !== 200) {
return false;
}
// After a redirection, the content type will keep the previous request value
// until it finds the next content-type header.
if (! $isRedirected || strpos(strtolower($data), 'content-type') !== false) {
$contentType = $curlGetInfo($ch, CURLINFO_CONTENT_TYPE);
}
if (!empty($contentType) && strpos($contentType, 'text/html') === false) {
return false;
}
if (!empty($contentType) && empty($charset)) {
$charset = header_extract_charset($contentType);
}
if (empty($charset)) {
$charset = html_extract_charset($data);
}
if (empty($title)) {
$title = html_extract_title($data);
$foundChunk = ! empty($title) ? $currentChunk : $foundChunk;
}
if ($retrieveDescription && empty($description)) {
$description = html_extract_tag('description', $data);
$foundChunk = ! empty($description) ? $currentChunk : $foundChunk;
}
if ($retrieveDescription && empty($keywords)) {
$keywords = html_extract_tag('keywords', $data);
if (! empty($keywords)) {
$foundChunk = $currentChunk;
// Keywords use the format tag1, tag2 multiple words, tag
// So we format them to match Shaarli's separator and glue multiple words with '-'
$keywords = implode(' ', array_map(function($keyword) {
return implode('-', preg_split('/\s+/', trim($keyword)));
}, explode(',', $keywords)));
}
}
// We got everything we want, stop the download.
// If we already found either the title, description or keywords,
// it's highly unlikely that we'll found the other metas further than
// in the same chunk of data or the next one. So we also stop the download after that.
if ((!empty($responseCode) && !empty($contentType) && !empty($charset)) && $foundChunk !== null
&& (! $retrieveDescription
|| $foundChunk < $currentChunk
|| (!empty($title) && !empty($description) && !empty($keywords))
)
) {
return false;
}
return strlen($data);
};
}

View file

@ -32,7 +32,7 @@ Here is a list :
``` ```
http://<replace_domain>/ http://<replace_domain>/
http://<replace_domain>/?nonope http://<replace_domain>/?nonope
http://<replace_domain>/?do=addlink http://<replace_domain>/add-shaare
http://<replace_domain>/?do=changepasswd http://<replace_domain>/?do=changepasswd
http://<replace_domain>/?do=changetag http://<replace_domain>/?do=changetag
http://<replace_domain>/configure http://<replace_domain>/configure

180
index.php
View file

@ -519,69 +519,20 @@ function renderPage($conf, $pluginManager, $bookmarkService, $history, $sessionM
// -------- User wants to rename a tag or delete it // -------- User wants to rename a tag or delete it
if ($targetPage == Router::$PAGE_CHANGETAG) { if ($targetPage == Router::$PAGE_CHANGETAG) {
header('./manage-tags'); header('Location: ./manage-tags');
exit; exit;
} }
// -------- User wants to add a link without using the bookmarklet: Show form. // -------- User wants to add a link without using the bookmarklet: Show form.
if ($targetPage == Router::$PAGE_ADDLINK) { if ($targetPage == Router::$PAGE_ADDLINK) {
$PAGE->assign('pagetitle', t('Shaare a new link') .' - '. $conf->get('general.title', 'Shaarli')); header('Location: ./shaare');
$PAGE->renderPage('addlink');
exit; exit;
} }
// -------- User clicked the "Save" button when editing a link: Save link to database. // -------- User clicked the "Save" button when editing a link: Save link to database.
if (isset($_POST['save_edit'])) { if (isset($_POST['save_edit'])) {
// Go away! // This route is no longer supported in legacy mode
if (! $sessionManager->checkToken($_POST['token'])) { header('Location: ./');
die(t('Wrong token.'));
}
// lf_id should only be present if the link exists.
$id = isset($_POST['lf_id']) ? intval(escape($_POST['lf_id'])) : null;
if ($id && $bookmarkService->exists($id)) {
// Edit
$bookmark = $bookmarkService->get($id);
} else {
// New link
$bookmark = new Bookmark();
}
$bookmark->setTitle($_POST['lf_title']);
$bookmark->setDescription($_POST['lf_description']);
$bookmark->setUrl($_POST['lf_url'], $conf->get('security.allowed_protocols'));
$bookmark->setPrivate(isset($_POST['lf_private']));
$bookmark->setTagsString($_POST['lf_tags']);
if ($conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE
&& ! $bookmark->isNote()
) {
$thumbnailer = new Thumbnailer($conf);
$bookmark->setThumbnail($thumbnailer->get($bookmark->getUrl()));
}
$bookmarkService->addOrSet($bookmark, false);
// To preserve backward compatibility with 3rd parties, plugins still use arrays
$factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
$formatter = $factory->getFormatter('raw');
$data = $formatter->format($bookmark);
$pluginManager->executeHooks('save_link', $data);
$bookmark->fromArray($data);
$bookmarkService->set($bookmark);
// If we are called from the bookmarklet, we must close the popup:
if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) {
echo '<script>self.close();</script>';
exit;
}
$returnurl = !empty($_POST['returnurl']) ? $_POST['returnurl'] : '?';
$location = generateLocation($returnurl, $_SERVER['HTTP_HOST'], array('addlink', 'post', 'edit_link'));
// Scroll to the link which has been edited.
$location .= '#' . $bookmark->getShortUrl();
// After saving the link, redirect to the page the user was on.
header('Location: '. $location);
exit; exit;
} }
@ -695,110 +646,13 @@ function ($item) {
// -------- User clicked the "EDIT" button on a link: Display link edit form. // -------- User clicked the "EDIT" button on a link: Display link edit form.
if (isset($_GET['edit_link'])) { if (isset($_GET['edit_link'])) {
$id = (int) escape($_GET['edit_link']); $id = (int) escape($_GET['edit_link']);
try { header('Location: ./shaare-' . $id);
$link = $bookmarkService->get($id); // Read database
} catch (BookmarkNotFoundException $e) {
// Link not found in database.
header('Location: ?');
exit;
}
$factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
$formatter = $factory->getFormatter('raw');
$formattedLink = $formatter->format($link);
$tags = $bookmarkService->bookmarksCountPerTag();
if ($conf->get('formatter') === 'markdown') {
$tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1;
}
$data = array(
'link' => $formattedLink,
'link_is_new' => false,
'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''),
'tags' => $tags,
);
$pluginManager->executeHooks('render_editlink', $data);
foreach ($data as $key => $value) {
$PAGE->assign($key, $value);
}
$PAGE->assign('pagetitle', t('Edit') .' '. t('Shaare') .' - '. $conf->get('general.title', 'Shaarli'));
$PAGE->renderPage('editlink');
exit; exit;
} }
// -------- User want to post a new link: Display link edit form. // -------- User want to post a new link: Display link edit form.
if (isset($_GET['post'])) { if (isset($_GET['post'])) {
$url = cleanup_url($_GET['post']); header('Location: ./shaare?' . http_build_query($_GET));
$link_is_new = false;
// Check if URL is not already in database (in this case, we will edit the existing link)
$bookmark = $bookmarkService->findByUrl($url);
if (! $bookmark) {
$link_is_new = true;
// Get title if it was provided in URL (by the bookmarklet).
$title = empty($_GET['title']) ? '' : escape($_GET['title']);
// Get description if it was provided in URL (by the bookmarklet). [Bronco added that]
$description = empty($_GET['description']) ? '' : escape($_GET['description']);
$tags = empty($_GET['tags']) ? '' : escape($_GET['tags']);
$private = !empty($_GET['private']) && $_GET['private'] === "1" ? 1 : 0;
// If this is an HTTP(S) link, we try go get the page to extract
// the title (otherwise we will to straight to the edit form.)
if (empty($title) && strpos(get_url_scheme($url), 'http') !== false) {
$retrieveDescription = $conf->get('general.retrieve_description');
// Short timeout to keep the application responsive
// The callback will fill $charset and $title with data from the downloaded page.
get_http_response(
$url,
$conf->get('general.download_timeout', 30),
$conf->get('general.download_max_size', 4194304),
get_curl_download_callback($charset, $title, $description, $tags, $retrieveDescription)
);
if (! empty($title) && strtolower($charset) != 'utf-8') {
$title = mb_convert_encoding($title, 'utf-8', $charset);
}
}
if ($url == '') {
$title = $conf->get('general.default_note_title', t('Note: '));
}
$url = escape($url);
$title = escape($title);
$link = [
'title' => $title,
'url' => $url,
'description' => $description,
'tags' => $tags,
'private' => $private,
];
} else {
$factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
$formatter = $factory->getFormatter('raw');
$link = $formatter->format($bookmark);
}
$tags = $bookmarkService->bookmarksCountPerTag();
if ($conf->get('formatter') === 'markdown') {
$tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1;
}
$data = [
'link' => $link,
'link_is_new' => $link_is_new,
'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''),
'source' => (isset($_GET['source']) ? $_GET['source'] : ''),
'tags' => $tags,
'default_private_links' => $conf->get('privacy.default_private_links', false),
];
$pluginManager->executeHooks('render_editlink', $data);
foreach ($data as $key => $value) {
$PAGE->assign($key, $value);
}
$PAGE->assign('pagetitle', t('Shaare') .' - '. $conf->get('general.title', 'Shaarli'));
$PAGE->renderPage('editlink');
exit; exit;
} }
@ -1351,19 +1205,29 @@ function install($conf, $sessionManager, $loginManager)
$this->post('/configure', '\Shaarli\Front\Controller\Admin\ConfigureController:save')->setName('saveConfigure'); $this->post('/configure', '\Shaarli\Front\Controller\Admin\ConfigureController:save')->setName('saveConfigure');
$this->get('/manage-tags', '\Shaarli\Front\Controller\Admin\ManageTagController:index')->setName('manageTag'); $this->get('/manage-tags', '\Shaarli\Front\Controller\Admin\ManageTagController:index')->setName('manageTag');
$this->post('/manage-tags', '\Shaarli\Front\Controller\Admin\ManageTagController:save')->setName('saveManageTag'); $this->post('/manage-tags', '\Shaarli\Front\Controller\Admin\ManageTagController:save')->setName('saveManageTag');
$this->get('/add-shaare', '\Shaarli\Front\Controller\Admin\PostBookmarkController:addShaare')->setName('addShaare');
$this
->get('/shaare', '\Shaarli\Front\Controller\Admin\PostBookmarkController:displayCreateForm')
->setName('newShaare');
$this
->get('/shaare-{id}', '\Shaarli\Front\Controller\Admin\PostBookmarkController:displayEditForm')
->setName('editShaare');
$this
->post('/shaare', '\Shaarli\Front\Controller\Admin\PostBookmarkController:save')
->setName('saveShaare');
$this
->get('/delete-shaare', '\Shaarli\Front\Controller\Admin\PostBookmarkController:deleteBookmark')
->setName('deleteShaare');
$this $this
->get('/links-per-page', '\Shaarli\Front\Controller\Admin\SessionFilterController:linksPerPage') ->get('/links-per-page', '\Shaarli\Front\Controller\Admin\SessionFilterController:linksPerPage')
->setName('filter-links-per-page') ->setName('filter-links-per-page');
;
$this $this
->get('/visibility/{visibility}', '\Shaarli\Front\Controller\Admin\SessionFilterController:visibility') ->get('/visibility/{visibility}', '\Shaarli\Front\Controller\Admin\SessionFilterController:visibility')
->setName('visibility') ->setName('visibility');
;
$this $this
->get('/untagged-only', '\Shaarli\Front\Controller\Admin\SessionFilterController:untaggedOnly') ->get('/untagged-only', '\Shaarli\Front\Controller\Admin\SessionFilterController:untaggedOnly')
->setName('untagged-only') ->setName('untagged-only');
;
})->add('\Shaarli\Front\ShaarliMiddleware'); })->add('\Shaarli\Front\ShaarliMiddleware');
$response = $app->run(true); $response = $app->run(true);

View file

@ -10,11 +10,13 @@
use Shaarli\Feed\FeedBuilder; use Shaarli\Feed\FeedBuilder;
use Shaarli\Formatter\FormatterFactory; use Shaarli\Formatter\FormatterFactory;
use Shaarli\History; use Shaarli\History;
use Shaarli\Http\HttpAccess;
use Shaarli\Plugin\PluginManager; use Shaarli\Plugin\PluginManager;
use Shaarli\Render\PageBuilder; use Shaarli\Render\PageBuilder;
use Shaarli\Render\PageCacheManager; use Shaarli\Render\PageCacheManager;
use Shaarli\Security\LoginManager; use Shaarli\Security\LoginManager;
use Shaarli\Security\SessionManager; use Shaarli\Security\SessionManager;
use Shaarli\Thumbnailer;
/** /**
* Test helper allowing auto-completion for MockObjects. * Test helper allowing auto-completion for MockObjects.
@ -31,6 +33,8 @@
* @property MockObject|FormatterFactory $formatterFactory * @property MockObject|FormatterFactory $formatterFactory
* @property MockObject|PageCacheManager $pageCacheManager * @property MockObject|PageCacheManager $pageCacheManager
* @property MockObject|FeedBuilder $feedBuilder * @property MockObject|FeedBuilder $feedBuilder
* @property MockObject|Thumbnailer $thumbnailer
* @property MockObject|HttpAccess $httpAccess
*/ */
class ShaarliTestContainer extends ShaarliContainer class ShaarliTestContainer extends ShaarliContainer
{ {

View file

@ -0,0 +1,652 @@
<?php
declare(strict_types=1);
namespace Shaarli\Front\Controller\Admin;
use PHPUnit\Framework\TestCase;
use Shaarli\Bookmark\Bookmark;
use Shaarli\Config\ConfigManager;
use Shaarli\Front\Exception\WrongTokenException;
use Shaarli\Http\HttpAccess;
use Shaarli\Security\SessionManager;
use Shaarli\Thumbnailer;
use Slim\Http\Request;
use Slim\Http\Response;
use Slim\Http\Uri;
class PostBookmarkControllerTest extends TestCase
{
use FrontAdminControllerMockHelper;
/** @var PostBookmarkController */
protected $controller;
public function setUp(): void
{
$this->createContainer();
$this->container->httpAccess = $this->createMock(HttpAccess::class);
$this->controller = new PostBookmarkController($this->container);
}
/**
* Test displaying add link page
*/
public function testAddShaare(): void
{
$assignedVariables = [];
$this->assignTemplateVars($assignedVariables);
$request = $this->createMock(Request::class);
$response = new Response();
$result = $this->controller->addShaare($request, $response);
static::assertSame(200, $result->getStatusCode());
static::assertSame('addlink', (string) $result->getBody());
static::assertSame('Shaare a new link - Shaarli', $assignedVariables['pagetitle']);
}
/**
* Test displaying bookmark create form
* Ensure that every step of the standard workflow works properly.
*/
public function testDisplayCreateFormWithUrl(): void
{
$this->container->environment = [
'HTTP_REFERER' => $referer = 'http://shaarli/subfolder/controller/?searchtag=abc'
];
$assignedVariables = [];
$this->assignTemplateVars($assignedVariables);
$url = 'http://url.tld/other?part=3&utm_ad=pay#hash';
$expectedUrl = str_replace('&utm_ad=pay', '', $url);
$remoteTitle = 'Remote Title';
$remoteDesc = 'Sometimes the meta description is relevant.';
$remoteTags = 'abc def';
$request = $this->createMock(Request::class);
$request->method('getParam')->willReturnCallback(function (string $key) use ($url): ?string {
return $key === 'post' ? $url : null;
});
$response = new Response();
$this->container->httpAccess
->expects(static::once())
->method('getCurlDownloadCallback')
->willReturnCallback(
function (&$charset, &$title, &$description, &$tags) use (
$remoteTitle,
$remoteDesc,
$remoteTags
): callable {
return function () use (
&$charset,
&$title,
&$description,
&$tags,
$remoteTitle,
$remoteDesc,
$remoteTags
): void {
$charset = 'ISO-8859-1';
$title = $remoteTitle;
$description = $remoteDesc;
$tags = $remoteTags;
};
}
)
;
$this->container->httpAccess
->expects(static::once())
->method('getHttpResponse')
->with($expectedUrl, 30, 4194304)
->willReturnCallback(function($url, $timeout, $maxBytes, $callback): void {
$callback();
})
;
$this->container->bookmarkService
->expects(static::once())
->method('bookmarksCountPerTag')
->willReturn($tags = ['tag1' => 2, 'tag2' => 1])
;
// Make sure that PluginManager hook is triggered
$this->container->pluginManager
->expects(static::at(0))
->method('executeHooks')
->willReturnCallback(function (string $hook, array $data) use ($remoteTitle, $remoteDesc): array {
static::assertSame('render_editlink', $hook);
static::assertSame($remoteTitle, $data['link']['title']);
static::assertSame($remoteDesc, $data['link']['description']);
return $data;
})
;
$result = $this->controller->displayCreateForm($request, $response);
static::assertSame(200, $result->getStatusCode());
static::assertSame('editlink', (string) $result->getBody());
static::assertSame('Shaare - Shaarli', $assignedVariables['pagetitle']);
static::assertSame($expectedUrl, $assignedVariables['link']['url']);
static::assertSame($remoteTitle, $assignedVariables['link']['title']);
static::assertSame($remoteDesc, $assignedVariables['link']['description']);
static::assertSame($remoteTags, $assignedVariables['link']['tags']);
static::assertFalse($assignedVariables['link']['private']);
static::assertTrue($assignedVariables['link_is_new']);
static::assertSame($referer, $assignedVariables['http_referer']);
static::assertSame($tags, $assignedVariables['tags']);
static::assertArrayHasKey('source', $assignedVariables);
static::assertArrayHasKey('default_private_links', $assignedVariables);
}
/**
* Test displaying bookmark create form
* Ensure all available query parameters are handled properly.
*/
public function testDisplayCreateFormWithFullParameters(): void
{
$assignedVariables = [];
$this->assignTemplateVars($assignedVariables);
$parameters = [
'post' => 'http://url.tld/other?part=3&utm_ad=pay#hash',
'title' => 'Provided Title',
'description' => 'Provided description.',
'tags' => 'abc def',
'private' => '1',
'source' => 'apps',
];
$expectedUrl = str_replace('&utm_ad=pay', '', $parameters['post']);
$request = $this->createMock(Request::class);
$request
->method('getParam')
->willReturnCallback(function (string $key) use ($parameters): ?string {
return $parameters[$key] ?? null;
});
$response = new Response();
$result = $this->controller->displayCreateForm($request, $response);
static::assertSame(200, $result->getStatusCode());
static::assertSame('editlink', (string) $result->getBody());
static::assertSame('Shaare - Shaarli', $assignedVariables['pagetitle']);
static::assertSame($expectedUrl, $assignedVariables['link']['url']);
static::assertSame($parameters['title'], $assignedVariables['link']['title']);
static::assertSame($parameters['description'], $assignedVariables['link']['description']);
static::assertSame($parameters['tags'], $assignedVariables['link']['tags']);
static::assertTrue($assignedVariables['link']['private']);
static::assertTrue($assignedVariables['link_is_new']);
static::assertSame($parameters['source'], $assignedVariables['source']);
}
/**
* Test displaying bookmark create form
* Without any parameter.
*/
public function testDisplayCreateFormEmpty(): void
{
$assignedVariables = [];
$this->assignTemplateVars($assignedVariables);
$request = $this->createMock(Request::class);
$response = new Response();
$this->container->httpAccess->expects(static::never())->method('getHttpResponse');
$this->container->httpAccess->expects(static::never())->method('getCurlDownloadCallback');
$result = $this->controller->displayCreateForm($request, $response);
static::assertSame(200, $result->getStatusCode());
static::assertSame('editlink', (string) $result->getBody());
static::assertSame('', $assignedVariables['link']['url']);
static::assertSame('Note: ', $assignedVariables['link']['title']);
static::assertSame('', $assignedVariables['link']['description']);
static::assertSame('', $assignedVariables['link']['tags']);
static::assertFalse($assignedVariables['link']['private']);
static::assertTrue($assignedVariables['link_is_new']);
}
/**
* Test displaying bookmark create form
* URL not using HTTP protocol: do not try to retrieve the title
*/
public function testDisplayCreateFormNotHttp(): void
{
$assignedVariables = [];
$this->assignTemplateVars($assignedVariables);
$url = 'magnet://kubuntu.torrent';
$request = $this->createMock(Request::class);
$request
->method('getParam')
->willReturnCallback(function (string $key) use ($url): ?string {
return $key === 'post' ? $url : null;
});
$response = new Response();
$this->container->httpAccess->expects(static::never())->method('getHttpResponse');
$this->container->httpAccess->expects(static::never())->method('getCurlDownloadCallback');
$result = $this->controller->displayCreateForm($request, $response);
static::assertSame(200, $result->getStatusCode());
static::assertSame('editlink', (string) $result->getBody());
static::assertSame($url, $assignedVariables['link']['url']);
static::assertTrue($assignedVariables['link_is_new']);
}
/**
* Test displaying bookmark create form
* When markdown formatter is enabled, the no markdown tag should be added to existing tags.
*/
public function testDisplayCreateFormWithMarkdownEnabled(): void
{
$assignedVariables = [];
$this->assignTemplateVars($assignedVariables);
$this->container->conf = $this->createMock(ConfigManager::class);
$this->container->conf
->expects(static::atLeastOnce())
->method('get')->willReturnCallback(function (string $key): ?string {
if ($key === 'formatter') {
return 'markdown';
}
return $key;
})
;
$request = $this->createMock(Request::class);
$response = new Response();
$result = $this->controller->displayCreateForm($request, $response);
static::assertSame(200, $result->getStatusCode());
static::assertSame('editlink', (string) $result->getBody());
static::assertSame(['nomarkdown' => 1], $assignedVariables['tags']);
}
/**
* Test displaying bookmark create form
* When an existing URL is submitted, we want to edit the existing link.
*/
public function testDisplayCreateFormWithExistingUrl(): void
{
$assignedVariables = [];
$this->assignTemplateVars($assignedVariables);
$url = 'http://url.tld/other?part=3&utm_ad=pay#hash';
$expectedUrl = str_replace('&utm_ad=pay', '', $url);
$request = $this->createMock(Request::class);
$request
->method('getParam')
->willReturnCallback(function (string $key) use ($url): ?string {
return $key === 'post' ? $url : null;
});
$response = new Response();
$this->container->httpAccess->expects(static::never())->method('getHttpResponse');
$this->container->httpAccess->expects(static::never())->method('getCurlDownloadCallback');
$this->container->bookmarkService
->expects(static::once())
->method('findByUrl')
->with($expectedUrl)
->willReturn(
(new Bookmark())
->setId($id = 23)
->setUrl($expectedUrl)
->setTitle($title = 'Bookmark Title')
->setDescription($description = 'Bookmark description.')
->setTags($tags = ['abc', 'def'])
->setPrivate(true)
->setCreated($createdAt = new \DateTime('2020-06-10 18:45:44'))
)
;
$result = $this->controller->displayCreateForm($request, $response);
static::assertSame(200, $result->getStatusCode());
static::assertSame('editlink', (string) $result->getBody());
static::assertSame('Edit Shaare - Shaarli', $assignedVariables['pagetitle']);
static::assertFalse($assignedVariables['link_is_new']);
static::assertSame($id, $assignedVariables['link']['id']);
static::assertSame($expectedUrl, $assignedVariables['link']['url']);
static::assertSame($title, $assignedVariables['link']['title']);
static::assertSame($description, $assignedVariables['link']['description']);
static::assertSame(implode(' ', $tags), $assignedVariables['link']['tags']);
static::assertTrue($assignedVariables['link']['private']);
static::assertSame($createdAt, $assignedVariables['link']['created']);
}
/**
* Test displaying bookmark edit form
* When an existing ID is provided, ensure that default workflow works properly.
*/
public function testDisplayEditFormDefault(): void
{
$assignedVariables = [];
$this->assignTemplateVars($assignedVariables);
$id = 11;
$request = $this->createMock(Request::class);
$response = new Response();
$this->container->httpAccess->expects(static::never())->method('getHttpResponse');
$this->container->httpAccess->expects(static::never())->method('getCurlDownloadCallback');
$this->container->bookmarkService
->expects(static::once())
->method('get')
->with($id)
->willReturn(
(new Bookmark())
->setId($id)
->setUrl($url = 'http://domain.tld')
->setTitle($title = 'Bookmark Title')
->setDescription($description = 'Bookmark description.')
->setTags($tags = ['abc', 'def'])
->setPrivate(true)
->setCreated($createdAt = new \DateTime('2020-06-10 18:45:44'))
)
;
$result = $this->controller->displayEditForm($request, $response, ['id' => (string) $id]);
static::assertSame(200, $result->getStatusCode());
static::assertSame('editlink', (string) $result->getBody());
static::assertSame('Edit Shaare - Shaarli', $assignedVariables['pagetitle']);
static::assertFalse($assignedVariables['link_is_new']);
static::assertSame($id, $assignedVariables['link']['id']);
static::assertSame($url, $assignedVariables['link']['url']);
static::assertSame($title, $assignedVariables['link']['title']);
static::assertSame($description, $assignedVariables['link']['description']);
static::assertSame(implode(' ', $tags), $assignedVariables['link']['tags']);
static::assertTrue($assignedVariables['link']['private']);
static::assertSame($createdAt, $assignedVariables['link']['created']);
}
/**
* Test save a new bookmark
*/
public function testSaveBookmark(): void
{
$id = 21;
$parameters = [
'lf_url' => 'http://url.tld/other?part=3#hash',
'lf_title' => 'Provided Title',
'lf_description' => 'Provided description.',
'lf_tags' => 'abc def',
'lf_private' => '1',
'returnurl' => 'http://shaarli.tld/subfolder/add-shaare'
];
$request = $this->createMock(Request::class);
$request
->method('getParam')
->willReturnCallback(function (string $key) use ($parameters): ?string {
return $parameters[$key] ?? null;
})
;
$request->method('getUri')->willReturnCallback(function (): Uri {
$uri = $this->createMock(Uri::class);
$uri->method('getBasePath')->willReturn('/subfolder');
return $uri;
});
$response = new Response();
$checkBookmark = function (Bookmark $bookmark) use ($parameters) {
static::assertSame($parameters['lf_url'], $bookmark->getUrl());
static::assertSame($parameters['lf_title'], $bookmark->getTitle());
static::assertSame($parameters['lf_description'], $bookmark->getDescription());
static::assertSame($parameters['lf_tags'], $bookmark->getTagsString());
static::assertTrue($bookmark->isPrivate());
};
$this->container->bookmarkService
->expects(static::once())
->method('addOrSet')
->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): void {
static::assertFalse($save);
$checkBookmark($bookmark);
$bookmark->setId($id);
})
;
$this->container->bookmarkService
->expects(static::once())
->method('set')
->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): void {
static::assertTrue($save);
$checkBookmark($bookmark);
static::assertSame($id, $bookmark->getId());
})
;
// Make sure that PluginManager hook is triggered
$this->container->pluginManager
->expects(static::at(0))
->method('executeHooks')
->willReturnCallback(function (string $hook, array $data) use ($parameters, $id): array {
static::assertSame('save_link', $hook);
static::assertSame($id, $data['id']);
static::assertSame($parameters['lf_url'], $data['url']);
static::assertSame($parameters['lf_title'], $data['title']);
static::assertSame($parameters['lf_description'], $data['description']);
static::assertSame($parameters['lf_tags'], $data['tags']);
static::assertTrue($data['private']);
return $data;
})
;
$result = $this->controller->save($request, $response);
static::assertSame(302, $result->getStatusCode());
static::assertRegExp('@/subfolder/#\w{6}@', $result->getHeader('location')[0]);
}
/**
* Test save an existing bookmark
*/
public function testSaveExistingBookmark(): void
{
$id = 21;
$parameters = [
'lf_id' => (string) $id,
'lf_url' => 'http://url.tld/other?part=3#hash',
'lf_title' => 'Provided Title',
'lf_description' => 'Provided description.',
'lf_tags' => 'abc def',
'lf_private' => '1',
'returnurl' => 'http://shaarli.tld/subfolder/?page=2'
];
$request = $this->createMock(Request::class);
$request
->method('getParam')
->willReturnCallback(function (string $key) use ($parameters): ?string {
return $parameters[$key] ?? null;
})
;
$request->method('getUri')->willReturnCallback(function (): Uri {
$uri = $this->createMock(Uri::class);
$uri->method('getBasePath')->willReturn('/subfolder');
return $uri;
});
$response = new Response();
$checkBookmark = function (Bookmark $bookmark) use ($parameters, $id) {
static::assertSame($id, $bookmark->getId());
static::assertSame($parameters['lf_url'], $bookmark->getUrl());
static::assertSame($parameters['lf_title'], $bookmark->getTitle());
static::assertSame($parameters['lf_description'], $bookmark->getDescription());
static::assertSame($parameters['lf_tags'], $bookmark->getTagsString());
static::assertTrue($bookmark->isPrivate());
};
$this->container->bookmarkService->expects(static::atLeastOnce())->method('exists')->willReturn(true);
$this->container->bookmarkService
->expects(static::once())
->method('get')
->willReturn((new Bookmark())->setId($id)->setUrl('http://other.url'))
;
$this->container->bookmarkService
->expects(static::once())
->method('addOrSet')
->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): void {
static::assertFalse($save);
$checkBookmark($bookmark);
})
;
$this->container->bookmarkService
->expects(static::once())
->method('set')
->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): void {
static::assertTrue($save);
$checkBookmark($bookmark);
static::assertSame($id, $bookmark->getId());
})
;
// Make sure that PluginManager hook is triggered
$this->container->pluginManager
->expects(static::at(0))
->method('executeHooks')
->willReturnCallback(function (string $hook, array $data) use ($parameters, $id): array {
static::assertSame('save_link', $hook);
static::assertSame($id, $data['id']);
static::assertSame($parameters['lf_url'], $data['url']);
static::assertSame($parameters['lf_title'], $data['title']);
static::assertSame($parameters['lf_description'], $data['description']);
static::assertSame($parameters['lf_tags'], $data['tags']);
static::assertTrue($data['private']);
return $data;
})
;
$result = $this->controller->save($request, $response);
static::assertSame(302, $result->getStatusCode());
static::assertRegExp('@/subfolder/\?page=2#\w{6}@', $result->getHeader('location')[0]);
}
/**
* Test save a bookmark - try to retrieve the thumbnail
*/
public function testSaveBookmarkWithThumbnail(): void
{
$parameters = ['lf_url' => 'http://url.tld/other?part=3#hash'];
$request = $this->createMock(Request::class);
$request
->method('getParam')
->willReturnCallback(function (string $key) use ($parameters): ?string {
return $parameters[$key] ?? null;
})
;
$request->method('getUri')->willReturnCallback(function (): Uri {
$uri = $this->createMock(Uri::class);
$uri->method('getBasePath')->willReturn('/subfolder');
return $uri;
});
$response = new Response();
$this->container->conf = $this->createMock(ConfigManager::class);
$this->container->conf->method('get')->willReturnCallback(function (string $key, $default) {
return $key === 'thumbnails.mode' ? Thumbnailer::MODE_ALL : $default;
});
$this->container->thumbnailer = $this->createMock(Thumbnailer::class);
$this->container->thumbnailer
->expects(static::once())
->method('get')
->with($parameters['lf_url'])
->willReturn($thumb = 'http://thumb.url')
;
$this->container->bookmarkService
->expects(static::once())
->method('addOrSet')
->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($thumb): void {
static::assertSame($thumb, $bookmark->getThumbnail());
})
;
$result = $this->controller->save($request, $response);
static::assertSame(302, $result->getStatusCode());
}
/**
* Change the password with a wrong existing password
*/
public function testSaveBookmarkFromBookmarklet(): void
{
$parameters = ['source' => 'bookmarklet'];
$request = $this->createMock(Request::class);
$request
->method('getParam')
->willReturnCallback(function (string $key) use ($parameters): ?string {
return $parameters[$key] ?? null;
})
;
$response = new Response();
$result = $this->controller->save($request, $response);
static::assertSame(200, $result->getStatusCode());
static::assertSame('<script>self.close();</script>', (string) $result->getBody());
}
/**
* Change the password with a wrong existing password
*/
public function testSaveBookmarkWrongToken(): void
{
$this->container->sessionManager = $this->createMock(SessionManager::class);
$this->container->sessionManager->method('checkToken')->willReturn(false);
$this->container->bookmarkService->expects(static::never())->method('addOrSet');
$this->container->bookmarkService->expects(static::never())->method('set');
$request = $this->createMock(Request::class);
$response = new Response();
$this->expectException(WrongTokenException::class);
$this->controller->save($request, $response);
}
}

View file

@ -9,7 +9,7 @@
<div class="pure-u-lg-1-3 pure-u-1-24"></div> <div class="pure-u-lg-1-3 pure-u-1-24"></div>
<div id="addlink-form" class="page-form page-form-light pure-u-lg-1-3 pure-u-22-24"> <div id="addlink-form" class="page-form page-form-light pure-u-lg-1-3 pure-u-22-24">
<h2 class="window-title">{"Shaare a new link"|t}</h2> <h2 class="window-title">{"Shaare a new link"|t}</h2>
<form method="GET" action="#" name="addform" class="addform"> <form method="GET" action="./shaare" name="addform" class="addform">
<div> <div>
<label for="shaare">{'URL or leave empty to post a note'|t}</label> <label for="shaare">{'URL or leave empty to post a note'|t}</label>
<input type="text" name="post" id="shaare" class="autofocus"> <input type="text" name="post" id="shaare" class="autofocus">

View file

@ -7,7 +7,11 @@
{include="page.header"} {include="page.header"}
<div id="editlinkform" class="edit-link-container" class="pure-g"> <div id="editlinkform" class="edit-link-container" class="pure-g">
<div class="pure-u-lg-1-5 pure-u-1-24"></div> <div class="pure-u-lg-1-5 pure-u-1-24"></div>
<form method="post" name="linkform" class="page-form pure-u-lg-3-5 pure-u-22-24 page-form page-form-light"> <form method="post"
name="linkform"
action="./shaare"
class="page-form pure-u-lg-3-5 pure-u-22-24 page-form page-form-light"
>
<h2 class="window-title"> <h2 class="window-title">
{if="!$link_is_new"}{'Edit Shaare'|t}{else}{'New Shaare'|t}{/if} {if="!$link_is_new"}{'Edit Shaare'|t}{else}{'New Shaare'|t}{/if}
</h2> </h2>

View file

@ -21,7 +21,7 @@
</li> </li>
{if="$is_logged_in || $openshaarli"} {if="$is_logged_in || $openshaarli"}
<li class="pure-menu-item"> <li class="pure-menu-item">
<a href="./?do=addlink" class="pure-menu-link" id="shaarli-menu-shaare"> <a href="./add-shaare" class="pure-menu-link" id="shaarli-menu-shaare">
<i class="fa fa-plus" aria-hidden="true"></i> {'Shaare'|t} <i class="fa fa-plus" aria-hidden="true"></i> {'Shaare'|t}
</a> </a>
</li> </li>

View file

@ -5,7 +5,7 @@
<div id="pageheader"> <div id="pageheader">
{include="page.header"} {include="page.header"}
<div id="headerform"> <div id="headerform">
<form method="GET" action="" name="addform" class="addform"> <form method="GET" action="./shaare" name="addform" class="addform">
<input type="text" name="post" class="linkurl"> <input type="text" name="post" class="linkurl">
<input type="submit" value="Add link" class="bigbutton"> <input type="submit" value="Add link" class="bigbutton">
</form> </form>

View file

@ -20,10 +20,10 @@
{if="$is_logged_in"} {if="$is_logged_in"}
<li><a href="./logout">Logout</a></li> <li><a href="./logout">Logout</a></li>
<li><a href="./tools">Tools</a></li> <li><a href="./tools">Tools</a></li>
<li><a href="?do=addlink">Add link</a></li> <li><a href="./add-shaare">Add link</a></li>
{elseif="$openshaarli"} {elseif="$openshaarli"}
<li><a href="./tools">Tools</a></li> <li><a href="./tools">Tools</a></li>
<li><a href="./?do=addlink">Add link</a></li> <li><a href="./add-shaare">Add link</a></li>
{else} {else}
<li><a href="./login">Login</a></li> <li><a href="./login">Login</a></li>
{/if} {/if}