php7-openssl \
php7-session \
php7-xml \
+ php7-simplexml \
php7-zlib \
s6
use DateTime;
use Exception;
use Shaarli\Bookmark\Bookmark;
+use Shaarli\Helper\FileUtils;
/**
* Class History
return $formatter->format($date);
}
+/**
+ * Format the date month according to the locale.
+ *
+ * @param DateTimeInterface $date to format.
+ *
+ * @return bool|string Formatted date, or false if the input is invalid.
+ */
+function format_month(DateTimeInterface $date)
+{
+ if (! $date instanceof DateTimeInterface) {
+ return false;
+ }
+
+ return strftime('%B', $date->getTimestamp());
+}
+
+
/**
* Check if the input is an integer, no matter its real type.
*
* Wrapper function for translation which match the API
* of gettext()/_() and ngettext().
*
- * @param string $text Text to translate.
- * @param string $nText The plural message ID.
- * @param int $nb The number of items for plural forms.
- * @param string $domain The domain where the translation is stored (default: shaarli).
+ * @param string $text Text to translate.
+ * @param string $nText The plural message ID.
+ * @param int $nb The number of items for plural forms.
+ * @param string $domain The domain where the translation is stored (default: shaarli).
+ * @param array $variables Associative array of variables to replace in translated text.
+ * @param bool $fixCase Apply `ucfirst` on the translated string, might be useful for strings with variables.
*
* @return string Text translated.
*/
-function t($text, $nText = '', $nb = 1, $domain = 'shaarli')
+function t($text, $nText = '', $nb = 1, $domain = 'shaarli', $variables = [], $fixCase = false)
{
- return dn__($domain, $text, $nText, $nb);
+ $postFunction = $fixCase ? 'ucfirst' : function ($input) { return $input; };
+
+ return $postFunction(dn__($domain, $text, $nText, $nb, $variables));
}
/**
$this->bookmarkService->add($bookmark);
$out = ApiUtils::formatLink($bookmark, index_url($this->ci['environment']));
- $redirect = $this->ci->router->relativePathFor('getLink', ['id' => $bookmark->getId()]);
+ $redirect = $this->ci->router->pathFor('getLink', ['id' => $bookmark->getId()]);
return $response->withAddedHeader('Location', $redirect)
->withJson($out, 201, $this->jsonStyle);
}
/**
* @inheritDoc
*/
- public function findByHash(string $hash): Bookmark
+ public function findByHash(string $hash, string $privateKey = null): Bookmark
{
$bookmark = $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_HASH, $hash);
// PHP 7.3 introduced array_key_first() to avoid this hack
$first = reset($bookmark);
- if (! $this->isLoggedIn && $first->isPrivate()) {
- throw new Exception('Not authorized');
+ if (!$this->isLoggedIn
+ && $first->isPrivate()
+ && (empty($privateKey) || $privateKey !== $first->getAdditionalContentEntry('private_key'))
+ ) {
+ throw new BookmarkNotFoundException();
}
return $first;
/**
* @inheritDoc
*/
- public function days(): array
- {
- $bookmarkDays = [];
- foreach ($this->search() as $bookmark) {
- $bookmarkDays[$bookmark->getCreated()->format('Ymd')] = 0;
+ public function findByDate(
+ \DateTimeInterface $from,
+ \DateTimeInterface $to,
+ ?\DateTimeInterface &$previous,
+ ?\DateTimeInterface &$next
+ ): array {
+ $out = [];
+ $previous = null;
+ $next = null;
+
+ foreach ($this->search([], null, false, false, true) as $bookmark) {
+ if ($to < $bookmark->getCreated()) {
+ $next = $bookmark->getCreated();
+ } else if ($from < $bookmark->getCreated() && $to > $bookmark->getCreated()) {
+ $out[] = $bookmark;
+ } else {
+ if ($previous !== null) {
+ break;
+ }
+ $previous = $bookmark->getCreated();
+ }
}
- $bookmarkDays = array_keys($bookmarkDays);
- sort($bookmarkDays);
- return array_map('strval', $bookmarkDays);
+ return $out;
}
/**
* @inheritDoc
*/
- public function filterDay(string $request)
+ public function getLatest(): ?Bookmark
{
- $visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC;
+ foreach ($this->search([], null, false, false, true) as $bookmark) {
+ return $bookmark;
+ }
- return $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_DAY, $request, false, $visibility);
+ return null;
}
/**
/**
* Find a bookmark by hash
*
- * @param string $hash
+ * @param string $hash Bookmark's hash
+ * @param string|null $privateKey Optional key used to access private links while logged out
*
* @return Bookmark
*
* @throws \Exception
*/
- public function findByHash(string $hash): Bookmark;
+ public function findByHash(string $hash, string $privateKey = null);
/**
* @param $url
public function bookmarksCountPerTag(array $filteringTags = [], ?string $visibility = null): array;
/**
- * Returns the list of days containing articles (oldest first)
+ * Return a list of bookmark matching provided period of time.
+ * It also update directly previous and next date outside of given period found in the datastore.
*
- * @return array containing days (in format YYYYMMDD).
+ * @param \DateTimeInterface $from Starting date.
+ * @param \DateTimeInterface $to Ending date.
+ * @param \DateTimeInterface|null $previous (by reference) updated with first created date found before $from.
+ * @param \DateTimeInterface|null $next (by reference) updated with first created date found after $to.
+ *
+ * @return array List of bookmarks matching provided period of time.
*/
- public function days(): array;
+ public function findByDate(
+ \DateTimeInterface $from,
+ \DateTimeInterface $to,
+ ?\DateTimeInterface &$previous,
+ ?\DateTimeInterface &$next
+ ): array;
/**
- * Returns the list of articles for a given day.
- *
- * @param string $request day to filter. Format: YYYYMMDD.
+ * Returns the latest bookmark by creation date.
*
- * @return Bookmark[] list of shaare found.
- *
- * @throws BookmarkNotFoundException
+ * @return Bookmark|null Found Bookmark or null if the datastore is empty.
*/
- public function filterDay(string $request);
+ public function getLatest(): ?Bookmark;
/**
* Creates the default database after a fresh install.
$data = file_get_contents($filepath);
$data = str_replace(self::getPhpHeaders(), '', $data);
$data = str_replace(self::getPhpSuffix(), '', $data);
- $data = json_decode($data, true);
+ $data = json_decode(trim($data), true);
if ($data === null) {
$errorCode = json_last_error();
$error = sprintf(
*/
public static function getPhpHeaders()
{
- return '<?php /*'. PHP_EOL;
+ return '<?php /*';
}
/**
*/
public static function getPhpSuffix()
{
- return PHP_EOL . '*/ ?>';
+ return '*/ ?>';
}
}
+++ /dev/null
-<?php
-
-declare(strict_types=1);
-
-namespace Shaarli\Front\Controller\Admin;
-
-use Shaarli\Bookmark\Bookmark;
-use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
-use Shaarli\Formatter\BookmarkMarkdownFormatter;
-use Shaarli\Render\TemplatePage;
-use Shaarli\Thumbnailer;
-use Slim\Http\Request;
-use Slim\Http\Response;
-
-/**
- * Class PostBookmarkController
- *
- * Slim controller used to handle Shaarli create or edit bookmarks.
- */
-class ManageShaareController extends ShaarliAdminController
-{
- /**
- * GET /admin/add-shaare - Displays the form used to create a new bookmark from an URL
- */
- public function addShaare(Request $request, Response $response): Response
- {
- $this->assignView(
- 'pagetitle',
- t('Shaare a new link') .' - '. $this->container->conf->get('general.title', 'Shaarli')
- );
-
- return $response->write($this->render(TemplatePage::ADDLINK));
- }
-
- /**
- * GET /admin/shaare - Displays the bookmark form for creation.
- * Note that if the URL is found in existing bookmarks, then it will be in edit mode.
- */
- public function displayCreateForm(Request $request, Response $response): Response
- {
- $url = cleanup_url($request->getParam('post'));
-
- $linkIsNew = false;
- // Check if URL is not already in database (in this case, we will edit the existing link)
- $bookmark = $this->container->bookmarkService->findByUrl($url);
- if (null === $bookmark) {
- $linkIsNew = true;
- // Get shaare data if it was provided in URL (e.g.: by the bookmarklet).
- $title = $request->getParam('title');
- $description = $request->getParam('description');
- $tags = $request->getParam('tags');
- $private = filter_var($request->getParam('private'), FILTER_VALIDATE_BOOLEAN);
-
- // If this is an HTTP(S) link, we try go get the page to extract
- // the title (otherwise we will to straight to the edit form.)
- if (true !== $this->container->conf->get('general.enable_async_metadata', true)
- && empty($title)
- && strpos(get_url_scheme($url) ?: '', 'http') !== false
- ) {
- $metadata = $this->container->metadataRetriever->retrieve($url);
- }
-
- if (empty($url)) {
- $metadata['title'] = $this->container->conf->get('general.default_note_title', t('Note: '));
- }
-
- $link = [
- 'title' => $title ?? $metadata['title'] ?? '',
- 'url' => $url ?? '',
- 'description' => $description ?? $metadata['description'] ?? '',
- 'tags' => $tags ?? $metadata['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') !== null ? 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
- && true !== $this->container->conf->get('general.enable_async_metadata', true)
- && $bookmark->shouldUpdateThumbnail()
- ) {
- $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,
- ['/admin/add-shaare', '/admin/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 = escape([
- 'link' => $link,
- 'link_is_new' => $isNew,
- 'http_referer' => $this->container->environment['HTTP_REFERER'] ?? '',
- 'source' => $request->getParam('source') ?? '',
- 'tags' => $tags,
- 'default_private_links' => $this->container->conf->get('privacy.default_private_links', false),
- 'async_metadata' => $this->container->conf->get('general.enable_async_metadata', true),
- 'retrieve_description' => $this->container->conf->get('general.retrieve_description', false),
- ]);
-
- $this->executePageHooks('render_editlink', $data, TemplatePage::EDIT_LINK);
-
- foreach ($data as $key => $value) {
- $this->assignView($key, $value);
- }
-
- $editLabel = false === $isNew ? t('Edit') .' ' : '';
- $this->assignView(
- 'pagetitle',
- $editLabel . t('Shaare') .' - '. $this->container->conf->get('general.title', 'Shaarli')
- );
-
- return $response->write($this->render(TemplatePage::EDIT_LINK));
- }
-}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Admin;
+
+use Shaarli\Helper\ApplicationUtils;
+use Shaarli\Helper\FileUtils;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+/**
+ * Slim controller used to handle Server administration page, and actions.
+ */
+class ServerController extends ShaarliAdminController
+{
+ /** @var string Cache type - main - by default pagecache/ and tmp/ */
+ protected const CACHE_MAIN = 'main';
+
+ /** @var string Cache type - thumbnails - by default cache/ */
+ protected const CACHE_THUMB = 'thumbnails';
+
+ /**
+ * GET /admin/server - Display page Server administration
+ */
+ public function index(Request $request, Response $response): Response
+ {
+ $latestVersion = 'v' . ApplicationUtils::getVersion(
+ ApplicationUtils::$GIT_RAW_URL . '/latest/' . ApplicationUtils::$VERSION_FILE
+ );
+ $currentVersion = ApplicationUtils::getVersion('./shaarli_version.php');
+ $currentVersion = $currentVersion === 'dev' ? $currentVersion : 'v' . $currentVersion;
+ $phpEol = new \DateTimeImmutable(ApplicationUtils::getPhpEol(PHP_VERSION));
+
+ $this->assignView('php_version', PHP_VERSION);
+ $this->assignView('php_eol', format_date($phpEol, false));
+ $this->assignView('php_has_reached_eol', $phpEol < new \DateTimeImmutable());
+ $this->assignView('php_extensions', ApplicationUtils::getPhpExtensionsRequirement());
+ $this->assignView('permissions', ApplicationUtils::checkResourcePermissions($this->container->conf));
+ $this->assignView('release_url', ApplicationUtils::$GITHUB_URL . '/releases/tag/' . $latestVersion);
+ $this->assignView('latest_version', $latestVersion);
+ $this->assignView('current_version', $currentVersion);
+ $this->assignView('thumbnails_mode', $this->container->conf->get('thumbnails.mode'));
+ $this->assignView('index_url', index_url($this->container->environment));
+ $this->assignView('client_ip', client_ip_id($this->container->environment));
+ $this->assignView('trusted_proxies', $this->container->conf->get('security.trusted_proxies', []));
+
+ $this->assignView(
+ 'pagetitle',
+ t('Server administration') . ' - ' . $this->container->conf->get('general.title', 'Shaarli')
+ );
+
+ return $response->write($this->render('server'));
+ }
+
+ /**
+ * GET /admin/clear-cache?type={$type} - Action to trigger cache folder clearing (either main or thumbnails).
+ */
+ public function clearCache(Request $request, Response $response): Response
+ {
+ $exclude = ['.htaccess'];
+
+ if ($request->getQueryParam('type') === static::CACHE_THUMB) {
+ $folders = [$this->container->conf->get('resource.thumbnails_cache')];
+
+ $this->saveWarningMessage(
+ t('Thumbnails cache has been cleared.') . ' ' .
+ '<a href="'. $this->container->basePath .'/admin/thumbnails">' . t('Please synchronize them.') .'</a>'
+ );
+ } else {
+ $folders = [
+ $this->container->conf->get('resource.page_cache'),
+ $this->container->conf->get('resource.raintpl_tmp'),
+ ];
+
+ $this->saveSuccessMessage(t('Shaarli\'s cache folder has been cleared!'));
+ }
+
+ // Make sure that we don't delete root cache folder
+ $folders = array_map('realpath', array_values(array_filter(array_map('trim', $folders))));
+ foreach ($folders as $folder) {
+ FileUtils::clearFolder($folder, false, $exclude);
+ }
+
+ return $this->redirect($response, '/admin/server');
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Admin;
+
+use Shaarli\Formatter\BookmarkMarkdownFormatter;
+use Shaarli\Render\TemplatePage;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+class ShaareAddController 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
+ {
+ $tags = $this->container->bookmarkService->bookmarksCountPerTag();
+ if ($this->container->conf->get('formatter') === 'markdown') {
+ $tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1;
+ }
+
+ $this->assignView(
+ 'pagetitle',
+ t('Shaare a new link') .' - '. $this->container->conf->get('general.title', 'Shaarli')
+ );
+ $this->assignView('tags', $tags);
+ $this->assignView('default_private_links', $this->container->conf->get('privacy.default_private_links', false));
+ $this->assignView('async_metadata', $this->container->conf->get('general.enable_async_metadata', true));
+
+ return $response->write($this->render(TemplatePage::ADDLINK));
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Admin;
+
+use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+/**
+ * Class PostBookmarkController
+ *
+ * Slim controller used to handle Shaarli create or edit bookmarks.
+ */
+class ShaareManageController extends ShaarliAdminController
+{
+ /**
+ * 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']);
+ }
+
+ /**
+ * GET /admin/shaare/private/{hash} - Attach a private key to given bookmark, then redirect to the sharing URL.
+ */
+ public function sharePrivate(Request $request, Response $response, array $args): Response
+ {
+ $this->checkToken($request);
+
+ $hash = $args['hash'] ?? '';
+ $bookmark = $this->container->bookmarkService->findByHash($hash);
+
+ if ($bookmark->isPrivate() !== true) {
+ return $this->redirect($response, '/shaare/' . $hash);
+ }
+
+ if (empty($bookmark->getAdditionalContentEntry('private_key'))) {
+ $privateKey = bin2hex(random_bytes(16));
+ $bookmark->addAdditionalContentEntry('private_key', $privateKey);
+ $this->container->bookmarkService->set($bookmark);
+ }
+
+ return $this->redirect(
+ $response,
+ '/shaare/' . $hash . '?key=' . $bookmark->getAdditionalContentEntry('private_key')
+ );
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Admin;
+
+use Shaarli\Bookmark\Bookmark;
+use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
+use Shaarli\Formatter\BookmarkFormatter;
+use Shaarli\Formatter\BookmarkMarkdownFormatter;
+use Shaarli\Render\TemplatePage;
+use Shaarli\Thumbnailer;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+class ShaarePublishController extends ShaarliAdminController
+{
+ /**
+ * @var BookmarkFormatter[] Statically cached instances of formatters
+ */
+ protected $formatters = [];
+
+ /**
+ * @var array Statically cached bookmark's tags counts
+ */
+ protected $tags;
+
+ /**
+ * 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'));
+ $link = $this->buildLinkDataFromUrl($request, $url);
+
+ return $this->displayForm($link, $link['linkIsNew'], $request, $response);
+ }
+
+ /**
+ * POST /admin/shaare-batch - Displays multiple creation/edit forms from bulk add in add-link page.
+ */
+ public function displayCreateBatchForms(Request $request, Response $response): Response
+ {
+ $urls = array_map('cleanup_url', explode(PHP_EOL, $request->getParam('urls')));
+
+ $links = [];
+ foreach ($urls as $url) {
+ if (empty($url)) {
+ continue;
+ }
+ $link = $this->buildLinkDataFromUrl($request, $url);
+ $data = $this->buildFormData($link, $link['linkIsNew'], $request);
+ $data['token'] = $this->container->sessionManager->generateToken();
+ $data['source'] = 'batch';
+
+ $this->executePageHooks('render_editlink', $data, TemplatePage::EDIT_LINK);
+
+ $links[] = $data;
+ }
+
+ $this->assignView('links', $links);
+ $this->assignView('batch_mode', true);
+ $this->assignView('async_metadata', $this->container->conf->get('general.enable_async_metadata', true));
+
+ return $response->write($this->render(TemplatePage::EDIT_LINK_BATCH));
+ }
+
+ /**
+ * 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->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') !== null ? 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
+ && true !== $this->container->conf->get('general.enable_async_metadata', true)
+ && $bookmark->shouldUpdateThumbnail()
+ ) {
+ $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->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>');
+ } elseif ($request->getParam('source') === 'batch') {
+ return $response;
+ }
+
+ if (!empty($request->getParam('returnurl'))) {
+ $this->container->environment['HTTP_REFERER'] = $request->getParam('returnurl');
+ }
+
+ return $this->redirectFromReferer(
+ $request,
+ $response,
+ ['/admin/add-shaare', '/admin/shaare'], ['addlink', 'post', 'edit_link'],
+ $bookmark->getShortUrl()
+ );
+ }
+
+ /**
+ * 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
+ {
+ $data = $this->buildFormData($link, $isNew, $request);
+
+ $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));
+ }
+
+ protected function buildLinkDataFromUrl(Request $request, string $url): array
+ {
+ // 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) {
+ // 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');
+ if ($request->getParam('private') !== null) {
+ $private = filter_var($request->getParam('private'), FILTER_VALIDATE_BOOLEAN);
+ } else {
+ $private = $this->container->conf->get('privacy.default_private_links', false);
+ }
+
+ // 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 (true !== $this->container->conf->get('general.enable_async_metadata', true)
+ && empty($title)
+ && strpos(get_url_scheme($url) ?: '', 'http') !== false
+ ) {
+ $metadata = $this->container->metadataRetriever->retrieve($url);
+ }
+
+ if (empty($url)) {
+ $metadata['title'] = $this->container->conf->get('general.default_note_title', t('Note: '));
+ }
+
+ return [
+ 'title' => $title ?? $metadata['title'] ?? '',
+ 'url' => $url ?? '',
+ 'description' => $description ?? $metadata['description'] ?? '',
+ 'tags' => $tags ?? $metadata['tags'] ?? '',
+ 'private' => $private,
+ 'linkIsNew' => true,
+ ];
+ }
+
+ $formatter = $this->getFormatter('raw');
+ $link = $formatter->format($bookmark);
+ $link['linkIsNew'] = false;
+
+ return $link;
+ }
+
+ protected function buildFormData(array $link, bool $isNew, Request $request): array
+ {
+ return escape([
+ 'link' => $link,
+ 'link_is_new' => $isNew,
+ 'http_referer' => $this->container->environment['HTTP_REFERER'] ?? '',
+ 'source' => $request->getParam('source') ?? '',
+ 'tags' => $this->getTags(),
+ 'default_private_links' => $this->container->conf->get('privacy.default_private_links', false),
+ 'async_metadata' => $this->container->conf->get('general.enable_async_metadata', true),
+ 'retrieve_description' => $this->container->conf->get('general.retrieve_description', false),
+ ]);
+ }
+
+ /**
+ * Memoize formatterFactory->getFormatter() calls.
+ */
+ protected function getFormatter(string $type): BookmarkFormatter
+ {
+ if (!array_key_exists($type, $this->formatters) || $this->formatters[$type] === null) {
+ $this->formatters[$type] = $this->container->formatterFactory->getFormatter($type);
+ }
+
+ return $this->formatters[$type];
+ }
+
+ /**
+ * Memoize bookmarkService->bookmarksCountPerTag() calls.
+ */
+ protected function getTags(): array
+ {
+ if ($this->tags === null) {
+ $this->tags = $this->container->bookmarkService->bookmarksCountPerTag();
+
+ if ($this->container->conf->get('formatter') === 'markdown') {
+ $this->tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1;
+ }
+ }
+
+ return $this->tags;
+ }
+}
*/
public function permalink(Request $request, Response $response, array $args): Response
{
+ $privateKey = $request->getParam('key');
+
try {
- $bookmark = $this->container->bookmarkService->findByHash($args['hash']);
+ $bookmark = $this->container->bookmarkService->findByHash($args['hash'], $privateKey);
} catch (BookmarkNotFoundException $e) {
$this->assignView('error_message', $e->getMessage());
*/
protected function updateThumbnail(Bookmark $bookmark, bool $writeDatastore = true): bool
{
- // Logged in, not async retrieval, thumbnails enabled, and thumbnail should be updated
- if ($this->container->loginManager->isLoggedIn()
- && true !== $this->container->conf->get('general.enable_async_metadata', true)
- && $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE
- && $bookmark->shouldUpdateThumbnail()
- ) {
- $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl()));
- $this->container->bookmarkService->set($bookmark, $writeDatastore);
-
- return true;
+ if (false === $this->container->loginManager->isLoggedIn()) {
+ return false;
+ }
+
+ // If thumbnail should be updated, we reset it to null
+ if ($bookmark->shouldUpdateThumbnail()) {
+ $bookmark->setThumbnail(null);
+
+ // Requires an update, not async retrieval, thumbnails enabled
+ if ($bookmark->shouldUpdateThumbnail()
+ && true !== $this->container->conf->get('general.enable_async_metadata', true)
+ && $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE
+ ) {
+ $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl()));
+ $this->container->bookmarkService->set($bookmark, $writeDatastore);
+
+ return true;
+ }
}
return false;
namespace Shaarli\Front\Controller\Visitor;
use DateTime;
-use DateTimeImmutable;
use Shaarli\Bookmark\Bookmark;
+use Shaarli\Helper\DailyPageHelper;
use Shaarli\Render\TemplatePage;
use Slim\Http\Request;
use Slim\Http\Response;
*/
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 = [];
- }
+ $type = DailyPageHelper::extractRequestedType($request);
+ $format = DailyPageHelper::getFormatByType($type);
+ $latestBookmark = $this->container->bookmarkService->getLatest();
+ $dateTime = DailyPageHelper::extractRequestedDateTime($type, $request->getQueryParam($type), $latestBookmark);
+ $start = DailyPageHelper::getStartDateTimeByType($type, $dateTime);
+ $end = DailyPageHelper::getEndDateTimeByType($type, $dateTime);
+ $dailyDesc = DailyPageHelper::getDescriptionByType($type, $dateTime);
+
+ $linksToDisplay = $this->container->bookmarkService->findByDate(
+ $start,
+ $end,
+ $previousDay,
+ $nextDay
+ );
$formatter = $this->container->formatterFactory->getFormatter();
$formatter->addContextData('base_path', $this->container->basePath);
$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 ?? '',
+ 'dayDate' => $start,
+ 'day' => $start->getTimestamp(),
+ 'previousday' => $previousDay ? $previousDay->format($format) : '',
+ 'nextday' => $nextDay ? $nextDay->format($format) : '',
+ 'dayDesc' => $dailyDesc,
+ 'type' => $type,
+ 'localizedType' => $this->translateType($type),
];
// Hooks are called before column construction so that plugins don't have to deal with columns.
$mainTitle = $this->container->conf->get('general.title', 'Shaarli');
$this->assignView(
'pagetitle',
- t('Daily') .' - '. format_date($dayDate, false) . ' - ' . $mainTitle
+ $data['localizedType'] . ' - ' . $data['dayDesc'] . ' - ' . $mainTitle
);
return $response->write($this->render(TemplatePage::DAILY));
}
$days = [];
+ $type = DailyPageHelper::extractRequestedType($request);
+ $format = DailyPageHelper::getFormatByType($type);
+ $length = DailyPageHelper::getRssLengthByType($type);
foreach ($this->container->bookmarkService->search() as $bookmark) {
- $day = $bookmark->getCreated()->format('Ymd');
+ $day = $bookmark->getCreated()->format($format);
// Stop iterating after DAILY_RSS_NB_DAYS entries
- if (count($days) === static::$DAILY_RSS_NB_DAYS && !isset($days[$day])) {
+ if (count($days) === $length && !isset($days[$day])) {
break;
}
/** @var Bookmark[] $bookmarks */
foreach ($days as $day => $bookmarks) {
- $dayDatetime = DateTimeImmutable::createFromFormat(Bookmark::LINK_DATE_FORMAT, $day.'_000000');
+ $dayDateTime = DailyPageHelper::extractRequestedDateTime($type, (string) $day);
+ $endDateTime = DailyPageHelper::getEndDateTimeByType($type, $dayDateTime);
+
+ // We only want the RSS entry to be published when the period is over.
+ if (new DateTime() < $endDateTime) {
+ continue;
+ }
+
$dataPerDay[$day] = [
- 'date' => $dayDatetime,
- 'date_rss' => $dayDatetime->format(DateTime::RSS),
- 'date_human' => format_date($dayDatetime, false, true),
- 'absolute_url' => $indexUrl . 'daily?day=' . $day,
+ 'date' => $endDateTime,
+ 'date_rss' => $endDateTime->format(DateTime::RSS),
+ 'date_human' => DailyPageHelper::getDescriptionByType($type, $dayDateTime),
+ 'absolute_url' => $indexUrl . 'daily?'. $type .'=' . $day,
'links' => [],
];
// Make permalink URL absolute
if ($bookmark->isNote()) {
- $dataPerDay[$day]['links'][$key]['url'] = $indexUrl . $bookmark->getUrl();
+ $dataPerDay[$day]['links'][$key]['url'] = rtrim($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);
+ $this->assignAllView([
+ 'title' => $this->container->conf->get('general.title', 'Shaarli'),
+ 'index_url' => $indexUrl,
+ 'page_url' => $pageUrl,
+ 'hide_timestamps' => $this->container->conf->get('privacy.hide_timestamps', false),
+ 'days' => $dataPerDay,
+ 'type' => $type,
+ 'localizedType' => $this->translateType($type),
+ ]);
$rssContent = $this->render(TemplatePage::DAILY_RSS);
return $columns;
}
+
+ protected function translateType($type): string
+ {
+ return [
+ t('day') => t('Daily'),
+ t('week') => t('Weekly'),
+ t('month') => t('Monthly'),
+ ][t($type)] ?? t('Daily');
+ }
}
namespace Shaarli\Front\Controller\Visitor;
-use Shaarli\ApplicationUtils;
use Shaarli\Container\ShaarliContainer;
use Shaarli\Front\Exception\AlreadyInstalledException;
use Shaarli\Front\Exception\ResourcePermissionException;
+use Shaarli\Helper\ApplicationUtils;
use Shaarli\Languages;
use Shaarli\Security\SessionManager;
use Slim\Http\Request;
$this->assignView('cities', $cities);
$this->assignView('languages', Languages::getAvailableLanguages());
+ $phpEol = new \DateTimeImmutable(ApplicationUtils::getPhpEol(PHP_VERSION));
+
+ $this->assignView('php_version', PHP_VERSION);
+ $this->assignView('php_eol', format_date($phpEol, false));
+ $this->assignView('php_has_reached_eol', $phpEol < new \DateTimeImmutable());
+ $this->assignView('php_extensions', ApplicationUtils::getPhpExtensionsRequirement());
+ $this->assignView('permissions', ApplicationUtils::checkResourcePermissions($this->container->conf));
+
+ $this->assignView('pagetitle', t('Install Shaarli'));
+
return $response->write($this->render('install'));
}
protected function checkPermissions(): bool
{
// Ensure Shaarli has proper access to its resources
- $errors = ApplicationUtils::checkResourcePermissions($this->container->conf);
+ $errors = ApplicationUtils::checkResourcePermissions($this->container->conf, true);
if (empty($errors)) {
return true;
}
<?php
-namespace Shaarli;
+namespace Shaarli\Helper;
use Exception;
use Shaarli\Config\ConfigManager;
*/
public static $VERSION_FILE = 'shaarli_version.php';
- private static $GIT_URL = 'https://raw.githubusercontent.com/shaarli/Shaarli';
- private static $GIT_BRANCHES = array('latest', 'stable');
+ public static $GITHUB_URL = 'https://github.com/shaarli/Shaarli';
+ public static $GIT_RAW_URL = 'https://raw.githubusercontent.com/shaarli/Shaarli';
+ public static $GIT_BRANCHES = array('latest', 'stable');
private static $VERSION_START_TAG = '<?php /* ';
private static $VERSION_END_TAG = ' */ ?>';
// Late Static Binding allows overriding within tests
// See http://php.net/manual/en/language.oop5.late-static-bindings.php
$latestVersion = static::getVersion(
- self::$GIT_URL . '/' . $branch . '/' . self::$VERSION_FILE
+ self::$GIT_RAW_URL . '/' . $branch . '/' . self::$VERSION_FILE
);
if (!$latestVersion) {
/**
* Checks Shaarli has the proper access permissions to its resources
*
- * @param ConfigManager $conf Configuration Manager instance.
+ * @param ConfigManager $conf Configuration Manager instance.
+ * @param bool $minimalMode In minimal mode we only check permissions to be able to display a template.
+ * Currently we only need to be able to read the theme and write in raintpl cache.
*
* @return array A list of the detected configuration issues
*/
- public static function checkResourcePermissions($conf)
+ public static function checkResourcePermissions(ConfigManager $conf, bool $minimalMode = false): array
{
- $errors = array();
+ $errors = [];
$rainTplDir = rtrim($conf->get('resource.raintpl_tpl'), '/');
// Check script and template directories are readable
- foreach (array(
+ foreach ([
'application',
'inc',
'plugins',
$rainTplDir,
$rainTplDir . '/' . $conf->get('resource.theme'),
- ) as $path) {
+ ] as $path) {
if (!is_readable(realpath($path))) {
$errors[] = '"' . $path . '" ' . t('directory is not readable');
}
}
// Check cache and data directories are readable and writable
- foreach (array(
- $conf->get('resource.thumbnails_cache'),
- $conf->get('resource.data_dir'),
- $conf->get('resource.page_cache'),
- $conf->get('resource.raintpl_tmp'),
- ) as $path) {
+ if ($minimalMode) {
+ $folders = [
+ $conf->get('resource.raintpl_tmp'),
+ ];
+ } else {
+ $folders = [
+ $conf->get('resource.thumbnails_cache'),
+ $conf->get('resource.data_dir'),
+ $conf->get('resource.page_cache'),
+ $conf->get('resource.raintpl_tmp'),
+ ];
+ }
+
+ foreach ($folders as $path) {
if (!is_readable(realpath($path))) {
$errors[] = '"' . $path . '" ' . t('directory is not readable');
}
}
}
+ if ($minimalMode) {
+ return $errors;
+ }
+
// Check configuration files are readable and writable
foreach (array(
$conf->getConfigFileExt(),
{
return hash_hmac('sha256', $currentVersion, $salt);
}
+
+ /**
+ * Get a list of PHP extensions used by Shaarli.
+ *
+ * @return array[] List of extension with following keys:
+ * - name: extension name
+ * - required: whether the extension is required to use Shaarli
+ * - desc: short description of extension usage in Shaarli
+ * - loaded: whether the extension is properly loaded or not
+ */
+ public static function getPhpExtensionsRequirement(): array
+ {
+ $extensions = [
+ ['name' => 'json', 'required' => true, 'desc' => t('Configuration parsing')],
+ ['name' => 'simplexml', 'required' => true, 'desc' => t('Slim Framework (routing, etc.)')],
+ ['name' => 'mbstring', 'required' => true, 'desc' => t('Multibyte (Unicode) string support')],
+ ['name' => 'gd', 'required' => false, 'desc' => t('Required to use thumbnails')],
+ ['name' => 'intl', 'required' => false, 'desc' => t('Localized text sorting (e.g. e->è->f)')],
+ ['name' => 'curl', 'required' => false, 'desc' => t('Better retrieval of bookmark metadata and thumbnail')],
+ ['name' => 'gettext', 'required' => false, 'desc' => t('Use the translation system in gettext mode')],
+ ['name' => 'ldap', 'required' => false, 'desc' => t('Login using LDAP server')],
+ ];
+
+ foreach ($extensions as &$extension) {
+ $extension['loaded'] = extension_loaded($extension['name']);
+ }
+
+ return $extensions;
+ }
+
+ /**
+ * Return the EOL date of given PHP version. If the version is unknown,
+ * we return today + 2 years.
+ *
+ * @param string $fullVersion PHP version, e.g. 7.4.7
+ *
+ * @return string Date format: YYYY-MM-DD
+ */
+ public static function getPhpEol(string $fullVersion): string
+ {
+ preg_match('/(\d+\.\d+)\.\d+/', $fullVersion, $matches);
+
+ return [
+ '7.1' => '2019-12-01',
+ '7.2' => '2020-11-30',
+ '7.3' => '2021-12-06',
+ '7.4' => '2022-11-28',
+ '8.0' => '2023-12-01',
+ ][$matches[1]] ?? (new \DateTime('+2 year'))->format('Y-m-d');
+ }
}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Helper;
+
+use Shaarli\Bookmark\Bookmark;
+use Slim\Http\Request;
+
+class DailyPageHelper
+{
+ public const MONTH = 'month';
+ public const WEEK = 'week';
+ public const DAY = 'day';
+
+ /**
+ * Extracts the type of the daily to display from the HTTP request parameters
+ *
+ * @param Request $request HTTP request
+ *
+ * @return string month/week/day
+ */
+ public static function extractRequestedType(Request $request): string
+ {
+ if ($request->getQueryParam(static::MONTH) !== null) {
+ return static::MONTH;
+ } elseif ($request->getQueryParam(static::WEEK) !== null) {
+ return static::WEEK;
+ }
+
+ return static::DAY;
+ }
+
+ /**
+ * Extracts a DateTimeImmutable from provided HTTP request.
+ * If no parameter is provided, we rely on the creation date of the latest provided created bookmark.
+ * If the datastore is empty or no bookmark is provided, we use the current date.
+ *
+ * @param string $type month/week/day
+ * @param string|null $requestedDate Input string extracted from the request
+ * @param Bookmark|null $latestBookmark Latest bookmark found in the datastore (by date)
+ *
+ * @return \DateTimeImmutable from input or latest bookmark.
+ *
+ * @throws \Exception Type not supported.
+ */
+ public static function extractRequestedDateTime(
+ string $type,
+ ?string $requestedDate,
+ Bookmark $latestBookmark = null
+ ): \DateTimeImmutable {
+ $format = static::getFormatByType($type);
+ if (empty($requestedDate)) {
+ return $latestBookmark instanceof Bookmark
+ ? new \DateTimeImmutable($latestBookmark->getCreated()->format(\DateTime::ATOM))
+ : new \DateTimeImmutable()
+ ;
+ }
+
+ // W is not supported by createFromFormat...
+ if ($type === static::WEEK) {
+ return (new \DateTimeImmutable())
+ ->setISODate((int) substr($requestedDate, 0, 4), (int) substr($requestedDate, 4, 2))
+ ;
+ }
+
+ return \DateTimeImmutable::createFromFormat($format, $requestedDate);
+ }
+
+ /**
+ * Get the DateTime format used by provided type
+ * Examples:
+ * - day: 20201016 (<year><month><day>)
+ * - week: 202041 (<year><week number>)
+ * - month: 202010 (<year><month>)
+ *
+ * @param string $type month/week/day
+ *
+ * @return string DateTime compatible format
+ *
+ * @see https://www.php.net/manual/en/datetime.format.php
+ *
+ * @throws \Exception Type not supported.
+ */
+ public static function getFormatByType(string $type): string
+ {
+ switch ($type) {
+ case static::MONTH:
+ return 'Ym';
+ case static::WEEK:
+ return 'YW';
+ case static::DAY:
+ return 'Ymd';
+ default:
+ throw new \Exception('Unsupported daily format type');
+ }
+ }
+
+ /**
+ * Get the first DateTime of the time period depending on given datetime and type.
+ * Note: DateTimeImmutable is required because we rely heavily on DateTime->modify() syntax
+ * and we don't want to alter original datetime.
+ *
+ * @param string $type month/week/day
+ * @param \DateTimeImmutable $requested DateTime extracted from request input
+ * (should come from extractRequestedDateTime)
+ *
+ * @return \DateTimeInterface First DateTime of the time period
+ *
+ * @throws \Exception Type not supported.
+ */
+ public static function getStartDateTimeByType(string $type, \DateTimeImmutable $requested): \DateTimeInterface
+ {
+ switch ($type) {
+ case static::MONTH:
+ return $requested->modify('first day of this month midnight');
+ case static::WEEK:
+ return $requested->modify('Monday this week midnight');
+ case static::DAY:
+ return $requested->modify('Today midnight');
+ default:
+ throw new \Exception('Unsupported daily format type');
+ }
+ }
+
+ /**
+ * Get the last DateTime of the time period depending on given datetime and type.
+ * Note: DateTimeImmutable is required because we rely heavily on DateTime->modify() syntax
+ * and we don't want to alter original datetime.
+ *
+ * @param string $type month/week/day
+ * @param \DateTimeImmutable $requested DateTime extracted from request input
+ * (should come from extractRequestedDateTime)
+ *
+ * @return \DateTimeInterface Last DateTime of the time period
+ *
+ * @throws \Exception Type not supported.
+ */
+ public static function getEndDateTimeByType(string $type, \DateTimeImmutable $requested): \DateTimeInterface
+ {
+ switch ($type) {
+ case static::MONTH:
+ return $requested->modify('last day of this month 23:59:59');
+ case static::WEEK:
+ return $requested->modify('Sunday this week 23:59:59');
+ case static::DAY:
+ return $requested->modify('Today 23:59:59');
+ default:
+ throw new \Exception('Unsupported daily format type');
+ }
+ }
+
+ /**
+ * Get localized description of the time period depending on given datetime and type.
+ * Example: for a month period, it returns `October, 2020`.
+ *
+ * @param string $type month/week/day
+ * @param \DateTimeImmutable $requested DateTime extracted from request input
+ * (should come from extractRequestedDateTime)
+ *
+ * @return string Localized time period description
+ *
+ * @throws \Exception Type not supported.
+ */
+ public static function getDescriptionByType(string $type, \DateTimeImmutable $requested): string
+ {
+ switch ($type) {
+ case static::MONTH:
+ return $requested->format('F') . ', ' . $requested->format('Y');
+ case static::WEEK:
+ $requested = $requested->modify('Monday this week');
+ return t('Week') . ' ' . $requested->format('W') . ' (' . format_date($requested, false) . ')';
+ case static::DAY:
+ $out = '';
+ if ($requested->format('Ymd') === date('Ymd')) {
+ $out = t('Today') . ' - ';
+ } elseif ($requested->format('Ymd') === date('Ymd', strtotime('-1 days'))) {
+ $out = t('Yesterday') . ' - ';
+ }
+ return $out . format_date($requested, false);
+ default:
+ throw new \Exception('Unsupported daily format type');
+ }
+ }
+
+ /**
+ * Get the number of items to display in the RSS feed depending on the given type.
+ *
+ * @param string $type month/week/day
+ *
+ * @return int number of elements
+ *
+ * @throws \Exception Type not supported.
+ */
+ public static function getRssLengthByType(string $type): int
+ {
+ switch ($type) {
+ case static::MONTH:
+ return 12; // 1 year
+ case static::WEEK:
+ return 26; // ~6 months
+ case static::DAY:
+ return 30; // ~1 month
+ default:
+ throw new \Exception('Unsupported daily format type');
+ }
+ }
+}
<?php
-namespace Shaarli;
+namespace Shaarli\Helper;
use Shaarli\Exceptions\IOException;
)
);
}
+
+ /**
+ * Recursively deletes a folder content, and deletes itself optionally.
+ * If an excluded file is found, folders won't be deleted.
+ *
+ * Additional security: raise an exception if it tries to delete a folder outside of Shaarli directory.
+ *
+ * @param string $path
+ * @param bool $selfDelete Delete the provided folder if true, only its content if false.
+ * @param array $exclude
+ */
+ public static function clearFolder(string $path, bool $selfDelete, array $exclude = []): bool
+ {
+ $skipped = false;
+
+ if (!is_dir($path)) {
+ throw new IOException(t('Provided path is not a directory.'));
+ }
+
+ if (!static::isPathInShaarliFolder($path)) {
+ throw new IOException(t('Trying to delete a folder outside of Shaarli path.'));
+ }
+
+ foreach (new \DirectoryIterator($path) as $file) {
+ if($file->isDot()) {
+ continue;
+ }
+
+ if (in_array($file->getBasename(), $exclude, true)) {
+ $skipped = true;
+ continue;
+ }
+
+ if ($file->isFile()) {
+ unlink($file->getPathname());
+ } elseif($file->isDir()) {
+ $skipped = static::clearFolder($file->getRealPath(), true, $exclude) || $skipped;
+ }
+ }
+
+ if ($selfDelete && !$skipped) {
+ rmdir($path);
+ }
+
+ return $skipped;
+ }
+
+ /**
+ * Checks that the given path is inside Shaarli directory.
+ */
+ public static function isPathInShaarliFolder(string $path): bool
+ {
+ $rootDirectory = dirname(dirname(dirname(__FILE__)));
+
+ return strpos(realpath($path), $rootDirectory) !== false;
+ }
}
use Iterator;
use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
use Shaarli\Exceptions\IOException;
-use Shaarli\FileUtils;
+use Shaarli\Helper\FileUtils;
use Shaarli\Render\PageCacheManager;
/**
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;
-use Shaarli\ApplicationUtils;
use Shaarli\Bookmark\Bookmark;
use Shaarli\Bookmark\BookmarkArray;
use Shaarli\Bookmark\BookmarkFilter;
use Shaarli\Config\ConfigManager;
use Shaarli\Config\ConfigPhp;
use Shaarli\Exceptions\IOException;
+use Shaarli\Helper\ApplicationUtils;
use Shaarli\Thumbnailer;
use Shaarli\Updater\Exception\UpdaterException;
use Exception;
use Psr\Log\LoggerInterface;
use RainTPL;
-use Shaarli\ApplicationUtils;
use Shaarli\Bookmark\BookmarkServiceInterface;
use Shaarli\Config\ConfigManager;
+use Shaarli\Helper\ApplicationUtils;
use Shaarli\Security\SessionManager;
use Shaarli\Thumbnailer;
$this->tpl->assign('formatter', $this->conf->get('formatter', 'default'));
- $this->tpl->assign('links_per_page', $this->session['LINKS_PER_PAGE']);
+ $this->tpl->assign('links_per_page', $this->session['LINKS_PER_PAGE'] ?? 20);
// To be removed with a proper theme configuration.
$this->tpl->assign('conf', $this->conf);
public const DAILY = 'daily';
public const DAILY_RSS = 'dailyrss';
public const EDIT_LINK = 'editlink';
+ public const EDIT_LINK_BATCH = 'editlink.batch';
public const ERROR = 'error';
public const EXPORT = 'export';
public const NETSCAPE_EXPORT_BOOKMARKS = 'export.bookmarks';
namespace Shaarli\Security;
use Psr\Log\LoggerInterface;
-use Shaarli\FileUtils;
+use Shaarli\Helper\FileUtils;
/**
* Class BanManager
return session_start();
}
- public function cookieParameters(int $lifeTime, string $path, string $domain): bool
+ /**
+ * Be careful, return type of session_set_cookie_params() changed between PHP 7.1 and 7.2.
+ */
+ public function cookieParameters(int $lifeTime, string $path, string $domain): void
{
- return session_set_cookie_params($lifeTime, $path, $domain);
+ session_set_cookie_params($lifeTime, $path, $domain);
}
public function regenerateId(bool $deleteOldSession = false): bool
(() => {
const basePath = document.querySelector('input[name="js_base_path"]').value;
- const loaders = document.querySelectorAll('.loading-input');
/*
* METADATA FOR EDIT BOOKMARK PAGE
*/
- const inputTitle = document.querySelector('input[name="lf_title"]');
- if (inputTitle != null) {
- if (inputTitle.value.length > 0) {
- clearLoaders(loaders);
- return;
- }
+ const inputTitles = document.querySelectorAll('input[name="lf_title"]');
+ if (inputTitles != null) {
+ [...inputTitles].forEach((inputTitle) => {
+ const form = inputTitle.closest('form[name="linkform"]');
+ const loaders = form.querySelectorAll('.loading-input');
+
+ if (inputTitle.value.length > 0) {
+ clearLoaders(loaders);
+ return;
+ }
- const url = document.querySelector('input[name="lf_url"]').value;
+ const url = form.querySelector('input[name="lf_url"]').value;
- const xhr = new XMLHttpRequest();
- xhr.open('GET', `${basePath}/admin/metadata?url=${encodeURI(url)}`, true);
- xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
- xhr.onload = () => {
- const result = JSON.parse(xhr.response);
- Object.keys(result).forEach((key) => {
- if (result[key] !== null && result[key].length) {
- const element = document.querySelector(`input[name="lf_${key}"], textarea[name="lf_${key}"]`);
- if (element != null && element.value.length === 0) {
- element.value = he.decode(result[key]);
+ const xhr = new XMLHttpRequest();
+ xhr.open('GET', `${basePath}/admin/metadata?url=${encodeURI(url)}`, true);
+ xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
+ xhr.onload = () => {
+ const result = JSON.parse(xhr.response);
+ Object.keys(result).forEach((key) => {
+ if (result[key] !== null && result[key].length) {
+ const element = form.querySelector(`input[name="lf_${key}"], textarea[name="lf_${key}"]`);
+ if (element != null && element.value.length === 0) {
+ element.value = he.decode(result[key]);
+ }
}
- }
- });
- clearLoaders(loaders);
- };
+ });
+ clearLoaders(loaders);
+ };
- xhr.send();
+ xhr.send();
+ });
}
/*
--- /dev/null
+const sendBookmarkForm = (basePath, formElement) => {
+ const inputs = formElement
+ .querySelectorAll('input[type="text"], textarea, input[type="checkbox"], input[type="hidden"]');
+
+ const formData = new FormData();
+ [...inputs].forEach((input) => {
+ formData.append(input.getAttribute('name'), input.value);
+ });
+
+ return new Promise((resolve, reject) => {
+ const xhr = new XMLHttpRequest();
+ xhr.open('POST', `${basePath}/admin/shaare`);
+ xhr.onload = () => {
+ if (xhr.status !== 200) {
+ alert(`An error occurred. Return code: ${xhr.status}`);
+ reject();
+ } else {
+ formElement.closest('.edit-link-container').remove();
+ resolve();
+ }
+ };
+ xhr.send(formData);
+ });
+};
+
+const sendBookmarkDelete = (buttonElement, formElement) => (
+ new Promise((resolve, reject) => {
+ const xhr = new XMLHttpRequest();
+ xhr.open('GET', buttonElement.href);
+ xhr.onload = () => {
+ if (xhr.status !== 200) {
+ alert(`An error occurred. Return code: ${xhr.status}`);
+ reject();
+ } else {
+ formElement.closest('.edit-link-container').remove();
+ resolve();
+ }
+ };
+ xhr.send();
+ })
+);
+
+const redirectIfEmptyBatch = (basePath, formElements, path) => {
+ if (formElements == null || formElements.length === 0) {
+ window.location.href = `${basePath}${path}`;
+ }
+};
+
+(() => {
+ const basePath = document.querySelector('input[name="js_base_path"]').value;
+ const getForms = () => document.querySelectorAll('form[name="linkform"]');
+
+ const cancelButtons = document.querySelectorAll('[name="cancel-batch-link"]');
+ if (cancelButtons != null) {
+ [...cancelButtons].forEach((cancelButton) => {
+ cancelButton.addEventListener('click', (e) => {
+ e.preventDefault();
+ e.target.closest('form[name="linkform"]').remove();
+ redirectIfEmptyBatch(basePath, getForms(), '/admin/add-shaare');
+ });
+ });
+ }
+
+ const saveButtons = document.querySelectorAll('[name="save_edit"]');
+ if (saveButtons != null) {
+ [...saveButtons].forEach((saveButton) => {
+ saveButton.addEventListener('click', (e) => {
+ e.preventDefault();
+
+ const formElement = e.target.closest('form[name="linkform"]');
+ sendBookmarkForm(basePath, formElement)
+ .then(() => redirectIfEmptyBatch(basePath, getForms(), '/'));
+ });
+ });
+ }
+
+ const saveAllButtons = document.querySelectorAll('[name="save_edit_batch"]');
+ if (saveAllButtons != null) {
+ [...saveAllButtons].forEach((saveAllButton) => {
+ saveAllButton.addEventListener('click', (e) => {
+ e.preventDefault();
+
+ const forms = [...getForms()];
+ const nbForm = forms.length;
+ let current = 0;
+ const progressBar = document.querySelector('.progressbar > div');
+ const progressBarCurrent = document.querySelector('.progressbar-current');
+
+ document.querySelector('.dark-layer').style.display = 'block';
+ document.querySelector('.progressbar-max').innerHTML = nbForm;
+ progressBarCurrent.innerHTML = current;
+
+ const promises = [];
+ forms.forEach((formElement) => {
+ promises.push(sendBookmarkForm(basePath, formElement).then(() => {
+ current += 1;
+ progressBar.style.width = `${(current * 100) / nbForm}%`;
+ progressBarCurrent.innerHTML = current;
+ }));
+ });
+
+ Promise.all(promises).then(() => {
+ window.location.href = basePath || '/';
+ });
+ });
+ });
+ }
+
+ const deleteButtons = document.querySelectorAll('[name="delete_link"]');
+ if (deleteButtons != null) {
+ [...deleteButtons].forEach((deleteButton) => {
+ deleteButton.addEventListener('click', (e) => {
+ e.preventDefault();
+
+ const formElement = e.target.closest('form[name="linkform"]');
+ sendBookmarkDelete(e.target, formElement)
+ .then(() => redirectIfEmptyBatch(basePath, getForms(), '/'));
+ });
+ });
+ }
+})();
});
});
}
+
+ const bulkCreationButton = document.querySelector('.addlink-batch-show-more-block');
+ if (bulkCreationButton != null) {
+ const toggleBulkCreationVisibility = (showMoreBlockElement, formElement) => {
+ if (bulkCreationButton.classList.contains('pure-u-0')) {
+ showMoreBlockElement.classList.remove('pure-u-0');
+ formElement.classList.add('pure-u-0');
+ } else {
+ showMoreBlockElement.classList.add('pure-u-0');
+ formElement.classList.remove('pure-u-0');
+ }
+ };
+
+ const bulkCreationForm = document.querySelector('.addlink-batch-form-block');
+
+ toggleBulkCreationVisibility(bulkCreationButton, bulkCreationForm);
+ bulkCreationButton.querySelector('a').addEventListener('click', (e) => {
+ e.preventDefault();
+ toggleBulkCreationVisibility(bulkCreationButton, bulkCreationForm);
+ });
+
+ // Force to send falsy value if the checkbox is not checked.
+ const privateButton = bulkCreationForm.querySelector('input[type="checkbox"][name="private"]');
+ const privateHiddenButton = bulkCreationForm.querySelector('input[type="hidden"][name="private"]');
+ privateButton.addEventListener('click', () => {
+ privateHiddenButton.disabled = !privateHiddenButton.disabled;
+ });
+ privateHiddenButton.disabled = privateButton.checked;
+ }
})();
&.button-red {
background: $red;
}
+
+ &.button-grey {
+ background: $light-grey;
+ }
}
.submit-buttons {
}
table {
- margin: auto;
+ margin: 10px auto 25px auto;
width: 90%;
.order {
position: absolute;
right: 5%;
}
+
+ &.button-grey {
+ position: absolute;
+ left: 5%;
+ }
}
}
}
}
}
+// SERVER PAGE
+
+.server-tables-page,
+.server-tables {
+ .window-subtitle {
+ &::before {
+ display: block;
+ margin: 8px auto;
+ background: linear-gradient(to right, var(--background-color), $dark-grey, var(--background-color));
+ width: 50%;
+ height: 1px;
+ content: '';
+ }
+ }
+
+ .server-row {
+ p {
+ height: 25px;
+ padding: 0 10px;
+ }
+ }
+
+ .server-label {
+ text-align: right;
+ font-weight: bold;
+ }
+
+ i {
+ &.fa-color-green {
+ color: $main-green;
+ }
+
+ &.fa-color-orange {
+ color: $orange;
+ }
+
+ &.fa-color-red {
+ color: $red;
+ }
+ }
+
+ @media screen and (max-width: 64em) {
+ .server-label {
+ text-align: center;
+ }
+
+ .server-row {
+ p {
+ text-align: center;
+ }
+ }
+ }
+}
+
+// Batch creation
+input[name='save_edit_batch'] {
+ @extend %page-form-button;
+}
+
+.addlink-batch-show-more {
+ display: flex;
+ align-items: center;
+ margin: 20px 0 8px;
+
+ a {
+ color: var(--main-color);
+ text-decoration: none;
+ }
+
+ &::before,
+ &::after {
+ content: "";
+ flex-grow: 1;
+ background: rgba(0, 0, 0, 0.35);
+ height: 1px;
+ font-size: 0;
+ line-height: 0;
+ }
+
+ &::before {
+ margin: 0 16px 0 0;
+ }
+
+ &::after {
+ margin: 0 0 0 16px;
+ }
+}
+
+.dark-layer {
+ display: none;
+ position: fixed;
+ height: 100%;
+ width: 100%;
+ z-index: 998;
+ background-color: rgba(0, 0, 0, .75);
+ color: #fff;
+
+ .screen-center {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ text-align: center;
+ min-height: 100vh;
+ }
+
+ .progressbar {
+ width: 33%;
+ }
+}
+
+.addlink-batch-form-block {
+ .pure-alert {
+ margin: 25px 0 0 0;
+ }
+}
+
// Print rules
@media print {
.shaarli-menu {
"Shaarli\\Front\\Controller\\Admin\\": "application/front/controller/admin",
"Shaarli\\Front\\Controller\\Visitor\\": "application/front/controller/visitor",
"Shaarli\\Front\\Exception\\": "application/front/exceptions",
+ "Shaarli\\Helper\\": "application/helper",
"Shaarli\\Http\\": "application/http",
"Shaarli\\Legacy\\": "application/legacy",
"Shaarli\\Netscape\\": "application/netscape",
msgid ""
msgstr ""
"Project-Id-Version: Shaarli\n"
-"POT-Creation-Date: 2020-10-16 20:01+0200\n"
-"PO-Revision-Date: 2020-10-16 20:02+0200\n"
+"POT-Creation-Date: 2020-10-27 19:44+0100\n"
+"PO-Revision-Date: 2020-10-27 19:44+0100\n"
"Last-Translator: \n"
"Language-Team: Shaarli\n"
"Language: fr_FR\n"
"X-Poedit-SearchPath-3: init.php\n"
"X-Poedit-SearchPath-4: plugins\n"
-#: application/ApplicationUtils.php:161
-#, php-format
-msgid ""
-"Your PHP version is obsolete! Shaarli requires at least PHP %s, and thus "
-"cannot run. Your PHP version has known security vulnerabilities and should "
-"be updated as soon as possible."
-msgstr ""
-"Votre version de PHP est obsolète ! Shaarli nécessite au moins PHP %s, et ne "
-"peut donc pas fonctionner. Votre version de PHP a des failles de sécurités "
-"connues et devrait être mise à jour au plus tôt."
-
-#: application/ApplicationUtils.php:192 application/ApplicationUtils.php:204
-msgid "directory is not readable"
-msgstr "le répertoire n'est pas accessible en lecture"
-
-#: application/ApplicationUtils.php:207
-msgid "directory is not writable"
-msgstr "le répertoire n'est pas accessible en écriture"
-
-#: application/ApplicationUtils.php:225
-msgid "file is not readable"
-msgstr "le fichier n'est pas accessible en lecture"
-
-#: application/ApplicationUtils.php:228
-msgid "file is not writable"
-msgstr "le fichier n'est pas accessible en écriture"
-
-#: application/History.php:179
+#: application/History.php:180
msgid "History file isn't readable or writable"
msgstr "Le fichier d'historique n'est pas accessible en lecture ou en écriture"
-#: application/History.php:190
+#: application/History.php:191
msgid "Could not parse history file"
msgstr "Format incorrect pour le fichier d'historique"
"l'extension php-gd doit être chargée pour utiliser les miniatures. Les "
"miniatures sont désormais désactivées. Rechargez la page."
-#: application/Utils.php:383
+#: application/Utils.php:402
msgid "Setting not set"
msgstr "Paramètre non défini"
-#: application/Utils.php:390
+#: application/Utils.php:409
msgid "Unlimited"
msgstr "Illimité"
-#: application/Utils.php:393
+#: application/Utils.php:412
msgid "B"
msgstr "o"
-#: application/Utils.php:393
+#: application/Utils.php:412
msgid "kiB"
msgstr "ko"
-#: application/Utils.php:393
+#: application/Utils.php:412
msgid "MiB"
msgstr "Mo"
-#: application/Utils.php:393
+#: application/Utils.php:412
msgid "GiB"
msgstr "Go"
-#: application/bookmark/BookmarkFileService.php:180
-#: application/bookmark/BookmarkFileService.php:202
-#: application/bookmark/BookmarkFileService.php:224
-#: application/bookmark/BookmarkFileService.php:238
+#: application/bookmark/BookmarkFileService.php:183
+#: application/bookmark/BookmarkFileService.php:205
+#: application/bookmark/BookmarkFileService.php:227
+#: application/bookmark/BookmarkFileService.php:241
msgid "You're not authorized to alter the datastore"
msgstr "Vous n'êtes pas autorisé à modifier les données"
-#: application/bookmark/BookmarkFileService.php:205
+#: application/bookmark/BookmarkFileService.php:208
msgid "This bookmarks already exists"
-msgstr "Ce marque-page existe déjà ."
+msgstr "Ce marque-page existe déjà "
#: application/bookmark/BookmarkInitializer.php:39
msgid "(private bookmark with thumbnail demo)"
msgstr "Liens directs"
#: application/feed/FeedBuilder.php:181
-#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:96
+#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103
+#: tmp/dailyrss.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:179
msgid "Permalink"
msgstr "Permalien"
msgstr "Vous avez activé ou changé le mode de miniatures."
#: application/front/controller/admin/ConfigureController.php:103
+#: application/front/controller/admin/ServerController.php:68
#: application/legacy/LegacyUpdater.php:538
msgid "Please synchronize them."
msgstr "Merci de les synchroniser."
#: application/front/controller/admin/ConfigureController.php:113
-#: application/front/controller/visitor/InstallController.php:136
+#: application/front/controller/visitor/InstallController.php:146
msgid "Error while writing config file after configuration update."
msgstr ""
"Une erreur s'est produite lors de la sauvegarde du fichier de configuration."
"le serveur web peut accepter (%s). Merci de l'envoyer en parties plus "
"légères."
-#: application/front/controller/admin/ManageShaareController.php:29
-#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
-msgid "Shaare a new link"
-msgstr "Partager un nouveau lien"
-
-#: application/front/controller/admin/ManageShaareController.php:78
-msgid "Note: "
-msgstr "Note : "
-
-#: application/front/controller/admin/ManageShaareController.php:109
-#: application/front/controller/admin/ManageShaareController.php:206
-#: application/front/controller/admin/ManageShaareController.php:275
-#: application/front/controller/admin/ManageShaareController.php:315
-#, php-format
-msgid "Bookmark with identifier %s could not be found."
-msgstr "Le lien avec l'identifiant %s n'a pas pu être trouvé."
-
-#: application/front/controller/admin/ManageShaareController.php:194
-#: application/front/controller/admin/ManageShaareController.php:252
-msgid "Invalid bookmark ID provided."
-msgstr "ID du lien non valide."
-
-#: application/front/controller/admin/ManageShaareController.php:260
-msgid "Invalid visibility provided."
-msgstr "Visibilité du lien non valide."
-
-#: application/front/controller/admin/ManageShaareController.php:363
-#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171
-msgid "Edit"
-msgstr "Modifier"
-
-#: application/front/controller/admin/ManageShaareController.php:366
-#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
-#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:28
-msgid "Shaare"
-msgstr "Shaare"
-
+#: application/front/controller/admin/ManageShaareController.php:64
+#: application/front/controller/admin/ManageShaareController.php:95
+#: application/front/controller/admin/ManageShaareController.php:193
+#: application/front/controller/admin/ManageShaareController.php:262
+#: application/front/controller/admin/ManageShaareController.php:302
+#: application/front/controller/admin/ManageShaareController.php:181
+#: application/front/controller/admin/ManageShaareController.php:239
+#: application/front/controller/admin/ManageShaareController.php:247
+#: application/front/controller/admin/ManageShaareController.php:378
+#: application/front/controller/admin/ManageShaareController.php:381
#: application/front/controller/admin/ManageTagController.php:29
#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
msgid "Manage tags"
msgstr "Gérer les tags"
#: application/front/controller/admin/PasswordController.php:28
#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
msgid "Change password"
msgstr "Modifier le mot de passe"
"Une erreur s'est produite lors de la sauvegarde de la configuration des "
"plugins : "
+#: application/front/controller/admin/ServerController.php:50
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
+msgid "Server administration"
+msgstr "Administration serveur"
+
+#: application/front/controller/admin/ServerController.php:67
+msgid "Thumbnails cache has been cleared."
+msgstr "Le cache des miniatures a été vidé."
+
+#: application/front/controller/admin/ServerController.php:76
+msgid "Shaarli's cache folder has been cleared!"
+msgstr "Le dossier de cache de Shaarli a été vidé !"
+
+#, php-format
+msgid "Bookmark with identifier %s could not be found."
+msgstr "Le lien avec l'identifiant %s n'a pas pu être trouvé."
+
+#: application/front/controller/admin/ShaareManageController.php:101
+msgid "Invalid visibility provided."
+msgstr "Visibilité du lien non valide."
+
+#: application/front/controller/admin/ShaarePublishController.php:154
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171
+msgid "Edit"
+msgstr "Modifier"
+
+#: application/front/controller/admin/ShaarePublishController.php:157
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:28
+msgid "Shaare"
+msgstr "Shaare"
+
+#: application/front/controller/admin/ShaarePublishController.php:184
+msgid "Note: "
+msgstr "Note : "
+
#: application/front/controller/admin/ThumbnailsController.php:37
#: tmp/thumbnails.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
msgid "Thumbnails update"
msgid "Search: "
msgstr "Recherche : "
-#: application/front/controller/visitor/DailyController.php:45
-msgid "Today"
-msgstr "Aujourd'hui"
-
-#: application/front/controller/visitor/DailyController.php:47
-msgid "Yesterday"
-msgstr "Hier"
+#: application/front/controller/visitor/DailyController.php:200
+msgid "day"
+msgstr "jour"
-#: application/front/controller/visitor/DailyController.php:85
+#: application/front/controller/visitor/DailyController.php:200
+#: application/front/controller/visitor/DailyController.php:203
+#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:48
msgid "Daily"
msgstr "Quotidien"
-#: application/front/controller/visitor/ErrorController.php:36
+#: application/front/controller/visitor/DailyController.php:201
+msgid "week"
+msgstr "semaine"
+
+#: application/front/controller/visitor/DailyController.php:201
+#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
+msgid "Weekly"
+msgstr "Hebdomadaire"
+
+#: application/front/controller/visitor/DailyController.php:202
+msgid "month"
+msgstr "mois"
+
+#: application/front/controller/visitor/DailyController.php:202
+#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
+msgid "Monthly"
+msgstr "Mensuel"
+
+#: application/front/controller/visitor/ErrorController.php:33
msgid "An unexpected error occurred."
msgstr "Une erreur inattendue s'est produite."
#: application/front/controller/visitor/ErrorNotFoundController.php:25
msgid "Requested page could not be found."
-msgstr ""
+msgstr "La page demandée n'a pas pu être trouvée."
+
+#: application/front/controller/visitor/InstallController.php:64
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22
+msgid "Install Shaarli"
+msgstr "Installation de Shaarli"
-#: application/front/controller/visitor/InstallController.php:73
+#: application/front/controller/visitor/InstallController.php:83
#, php-format
msgid ""
"<pre>Sessions do not seem to work correctly on your server.<br>Make sure the "
"des cookies. Nous vous recommandons d'accéder à votre serveur depuis son "
"adresse IP ou un <em>Fully Qualified Domain Name</em>.<br>"
-#: application/front/controller/visitor/InstallController.php:144
+#: application/front/controller/visitor/InstallController.php:154
msgid ""
"Shaarli is now configured. Please login and start shaaring your bookmarks!"
msgstr ""
"Shaarli est maintenant configuré. Vous pouvez vous connecter et commencez à "
"shaare vos liens !"
-#: application/front/controller/visitor/InstallController.php:158
+#: application/front/controller/visitor/InstallController.php:168
msgid "Insufficient permissions:"
msgstr "Permissions insuffisantes :"
msgid "Login"
msgstr "Connexion"
-#: application/front/controller/visitor/LoginController.php:78
+#: application/front/controller/visitor/LoginController.php:77
msgid "Wrong login/password."
msgstr "Nom d'utilisateur ou mot de passe incorrect(s)."
#: application/front/controller/visitor/TagCloudController.php:88
msgid "Tag "
-msgstr "Tag"
+msgstr "Tag "
#: application/front/exceptions/AlreadyInstalledException.php:11
msgid "Shaarli has already been installed. Login to edit the configuration."
msgid "Wrong token."
msgstr "Jeton invalide."
+#: application/helper/ApplicationUtils.php:162
+#, php-format
+msgid ""
+"Your PHP version is obsolete! Shaarli requires at least PHP %s, and thus "
+"cannot run. Your PHP version has known security vulnerabilities and should "
+"be updated as soon as possible."
+msgstr ""
+"Votre version de PHP est obsolète ! Shaarli nécessite au moins PHP %s, et ne "
+"peut donc pas fonctionner. Votre version de PHP a des failles de sécurités "
+"connues et devrait être mise à jour au plus tôt."
+
+#: application/helper/ApplicationUtils.php:195
+#: application/helper/ApplicationUtils.php:215
+msgid "directory is not readable"
+msgstr "le répertoire n'est pas accessible en lecture"
+
+#: application/helper/ApplicationUtils.php:218
+msgid "directory is not writable"
+msgstr "le répertoire n'est pas accessible en écriture"
+
+#: application/helper/ApplicationUtils.php:240
+msgid "file is not readable"
+msgstr "le fichier n'est pas accessible en lecture"
+
+#: application/helper/ApplicationUtils.php:243
+msgid "file is not writable"
+msgstr "le fichier n'est pas accessible en écriture"
+
+#: application/helper/ApplicationUtils.php:277
+msgid "Configuration parsing"
+msgstr "Chargement de la configuration"
+
+#: application/helper/ApplicationUtils.php:278
+msgid "Slim Framework (routing, etc.)"
+msgstr "Slim Framwork (routage, etc.)"
+
+#: application/helper/ApplicationUtils.php:279
+msgid "Multibyte (Unicode) string support"
+msgstr "Support des chaînes de caractère multibytes (Unicode)"
+
+#: application/helper/ApplicationUtils.php:280
+msgid "Required to use thumbnails"
+msgstr "Obligatoire pour utiliser les miniatures"
+
+#: application/helper/ApplicationUtils.php:281
+msgid "Localized text sorting (e.g. e->è->f)"
+msgstr "Tri des textes traduits (ex : e->è->f)"
+
+#: application/helper/ApplicationUtils.php:282
+msgid "Better retrieval of bookmark metadata and thumbnail"
+msgstr "Meilleure récupération des meta-données des marque-pages et minatures"
+
+#: application/helper/ApplicationUtils.php:283
+msgid "Use the translation system in gettext mode"
+msgstr "Utiliser le système de traduction en mode gettext"
+
+#: application/helper/ApplicationUtils.php:284
+msgid "Login using LDAP server"
+msgstr "Authentification via un serveur LDAP"
+
+#: application/helper/DailyPageHelper.php:172
+msgid "Week"
+msgstr "Semaine"
+
+#: application/helper/DailyPageHelper.php:176
+msgid "Today"
+msgstr "Aujourd'hui"
+
+#: application/helper/DailyPageHelper.php:178
+msgid "Yesterday"
+msgstr "Hier"
+
+#: application/helper/FileUtils.php:100
+msgid "Provided path is not a directory."
+msgstr "Le chemin fourni n'est pas un dossier."
+
+#: application/helper/FileUtils.php:104
+msgid "Trying to delete a folder outside of Shaarli path."
+msgstr "Tentative de supprimer un dossier en dehors du chemin de Shaarli."
+
#: application/legacy/LegacyLinkDB.php:131
msgid "You are not authorized to add a link."
msgstr "Vous n'êtes pas autorisé à ajouter un lien."
msgid "An error occurred while running the update "
msgstr "Une erreur s'est produite lors de l'exécution de la mise à jour "
-#: index.php:65
+#: index.php:80
msgid "Shared bookmarks on "
msgstr "Liens partagés sur "
msgid "URL or leave empty to post a note"
msgstr "URL ou laisser vide pour créer une note"
+#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
+msgid "BULK CREATION"
+msgstr "CRÉATION DE MASSE"
+
+#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40
+msgid "Metadata asynchronous retrieval is disabled."
+msgstr "La récupération asynchrone des meta-données est désactivée."
+
+#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
+msgid ""
+"We recommend that you enable the setting <em>general > "
+"enable_async_metadata</em> in your configuration file to use bulk link "
+"creation."
+msgstr ""
+"Nous recommandons d'activer le paramètre <em>general > "
+"enable_async_metadata</em> dans votre fichier de configuration pour utiliser "
+"la création de masse."
+
+#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:56
+msgid "Shaare multiple new links"
+msgstr "Partagez plusieurs nouveaux liens"
+
+#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:59
+msgid "Add one URL per line to create multiple bookmarks."
+msgstr "Ajouter une URL par ligne pour créer plusieurs marque-pages."
+
+#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:63
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:67
+msgid "Tags"
+msgstr "Tags"
+
+#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:73
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83
+#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169
+msgid "Private"
+msgstr "Privé"
+
+#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:78
+msgid "Add links"
+msgstr "Ajouter des liens"
+
#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
msgid "Current password"
msgstr "Mot de passe actuel"
"miniatures."
#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:328
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:56
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:122
msgid "Synchronize thumbnails"
msgstr "Synchroniser les miniatures"
#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:339
#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
msgid "All"
msgstr "Tous"
#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:343
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:106
msgid "Only common media hosts"
msgstr "Seulement les hébergeurs de média connus"
#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:347
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:110
msgid "None"
msgstr "Aucune"
#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:355
-#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:88
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139
#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199
msgid "Save"
msgstr "Enregistrer"
-#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
-msgid "The Daily Shaarli"
-msgstr "Le Quotidien Shaarli"
-
-#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17
-msgid "1 RSS entry per day"
-msgstr "1 entrée RSS par jour"
-
-#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:37
-msgid "Previous day"
-msgstr "Jour précédent"
-
-#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
-msgid "All links of one day in a single page."
-msgstr "Tous les liens d'un jour sur une page."
-
-#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:51
-msgid "Next day"
-msgstr "Jour suivant"
-
-#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18
+#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
+msgid "1 RSS entry per :type"
+msgid_plural ""
+msgstr[0] "1 entrée RSS par :type"
+msgstr[1] ""
+
+#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49
+msgid "Previous :type"
+msgid_plural ""
+msgstr[0] ":type précédent"
+msgstr[1] "Jour précédent"
+
+#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:56
+#: tmp/dailyrss.b91ef64efc3688266305ea9b42e5017e.rtpl.php:7
+msgid "All links of one :type in a single page."
+msgid_plural ""
+msgstr[0] "Tous les liens d'un :type sur une page."
+msgstr[1] "Tous les liens d'un jour sur une page."
+
+#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:63
+msgid "Next :type"
+msgid_plural ""
+msgstr[0] ":type suivant"
+msgstr[1] ""
+
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:21
msgid "Edit Shaare"
msgstr "Modifier le Shaare"
-#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:21
msgid "New Shaare"
msgstr "Nouveau Shaare"
-#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
msgid "Created:"
msgstr "Création :"
-#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:32
msgid "URL"
msgstr "URL"
-#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:38
msgid "Title"
msgstr "Titre"
-#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49
#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:75
#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:99
msgid "Description"
msgstr "Description"
-#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47
-msgid "Tags"
-msgstr "Tags"
-
-#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:60
-#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
-#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169
-msgid "Private"
-msgstr "Privé"
-
-#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:66
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:80
msgid "Description will be rendered with"
msgstr "La description sera générée avec"
-#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:68
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:82
msgid "Markdown syntax documentation"
msgstr "Documentation sur la syntaxe Markdown"
-#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:69
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83
msgid "Markdown syntax"
msgstr "la syntaxe Markdown"
-#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:88
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
+msgid "Cancel"
+msgstr "Annuler"
+
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:121
msgid "Apply Changes"
msgstr "Appliquer les changements"
-#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:93
+#: tmp/editlink.batch.b91ef64efc3688266305ea9b42e5017e.rtpl.php:12
+#: tmp/editlink.batch.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
+msgid "Save all"
+msgstr "Tout enregistrer"
+
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:107
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:173
#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147
#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:147
msgid "Add default tags"
msgstr "Ajouter des tags par défaut"
-#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22
-msgid "Install Shaarli"
-msgstr "Installation de Shaarli"
-
#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25
msgid "It looks like it's the first time you run Shaarli. Please configure it."
msgstr ""
msgid "Install"
msgstr "Installer"
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:190
+msgid "Server requirements"
+msgstr "Pré-requis serveur"
+
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:79
msgid "shaare"
msgid "Sticky"
msgstr "Épinglé"
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:189
+msgid "Share a private link"
+msgstr "Partager un lien privé"
+
#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:5
#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:5
msgid "Filters"
msgid "No parameter available."
msgstr "Aucun paramètre disponible."
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
+msgid "General"
+msgstr "Général"
+
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:20
+msgid "Index URL"
+msgstr "URL de l'index"
+
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
+msgid "Base path"
+msgstr "Chemin de base"
+
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
+msgid "Client IP"
+msgstr "IP du client"
+
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
+msgid "Trusted reverse proxies"
+msgstr "Reverse proxies de confiance"
+
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58
+msgid "N/A"
+msgstr "N/A"
+
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:84
+msgid "Visit releases page on Github"
+msgstr "Visiter la page des releases sur Github"
+
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:121
+msgid "Synchronize all link thumbnails"
+msgstr "Synchroniser toutes les miniatures"
+
+#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:2
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:2
+msgid "Permissions"
+msgstr "Permissions"
+
+#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:8
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:8
+msgid "There are permissions that need to be fixed."
+msgstr "Il y a des permissions qui doivent être corrigées."
+
+#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:23
+msgid "All read/write permissions are properly set."
+msgstr "Toutes les permissions de lecture/écriture sont définies correctement."
+
+#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:32
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:32
+msgid "Running PHP"
+msgstr "Fonctionnant avec PHP"
+
+#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:36
+msgid "End of life: "
+msgstr "Fin de vie : "
+
+#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:48
+msgid "Extension"
+msgstr "Extension"
+
+#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:49
+msgid "Usage"
+msgstr "Utilisation"
+
+#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:50
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:50
+msgid "Status"
+msgstr "Statut"
+
+#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:51
+#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:66
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:51
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:66
+msgid "Loaded"
+msgstr "Chargé"
+
+#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:60
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:60
+msgid "Required"
+msgstr "Obligatoire"
+
+#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:60
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:60
+msgid "Optional"
+msgstr "Optionnel"
+
+#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:70
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:70
+msgid "Not loaded"
+msgstr "Non chargé"
+
#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
msgid "tags"
msgid "Enable, disable and configure plugins"
msgstr "Activer, désactiver et configurer les extensions"
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:27
+msgid "Check instance's server configuration"
+msgstr "Vérifier la configuration serveur de l'instance"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34
msgid "Change your password"
msgstr "Modifier le mot de passe"
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
msgid "Rename or delete a tag in all links"
msgstr "Renommer ou supprimer un tag dans tous les liens"
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47
msgid ""
"Import Netscape HTML bookmarks (as exported from Firefox, Chrome, Opera, "
"delicious...)"
"Importer des marques pages au format Netscape HTML (comme exportés depuis "
"Firefox, Chrome, Opera, delicious...)"
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
msgid "Import links"
msgstr "Importer des liens"
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:53
msgid ""
"Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, "
"Opera, delicious...)"
"Exporter les marques pages au format Netscape HTML (comme exportés depuis "
"Firefox, Chrome, Opera, delicious...)"
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:54
msgid "Export database"
msgstr "Exporter les données"
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:55
-msgid "Synchronize all link thumbnails"
-msgstr "Synchroniser toutes les miniatures"
-
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:81
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77
msgid ""
"Drag one of these button to your bookmarks toolbar or right-click it and "
"\"Bookmark This Link\""
"Glisser un de ces boutons dans votre barre de favoris ou cliquer droit "
"dessus et « Ajouter aux favoris »"
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:82
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:78
msgid "then click on the bookmarklet in any page you want to share."
msgstr ""
"puis cliquer sur le marque-page depuis un site que vous souhaitez partager."
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:110
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:82
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:106
msgid ""
"Drag this link to your bookmarks toolbar or right-click it and Bookmark This "
"Link"
"Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « "
"Ajouter aux favoris »"
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:87
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83
msgid "then click ✚Shaare link button in any page you want to share"
msgstr "puis cliquer sur ✚Shaare depuis un site que vous souhaitez partager"
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:96
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:118
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:92
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:114
msgid "The selected text is too long, it will be truncated."
msgstr "Le texte sélectionné est trop long, il sera tronqué."
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:106
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
msgid "Shaare link"
msgstr "Shaare"
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:111
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:107
msgid ""
"Then click ✚Add Note button anytime to start composing a private Note (text "
"post) to your Shaarli"
msgstr ""
"Puis cliquer sur ✚Add Note pour commencer à rédiger une Note sur Shaarli"
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:127
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123
msgid "Add Note"
msgstr "Ajouter une Note"
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:136
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:132
msgid "3rd party"
msgstr "Applications tierces"
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:144
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:135
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:140
msgid "plugin"
msgstr "extension"
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:165
msgid ""
"Drag this link to your bookmarks toolbar, or right-click it and choose "
"Bookmark This Link"
"Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « "
"Ajouter aux favoris »"
-#~ msgid "Provided data is invalid"
-#~ msgstr "Les informations fournies ne sont pas valides"
+#~ msgid "Display:"
+#~ msgstr "Afficher :"
-#~ msgid "Rename"
-#~ msgstr "Renommer"
+#~ msgid "The Daily Shaarli"
+#~ msgstr "Le Quotidien Shaarli"
#, fuzzy
#~| msgid "Selection"
$this->post('/configure', '\Shaarli\Front\Controller\Admin\ConfigureController:save');
$this->get('/tags', '\Shaarli\Front\Controller\Admin\ManageTagController:index');
$this->post('/tags', '\Shaarli\Front\Controller\Admin\ManageTagController:save');
- $this->get('/add-shaare', '\Shaarli\Front\Controller\Admin\ManageShaareController:addShaare');
- $this->get('/shaare', '\Shaarli\Front\Controller\Admin\ManageShaareController:displayCreateForm');
- $this->get('/shaare/{id:[0-9]+}', '\Shaarli\Front\Controller\Admin\ManageShaareController:displayEditForm');
- $this->post('/shaare', '\Shaarli\Front\Controller\Admin\ManageShaareController:save');
- $this->get('/shaare/delete', '\Shaarli\Front\Controller\Admin\ManageShaareController:deleteBookmark');
- $this->get('/shaare/visibility', '\Shaarli\Front\Controller\Admin\ManageShaareController:changeVisibility');
- $this->get('/shaare/{id:[0-9]+}/pin', '\Shaarli\Front\Controller\Admin\ManageShaareController:pinBookmark');
+ $this->get('/add-shaare', '\Shaarli\Front\Controller\Admin\ShaareAddController:addShaare');
+ $this->get('/shaare', '\Shaarli\Front\Controller\Admin\ShaarePublishController:displayCreateForm');
+ $this->get('/shaare/{id:[0-9]+}', '\Shaarli\Front\Controller\Admin\ShaarePublishController:displayEditForm');
+ $this->get('/shaare/private/{hash}', '\Shaarli\Front\Controller\Admin\ShaareManageController:sharePrivate');
+ $this->post('/shaare-batch', '\Shaarli\Front\Controller\Admin\ShaarePublishController:displayCreateBatchForms');
+ $this->post('/shaare', '\Shaarli\Front\Controller\Admin\ShaarePublishController:save');
+ $this->get('/shaare/delete', '\Shaarli\Front\Controller\Admin\ShaareManageController:deleteBookmark');
+ $this->get('/shaare/visibility', '\Shaarli\Front\Controller\Admin\ShaareManageController:changeVisibility');
+ $this->get('/shaare/{id:[0-9]+}/pin', '\Shaarli\Front\Controller\Admin\ShaareManageController:pinBookmark');
$this->patch(
'/shaare/{id:[0-9]+}/update-thumbnail',
'\Shaarli\Front\Controller\Admin\ThumbnailsController:ajaxUpdate'
$this->get('/plugins', '\Shaarli\Front\Controller\Admin\PluginsController:index');
$this->post('/plugins', '\Shaarli\Front\Controller\Admin\PluginsController:save');
$this->get('/token', '\Shaarli\Front\Controller\Admin\TokenController:getToken');
+ $this->get('/server', '\Shaarli\Front\Controller\Admin\ServerController:index');
+ $this->get('/clear-cache', '\Shaarli\Front\Controller\Admin\ServerController:clearCache');
$this->get('/thumbnails', '\Shaarli\Front\Controller\Admin\ThumbnailsController:index');
$this->get('/metadata', '\Shaarli\Front\Controller\Admin\MetadataController:ajaxRetrieveTitle');
$this->get('/visibility/{visibility}', '\Shaarli\Front\Controller\Admin\SessionFilterController:visibility');
require_once __DIR__ . '/vendor/autoload.php';
-use Shaarli\ApplicationUtils;
+use Shaarli\Helper\ApplicationUtils;
use Shaarli\Security\SessionManager;
// Set 'UTC' as the default timezone if it is not defined in php.ini
$mock = $this->createMock(Router::class);
$mock->expects($this->any())
- ->method('relativePathFor')
- ->willReturn('api/v1/bookmarks/1');
+ ->method('pathFor')
+ ->willReturn('/api/v1/bookmarks/1');
// affect @property-read... seems to work
$this->controller->getCi()->router = $mock;
$response = $this->controller->postLink($request, new Response());
$this->assertEquals(201, $response->getStatusCode());
- $this->assertEquals('api/v1/bookmarks/1', $response->getHeader('Location')[0]);
+ $this->assertEquals('/api/v1/bookmarks/1', $response->getHeader('Location')[0]);
$data = json_decode((string) $response->getBody(), true);
$this->assertEquals(self::NB_FIELDS_LINK, count($data));
$this->assertEquals(43, $data['id']);
$response = $this->controller->postLink($request, new Response());
$this->assertEquals(201, $response->getStatusCode());
- $this->assertEquals('api/v1/bookmarks/1', $response->getHeader('Location')[0]);
+ $this->assertEquals('/api/v1/bookmarks/1', $response->getHeader('Location')[0]);
$data = json_decode((string) $response->getBody(), true);
$this->assertEquals(self::NB_FIELDS_LINK, count($data));
$this->assertEquals(43, $data['id']);
$this->assertEquals(0, $linkDB->count());
}
- /**
- * List the days for which bookmarks have been posted
- */
- public function testDays()
- {
- $this->assertSame(
- ['20100309', '20100310', '20121206', '20121207', '20130614', '20150310'],
- $this->publicLinkDB->days()
- );
-
- $this->assertSame(
- ['20100309', '20100310', '20121206', '20121207', '20130614', '20141125', '20150310'],
- $this->privateLinkDB->days()
- );
- }
-
/**
* The URL corresponds to an existing entry in the DB
*/
$this->publicLinkDB->findByHash('');
}
+ /**
+ * Test filterHash() on a private bookmark while logged out.
+ */
+ public function testFilterHashPrivateWhileLoggedOut()
+ {
+ $this->expectException(BookmarkNotFoundException::class);
+ $this->expectExceptionMessage('The link you are trying to reach does not exist or has been deleted');
+
+ $hash = smallHash('20141125_084734' . 6);
+
+ $this->publicLinkDB->findByHash($hash);
+ }
+
+ /**
+ * Test filterHash() with private key.
+ */
+ public function testFilterHashWithPrivateKey()
+ {
+ $hash = smallHash('20141125_084734' . 6);
+ $privateKey = 'this is usually auto generated';
+
+ $bookmark = $this->privateLinkDB->findByHash($hash);
+ $bookmark->addAdditionalContentEntry('private_key', $privateKey);
+ $this->privateLinkDB->save();
+
+ $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, false);
+ $bookmark = $this->privateLinkDB->findByHash($hash, $privateKey);
+
+ static::assertSame(6, $bookmark->getId());
+ }
+
/**
* Test linksCountPerTag all tags without filter.
* Equal occurrences should be sorted alphabetically.
}
/**
- * Test filterDay while logged in
+ * Test find by dates in the middle of the datastore (sorted by dates) with a single bookmark as a result.
*/
- public function testFilterDayLoggedIn(): void
+ public function testFilterByDateMidTimePeriodSingleBookmark(): void
{
- $bookmarks = $this->privateLinkDB->filterDay('20121206');
- $expectedIds = [4, 9, 1, 0];
+ $bookmarks = $this->privateLinkDB->findByDate(
+ DateTime::createFromFormat('Ymd_His', '20121206_150000'),
+ DateTime::createFromFormat('Ymd_His', '20121206_160000'),
+ $before,
+ $after
+ );
- static::assertCount(4, $bookmarks);
- foreach ($bookmarks as $bookmark) {
- $i = ($i ?? -1) + 1;
- static::assertSame($expectedIds[$i], $bookmark->getId());
- }
+ static::assertCount(1, $bookmarks);
+
+ static::assertSame(9, $bookmarks[0]->getId());
+ static::assertEquals(DateTime::createFromFormat('Ymd_His', '20121206_142300'), $before);
+ static::assertEquals(DateTime::createFromFormat('Ymd_His', '20121206_172539'), $after);
}
/**
- * Test filterDay while logged out
+ * Test find by dates in the middle of the datastore (sorted by dates) with a multiple bookmarks as a result.
*/
- public function testFilterDayLoggedOut(): void
+ public function testFilterByDateMidTimePeriodMultipleBookmarks(): void
{
- $bookmarks = $this->publicLinkDB->filterDay('20121206');
- $expectedIds = [4, 9, 1];
+ $bookmarks = $this->privateLinkDB->findByDate(
+ DateTime::createFromFormat('Ymd_His', '20121206_150000'),
+ DateTime::createFromFormat('Ymd_His', '20121206_180000'),
+ $before,
+ $after
+ );
- static::assertCount(3, $bookmarks);
- foreach ($bookmarks as $bookmark) {
- $i = ($i ?? -1) + 1;
- static::assertSame($expectedIds[$i], $bookmark->getId());
- }
+ static::assertCount(2, $bookmarks);
+
+ static::assertSame(1, $bookmarks[0]->getId());
+ static::assertSame(9, $bookmarks[1]->getId());
+ static::assertEquals(DateTime::createFromFormat('Ymd_His', '20121206_142300'), $before);
+ static::assertEquals(DateTime::createFromFormat('Ymd_His', '20121206_182539'), $after);
+ }
+
+ /**
+ * Test find by dates at the end of the datastore (sorted by dates).
+ */
+ public function testFilterByDateLastTimePeriod(): void
+ {
+ $after = new DateTime();
+ $bookmarks = $this->privateLinkDB->findByDate(
+ DateTime::createFromFormat('Ymd_His', '20150310_114640'),
+ DateTime::createFromFormat('Ymd_His', '20450101_010101'),
+ $before,
+ $after
+ );
+
+ static::assertCount(1, $bookmarks);
+
+ static::assertSame(41, $bookmarks[0]->getId());
+ static::assertEquals(DateTime::createFromFormat('Ymd_His', '20150310_114633'), $before);
+ static::assertNull($after);
+ }
+
+ /**
+ * Test find by dates at the beginning of the datastore (sorted by dates).
+ */
+ public function testFilterByDateFirstTimePeriod(): void
+ {
+ $before = new DateTime();
+ $bookmarks = $this->privateLinkDB->findByDate(
+ DateTime::createFromFormat('Ymd_His', '20000101_101010'),
+ DateTime::createFromFormat('Ymd_His', '20100309_110000'),
+ $before,
+ $after
+ );
+
+ static::assertCount(1, $bookmarks);
+
+ static::assertSame(11, $bookmarks[0]->getId());
+ static::assertNull($before);
+ static::assertEquals(DateTime::createFromFormat('Ymd_His', '20100310_101010'), $after);
+ }
+
+ /**
+ * Test getLatest with a sticky bookmark: it should be ignored and return the latest by creation date instead.
+ */
+ public function testGetLatestWithSticky(): void
+ {
+ $bookmark = $this->publicLinkDB->getLatest();
+
+ static::assertSame(41, $bookmark->getId());
+ }
+
+ /**
+ * Test getLatest with a sticky bookmark: it should be ignored and return the latest by creation date instead.
+ */
+ public function testGetLatestEmptyDatastore(): void
+ {
+ unlink($this->conf->get('resource.datastore'));
+ $this->publicLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, false);
+
+ $bookmark = $this->publicLinkDB->getLatest();
+
+ static::assertNull($bookmark);
}
/**
+++ /dev/null
-<?php
-
-declare(strict_types=1);
-
-namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest;
-
-use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
-use Shaarli\Front\Controller\Admin\ManageShaareController;
-use Shaarli\Http\HttpAccess;
-use Shaarli\TestCase;
-use Slim\Http\Request;
-use Slim\Http\Response;
-
-class AddShaareTest extends TestCase
-{
- use FrontAdminControllerMockHelper;
-
- /** @var ManageShaareController */
- protected $controller;
-
- public function setUp(): void
- {
- $this->createContainer();
-
- $this->container->httpAccess = $this->createMock(HttpAccess::class);
- $this->controller = new ManageShaareController($this->container);
- }
-
- /**
- * Test displaying add link page
- */
- public function testAddShaare(): void
- {
- $assignedVariables = [];
- $this->assignTemplateVars($assignedVariables);
-
- $request = $this->createMock(Request::class);
- $response = new Response();
-
- $result = $this->controller->addShaare($request, $response);
-
- static::assertSame(200, $result->getStatusCode());
- static::assertSame('addlink', (string) $result->getBody());
-
- static::assertSame('Shaare a new link - Shaarli', $assignedVariables['pagetitle']);
- }
-}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Admin;
+
+use Shaarli\Config\ConfigManager;
+use Shaarli\Security\SessionManager;
+use Shaarli\TestCase;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+/**
+ * Test Server administration controller.
+ */
+class ServerControllerTest extends TestCase
+{
+ use FrontAdminControllerMockHelper;
+
+ /** @var ServerController */
+ protected $controller;
+
+ public function setUp(): void
+ {
+ $this->createContainer();
+
+ $this->controller = new ServerController($this->container);
+
+ // initialize dummy cache
+ @mkdir('sandbox/');
+ foreach (['pagecache', 'tmp', 'cache'] as $folder) {
+ @mkdir('sandbox/' . $folder);
+ @touch('sandbox/' . $folder . '/.htaccess');
+ @touch('sandbox/' . $folder . '/1');
+ @touch('sandbox/' . $folder . '/2');
+ }
+ }
+
+ public function tearDown(): void
+ {
+ foreach (['pagecache', 'tmp', 'cache'] as $folder) {
+ @unlink('sandbox/' . $folder . '/.htaccess');
+ @unlink('sandbox/' . $folder . '/1');
+ @unlink('sandbox/' . $folder . '/2');
+ @rmdir('sandbox/' . $folder);
+ }
+ }
+
+ /**
+ * Test default display of server administration page.
+ */
+ public function testIndex(): void
+ {
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ // Save RainTPL assigned variables
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ $result = $this->controller->index($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('server', (string) $result->getBody());
+
+ static::assertSame(PHP_VERSION, $assignedVariables['php_version']);
+ static::assertArrayHasKey('php_has_reached_eol', $assignedVariables);
+ static::assertArrayHasKey('php_eol', $assignedVariables);
+ static::assertArrayHasKey('php_extensions', $assignedVariables);
+ static::assertArrayHasKey('permissions', $assignedVariables);
+ static::assertEmpty($assignedVariables['permissions']);
+
+ static::assertRegExp(
+ '#https://github\.com/shaarli/Shaarli/releases/tag/v\d+\.\d+\.\d+#',
+ $assignedVariables['release_url']
+ );
+ static::assertRegExp('#v\d+\.\d+\.\d+#', $assignedVariables['latest_version']);
+ static::assertRegExp('#(v\d+\.\d+\.\d+|dev)#', $assignedVariables['current_version']);
+ static::assertArrayHasKey('index_url', $assignedVariables);
+ static::assertArrayHasKey('client_ip', $assignedVariables);
+ static::assertArrayHasKey('trusted_proxies', $assignedVariables);
+
+ static::assertSame('Server administration - Shaarli', $assignedVariables['pagetitle']);
+ }
+
+ /**
+ * Test clearing the main cache
+ */
+ public function testClearMainCache(): void
+ {
+ $this->container->conf = $this->createMock(ConfigManager::class);
+ $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) {
+ if ($key === 'resource.page_cache') {
+ return 'sandbox/pagecache';
+ } elseif ($key === 'resource.raintpl_tmp') {
+ return 'sandbox/tmp';
+ } elseif ($key === 'resource.thumbnails_cache') {
+ return 'sandbox/cache';
+ } else {
+ return $default;
+ }
+ });
+
+ $this->container->sessionManager
+ ->expects(static::once())
+ ->method('setSessionParameter')
+ ->with(SessionManager::KEY_SUCCESS_MESSAGES, ['Shaarli\'s cache folder has been cleared!'])
+ ;
+
+ $request = $this->createMock(Request::class);
+ $request->method('getQueryParam')->with('type')->willReturn('main');
+ $response = new Response();
+
+ $result = $this->controller->clearCache($request, $response);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame('/subfolder/admin/server', (string) $result->getHeaderLine('Location'));
+
+ static::assertFileNotExists('sandbox/pagecache/1');
+ static::assertFileNotExists('sandbox/pagecache/2');
+ static::assertFileNotExists('sandbox/tmp/1');
+ static::assertFileNotExists('sandbox/tmp/2');
+
+ static::assertFileExists('sandbox/pagecache/.htaccess');
+ static::assertFileExists('sandbox/tmp/.htaccess');
+ static::assertFileExists('sandbox/cache');
+ static::assertFileExists('sandbox/cache/.htaccess');
+ static::assertFileExists('sandbox/cache/1');
+ static::assertFileExists('sandbox/cache/2');
+ }
+
+ /**
+ * Test clearing thumbnails cache
+ */
+ public function testClearThumbnailsCache(): void
+ {
+ $this->container->conf = $this->createMock(ConfigManager::class);
+ $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) {
+ if ($key === 'resource.page_cache') {
+ return 'sandbox/pagecache';
+ } elseif ($key === 'resource.raintpl_tmp') {
+ return 'sandbox/tmp';
+ } elseif ($key === 'resource.thumbnails_cache') {
+ return 'sandbox/cache';
+ } else {
+ return $default;
+ }
+ });
+
+ $this->container->sessionManager
+ ->expects(static::once())
+ ->method('setSessionParameter')
+ ->willReturnCallback(function (string $key, array $value): SessionManager {
+ static::assertSame(SessionManager::KEY_WARNING_MESSAGES, $key);
+ static::assertCount(1, $value);
+ static::assertStringStartsWith('Thumbnails cache has been cleared.', $value[0]);
+
+ return $this->container->sessionManager;
+ });
+ ;
+
+ $request = $this->createMock(Request::class);
+ $request->method('getQueryParam')->with('type')->willReturn('thumbnails');
+ $response = new Response();
+
+ $result = $this->controller->clearCache($request, $response);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame('/subfolder/admin/server', (string) $result->getHeaderLine('Location'));
+
+ static::assertFileNotExists('sandbox/cache/1');
+ static::assertFileNotExists('sandbox/cache/2');
+
+ static::assertFileExists('sandbox/cache/.htaccess');
+ static::assertFileExists('sandbox/pagecache');
+ static::assertFileExists('sandbox/pagecache/.htaccess');
+ static::assertFileExists('sandbox/pagecache/1');
+ static::assertFileExists('sandbox/pagecache/2');
+ static::assertFileExists('sandbox/tmp');
+ static::assertFileExists('sandbox/tmp/.htaccess');
+ static::assertFileExists('sandbox/tmp/1');
+ static::assertFileExists('sandbox/tmp/2');
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Admin;
+
+use Shaarli\Config\ConfigManager;
+use Shaarli\Formatter\BookmarkMarkdownFormatter;
+use Shaarli\Http\HttpAccess;
+use Shaarli\TestCase;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+class ShaareAddControllerTest extends TestCase
+{
+ use FrontAdminControllerMockHelper;
+
+ /** @var ShaareAddController */
+ protected $controller;
+
+ public function setUp(): void
+ {
+ $this->createContainer();
+
+ $this->container->httpAccess = $this->createMock(HttpAccess::class);
+ $this->controller = new ShaareAddController($this->container);
+ }
+
+ /**
+ * Test displaying add link page
+ */
+ public function testAddShaare(): void
+ {
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $expectedTags = [
+ 'tag1' => 32,
+ 'tag2' => 24,
+ 'tag3' => 1,
+ ];
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('bookmarksCountPerTag')
+ ->willReturn($expectedTags)
+ ;
+ $expectedTags = array_merge($expectedTags, [BookmarkMarkdownFormatter::NO_MD_TAG => 1]);
+
+ $this->container->conf = $this->createMock(ConfigManager::class);
+ $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) {
+ return $key === 'formatter' ? 'markdown' : $default;
+ });
+
+ $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']);
+ static::assertFalse($assignedVariables['default_private_links']);
+ static::assertTrue($assignedVariables['async_metadata']);
+ static::assertSame($expectedTags, $assignedVariables['tags']);
+ }
+
+ /**
+ * Test displaying add link page
+ */
+ public function testAddShaareWithoutMd(): void
+ {
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $expectedTags = [
+ 'tag1' => 32,
+ 'tag2' => 24,
+ 'tag3' => 1,
+ ];
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('bookmarksCountPerTag')
+ ->willReturn($expectedTags)
+ ;
+
+ $result = $this->controller->addShaare($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('addlink', (string) $result->getBody());
+
+ static::assertSame($expectedTags, $assignedVariables['tags']);
+ }
+}
declare(strict_types=1);
-namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest;
+namespace Shaarli\Front\Controller\Admin\ShaareManageControllerTest;
use Shaarli\Bookmark\Bookmark;
use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
use Shaarli\Formatter\BookmarkRawFormatter;
use Shaarli\Formatter\FormatterFactory;
use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
-use Shaarli\Front\Controller\Admin\ManageShaareController;
+use Shaarli\Front\Controller\Admin\ShaareManageController;
use Shaarli\Http\HttpAccess;
use Shaarli\Security\SessionManager;
use Shaarli\TestCase;
{
use FrontAdminControllerMockHelper;
- /** @var ManageShaareController */
+ /** @var ShaareManageController */
protected $controller;
public function setUp(): void
$this->createContainer();
$this->container->httpAccess = $this->createMock(HttpAccess::class);
- $this->controller = new ManageShaareController($this->container);
+ $this->controller = new ShaareManageController($this->container);
}
/**
declare(strict_types=1);
-namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest;
+namespace Shaarli\Front\Controller\Admin\ShaareManageControllerTest;
use Shaarli\Bookmark\Bookmark;
use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
use Shaarli\Formatter\BookmarkFormatter;
use Shaarli\Formatter\FormatterFactory;
use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
-use Shaarli\Front\Controller\Admin\ManageShaareController;
+use Shaarli\Front\Controller\Admin\ShaareManageController;
use Shaarli\Http\HttpAccess;
use Shaarli\Security\SessionManager;
use Shaarli\TestCase;
{
use FrontAdminControllerMockHelper;
- /** @var ManageShaareController */
+ /** @var ShaareManageController */
protected $controller;
public function setUp(): void
$this->createContainer();
$this->container->httpAccess = $this->createMock(HttpAccess::class);
- $this->controller = new ManageShaareController($this->container);
+ $this->controller = new ShaareManageController($this->container);
}
/**
declare(strict_types=1);
-namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest;
+namespace Shaarli\Front\Controller\Admin\ShaareManageControllerTest;
use Shaarli\Bookmark\Bookmark;
use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
-use Shaarli\Front\Controller\Admin\ManageShaareController;
+use Shaarli\Front\Controller\Admin\ShaareManageController;
use Shaarli\Http\HttpAccess;
use Shaarli\Security\SessionManager;
use Shaarli\TestCase;
{
use FrontAdminControllerMockHelper;
- /** @var ManageShaareController */
+ /** @var ShaareManageController */
protected $controller;
public function setUp(): void
$this->createContainer();
$this->container->httpAccess = $this->createMock(HttpAccess::class);
- $this->controller = new ManageShaareController($this->container);
+ $this->controller = new ShaareManageController($this->container);
}
/**
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Admin\ShaareManageControllerTest;
+
+use Shaarli\Bookmark\Bookmark;
+use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
+use Shaarli\Front\Controller\Admin\ShaareManageController;
+use Shaarli\Http\HttpAccess;
+use Shaarli\TestCase;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+/**
+ * Test GET /admin/shaare/private/{hash}
+ */
+class SharePrivateTest extends TestCase
+{
+ use FrontAdminControllerMockHelper;
+
+ /** @var ShaareManageController */
+ protected $controller;
+
+ public function setUp(): void
+ {
+ $this->createContainer();
+
+ $this->container->httpAccess = $this->createMock(HttpAccess::class);
+ $this->controller = new ShaareManageController($this->container);
+ }
+
+ /**
+ * Test shaare private with a private bookmark which does not have a key yet.
+ */
+ public function testSharePrivateWithNewPrivateBookmark(): void
+ {
+ $hash = 'abcdcef';
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $bookmark = (new Bookmark())
+ ->setId(123)
+ ->setUrl('http://domain.tld')
+ ->setTitle('Title 123')
+ ->setPrivate(true)
+ ;
+
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('findByHash')
+ ->with($hash)
+ ->willReturn($bookmark)
+ ;
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('set')
+ ->with($bookmark, true)
+ ->willReturnCallback(function (Bookmark $bookmark): Bookmark {
+ static::assertSame(32, strlen($bookmark->getAdditionalContentEntry('private_key')));
+
+ return $bookmark;
+ })
+ ;
+
+ $result = $this->controller->sharePrivate($request, $response, ['hash' => $hash]);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertRegExp('#/subfolder/shaare/' . $hash . '\?key=\w{32}#', $result->getHeaderLine('Location'));
+ }
+
+ /**
+ * Test shaare private with a private bookmark which does already have a key.
+ */
+ public function testSharePrivateWithExistingPrivateBookmark(): void
+ {
+ $hash = 'abcdcef';
+ $existingKey = 'this is a private key';
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $bookmark = (new Bookmark())
+ ->setId(123)
+ ->setUrl('http://domain.tld')
+ ->setTitle('Title 123')
+ ->setPrivate(true)
+ ->addAdditionalContentEntry('private_key', $existingKey)
+ ;
+
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('findByHash')
+ ->with($hash)
+ ->willReturn($bookmark)
+ ;
+ $this->container->bookmarkService
+ ->expects(static::never())
+ ->method('set')
+ ;
+
+ $result = $this->controller->sharePrivate($request, $response, ['hash' => $hash]);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame('/subfolder/shaare/' . $hash . '?key=' . $existingKey, $result->getHeaderLine('Location'));
+ }
+
+ /**
+ * Test shaare private with a public bookmark.
+ */
+ public function testSharePrivateWithPublicBookmark(): void
+ {
+ $hash = 'abcdcef';
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $bookmark = (new Bookmark())
+ ->setId(123)
+ ->setUrl('http://domain.tld')
+ ->setTitle('Title 123')
+ ->setPrivate(false)
+ ;
+
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('findByHash')
+ ->with($hash)
+ ->willReturn($bookmark)
+ ;
+ $this->container->bookmarkService
+ ->expects(static::never())
+ ->method('set')
+ ;
+
+ $result = $this->controller->sharePrivate($request, $response, ['hash' => $hash]);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame('/subfolder/shaare/' . $hash, $result->getHeaderLine('Location'));
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Admin\ShaarePublishControllerTest;
+
+use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
+use Shaarli\Front\Controller\Admin\ShaarePublishController;
+use Shaarli\Http\HttpAccess;
+use Shaarli\Http\MetadataRetriever;
+use Shaarli\TestCase;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+class DisplayCreateBatchFormTest extends TestCase
+{
+ use FrontAdminControllerMockHelper;
+
+ /** @var ShaarePublishController */
+ protected $controller;
+
+ public function setUp(): void
+ {
+ $this->createContainer();
+
+ $this->container->httpAccess = $this->createMock(HttpAccess::class);
+ $this->container->metadataRetriever = $this->createMock(MetadataRetriever::class);
+ $this->controller = new ShaarePublishController($this->container);
+ }
+
+ /**
+ * TODO
+ */
+ public function testDisplayCreateFormBatch(): void
+ {
+ $urls = [
+ 'https://domain1.tld/url1',
+ 'https://domain2.tld/url2',
+ ' ',
+ 'https://domain3.tld/url3',
+ ];
+
+ $request = $this->createMock(Request::class);
+ $request->method('getParam')->willReturnCallback(function (string $key) use ($urls): ?string {
+ return $key === 'urls' ? implode(PHP_EOL, $urls) : null;
+ });
+ $response = new Response();
+
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ $result = $this->controller->displayCreateBatchForms($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('editlink.batch', (string) $result->getBody());
+
+ static::assertTrue($assignedVariables['batch_mode']);
+ static::assertCount(3, $assignedVariables['links']);
+ static::assertSame($urls[0], $assignedVariables['links'][0]['link']['url']);
+ static::assertSame($urls[1], $assignedVariables['links'][1]['link']['url']);
+ static::assertSame($urls[3], $assignedVariables['links'][2]['link']['url']);
+ }
+}
declare(strict_types=1);
-namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest;
+namespace Shaarli\Front\Controller\Admin\ShaarePublishControllerTest;
use Shaarli\Bookmark\Bookmark;
use Shaarli\Config\ConfigManager;
use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
-use Shaarli\Front\Controller\Admin\ManageShaareController;
+use Shaarli\Front\Controller\Admin\ShaarePublishController;
use Shaarli\Http\HttpAccess;
use Shaarli\Http\MetadataRetriever;
use Shaarli\TestCase;
{
use FrontAdminControllerMockHelper;
- /** @var ManageShaareController */
+ /** @var ShaarePublishController */
protected $controller;
public function setUp(): void
$this->container->httpAccess = $this->createMock(HttpAccess::class);
$this->container->metadataRetriever = $this->createMock(MetadataRetriever::class);
- $this->controller = new ManageShaareController($this->container);
+ $this->controller = new ShaarePublishController($this->container);
}
/**
declare(strict_types=1);
-namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest;
+namespace Shaarli\Front\Controller\Admin\ShaarePublishControllerTest;
use Shaarli\Bookmark\Bookmark;
use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
-use Shaarli\Front\Controller\Admin\ManageShaareController;
+use Shaarli\Front\Controller\Admin\ShaarePublishController;
use Shaarli\Http\HttpAccess;
use Shaarli\Security\SessionManager;
use Shaarli\TestCase;
{
use FrontAdminControllerMockHelper;
- /** @var ManageShaareController */
+ /** @var ShaarePublishController */
protected $controller;
public function setUp(): void
$this->createContainer();
$this->container->httpAccess = $this->createMock(HttpAccess::class);
- $this->controller = new ManageShaareController($this->container);
+ $this->controller = new ShaarePublishController($this->container);
}
/**
declare(strict_types=1);
-namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest;
+namespace Shaarli\Front\Controller\Admin\ShaarePublishControllerTest;
use Shaarli\Bookmark\Bookmark;
use Shaarli\Config\ConfigManager;
use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
-use Shaarli\Front\Controller\Admin\ManageShaareController;
+use Shaarli\Front\Controller\Admin\ShaarePublishController;
use Shaarli\Front\Exception\WrongTokenException;
use Shaarli\Http\HttpAccess;
use Shaarli\Security\SessionManager;
{
use FrontAdminControllerMockHelper;
- /** @var ManageShaareController */
+ /** @var ShaarePublishController */
protected $controller;
public function setUp(): void
$this->createContainer();
$this->container->httpAccess = $this->createMock(HttpAccess::class);
- $this->controller = new ManageShaareController($this->container);
+ $this->controller = new ShaarePublishController($this->container);
}
/**
);
}
+ /**
+ * Test GET /shaare/{hash}?key={key} - Find a link by hash using a private link.
+ */
+ public function testPermalinkWithPrivateKey(): void
+ {
+ $hash = 'abcdef';
+ $privateKey = 'this is a private key';
+
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ $request = $this->createMock(Request::class);
+ $request->method('getParam')->willReturnCallback(function (string $key, $default = null) use ($privateKey) {
+ return $key === 'key' ? $privateKey : $default;
+ });
+ $response = new Response();
+
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('findByHash')
+ ->with($hash, $privateKey)
+ ->willReturn((new Bookmark())->setId(123)->setTitle('Title 1')->setUrl('http://url1.tld'))
+ ;
+
+ $result = $this->controller->permalink($request, $response, ['hash' => $hash]);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('linklist', (string) $result->getBody());
+ static::assertCount(1, $assignedVariables['links']);
+ }
+
/**
* Test getting link list with thumbnail updates.
* -> 2 thumbnails update, only 1 datastore write
public function testValidIndexControllerInvokeDefault(): void
{
$currentDay = new \DateTimeImmutable('2020-05-13');
+ $previousDate = new \DateTime('2 days ago 00:00:00');
+ $nextDate = new \DateTime('today 00:00:00');
$request = $this->createMock(Request::class);
- $request->method('getQueryParam')->willReturn($currentDay->format('Ymd'));
+ $request->method('getQueryParam')->willReturnCallback(function (string $key) use ($currentDay): ?string {
+ return $key === 'day' ? $currentDay->format('Ymd') : null;
+ });
$response = new Response();
// Save RainTPL assigned variables
$assignedVariables = [];
$this->assignTemplateVars($assignedVariables);
- // Links dataset: 2 links with thumbnails
- $this->container->bookmarkService
- ->expects(static::once())
- ->method('days')
- ->willReturnCallback(function () use ($currentDay): array {
- return [
- '20200510',
- $currentDay->format('Ymd'),
- '20200516',
- ];
- })
- ;
$this->container->bookmarkService
->expects(static::once())
- ->method('filterDay')
- ->willReturnCallback(function (): array {
- return [
- (new Bookmark())
- ->setId(1)
- ->setUrl('http://url.tld')
- ->setTitle(static::generateString(50))
- ->setDescription(static::generateString(500))
- ,
- (new Bookmark())
- ->setId(2)
- ->setUrl('http://url2.tld')
- ->setTitle(static::generateString(50))
- ->setDescription(static::generateString(500))
- ,
- (new Bookmark())
- ->setId(3)
- ->setUrl('http://url3.tld')
- ->setTitle(static::generateString(50))
- ->setDescription(static::generateString(500))
- ,
- ];
- })
+ ->method('findByDate')
+ ->willReturnCallback(
+ function ($from, $to, &$previous, &$next) use ($currentDay, $previousDate, $nextDate): array {
+ $previous = $previousDate;
+ $next = $nextDate;
+
+ return [
+ (new Bookmark())
+ ->setId(1)
+ ->setUrl('http://url.tld')
+ ->setTitle(static::generateString(50))
+ ->setDescription(static::generateString(500))
+ ,
+ (new Bookmark())
+ ->setId(2)
+ ->setUrl('http://url2.tld')
+ ->setTitle(static::generateString(50))
+ ->setDescription(static::generateString(500))
+ ,
+ (new Bookmark())
+ ->setId(3)
+ ->setUrl('http://url3.tld')
+ ->setTitle(static::generateString(50))
+ ->setDescription(static::generateString(500))
+ ,
+ ];
+ }
+ )
;
// Make sure that PluginManager hook is triggered
->expects(static::atLeastOnce())
->method('executeHooks')
->withConsecutive(['render_daily'])
- ->willReturnCallback(function (string $hook, array $data, array $param) use ($currentDay): array {
- if ('render_daily' === $hook) {
- static::assertArrayHasKey('linksToDisplay', $data);
- static::assertCount(3, $data['linksToDisplay']);
- static::assertSame(1, $data['linksToDisplay'][0]['id']);
- static::assertSame($currentDay->getTimestamp(), $data['day']);
- static::assertSame('20200510', $data['previousday']);
- static::assertSame('20200516', $data['nextday']);
-
- static::assertArrayHasKey('loggedin', $param);
+ ->willReturnCallback(
+ function (string $hook, array $data, array $param) use ($currentDay, $previousDate, $nextDate): array {
+ if ('render_daily' === $hook) {
+ static::assertArrayHasKey('linksToDisplay', $data);
+ static::assertCount(3, $data['linksToDisplay']);
+ static::assertSame(1, $data['linksToDisplay'][0]['id']);
+ static::assertSame($currentDay->getTimestamp(), $data['day']);
+ static::assertSame($previousDate->format('Ymd'), $data['previousday']);
+ static::assertSame($nextDate->format('Ymd'), $data['nextday']);
+
+ static::assertArrayHasKey('loggedin', $param);
+ }
+
+ return $data;
}
-
- return $data;
- })
+ )
;
$result = $this->controller->index($request, $response);
);
static::assertEquals($currentDay, $assignedVariables['dayDate']);
static::assertEquals($currentDay->getTimestamp(), $assignedVariables['day']);
+ static::assertSame($previousDate->format('Ymd'), $assignedVariables['previousday']);
+ static::assertSame($nextDate->format('Ymd'), $assignedVariables['nextday']);
+ static::assertSame('day', $assignedVariables['type']);
+ static::assertSame('May 13, 2020', $assignedVariables['dayDesc']);
+ static::assertSame('Daily', $assignedVariables['localizedType']);
static::assertCount(3, $assignedVariables['linksToDisplay']);
$link = $assignedVariables['linksToDisplay'][0];
$currentDay = new \DateTimeImmutable('2020-05-13');
$request = $this->createMock(Request::class);
+ $request->method('getQueryParam')->willReturnCallback(function (string $key) use ($currentDay): ?string {
+ return $key === 'day' ? $currentDay->format('Ymd') : null;
+ });
$response = new Response();
// Save RainTPL assigned variables
$assignedVariables = [];
$this->assignTemplateVars($assignedVariables);
- // Links dataset: 2 links with thumbnails
$this->container->bookmarkService
->expects(static::once())
- ->method('days')
+ ->method('findByDate')
->willReturnCallback(function () use ($currentDay): array {
- return [
- $currentDay->format($currentDay->format('Ymd')),
- ];
- })
- ;
- $this->container->bookmarkService
- ->expects(static::once())
- ->method('filterDay')
- ->willReturnCallback(function (): array {
return [
(new Bookmark())
->setId(1)
$assignedVariables = [];
$this->assignTemplateVars($assignedVariables);
- // Links dataset: 2 links with thumbnails
$this->container->bookmarkService
->expects(static::once())
- ->method('days')
+ ->method('findByDate')
->willReturnCallback(function () use ($currentDay): array {
- return [
- $currentDay->format($currentDay->format('Ymd')),
- ];
- })
- ;
- $this->container->bookmarkService
- ->expects(static::once())
- ->method('filterDay')
- ->willReturnCallback(function (): array {
return [
(new Bookmark())->setId(1)->setUrl('http://url.tld')->setTitle('title'),
(new Bookmark())
// Links dataset: 2 links with thumbnails
$this->container->bookmarkService
->expects(static::once())
- ->method('days')
- ->willReturnCallback(function (): array {
- return [];
- })
- ;
- $this->container->bookmarkService
- ->expects(static::once())
- ->method('filterDay')
+ ->method('findByDate')
->willReturnCallback(function (): array {
return [];
})
static::assertSame(200, $result->getStatusCode());
static::assertSame('daily', (string) $result->getBody());
static::assertCount(0, $assignedVariables['linksToDisplay']);
- static::assertSame('Today', $assignedVariables['dayDesc']);
+ static::assertSame('Today - ' . (new \DateTime())->format('F j, Y'), $assignedVariables['dayDesc']);
static::assertEquals((new \DateTime())->setTime(0, 0)->getTimestamp(), $assignedVariables['day']);
static::assertEquals((new \DateTime())->setTime(0, 0), $assignedVariables['dayDate']);
}
new \DateTimeImmutable('2020-05-17'),
new \DateTimeImmutable('2020-05-15'),
new \DateTimeImmutable('2020-05-13'),
+ new \DateTimeImmutable('+1 month'),
];
$request = $this->createMock(Request::class);
(new Bookmark())->setId(2)->setCreated($dates[1])->setUrl('http://domain.tld/2'),
(new Bookmark())->setId(3)->setCreated($dates[1])->setUrl('http://domain.tld/3'),
(new Bookmark())->setId(4)->setCreated($dates[2])->setUrl('http://domain.tld/4'),
+ (new Bookmark())->setId(5)->setCreated($dates[3])->setUrl('http://domain.tld/5'),
]);
$this->container->pageCacheManager
static::assertSame('http://shaarli/subfolder/', $assignedVariables['index_url']);
static::assertSame('http://shaarli/subfolder/daily-rss', $assignedVariables['page_url']);
static::assertFalse($assignedVariables['hide_timestamps']);
- static::assertCount(2, $assignedVariables['days']);
+ static::assertCount(3, $assignedVariables['days']);
$day = $assignedVariables['days'][$dates[0]->format('Ymd')];
+ $date = $dates[0]->setTime(23, 59, 59);
- static::assertEquals($dates[0], $day['date']);
- static::assertSame($dates[0]->format(\DateTime::RSS), $day['date_rss']);
- static::assertSame(format_date($dates[0], false), $day['date_human']);
+ static::assertEquals($date, $day['date']);
+ static::assertSame($date->format(\DateTime::RSS), $day['date_rss']);
+ static::assertSame(format_date($date, false), $day['date_human']);
static::assertSame('http://shaarli/subfolder/daily?day='. $dates[0]->format('Ymd'), $day['absolute_url']);
static::assertCount(1, $day['links']);
static::assertSame(1, $day['links'][0]['id']);
static::assertEquals($dates[0], $day['links'][0]['created']);
$day = $assignedVariables['days'][$dates[1]->format('Ymd')];
+ $date = $dates[1]->setTime(23, 59, 59);
- static::assertEquals($dates[1], $day['date']);
- static::assertSame($dates[1]->format(\DateTime::RSS), $day['date_rss']);
- static::assertSame(format_date($dates[1], false), $day['date_human']);
+ static::assertEquals($date, $day['date']);
+ static::assertSame($date->format(\DateTime::RSS), $day['date_rss']);
+ static::assertSame(format_date($date, false), $day['date_human']);
static::assertSame('http://shaarli/subfolder/daily?day='. $dates[1]->format('Ymd'), $day['absolute_url']);
static::assertCount(2, $day['links']);
static::assertSame(3, $day['links'][1]['id']);
static::assertSame('http://domain.tld/3', $day['links'][1]['url']);
static::assertEquals($dates[1], $day['links'][1]['created']);
+
+ $day = $assignedVariables['days'][$dates[2]->format('Ymd')];
+ $date = $dates[2]->setTime(23, 59, 59);
+
+ static::assertEquals($date, $day['date']);
+ static::assertSame($date->format(\DateTime::RSS), $day['date_rss']);
+ static::assertSame(format_date($date, false), $day['date_human']);
+ static::assertSame('http://shaarli/subfolder/daily?day='. $dates[2]->format('Ymd'), $day['absolute_url']);
+ static::assertCount(1, $day['links']);
+ static::assertSame(4, $day['links'][0]['id']);
+ static::assertSame('http://domain.tld/4', $day['links'][0]['url']);
+ static::assertEquals($dates[2], $day['links'][0]['created']);
}
/**
static::assertFalse($assignedVariables['hide_timestamps']);
static::assertCount(0, $assignedVariables['days']);
}
+
+ /**
+ * Test simple display index with week parameter
+ */
+ public function testSimpleIndexWeekly(): void
+ {
+ $currentDay = new \DateTimeImmutable('2020-05-13');
+ $expectedDay = new \DateTimeImmutable('2020-05-11');
+
+ $request = $this->createMock(Request::class);
+ $request->method('getQueryParam')->willReturnCallback(function (string $key) use ($currentDay): ?string {
+ return $key === 'week' ? $currentDay->format('YW') : null;
+ });
+ $response = new Response();
+
+ // Save RainTPL assigned variables
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('findByDate')
+ ->willReturnCallback(
+ function (): array {
+ return [
+ (new Bookmark())
+ ->setId(1)
+ ->setUrl('http://url.tld')
+ ->setTitle(static::generateString(50))
+ ->setDescription(static::generateString(500))
+ ,
+ (new Bookmark())
+ ->setId(2)
+ ->setUrl('http://url2.tld')
+ ->setTitle(static::generateString(50))
+ ->setDescription(static::generateString(500))
+ ,
+ ];
+ }
+ )
+ ;
+
+ $result = $this->controller->index($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('daily', (string) $result->getBody());
+ static::assertSame(
+ 'Weekly - Week 20 (May 11, 2020) - Shaarli',
+ $assignedVariables['pagetitle']
+ );
+
+ static::assertCount(2, $assignedVariables['linksToDisplay']);
+ static::assertEquals($expectedDay->setTime(0, 0), $assignedVariables['dayDate']);
+ static::assertSame($expectedDay->setTime(0, 0)->getTimestamp(), $assignedVariables['day']);
+ static::assertSame('', $assignedVariables['previousday']);
+ static::assertSame('', $assignedVariables['nextday']);
+ static::assertSame('Week 20 (May 11, 2020)', $assignedVariables['dayDesc']);
+ static::assertSame('week', $assignedVariables['type']);
+ static::assertSame('Weekly', $assignedVariables['localizedType']);
+ }
+
+ /**
+ * Test simple display index with month parameter
+ */
+ public function testSimpleIndexMonthly(): void
+ {
+ $currentDay = new \DateTimeImmutable('2020-05-13');
+ $expectedDay = new \DateTimeImmutable('2020-05-01');
+
+ $request = $this->createMock(Request::class);
+ $request->method('getQueryParam')->willReturnCallback(function (string $key) use ($currentDay): ?string {
+ return $key === 'month' ? $currentDay->format('Ym') : null;
+ });
+ $response = new Response();
+
+ // Save RainTPL assigned variables
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ $this->container->bookmarkService
+ ->expects(static::once())
+ ->method('findByDate')
+ ->willReturnCallback(
+ function (): array {
+ return [
+ (new Bookmark())
+ ->setId(1)
+ ->setUrl('http://url.tld')
+ ->setTitle(static::generateString(50))
+ ->setDescription(static::generateString(500))
+ ,
+ (new Bookmark())
+ ->setId(2)
+ ->setUrl('http://url2.tld')
+ ->setTitle(static::generateString(50))
+ ->setDescription(static::generateString(500))
+ ,
+ ];
+ }
+ )
+ ;
+
+ $result = $this->controller->index($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertSame('daily', (string) $result->getBody());
+ static::assertSame(
+ 'Monthly - May, 2020 - Shaarli',
+ $assignedVariables['pagetitle']
+ );
+
+ static::assertCount(2, $assignedVariables['linksToDisplay']);
+ static::assertEquals($expectedDay->setTime(0, 0), $assignedVariables['dayDate']);
+ static::assertSame($expectedDay->setTime(0, 0)->getTimestamp(), $assignedVariables['day']);
+ static::assertSame('', $assignedVariables['previousday']);
+ static::assertSame('', $assignedVariables['nextday']);
+ static::assertSame('May, 2020', $assignedVariables['dayDesc']);
+ static::assertSame('month', $assignedVariables['type']);
+ static::assertSame('Monthly', $assignedVariables['localizedType']);
+ }
+
+ /**
+ * Test simple display RSS with week parameter
+ */
+ public function testSimpleRssWeekly(): void
+ {
+ $dates = [
+ new \DateTimeImmutable('2020-05-19'),
+ new \DateTimeImmutable('2020-05-13'),
+ ];
+ $expectedDates = [
+ new \DateTimeImmutable('2020-05-24 23:59:59'),
+ new \DateTimeImmutable('2020-05-17 23:59:59'),
+ ];
+
+ $this->container->environment['QUERY_STRING'] = 'week';
+ $request = $this->createMock(Request::class);
+ $request->method('getQueryParam')->willReturnCallback(function (string $key): ?string {
+ return $key === 'week' ? '' : null;
+ });
+ $response = new Response();
+
+ $this->container->bookmarkService->expects(static::once())->method('search')->willReturn([
+ (new Bookmark())->setId(1)->setCreated($dates[0])->setUrl('http://domain.tld/1'),
+ (new Bookmark())->setId(2)->setCreated($dates[1])->setUrl('http://domain.tld/2'),
+ (new Bookmark())->setId(3)->setCreated($dates[1])->setUrl('http://domain.tld/3'),
+ ]);
+
+ // Save RainTPL assigned variables
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ $result = $this->controller->rss($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertStringContainsString('application/rss', $result->getHeader('Content-Type')[0]);
+ static::assertSame('dailyrss', (string) $result->getBody());
+ static::assertSame('Shaarli', $assignedVariables['title']);
+ static::assertSame('http://shaarli/subfolder/', $assignedVariables['index_url']);
+ static::assertSame('http://shaarli/subfolder/daily-rss?week', $assignedVariables['page_url']);
+ static::assertFalse($assignedVariables['hide_timestamps']);
+ static::assertCount(2, $assignedVariables['days']);
+
+ $day = $assignedVariables['days'][$dates[0]->format('YW')];
+ $date = $expectedDates[0];
+
+ static::assertEquals($date, $day['date']);
+ static::assertSame($date->format(\DateTime::RSS), $day['date_rss']);
+ static::assertSame('Week 21 (May 18, 2020)', $day['date_human']);
+ static::assertSame('http://shaarli/subfolder/daily?week='. $dates[0]->format('YW'), $day['absolute_url']);
+ static::assertCount(1, $day['links']);
+
+ $day = $assignedVariables['days'][$dates[1]->format('YW')];
+ $date = $expectedDates[1];
+
+ static::assertEquals($date, $day['date']);
+ static::assertSame($date->format(\DateTime::RSS), $day['date_rss']);
+ static::assertSame('Week 20 (May 11, 2020)', $day['date_human']);
+ static::assertSame('http://shaarli/subfolder/daily?week='. $dates[1]->format('YW'), $day['absolute_url']);
+ static::assertCount(2, $day['links']);
+ }
+
+ /**
+ * Test simple display RSS with month parameter
+ */
+ public function testSimpleRssMonthly(): void
+ {
+ $dates = [
+ new \DateTimeImmutable('2020-05-19'),
+ new \DateTimeImmutable('2020-04-13'),
+ ];
+ $expectedDates = [
+ new \DateTimeImmutable('2020-05-31 23:59:59'),
+ new \DateTimeImmutable('2020-04-30 23:59:59'),
+ ];
+
+ $this->container->environment['QUERY_STRING'] = 'month';
+ $request = $this->createMock(Request::class);
+ $request->method('getQueryParam')->willReturnCallback(function (string $key): ?string {
+ return $key === 'month' ? '' : null;
+ });
+ $response = new Response();
+
+ $this->container->bookmarkService->expects(static::once())->method('search')->willReturn([
+ (new Bookmark())->setId(1)->setCreated($dates[0])->setUrl('http://domain.tld/1'),
+ (new Bookmark())->setId(2)->setCreated($dates[1])->setUrl('http://domain.tld/2'),
+ (new Bookmark())->setId(3)->setCreated($dates[1])->setUrl('http://domain.tld/3'),
+ ]);
+
+ // Save RainTPL assigned variables
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ $result = $this->controller->rss($request, $response);
+
+ static::assertSame(200, $result->getStatusCode());
+ static::assertStringContainsString('application/rss', $result->getHeader('Content-Type')[0]);
+ static::assertSame('dailyrss', (string) $result->getBody());
+ static::assertSame('Shaarli', $assignedVariables['title']);
+ static::assertSame('http://shaarli/subfolder/', $assignedVariables['index_url']);
+ static::assertSame('http://shaarli/subfolder/daily-rss?month', $assignedVariables['page_url']);
+ static::assertFalse($assignedVariables['hide_timestamps']);
+ static::assertCount(2, $assignedVariables['days']);
+
+ $day = $assignedVariables['days'][$dates[0]->format('Ym')];
+ $date = $expectedDates[0];
+
+ static::assertEquals($date, $day['date']);
+ static::assertSame($date->format(\DateTime::RSS), $day['date_rss']);
+ static::assertSame('May, 2020', $day['date_human']);
+ static::assertSame('http://shaarli/subfolder/daily?month='. $dates[0]->format('Ym'), $day['absolute_url']);
+ static::assertCount(1, $day['links']);
+
+ $day = $assignedVariables['days'][$dates[1]->format('Ym')];
+ $date = $expectedDates[1];
+
+ static::assertEquals($date, $day['date']);
+ static::assertSame($date->format(\DateTime::RSS), $day['date_rss']);
+ static::assertSame('April, 2020', $day['date_human']);
+ static::assertSame('http://shaarli/subfolder/daily?month='. $dates[1]->format('Ym'), $day['absolute_url']);
+ static::assertCount(2, $day['links']);
+ }
}
static::assertIsArray($assignedVariables['languages']);
static::assertSame('Automatic', $assignedVariables['languages']['auto']);
static::assertSame('French', $assignedVariables['languages']['fr']);
+
+ static::assertSame(PHP_VERSION, $assignedVariables['php_version']);
+ static::assertArrayHasKey('php_has_reached_eol', $assignedVariables);
+ static::assertArrayHasKey('php_eol', $assignedVariables);
+ static::assertArrayHasKey('php_extensions', $assignedVariables);
+ static::assertArrayHasKey('permissions', $assignedVariables);
+ static::assertEmpty($assignedVariables['permissions']);
+
+ static::assertSame('Install Shaarli', $assignedVariables['pagetitle']);
}
/**
<?php
-namespace Shaarli;
+namespace Shaarli\Helper;
use Shaarli\Config\ConfigManager;
+use Shaarli\FakeApplicationUtils;
require_once 'tests/utils/FakeApplicationUtils.php';
);
}
+ /**
+ * Checks resource permissions in minimal mode.
+ */
+ public function testCheckCurrentResourcePermissionsErrorsMinimalMode(): void
+ {
+ $conf = new ConfigManager('');
+ $conf->set('resource.thumbnails_cache', 'null/cache');
+ $conf->set('resource.config', 'null/data/config.php');
+ $conf->set('resource.data_dir', 'null/data');
+ $conf->set('resource.datastore', 'null/data/store.php');
+ $conf->set('resource.ban_file', 'null/data/ipbans.php');
+ $conf->set('resource.log', 'null/data/log.txt');
+ $conf->set('resource.page_cache', 'null/pagecache');
+ $conf->set('resource.raintpl_tmp', 'null/tmp');
+ $conf->set('resource.raintpl_tpl', 'null/tpl');
+ $conf->set('resource.raintpl_theme', 'null/tpl/default');
+ $conf->set('resource.update_check', 'null/data/lastupdatecheck.txt');
+
+ static::assertSame(
+ [
+ '"null/tpl" directory is not readable',
+ '"null/tpl/default" directory is not readable',
+ '"null/tmp" directory is not readable',
+ '"null/tmp" directory is not writable'
+ ],
+ ApplicationUtils::checkResourcePermissions($conf, true)
+ );
+ }
+
/**
* Check update with 'dev' as curent version (master branch).
* It should always return false.
ApplicationUtils::checkUpdate('dev', self::$testUpdateFile, 100, true, true)
);
}
+
+ /**
+ * Basic test of getPhpExtensionsRequirement()
+ */
+ public function testGetPhpExtensionsRequirementSimple(): void
+ {
+ static::assertCount(8, ApplicationUtils::getPhpExtensionsRequirement());
+ static::assertSame([
+ 'name' => 'json',
+ 'required' => true,
+ 'desc' => 'Configuration parsing',
+ 'loaded' => true,
+ ], ApplicationUtils::getPhpExtensionsRequirement()[0]);
+ }
+
+ /**
+ * Test getPhpEol with a known version: 7.4 -> 2022
+ */
+ public function testGetKnownPhpEol(): void
+ {
+ static::assertSame('2022-11-28', ApplicationUtils::getPhpEol('7.4.7'));
+ }
+
+ /**
+ * Test getPhpEol with an unknown version: 7.4 -> 2022
+ */
+ public function testGetUnknownPhpEol(): void
+ {
+ static::assertSame(
+ (((int) (new \DateTime())->format('Y')) + 2) . (new \DateTime())->format('-m-d'),
+ ApplicationUtils::getPhpEol('7.51.34')
+ );
+ }
}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Helper;
+
+use Shaarli\Bookmark\Bookmark;
+use Shaarli\TestCase;
+use Slim\Http\Request;
+
+class DailyPageHelperTest extends TestCase
+{
+ /**
+ * @dataProvider getRequestedTypes
+ */
+ public function testExtractRequestedType(array $queryParams, string $expectedType): void
+ {
+ $request = $this->createMock(Request::class);
+ $request->method('getQueryParam')->willReturnCallback(function ($key) use ($queryParams): ?string {
+ return $queryParams[$key] ?? null;
+ });
+
+ $type = DailyPageHelper::extractRequestedType($request);
+
+ static::assertSame($type, $expectedType);
+ }
+
+ /**
+ * @dataProvider getRequestedDateTimes
+ */
+ public function testExtractRequestedDateTime(
+ string $type,
+ string $input,
+ ?Bookmark $bookmark,
+ \DateTimeInterface $expectedDateTime,
+ string $compareFormat = 'Ymd'
+ ): void {
+ $dateTime = DailyPageHelper::extractRequestedDateTime($type, $input, $bookmark);
+
+ static::assertSame($dateTime->format($compareFormat), $expectedDateTime->format($compareFormat));
+ }
+
+ public function testExtractRequestedDateTimeExceptionUnknownType(): void
+ {
+ $this->expectException(\Exception::class);
+ $this->expectExceptionMessage('Unsupported daily format type');
+
+ DailyPageHelper::extractRequestedDateTime('nope', null, null);
+ }
+
+ /**
+ * @dataProvider getFormatsByType
+ */
+ public function testGetFormatByType(string $type, string $expectedFormat): void
+ {
+ $format = DailyPageHelper::getFormatByType($type);
+
+ static::assertSame($expectedFormat, $format);
+ }
+
+ public function testGetFormatByTypeExceptionUnknownType(): void
+ {
+ $this->expectException(\Exception::class);
+ $this->expectExceptionMessage('Unsupported daily format type');
+
+ DailyPageHelper::getFormatByType('nope');
+ }
+
+ /**
+ * @dataProvider getStartDatesByType
+ */
+ public function testGetStartDatesByType(
+ string $type,
+ \DateTimeImmutable $dateTime,
+ \DateTimeInterface $expectedDateTime
+ ): void {
+ $startDateTime = DailyPageHelper::getStartDateTimeByType($type, $dateTime);
+
+ static::assertEquals($expectedDateTime, $startDateTime);
+ }
+
+ public function testGetStartDatesByTypeExceptionUnknownType(): void
+ {
+ $this->expectException(\Exception::class);
+ $this->expectExceptionMessage('Unsupported daily format type');
+
+ DailyPageHelper::getStartDateTimeByType('nope', new \DateTimeImmutable());
+ }
+
+ /**
+ * @dataProvider getEndDatesByType
+ */
+ public function testGetEndDatesByType(
+ string $type,
+ \DateTimeImmutable $dateTime,
+ \DateTimeInterface $expectedDateTime
+ ): void {
+ $endDateTime = DailyPageHelper::getEndDateTimeByType($type, $dateTime);
+
+ static::assertEquals($expectedDateTime, $endDateTime);
+ }
+
+ public function testGetEndDatesByTypeExceptionUnknownType(): void
+ {
+ $this->expectException(\Exception::class);
+ $this->expectExceptionMessage('Unsupported daily format type');
+
+ DailyPageHelper::getEndDateTimeByType('nope', new \DateTimeImmutable());
+ }
+
+ /**
+ * @dataProvider getDescriptionsByType
+ */
+ public function testGeDescriptionsByType(
+ string $type,
+ \DateTimeImmutable $dateTime,
+ string $expectedDescription
+ ): void {
+ $description = DailyPageHelper::getDescriptionByType($type, $dateTime);
+
+ static::assertEquals($expectedDescription, $description);
+ }
+
+ public function getDescriptionByTypeExceptionUnknownType(): void
+ {
+ $this->expectException(\Exception::class);
+ $this->expectExceptionMessage('Unsupported daily format type');
+
+ DailyPageHelper::getDescriptionByType('nope', new \DateTimeImmutable());
+ }
+
+ /**
+ * @dataProvider getRssLengthsByType
+ */
+ public function testGeRssLengthsByType(string $type): void {
+ $length = DailyPageHelper::getRssLengthByType($type);
+
+ static::assertIsInt($length);
+ }
+
+ public function testGeRssLengthsByTypeExceptionUnknownType(): void
+ {
+ $this->expectException(\Exception::class);
+ $this->expectExceptionMessage('Unsupported daily format type');
+
+ DailyPageHelper::getRssLengthByType('nope');
+ }
+
+ /**
+ * Data provider for testExtractRequestedType() test method.
+ */
+ public function getRequestedTypes(): array
+ {
+ return [
+ [['month' => null], DailyPageHelper::DAY],
+ [['month' => ''], DailyPageHelper::MONTH],
+ [['month' => 'content'], DailyPageHelper::MONTH],
+ [['week' => null], DailyPageHelper::DAY],
+ [['week' => ''], DailyPageHelper::WEEK],
+ [['week' => 'content'], DailyPageHelper::WEEK],
+ [['day' => null], DailyPageHelper::DAY],
+ [['day' => ''], DailyPageHelper::DAY],
+ [['day' => 'content'], DailyPageHelper::DAY],
+ ];
+ }
+
+ /**
+ * Data provider for testExtractRequestedDateTime() test method.
+ */
+ public function getRequestedDateTimes(): array
+ {
+ return [
+ [DailyPageHelper::DAY, '20201013', null, new \DateTime('2020-10-13')],
+ [
+ DailyPageHelper::DAY,
+ '',
+ (new Bookmark())->setCreated($date = new \DateTime('2020-10-13 12:05:31')),
+ $date,
+ ],
+ [DailyPageHelper::DAY, '', null, new \DateTime()],
+ [DailyPageHelper::WEEK, '202030', null, new \DateTime('2020-07-20')],
+ [
+ DailyPageHelper::WEEK,
+ '',
+ (new Bookmark())->setCreated($date = new \DateTime('2020-10-13 12:05:31')),
+ new \DateTime('2020-10-13'),
+ ],
+ [DailyPageHelper::WEEK, '', null, new \DateTime(), 'Ym'],
+ [DailyPageHelper::MONTH, '202008', null, new \DateTime('2020-08-01'), 'Ym'],
+ [
+ DailyPageHelper::MONTH,
+ '',
+ (new Bookmark())->setCreated($date = new \DateTime('2020-10-13 12:05:31')),
+ new \DateTime('2020-10-13'),
+ 'Ym'
+ ],
+ [DailyPageHelper::MONTH, '', null, new \DateTime(), 'Ym'],
+ ];
+ }
+
+ /**
+ * Data provider for testGetFormatByType() test method.
+ */
+ public function getFormatsByType(): array
+ {
+ return [
+ [DailyPageHelper::DAY, 'Ymd'],
+ [DailyPageHelper::WEEK, 'YW'],
+ [DailyPageHelper::MONTH, 'Ym'],
+ ];
+ }
+
+ /**
+ * Data provider for testGetStartDatesByType() test method.
+ */
+ public function getStartDatesByType(): array
+ {
+ return [
+ [DailyPageHelper::DAY, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-09 00:00:00')],
+ [DailyPageHelper::WEEK, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-05 00:00:00')],
+ [DailyPageHelper::MONTH, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-01 00:00:00')],
+ ];
+ }
+
+ /**
+ * Data provider for testGetEndDatesByType() test method.
+ */
+ public function getEndDatesByType(): array
+ {
+ return [
+ [DailyPageHelper::DAY, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-09 23:59:59')],
+ [DailyPageHelper::WEEK, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-11 23:59:59')],
+ [DailyPageHelper::MONTH, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-31 23:59:59')],
+ ];
+ }
+
+ /**
+ * Data provider for testGetDescriptionsByType() test method.
+ */
+ public function getDescriptionsByType(): array
+ {
+ return [
+ [DailyPageHelper::DAY, $date = new \DateTimeImmutable(), 'Today - ' . $date->format('F j, Y')],
+ [DailyPageHelper::DAY, $date = new \DateTimeImmutable('-1 day'), 'Yesterday - ' . $date->format('F j, Y')],
+ [DailyPageHelper::DAY, new \DateTimeImmutable('2020-10-09 04:05:06'), 'October 9, 2020'],
+ [DailyPageHelper::WEEK, new \DateTimeImmutable('2020-10-09 04:05:06'), 'Week 41 (October 5, 2020)'],
+ [DailyPageHelper::MONTH, new \DateTimeImmutable('2020-10-09 04:05:06'), 'October, 2020'],
+ ];
+ }
+
+ /**
+ * Data provider for testGetDescriptionsByType() test method.
+ */
+ public function getRssLengthsByType(): array
+ {
+ return [
+ [DailyPageHelper::DAY],
+ [DailyPageHelper::WEEK],
+ [DailyPageHelper::MONTH],
+ ];
+ }
+}
<?php
-namespace Shaarli;
+namespace Shaarli\Helper;
use Exception;
+use Shaarli\Exceptions\IOException;
+use Shaarli\TestCase;
/**
* Class FileUtilsTest
*
* Test file utility class.
*/
-class FileUtilsTest extends \Shaarli\TestCase
+class FileUtilsTest extends TestCase
{
/**
* @var string Test file path.
*/
protected static $file = 'sandbox/flat.db';
+ protected function setUp(): void
+ {
+ @mkdir('sandbox');
+ mkdir('sandbox/folder2');
+ touch('sandbox/file1');
+ touch('sandbox/file2');
+ mkdir('sandbox/folder1');
+ touch('sandbox/folder1/file1');
+ touch('sandbox/folder1/file2');
+ mkdir('sandbox/folder3');
+ mkdir('/tmp/shaarli-to-delete');
+ }
+
/**
* Delete test file after every test.
*/
protected function tearDown(): void
{
@unlink(self::$file);
+
+ @unlink('sandbox/folder1/file1');
+ @unlink('sandbox/folder1/file2');
+ @rmdir('sandbox/folder1');
+ @unlink('sandbox/file1');
+ @unlink('sandbox/file2');
+ @rmdir('sandbox/folder2');
+ @rmdir('sandbox/folder3');
+ @rmdir('/tmp/shaarli-to-delete');
}
/**
$this->assertEquals(null, FileUtils::readFlatDB(self::$file));
$this->assertEquals(['test'], FileUtils::readFlatDB(self::$file, ['test']));
}
+
+ /**
+ * Test clearFolder with self delete and excluded files
+ */
+ public function testClearFolderSelfDeleteWithExclusion(): void
+ {
+ FileUtils::clearFolder('sandbox', true, ['file2']);
+
+ static::assertFileExists('sandbox/folder1/file2');
+ static::assertFileExists('sandbox/folder1');
+ static::assertFileExists('sandbox/file2');
+ static::assertFileExists('sandbox');
+
+ static::assertFileNotExists('sandbox/folder1/file1');
+ static::assertFileNotExists('sandbox/file1');
+ static::assertFileNotExists('sandbox/folder3');
+ }
+
+ /**
+ * Test clearFolder with self delete and excluded files
+ */
+ public function testClearFolderSelfDeleteWithoutExclusion(): void
+ {
+ FileUtils::clearFolder('sandbox', true);
+
+ static::assertFileNotExists('sandbox');
+ }
+
+ /**
+ * Test clearFolder with self delete and excluded files
+ */
+ public function testClearFolderNoSelfDeleteWithoutExclusion(): void
+ {
+ FileUtils::clearFolder('sandbox', false);
+
+ static::assertFileExists('sandbox');
+
+ // 2 because '.' and '..'
+ static::assertCount(2, new \DirectoryIterator('sandbox'));
+ }
+
+ /**
+ * Test clearFolder on a file instead of a folder
+ */
+ public function testClearFolderOnANonDirectory(): void
+ {
+ $this->expectException(IOException::class);
+ $this->expectExceptionMessage('Provided path is not a directory.');
+
+ FileUtils::clearFolder('sandbox/file1', false);
+ }
+
+ /**
+ * Test clearFolder on a file instead of a folder
+ */
+ public function testClearFolderOutsideOfShaarliDirectory(): void
+ {
+ $this->expectException(IOException::class);
+ $this->expectExceptionMessage('Trying to delete a folder outside of Shaarli path.');
+
+
+ FileUtils::clearFolder('/tmp/shaarli-to-delete', true);
+ }
}
namespace Shaarli\Security;
use Psr\Log\LoggerInterface;
-use Shaarli\FileUtils;
+use Shaarli\Helper\FileUtils;
use Shaarli\TestCase;
/**
namespace Shaarli;
+use Shaarli\Helper\ApplicationUtils;
+
/**
* Fake ApplicationUtils class to avoid HTTP requests
*/
<?php
-use Shaarli\FileUtils;
+use Shaarli\Helper\FileUtils;
use Shaarli\History;
/**
</form>
</div>
</div>
+
+<div class="pure-g addlink-batch-show-more-block pure-u-0">
+ <div class="pure-u-lg-1-3 pure-u-1-24"></div>
+ <div class="pure-u-lg-1-3 pure-u-22-24 addlink-batch-show-more">
+ <a href="#">{'BULK CREATION'|t} <i class="fa fa-plus-circle" aria-hidden="true"></i></a>
+ </div>
+</div>
+
+<div class="addlink-batch-form-block">
+ {if="empty($async_metadata)"}
+ <div class="pure-g pure-alert pure-alert-warning pure-alert-closable">
+ <div class="pure-u-2-24"></div>
+ <div class="pure-u-20-24">
+ <p>
+ {'Metadata asynchronous retrieval is disabled.'|t}
+ {'We recommend that you enable the setting <em>general > enable_async_metadata</em> in your configuration file to use bulk link creation.'|t}
+ </p>
+ </div>
+ <div class="pure-u-2-24">
+ <i class="fa fa-times pure-alert-close"></i>
+ </div>
+ </div>
+ {/if}
+
+ <div class="pure-g">
+ <div class="pure-u-lg-1-3 pure-u-1-24"></div>
+ <div id="batch-addlink-form" class="page-form page-form-light pure-u-lg-1-3 pure-u-22-24">
+ <h2 class="window-title">{"Shaare multiple new links"|t}</h2>
+ <form method="POST" action="{$base_path}/admin/shaare-batch" name="batch-addform" class="batch-addform">
+ <div>
+ <label for="urls">{'Add one URL per line to create multiple bookmarks.'|t}</label>
+ <textarea name="urls" id="urls"></textarea>
+
+ <div>
+ <label for="tags">{'Tags'|t}</label>
+ </div>
+ <div>
+ <input type="text" name="tags" id="tags" class="lf_input"
+ data-list="{loop="$tags"}{$key}, {/loop}" data-multiple data-autofirst autocomplete="off">
+ </div>
+
+ <div>
+ <input type="hidden" name="private" value="0">
+ <input type="checkbox" name="private" {if="$default_private_links"} checked="checked"{/if}>
+ <label for="lf_private">{'Private'|t}</label>
+ </div>
+ </div>
+ <div>
+ <input type="hidden" name="token" value="{$token}">
+ <input type="submit" value="{'Add links'|t}">
+ </div>
+ </form>
+ </div>
+ </div>
+</div>
+
{include="page.footer"}
</body>
</html>
<body>
{include="page.header"}
+<div class="pure-g">
+ <div class="pure-u-1 pure-alert pure-alert-success tag-sort">
+ <a href="{$base_path}/daily?day">{'Daily'|t}</a>
+ <a href="{$base_path}/daily?week">{'Weekly'|t}</a>
+ <a href="{$base_path}/daily?month">{'Monthly'|t}</a>
+ </div>
+</div>
+
+
<div class="pure-g">
<div class="pure-u-lg-1-6 pure-u-1-24"></div>
<div class="pure-u-lg-2-3 pure-u-22-24 page-form page-visitor" id="daily">
<h2 class="window-title">
- {'The Daily Shaarli'|t}
- <a href="{$base_path}/daily-rss" title="{'1 RSS entry per day'|t}"><i class="fa fa-rss"></i></a>
+ {$localizedType} Shaarli
+ <a href="{$base_path}/daily-rss?{$type}"
+ title="{function="t('1 RSS entry per :type', '', 1, 'shaarli', [':type' => t($type)])"}"
+ >
+ <i class="fa fa-rss"></i>
+ </a>
</h2>
<div id="plugin_zone_start_daily" class="plugin_zone">
<div class="pure-g">
<div class="pure-u-lg-1-3 pure-u-1 center">
{if="$previousday"}
- <a href="{$base_path}/daily?day={$previousday}">
+ <a href="{$base_path}/daily?{$type}={$previousday}">
<i class="fa fa-arrow-left"></i>
- {'Previous day'|t}
+ {function="t('Previous :type', '', 1, 'shaarli', [':type' => t($type)], true)"}
</a>
{/if}
</div>
<div class="daily-desc pure-u-lg-1-3 pure-u-1 center">
- {'All links of one day in a single page.'|t}
+ {function="t('All links of one :type in a single page.', '', 1, 'shaarli', [':type' => t($type)])"}
</div>
<div class="pure-u-lg-1-3 pure-u-1 center">
{if="$nextday"}
- <a href="{$base_path}/daily?day={$nextday}">
- {'Next day'|t}
+ <a href="{$base_path}/daily?{$type}={$nextday}">
+ {function="t('Next :type', '', 1, 'shaarli', [':type' => t($type)], true)"}
<i class="fa fa-arrow-right"></i>
</a>
{/if}
</div>
<div>
<h3 class="window-subtitle">
- {if="!empty($dayDesc)"}
- {$dayDesc} -
- {/if}
- {function="format_date($dayDate, false)"}
+ {$dayDesc}
</h3>
<div id="plugin_zone_about_daily" class="plugin_zone">
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
- <title>Daily - {$title}</title>
+ <title>{$localizedType} - {$title}</title>
<link>{$index_url}</link>
- <description>Daily shaared bookmarks</description>
+ <description>{function="t('All links of one :type in a single page.', '', 1, 'shaarli', [':type' => t($type)])"}</description>
<language>{$language}</language>
<copyright>{$index_url}</copyright>
<generator>Shaarli</generator>
{loop="$value.links"}
<h3><a href="{$value.url}">{$value.title}</a></h3>
<small>
- {if="!$hide_timestamps"}{$value.created|format_date} - {/if}{if="$value.tags"}{$value.tags}{/if}<br>
+ {if="!$hide_timestamps"}{$value.created|format_date} — {/if}
+ <a href="{$index_url}shaare/{$value.shorturl}">{'Permalink'|t}</a>
+ {if="$value.tags"} — {$value.tags}{/if}
+ <br>
{$value.url}
</small><br>
{if="$value.thumbnail"}<img src="{$index_url}{$value.thumbnail}#" alt="thumbnail" />{/if}<br>
{if="$value.description"}{$value.description}{/if}
- <br><br><hr>
+ <br><hr>
{/loop}
]]></description>
</item>
--- /dev/null
+<!DOCTYPE html>
+<html{if="$language !== 'auto'"} lang="{$language}"{/if}>
+<head>
+ {include="includes"}
+</head>
+<body>
+<div class="dark-layer">
+ <div class="screen-center">
+ <div><span class="progressbar-current"></span> / <span class="progressbar-max"></span></div>
+ <div class="progressbar">
+ <div></div>
+ </div>
+ </div>
+</div>
+
+{include="page.header"}
+
+<div class="center">
+ <input type="submit" name="save_edit_batch" class="pure-button-shaarli" value="{'Save all'|t}">
+</div>
+
+{loop="$links"}
+ {include="editlink"}
+{/loop}
+
+<div class="center">
+ <input type="submit" name="save_edit_batch" class="pure-button-shaarli" value="{'Save all'|t}">
+</div>
+
+{include="page.footer"}
+{if="$async_metadata"}<script src="{$asset_path}/js/metadata.min.js?v={$version_hash}#"></script>{/if}
+<script src="{$asset_path}/js/shaare_batch.min.js?v={$version_hash}#"></script>
+{if="empty($batch_mode)"}
<!DOCTYPE html>
<html{if="$language !== 'auto'"} lang="{$language}"{/if}>
<head>
</head>
<body>
{include="page.header"}
+{else}
+ {ignore}Lil hack: when included in a loop in batch mode, `$value` is assigned by RainTPL with template vars.{/ignore}
+ {function="extract($value) ? '' : ''"}
+{/if}
<div id="editlinkform" class="edit-link-container" class="pure-g">
<div class="pure-u-lg-1-5 pure-u-1-24"></div>
<form method="post"
<div>
<input type="checkbox" name="lf_private" id="lf_private"
- {if="($link_is_new && $default_private_links || $link.private == true)"}
+ {if="$link.private === true"}
checked="checked"
{/if}>
<label for="lf_private">{'Private'|t}</label>
<div class="submit-buttons center">
+ {if="!empty($batch_mode)"}
+ <a href="#" class="button button-grey" name="cancel-batch-link"
+ title="{'Remove this bookmark from batch creation/modification.'}"
+ >
+ {'Cancel'|t}
+ </a>
+ {/if}
<input type="submit" name="save_edit" class="" id="button-save-edit"
value="{if="$link_is_new"}{'Save'|t}{else}{'Apply Changes'|t}{/if}">
{if="!$link_is_new"}
{/if}
</form>
</div>
+
+{if="empty($batch_mode)"}
{include="page.footer"}
{if="$link_is_new && $async_metadata"}<script src="{$asset_path}/js/metadata.min.js?v={$version_hash}#"></script>{/if}
</body>
</html>
+{/if}
</div>
</div>
</form>
+
+<div class="pure-g">
+ <div class="pure-u-lg-1-6 pure-u-1-24"></div>
+ <div class="pure-u-lg-2-3 pure-u-22-24 page-form page-form-complete">
+ <h2 class="window-title">{'Server requirements'|t}</h2>
+
+ {include="server.requirements"}
+ </div>
+</div>
+
{include="page.footer"}
</body>
</html>
{$strAddTag=t('Add tag')}
{$strToggleSticky=t('Toggle sticky')}
{$strSticky=t('Sticky')}
+ {$strShaarePrivate=t('Share a private link')}
{ignore}End of translations{/ignore}
{loop="links"}
<div class="anchor" id="{$value.shorturl}"></div>
{$strPermalinkLc}
</a>
+ {if="$is_logged_in && $value.private"}
+ <a href="{$base_path}/admin/shaare/private/{$value.shorturl}?token={$token}" title="{$strShaarePrivate}">
+ <i class="fa fa-share-alt"></i>
+ </a>
+ {/if}
+
<div class="pure-u-0 pure-u-lg-visible">
{if="isset($value.link_plugin)"}
·
--- /dev/null
+<!DOCTYPE html>
+<html{if="$language !== 'auto'"} lang="{$language}"{/if}>
+<head>
+ {include="includes"}
+</head>
+<body>
+{include="page.header"}
+
+<div class="pure-g">
+ <div class="pure-u-lg-1-4 pure-u-1-24"></div>
+ <div class="pure-u-lg-1-2 pure-u-22-24 page-form server-tables-page">
+ <h2 class="window-title">{'Server administration'|t}</h2>
+
+ <h3 class="window-subtitle">{'General'|t}</h3>
+
+ <div class="pure-g server-row">
+ <div class="pure-u-lg-1-2 pure-u-1 server-label">
+ <p>{'Index URL'|t}</p>
+ </div>
+ <div class="pure-u-lg-1-2 pure-u-1">
+ <p><a href="{$index_url}" title="{$pagetitle}">{$index_url}</a></p>
+ </div>
+ </div>
+ <div class="pure-g server-row">
+ <div class="pure-u-lg-1-2 pure-u-1 server-label">
+ <p>{'Base path'|t}</p>
+ </div>
+ <div class="pure-u-lg-1-2 pure-u-1">
+ <p>{$base_path}</p>
+ </div>
+ </div>
+ <div class="pure-g server-row">
+ <div class="pure-u-lg-1-2 pure-u-1 server-label">
+ <p>{'Client IP'|t}</p>
+ </div>
+ <div class="pure-u-lg-1-2 pure-u-1">
+ <p>{$client_ip}</p>
+ </div>
+ </div>
+ <div class="pure-g server-row">
+ <div class="pure-u-lg-1-2 pure-u-1 server-label">
+ <p>{'Trusted reverse proxies'|t}</p>
+ </div>
+ <div class="pure-u-lg-1-2 pure-u-1">
+ {if="count($trusted_proxies) > 0"}
+ <p>
+ {loop="$trusted_proxies"}
+ {$value}<br>
+ {/loop}
+ </p>
+ {else}
+ <p>{'N/A'|t}</p>
+ {/if}
+ </div>
+ </div>
+
+ {include="server.requirements"}
+
+ <h3 class="window-subtitle">Version</h3>
+
+ <div class="pure-g server-row">
+ <div class="pure-u-lg-1-2 pure-u-1 server-label">
+ <p>Current version</p>
+ </div>
+ <div class="pure-u-lg-1-2 pure-u-1">
+ <p>{$current_version}</p>
+ </div>
+ </div>
+
+ <div class="pure-g server-row">
+ <div class="pure-u-lg-1-2 pure-u-1 server-label">
+ <p>Latest release</p>
+ </div>
+ <div class="pure-u-lg-1-2 pure-u-1">
+ <p>
+ <a href="{$release_url}" title="{'Visit releases page on Github'|t}">
+ {$latest_version}
+ </a>
+ </p>
+ </div>
+ </div>
+
+ <h3 class="window-subtitle">Thumbnails</h3>
+
+ <div class="pure-g server-row">
+ <div class="pure-u-lg-1-2 pure-u-1 server-label">
+ <p>Thumbnails status</p>
+ </div>
+ <div class="pure-u-lg-1-2 pure-u-1">
+ <p>
+ {if="$thumbnails_mode==='all'"}
+ {'All'|t}
+ {elseif="$thumbnails_mode==='common'"}
+ {'Only common media hosts'|t}
+ {else}
+ {'None'|t}
+ {/if}
+ </p>
+ </div>
+ </div>
+
+ {if="$thumbnails_mode!=='none'"}
+ <div class="center tools-item">
+ <a href="{$base_path}/admin/thumbnails" title="{'Synchronize all link thumbnails'|t}">
+ <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Synchronize thumbnails'|t}</span>
+ </a>
+ </div>
+ {/if}
+
+ <h3 class="window-subtitle">Cache</h3>
+
+ <div class="center tools-item">
+ <a href="{$base_path}/admin/clear-cache?type=main">
+ <span class="pure-button pure-u-lg-2-3 pure-u-3-4">Clear main cache</span>
+ </a>
+ </div>
+
+ <div class="center tools-item">
+ <a href="{$base_path}/admin/clear-cache?type=thumbnails">
+ <span class="pure-button pure-u-lg-2-3 pure-u-3-4">Clear thumbnails cache</span>
+ </a>
+ </div>
+ </div>
+</div>
+
+{include="page.footer"}
+
+</body>
+</html>
--- /dev/null
+<div class="server-tables">
+ <h3 class="window-subtitle">{'Permissions'|t}</h3>
+
+ {if="count($permissions) > 0"}
+ <p class="center">
+ <i class="fa fa-close fa-color-red" aria-hidden="true"></i>
+ {'There are permissions that need to be fixed.'|t}
+ </p>
+
+ <p>
+ {loop="$permissions"}
+ <div class="center">{$value}</div>
+ {/loop}
+ </p>
+ {else}
+ <p class="center">
+ <i class="fa fa-check fa-color-green" aria-hidden="true"></i>
+ {'All read/write permissions are properly set.'|t}
+ </p>
+ {/if}
+
+ <h3 class="window-subtitle">PHP</h3>
+
+ <p class="center">
+ <strong>{'Running PHP'|t} {$php_version}</strong>
+ {if="$php_has_reached_eol"}
+ <i class="fa fa-circle fa-color-orange" aria-label="hidden"></i><br>
+ {'End of life: '|t} {$php_eol}
+ {else}
+ <i class="fa fa-circle fa-color-green" aria-label="hidden"></i><br>
+ {/if}
+ </p>
+
+ <table class="center">
+ <thead>
+ <tr>
+ <th>{'Extension'|t}</th>
+ <th>{'Usage'|t}</th>
+ <th>{'Status'|t}</th>
+ <th>{'Loaded'|t}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {loop="$php_extensions"}
+ <tr>
+ <td>{$value.name}</td>
+ <td>{$value.desc}</td>
+ <td>{$value.required ? t('Required') : t('Optional')}</td>
+ <td>
+ {if="$value.loaded"}
+ {$classLoaded="fa-color-green"}
+ {$strLoaded=t('Loaded')}
+ {else}
+ {$strLoaded=t('Not loaded')}
+ {if="$value.required"}
+ {$classLoaded="fa-color-red"}
+ {else}
+ {$classLoaded="fa-color-orange"}
+ {/if}
+ {/if}
+
+ <i class="fa fa-circle {$classLoaded}" aria-label="{$strLoaded}" title="{$strLoaded}"></i>
+ </td>
+ </tr>
+ {/loop}
+ </tbody>
+ </table>
+</div>
<span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Plugin administration'|t}</span>
</a>
</div>
+ <div class="tools-item">
+ <a href="{$base_path}/admin/server"
+ title="{'Check instance\'s server configuration'|t}">
+ <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Server administration'|t}</span>
+ </a>
+ </div>
{if="!$openshaarli"}
<div class="tools-item">
<a href="{$base_path}/admin/password" title="{'Change your password'|t}">
</a>
</div>
- {if="$thumbnails_enabled"}
- <div class="tools-item">
- <a href="{$base_path}/admin/thumbnails" title="{'Synchronize all link thumbnails'|t}">
- <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Synchronize thumbnails'|t}</span>
- </a>
- </div>
- {/if}
-
{loop="$tools_plugin"}
<div class="tools-item">
{$value}
{
mode: 'production',
entry: {
+ shaare_batch: './assets/common/js/shaare-batch.js',
thumbnails: './assets/common/js/thumbnails.js',
thumbnails_update: './assets/common/js/thumbnails-update.js',
metadata: './assets/common/js/metadata.js',